Get Started

Install Gribouille and build a real plot, one layer at a time.

Prerequisites

  • Typst 0.14 or later.
  • Familiarity with Typst #import syntax. No prior experience with the grammar of graphics is assumed, but if you have used ggplot2 in R or plotnine in Python the mental model will feel familiar.

Installation

Import Gribouille from Typst Universe, or from a local clone of the repository.

From Typst Universe

#import "@preview/gribouille:dev": *

From a local clone

#import "./path/to/gribouille/lib.typ": *

This imports every public symbol so you can call plot, aes, geom-point, and friends directly.

Tutorial: build a penguins plot, one layer at a time

The library ships the Palmer Penguins dataset as the penguins symbol. We will build a single figure end to end, adding one grammar concept per step so you can see the role each piece plays.

1. Data and aesthetics

Every plot starts with a dataset and a aes mapping that binds column names to visual channels. At minimum geom-point needs x and y.

#plot(
  data: penguins,
  mapping: aes(x: "flipper-len", y: "body-mass"),
  layers: (geom-point(size: 2pt),),
  width: 12cm,
  height: 9cm,
)

Scatter of penguin body mass against flipper length, all points the same colour.

Scatter of penguin body mass against flipper length, all points the same colour.

2. Encode a third variable through colour

Add colour: "species" to the mapping and the points split visually by group. Every aesthetic mapping is independent of the geom, so the same colour scale will apply to whatever layers you stack on top.

#plot(
  data: penguins,
  mapping: aes(
    x: "flipper-len",
    y: "body-mass",
    colour: "species",
  ),
  layers: (geom-point(size: 2pt, alpha: 0.85),),
  width: 12cm,
  height: 9cm,
)

Scatter of body mass against flipper length, points coloured by species.

Scatter of body mass against flipper length, points coloured by species.

3. Stack a second layer (geom-smooth)

Layers stack in order. Adding a geom-smooth draws one fitted line per colour group because the colour aesthetic is inherited from the plot-level mapping.

#plot(
  data: penguins,
  mapping: aes(
    x: "flipper-len",
    y: "body-mass",
    colour: "species",
    fill: "species",
  ),
  layers: (
    geom-point(size: 2pt, alpha: 0.6),
    geom-smooth(method: "lm", alpha: 0.2),
  ),
  width: 12cm,
  height: 9cm,
)

Per-species scatter with linear fits and 95% confidence ribbons.

Per-species scatter with linear fits and 95% confidence ribbons.

4. Pick a custom colour palette

scale-colour-discrete replaces the default palette. A matching scale-fill-discrete keeps the smooth ribbon’s fill in sync with the points.

#let cb = (
  rgb("#0072B2"),  // blue
  rgb("#D55E00"),  // vermillion
  rgb("#009E73"),  // bluish green
)

#plot(
  data: penguins,
  mapping: aes(
    x: "flipper-len",
    y: "body-mass",
    colour: "species",
    fill: "species",
  ),
  layers: (
    geom-point(size: 2pt, alpha: 0.6),
    geom-smooth(method: "lm", alpha: 0.2),
  ),
  scales: (
    scale-colour-discrete(palette: cb),
    scale-fill-discrete(palette: cb),
  ),
  width: 12cm,
  height: 9cm,
)

Same scatter as step 3 with a colour-blind-friendly palette.

Same scatter as step 3 with a colour-blind-friendly palette.

5. Split into small multiples with facet-wrap

facet-wrap lays one panel per level of a discrete column. The same scales apply to every panel, so the species clusters can be compared across islands.

#let cb = (
  rgb("#0072B2"),
  rgb("#D55E00"),
  rgb("#009E73"),
)

#plot(
  data: penguins,
  mapping: aes(
    x: "flipper-len",
    y: "body-mass",
    colour: "species",
    fill: "species",
  ),
  layers: (
    geom-point(size: 2pt, alpha: 0.6),
    geom-smooth(method: "lm", alpha: 0.2),
  ),
  scales: (
    scale-colour-discrete(palette: cb),
    scale-fill-discrete(palette: cb),
  ),
  facet: facet-wrap("island"),
  width: 12cm,
  height: 9cm,
)

