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:
- Fix panel widths in the ggplot gtable so the panel region spans the target text width exactly.
-
Export with
embed_panel_shift = TRUE, which bakes the horizontal alignment shift directly into the Typst fragment — no metadata parsing needed in the wrapper. -
Include the fragment in a Typst document with
set align(left). The fragment shifts itself left bypanel_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):