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>

See also

plot, aes, guides.

Back to top