Three side-by-side panels, one per island, sharing colour and axis scales.

Three side-by-side panels, one per island, sharing colour and axis scales.

6. Polish with theme() and labs()

theme-minimal drops most of the chrome. labs sets the title block and renames every axis and legend. The single theme call further tweaks the title surface.

#let cb = (
  rgb("#0072B2"),
  rgb("#D55E00"),
  rgb("#009E73"),
)

#plot(
  data: penguins,
  mapping: aes(
    x: "flipper-len",
    y: "body-mass",
    colour: "species",
    fill: "species",
  ),
  layers: (
    geom-point(size: 2pt, alpha: 0.6),
    geom-smooth(method: "lm", alpha: 0.2),
  ),
  scales: (
    scale-colour-discrete(palette: cb),
    scale-fill-discrete(palette: cb),
  ),
  facet: facet-wrap("island"),
  labs: labs(
    title: "Penguin Body Mass Scales with Flipper Length",
    subtitle: "Same trend on every island, even though sample sizes differ",
    x: "Flipper Length (mm)",
    y: "Body Mass (g)",
    colour: "Species",
    fill: "Species",
  ),
  theme: theme-minimal(
    plot-title: element-text(size: 12pt, weight: "bold")
  ),
  width: 12cm,
  height: 9cm,
)

Polished version of the faceted scatter with title block, axis labels, and minimal theme.

Polished version of the faceted scatter with title block, axis labels, and minimal theme.

7. Format tick labels with format-comma

Tick labels accept any formatter callable. scale-y-continuous with labels: format-comma() renders body mass as 3,000, 4,000, … instead of bare digits. Sibling formatters format-percent, format-currency, and format-scientific share the same call shape.

#let cb = (
  rgb("#0072B2"),
  rgb("#D55E00"),
  rgb("#009E73"),
)

#plot(
  data: penguins,
  mapping: aes(
    x: "flipper-len",
    y: "body-mass",
    colour: "species",
    fill: "species",
  ),
  layers: (
    geom-point(size: 2pt, alpha: 0.6),
    geom-smooth(method: "lm", alpha: 0.2),
  ),
  scales: (
    scale-colour-discrete(palette: cb),
    scale-fill-discrete(palette: cb),
    scale-y-continuous(labels: format-comma()),
  ),
  facet: facet-wrap("island"),
  labs: labs(
    title: "Penguin Body Mass Scales with Flipper Length",
    subtitle: "Same trend on every island, even though sample sizes differ",
    x: "Flipper Length (mm)",
    y: "Body Mass (g)",
    colour: "Species",
    fill: "Species",
  ),
  theme: theme-minimal(
    plot-title: element-text(size: 12pt, weight: "bold")
  ),
  width: 12cm,
  height: 9cm,
)

Polished faceted scatter whose body-mass tick labels carry thousands separators.

Polished faceted scatter whose body-mass tick labels carry thousands separators.

8. Render Typst markup in the subtitle with element-typst

element-typst is a drop-in replacement for element-text that evaluates plain strings as Typst markup. Wiring it to the plot-subtitle theme surface lets the subtitle mix prose with math: anything between $ $ becomes a rendered formula while the rest reads as ordinary text.

#let cb = (
  rgb("#0072B2"),
  rgb("#D55E00"),
  rgb("#009E73"),
)
#let n = penguins.species.len()

#plot(
  data: penguins,
  mapping: aes(
    x: "flipper-len",
    y: "body-mass",
    colour: "species",
    fill: "species",
  ),
  layers: (
    geom-point(size: 2pt, alpha: 0.6),
    geom-smooth(method: "lm", alpha: 0.2),
  ),
  scales: (
    scale-colour-discrete(palette: cb),
    scale-fill-discrete(palette: cb),
    scale-y-continuous(labels: format-comma()),
  ),
  facet: facet-wrap("island"),
  labs: labs(
    title: "Penguin Body Mass Scales with Flipper Length",
    subtitle: "Linear fit: $hat(y) = beta_0 + beta_1 dot.c x$ for $n = " + str(n) + "$ birds",
    x: "Flipper Length (mm)",
    y: "Body Mass (g)",
    colour: "Species",
    fill: "Species",
  ),
  theme: theme-minimal(
    plot-title: element-text(size: 12pt, weight: "bold"),
    plot-subtitle: element-typst(size: 9pt),
  ),
  width: 12cm,
  height: 9cm,
)

Step 7 plot with a subtitle whose linear-fit formula renders as Typst math.

Step 7 plot with a subtitle whose linear-fit formula renders as Typst math.

9. Annotate inside the panel with geom-typst

geom-typst is the Typst-markup sibling of geom-text: every value of the label aesthetic renders as Typst at the row’s (x, y). The annotation layer carries one row per (species, island) cell so each $N_"species" = ...$ label rides the colour scale defined in step 4. Every row also carries an island value so facet-wrap routes it to the right panel.

#let cb = (
  rgb("#0072B2"),
  rgb("#D55E00"),
  rgb("#009E73"),
)
#let n = penguins.species.len()

#let species-counts = (
  (island: "Torgersen", flipper-len: 172, body-mass: 6200, species: "Adelie",    label: $N_"Adelie" = 52$),
  (island: "Biscoe",    flipper-len: 172, body-mass: 6200, species: "Adelie",    label: $N_"Adelie" = 44$),
  (island: "Dream",     flipper-len: 172, body-mass: 6200, species: "Adelie",    label: $N_"Adelie" = 56$),
  (island: "Dream",     flipper-len: 172, body-mass: 5700, species: "Chinstrap", label: $N_"Chinstrap" = 68$),
  (island: "Biscoe",    flipper-len: 172, body-mass: 5700, species: "Gentoo",    label: $N_"Gentoo" = 124$),
)

#plot(
  data: penguins,
  mapping: aes(
    x: "flipper-len",
    y: "body-mass",
    colour: "species",
    fill: "species",
  ),
  layers: (
    geom-point(size: 2pt, alpha: 0.6),
    geom-smooth(method: "lm", alpha: 0.2),
    geom-typst(
      data: species-counts,
      mapping: aes(
        x: "flipper-len",
        y: "body-mass",
        colour: "species",
        label: "label",
      ),
      inherit-aes: false,
      anchor: "west",
      size: 9pt,
    ),
  ),
  scales: (
    scale-colour-discrete(palette: cb),
    scale-fill-discrete(palette: cb),
    scale-y-continuous(labels: format-comma()),
  ),
  facet: facet-wrap("island"),
  labs: labs(
    title: "Penguin Body Mass Scales with Flipper Length",
    subtitle: "Linear fit: $hat(y) = beta_0 + beta_1 dot.c x$ for $n = " + str(n) + "$ birds",
    x: "Flipper Length (mm)",
    y: "Body Mass (g)",
    colour: "Species",
    fill: "Species",
  ),
  theme: theme-minimal(
    plot-title: element-text(size: 12pt, weight: "bold"),
    plot-subtitle: element-typst(size: 9pt),
  ),
  width: 12cm,
  height: 9cm,
)

Step 8 plot with per-species sample-size labels rendered as Typst math inside each facet panel.

Step 8 plot with per-species sample-size labels rendered as Typst math inside each facet panel.

The grammar in one diagram

Every plot you build composes the same pieces. plot() wraps a stack of layers, each sheet sitting on the one below, from data and mapping at the foundation up through theme and labels.

A plot() container surrounding six stacked parallax sheets representing the grammar layers, from bottom to top: data plus aes(), layer (geom plus stat plus position), scale-*, coord-*, facet-*, and theme plus labs.

A plot() container surrounding six stacked parallax sheets representing the grammar layers, from bottom to top: data plus aes(), layer (geom plus stat plus position), scale-*, coord-*, facet-*, and theme plus labs.

Next steps

  • Read the Reference to see every function the library exposes.
  • Browse the Examples gallery for idiomatic usage.
  • Check the Changelog for recent changes.
Back to top