diff --git a/R/guide-.R b/R/guide-.R index daf026e88a..f3047a450a 100644 --- a/R/guide-.R +++ b/R/guide-.R @@ -25,13 +25,8 @@ new_guide <- function(..., available_aes = "any", super) { params <- intersect(names(args), param_names) params <- defaults(args[params], super$params) - # Set elements - elems_names <- names(super$elements) - elems <- intersect(names(args), elems_names) - elems <- defaults(args[elems], super$elements) - # Warn about extra arguments - extra_args <- setdiff(names(args), union(param_names, elems_names)) + extra_args <- setdiff(names(args), param_names) if (length(extra_args) > 0) { cli::cli_warn(paste0( "Ignoring unknown {cli::qty(extra_args)} argument{?s} to ", @@ -56,8 +51,7 @@ new_guide <- function(..., available_aes = "any", super) { ggproto( NULL, super, - params = params, - elements = elems, + params = params, available_aes = available_aes ) } @@ -128,6 +122,11 @@ new_guide <- function(..., available_aes = "any", super) { #' It can be used to overrule user-supplied parameters or perform checks on #' the `params` property. #' +#' - `setup_elements()` Used to extract elements from the theme. The base +#' `Guide` uses this method to merge a potential `internal_theme` with the +#' global theme and to calculate the elements declared in `Guide$elements`. +#' For other guides, this is a good place to intervene in theme inheritance. +#' #' - `override_elements()` Take populated theme elements derived from the #' `elements` property and allows overriding these theme settings. #' @@ -274,6 +273,7 @@ Guide <- ggproto( # Converts the `elements` field to proper elements to be accepted by # `element_grob()`. String-interpolates aesthetic/position dependent elements. setup_elements = function(params, elements, theme) { + theme <- merge_internal_theme(theme, params$internal_theme) is_char <- vapply(elements, is.character, logical(1)) elements[is_char] <- lapply(elements[is_char], calc_element, theme = theme) elements @@ -457,3 +457,53 @@ validate_labels <- function(labels) { unlist(labels) } } + +# This logic is similar to `add_theme()` with the following exceptions: +# +# 1. Elements in `new` that have `inherit.blank = TRUE` and are blank in `old` +# will remain blank. +# 2. `NULL` elements in `new` are ignored. +# 3. When an `old` element is a subclass of the `new` element, that subclass +# is preserved. +merge_internal_theme <- function(old, new, new_name = caller_arg(new)) { + if (length(new) == 0) { + return(old) + } + # Get non empty names of new theme + nms <- names(new)[!vapply(new, is.null, logical(1))] + + # Does any of the new theme elements carry over blank elements? + inherit_blank <- vapply( + new[nms], FUN.VALUE = logical(1), + function(x) is.list(x) && isTRUE(x$inherit.blank) + ) + # Are their equivalents in the old theme blank? + is_blank <- vapply(old[nms], inherits, logical(1), what = "element_blank") + + # Only merge in elements that shouldn't become blank + keep <- nms[!(inherit_blank & is_blank)] + + try_fetch( + for (item in keep) { + x <- merge_subclass(new[[item]], old[[item]]) + old[item] <- list(x) + }, + error = function(cnd) { + cli::cli_abort( + "Problem merging the {.var {item}} theme element.", + parent = cnd + ) + } + ) + old +} + +merge_subclass <- function(new, old) { + if (!is.subclass(old, new)) { + return(merge_element(new, old)) + } + idx <- !vapply(new, is.null, logical(1)) + idx <- names(new)[idx] + old[idx] <- new[idx] + old +} diff --git a/R/guide-bins.R b/R/guide-bins.R index 1f2228c8c3..71a938cf8e 100644 --- a/R/guide-bins.R +++ b/R/guide-bins.R @@ -108,16 +108,6 @@ guide_bins <- function( show.limits = NULL, ... ) { - - if (!(is.null(keywidth) || is.unit(keywidth))) { - keywidth <- unit(keywidth, default.unit) - } - if (!(is.null(keyheight) || is.unit(keyheight))) { - keyheight <- unit(keyheight, default.unit) - } - if (!is.unit(ticks.length)) { - ticks.length <- unit(ticks.length, default.unit) - } if (!is.null(title.position)) { title.position <- arg_match0(title.position, .trbl) } @@ -127,13 +117,12 @@ guide_bins <- function( if (!is.null(label.position)) { label.position <- arg_match0(label.position, .trbl) } - if (is.logical(axis)) { - axis <- if (axis) element_line() else element_rect() + axis <- if (!isFALSE(axis)) element_line() else element_blank() } if (inherits(axis, "element_line")) { - axis$colour <- axis.colour %||% axis$colour %||% "black" - axis$linewidth <- axis.linewidth %||% axis$linewidth %||% (0.5 / .pt) + axis$colour <- axis.colour %||% axis$colour %||% "black" + axis$linewidth <- axis.linewidth %||% axis$linewidth %||% (0.5 / .pt) axis$arrow <- axis.arrow %||% axis$arrow } else { axis <- element_blank() @@ -144,29 +133,34 @@ guide_bins <- function( ticks$arrow <- NULL } + label.theme <- if (!isFALSE(label)) label.theme else element_blank() + label.theme <- combine_elements( + label.theme, + element_text(hjust = label.hjust, vjust = label.vjust, inherit.blank = TRUE) + ) + title.theme <- combine_elements( + title.theme, + element_text(hjust = title.hjust, vjust = title.vjust, inherit.blank = TRUE) + ) + + internal_theme <- theme( + legend.text = label.theme, + legend.title = title.theme, + legend.key.width = set_default_unit(keywidth, default.unit), + legend.key.height = set_default_unit(keyheight, default.unit), + axis_line = axis, + ticks = ticks, + ticks_length = set_default_unit(ticks.length, default.unit) + ) + new_guide( # title title = title, title.position = title.position, - title.theme = title.theme, - title.hjust = title.hjust, - title.vjust = title.vjust, - - # label - label = label, label.position = label.position, - label.theme = label.theme, - label.hjust = label.hjust, - label.vjust = label.vjust, - # key - keywidth = keywidth, - keyheight = keyheight, - - # ticks - line = axis, - ticks = ticks, - ticks_length = ticks.length, + # theme + internal_theme = internal_theme, # general direction = direction, @@ -193,20 +187,10 @@ GuideBins <- ggproto( params = list( title = waiver(), title.position = NULL, - title.theme = NULL, - title.hjust = NULL, - title.vjust = NULL, - - label = TRUE, label.position = NULL, - label.theme = NULL, - label.hjust = NULL, - label.vjust = NULL, - - keywidth = NULL, - keyheight = NULL, + internal_theme = NULL, - direction = NULL, + # direction = NULL, override.aes = list(), reverse = FALSE, order = 0, @@ -221,8 +205,8 @@ GuideBins <- ggproto( elements = c( GuideLegend$elements, list( - line = "line", - ticks = "line", + axis_line = "line", + ticks = "line", ticks_length = unit(0.2, "npc") ) ), @@ -329,8 +313,9 @@ GuideBins <- ggproto( }, override_elements = function(params, elements, theme) { - elements$ticks <- combine_elements(elements$ticks, theme$line) - elements$line <- combine_elements(elements$line, theme$line) + itheme <- params$internal_theme + elements$ticks <- combine_elements(itheme$ticks, elements$ticks) + elements$line <- combine_elements(itheme$axis_line, elements$axis_line) GuideLegend$override_elements(params, elements, theme) }, diff --git a/R/guide-colorbar.R b/R/guide-colorbar.R index 265d6bec61..0f6a7f89aa 100644 --- a/R/guide-colorbar.R +++ b/R/guide-colorbar.R @@ -158,16 +158,6 @@ guide_colourbar <- function( available_aes = c("colour", "color", "fill"), ... ) { - if (!(is.null(barwidth) || is.unit(barwidth))) { - barwidth <- unit(barwidth, default.unit) - } - if (!(is.null(barheight) || is.unit(barheight))) { - barheight <- unit(barheight, default.unit) - } - if (!is.unit(ticks.length)) { - ticks.length <- unit(ticks.length, default.unit) - } - if (!is.null(title.position)) { title.position <- arg_match0(title.position, .trbl) } @@ -205,6 +195,26 @@ guide_colourbar <- function( ticks$linewidth <- ticks.linewidth %||% ticks$linewidth %||% (0.5 / .pt) } + label.theme <- if (!isFALSE(label)) label.theme else element_blank() + label.theme <- combine_elements( + label.theme, + element_text(hjust = label.hjust, vjust = label.vjust) + ) + title.theme <- combine_elements( + title.theme, + element_text(hjust = title.hjust, vjust = title.vjust) + ) + + internal_theme <- theme( + legend.text = label.theme, + legend.title = title.theme, + legend.key.width = set_default_unit(barwidth, default.unit), + legend.key.height = set_default_unit(barheight, default.unit), + frame = frame, + ticks = ticks, + ticks.length = set_default_unit(ticks.length, default.unit) + ) + # Trick to re-use this constructor in `guide_coloursteps()`. args <- list2(...) super <- args$super %||% GuideColourbar @@ -214,29 +224,14 @@ guide_colourbar <- function( # title title = title, title.position = title.position, - title.theme = title.theme, - title.hjust = title.hjust, - title.vjust = title.vjust, - - # label - label = label, label.position = label.position, - label.theme = label.theme, - label.hjust = label.hjust, - label.vjust = label.vjust, + + # theme + internal_theme = internal_theme, # bar - keywidth = barwidth, - keyheight = barheight, nbin = nbin, raster = raster, - - # frame - frame = frame, - - # ticks - ticks = ticks, - ticks_length = ticks.length, draw_lim = c(isTRUE(draw.llim), isTRUE(draw.ulim)), # general @@ -267,20 +262,10 @@ GuideColourbar <- ggproto( # title title = waiver(), title.position = NULL, - title.theme = NULL, - title.hjust = NULL, - title.vjust = NULL, - - # label - label = TRUE, label.position = NULL, - label.theme = NULL, - label.hjust = NULL, - label.vjust = NULL, # bar - keywidth = NULL, - keyheight = NULL, + internal_theme = NULL, nbin = 300, raster = TRUE, @@ -311,7 +296,7 @@ GuideColourbar <- ggproto( key.height = "legend.key.height", key.width = "legend.key.width", text = "legend.text", - theme.title = "legend.title" + title = "legend.title" ), extract_key = function(scale, aesthetic, ...) { @@ -392,15 +377,20 @@ GuideColourbar <- ggproto( params }, - override_elements = function(params, elements, theme) { - # These key sizes are the defaults, the GuideLegend method may overrule this + setup_elements = function(params, elements, theme) { + # Key sizes are already calculated before `Guides$draw()` if (params$direction == "horizontal") { - elements$key.width <- elements$key.width * 5 + theme$legend.key.width <- theme$legend.key.width * 5 } else { - elements$key.height <- elements$key.height * 5 + theme$legend.key.height <- theme$legend.key.height * 5 } - elements$ticks <- combine_elements(elements$ticks, theme$line) - elements$frame <- combine_elements(elements$frame, theme$rect) + GuideLegend$setup_elements(params, elements, theme) + }, + + override_elements = function(params, elements, theme) { + itheme <- params$internal_theme + elements$ticks <- combine_elements(itheme$ticks, elements$ticks) + elements$frame <- combine_elements(itheme$frame, elements$frame) GuideLegend$override_elements(params, elements, theme) }, diff --git a/R/guide-legend.R b/R/guide-legend.R index 3a0d2bb8a7..65f0e380f8 100644 --- a/R/guide-legend.R +++ b/R/guide-legend.R @@ -32,8 +32,7 @@ #' object of [element_text()] is expected. By default, the theme is #' specified by `legend.text` in [theme()]. #' @param label.hjust A numeric specifying horizontal justification of the -#' label text. The default for standard text is 0 (left-aligned) and 1 -#' (right-aligned) for expressions. +#' label text. #' @param label.vjust A numeric specifying vertical justification of the label #' text. #' @param keywidth A numeric or a [grid::unit()] object specifying @@ -163,49 +162,43 @@ guide_legend <- function( ... ) { # Resolve key sizes - if (!(is.null(keywidth) || is.unit(keywidth))) { - keywidth <- unit(keywidth, default.unit) - } - if (!(is.null(keyheight) || is.unit(keyheight))) { - keyheight <- unit(keyheight, default.unit) - } - - # Resolve spacing - key.spacing.x <- key.spacing.x %||% key.spacing - if (!is.null(key.spacing.x) || is.unit(key.spacing.x)) { - key.spacing.x <- unit(key.spacing.x, default.unit) - } - key.spacing.y <- key.spacing.y %||% key.spacing - if (!is.null(key.spacing.y) || is.unit(key.spacing.y)) { - key.spacing.y <- unit(key.spacing.y, default.unit) - } - - if (!is.null(title.position)) { title.position <- arg_match0(title.position, .trbl) } if (!is.null(label.position)) { label.position <- arg_match0(label.position, .trbl) } + label.theme <- if (!isFALSE(label)) label.theme else element_blank() + label.theme <- combine_elements( + label.theme, + element_text(hjust = label.hjust, vjust = label.vjust, inherit.blank = TRUE) + ) + title.theme <- combine_elements( + title.theme, + element_text(hjust = title.hjust, vjust = label.vjust, inherit.blank = TRUE) + ) + + internal_theme <- theme( + legend.text = label.theme, + legend.title = title.theme, + legend.key.width = set_default_unit(keywidth, default.unit), + legend.key.height = set_default_unit(keyheight, default.unit), + ) + + # Resolve spacing + key.spacing.x <- set_default_unit(key.spacing.x %||% key.spacing, default.unit) + key.spacing.y <- set_default_unit(key.spacing.y %||% key.spacing, default.unit) new_guide( # Title title = title, title.position = title.position, - title.theme = title.theme, - title.hjust = title.hjust, - title.vjust = title.vjust, # Label - label = label, label.position = label.position, - label.theme = label.theme, - label.hjust = label.hjust, - label.vjust = label.vjust, - # Key size - keywidth = keywidth, - keyheight = keyheight, + # Theme + internal_theme = internal_theme, key.spacing.x = key.spacing.x, key.spacing.y = key.spacing.y, @@ -235,18 +228,8 @@ GuideLegend <- ggproto( params = list( title = waiver(), title.position = NULL, - title.theme = NULL, - title.hjust = NULL, - title.vjust = NULL, - - label = TRUE, label.position = NULL, - label.theme = NULL, - label.hjust = NULL, - label.vjust = NULL, - - keywidth = NULL, - keyheight = NULL, + internal_theme = NULL, key.spacing.x = NULL, key.spacing.y = NULL, @@ -275,7 +258,7 @@ GuideLegend <- ggproto( key.height = "legend.key.height", key.width = "legend.key.width", text = "legend.text", - theme.title = "legend.title" + title = "legend.title" ), extract_params = function(scale, params, @@ -410,52 +393,31 @@ GuideLegend <- ggproto( params }, - override_elements = function(params, elements, theme) { + setup_elements = function(params, elements, theme) { - # Title - title <- combine_elements(params$title.theme, elements$theme.title) - title$hjust <- params$title.hjust %||% title$hjust %||% 0 - title$vjust <- params$title.vjust %||% title$vjust %||% 0.5 - elements$title <- title - - # Labels - if (!is.null(elements$text)) { - label <- combine_elements(params$label.theme, elements$text) - if (!params$label || is.null(params$key$.label)) { - label <- element_blank() - } else { - hjust <- unname(label_hjust_defaults[params$label.position]) - vjust <- unname(label_vjust_defaults[params$label.position]) - # Expressions default to right-justified - if (hjust == 0 && any(is.expression(params$key$.label))) { - hjust <- 1 - } - # Breaking justification inheritance for intuition purposes. - if (is.null(params$label.theme$hjust) && - is.null(theme$legend.text$hjust)) { - label$hjust <- NULL - } - if (is.null(params$label.theme$vjust) && - is.null(theme$legend.text$vjust)) { - label$vjust <- NULL - } - label$hjust <- params$label.hjust %||% label$hjust %||% hjust - label$vjust <- params$label.vjust %||% label$vjust %||% vjust - } - elements$text <- label - } + # We break inheritance of text justification for intuition purposes + default_just <- legend_label_just(params$label.position) + theme$legend.text$hjust <- theme$legend.text$hjust %||% default_just[1] + theme$legend.text$vjust <- theme$legend.text$vjust %||% default_just[2] - # Keys - if (any(c("key.width", "key.height") %in% names(elements))) { - elements$key.width <- width_cm( params$keywidth %||% elements$key.width) - elements$key.height <- height_cm(params$keyheight %||% elements$key.height) - } + # We break inheritance of the text margins to add small spacings between + # labels and keys. + # We set the global text's margin to NULL here, so that a NULL in the + # computed elements indicates that no explicit margin has been set in + # neither the guide nor the theme's legend.text/legend.title. + theme$text$margin <- NULL + + Guide$setup_elements(params, elements, theme) + }, + + override_elements = function(params, elements, theme) { + + elements$key.width <- width_cm(elements$key.width) + elements$key.height <- height_cm(elements$key.height) # Spacing - gap <- title$size %||% elements$theme.title$size %||% - elements$text$size %||% 11 + gap <- elements$title$size %||% elements$text$size %||% 11 gap <- unit(gap * 0.5, "pt") - # Should maybe be elements$spacing.{x/y} instead of the theme's spacing? if (params$direction == "vertical") { # For backward compatibility, vertical default is no spacing @@ -471,18 +433,16 @@ GuideLegend <- ggproto( "cm", valueOnly = TRUE ) - # When no explicit margin has been set, either in this guide or in the - # theme, we set a default text margin to leave a small gap in between - # the label and the key. - if (is.null(params$label.theme$margin %||% theme$legend.text$margin) && - !inherits(elements$text, "element_blank")) { - i <- match(params$label.position, .trbl[c(3, 4, 1, 2)]) - elements$text$margin[i] <- elements$text$margin[i] + gap + # When no explicit margin has been set, we set a default text margin + # to leave a small gap in between the label and the key. + margin <- theme$text$margin %||% margin() + if (is.null(elements$text$margin)) { + i <- match(opposite_position(params$label.position), .trbl) # match opposite + elements$text$margin <- replace(margin, i, margin[i] + gap) } - if (is.null(params$title.theme$margin %||% theme$legend.title$margin) && - !inherits(elements$title, "element_blank")) { - i <- match(params$title.position, .trbl[c(3, 4, 1, 2)]) - elements$title$margin[i] <- elements$title$margin[i] + gap + if (is.null(elements$title$margin)) { + i <- match(opposite_position(params$title.position), .trbl) + elements$title$margin <- replace(margin, i, margin[i] + gap) } # Evaluate backgrounds early @@ -773,8 +733,16 @@ GuideLegend <- ggproto( } ) -label_hjust_defaults <- c(top = 0.5, bottom = 0.5, left = 1, right = 0) -label_vjust_defaults <- c(top = 0, bottom = 1, left = 0.5, right = 0.5) +legend_label_just <- function(position) { + default_just <- switch( + position, + top = c(0.5, 0), + bottom = c(0.5, 1), + left = c(1, 0.5), + right = c(0, 0.5), + arg_match0(position, .trbl, arg = "label.position") + ) +} measure_legend_keys <- function(decor, n, dim, byrow = FALSE, default_width = 1, default_height = 1) { diff --git a/R/theme.R b/R/theme.R index e07fd29214..1b36401dc7 100644 --- a/R/theme.R +++ b/R/theme.R @@ -560,7 +560,6 @@ add_theme <- function(t1, t2, t2name, call = caller_env()) { t1 } - #' Calculate the element properties, by inheriting properties from its parents #' #' @param element The name of the theme element to calculate @@ -775,12 +774,12 @@ combine_elements <- function(e1, e2) { # Calculate relative sizes if (is.rel(e1$size)) { - e1$size <- e2$size * unclass(e1$size) + e1$size <- (e2$size %||% rel(1)) * unclass(e1$size) } # Calculate relative linewidth if (is.rel(e1$linewidth)) { - e1$linewidth <- e2$linewidth * unclass(e1$linewidth) + e1$linewidth <- (e2$linewidth %||% rel(1)) * unclass(e1$linewidth) } # If e2 is 'richer' than e1, fill e2 with e1 parameters diff --git a/R/utilities-grid.R b/R/utilities-grid.R index 389dad3eea..72ccfddf33 100644 --- a/R/utilities-grid.R +++ b/R/utilities-grid.R @@ -35,3 +35,10 @@ height_cm <- function(x) { cli::cli_abort("Don't know how to get height of {.cls {class(x)}} object") } } + +set_default_unit <- function(x, default.unit) { + if (is.null(x) || is.unit(x)) { + return(x) + } + unit(x, default.unit) +} diff --git a/man/ggplot2-ggproto.Rd b/man/ggplot2-ggproto.Rd index 04e9780bfe..82fbf21dd0 100644 --- a/man/ggplot2-ggproto.Rd +++ b/man/ggplot2-ggproto.Rd @@ -423,12 +423,18 @@ range. \item \code{merge()} Combines information from multiple guides with the same \code{params$hash}. This ensures that e.g. \code{guide_legend()} can display both \code{shape} and \code{colour} in the same guide. -\item \code{get_layer_key()} Extract information from layers. This can be used to -check that the guide's aesthetic is actually in use, or to gather -information about how legend keys should be displayed. +\item \code{process_layers()} Extract information from layers. This acts mostly +as a filter for which layers to include and these are then (typically) +forwarded to \code{get_layer_key()}. +\item \code{get_layer_key()} This can be used to gather information about how legend +keys should be displayed. \item \code{setup_params()} Set up parameters at the beginning of drawing stages. It can be used to overrule user-supplied parameters or perform checks on the \code{params} property. +\item \code{setup_elements()} Used to extract elements from the theme. The base +\code{Guide} uses this method to merge a potential \code{internal_theme} with the +global theme and to calculate the elements declared in \code{Guide$elements}. +For other guides, this is a good place to intervene in theme inheritance. \item \code{override_elements()} Take populated theme elements derived from the \code{elements} property and allows overriding these theme settings. \item \code{build_title()} Render the guide's title. diff --git a/man/guide_bins.Rd b/man/guide_bins.Rd index 6eeada9598..dcca3c5a33 100644 --- a/man/guide_bins.Rd +++ b/man/guide_bins.Rd @@ -64,8 +64,7 @@ object of \code{\link[=element_text]{element_text()}} is expected. By default, t specified by \code{legend.text} in \code{\link[=theme]{theme()}}.} \item{label.hjust}{A numeric specifying horizontal justification of the -label text. The default for standard text is 0 (left-aligned) and 1 -(right-aligned) for expressions.} +label text.} \item{label.vjust}{A numeric specifying vertical justification of the label text.} diff --git a/man/guide_colourbar.Rd b/man/guide_colourbar.Rd index 7813c12b1c..3ae922e0dc 100644 --- a/man/guide_colourbar.Rd +++ b/man/guide_colourbar.Rd @@ -103,8 +103,7 @@ object of \code{\link[=element_text]{element_text()}} is expected. By default, t specified by \code{legend.text} in \code{\link[=theme]{theme()}}.} \item{label.hjust}{A numeric specifying horizontal justification of the -label text. The default for standard text is 0 (left-aligned) and 1 -(right-aligned) for expressions.} +label text.} \item{label.vjust}{A numeric specifying vertical justification of the label text.} diff --git a/man/guide_coloursteps.Rd b/man/guide_coloursteps.Rd index e97230b6f4..76f611ca79 100644 --- a/man/guide_coloursteps.Rd +++ b/man/guide_coloursteps.Rd @@ -92,8 +92,7 @@ label. One of "top", "bottom" (default for horizontal guide), "left", or object of \code{\link[=element_text]{element_text()}} is expected. By default, the theme is specified by \code{legend.text} in \code{\link[=theme]{theme()}}.} \item{\code{label.hjust}}{A numeric specifying horizontal justification of the -label text. The default for standard text is 0 (left-aligned) and 1 -(right-aligned) for expressions.} +label text.} \item{\code{label.vjust}}{A numeric specifying vertical justification of the label text.} \item{\code{order}}{positive integer less than 99 that specifies the order of diff --git a/man/guide_legend.Rd b/man/guide_legend.Rd index 224de5587a..c41e42ab27 100644 --- a/man/guide_legend.Rd +++ b/man/guide_legend.Rd @@ -63,8 +63,7 @@ object of \code{\link[=element_text]{element_text()}} is expected. By default, t specified by \code{legend.text} in \code{\link[=theme]{theme()}}.} \item{label.hjust}{A numeric specifying horizontal justification of the -label text. The default for standard text is 0 (left-aligned) and 1 -(right-aligned) for expressions.} +label text.} \item{label.vjust}{A numeric specifying vertical justification of the label text.}