diff options
Diffstat (limited to '2026/2026-05-17-gribouille/gribouille-skill')
3 files changed, 798 insertions, 0 deletions
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)),) +``` |
