Skip to contents

This vignette shows how to align ggplot panel edges with the text block margins of a Typst document, so axis labels and legends bleed naturally into the page margins.

The key steps are:

  1. Fix panel widths in the ggplot gtable so the panel region spans the target text width exactly.
  2. Export with embed_panel_shift = TRUE, which bakes the horizontal alignment shift directly into the Typst fragment — no metadata parsing needed in the wrapper.
  3. Include the fragment in a Typst document with set align(left). The fragment shifts itself left by panel_left, so the panel edges land on the text margins.

To suppress the built-in shift in a particular document, add #metadata(true) <disable-panel-shift> before the include.

if (!nzchar(typst_bin)) {
  stop("The 'typst' binary is required for this vignette.")
}

page_width_in <- 8.27
left_margin_in <- 1.5
body_width_in <- 5.0
right_margin_in <- page_width_in - left_margin_in - body_width_in

fragment_typ <- file.path(fig_dir, "multi-panel-fragment.typ")
wrapper_typ  <- file.path(fig_dir, "dummy-article.typ")
output_svg   <- file.path(fig_dir, "dummy-article.svg")

# ---- 1. Build ggplot and fix panel widths ------------------------------------
p <- ggplot(mpg, aes(displ, hwy, colour = class)) +
  geom_point(alpha = 0.65, size = 1.3) +
  geom_smooth(se = FALSE, linewidth = 0.45, method = "lm", formula = y ~ x, colour = "grey25") +
  facet_wrap(~drv, nrow = 1) +
  scale_colour_brewer(palette = "Set2") +
  labs(
    title = "Facet example with legend in the margin",
    subtitle = "Panel edges aligned to text block",
    x = "Engine displacement",
    y = "Highway MPG"
  ) +
  theme_grey(base_size = 9) +
  theme(
    legend.position = "right",
    plot.title.position = "panel",
    panel.spacing = unit(5, "pt"),
    plot.margin = margin(0, 0, 0, 0, unit = "pt"),
    plot.background = element_blank(),
    legend.background = element_rect(fill = NA, colour = NA),
    legend.box.background = element_rect(fill = NA, colour = NA),
    legend.key = element_rect(fill = NA, colour = NA)
  )

g <- ggplotGrob(p)
panel_cols <- sort(unique(g$layout$l[grepl("panel", g$layout$name)]))
if (!length(panel_cols)) stop("Could not identify ggplot panel columns.")

spacer_cols <- setdiff(seq.int(min(panel_cols), max(panel_cols)), panel_cols)
spacer_total_in <- if (length(spacer_cols)) {
  convertWidth(sum(g$widths[spacer_cols]), "in", valueOnly = TRUE)
} else 0

panel_width_in <- (body_width_in - spacer_total_in) / length(panel_cols)
g$widths[panel_cols] <- unit(panel_width_in, "in")
fig_width_in <- convertWidth(sum(g$widths), "in", valueOnly = TRUE)

# ---- 2. Export: shift is embedded in the fragment ----------------------------
gridcetz(
  filename = fragment_typ,
  expr = { grid.newpage(); grid.draw(g) },
  width  = fig_width_in,
  height = 3.5 * 2 / 3,
  embed_panel_shift = TRUE
)

# ---- 3. Build a minimal Typst wrapper ----------------------------------------
# The fragment is self-aligning; the wrapper only sets page geometry and fonts.
wrapper_lines <- c(
  sprintf("#set page(paper: \"a4\", margin: (left: %.3fin, right: %.3fin, y: 0.9in),", left_margin_in, right_margin_in),
  "  background: {",
  "    place(box(width: 100%, height: 100%, fill: rgb(\"#fffcf5\")), dx: 0in)",
  sprintf("    place(box(width: %.3fin, height: 110%%, fill: rgb(\"#faf7f0\")), dx: %.3fin)", body_width_in, left_margin_in),
  "  },",
  sprintf("  foreground: place(box(width: %.3fin, height: 110%%, stroke: (paint: luma(50%%), thickness: 0.2pt, dash: \"dashed\")), dx: %.3fin))", body_width_in, left_margin_in),
  "#set text(size: 10pt, font: \"Luciole\")",
  "#set par(justify: true)",
  "#show figure.caption: set align(left)",
  "#show figure.caption: set text(size: 0.9em, fill: luma(30%))",
  "#set figure(gap: 4pt)",
  "#set figure.caption(position: bottom)",
  "",
  "#lorem(70)",
  "",
  "#figure(",
  "  { set align(left); include(\"multi-panel-fragment.typ\") },",
  "  caption: [Multi-panel ggplot exported through gridcetz; panel edges flush",
  "    to the text block, axes and legend bleed into the margins.]",
  ")",
  "",
  "#lorem(70)"
)
writeLines(wrapper_lines, wrapper_typ)

# Export local copies for direct editing.
file.copy(fragment_typ, file.path(root_dir, "typst-panel-bleed-fragment.typ"), overwrite = TRUE)
## [1] TRUE
writeLines(
  gsub("multi-panel-fragment.typ", "typst-panel-bleed-fragment.typ", wrapper_lines, fixed = TRUE),
  file.path(root_dir, "typst-panel-bleed-demo.typ")
)

# ---- 4. Compile --------------------------------------------------------------
status <- system2(typst_bin,
  args = c("compile", "--root", root_dir, wrapper_typ, output_svg),
  stdout = TRUE, stderr = TRUE)
if (!is.null(attr(status, "status")) && !identical(attr(status, "status"), 0L))
  stop(paste(c("typst compile failed:", status), collapse = "\n"))

Rendered Typst page as SVG (embedded in this HTML vignette):