aboutsummaryrefslogtreecommitdiff
path: root/2026/2026-05-17-gribouille
diff options
context:
space:
mode:
Diffstat (limited to '2026/2026-05-17-gribouille')
-rw-r--r--2026/2026-05-17-gribouille/LICENSE21
-rw-r--r--2026/2026-05-17-gribouille/README.md69
-rw-r--r--2026/2026-05-17-gribouille/drop.typ48
-rw-r--r--2026/2026-05-17-gribouille/gribouille-skill/SKILL.md442
-rw-r--r--2026/2026-05-17-gribouille/gribouille-skill/references/geom-table.md139
-rw-r--r--2026/2026-05-17-gribouille/gribouille-skill/references/scale-table.md217
-rw-r--r--2026/2026-05-17-gribouille/src_ip_last1h.csv944
7 files changed, 1880 insertions, 0 deletions
diff --git a/2026/2026-05-17-gribouille/LICENSE b/2026/2026-05-17-gribouille/LICENSE
new file mode 100644
index 0000000..b94b223
--- /dev/null
+++ b/2026/2026-05-17-gribouille/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 boB Rudis
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/2026/2026-05-17-gribouille/README.md b/2026/2026-05-17-gribouille/README.md
new file mode 100644
index 0000000..7d2c829
--- /dev/null
+++ b/2026/2026-05-17-gribouille/README.md
@@ -0,0 +1,69 @@
+# gribouille-skill
+
+A Claude Code / OpenCode skill for generating [gribouille](https://m.canouil.dev/gribouille) charts in [Typst](https://typst.app), plus a working example.
+
+## What's here
+
+| File | Purpose |
+|------|---------|
+| `drop.typ` | Standalone example — a scatter plot of sensor fleet activity (log-log scales, annotations, size encoding) |
+| `gribouille-skill/SKILL.md` | The agent skill definition with idiomatic gribouille patterns, anti-patterns, and a quick-reference |
+| `gribouille-skill/references/geom-table.md` | Full parameter reference for geoms |
+| `gribouille-skill/references/scale-table.md` | Full parameter reference for scales |
+
+## Using the skill
+
+The skill implements Wilkinson's Grammar of Graphics (same mental model as ggplot2 / plotnine) compiled natively in Typst.
+
+### Quick start
+
+1. Install gribouille from Typst Universe or clone it locally.
+2. Clone `https://github.com/mcanouil/gribouille` into the directory where your doc is (until it's in the Universe)
+3. Import the library:
+ ```typst
+ #import "./gribouille/lib.typ":*
+ ```
+4. Build a plot:
+ ```typst
+ #plot(
+ data: penguins,
+ mapping: aes(x: "flipper-len", y: "body-mass", colour: "species"),
+ layers: (geom-point(size: 2pt),),
+ theme: theme-minimal(),
+ width: 12cm, height: 9cm,
+ )
+ ```
+
+### Supported chart types
+
+Scatter, line, area, bar, histogram, boxplot, heatmap, faceted small-multiples, multi-panel compositions, and more. See `gribouille-skill/SKILL.md` for the full chart-type decision table.
+
+### Key conventions
+
+- Column names in `aes()` must be **quoted strings**: `aes(x: "flipper-len")`.
+- Always include `width:` and `height:` — gribouille has no default size.
+- Use `as-factor("col")` for numeric-looking categorical strings.
+- Use `typst("markup")` inside `labs()` when you need rich text.
+
+## Example: `drop.typ`
+
+`drop.typ` reads `src_ip_last1h.csv` and renders a log-log scatter of source-IP activity:
+
+- **x**: total sessions (`scale-x-log10`)
+- **y**: unique destination ports (`scale-y-log10`)
+- **size**: number of unique sensors (`scale-size-area`)
+- **annotation**: callout label for the dominant IP
+
+Compile with:
+```bash
+typst compile drop.typ
+```
+
+## Resources
+
+- [gribouille docs](https://m.canouil.dev/gribouille)
+- [Typst docs](https://typst.app/docs)
+
+## License
+
+MIT
diff --git a/2026/2026-05-17-gribouille/drop.typ b/2026/2026-05-17-gribouille/drop.typ
new file mode 100644
index 0000000..b830494
--- /dev/null
+++ b/2026/2026-05-17-gribouille/drop.typ
@@ -0,0 +1,48 @@
+#import "./gribouille/lib.typ":*
+
+#set text(font: "Goldman Sans")
+
+#let results = csv("src_ip_last1h.csv", row-type: dictionary)
+
+#let p = plot(
+ data: results,
+ mapping: aes(
+ x: "total_sessions",
+ y: "unique_dst_ports",
+ size: "unique_sensors",
+ ),
+ layers: (
+ geom-point(alpha: 0.65),
+ annotate(
+ "text",
+ x: 23424,
+ y: 3,
+ label: typst(align(right)[`93.123.72.166` dominates with\ ~77% of non-spoofable traffic.]),
+ anchor: "south-east",
+ dx: 0.0,
+ dy: 0.3,
+ size: 7pt,
+ ),
+ ),
+ scales: (
+ scale-x-log10(labels: format-comma()),
+ scale-y-log10(),
+ scale-size-area(),
+ ),
+ labs: labs(
+ title: typst([#h(1.5cm)Sensor Fleet Activity (1 Hour)]),
+ subtitle: typst(pad(left: 1.5cm)[943 source IPs across 30,460 sessions in the last hour.]),
+ x: "Total Sessions (log₁₀)",
+ y: "Unique Destination Ports",
+ size: "# Unique Sensors",
+ ),
+ theme: theme-minimal(
+ text: element-text(),
+ plot-title: element-text(size: 14pt, weight: "bold"),
+ plot-subtitle: element-text(margin: margin(left:3in)),
+ ),
+ width: 14cm,
+ height: 9cm,
+ )
+
+ #p
diff --git a/2026/2026-05-17-gribouille/gribouille-skill/SKILL.md b/2026/2026-05-17-gribouille/gribouille-skill/SKILL.md
new file mode 100644
index 0000000..9da974d
--- /dev/null
+++ b/2026/2026-05-17-gribouille/gribouille-skill/SKILL.md
@@ -0,0 +1,442 @@
+---
+name: gribouille
+description: >
+ Generate correct, idiomatic gribouille charts for Typst. Trigger on: "chart",
+ "plot", "scatter", "histogram", "bar chart", "boxplot", "heatmap", "time series",
+ or any request to visualise data in Typst using a Grammar of Graphics approach.
+version: 0.2.0
+min-binary-version: "4.0.0"
+allowed-tools:
+ - Read
+ - Write
+ - Edit
+ - Bash
+ - AskUserQuestion
+---
+
+# /gribouille
+
+Generate correct, idiomatic [gribouille](https://m.canouil.dev/gribouille) charts for Typst.
+Gribouille implements Wilkinson's Grammar of Graphics — the same mental model as ggplot2 and plotnine — compiled natively in Typst.
+
+```
+/gribouille scatter penguins flipper-len vs body-mass by species
+/gribouille bar chart of mpg highway economy by class, faceted by cyl
+/gribouille histogram of my-data.csv column "score"
+/gribouille compose two panels: weight vs mpg and horsepower vs mpg
+```
+
+---
+
+## Step 0 · Context detection
+
+Determine two things silently before asking anything.
+
+**A. Import mode:**
+
+| Signal | Import line |
+|---|---|
+| User mentions Typst Universe / `@preview` | `#import "@preview/gribouille:0.0.1": *` |
+| User has a local gribouille clone / `lib.typ` path | `#import "./path/to/lib.typ": *` |
+| Ambiguous | Ask: "Are you using gribouille from Typst Universe (`@preview`) or a local clone?" |
+
+**B. Embedding context:**
+
+| Signal | Action |
+|---|---|
+| User is inside a typst-author document | Generate only the `#plot(...)` block; add Step 9 handoff note |
+| User wants a standalone `.typ` file | Prepend `#set page(width: auto, height: auto, margin: 0.5cm)` |
+| Unclear | Default to standalone |
+
+---
+
+## Step 1 · Intent extraction (silent checklist)
+
+Before writing code, confirm these four dimensions. Ask only if 2+ are missing — one compact question at most, never a four-item form.
+
+| Dimension | What to extract | Example |
+|---|---|---|
+| **Chart type** | What visual form? | scatter, bar, histogram, boxplot, line, heatmap |
+| **Data** | Inline / built-in / external file? | `penguins`, a CSV path, or an array literal |
+| **Variables** | Which columns → which aesthetics? | x=flipper-len, y=body-mass, colour=species |
+| **Facets** | Split into panels? By what variable? | one panel per island |
+
+If chart type and data are both clear from context, skip the question entirely.
+
+---
+
+## Step 2 · Select chart type
+
+First-match table. Apply the first row that fits the user's description.
+
+| User says / data shape | Geom combination | Notes |
+|---|---|---|
+| "scatter", "relationship", two continuous vars | `geom-point(size: 2pt)` | Add `geom-smooth(method: "lm")` if trend desired |
+| "scatter + fit / regression / smoother" | `geom-point(size: 2pt, alpha: 0.65)` + `geom-smooth(method: "lm", se: true, alpha: 0.2)` | v1 supports `method: "lm"` only — not loess or glm |
+| "scatter + convex hull / group outlines" | above + `geom-mark(method: "hull", expand: 5pt, alpha: 0.25)` | |
+| "scatter + error bars" | `geom-point()` + `geom-errorbar(width: 0.3)` with `ymin`/`ymax` in mapping | |
+| "line chart / time series / trend over x" | `geom-line()` | Sort data by x first; use `geom-area()` if area-under-line matters |
+| "area chart" | `geom-area(alpha: 0.7)` | Requires ordered x |
+| "bar chart (counts)" | `geom-bar()` | Uses stat-count by default; map `x` only |
+| "bar chart (values / col chart)" | `geom-col()` | Data already has y values; map both `x` and `y` |
+| "stacked bar" | `geom-bar(position: "stack")` | Add `fill` aesthetic |
+| "dodged / grouped bar" | `geom-bar(position: "dodge")` | Add `fill` aesthetic |
+| "filled / proportional bar (100% stacked)" | `geom-bar(position: "fill")` | y becomes proportion 0–1 |
+| "histogram / distribution of continuous var" | `geom-histogram(bins: 12)` | Tune `bins` or use `binwidth` instead |
+| "overlapping distributions / frequency polygon" | `geom-freqpoly(bins: 12)` | Line version of histogram; good for comparing groups |
+| "boxplot / box-and-whisker" | `geom-boxplot()` | Add `geom-jitter(width: 0.2, alpha: 0.3)` for raw data overlay |
+| "heatmap / tile map" | `geom-tile()` | Requires x, y, fill aesthetics |
+| "2D histogram / bin2d" | `geom-bin-2d()` | Bins both x and y |
+| "hex bin (dense scatter)" | `geom-hex()` | Alternative to scatter for large datasets |
+| "contour lines" | `geom-contour()` | Requires x, y, z |
+| "filled contours" | `geom-contour-filled()` | Requires x, y, z, fill |
+| "error bars" | `geom-errorbar(width: 0.3)` with `ymin`/`ymax` in mapping | |
+| "horizontal error bars" | `geom-errorbarh(height: 0.3)` with `xmin`/`xmax` in mapping | |
+| "ribbon / confidence band" | `geom-ribbon(alpha: 0.2)` with `ymin`/`ymax` in mapping | Pair with `geom-line()` |
+| "rug plot (marginal ticks)" | `geom-rug(sides: "bl")` | Stack with another geom |
+| "path (connected in data order)" | `geom-path()` | Unlike `geom-line()`, does not sort by x |
+| "step function" | `geom-step(direction: "hv")` | Options: "hv", "vh", "mid" |
+| "segments / connectors" | `geom-segment(stroke: 1pt)` | `geom-curve()` for curved connectors |
+| "text labels on data points" | `geom-text(mapping: aes(label: "col"))` | Combine with `geom-point()` |
+| "labelled boxes (callouts)" | `geom-label(mapping: aes(label: "col"))` | Adds background box around text |
+| "dot plot" | `geom-dotplot()` | |
+| "Q-Q plot" | `geom-qq()` + `geom-qq-line()` | |
+| "quantile regression" | `geom-quantile()` | |
+| "spoke / wind vectors" | `geom-spoke()` | Requires x, y, angle, radius in mapping |
+| "count overplotting" | `geom-count()` | Size encodes count |
+| "function curve y=f(x)" | `geom-function(fun: x => calc.sin(x), n: 101)` | |
+| "horizontal reference line" | `geom-hline(yintercept: 0)` | |
+| "vertical reference line" | `geom-vline(xintercept: 0)` | |
+| "diagonal / slope reference" | `geom-abline(slope: 1, intercept: 0)` | |
+| "shaded rectangle region" | `geom-rect()` with `xmin`/`xmax`/`ymin`/`ymax` in mapping | |
+| "polygon" | `geom-polygon()` | |
+| "confidence ellipse" | `geom-ellipse()` | Or use `geom-mark(method: "ellipse")` |
+| "faceted" | Any geom above + `facet: facet-wrap("col")` or `facet: facet-grid(rows: "r", columns: "c")` | |
+
+Read `references/geom-table.md` for full parameter reference on any geom.
+
+---
+
+## Step 3 · Wire the data
+
+Choose the pattern that matches the data source.
+
+**Pattern A — Built-in dataset (no extra code needed):**
+```typst
+// penguins, mpg, and economics are exported by gribouille
+#plot(
+ data: penguins, // or: mpg, economics
+ ...
+)
+```
+
+**Pattern B — Inline array literal:**
+```typst
+#let my-data = (
+ (x: 1.0, y: 2.1, group: "a"),
+ (x: 2.0, y: 3.0, group: "a"),
+ (x: 3.5, y: 1.8, group: "b"),
+)
+#plot(
+ data: my-data,
+ ...
+)
+```
+
+**Pattern C — External CSV (column headers become dictionary keys):**
+```typst
+#let raw = csv("data.csv", row-type: dictionary)
+// Numeric values arrive as strings from csv(); cast as needed:
+#let my-data = raw.map(r => (
+ x: float(r.x),
+ y: float(r.y),
+ group: r.group,
+))
+#plot(
+ data: my-data,
+ ...
+)
+```
+
+**Key rule — always quote column names in `aes()`:**
+```typst
+// CORRECT
+mapping: aes(x: "flipper-len", y: "body-mass")
+
+// WRONG — causes a compile error
+mapping: aes(x: flipper-len, y: body-mass)
+```
+
+---
+
+## Step 4 · Build the plot block
+
+Canonical skeleton. Include only the arguments that are actually needed — omit optional sections when defaults suffice.
+
+```typst
+#import "@preview/gribouille:0.0.1": *
+
+#set page(width: auto, height: auto, margin: 0.5cm) // standalone only; remove in typst-author docs
+
+#plot(
+ data: <data>,
+ mapping: aes(
+ x: "<x-col>",
+ y: "<y-col>",
+ // add aesthetics as needed:
+ colour: "<group-col>",
+ fill: "<group-col>",
+ shape: "<group-col>",
+ size: "<numeric-col>",
+ alpha: "<numeric-col>",
+ ),
+ layers: (
+ geom-point(size: 2pt, alpha: 0.85),
+ // stack additional geoms here in draw order (bottom to top)
+ ),
+ // scales: only include when overriding defaults
+ scales: (
+ scale-y-continuous(labels: format-comma()), // for large numbers (>9999)
+ // scale-x-log10(),
+ // scale-colour-viridis-d(),
+ ),
+ labs: labs(
+ title: "Chart Title",
+ subtitle: "Optional subtitle",
+ x: "X-Axis Label",
+ y: "Y-Axis Label",
+ colour: "Legend Title",
+ fill: "Legend Title",
+ caption: "Data source.",
+ ),
+ theme: theme-minimal(), // alternatives: theme-classic(), theme-void()
+ width: 12cm,
+ height: 9cm,
+)
+```
+
+**Scale auto-selection — apply these rules silently before emitting code:**
+
+| Aesthetic + data type | Scale to emit |
+|---|---|
+| x/y continuous, values ≤9999 | Omit — default is fine |
+| x/y continuous, values >9999 | `scale-x/y-continuous(labels: format-comma())` |
+| x/y log scale requested | `scale-x/y-log10()` |
+| x/y date values | `scale-x/y-date(date-format: "[year]-[month repr:numerical]")` |
+| colour/fill discrete (categorical) | Omit — gribouille auto-trains; add `scale-colour-discrete(palette: ...)` only for custom colours |
+| colour/fill continuous | `scale-colour-continuous()` or `scale-colour-gradient(low: rgb(...), high: rgb(...))` |
+| colour viridis | `scale-colour-viridis-c()` (continuous) or `scale-colour-viridis-d()` (discrete) |
+| colour ColorBrewer | `scale-colour-brewer(palette: "Blues")` |
+| colour colourblind-safe | `scale-colour-okabe-ito()` |
+| alpha mapped to data | `scale-alpha-continuous(range: (0.1, 1))` |
+
+Read `references/scale-table.md` for full parameter reference.
+
+**Categorical column rule — use `as-factor()` when a column holds numeric-looking strings used for grouping:**
+```typst
+// cyl column contains "4", "6", "8" as strings
+mapping: aes(x: "displ", y: "hwy", colour: as-factor("cyl"))
+// ↑ forces discrete treatment
+```
+
+**Typst markup in labs — use the `typst()` helper, not raw strings:**
+```typst
+labs(
+ title: typst("*Bold title* and #text(fill: blue)[blue text]"),
+)
+```
+
+**Theme customisation — verified parameter names from source:**
+```typst
+theme: theme-minimal(
+ text: element-text(family: "Source Sans Pro", size: 10pt), // family:, NOT font:
+ plot-title: element-text(size: 14pt, weight: "bold"),
+ axis-text: element-text(size: 8pt),
+ axis-line: element-line(thickness: 0.5pt), // thickness:, NOT linewidth:
+ tick-length: 0.08cm,
+ tick-labels: true,
+)
+```
+
+`element-text()` accepts: `size`, `weight`, `colour`, `angle`, `family`, `margin`
+`element-line()` accepts: `colour`, `thickness`
+`element-rect()` accepts: `fill`, `stroke`
+
+---
+
+## Step 5 · Faceting
+
+Add a `facet:` argument to split the chart into small multiples.
+
+**One variable → `facet-wrap`:**
+```typst
+facet: facet-wrap("island", ncolumn: 3),
+// Options: nrow:, ncolumn:, scales: "fixed"|"free"|"free_x"|"free_y"
+// labeller: label-value() (default) | label-both()
+// axes: "margins" (default) | "all_x" | "all_y" | "all"
+```
+
+**Two variables → `facet-grid`:**
+```typst
+facet: facet-grid(rows: "sex", columns: "island"),
+// v1 supports scales: "fixed" only
+// Either rows: or columns: may be omitted (but not both)
+```
+
+---
+
+## Step 6 · Multi-panel composition
+
+Use `compose()` when the user wants multiple **independent** plots arranged together. Use `facet-wrap/grid` when it's the same chart split by a grouping variable.
+
+```typst
+#let p1 = plot(
+ data: d,
+ mapping: aes(x: "wt", y: "mpg", colour: as-factor("cyl")),
+ layers: (geom-point(size: 2pt),),
+ width: 6cm, height: 4cm,
+ defer: true, // REQUIRED for compose()
+)
+#let p2 = plot(
+ data: d,
+ mapping: aes(x: "hp", y: "mpg", colour: as-factor("cyl")),
+ layers: (geom-point(size: 2pt),),
+ width: 6cm, height: 4cm,
+ defer: true,
+)
+
+#compose(
+ p1, p2,
+ layout: "grid", // "grid" or "stack"
+ columns: 2,
+ collect: auto, // auto hoists shared legends; none = keep per-panel
+ guides-placement: "right", // "right" | "left" | "top" | "bottom"
+)
+```
+
+---
+
+## Step 7 · Guides / legend control
+
+```typst
+guides: guides(
+ colour: guide-legend(position: "bottom", direction: "horizontal"),
+ fill: guide-none(),
+ shape: guide-legend(nrow: 1),
+),
+```
+
+`guide-legend()` params: `title, nrow, ncolumn, reverse, position, direction, order, byrow`.
+`position` accepts: `"top"`, `"right"`, `"bottom"`, `"left"`, `"none"`, a Typst alignment like `top + right`, or a dict `(dx:, dy:)`.
+
+---
+
+## Step 8 · Accessibility
+
+Always emit an alt text string. Cache the plot in a `let` binding and call `get-alt-text()`:
+
+```typst
+#let p = plot(
+ data: penguins,
+ mapping: aes(x: "flipper-len", y: "body-mass", colour: "species"),
+ layers: (geom-point(size: 2pt),),
+ labs: labs(title: "Penguin Scatter", x: "Flipper Length (mm)", y: "Body Mass (g)"),
+ theme: theme-minimal(),
+ width: 12cm, height: 9cm,
+)
+
+#figure(
+ p,
+ caption: [Flipper length versus body mass for three penguin species.],
+ alt: get-alt-text(p),
+)
+```
+
+Tell the user to review and refine the generated alt text for their specific audience.
+
+---
+
+## Step 9 · Typst-author handoff
+
+When the chart is destined for a full typst-author document (not a standalone file):
+
+1. Generate the `#plot(...)` block without `#set page(...)`.
+2. Tell the user:
+
+> Paste the `#plot(...)` block inside a `#figure()` in your document:
+>
+> ```typst
+> #let p = plot(
+> // ... your plot arguments ...
+> width: 12cm, height: 9cm,
+> )
+>
+> #figure(
+> p,
+> caption: [Your caption here.],
+> alt: get-alt-text(p),
+> )
+> ```
+>
+> Remove `#set page(...)` if you added it during standalone testing — the enclosing typst-author document controls page geometry.
+
+---
+
+## Step 10 · Anti-patterns
+
+Never emit these. Each has a correct alternative.
+
+| Wrong | Correct | Why |
+|---|---|---|
+| `aes(x: col-name)` | `aes(x: "col-name")` | Column names must be quoted strings; unquoted is a Typst identifier |
+| `aes(colour: "cyl")` when cyl holds `"4"/"6"/"8"` | `aes(colour: as-factor("cyl"))` | Numeric-looking strings need `as-factor()` for correct discrete training |
+| `element-text(font: "MyFont")` | `element-text(family: "MyFont")` | The param is `family:`, not `font:` |
+| `element-line(linewidth: 0.5pt)` | `element-line(thickness: 0.5pt)` | The param is `thickness:`, not `linewidth:` |
+| `scale-y-continuous()` when y values are small | Omit the scale | Unnecessary boilerplate; defaults are fine |
+| `#grid(columns: 2)[#p1][#p2]` for multi-panel | `#compose(p1, p2, layout: "grid", columns: 2)` | Typst `grid()` bypasses gribouille's legend hoisting |
+| `labs(title: "*Bold*")` | `labs(title: typst("*Bold*"))` | Raw strings in labs are not parsed as Typst markup |
+| `plot(..., data: csv("f.csv"))` | `#let d = csv("f.csv", row-type: dictionary)` then `plot(data: d, ...)` | `csv()` must use `row-type: dictionary` and be called outside `plot()` |
+| `geom-histogram()` without an `x` mapping | `mapping: aes(x: "col")` always | Histogram requires a single `x` aesthetic |
+| Omitting `width:` and `height:` | Always include `width: 12cm, height: 9cm` | Gribouille requires explicit dimensions; there is no default |
+| `geom-smooth(method: "loess")` | `geom-smooth(method: "lm")` | v1 only supports `method: "lm"` — loess and glm are not available |
+| `plot(..., defer: false)` inside `compose()` | `plot(..., defer: true)` | `compose()` only works with deferred specs |
+| `scale-size-radius()` | `scale-radius()` | The export is `scale-radius`, not `scale-size-radius` |
+| `scale-size-area(max.size: 6pt)` | `scale-size-area(range: (1pt, 12pt))` | The param is `range:`, not `max.size:` |
+| `n.breaks:` in any scale | `n-breaks:` | Gribouille uses hyphens (Typst convention), not dots |
+
+---
+
+## Quick reference
+
+**Three built-in datasets:**
+
+| Symbol | Description | Key columns |
+|---|---|---|
+| `penguins` | Palmer Archipelago penguins | `flipper-len`, `body-mass`, `species`, `island`, `sex` |
+| `mpg` | Fuel economy (ggplot2 mpg) | `displ`, `hwy`, `cty`, `class`, `cyl`, `drv` |
+| `economics` | US economic time series | `date`, `unemploy`, `pop`, `psavert`, `uempmed` |
+
+**Utility functions:**
+
+| Function | Purpose |
+|---|---|
+| `as-factor("col")` | Force column to discrete treatment |
+| `as-numeric("col")` | Force column to continuous |
+| `typst("markup")` | Typst markup in labs fields |
+| `after-scale(fn)` | Late-binding: evaluated after scale training |
+| `after-stat(fn)` | Late-binding: evaluated after stat transform |
+| `get-alt-text(p)` | Generate alt text from a plot result |
+| `format-comma()` | Number formatter: 1,234,567 |
+| `format-percent()` | Number formatter: 12.3% |
+| `format-scientific()` | Number formatter: 1.23×10⁴ |
+| `format-currency()` | Number formatter: $1,234 |
+| `format-number(digits: N)` | Fixed decimal places |
+| `format-wrap(width: N)` | Word-wrap labels at N characters |
+
+**Three theme presets:**
+
+| Theme | Character |
+|---|---|
+| `theme-minimal()` | No background panel, minimal chrome — default |
+| `theme-classic()` | Classic R style with axis lines |
+| `theme-void()` | No axes, no grid, just data |
diff --git a/2026/2026-05-17-gribouille/gribouille-skill/references/geom-table.md b/2026/2026-05-17-gribouille/gribouille-skill/references/geom-table.md
new file mode 100644
index 0000000..5f2721e
--- /dev/null
+++ b/2026/2026-05-17-gribouille/gribouille-skill/references/geom-table.md
@@ -0,0 +1,139 @@
+# Gribouille Geom Reference
+
+Full parameter reference for every geom exported in `lib.typ`. All parameters verified against source code.
+
+Columns: **Geom** | **Primary aes channels** | **Key params** | **Best used when**
+
+---
+
+## Point / scatter family
+
+| Geom | Primary aes | Key params | Best used when |
+|---|---|---|---|
+| `geom-point` | x, y, colour, fill, shape, size, alpha | `size` (default auto), `stroke` (outline), `alpha`, `shape` | Scatter plots; discrete x vs continuous y |
+| `geom-jitter` | x, y, colour, fill, shape, size, alpha | `size`, `stroke: 0.5pt`, `fill`, `colour`, `alpha`, `shape`; position defaults to `"jitter"` | Overplotted categoricals; combine with `geom-boxplot` |
+| `geom-count` | x, y, colour, fill, shape | `size: 3pt`, `stroke: none`, `fill`, `colour`, `alpha`, `shape` | Overplotted integer grids; size encodes count |
+| `geom-dotplot` | x, fill | `bins: 30`, `binwidth`, `dotsize: 1.0`, `stackratio: 1.0`, `fill`, `colour`, `stroke`, `alpha` | Dot histogram; alternative to `geom-histogram` |
+| `geom-rug` | x, y, colour | `sides: "bl"` (b=bottom, l=left, t=top, r=right), `length: 0.15cm`, `stroke: 0.4pt`, `colour`, `alpha` | Marginal data density; stack with another geom |
+
+## Line / path family
+
+| Geom | Primary aes | Key params | Best used when |
+|---|---|---|---|
+| `geom-line` | x, y, colour, linetype, alpha | `stroke: 0.8pt`, `colour`, `alpha`, `linetype` | Time series; connected data sorted by x |
+| `geom-path` | x, y, colour, linetype | `stroke: 0.8pt`, `colour`, `alpha`, `linetype` | Lines connected in data-row order (not sorted by x) |
+| `geom-step` | x, y, colour, linetype | `direction: "hv"\|"vh"\|"mid"`, `stroke: 0.8pt`, `colour`, `alpha`, `linetype` | Step functions; ECDF; survival curves |
+| `geom-area` | x, y, fill, alpha | `colour`, `fill`, `stroke: none`, `alpha` | Area under a line; stacked areas |
+| `geom-ribbon` | x, ymin, ymax, fill | `colour`, `fill`, `stroke: none`, `alpha` | Confidence bands; prediction intervals; pair with `geom-line` |
+| `geom-freqpoly` | x, colour, linetype | `bins: 30`, `binwidth`, `stroke: 0.8pt`, `colour`, `alpha`, `linetype` | Overlapping distributions as lines; alternative to `geom-histogram` |
+
+## Bar / column family
+
+| Geom | Primary aes | Key params | Best used when |
+|---|---|---|---|
+| `geom-bar` | x, fill, colour, alpha | `width: 0.9`, `colour`, `fill`, `stroke: none`, `alpha`, `position: "stack"` | Count bars; map `x` only, gribouille counts automatically |
+| `geom-col` | x, y, fill, colour, alpha | `width: 0.9`, `colour`, `fill`, `stroke: none`, `alpha`, `position: "identity"` | Pre-aggregated bar values; map both `x` and `y` |
+| `geom-histogram` | x, fill, colour, alpha | `bins: 30`, `binwidth`, `width: 1.0`, `colour`, `fill`, `stroke: none`, `alpha`, `position: "stack"` | Continuous variable distribution; requires `x` mapping |
+
+## Distribution summary family
+
+| Geom | Primary aes | Key params | Best used when |
+|---|---|---|---|
+| `geom-boxplot` | x, y (or y alone), fill, colour | `width: 0.6`, `colour`, `fill`, `stroke: 0.6pt`, `alpha`, `outlier-size: 1.8pt`, `outlier-colour: auto`, `whisker-cap: 0.5` | Distribution summary by group |
+| `geom-errorbar` | x, ymin, ymax, colour | `width: 0.4` (cap span; number=data units, length=panel units), `stroke: 0.8pt`, `colour`, `alpha`, `linetype: "solid"` | Vertical error bars |
+| `geom-errorbarh` | y, xmin, xmax, colour | `height: 0.4` (cap span), `stroke: 0.8pt`, `colour`, `alpha`, `linetype: "solid"` | Horizontal error bars |
+| `geom-linerange` | x, ymin, ymax, colour | `stroke: 0.8pt`, `colour`, `alpha`, `linetype: "solid"` | Vertical range lines without caps |
+| `geom-crossbar` | x, y, ymin, ymax, fill, colour | `width: 0.6`, `colour`, `fill`, `stroke: 0.6pt`, `middle-stroke: 1.2pt`, `alpha` | Box without whiskers or outliers |
+| `geom-pointrange` | x, y, ymin, ymax, colour | `size: 2.5pt`, `stroke: 0.8pt`, `colour`, `fill`, `alpha`, `linetype: "solid"` | Point with range line |
+| `geom-smooth` | x, y, colour, fill | `method: "lm"` (only option in v1), `se: true` (show ribbon), `alpha: auto` (ribbon alpha), `level: 0.95` (CI level), `stroke: 1pt`, `colour`, `fill`, `linetype` | Fitted trend + optional confidence ribbon |
+| `geom-quantile` | x, y, colour, linetype | `quantiles: (0.25, 0.5, 0.75)`, `n-samples: 64`, `stroke: 0.6pt`, `colour`, `alpha`, `linetype`, `linewidth` | Quantile regression lines |
+
+## Annotation family
+
+| Geom | Primary aes | Key params | Best used when |
+|---|---|---|---|
+| `geom-text` | x, y, label, colour, alpha | `size: 8pt`, `colour`, `alpha`, `anchor: "center"` (CeTZ anchor), `dx: 0`, `dy: 0` | Data labels at point positions |
+| `geom-label` | x, y, label, colour, fill | `size: 8pt`, `colour`, `fill`, `stroke: 0.4pt`, `alpha`, `inset: 2pt`, `radius: 1pt`, `anchor: "center"`, `dx: 0`, `dy: 0` | Text with background box; callout labels |
+| `geom-typst` | x, y, label (Typst content) | `size: 10pt`, `colour`, `alpha`, `anchor: "center"`, `dx: 0`, `dy: 0`, `label: none` | Arbitrary Typst content at data coordinates |
+| `geom-hline` | yintercept | `yintercept` (scalar or array), `colour`, `stroke: 0.6pt`, `alpha`, `linetype: "solid"` | Horizontal reference line; does NOT inherit plot mapping |
+| `geom-vline` | xintercept | `xintercept` (scalar or array), `colour`, `stroke: 0.6pt`, `alpha`, `linetype: "solid"` | Vertical reference line; does NOT inherit plot mapping |
+| `geom-abline` | slope, intercept | `slope: 1`, `intercept: 0`, `colour`, `stroke: 0.6pt`, `alpha`, `linetype: "solid"` | Diagonal reference (y = a + bx); does NOT inherit plot mapping |
+| `geom-segment` | x, y, xend, yend, colour | `stroke: 0.8pt`, `colour`, `alpha`, `linetype: "solid"` | Line segments between two data points |
+| `geom-curve` | x, y, xend, yend, colour | `curvature: 0.5`, `angle: 90deg`, `n: 32`, `stroke: 0.8pt`, `colour`, `alpha`, `linetype: "solid"` | Curved connectors |
+| `geom-rect` | xmin, xmax, ymin, ymax, fill, colour | `colour`, `fill`, `stroke: none`, `alpha` | Shaded rectangular regions; highlight bands |
+| `geom-polygon` | x, y, fill, colour, group | `colour`, `fill`, `stroke: none`, `alpha` | Arbitrary filled polygons; map outlines |
+| `geom-blank` | — | `mapping: none`, `data: none` | Reserve plot area without drawing; useful for setting axis limits |
+
+## Group / density family
+
+| Geom | Primary aes | Key params | Best used when |
+|---|---|---|---|
+| `geom-mark` | colour, fill | `method: "rect"\|"circle"\|"ellipse"\|"hull"`, `expand: 0pt` (padding), `n: 64` (ellipse smoothness), `colour`, `fill`, `stroke: 0.5pt`, `alpha` | Group outlines; convex hull or enclosing shape per group |
+| `geom-ellipse` | x, y, colour, fill | `a: 1`, `b: 1` (semi-axes), `angle: 0`, `n: 64` (polygon segments), `colour`, `fill`, `stroke: none`, `alpha` | Manually sized ellipses at group centroids |
+
+## 2D density / grid family
+
+| Geom | Primary aes | Key params | Best used when |
+|---|---|---|---|
+| `geom-tile` | x, y, fill, colour | `width: 1`, `height: 1`, `colour`, `fill`, `stroke: none`, `alpha` | Heatmaps; requires pre-computed fill values |
+| `geom-bin-2d` | x, y, fill | `bins: 30`, `binwidth`, `colour`, `fill`, `stroke: none`, `alpha` | 2D histogram; bins both x and y |
+| `geom-hex` | x, y, fill | `bins: 30`, `binwidth`, `colour`, `fill`, `stroke: none`, `alpha` | Hexagonal binning; alternative to scatter for large N |
+| `geom-contour` | x, y, z, colour, linetype | (no user params beyond mapping) | Topographic contour lines on a regular grid |
+| `geom-contour-filled` | x, y, z, fill | (no user params beyond mapping) | Filled contour regions |
+
+## Specialised family
+
+| Geom | Primary aes | Key params | Best used when |
+|---|---|---|---|
+| `geom-spoke` | x, y, angle, radius | Fixed params: `angle: 0deg`, `radius: 1`, `stroke: 0.8pt`, `colour`, `alpha`, `linetype: "solid"` | Wind rose / directional vector field |
+| `geom-qq` | sample | `size`, `stroke: none`, `fill`, `colour`, `alpha`, `shape`, `distribution: "normal"` | Q-Q scatter plot |
+| `geom-qq-line` | sample | `stroke: 0.8pt`, `colour`, `alpha`, `linetype`, `distribution: "normal"` | Reference line for `geom-qq` |
+| `geom-function` | — | `fun` (callable `x => y`), `n: 101`, `xlim: none` (overrides x-domain), `stroke: 0.8pt`, `colour`, `alpha`, `linetype: "solid"`; does NOT inherit aes | Draw y = f(x) curve; no data needed |
+
+---
+
+## Stat helpers (used inside geoms via `stat:` param)
+
+Note: most geoms accept `stat: "identity"` (default) or a stat object. The common pattern is to use the geom's default stat.
+
+| Stat | Created by | Key params | Notes |
+|---|---|---|---|
+| `stat-bin(...)` | `geom-histogram` default | `bins: 30`, `binwidth: none` | Bin continuous x into counts |
+| `stat-count` | `geom-bar` default | — | Count rows per x level |
+| `stat-boxplot` | `geom-boxplot` default | — | Five-number summary per group |
+| `stat-smooth` | `geom-smooth` default | `method: "lm"`, `se: true`, `level: 0.95` | Fit linear model |
+| `stat-sum` | `geom-count` default | — | Count overlapping points |
+
+---
+
+## Position adjustments
+
+Pass these as the `position:` argument in geoms. String shortcuts (`"stack"`, `"dodge"`, `"fill"`, `"jitter"`, `"identity"`) also work.
+
+| Position | Key params | Notes |
+|---|---|---|
+| `position-stack()` | — | Stacked bars/areas |
+| `position-fill()` | — | 100% stacked bars |
+| `position-dodge(width: 0.9, padding: 0.1)` | `width`, `padding` | Side-by-side bars |
+| `position-jitter(width: 0.4, height: 0.4, seed: 0)` | `width`, `height`, `seed` | Jitter points |
+| `position-jitterdodge(...)` | `jitter.width`, `dodge.width` | Jitter within dodge |
+| `position-nudge(x: 0, y: 0)` | `x`, `y` | Offset text/labels |
+| `position-identity()` | — | No adjustment (default) |
+
+---
+
+## `geom-text` / `geom-label` anchor values
+
+Both `geom-text` and `geom-label` use CeTZ anchors, not ggplot2-style hjust/vjust:
+
+| Anchor | Meaning |
+|---|---|
+| `"center"` | Centred on point (default) |
+| `"north"` | Above point |
+| `"south"` | Below point |
+| `"east"` | Right of point |
+| `"west"` | Left of point |
+| `"north-east"` | Upper-right |
+| `"south-west"` | Lower-left |
+
+Use `dx` and `dy` (numbers in canvas units where 1 = 1cm, or Typst lengths) for fine offsets.
diff --git a/2026/2026-05-17-gribouille/gribouille-skill/references/scale-table.md b/2026/2026-05-17-gribouille/gribouille-skill/references/scale-table.md
new file mode 100644
index 0000000..dfed574
--- /dev/null
+++ b/2026/2026-05-17-gribouille/gribouille-skill/references/scale-table.md
@@ -0,0 +1,217 @@
+# Gribouille Scale Reference
+
+Full parameter reference for every scale family exported in `lib.typ`. All parameters verified against source code.
+
+Columns: **Scale** | **Aesthetic** | **Key params** | **When to use**
+
+---
+
+## Position scales — x axis
+
+| Scale | Aesthetic | Key params | When to use |
+|---|---|---|---|
+| `scale-x-continuous(name, limits, breaks, labels, transform: "identity", expand, secondary)` | x | `name`, `limits`, `breaks`, `labels`, `transform` | Override x axis ticks, labels, or limits |
+| `scale-x-log10(name, limits, breaks, labels)` | x | `name`, `breaks`, `labels` | Log₁₀ x axis; data must be positive |
+| `scale-x-sqrt(name, limits, breaks, labels)` | x | `name`, `breaks`, `labels` | Square-root x axis |
+| `scale-x-reverse(name, limits, breaks, labels)` | x | `name` | Reverse x direction |
+| `scale-x-binned(name, limits, n-breaks: 10, labels)` | x | `n-breaks`, `labels`, `limits` | Bin continuous x into discrete intervals |
+| `scale-x-discrete(name, limits, labels, expand)` | x | `limits` (reorder levels), `labels` | Force discrete x; reorder categories |
+| `scale-x-date(name, limits, breaks, labels, date-format, expand)` | x | `date-format` (Typst datetime.display pattern) | Date x axis; values as numeric days since 2000-01-01 or ISO-8601 strings |
+| `scale-x-datetime(...)` | x | same as `scale-x-date` | Datetime x axis |
+| `scale-x-time(...)` | x | same as `scale-x-date` | Time-of-day x axis |
+
+## Position scales — y axis
+
+| Scale | Aesthetic | Key params | When to use |
+|---|---|---|---|
+| `scale-y-continuous(name, limits, breaks, labels, transform: "identity", expand, secondary)` | y | same as `scale-x-continuous` | Override y axis |
+| `scale-y-log10(name, limits, breaks, labels)` | y | `name`, `breaks`, `labels` | Log₁₀ y axis |
+| `scale-y-sqrt(...)` | y | — | Square-root y axis |
+| `scale-y-reverse(...)` | y | — | Flip y direction |
+| `scale-y-binned(name, limits, n-breaks: 10, labels)` | y | `n-breaks`, `labels` | Bin continuous y |
+| `scale-y-discrete(name, limits, labels, expand)` | y | `limits`, `labels` | Force discrete y |
+| `scale-y-date(...)` | y | `date-format`, `limits`, `breaks` | Date y axis |
+| `scale-y-datetime(...)` | y | same | Datetime y axis |
+| `scale-y-time(...)` | y | same | Time-of-day y axis |
+
+---
+
+## Colour scales (discrete)
+
+| Scale | Aesthetic | Key params | When to use |
+|---|---|---|---|
+| `scale-colour-discrete(name, palette, limits, labels)` | colour | `palette` (array of colours or `auto`), `limits` | Custom discrete colour palette |
+| `scale-colour-manual(values, name, limits, labels)` | colour | `values` (array of colours or dict `level -> colour`) | Explicit named mapping |
+| `scale-colour-identity(name)` | colour | — | Colour column holds literal colour values |
+| `scale-colour-okabe-ito(name, limits, labels)` | colour | — | Colourblind-safe 8-colour discrete palette |
+| `scale-colour-hue(hue, chroma, luminance, name, limits, labels)` | colour | `hue` (range, e.g. `(15deg, 375deg)`), `chroma: 100`, `luminance: 65` | Hue-based palette; tune saturation |
+| `scale-colour-grey(start, end, name, limits, labels)` | colour | `start: 0.2`, `end: 0.8` (grey levels 0–1) | Greyscale discrete |
+| `scale-colour-brewer(palette, name, limits, labels)` | colour | `palette: "Set1"` (ColorBrewer palette name) | ColorBrewer palettes |
+
+## Colour scales (continuous)
+
+| Scale | Aesthetic | Key params | When to use |
+|---|---|---|---|
+| `scale-colour-continuous(name, palette, limits, breaks, labels)` | colour | `palette` (gradient or colour array) | Continuous colour from palette |
+| `scale-colour-gradient(low, high, name, limits, breaks, labels)` | colour | `low: rgb("#132B43")`, `high: rgb("#56B1F7")` | Simple two-colour gradient |
+| `scale-colour-gradient2(low, mid, high, midpoint, name, limits, breaks, labels)` | colour | `low`, `mid: white`, `high`, `midpoint: 0` | Diverging gradient centred at `midpoint` |
+| `scale-colour-gradientn(colours, name, limits, breaks, labels)` | colour | `colours` (array of 3+ colours) | Multi-stop gradient |
+| `scale-colour-distiller(palette, direction, name, limits, breaks, labels)` | colour | `palette: "Spectral"`, `direction: 1\|-1` | Brewer palettes interpolated to continuous |
+| `scale-colour-steps(low, high, n-breaks, name, limits, labels)` | colour | `low`, `high`, `n-breaks: 5` | Stepped two-colour gradient |
+| `scale-colour-steps2(low, mid, high, midpoint, n-breaks, name, limits, labels)` | colour | `low`, `mid: white`, `high`, `midpoint: 0`, `n-breaks: 5` | Stepped diverging gradient |
+| `scale-colour-stepsn(colours, n-breaks, name, limits, labels)` | colour | `colours`, `n-breaks: 5` | Stepped multi-stop gradient |
+| `scale-colour-fermenter(palette, n-breaks, direction, name, limits, labels)` | colour | `palette: "Spectral"`, `n-breaks: 5`, `direction: 1` | Brewer palettes cut into discrete bins |
+
+## Colour scales (viridis family)
+
+Supported `option` values: `"viridis"` (default), `"magma"`, `"plasma"`, `"inferno"`, `"cividis"`.
+
+| Scale | Aesthetic | Key params | When to use |
+|---|---|---|---|
+| `scale-colour-viridis-c(option, name, limits, breaks, labels)` | colour | `option: "viridis"` | Perceptually uniform continuous colour |
+| `scale-colour-viridis-d(option, name, limits, labels)` | colour | `option: "viridis"` | Perceptually uniform discrete colour |
+| `scale-colour-viridis-b(option, n-breaks, name, limits, labels)` | colour | `option: "viridis"`, `n-breaks: 5` | Perceptually uniform binned colour |
+
+## Fill scales
+
+Every colour scale above has an exact fill counterpart. Replace `colour` with `fill`:
+
+```
+scale-fill-discrete() scale-fill-continuous()
+scale-fill-manual() scale-fill-gradient()
+scale-fill-identity() scale-fill-gradient2()
+scale-fill-okabe-ito() scale-fill-gradientn()
+scale-fill-hue() scale-fill-brewer()
+scale-fill-grey() scale-fill-distiller()
+scale-fill-viridis-c() scale-fill-fermenter()
+scale-fill-viridis-d() scale-fill-steps()
+scale-fill-viridis-b() scale-fill-steps2()
+ scale-fill-stepsn()
+```
+
+---
+
+## Alpha scales
+
+| Scale | Aesthetic | Key params | When to use |
+|---|---|---|---|
+| `scale-alpha-continuous(name, range, limits, breaks, labels)` | alpha | `range: (0.1, 1)` | Map a continuous variable to transparency |
+| `scale-alpha-binned(n-breaks, range, name, limits, labels)` | alpha | `n-breaks: 4`, `range: (0.1, 1)` | Binned (stepped) alpha |
+| `scale-alpha-manual(values, name, limits, labels)` | alpha | `values` (array of 0–1 values) | Explicit alpha per level |
+| `scale-alpha-identity(name)` | alpha | — | Alpha column holds literal 0–1 values |
+
+---
+
+## Size scales
+
+| Scale | Aesthetic | Key params | When to use |
+|---|---|---|---|
+| `scale-size-continuous(name, range, limits, breaks, labels)` | size | `range: (1pt, 6pt)` | Map continuous var to point size |
+| `scale-radius(name, range, limits, breaks, labels)` | size | `range: (1pt, 6pt)` | Alias of `scale-size-continuous`; map to radius |
+| `scale-size-area(name, range, limits, breaks, labels)` | size | `range: (1pt, 6pt)` | Map to area (perceptually correct for magnitude) |
+| `scale-size-binned(n-breaks, range, name, limits, labels)` | size | `n-breaks: 4`, `range: (1pt, 6pt)` | Binned size scale |
+| `scale-size-binned-area(n-breaks, range, name, limits, labels)` | size | `n-breaks: 4`, `range: (1pt, 6pt)` | Binned area scale |
+| `scale-size-identity(name)` | size | — | Size column holds literal length values |
+| `scale-size-manual(values, name, limits, labels)` | size | `values` (array of lengths) | Explicit size per level |
+
+---
+
+## Linewidth scales
+
+| Scale | Aesthetic | Key params | When to use |
+|---|---|---|---|
+| `scale-linewidth-continuous(name, range, limits, breaks, labels)` | linewidth | `range: (0.4pt, 1.4pt)` | Map continuous var to line width |
+| `scale-linewidth-binned(n-breaks, range, name, limits, labels)` | linewidth | `n-breaks: 4`, `range: (0.4pt, 1.4pt)` | Binned linewidth |
+| `scale-linewidth-manual(values, name, limits, labels)` | linewidth | `values` (array of lengths) | Explicit linewidth per level |
+| `scale-linewidth-identity(name)` | linewidth | — | Linewidth column holds literal length values |
+
+---
+
+## Shape / linetype scales
+
+| Scale | Aesthetic | Key params | When to use |
+|---|---|---|---|
+| `scale-shape(name, palette, limits, labels)` | shape | `palette` (array of shape keywords or `auto`) | Discrete point shapes |
+| `scale-shape-manual(values, name, limits, labels)` | shape | `values` (array of shape keywords) | Explicit shape per level |
+| `scale-shape-identity(name)` | shape | — | Shape column holds literal keywords |
+| `scale-shape-binned(n-breaks, palette, name, limits, labels)` | shape | `n-breaks: 4`, `palette` | Binned shape scale |
+| `scale-linetype(name, palette, limits, labels)` | linetype | `palette` (array of dash keywords or `auto`) | Discrete line types |
+| `scale-linetype-manual(values, name, limits, labels)` | linetype | `values` (array of dash keywords) | Explicit linetype per level |
+| `scale-linetype-identity(name)` | linetype | — | Linetype column holds literal keywords |
+| `scale-linetype-binned(n-breaks, palette, name, limits, labels)` | linetype | `n-breaks: 4`, `palette` | Binned linetype scale (continuous var) |
+| `scale-linetype-continuous(name, palette, limits, labels)` | linetype | alias of `scale-linetype-binned(n-breaks: 4)` | Alias |
+| `scale-linetype-discrete(name, palette, limits, labels)` | linetype | alias of `scale-linetype()` | Alias |
+
+Shape keywords: `"circle"`, `"square"`, `"triangle"`, `"diamond"`, `"cross"`, `"x"`, `"star"`, `"triangle-down"`
+
+Linetype keywords: `"solid"`, `"dashed"`, `"dotted"`, `"dash-dotted"`, `"densely-dashed"`, `"loosely-dashed"`
+
+---
+
+## Format helpers (use in `labels:` parameter)
+
+| Function | Output example | Notes |
+|---|---|---|
+| `format-comma()` | 1,234,567 | Thousands separator; best for y-axis with large integers |
+| `format-percent()` | 12.3% | Multiply by 100 and append %; input should be 0–1 |
+| `format-scientific()` | 1.23×10⁴ | Scientific notation |
+| `format-currency()` | $1,234 | Dollar prefix + comma separator |
+| `format-number(digits: N)` | 3.14 | Fixed decimal places |
+| `format-lower()` | lowercase | Convert labels to lowercase |
+| `format-upper()` | UPPERCASE | Convert labels to uppercase |
+| `format-title()` | Title Case | Capitalise each word |
+| `format-wrap(width: N)` | wrapped text | Word-wrap long labels at N characters |
+
+---
+
+## Common patterns
+
+**Large y-axis numbers:**
+```typst
+scales: (scale-y-continuous(labels: format-comma()),)
+```
+
+**Log-log axes:**
+```typst
+scales: (scale-x-log10(), scale-y-log10(),)
+```
+
+**Custom discrete colour palette:**
+```typst
+scales: (
+ scale-colour-manual(
+ values: ("Setosa": rgb("#E69F00"), "Versicolor": rgb("#56B4E9"), "Virginica": rgb("#009E73")),
+ limits: ("Setosa", "Versicolor", "Virginica"),
+ ),
+)
+```
+
+**Colourblind-safe palette:**
+```typst
+scales: (scale-colour-okabe-ito(),)
+```
+
+**Viridis continuous fill for heatmap:**
+```typst
+scales: (scale-fill-viridis-c(option: "viridis"),)
+```
+
+**Diverging colour centred at zero:**
+```typst
+scales: (scale-colour-gradient2(low: blue, mid: white, high: red, midpoint: 0),)
+```
+
+**Reorder discrete x axis:**
+```typst
+scales: (scale-x-discrete(limits: ("small", "medium", "large")),)
+```
+
+**Date axis:**
+```typst
+scales: (scale-x-date(date-format: "[month repr:short] [year]"),)
+```
+
+**Size range for bubble chart:**
+```typst
+scales: (scale-size-area(range: (1pt, 12pt)),)
+```
diff --git a/2026/2026-05-17-gribouille/src_ip_last1h.csv b/2026/2026-05-17-gribouille/src_ip_last1h.csv
new file mode 100644
index 0000000..86c0576
--- /dev/null
+++ b/2026/2026-05-17-gribouille/src_ip_last1h.csv
@@ -0,0 +1,944 @@
+src_ip,unique_dst_ports,unique_sensors,total_sessions
+93.123.72.166,3,5,23424
+128.199.225.7,1,1,494
+8.138.98.143,1,1,329
+193.32.162.34,1,1,292
+162.243.84.33,1,1,267
+137.184.121.249,1,1,266
+162.243.27.58,1,1,266
+129.212.181.73,1,1,265
+165.22.9.166,1,1,264
+89.167.72.144,3,1,258
+164.92.78.51,1,1,235
+45.64.134.75,2,1,189
+157.7.195.26,1,1,160
+45.153.34.186,1,1,118
+217.216.78.75,1,1,117
+85.14.245.122,1,1,110
+41.33.34.161,1,1,81
+103.249.87.183,1,1,75
+80.209.241.86,1,1,60
+38.54.2.209,4,1,58
+34.62.197.208,1,1,58
+171.245.33.42,1,1,55
+18.218.118.203,12,5,53
+41.226.251.192,1,1,49
+217.15.163.226,4,1,47
+45.222.101.19,1,1,47
+3.129.187.38,8,5,44
+18.116.101.220,9,5,44
+104.236.195.21,1,1,32
+184.154.96.254,1,1,31
+209.141.62.110,4,1,30
+16.58.56.214,6,4,28
+159.195.21.150,3,1,28
+199.45.154.136,2,2,27
+170.106.74.121,1,1,27
+64.89.162.15,2,5,25
+114.134.187.208,1,1,24
+204.76.203.212,7,1,24
+89.190.159.181,24,1,24
+115.231.78.11,8,2,23
+37.27.236.158,1,1,22
+38.54.2.232,3,1,21
+43.159.39.252,1,1,20
+37.27.236.157,1,1,20
+194.152.44.1,5,2,20
+3.130.168.2,5,3,20
+204.76.203.18,1,1,19
+150.109.5.194,1,1,19
+14.103.117.97,1,1,19
+43.134.184.25,1,1,19
+45.148.10.183,1,1,19
+2a01:4f9:c014:5617::1,2,1,19
+103.164.54.12,1,1,16
+220.135.214.45,1,1,16
+103.164.54.11,1,1,15
+150.107.36.236,2,2,15
+66.132.172.200,1,1,14
+199.45.154.117,1,1,14
+66.132.195.45,1,1,14
+199.45.155.105,1,1,14
+66.132.172.192,1,1,14
+66.132.172.44,1,1,13
+66.132.186.171,1,1,13
+66.132.172.196,1,1,13
+167.94.146.48,1,1,13
+66.132.224.230,1,1,13
+167.94.146.63,1,1,13
+199.45.155.80,1,1,13
+45.148.10.240,1,1,13
+66.132.195.58,1,1,13
+66.132.172.134,1,1,13
+66.132.195.61,1,1,13
+141.98.83.48,3,4,12
+66.132.172.16,12,5,12
+146.190.154.85,1,1,12
+146.190.41.214,1,1,12
+159.203.184.44,1,1,12
+3.131.220.121,2,3,11
+93.152.208.42,3,3,11
+2a0a:4cc0:c1:3eeb:8841:d3ff:feb8:dd3,2,1,11
+47.92.114.35,1,1,11
+167.94.146.60,1,1,10
+47.76.98.153,1,1,10
+162.216.150.40,1,3,10
+106.75.9.232,1,1,9
+2.223.190.255,1,1,9
+199.45.155.64,1,1,9
+193.32.162.28,1,2,9
+184.105.247.195,1,1,9
+151.243.11.35,2,3,8
+77.91.118.46,4,3,8
+45.205.1.80,1,4,8
+152.32.255.94,1,1,8
+35.203.211.111,1,2,8
+66.132.172.109,1,1,8
+198.211.107.181,1,1,8
+175.42.62.216,1,1,8
+3.26.101.205,1,2,8
+192.169.213.223,1,1,7
+123.160.223.72,6,3,7
+185.191.126.221,1,1,7
+186.195.16.41,2,1,7
+93.152.208.18,1,1,7
+3.107.172.227,1,2,7
+54.206.215.53,1,3,7
+13.210.172.95,1,2,7
+199.45.154.32,6,4,6
+123.160.223.74,5,2,6
+168.222.142.246,1,1,6
+45.139.122.80,2,2,6
+45.198.224.9,2,2,6
+fe80::7438:2f67:501f:25c1,2,1,6
+66.132.186.163,1,1,6
+162.216.150.104,1,1,6
+45.198.224.5,1,3,6
+35.203.211.249,1,1,6
+5.61.209.126,1,3,6
+147.185.133.102,1,1,6
+162.216.150.67,1,1,6
+147.185.132.154,1,1,6
+35.203.211.97,1,1,6
+216.26.240.139,1,1,6
+203.124.54.250,1,1,6
+147.185.133.57,1,1,6
+35.203.211.211,1,1,6
+147.185.132.241,1,1,6
+43.131.23.154,1,1,6
+162.216.149.105,1,1,6
+162.216.149.88,1,1,6
+216.180.246.51,1,1,6
+3.25.242.106,1,2,6
+134.209.31.202,1,1,6
+147.185.132.124,1,1,6
+32.236.17.124,1,2,6
+162.216.150.156,1,3,6
+147.185.133.151,1,1,6
+35.203.211.223,1,1,6
+147.185.133.234,1,1,6
+45.142.193.63,1,1,5
+43.228.157.8,2,2,5
+45.148.10.67,2,3,5
+45.142.193.161,1,5,5
+66.240.236.119,1,1,5
+46.101.135.25,1,1,5
+159.223.96.57,1,1,5
+205.210.31.210,1,1,5
+137.135.210.132,1,1,5
+8.138.154.105,1,1,5
+198.235.24.65,1,1,5
+106.75.5.16,1,1,5
+35.171.184.69,1,1,5
+44.193.5.244,1,1,5
+3.91.59.38,1,1,5
+212.102.40.218,1,1,5
+47.91.21.128,1,1,5
+152.32.151.121,1,1,5
+8.209.89.108,1,1,5
+15.228.43.175,1,1,4
+35.176.94.140,1,1,4
+165.22.41.116,1,1,4
+62.171.133.1,1,3,4
+47.254.177.181,1,1,4
+45.148.10.121,3,3,4
+47.89.192.3,1,1,4
+2a02:c207:2324:3483::1,2,1,4
+80.66.83.43,4,1,4
+36.72.198.124,1,1,4
+194.164.107.5,2,2,4
+162.216.150.54,2,2,4
+198.7.56.121,1,2,4
+8.211.171.136,1,1,4
+91.92.240.13,1,1,4
+202.169.231.163,1,1,4
+5.37.196.96,1,1,4
+162.216.149.87,2,1,4
+43.228.157.9,1,3,4
+45.148.10.215,1,4,4
+92.63.197.47,4,1,4
+47.84.137.71,1,1,4
+47.245.129.160,1,1,4
+16.78.234.79,1,1,4
+16.78.35.218,1,1,4
+16.79.151.160,1,1,4
+77.91.118.18,2,2,4
+213.151.0.23,1,1,4
+35.203.211.80,2,2,4
+62.210.124.141,1,1,4
+54.196.178.144,1,1,4
+206.189.169.67,1,1,4
+47.254.156.46,1,1,4
+147.185.133.219,1,2,4
+3.25.134.46,1,2,4
+44.220.185.174,1,1,4
+159.89.140.183,1,1,4
+47.250.186.31,1,1,4
+47.254.170.206,1,1,4
+45.156.129.100,1,1,4
+35.203.211.159,1,4,4
+3.11.79.96,1,1,3
+15.228.200.159,1,1,3
+54.199.45.123,1,1,3
+18.228.188.197,1,1,3
+157.7.113.83,1,1,3
+15.229.254.21,1,1,3
+54.232.134.124,1,1,3
+18.177.145.24,1,1,3
+3.10.4.212,1,1,3
+40.112.183.29,1,1,3
+190.128.201.18,1,1,3
+20.14.254.44,1,1,3
+13.244.59.158,1,1,3
+38.54.2.221,1,1,3
+fe80::5054:8fff:fe9d:8866,1,1,3
+13.244.254.124,1,1,3
+38.54.2.148,1,1,3
+38.54.2.170,1,1,3
+45.155.40.62,1,1,3
+80.253.31.232,1,1,3
+43.159.34.84,1,1,3
+112.135.221.86,1,1,3
+147.185.133.93,1,1,3
+106.12.18.199,1,1,3
+194.26.211.71,1,1,3
+36.92.140.209,1,1,3
+20.65.194.123,1,1,3
+137.184.135.145,3,3,3
+43.130.48.59,1,1,3
+150.109.15.37,1,1,3
+20.29.24.158,1,1,3
+13.247.217.152,1,1,3
+78.13.64.10,1,1,3
+18.189.74.1,2,3,3
+14.103.112.104,1,1,3
+18.130.154.15,1,1,3
+100.50.17.159,2,3,3
+18.119.209.50,2,3,3
+35.203.210.84,1,2,3
+176.65.139.177,2,1,3
+95.215.0.144,2,2,3
+82.149.77.39,1,1,3
+51.178.114.78,1,1,3
+43.134.56.214,1,1,3
+185.246.128.133,1,2,3
+89.163.146.177,1,1,3
+43.134.114.90,1,1,3
+176.65.139.102,1,3,3
+91.196.152.29,3,3,3
+45.156.129.61,1,1,3
+45.156.129.62,1,1,3
+71.6.134.231,1,1,3
+43.134.72.170,1,1,3
+193.169.194.14,1,3,3
+147.185.133.5,2,1,3
+40.124.172.38,1,1,3
+124.156.194.172,1,1,3
+20.65.194.43,1,1,3
+45.142.193.169,3,1,3
+42.1.65.137,1,2,3
+81.16.152.2,1,3,3
+40.124.175.226,1,1,3
+91.191.209.118,2,1,3
+108.136.46.13,1,1,3
+91.227.114.132,3,1,3
+111.92.164.11,1,1,3
+108.136.125.105,1,1,3
+209.127.178.212,1,1,3
+108.136.236.183,1,1,3
+86.54.31.38,1,1,3
+35.203.210.134,2,2,3
+41.59.229.33,1,1,3
+20.29.22.204,1,1,3
+132.145.213.106,1,1,3
+27.150.188.148,1,1,3
+43.159.170.156,1,1,3
+200.77.172.159,1,1,3
+128.203.200.235,1,1,3
+20.64.105.230,1,1,3
+103.13.211.80,1,1,3
+20.65.184.116,1,1,3
+52.165.81.251,1,1,3
+52.165.88.155,1,1,3
+52.201.220.223,1,1,3
+35.173.35.12,1,1,3
+54.237.19.32,1,1,3
+124.156.198.118,1,1,3
+170.106.76.70,1,1,3
+18.234.37.48,1,1,3
+20.65.194.116,1,1,3
+115.231.78.10,3,2,3
+49.51.195.205,1,1,3
+35.203.210.204,1,3,3
+185.65.50.68,1,1,3
+45.233.110.69,1,1,3
+58.33.97.119,1,1,3
+43.135.135.126,1,1,3
+162.216.149.69,1,1,3
+34.156.120.97,1,2,3
+135.237.126.199,1,1,3
+3.107.19.156,1,1,3
+91.219.164.69,1,1,3
+74.249.177.110,1,1,3
+179.62.216.38,1,1,3
+45.150.175.7,1,1,3
+103.211.217.182,1,1,3
+43.153.70.163,1,1,3
+147.185.133.59,1,3,3
+147.185.133.145,1,1,3
+40.124.175.234,1,1,3
+43.156.0.171,1,1,3
+36.95.221.140,1,1,3
+139.59.57.109,1,1,3
+49.51.193.224,1,1,3
+35.72.182.176,1,1,2
+43.207.87.147,1,1,2
+18.170.78.254,1,1,2
+35.77.119.154,1,1,2
+54.248.132.122,1,1,2
+45.198.224.12,1,2,2
+65.49.1.232,2,2,2
+18.170.227.75,1,1,2
+18.228.4.207,1,1,2
+18.171.155.227,1,1,2
+199.241.136.225,1,1,2
+51.89.235.201,1,1,2
+86.106.74.244,1,1,2
+38.54.2.195,2,1,2
+74.80.182.100,1,2,2
+104.243.35.120,1,2,2
+185.12.59.118,1,2,2
+45.79.0.241,2,1,2
+144.202.82.88,1,2,2
+45.79.213.172,1,2,2
+93.152.208.26,1,1,2
+193.163.125.163,2,1,2
+185.104.63.91,1,2,2
+43.228.157.10,2,2,2
+18.175.147.36,1,1,2
+15.204.157.192,1,2,2
+18.133.141.48,1,1,2
+13.124.59.132,1,1,2
+176.65.148.197,1,1,2
+35.203.210.209,1,1,2
+35.203.210.153,1,1,2
+117.50.185.190,1,1,2
+91.231.89.112,2,2,2
+91.231.89.205,2,2,2
+147.185.133.58,1,2,2
+172.105.218.179,1,1,2
+104.241.232.225,1,1,2
+72.56.1.31,1,1,2
+2407:3640:2317:7282::1,1,1,2
+35.203.211.161,1,1,2
+87.251.64.155,1,2,2
+147.185.132.180,1,1,2
+77.83.39.197,1,2,2
+195.178.110.188,2,1,2
+146.88.241.28,1,2,2
+3.83.245.221,1,2,2
+162.216.149.63,1,1,2
+45.142.193.35,1,2,2
+213.177.179.193,1,1,2
+77.91.118.50,1,1,2
+18.217.208.51,1,2,2
+167.71.102.95,1,1,2
+162.216.150.81,1,1,2
+87.251.64.141,2,2,2
+45.142.193.53,2,1,2
+183.56.198.150,1,1,2
+35.203.210.250,1,1,2
+35.203.210.136,1,1,2
+45.136.6.159,1,2,2
+92.63.197.236,1,2,2
+162.216.150.184,1,2,2
+62.164.177.41,2,2,2
+147.185.133.194,1,1,2
+108.136.169.25,1,1,2
+135.237.126.37,1,1,2
+162.216.150.94,1,1,2
+35.203.211.34,1,1,2
+66.56.215.97,1,1,2
+198.235.24.74,1,1,2
+147.185.132.65,1,1,2
+162.216.149.182,1,1,2
+66.132.172.136,1,1,2
+86.54.31.34,1,1,2
+1.31.32.149,1,1,2
+147.185.132.95,1,1,2
+213.177.179.79,1,1,2
+43.218.119.126,1,1,2
+43.218.116.32,1,1,2
+178.16.54.226,1,2,2
+147.185.132.230,1,1,2
+162.216.150.174,1,1,2
+193.163.125.254,2,2,2
+104.243.35.104,1,2,2
+16.79.172.106,1,1,2
+16.78.249.242,1,1,2
+108.137.6.79,1,1,2
+16.78.78.6,1,1,2
+162.216.149.49,1,1,2
+199.45.154.176,2,2,2
+147.185.133.27,1,1,2
+35.203.210.132,1,1,2
+20.169.106.61,1,1,2
+18.190.15.50,1,2,2
+16.79.108.214,1,1,2
+35.203.210.226,1,2,2
+185.93.89.79,1,1,2
+44.198.50.187,1,1,2
+162.216.150.15,1,1,2
+44.201.78.152,1,1,2
+3.236.79.105,1,1,2
+3.131.24.55,1,2,2
+162.216.149.149,1,1,2
+45.142.193.149,1,1,2
+52.20.198.190,1,2,2
+18.221.179.104,1,2,2
+35.203.211.109,1,1,2
+34.238.53.37,1,1,2
+13.218.109.202,1,1,2
+35.203.211.113,1,1,2
+165.232.117.238,2,2,2
+3.142.170.60,1,2,2
+98.81.220.221,1,1,2
+98.92.77.160,1,1,2
+23.251.136.130,1,2,2
+98.92.95.0,1,1,2
+100.26.140.230,1,1,2
+151.115.48.199,1,1,2
+35.169.206.177,1,2,2
+176.65.149.135,1,1,2
+5.187.35.26,1,2,2
+147.185.133.195,1,2,2
+206.189.158.17,1,1,2
+47.82.163.231,1,1,2
+35.205.205.2,1,2,2
+3.238.223.82,1,1,2
+77.91.118.42,1,1,2
+18.214.89.148,1,1,2
+32.194.91.155,1,1,2
+94.154.35.215,1,2,2
+205.210.31.86,2,1,2
+35.203.211.5,1,1,2
+34.197.70.90,1,2,2
+35.203.210.249,1,1,2
+147.185.133.140,1,1,2
+162.216.149.172,1,2,2
+34.203.228.168,1,1,2
+34.228.104.231,1,2,2
+35.203.210.233,1,1,2
+147.185.132.107,1,1,2
+192.253.248.33,2,2,2
+94.231.206.200,1,1,2
+54.157.197.248,1,1,2
+103.146.23.85,1,1,2
+185.226.197.57,1,1,2
+64.23.159.132,1,1,2
+13.55.220.4,1,2,2
+147.185.133.235,1,1,2
+54.236.50.135,1,1,2
+44.223.70.88,1,1,2
+176.65.139.64,1,1,2
+4.227.179.79,1,1,2
+20.61.127.56,1,2,2
+104.243.41.89,1,2,2
+147.185.133.130,1,1,2
+147.185.133.197,1,1,2
+205.210.31.70,2,2,2
+16.162.86.219,1,1,2
+172.212.200.29,1,1,2
+216.25.89.87,1,2,2
+162.216.150.129,1,1,2
+154.197.56.163,1,1,2
+198.235.24.167,2,1,2
+3.10.208.91,1,1,1
+87.251.64.176,1,1,1
+184.154.78.51,1,1,1
+184.105.247.248,1,1,1
+206.189.174.93,1,1,1
+193.163.125.225,1,1,1
+64.62.156.53,1,1,1
+104.248.127.231,1,1,1
+45.79.192.147,1,1,1
+121.123.58.77,1,1,1
+65.49.20.77,1,1,1
+38.54.2.75,1,1,1
+45.56.83.110,1,1,1
+37.60.241.154,1,1,1
+66.154.118.5,1,1,1
+146.190.66.238,1,1,1
+78.128.114.42,1,1,1
+194.195.208.6,1,1,1
+91.231.89.237,1,1,1
+38.54.2.82,1,1,1
+66.132.186.249,1,1,1
+165.22.86.138,1,1,1
+65.49.1.237,1,1,1
+35.77.67.6,1,1,1
+35.178.188.252,1,1,1
+222.166.63.193,1,1,1
+172.235.40.131,1,1,1
+216.245.216.166,1,1,1
+104.248.190.235,1,1,1
+112.140.187.102,1,1,1
+18.175.241.255,1,1,1
+185.150.191.165,1,1,1
+35.75.5.41,1,1,1
+52.198.137.20,1,1,1
+45.43.62.37,1,1,1
+143.244.163.241,1,1,1
+91.231.89.255,1,1,1
+91.231.89.154,1,1,1
+177.69.176.208,1,1,1
+91.196.152.195,1,1,1
+13.244.117.140,1,1,1
+52.199.185.193,1,1,1
+58.18.253.253,1,1,1
+137.184.36.28,1,1,1
+119.160.195.109,1,1,1
+64.62.156.14,1,1,1
+118.193.36.149,1,1,1
+216.25.89.98,1,1,1
+148.135.33.66,1,1,1
+103.166.159.217,1,1,1
+138.59.121.97,1,1,1
+51.224.30.180,1,1,1
+157.245.245.228,1,1,1
+138.68.47.81,1,1,1
+160.242.36.130,1,1,1
+194.187.178.44,1,1,1
+194.187.178.148,1,1,1
+138.68.255.210,1,1,1
+167.94.146.39,1,1,1
+45.79.192.34,1,1,1
+213.209.159.56,1,1,1
+120.28.215.124,1,1,1
+14.103.114.205,1,1,1
+43.135.180.42,1,1,1
+185.12.59.117,1,1,1
+146.190.74.76,1,1,1
+205.210.31.95,1,1,1
+141.98.10.205,1,1,1
+204.48.17.249,1,1,1
+199.45.154.179,1,1,1
+43.156.111.59,1,1,1
+71.6.134.233,1,1,1
+149.40.57.240,1,1,1
+192.81.131.148,1,1,1
+175.110.115.68,1,1,1
+102.165.66.187,1,1,1
+170.106.84.129,1,1,1
+120.48.150.146,1,1,1
+199.45.154.191,1,1,1
+194.187.179.191,1,1,1
+43.135.115.233,1,1,1
+216.73.116.170,1,1,1
+188.132.249.246,1,1,1
+106.75.11.34,1,1,1
+198.235.24.110,1,1,1
+106.75.139.1,1,1,1
+101.255.81.92,1,1,1
+179.130.249.237,1,1,1
+101.36.114.252,1,1,1
+78.12.230.151,1,1,1
+78.13.62.184,1,1,1
+209.141.45.103,1,1,1
+65.49.1.142,1,1,1
+45.142.193.164,1,1,1
+157.230.251.67,1,1,1
+65.49.1.216,1,1,1
+65.49.20.81,1,1,1
+198.235.24.233,1,1,1
+78.128.114.102,1,1,1
+3.34.51.6,1,1,1
+141.98.11.129,1,1,1
+15.165.11.191,1,1,1
+221.226.251.234,1,1,1
+78.13.63.111,1,1,1
+78.12.203.229,1,1,1
+79.124.58.142,1,1,1
+79.124.40.138,1,1,1
+65.49.1.151,1,1,1
+65.49.1.215,1,1,1
+209.222.101.194,1,1,1
+185.242.3.179,1,1,1
+128.199.175.30,1,1,1
+198.235.24.83,1,1,1
+146.88.241.78,1,1,1
+54.116.39.64,1,1,1
+43.202.157.48,1,1,1
+91.196.152.183,1,1,1
+134.122.95.87,1,1,1
+91.231.89.212,1,1,1
+162.216.149.187,1,1,1
+176.65.148.38,1,1,1
+78.13.246.229,1,1,1
+78.12.184.123,1,1,1
+16.28.75.158,1,1,1
+198.235.24.234,1,1,1
+159.89.155.166,1,1,1
+193.3.53.5,1,1,1
+58.247.139.54,1,1,1
+216.25.89.157,1,1,1
+38.35.23.153,1,1,1
+64.62.156.94,1,1,1
+23.237.188.34,1,1,1
+69.164.203.32,1,1,1
+111.18.232.160,1,1,1
+91.231.89.201,1,1,1
+91.231.89.203,1,1,1
+185.242.226.68,1,1,1
+65.49.1.48,1,1,1
+167.94.145.26,1,1,1
+187.59.150.239,1,1,1
+204.76.203.15,1,1,1
+66.132.186.240,1,1,1
+20.127.217.70,1,1,1
+36.138.95.222,1,1,1
+45.156.129.60,1,1,1
+45.156.129.63,1,1,1
+216.25.89.111,1,1,1
+206.72.242.222,1,1,1
+66.132.172.250,1,1,1
+79.137.136.87,1,1,1
+79.124.62.178,1,1,1
+65.49.1.202,1,1,1
+162.216.149.219,1,1,1
+193.163.125.242,1,1,1
+187.49.79.88,1,1,1
+31.148.99.199,1,1,1
+173.255.192.158,1,1,1
+8.216.4.234,1,1,1
+193.163.125.198,1,1,1
+198.58.100.172,1,1,1
+100.28.153.226,1,1,1
+65.49.1.34,1,1,1
+165.154.129.130,1,1,1
+198.46.134.148,1,1,1
+35.203.211.81,1,1,1
+147.185.132.235,1,1,1
+24.64.89.135,1,1,1
+185.31.159.76,1,1,1
+45.142.154.10,1,1,1
+65.49.1.203,1,1,1
+68.183.90.27,1,1,1
+208.87.242.183,1,1,1
+184.167.242.57,1,1,1
+45.169.108.50,1,1,1
+31.220.3.165,1,1,1
+170.64.194.233,1,1,1
+148.153.121.224,1,1,1
+212.252.73.26,1,1,1
+119.246.167.217,1,1,1
+123.160.223.75,1,1,1
+194.180.49.70,1,1,1
+217.23.12.21,1,1,1
+47.88.94.125,1,1,1
+47.251.188.16,1,1,1
+216.26.250.68,1,1,1
+85.217.149.6,1,1,1
+147.185.133.147,1,1,1
+54.183.104.55,1,1,1
+66.175.212.77,1,1,1
+103.75.71.17,1,1,1
+64.62.156.51,1,1,1
+185.242.226.112,1,1,1
+147.185.132.168,1,1,1
+66.132.186.247,1,1,1
+5.37.248.3,1,1,1
+159.195.21.197,1,1,1
+170.187.165.139,1,1,1
+45.148.10.230,1,1,1
+177.128.40.60,1,1,1
+79.124.60.146,1,1,1
+45.186.105.158,1,1,1
+23.131.184.100,1,1,1
+176.65.149.182,1,1,1
+198.58.100.4,1,1,1
+167.62.208.92,1,1,1
+91.231.89.27,1,1,1
+91.231.89.30,1,1,1
+218.82.103.43,1,1,1
+92.63.197.5,1,1,1
+51.159.110.167,1,1,1
+165.227.201.85,1,1,1
+176.65.148.29,1,1,1
+35.203.210.62,1,1,1
+142.93.101.54,1,1,1
+45.33.52.85,1,1,1
+170.187.165.130,1,1,1
+43.218.82.95,1,1,1
+108.136.138.211,1,1,1
+193.163.125.210,1,1,1
+172.236.111.197,1,1,1
+162.216.149.180,1,1,1
+106.75.184.36,1,1,1
+66.132.186.210,1,1,1
+162.216.149.252,1,1,1
+35.203.210.180,1,1,1
+88.168.139.76,1,1,1
+85.217.140.47,1,1,1
+206.81.23.7,1,1,1
+143.42.1.34,1,1,1
+108.136.243.30,1,1,1
+8.208.14.66,1,1,1
+162.216.149.3,1,1,1
+107.174.63.105,1,1,1
+14.1.105.70,1,1,1
+185.217.0.181,1,1,1
+193.163.125.243,1,1,1
+65.49.1.81,1,1,1
+65.49.1.39,1,1,1
+74.80.182.98,1,1,1
+162.216.150.245,1,1,1
+162.216.149.223,1,1,1
+45.33.105.182,1,1,1
+64.89.163.173,1,1,1
+45.74.17.15,1,1,1
+35.203.211.210,1,1,1
+66.132.172.240,1,1,1
+96.241.210.217,1,1,1
+113.211.214.98,1,1,1
+64.62.156.38,1,1,1
+170.187.165.219,1,1,1
+64.62.156.61,1,1,1
+168.197.250.14,1,1,1
+176.65.132.171,1,1,1
+35.203.210.252,1,1,1
+66.132.172.234,1,1,1
+64.62.156.41,1,1,1
+182.44.12.249,1,1,1
+117.50.186.80,1,1,1
+141.95.34.214,1,1,1
+185.242.3.226,1,1,1
+34.97.154.33,1,1,1
+35.203.210.147,1,1,1
+78.128.114.58,1,1,1
+172.232.20.86,1,1,1
+206.189.97.71,1,1,1
+216.25.89.103,1,1,1
+66.132.186.212,1,1,1
+89.248.163.181,1,1,1
+47.246.28.133,1,1,1
+200.32.223.183,1,1,1
+128.0.104.39,1,1,1
+34.79.226.102,1,1,1
+43.134.100.210,1,1,1
+198.235.24.239,1,1,1
+118.193.59.41,1,1,1
+155.102.237.231,1,1,1
+89.37.117.103,1,1,1
+144.31.250.102,1,1,1
+92.63.197.180,1,1,1
+177.136.235.46,1,1,1
+36.107.94.31,1,1,1
+69.164.219.86,1,1,1
+88.8.56.187,1,1,1
+14.116.219.149,1,1,1
+5.161.101.51,1,1,1
+106.75.144.89,1,1,1
+54.157.161.191,1,1,1
+91.169.185.104,1,1,1
+106.13.57.206,1,1,1
+54.227.13.14,1,1,1
+125.227.225.146,1,1,1
+162.216.150.77,1,1,1
+120.48.174.177,1,1,1
+199.127.63.58,1,1,1
+91.196.152.181,1,1,1
+18.208.211.225,1,1,1
+205.210.31.196,1,1,1
+34.77.191.38,1,1,1
+34.65.70.65,1,1,1
+88.230.99.12,1,1,1
+1.64.84.170,1,1,1
+12.165.97.6,1,1,1
+100.29.192.106,1,1,1
+206.221.176.60,1,1,1
+193.163.125.202,1,1,1
+216.180.246.67,1,1,1
+91.235.244.212,1,1,1
+44.200.244.16,1,1,1
+35.203.210.101,1,1,1
+66.132.172.228,1,1,1
+34.156.35.19,1,1,1
+191.11.141.94,1,1,1
+66.132.186.246,1,1,1
+45.248.78.120,1,1,1
+151.243.11.37,1,1,1
+91.196.152.115,1,1,1
+91.231.89.15,1,1,1
+176.65.139.103,1,1,1
+162.216.150.189,1,1,1
+176.65.149.174,1,1,1
+152.32.156.158,1,1,1
+193.163.125.222,1,1,1
+147.185.132.189,1,1,1
+52.90.232.112,1,1,1
+115.77.72.185,1,1,1
+220.158.234.185,1,1,1
+121.199.4.44,1,1,1
+173.255.223.32,1,1,1
+172.239.62.109,1,1,1
+162.216.150.66,1,1,1
+143.42.0.97,1,1,1
+220.134.42.205,1,1,1
+45.142.154.99,1,1,1
+47.91.105.164,1,1,1
+94.231.206.155,1,1,1
+216.25.89.99,1,1,1
+54.208.55.151,1,1,1
+65.49.1.235,1,1,1
+44.220.188.102,1,1,1
+45.142.193.12,1,1,1
+13.218.111.35,1,1,1
+218.17.184.95,1,1,1
+200.46.216.165,1,1,1
+112.83.254.115,1,1,1
+91.231.89.1,1,1,1
+91.231.89.108,1,1,1
+202.111.36.218,1,1,1
+199.45.154.181,1,1,1
+65.49.1.86,1,1,1
+216.25.89.123,1,1,1
+193.163.125.119,1,1,1
+103.220.165.11,1,1,1
+65.49.1.238,1,1,1
+147.185.133.26,1,1,1
+184.105.247.203,1,1,1
+44.223.32.38,1,1,1
+45.181.122.136,1,1,1
+212.132.127.66,1,1,1
+91.196.152.21,1,1,1
+187.126.60.121,1,1,1
+47.87.133.160,1,1,1
+185.242.226.97,1,1,1
+79.117.164.35,1,1,1
+162.216.149.209,1,1,1
+199.27.158.248,1,1,1
+79.124.49.70,1,1,1
+185.226.197.60,1,1,1
+184.72.117.131,1,1,1
+64.62.197.23,1,1,1
+185.38.217.150,1,1,1
+3.90.141.148,1,1,1
+32.194.166.114,1,1,1
+204.144.249.116,1,1,1
+92.63.197.181,1,1,1
+90.133.136.27,1,1,1
+122.227.221.18,1,1,1
+23.92.27.206,1,1,1
+60.50.112.205,1,1,1
+216.180.246.135,1,1,1
+120.201.53.112,1,1,1
+130.12.180.51,1,1,1
+66.228.40.98,1,1,1
+185.235.10.249,1,1,1
+185.235.10.248,1,1,1
+172.234.25.150,1,1,1
+185.242.226.166,1,1,1
+86.111.187.163,1,1,1
+91.231.89.195,1,1,1
+147.203.255.20,1,1,1
+176.32.193.16,1,1,1
+64.62.197.48,1,1,1
+152.32.183.236,1,1,1
+185.226.197.58,1,1,1
+91.196.152.210,1,1,1
+91.196.152.208,1,1,1
+146.190.21.73,1,1,1
+172.239.51.96,1,1,1
+119.129.205.228,1,1,1
+152.32.201.217,1,1,1
+152.249.76.76,1,1,1
+173.255.242.196,1,1,1
+216.25.89.144,1,1,1
+34.22.154.12,1,1,1
+74.80.182.78,1,1,1
+64.62.197.56,1,1,1
+218.26.204.42,1,1,1
+123.160.223.73,1,1,1
+88.210.63.193,1,1,1
+172.236.111.98,1,1,1
+66.132.224.20,1,1,1
+189.129.114.252,1,1,1
+49.79.83.214,1,1,1
+13.219.102.74,1,1,1
+143.42.1.128,1,1,1
+66.132.172.235,1,1,1
+96.126.101.29,1,1,1
+91.231.89.191,1,1,1
+91.231.89.190,1,1,1
+50.116.46.114,1,1,1
+94.231.206.133,1,1,1
+79.124.40.130,1,1,1
+51.254.17.136,1,1,1
+113.177.120.169,1,1,1
+35.203.210.214,1,1,1
+100.55.53.147,1,1,1
+34.239.169.116,1,1,1
+45.33.84.124,1,1,1
+87.120.104.75,1,1,1
+185.222.138.254,1,1,1
+159.223.102.155,1,1,1
+210.79.142.221,1,1,1
+186.193.158.139,1,1,1
+89.248.163.200,1,1,1
+138.118.24.121,1,1,1
+45.79.192.172,1,1,1
+64.62.197.35,1,1,1
+82.66.201.65,1,1,1
+128.14.227.52,1,1,1
+200.88.253.33,1,1,1
+35.203.211.105,1,1,1
+100.26.49.143,1,1,1
+43.199.45.121,1,1,1
+3.103.165.33,1,1,1
+3.102.45.127,1,1,1
+3.102.125.154,1,1,1
+43.210.15.233,1,1,1
+43.209.178.20,1,1,1
+128.9.29.128,1,1,1
+43.210.4.237,1,1,1
+18.167.42.176,1,1,1
+45.148.10.210,1,1,1
+98.91.180.98,1,1,1
+16.79.2.146,1,1,1
+45.161.192.131,1,1,1
+157.245.247.222,1,1,1
+70.122.52.128,1,1,1
+100.29.192.122,1,1,1
+74.80.182.80,1,1,1
+64.79.198.214,1,1,1
+45.142.193.139,1,1,1
+45.156.129.101,1,1,1
+45.156.129.103,1,1,1
+65.49.20.109,1,1,1
+47.88.35.29,1,1,1
+184.105.247.254,1,1,1
+109.160.13.100,1,1,1
+208.87.243.51,1,1,1
+65.49.1.212,1,1,1
+43.198.16.121,1,1,1