compose
Arrange multiple plots into a grid or stack with a shared, hoisted legend.
compose is the multi-plot orchestrator: it takes deferred plots, probes each panel’s would-be guides, decides which legends are identical across every panel, lifts them into a single shared block on the requested side, and re-renders the panels with the hoisted aesthetics suppressed so each reclaims the margin its legend would have occupied. Inspired by patchwork::plot_layout(guides = "collect").
Every positional argument must be a deferred plot (plot(..., defer: true)); passing rendered content panics, because compose needs the spec to re-render.
Usage
compose(
..panels-positional,
layout: "grid",
columns: 2,
direction: ttb,
gutter: 0.5cm,
collect: auto,
guides-placement: "right",
)Parameters
| Parameter | Default | Description |
|---|---|---|
..panels-positional |
Two or more deferred plots produced byplot with defer: true. Order is preserved by the layout (left-to-right, top-to-bottom for grids; per dir for stacks). |
|
layout |
"grid" |
"grid" (default) lays panels into a Typst grid with columns columns; "stack" lays them into a Typst stack flowing in dir. |
columns |
2 |
Number of columns in "grid" layout. Ignored for "stack". |
direction |
ttb |
Stack direction (ttb, btt, ltr, rtl) used by "stack" layout. Ignored for "grid". |
gutter |
0.5cm |
Spacing between panels and between the panel block and the shared legend. |
collect |
auto |
Which aesthetics to hoist into the shared legend. - auto (default) hoists every aesthetic whose guide is identical across all panels (kind, title, levels/domain, breaks, labels, aesthetic mix). Aesthetics that disagree on any of those fields stay per-plot, so a mismatched panel never silently borrows another panel’s legend. - none disables hoisting entirely; each plot keeps its own legends. - An array of aesthetic names (e.g., ("colour", "fill")) restricts hoisting to the listed aesthetics. Listed aesthetics that aren’t mergeable across panels still stay per-plot; non-listed aesthetics are never hoisted regardless of agreement. The merge predicate ignores per-panel placement and grid shape (nrow / ncol); compose forces a single shared side and grid layout for the hoisted block. Custom guides (guide-custom) never hoist. |
guides-placement |
"right" |
Side where the shared legend appears, relative to the panel block. One of "right" (default), "left", "top", or "bottom". The hoisted guides’ direction is set automatically: "vertical" for left/right, "horizontal" for top/bottom. |
Returns
Typst content block: the panel layout with the shared legend stacked on guides-placement, or the bare panel layout when no aesthetic ends up hoisted.
Examples
Auto-collect: identical colour legend hoisted to the right.
#let panel(map) = plot(
data: mtcars, mapping: map,
layers: (geom-point(size: 3pt),),
width: 6cm, height: 4cm, defer: true,
)
#compose(
panel(aes(x: "wt", y: "mpg", colour: as-factor("cyl"))),
panel(aes(x: "hp", y: "mpg", colour: as-factor("cyl"))),
layout: "grid", columns: (auto, auto),
)[typst-render] Compilation failed for this block.
Error running typst (error code 1): <no output>
Restrict hoisting: shared colour only, per-plot size ladders stay in each panel.
#compose(
panel(aes(x: "wt", y: "mpg", colour: as-factor("cyl"), size: "hp")),
panel(aes(x: "hp", y: "mpg", colour: as-factor("cyl"), size: "wt")),
layout: "grid", columns: (auto, auto),
collect: ("colour",),
)[typst-render] Compilation failed for this block.
Error running typst (error code 1): <no output>
Place the shared legend below the panels.
#compose(
panel(aes(x: "wt", y: "mpg", colour: as-factor("cyl"))),
panel(aes(x: "hp", y: "mpg", colour: as-factor("cyl"))),
layout: "grid", columns: (auto, auto),
guides-placement: "bottom",
)[typst-render] Compilation failed for this block.
Error running typst (error code 1): <no output>