Skip to content

Commit f89212d

Browse files
authored
Tags at panels (#5167)
* Fix calc_element for character/numeric input * Add function for adding tag to table * Decommission previous tag logic * Update test * Strictness about manual placement * Adjust argument description * Roxygenate * Add NEWS bullet * Add test * Simplify padding * Revert "Fix calc_element for character/numeric input" This reverts commit 5dd6c2a. * Make placement choices more consistent * Tinker tests * Fix typos * Add `plot.tag.location` theme element * Use `plot.tag.location` in tag construction * Cover all combinations in tests * Update bullet
1 parent f7246d4 commit f89212d

14 files changed

+431
-87
lines changed

NEWS.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# ggplot2 (development version)
22

3+
* New `plot.tag.location` in `theme()` can control placement of the plot tag
4+
in the `"margin"`, `"plot"` or the new `"panel"` option (#4297).
5+
36
* `geom_text()` and `geom_label()` gained a `size.unit` parameter that set the
47
text size to millimetres, points, centimetres, inches or picas
58
(@teunbrand, #3799).
@@ -98,7 +101,7 @@ changes and a few bug fixes as well.
98101

99102
* Fixed bug in `coord_sf()` where graticule lines didn't obey
100103
`panel.grid.major`'s linewidth setting (@teunbrand, #5179).
101-
104+
102105
* `geom_text()` drops observations where `angle = NA` instead of throwing an
103106
error (@teunbrand, #2757).
104107

R/plot-build.R

Lines changed: 115 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -268,11 +268,6 @@ ggplot_gtable.ggplot_built <- function(data) {
268268
subtitle <- element_render(theme, "plot.subtitle", plot$labels$subtitle, margin_y = TRUE)
269269
subtitle_height <- grobHeight(subtitle)
270270

271-
# Tag
272-
tag <- element_render(theme, "plot.tag", plot$labels$tag, margin_y = TRUE, margin_x = TRUE)
273-
tag_height <- grobHeight(tag)
274-
tag_width <- grobWidth(tag)
275-
276271
# whole plot annotation
277272
caption <- element_render(theme, "plot.caption", plot$labels$caption, margin_y = TRUE)
278273
caption_height <- grobHeight(caption)
@@ -318,75 +313,7 @@ ggplot_gtable.ggplot_built <- function(data) {
318313
plot_table <- gtable_add_grob(plot_table, caption, name = "caption",
319314
t = -1, b = -1, l = caption_l, r = caption_r, clip = "off")
320315

321-
plot_table <- gtable_add_rows(plot_table, unit(0, 'pt'), pos = 0)
322-
plot_table <- gtable_add_cols(plot_table, unit(0, 'pt'), pos = 0)
323-
plot_table <- gtable_add_rows(plot_table, unit(0, 'pt'), pos = -1)
324-
plot_table <- gtable_add_cols(plot_table, unit(0, 'pt'), pos = -1)
325-
326-
tag_pos <- theme$plot.tag.position %||% "topleft"
327-
if (length(tag_pos) == 2) tag_pos <- "manual"
328-
valid_pos <- c("topleft", "top", "topright", "left", "right", "bottomleft",
329-
"bottom", "bottomright")
330-
331-
if (!(tag_pos == "manual" || tag_pos %in% valid_pos)) {
332-
cli::cli_abort("{.arg plot.tag.position} should be a coordinate or one of {.or {.val {valid_pos}}}")
333-
}
334-
335-
if (tag_pos == "manual") {
336-
xpos <- theme$plot.tag.position[1]
337-
ypos <- theme$plot.tag.position[2]
338-
tag_parent <- justify_grobs(tag, x = xpos, y = ypos,
339-
hjust = theme$plot.tag$hjust,
340-
vjust = theme$plot.tag$vjust,
341-
int_angle = theme$plot.tag$angle,
342-
debug = theme$plot.tag$debug)
343-
plot_table <- gtable_add_grob(plot_table, tag_parent, name = "tag", t = 1,
344-
b = nrow(plot_table), l = 1,
345-
r = ncol(plot_table), clip = "off")
346-
} else {
347-
# Widths and heights are reassembled below instead of assigning into them
348-
# in order to avoid bug in grid 3.2 and below.
349-
if (tag_pos == "topleft") {
350-
plot_table$widths <- unit.c(tag_width, plot_table$widths[-1])
351-
plot_table$heights <- unit.c(tag_height, plot_table$heights[-1])
352-
plot_table <- gtable_add_grob(plot_table, tag, name = "tag",
353-
t = 1, l = 1, clip = "off")
354-
} else if (tag_pos == "top") {
355-
plot_table$heights <- unit.c(tag_height, plot_table$heights[-1])
356-
plot_table <- gtable_add_grob(plot_table, tag, name = "tag",
357-
t = 1, l = 1, r = ncol(plot_table),
358-
clip = "off")
359-
} else if (tag_pos == "topright") {
360-
plot_table$widths <- unit.c(plot_table$widths[-ncol(plot_table)], tag_width)
361-
plot_table$heights <- unit.c(tag_height, plot_table$heights[-1])
362-
plot_table <- gtable_add_grob(plot_table, tag, name = "tag",
363-
t = 1, l = ncol(plot_table), clip = "off")
364-
} else if (tag_pos == "left") {
365-
plot_table$widths <- unit.c(tag_width, plot_table$widths[-1])
366-
plot_table <- gtable_add_grob(plot_table, tag, name = "tag",
367-
t = 1, b = nrow(plot_table), l = 1,
368-
clip = "off")
369-
} else if (tag_pos == "right") {
370-
plot_table$widths <- unit.c(plot_table$widths[-ncol(plot_table)], tag_width)
371-
plot_table <- gtable_add_grob(plot_table, tag, name = "tag",
372-
t = 1, b = nrow(plot_table), l = ncol(plot_table),
373-
clip = "off")
374-
} else if (tag_pos == "bottomleft") {
375-
plot_table$widths <- unit.c(tag_width, plot_table$widths[-1])
376-
plot_table$heights <- unit.c(plot_table$heights[-nrow(plot_table)], tag_height)
377-
plot_table <- gtable_add_grob(plot_table, tag, name = "tag",
378-
t = nrow(plot_table), l = 1, clip = "off")
379-
} else if (tag_pos == "bottom") {
380-
plot_table$heights <- unit.c(plot_table$heights[-nrow(plot_table)], tag_height)
381-
plot_table <- gtable_add_grob(plot_table, tag, name = "tag",
382-
t = nrow(plot_table), l = 1, r = ncol(plot_table), clip = "off")
383-
} else if (tag_pos == "bottomright") {
384-
plot_table$widths <- unit.c(plot_table$widths[-ncol(plot_table)], tag_width)
385-
plot_table$heights <- unit.c(plot_table$heights[-nrow(plot_table)], tag_height)
386-
plot_table <- gtable_add_grob(plot_table, tag, name = "tag",
387-
t = nrow(plot_table), l = ncol(plot_table), clip = "off")
388-
}
389-
}
316+
plot_table <- table_add_tag(plot_table, plot$labels$tag, theme)
390317

391318
# Margins
392319
plot_table <- gtable_add_rows(plot_table, theme$plot.margin[1], pos = 0)
@@ -431,3 +358,117 @@ by_layer <- function(f, layers, data, step = NULL) {
431358
)
432359
out
433360
}
361+
362+
# Add the tag element to the gtable
363+
table_add_tag <- function(table, label, theme) {
364+
# Initialise the tag margins
365+
table <- gtable_add_padding(table, unit(0, "pt"))
366+
367+
# Early exit when label is absent or element is blank
368+
if (length(label) < 1) {
369+
return(table)
370+
}
371+
element <- calc_element("plot.tag", theme)
372+
if (inherits(element, "element_blank")) {
373+
return(table)
374+
}
375+
376+
# Resolve position
377+
position <- calc_element("plot.tag.position", theme) %||% "topleft"
378+
location <- calc_element("plot.tag.location", theme) %||%
379+
(if (is.numeric(position)) "plot" else "margin")
380+
381+
if (is.numeric(position)) {
382+
if (location == "margin") {
383+
cli::cli_abort(paste0(
384+
"A {.cls numeric} {.arg plot.tag.position} cannot be used with ",
385+
"{.code \"margin\"} as {.arg plot.tag.location}."
386+
))
387+
}
388+
if (length(position) != 2) {
389+
cli::cli_abort(paste0(
390+
"A {.cls numeric} {.arg plot.tag.position} ",
391+
"theme setting must have length 2."
392+
))
393+
}
394+
top <- left <- right <- bottom <- FALSE
395+
} else {
396+
# Break position into top/left/right/bottom
397+
position <- arg_match0(
398+
position[1],
399+
c("topleft", "top", "topright", "left",
400+
"right", "bottomleft", "bottom", "bottomright"),
401+
arg_nm = "plot.tag.position"
402+
)
403+
top <- position %in% c("topleft", "top", "topright")
404+
left <- position %in% c("topleft", "left", "bottomleft")
405+
right <- position %in% c("topright", "right", "bottomright")
406+
bottom <- position %in% c("bottomleft", "bottom", "bottomright")
407+
}
408+
409+
# Resolve tag and sizes
410+
tag <- element_grob(element, label = label, margin_y = TRUE, margin_x = TRUE)
411+
height <- grobHeight(tag)
412+
width <- grobWidth(tag)
413+
414+
if (location %in% c("plot", "panel")) {
415+
if (!is.numeric(position)) {
416+
if (right || left) {
417+
x <- (1 - element$hjust) * width
418+
if (right) {
419+
x <- unit(1, "npc") - x
420+
}
421+
} else {
422+
x <- unit(element$hjust, "npc")
423+
}
424+
if (top || bottom) {
425+
y <- (1 - element$vjust) * height
426+
if (top) {
427+
y <- unit(1, "npc") - y
428+
}
429+
} else {
430+
y <- unit(element$vjust, "npc")
431+
}
432+
} else {
433+
x <- unit(position[1], "npc")
434+
y <- unit(position[2], "npc")
435+
}
436+
# Do manual placement of tag
437+
tag <- justify_grobs(
438+
tag, x = x, y = y,
439+
hjust = element$hjust, vjust = element$vjust,
440+
int_angle = element$angle, debug = element$debug
441+
)
442+
if (location == "plot") {
443+
table <- gtable_add_grob(
444+
table, tag, name = "tag", clip = "off",
445+
t = 1, b = nrow(table), l = 1, r = ncol(table)
446+
)
447+
return(table)
448+
}
449+
}
450+
451+
if (location == "panel") {
452+
place <- find_panel(table)
453+
} else {
454+
n_col <- ncol(table)
455+
n_row <- nrow(table)
456+
# Actually fill margin with relevant units
457+
if (top) table$heights <- unit.c(height, table$heights[-1])
458+
if (left) table$widths <- unit.c(width, table$widths[-1])
459+
if (right) table$widths <- unit.c(table$widths[-n_col], width)
460+
if (bottom) table$heights <- unit.c(table$heights[-n_row], height)
461+
place <- data_frame0(t = 1L, r = n_col, b = n_row, l = 1L)
462+
}
463+
464+
# Shrink placement to position
465+
if (top) place$b <- place$t
466+
if (left) place$r <- place$l
467+
if (right) place$l <- place$r
468+
if (bottom) place$t <- place$b
469+
470+
gtable_add_grob(
471+
table, tag, name = "tag", clip = "off",
472+
t = place$t, l = place$l, b = place$b, r = place$r
473+
)
474+
}

R/theme-elements.R

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,7 @@ el_def <- function(class = NULL, inherit = NULL, description = NULL) {
523523
plot.caption.position = el_def("character"),
524524
plot.tag = el_def("element_text", "title"),
525525
plot.tag.position = el_def(c("character", "numeric")), # Need to also accept numbers
526+
plot.tag.location = el_def("character"),
526527
plot.margin = el_def("margin"),
527528

528529
aspect.ratio = el_def("numeric")

R/theme.R

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,15 @@
133133
#' for margins and plot tag).
134134
#' @param plot.tag upper-left label to identify a plot (text appearance)
135135
#' ([element_text()]; inherits from `title`) left-aligned by default
136+
#' @param plot.tag.location The placement of the tag as a string, one of
137+
#' `"panel"`, `"plot"` or `"margin"`. Respectively, these will place the tag
138+
#' inside the panel space, anywhere in the plot as a whole, or in the margin
139+
#' around the panel space.
136140
#' @param plot.tag.position The position of the tag as a string ("topleft",
137-
#' "top", "topright", "left", "right", "bottomleft", "bottom", "bottomright)
138-
#' or a coordinate. If a string, extra space will be added to accommodate the
139-
#' tag.
141+
#' "top", "topright", "left", "right", "bottomleft", "bottom", "bottomright")
142+
#' or a coordinate. If a coordinate, can be a numeric vector of length 2 to
143+
#' set the x,y-coordinate relative to the whole plot. The coordinate option
144+
#' is unavailable for `plot.tag.location = "margin"`.
140145
#' @param plot.margin margin around entire plot (`unit` with the sizes of
141146
#' the top, right, bottom, and left margins)
142147
#'
@@ -357,6 +362,7 @@ theme <- function(line,
357362
plot.caption.position,
358363
plot.tag,
359364
plot.tag.position,
365+
plot.tag.location,
360366
plot.margin,
361367
strip.background,
362368
strip.background.x,

man/theme.Rd

Lines changed: 10 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/_snaps/labels.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# plot.tag.position rejects invalid input
2+
3+
The `plot.tag.position` theme element must be a <character/numeric> object.
4+
5+
---
6+
7+
`plot.tag.position` must be one of "topleft", "top", "topright", "left", "right", "bottomleft", "bottom", or "bottomright", not "foobar".
8+

tests/testthat/_snaps/labels/other-position.svg renamed to tests/testthat/_snaps/labels/tag-in-margin.svg

Lines changed: 1 addition & 1 deletion
Loading

0 commit comments

Comments
 (0)