vincentarelbundock · GitHub

@vincentarelbundock

@vincentarelbundock changed the title [WIP] Issue #303 margin adjustment for multi-line labels and titles Issue #303 margin adjustment for multi-line labels and titles

Feb 15, 2026

@vincentarelbundock

grantmcdermott

…plot into pr/vincentarelbundock/549

@grantmcdermott

Instead of starting with a pre-inflated mar and subtracting when titles
are absent, dynamic themes now start at c(0.1, 0.1, 0.1, 0.1) and build
up per-side margins additively for the elements that are actually
present (tick row, axis labels, main, sub).
Key changes:
- New helper dynmar_side() in R/utils.R is the single source of truth
  for 'how much margin does this side need'. Both the pre-title path in
  tinyplot.default() and draw_facet_window() call into it.
- theme_dynamic$mar reduced to the minimal pad baseline. All dynamic
  derivatives (clean, clean2, bw, classic, minimal, ipsum, dark, ridge,
  ridge2) inherit via modifyList.
- draw_facet_window() simplified: no more 'if missing, subtract 1' per
  side; the new formula takes max(tick_extent, label_extent) because
  tick labels and axis titles share vertical space.
- Pre-sizing margins and calling plot.window() before draw_title()
  ensures title/mtext alignment measures against the plot region (not
  the default [0,1,0,1] figure region).
- draw_title() refactored: removed post-hoc ylab/main compensators that
  were workarounds for the old undersized-margin regime. Added explicit
  line = mgp[1] + (N-1)*cex for multi-line xlab so line 1 lands where a
  single-line xlab would sit (rather than being pushed up into the
  tick-label zone by base R's default multi-line layout).
- line_sub default tightened from 1.7 to 0.7 (side.sub=3) to reduce the
  gap between sub and the plot box top.
- xaxt / yaxt = 'n' is now honored: no tick row reserved when axes are
  off under a dynamic theme.
Non-dynmar themes (default, basic, tufte, void) are unchanged.
Snapshots will need regeneration on Linux (devcontainer) since the
pixel output has shifted for every dynamic theme.
Previously, main was drawn at base R's default line when sub was absent,
and at a tinyplot-specified line when sub was present. The two differed
slightly, causing main to shift when sub was toggled on/off.
Now we explicitly set line_main = mgp[3] + 0.6 for dynmar themes
regardless of sub presence. The sub branch still bumps main up by 1.2
lines when needed. Non-dynmar themes retain base R's default.
!is.null(NA) is TRUE, so the 'push main up to make room for sub' branch
was firing for sub = NA even though no sub space was reserved. Use
text_line_count(sub) > 0 so NA/empty sub is treated the same as NULL.
title() stacks multi-line main above the passed line (line N sits at
line, earlier lines extend upward). Dropping the old -(N-1)/2
adjustment, which was centering the block and causing line 1 to drift
with N. The top margin reservation already accounts for (N-1)*cex_main
extra lines.
The sub-row offset applied to line_main was a hardcoded +1.2
regardless of sub line count, so main overlapped multi-line sub. Now
scales as 1.2 + (N-1)*cex.sub, mirroring the margin reservation in
dynmar_side().
The main-push-up and margin-reservation formulas for sub both used a
hardcoded +1.2 for the first sub row, which happens to equal the
default cex.sub but doesn't scale if the user customises it. Express
the first-row bump as cex_sub + 0.2 (0.2-line breathing room) so it
tracks user-supplied cex.sub. Also fix the get_tpar default for
cex.sub in dynmar_side() to match the 1.2 used in draw_title().
No visual change for default cex.sub = 1.
Previously dynmar_side() smuggled in a pad = 0.3 constant as baseline
breathing room, which conflicted with the theme's own mar values. Now
the theme's mar acts as the baseline (via pmax), and dynmar_side()
returns exactly what's needed for the elements actually present.
Also:
- Main contribution now scales the top-line ascender with cex_main
  (0.6 * cex_main) so custom cex.main values (e.g. 2) don't clip.
- Sub contribution adds its own ascender when drawn alone (no main),
  derived from 0.6 * cex_sub. When main is present, main's ascender
  already covers the top so sub just contributes its row heights.
- Comments updated; call sites in tinyplot.R and facet.R use
  pmax(theme_mar, computed) as documented.
Without this, main/sub titles drawn above the top facet strip would
overlap the strip contents. fmar[3] already encodes facet_newlines and
the facet_grid special-case; we add back the 0.5 line that's stripped
when frame.plot is FALSE (that reduction is for inter-panel gaps, not
the top strip itself).
The right side never gets tick/label content (except spineplot), so
theme mar is the sole source of padding there. Bumping from 0.1 to
0.6 restores the breathing room the pre-refactor layout had. Other
sides stay at 0.1 (they get ample space from tick rows and labels
when present).
…sides
- Move full dynmar margin computation (theme_mar + dynmar_side + whtsbp)
  into tinyplot.default before legend drawing, pass dynmar_computed through
  to draw_facet_window instead of recomputing in two places.
- On outer-legend sides, zero only the theme baseline padding (.theme_mar)
  so the plot meets the legend's oma flush, while preserving axis-driven
  bumps (.dyn) for left!/bottom! legends.
- Detect legend position via tryCatch(eval(legend)) since sanitize_legend
  runs later inside draw_legend; default NULL legend treated as 'right!'.
- Hoist .whtsbp default so post-block references are always defined.
- draw_title gains ylab_line_offset for whtsbp-aware ylab placement.
- theme_dynamic baseline mar changed to c(0.1, 0.1, 0.6, 0.6).

@grantmcdermott grantmcdermott changed the title Issue #303 margin adjustment for multi-line labels and titles Rework dynamic theme logic

Apr 28, 2026

@grantmcdermott

Closed

- drop "Delta" part since that triggers a font rendering issue on the CI's Linux  box

Closed

@grantmcdermott

This was referenced

May 15, 2026

Closed

Closed

Closed

Read the original on github.com ↗