Skip to content

Commit 3112348

Browse files
authored
Reorganise plot rendering (#6665)
* revert d35f103 * break up `ggplot_gtable()` method into smaller functions * move `by_layer` * scales have already been extracted from plot object * redocument
1 parent 194d8c9 commit 3112348

File tree

5 files changed

+372
-349
lines changed

5 files changed

+372
-349
lines changed

DESCRIPTION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ Collate:
213213
'plot-build.R'
214214
'plot-construction.R'
215215
'plot-last.R'
216+
'plot-render.R'
216217
'plot.R'
217218
'position-.R'
218219
'position-collide.R'

R/plot-build.R

Lines changed: 1 addition & 347 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ build_ggplot <- S7::method(ggplot_build, class_ggplot) <- function(plot, ...) {
8383
data <- by_layer(function(l, d) l$map_statistic(d, plot), layers, data, "mapping stat to aesthetics")
8484

8585
# Make sure missing (but required) aesthetics are added
86-
plot@scales$add_missing(c("x", "y"), plot@plot_env)
86+
scales$add_missing(c("x", "y"), plot@plot_env)
8787

8888
# Reparameterise geoms from (e.g.) y and width to ymin and ymax
8989
data <- by_layer(function(l, d) l$compute_geom_1(d), layers, data, "setting up geom")
@@ -190,146 +190,6 @@ get_layer_grob <- function(plot = get_last_plot(), i = 1L) {
190190
#' @rdname ggplot_build
191191
layer_grob <- get_layer_grob
192192

193-
#' Build a plot with all the usual bits and pieces.
194-
#'
195-
#' This function builds all grobs necessary for displaying the plot, and
196-
#' stores them in a special data structure called a [`gtable`][gtable::gtable].
197-
#' This object is amenable to programmatic manipulation, should you want
198-
#' to (e.g.) make the legend box 2 cm wide, or combine multiple plots into
199-
#' a single display, preserving aspect ratios across the plots.
200-
#'
201-
#' The `ggplot_gtable()` function is vestigial and the `gtable_ggplot()` function
202-
#' should be used instead.
203-
#'
204-
#' @seealso
205-
#' [print.ggplot()] and [benchplot()] for
206-
#' for functions that contain the complete set of steps for generating
207-
#' a ggplot2 plot.
208-
#'
209-
#' The `r link_book("gtable step section", "internals#sec-ggplotgtable")`
210-
#' @return a `gtable` object
211-
#' @keywords internal
212-
#' @param data plot data generated by [ggplot_build()]
213-
#' @export
214-
ggplot_gtable <- function(data) {
215-
# TODO: Swap to S7 generic once S7/#543 is resolved
216-
attach_plot_env(data@plot@plot_env)
217-
UseMethod("ggplot_gtable")
218-
}
219-
220-
S7::method(ggplot_gtable, class_ggplot_built) <- function(data) {
221-
plot <- data@plot
222-
layout <- data@layout
223-
data <- data@data
224-
theme <- plot@theme
225-
226-
geom_grobs <- by_layer(function(l, d) l$draw_geom(d, layout), plot@layers, data, "converting geom to grob")
227-
228-
plot_table <- layout$render(geom_grobs, data, theme, plot@labels)
229-
230-
# Legends
231-
legend_box <- plot@guides$assemble(theme)
232-
plot_table <- table_add_legends(plot_table, legend_box, theme)
233-
234-
# Title
235-
title <- element_render(
236-
theme, "plot.title", plot@labels$title,
237-
margin_y = TRUE, margin_x = TRUE
238-
)
239-
title_height <- grobHeight(title)
240-
241-
# Subtitle
242-
subtitle <- element_render(
243-
theme, "plot.subtitle", plot@labels$subtitle,
244-
margin_y = TRUE, margin_x = TRUE
245-
)
246-
subtitle_height <- grobHeight(subtitle)
247-
248-
# whole plot annotation
249-
caption <- element_render(
250-
theme, "plot.caption", plot@labels$caption,
251-
margin_y = TRUE, margin_x = TRUE
252-
)
253-
caption_height <- grobHeight(caption)
254-
255-
# positioning of title and subtitle is governed by plot.title.position
256-
# positioning of caption is governed by plot.caption.position
257-
# "panel" means align to the panel(s)
258-
# "plot" means align to the entire plot (except margins and tag)
259-
title_pos <- arg_match0(
260-
theme$plot.title.position %||% "panel",
261-
c("panel", "plot"),
262-
arg_nm = "plot.title.position",
263-
error_call = expr(theme())
264-
)
265-
266-
caption_pos <- arg_match0(
267-
theme$plot.caption.position %||% "panel",
268-
values = c("panel", "plot"),
269-
arg_nm = "plot.caption.position",
270-
error_call = expr(theme())
271-
)
272-
273-
pans <- plot_table$layout[grepl("^panel", plot_table$layout$name), , drop = FALSE]
274-
if (title_pos == "panel") {
275-
title_l <- min(pans$l)
276-
title_r <- max(pans$r)
277-
} else {
278-
title_l <- 1
279-
title_r <- ncol(plot_table)
280-
}
281-
if (caption_pos == "panel") {
282-
caption_l <- min(pans$l)
283-
caption_r <- max(pans$r)
284-
} else {
285-
caption_l <- 1
286-
caption_r <- ncol(plot_table)
287-
}
288-
289-
plot_table <- gtable_add_rows(plot_table, subtitle_height, pos = 0)
290-
plot_table <- gtable_add_grob(plot_table, subtitle, name = "subtitle",
291-
t = 1, b = 1, l = title_l, r = title_r, clip = "off")
292-
293-
plot_table <- gtable_add_rows(plot_table, title_height, pos = 0)
294-
plot_table <- gtable_add_grob(plot_table, title, name = "title",
295-
t = 1, b = 1, l = title_l, r = title_r, clip = "off")
296-
297-
plot_table <- gtable_add_rows(plot_table, caption_height, pos = -1)
298-
plot_table <- gtable_add_grob(plot_table, caption, name = "caption",
299-
t = -1, b = -1, l = caption_l, r = caption_r, clip = "off")
300-
301-
plot_table <- table_add_tag(plot_table, plot@labels$tag, theme)
302-
303-
# Margins
304-
plot_margin <- calc_element("plot.margin", theme) %||% margin()
305-
plot_table <- gtable_add_padding(plot_table, plot_margin)
306-
307-
if (is_theme_element(theme$plot.background)) {
308-
plot_table <- gtable_add_grob(plot_table,
309-
element_render(theme, "plot.background"),
310-
t = 1, l = 1, b = -1, r = -1, name = "background", z = -Inf)
311-
plot_table$layout <- plot_table$layout[c(nrow(plot_table$layout), 1:(nrow(plot_table$layout) - 1)),]
312-
plot_table$grobs <- plot_table$grobs[c(nrow(plot_table$layout), 1:(nrow(plot_table$layout) - 1))]
313-
}
314-
315-
# add alt-text as attribute
316-
attr(plot_table, "alt-label") <- plot@labels$alt
317-
318-
plot_table
319-
}
320-
321-
#' Generate a ggplot2 plot grob.
322-
#'
323-
#' @param x ggplot2 object
324-
#' @keywords internal
325-
#' @export
326-
ggplotGrob <- function(x) {
327-
ggplot_gtable(ggplot_build(x))
328-
}
329-
330-
S7::method(as.gtable, class_ggplot) <- function(x, ...) ggplotGrob(x)
331-
S7::method(as.gtable, class_ggplot_built) <- function(x, ...) ggplot_gtable(x)
332-
333193
# Apply function to layer and matching data
334194
by_layer <- function(f, layers, data, step = NULL) {
335195
ordinal <- label_ordinal()
@@ -349,209 +209,3 @@ by_layer <- function(f, layers, data, step = NULL) {
349209
)
350210
out
351211
}
352-
353-
# Add the tag element to the gtable
354-
table_add_tag <- function(table, label, theme) {
355-
# Initialise the tag margins
356-
table <- gtable_add_padding(table, unit(0, "pt"))
357-
358-
# Early exit when label is absent or element is blank
359-
if (length(label) < 1) {
360-
return(table)
361-
}
362-
element <- calc_element("plot.tag", theme)
363-
if (is_theme_element(element, "blank")) {
364-
return(table)
365-
}
366-
367-
# Resolve position
368-
position <- calc_element("plot.tag.position", theme) %||% "topleft"
369-
location <- calc_element("plot.tag.location", theme) %||%
370-
(if (is.numeric(position)) "plot" else "margin")
371-
372-
if (is.numeric(position)) {
373-
if (location == "margin") {
374-
cli::cli_abort(paste0(
375-
"A {.cls numeric} {.arg plot.tag.position} cannot be used with ",
376-
"`{.val margin}` as {.arg plot.tag.location}."
377-
),
378-
call = expr(theme()))
379-
}
380-
check_length(
381-
position, 2L, call = expr(theme()),
382-
arg = I("A {.cls numeric} {.arg plot.tag.position}")
383-
)
384-
top <- left <- right <- bottom <- FALSE
385-
} else {
386-
# Break position into top/left/right/bottom
387-
position <- arg_match0(
388-
position[1],
389-
c("topleft", "top", "topright", "left",
390-
"right", "bottomleft", "bottom", "bottomright"),
391-
arg_nm = "plot.tag.position",
392-
error_call = expr(theme())
393-
)
394-
top <- position %in% c("topleft", "top", "topright")
395-
left <- position %in% c("topleft", "left", "bottomleft")
396-
right <- position %in% c("topright", "right", "bottomright")
397-
bottom <- position %in% c("bottomleft", "bottom", "bottomright")
398-
}
399-
400-
# Resolve tag and sizes
401-
tag <- element_grob(element, label = label, margin_y = TRUE, margin_x = TRUE)
402-
height <- grobHeight(tag)
403-
width <- grobWidth(tag)
404-
405-
if (location %in% c("plot", "panel")) {
406-
if (!is.numeric(position)) {
407-
hjust <- try_prop(element, "hjust", default = 0.5)
408-
if (right || left) {
409-
x <- (1 - hjust) * width
410-
if (right) {
411-
x <- unit(1, "npc") - x
412-
}
413-
} else {
414-
x <- unit(hjust, "npc")
415-
}
416-
if (top || bottom) {
417-
vjust <- try_prop(element, "vjust", default = 0.5)
418-
y <- (1 - vjust) * height
419-
if (top) {
420-
y <- unit(1, "npc") - y
421-
}
422-
} else {
423-
y <- unit(vjust, "npc")
424-
}
425-
} else {
426-
x <- unit(position[1], "npc")
427-
y <- unit(position[2], "npc")
428-
}
429-
# Re-render with manual positions
430-
tag <- element_grob(
431-
element, x = x, y = y, label = label,
432-
margin_y = TRUE, margin_x = TRUE
433-
)
434-
if (location == "plot") {
435-
table <- gtable_add_grob(
436-
table, tag, name = "tag", clip = "off",
437-
t = 1, b = nrow(table), l = 1, r = ncol(table)
438-
)
439-
return(table)
440-
}
441-
}
442-
443-
if (location == "panel") {
444-
place <- find_panel(table)
445-
} else {
446-
n_col <- ncol(table)
447-
n_row <- nrow(table)
448-
# Actually fill margin with relevant units
449-
if (top) table$heights <- unit.c(height, table$heights[-1])
450-
if (left) table$widths <- unit.c(width, table$widths[-1])
451-
if (right) table$widths <- unit.c(table$widths[-n_col], width)
452-
if (bottom) table$heights <- unit.c(table$heights[-n_row], height)
453-
place <- data_frame0(t = 1L, r = n_col, b = n_row, l = 1L)
454-
}
455-
456-
# Shrink placement to position
457-
if (top) place$b <- place$t
458-
if (left) place$r <- place$l
459-
if (right) place$l <- place$r
460-
if (bottom) place$t <- place$b
461-
462-
gtable_add_grob(
463-
table, tag, name = "tag", clip = "off",
464-
t = place$t, l = place$l, b = place$b, r = place$r
465-
)
466-
}
467-
468-
# Add the legends to the gtable
469-
table_add_legends <- function(table, legends, theme) {
470-
471-
if (is_zero(legends)) {
472-
legends <- rep(list(zeroGrob()), 5)
473-
names(legends) <- c(.trbl, "inside")
474-
}
475-
476-
# Extract sizes
477-
widths <- heights <- set_names(
478-
rep(list(unit(0, "cm")), length(legends)),
479-
names(legends)
480-
)
481-
482-
empty <- vapply(legends, is_zero, logical(1))
483-
widths[!empty] <- lapply(legends[!empty], gtable_width)
484-
heights[!empty] <- lapply(legends[!empty], gtable_height)
485-
spacing <- calc_element("legend.box.spacing", theme) %||% unit(0.2, "cm")
486-
487-
# If legend is missing, set spacing to zero for that legend
488-
zero <- unit(0, "pt")
489-
spacing <- lapply(empty, function(is_empty) if (is_empty) zero else spacing)
490-
491-
location <- switch(
492-
theme$legend.location %||% "panel",
493-
"plot" = plot_extent,
494-
find_panel
495-
)
496-
497-
place <- location(table)
498-
499-
# Add right legend
500-
table <- gtable_add_cols(table, spacing$right, pos = -1)
501-
table <- gtable_add_cols(table, widths$right, pos = -1)
502-
table <- gtable_add_grob(
503-
table, legends$right, clip = "off",
504-
t = place$t, b = place$b, l = -1, r = -1,
505-
name = "guide-box-right"
506-
)
507-
508-
# Add left legend
509-
table <- gtable_add_cols(table, spacing$left, pos = 0)
510-
table <- gtable_add_cols(table, widths$left, pos = 0)
511-
table <- gtable_add_grob(
512-
table, legends$left, clip = "off",
513-
t = place$t, b = place$b, l = 1, r = 1,
514-
name = "guide-box-left"
515-
)
516-
517-
place <- location(table)
518-
519-
# Add bottom legend
520-
table <- gtable_add_rows(table, spacing$bottom, pos = -1)
521-
table <- gtable_add_rows(table, heights$bottom, pos = -1)
522-
table <- gtable_add_grob(
523-
table, legends$bottom, clip = "off",
524-
t = -1, b = -1, l = place$l, r = place$r,
525-
name = "guide-box-bottom"
526-
)
527-
528-
# Add top legend
529-
table <- gtable_add_rows(table, spacing$top, pos = 0)
530-
table <- gtable_add_rows(table, heights$top, pos = 0)
531-
table <- gtable_add_grob(
532-
table, legends$top, clip = "off",
533-
t = 1, b = 1, l = place$l, r = place$r,
534-
name = "guide-box-top"
535-
)
536-
537-
# Add manual legend
538-
place <- find_panel(table)
539-
table <- gtable_add_grob(
540-
table, legends$inside, clip = "off",
541-
t = place$t, b = place$b, l = place$l, r = place$r,
542-
name = "guide-box-inside"
543-
)
544-
545-
table
546-
}
547-
548-
plot_extent <- function(table) {
549-
layout <- table$layout
550-
data_frame0(
551-
t = min(layout[["t"]]),
552-
r = max(layout[["r"]]),
553-
b = max(layout[["b"]]),
554-
l = min(layout[["l"]]),
555-
.size = 1L
556-
)
557-
}

0 commit comments

Comments
 (0)