Rework dynamic theme logic#549
Rework dynamic theme logic#549vincentarelbundock wants to merge 30 commits intograntmcdermott:mainfrom
Conversation
|
Super, thanks for taking this on. I kept meaning to, but couldn't find the time... |
|
Frankly, I'm not sure I can wrap my head entirely around the margins part of the code. But this seems to be working for the cases I investigated. So I'd say this is OK for review. |
grantmcdermott
left a comment
There was a problem hiding this comment.
Left a few quick comments while I had a sec.
I also have a slightly more involved refactor in mind for the core dynamic theme margin adjustment logic. tl;dr Right now, we do things a bit back to front; we assign title space up front and then remove it if it is absent, or increase if it has multiple lines per your PR here. I think we could simplify things for these dnymaic themes, by setting margins that assume no titles a priori and then make (multline) space for them if these titles are present. I'll take a crack at this later today, hopefully.
…plot into pr/vincentarelbundock/549
|
Thanks for this. I'm completely snowed ATM and didn't get around to implementing the full margin-logic refactor that I mentioned above, i.e.
But... I did at least implement a |
|
I'm not in a rush at all, and TBH I don't understand this code super well. Probably best to wait. |
|
Hey bud, I'm going to submit a 0.6.1 release to CRAN shortly, which contains mostly bug fixes. I'll take another look at this PR afterwards, since I'd like to pair it with #500 as part of some "breaking" aesthetic changes for 0.7.0. |
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).
|
I've co-opted this PR to push through a larger refactoring of the dynamic theme (margins) logic that I've been meaning to implement for a while. Specifically, I've inverted the dynamic themes' margin ( Here's my + Claude's summary of all of the changes:
Some internal cleanups:
MWEThat's a lot of text. Seeing is believing, so here are a couple snapshot diffs that demonstrate some of these ideas: |
|
@vincentarelbundock @zeileis Please give this a test if you get a chance. The basic idea is that dynamic themes should give better consistency and reduce whitespace where possible. I'd like to merge this before we tackle any of the other open PRs. |
|
This seems like an excellent change, congrats @grantmcdermott ! The screenshots look fantastic. |
|
@zeileis no pressure either way, but LMK if you'd like time to review this, or are happy for me to merge. |


GM edits:
Closes #303
Closes #479
Closes #573
Closes #574
This change fixes dynamic margins when annotation text is missing (
NA,NULL, or empty) and when labels contain multiple lines.Internal changes:
text_line_count()to normalize line counting for labels/subtitles (treat missing text as 0 lines).main/sub/xlab/ylabinto facet margin logic and adjusted margins by line count (shrink for missing text, expand for multi-line text).subin legend layout, sosub = NAno longer reserves extra bottom space.Testing note: