Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b088f5c
issue #303 margin adjustment for multi-line labels and titles
vincentarelbundock Feb 13, 2026
53cbb82
update snapshot
vincentarelbundock Feb 15, 2026
d1d2cba
docs
vincentarelbundock Feb 15, 2026
fbd19f8
theme_dynamic
grantmcdermott Feb 16, 2026
109640b
get_tpar instead of par()
vincentarelbundock Feb 21, 2026
3ab1f94
Merge branch 'issue303' of https://github.com/vincentarelbundock/tiny…
grantmcdermott Feb 22, 2026
ebf44db
tests
grantmcdermott Feb 22, 2026
9bbcff9
Merge branch 'main' into pr/vincentarelbundock/549
grantmcdermott Apr 20, 2026
03c07ff
Refactor dynamic margin logic to additive build
grantmcdermott Apr 21, 2026
7cf3082
Anchor main title at consistent line under dynmar
grantmcdermott Apr 21, 2026
096097b
Gotcha: treat sub=NA as absent when positioning main
grantmcdermott Apr 21, 2026
7ff977d
Anchor multi-line main at line N, not center
grantmcdermott Apr 21, 2026
d472f53
Scale main title bump with multi-line sub
grantmcdermott Apr 21, 2026
daae773
Replace hardcoded 1.2 sub-row bump with cex_sub + 0.2
grantmcdermott Apr 21, 2026
f0919ae
Remove internal pad from dynmar_side(); use theme mar as baseline
grantmcdermott Apr 21, 2026
4e9a9b0
Bump top outer margin by facet-strip height under dynmar
grantmcdermott Apr 22, 2026
eb50612
Add 0.6-line baseline padding to dynamic theme mar[4]
grantmcdermott Apr 22, 2026
ac711ab
Compute dynmar margins up front; drop theme baseline on outer-legend …
grantmcdermott Apr 22, 2026
e55b771
known false positives for test suite on macos
grantmcdermott Apr 27, 2026
cadbc4c
fix mfrow + plot.new clash
grantmcdermott Apr 27, 2026
11e2406
update test snapshots
grantmcdermott Apr 27, 2026
fd84857
avoid duplicate sanitize_legend calls
grantmcdermott Apr 27, 2026
853135f
remove unreachable dynmar_side fallbacks
grantmcdermott Apr 27, 2026
c8fe8ad
fix r cmd check errors
grantmcdermott Apr 27, 2026
9e3abfb
test tweaks
grantmcdermott Apr 27, 2026
8c385e8
issue #479
grantmcdermott Apr 28, 2026
55f9afb
update test snapshots
grantmcdermott Apr 28, 2026
d0c4652
news and dev version bump
grantmcdermott Apr 28, 2026
cd06059
issue #574
grantmcdermott Apr 28, 2026
fc63283
scalar comments for provenance
grantmcdermott Apr 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions R/facet.R
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ draw_facet_window = function(
draw,
grid,
has_legend,
main,
sub,
type,
xlab,
x, xmax, xmin,
ylab,
y, ymax, ymin,
tpars = NULL
) {
Expand Down Expand Up @@ -155,6 +159,20 @@ draw_facet_window = function(
fmar[1] = fmar[1] - (whtsbp * cex_fct_adj)
}
}

# Adjust margins for missing and multi-line annotation strings.
xlab_lines = text_line_count(xlab)
ylab_lines = text_line_count(ylab)
main_lines = text_line_count(main)

if (xlab_lines == 0) omar[1] = omar[1] - 1
if (ylab_lines == 0) omar[2] = omar[2] - 1
if (main_lines == 0) omar[3] = omar[3] - 1

if (xlab_lines > 1) omar[1] = omar[1] + (xlab_lines - 1) * par("cex.lab")
Comment thread
vincentarelbundock marked this conversation as resolved.
Outdated
if (ylab_lines > 1) omar[2] = omar[2] + (ylab_lines - 1) * par("cex.lab")
if (main_lines > 1) omar[3] = omar[3] + (main_lines - 1) * par("cex.main")

# FIXME: Is this causing issues for lhs legends with facet_grid?
# catch for missing rhs legend
if (isTRUE(attr(facet, "facet_grid")) && !has_legend) {
Expand Down Expand Up @@ -218,6 +236,20 @@ draw_facet_window = function(
omar[1] = omar[1] + whtsbp
}
}

# Adjust margins for missing and multi-line annotation strings.
xlab_lines = text_line_count(xlab)
ylab_lines = text_line_count(ylab)
main_lines = text_line_count(main)

if (xlab_lines == 0) omar[1] = omar[1] - 1
if (ylab_lines == 0) omar[2] = omar[2] - 1
if (main_lines == 0) omar[3] = omar[3] - 1

if (xlab_lines > 1) omar[1] = omar[1] + (xlab_lines - 1) * par("cex.lab")
if (ylab_lines > 1) omar[2] = omar[2] + (ylab_lines - 1) * par("cex.lab")
if (main_lines > 1) omar[3] = omar[3] + (main_lines - 1) * par("cex.main")

par(mar = omar)
}

Expand Down
2 changes: 1 addition & 1 deletion R/legend.R
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ prepare_legend = function(settings) {
}

legend_draw_flag = (is.null(legend) || !is.character(legend) || legend != "none" || bubble) && !isTRUE(add)
has_sub = !is.null(sub)
has_sub = text_line_count(sub) > 0L

# Generate labels for discrete legends
if (legend_draw_flag && isFALSE(by_continuous) && (!bubble || multi_legend)) {
Expand Down
9 changes: 8 additions & 1 deletion R/tinyplot.R
Original file line number Diff line number Diff line change
Expand Up @@ -1051,8 +1051,12 @@ tinyplot.default = function(
draw = draw,
grid = grid,
has_legend = has_legend,
main = main,
sub = sub,
type = type,
xlab = xlab,
x = x, xmax = xmax, xmin = xmin,
ylab = ylab,
y = y, ymax = ymax, ymin = ymin,
tpars = tpars
),
Expand All @@ -1075,16 +1079,19 @@ tinyplot.default = function(
draw = draw,
grid = grid,
has_legend = has_legend,
main = main,
sub = sub,
type = type,
xlab = xlab,
x = datapoints$x, xmax = datapoints$xmax, xmin = datapoints$xmin,
ylab = ylab,
y = datapoints$y, ymax = datapoints$ymax, ymin = datapoints$ymin,
tpars = tpar() # https://github.com/grantmcdermott/tinyplot/issues/474
),
getNamespace("tinyplot")
)
list2env(facet_window_args, environment())


#
## split and draw datapoints -----
#
Expand Down
2 changes: 0 additions & 2 deletions R/tinytheme.R
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@
#' Known current limitations include:
#'
#' - Themes do not work well when `legend = "top!"`.
#' - Dynamic margin spacing does not account for multi-line strings (e.g., axes
#' or main titles that contain "\\n").
#'
#' @return The function returns nothing. It is called for its side effects.
#'
Expand Down
41 changes: 40 additions & 1 deletion R/title.R
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ draw_title = function(main, sub, xlab, ylab, legend, legend_args, opar) {
if (is.null(line_main)) line_main = par("mgp")[3] + 1.7 - .1
line_main = line_main + 1.2
}
}

if (!is.null(sub)) {
if (isTRUE(get_tpar("side.sub", 1) == 3)) {
line_sub = get_tpar("line.sub", 1.7)
} else {
Expand All @@ -49,13 +52,33 @@ draw_title = function(main, sub, xlab, ylab, legend, legend_args, opar) {
}

if (!is.null(main)) {
main_lines = text_line_count(main)
if (main_lines > 1L) {
# Keep line 1 aligned with single-line titles by shifting the centered
# multi-line block downward by half its extra line height.
if (is.null(line_main)) line_main = par("mgp")[3] + 1.1
Comment thread
vincentarelbundock marked this conversation as resolved.
Outdated
line_main = line_main - (main_lines - 1) / 2
}
adj_main = get_tpar(c("adj.main", "adj"), 3)
ylab_lines = text_line_count(ylab)
# dynmar can expand left margin for multi-line ylab after title draw; apply
# a compensating right shift so main stays aligned with the plot box.
if (ylab_lines > 1L && isTRUE(get_tpar("dynmar", FALSE))) {
delta_in = (ylab_lines - 1) * par("csi") * par("cex.lab")
if (is.finite(par("pin")[1]) && par("pin")[1] > 0) {
multi_panel = prod(par("mfrow")) > 1 || prod(par("mfcol")) > 1
panel_boost = if (isTRUE(multi_panel)) 2 else 1
adj_main = adj_main + panel_boost * (delta_in / par("pin")[1])
}
adj_main = min(1, max(0, adj_main))
}
args = list(
main = main,
line = line_main,
cex.main = get_tpar("cex.main", 1.4),
col.main = get_tpar("col.main", "black"),
font.main = get_tpar("font.main", 2),
adj = get_tpar(c("adj.main", "adj"), 3))
adj = adj_main)
args = Filter(function(x) !is.null(x), args)
do.call(title, args)
}
Expand All @@ -65,7 +88,23 @@ draw_title = function(main, sub, xlab, ylab, legend, legend_args, opar) {
args = list(xlab = xlab)
args[["adj"]] = get_tpar(c("adj.xlab", "adj"))
do.call(title, args)

args = list(ylab = ylab)
ylab_lines = text_line_count(ylab)
if (ylab_lines > 1L) {
# Keep multi-line ylab centered around the default label line so outer
# lines do not get pushed off-device in tighter layouts (e.g., mfrow 2x2).
line_ylab = par("mgp")[1] - (ylab_lines - 1)
cex_ylab = get_tpar(c("cex.ylab", "cex.lab"), 1)
csi = par("csi")
left_margin_in = par("mai")[2]
# Keep roughly one glyph-width of room from the left device edge to avoid
# clipping of the outermost ylab line on compact multi-panel layouts.
edge_pad_in = 0.75 * csi * cex_ylab
Comment thread
grantmcdermott marked this conversation as resolved.
Outdated
max_line = (left_margin_in - edge_pad_in) / csi
line_ylab = min(line_ylab, max_line)
args[["line"]] = max(0, line_ylab)
}
args[["adj"]] = get_tpar(c("adj.ylab", "adj"))
do.call(title, args)
}
12 changes: 12 additions & 0 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ if (getRversion() <= "4.4.0") {
`%||%` = function(x, y) if (is.null(x)) y else x
}

# Count text lines. Returns 0 for absent text and 1 for expression objects.
text_line_count = function(x) {
if (is.null(x)) return(0L)
if (identical(x, NA) || identical(x, NA_character_)) return(0L)
if (!is.character(x)) return(1L)
if (!length(x)) return(0L)
keep = !is.na(x) & nzchar(x)
if (!any(keep)) return(0L)
x = x[which(keep)[1L]]
as.integer(1L + nchar(gsub("[^\n]", "", x)))
}


## Function that computes an appropriate bandwidth kernel based on a string
## input
Expand Down
124 changes: 124 additions & 0 deletions inst/tinytest/_tinysnapshot/margins_facet_multiline.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading