Nothing
#' @include theme-elements.R
NULL
#' Guide constructor
#'
#' A constructor function for guides, which performs some standard compatibility
#' checks between the guide and provided arguments.
#'
#' @param ... Named arguments that match the parameters of `super$params` or
#' the theme elements in `super$elements`.
#' @param available_aes A vector of character strings listing the aesthetics
#' for which the guide can be drawn.
#' @param super The super class to use for the constructed guide. Should be a
#' Guide class object.
#'
#' @return A `Guide` ggproto object.
#' @keywords internal
#' @export
new_guide <- function(..., available_aes = "any", super) {
pf <- parent.frame()
super <- check_subclass(super, "Guide", env = pf)
args <- list2(...)
# Set parameters
param_names <- names(super$params)
params <- intersect(names(args), param_names)
params <- defaults(args[params], super$params)
# Warn about extra arguments
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 ",
"{.fn {snake_class(super)}}: {.arg {extra_args}}."
))
}
# Stop when some required parameters are missing.
# This should only happen with mis-constructed guides
required_params <- names(Guide$params)
missing_params <- setdiff(required_params, names(params))
if (length(missing_params) > 0) {
cli::cli_abort(paste0(
"The following parameter{?s} {?is/are} required for setting up a guide, ",
"but {?is/are} missing: {.field {missing_params}}"
))
}
# Validate theme settings
if (!is.null(params$theme)) {
check_object(params$theme, is.theme, what = "a {.cls theme} object")
validate_theme(params$theme, call = caller_env())
params$direction <- params$direction %||% params$theme$legend.direction
}
# Ensure 'order' is length 1 integer
params$order <- vec_cast(params$order, 0L, x_arg = "order", call = pf)
vec_assert(params$order, 0L, size = 1L, arg = "order", call = pf)
ggproto(
NULL, super,
params = params,
available_aes = available_aes
)
}
#' @section Guides:
#'
#' The `guide_*()` functions, such as `guide_legend()` return an object that
#' is responsible for displaying how objects in the plotting panel are related
#' to actual values.
#'
#' Each of the `Guide*` object is a [ggproto()] object, descended from the
#' top-level `Guide`, and each implements their own methods for drawing.
#'
#' To create a new type of Guide object, you typically will want to override
#' one or more of the following:
#'
#' Properties:
#'
#' - `available_aes` A `character` vector with aesthetics that this guide
#' supports. The value `"any"` indicates all non-position aesthetics.
#'
#' - `params` A named `list` of parameters that the guide needs to function.
#' It has the following roles:
#'
#' - `params` provides the defaults for a guide.
#' - `names(params)` determines what are valid arguments to `new_guide()`.
#' Some parameters are *required* to render the guide. These are: `title`,
#' `name`, `position`, `direction`, `order` and `hash`.
#' - During build stages, `params` holds information about the guide.
#'
#' - `elements` A named list of `character`s, giving the name of theme elements
#' that should be retrieved automatically, for example `"legend.text"`.
#'
#' - `hashables` An `expression` that can be evaluated in the context of
#' `params`. The hash of the evaluated expression determines the merge
#' compatibility of guides, and is stored in `params$hash`.
#'
#' Methods:
#'
#' - `extract_key()` Returns a `data.frame` with (mapped) breaks and labels
#' extracted from the scale, which will be stored in `params$key`.
#'
#' - `extract_decor()` Returns a `data.frame` containing other structured
#' information extracted from the scale, which will be stored in
#' `params$decor`. The `decor` has a guide-specific meaning: it is the bar in
#' `guide_colourbar()`, but specifies the `axis.line` in `guide_axis()`.
#'
#' - `extract_params()` Updates the `params` with other, unstructured
#' information from the scale. An example of this is inheriting the guide's
#' title from the `scale$name` field.
#'
#' - `transform()` Updates the `params$key` based on the coordinates. This
#' applies to position guides, as it rescales the aesthetic to the \[0, 1\]
#' range.
#'
#' - `merge()` Combines information from multiple guides with the same
#' `params$hash`. This ensures that e.g. `guide_legend()` can display both
#' `shape` and `colour` in the same guide.
#'
#' - `process_layers()` Extract information from layers. This acts mostly
#' as a filter for which layers to include and these are then (typically)
#' forwarded to `get_layer_key()`.
#'
#' - `get_layer_key()` This can be used to gather information about how legend
#' keys should be displayed.
#'
#' - `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 `params` property.
#'
#' - `override_elements()` Take populated theme elements derived from the
#' `elements` property and allows overriding these theme settings.
#'
#' - `build_title()` Render the guide's title.
#'
#' - `build_labels()` Render the guide's labels.
#'
#' - `build_decor()` Render the `params$decor`, which is different for every
#' guide.
#'
#' - `build_ticks()` Render tick marks.
#'
#' - `measure_grobs()` Measure dimensions of the graphical objects produced
#' by the `build_*()` methods to be used in the layout or assembly.
#'
#' - `arrange_layout()` Set up a layout for how graphical objects produced by
#' the `build_*()` methods should be arranged.
#'
#' - `assemble_drawing()` Take the graphical objects produced by the `build_*()`
#' methods, the measurements from `measure_grobs()` and layout from
#' `arrange_layout()` to finalise the guide.
#'
#' - `add_title` Adds the title to a gtable, taking into account the size
#' of the title as well as the gtable size.
#'
#' @rdname ggplot2-ggproto
#' @format NULL
#' @usage NULL
#' @export
Guide <- ggproto(
"Guide",
# `params` is a list of initial parameters that gets updated upon
# construction. After construction, parameters are manged by the
# `GuidesList` class.
params = list(
title = waiver(),
theme = NULL,
name = character(),
position = waiver(),
direction = NULL,
order = 0,
hash = character()
),
# A list of theme elements that should be calculated
elements = list(),
# The aesthetics for which this guide is appropriate
available_aes = character(),
# The `hashables` are the parameters of the guide that are used to generate a
# unique hash that determines whether other guides are compatible.
hashables = exprs(title, name),
# Training has the task of updating parameters based the scale.
# There are 3 sub-tasks:
# 1. Extract a key from the scale
# 2. (Optionally) extract further decoration from the scale (e.g. the
# colour bar).
# 3. Extract further parameters
train = function(self, params = self$params, scale, aesthetic = NULL, ...) {
params$aesthetic <- aesthetic %||% scale$aesthetics[1]
params$key <- inject(self$extract_key(scale, !!!params))
if (is.null(params$key)) {
return(NULL)
}
params$decor <- inject(self$extract_decor(scale, !!!params))
params <- self$extract_params(scale, params, ...)
# Make hash
# TODO: Maybe we only need the hash on demand during merging?
params$hash <- hash(lapply(unname(self$hashables), eval_tidy, data = params))
params
},
# Setup parameters that are only available after training
extract_params = function(scale, params, ...) {
params
},
# Function for generating a `key` data.frame from the scale
extract_key = function(scale, aesthetic, ...) {
breaks <- scale$get_breaks()
if (length(breaks) == 0) {
return(NULL)
}
mapped <- scale$map(breaks)
labels <- scale$get_labels(breaks)
# {vctrs} doesn't play nice with expressions, convert to list.
# see also https://github.com/r-lib/vctrs/issues/559
if (is.expression(labels)) {
labels <- as.list(labels)
}
key <- data_frame(mapped, .name_repair = ~ aesthetic)
key$.value <- breaks
key$.label <- labels
if (is.numeric(breaks)) {
range <- scale$continuous_range %||% scale$get_limits()
key <- vec_slice(key, is.finite(oob_censor_any(breaks, range)))
} else {
key
}
},
# Function for extracting decoration from the scale.
# This is for `guide_colourbar` to extract the bar as well as the key,
# and might be a good extension point.
extract_decor = function(scale, aesthetic, ...) {
return(invisible()) # By default, nothing else needs to be extracted
},
# Function for merging multiple guides.
# Mostly applies to `guide_legend()` and `guide_binned()`.
# Defaults to returning the *other* guide, because this parent class is
# mostly a virtual class and children should implement their own merges.
merge = function(self, params, new_guide, new_params) {
return(list(guide = new_guide, params = new_params))
},
# Function for applying coord-transformation.
# Mostly applied to position guides, such as `guide_axis()`.
transform = function(self, params, coord, ...) {
cli::cli_abort(c(
"{.fn {snake_class(self)}} does not implement a {.fn transform} method.",
"i" = "Did you mean to use {.fn guide_axis}?"
))
},
# Function for extracting information from the layers.
# Mostly applies to `guide_legend()` and `guide_binned()`
process_layers = function(self, params, layers, data = NULL) {
self$get_layer_key(params, layers, data)
},
get_layer_key = function(params, layers, data = NULL) {
return(params)
},
# Called at start of the `draw` method. Typically used to either overrule
# user-specified parameters or populate extra parameters derived from
# the guide's direction or position.
setup_params = function(params) {
params
},
# 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 <- add_theme(theme, params$theme)
is_char <- vapply(elements, is.character, logical(1))
elements[is_char] <- lapply(elements[is_char], calc_element, theme = theme)
elements
},
# Called after `setup_elements` to overrule any element defaults descended
# from the theme.
override_elements = function(params, elements, theme) {
elements
},
# Main drawing function that organises more specialised aspects of guide
# drawing.
draw = function(self, theme, position = NULL, direction = NULL,
params = self$params) {
# Setup parameters
params <- replace_null(params, position = position, direction = direction)
params <- self$setup_params(params)
key <- params$key
# Setup style options
elems <- self$setup_elements(params, self$elements, theme)
elems <- self$override_elements(params, elems, theme)
# Allow early exit when key is empty
if (prod(dim(key)) == 0) {
out <- self$draw_early_exit(params, elems)
return(out)
}
# Build grobs
grobs <- list(
title = self$build_title(params$title, elems, params),
ticks = self$build_ticks(key, elems, params)
)
if (params$draw_label %||% TRUE) {
grobs$labels <- self$build_labels(key, elems, params)
} else {
grobs$labels <- list(zeroGrob())
}
grobs$decor <- self$build_decor(params$decor, grobs, elems, params)
# Arrange and assemble grobs
sizes <- self$measure_grobs(grobs, params, elems)
layout <- self$arrange_layout(key, sizes, params, elems)
self$assemble_drawing(grobs, layout, sizes, params, elems)
},
# Makes measurements of grobs that can be used in the layout or assembly
# stages of guide drawing.
measure_grobs = function(grobs, params, elements) {
return(invisible())
},
# Takes care of where grobs should be added to the output gtable.
arrange_layout = function(key, sizes, params, elements) {
return(invisible())
},
# Combines grobs into a single gtable.
assemble_drawing = function(grobs, layout, sizes, params, elements) {
zeroGrob()
},
# Renders the guide title
build_title = function(label, elements, params) {
ggname(
"guide.title",
element_grob(
elements$title,
label = label,
margin_x = TRUE,
margin_y = TRUE
)
)
},
# Renders the guide labels
# TODO: See if we can generalise label drawing for many guides
build_labels = function(key, elements, params) {
zeroGrob()
},
# Renders 'decor', which can have different meanings for different guides.
# The other grobs are provided, as a colourbar might use the ticks for example
build_decor = function(decor, grobs, elements, params) {
zeroGrob()
},
# Renders tickmarks
build_ticks = function(key, elements, params, position = params$position,
length = elements$ticks_length) {
if (!inherits(elements, "element")) {
elements <- elements$ticks
}
if (!inherits(elements, "element_line")) {
return(zeroGrob())
}
if (!is.list(key)) {
breaks <- key
} else {
breaks <- key[[params$aes]]
}
n_breaks <- length(breaks)
# Early exit if there are no breaks
if (n_breaks < 1) {
return(zeroGrob())
}
tick_len <- rep(length %||% unit(0.2, "npc"), length.out = n_breaks)
# Resolve mark
mark <- unit(rep(breaks, each = 2), "npc")
# Resolve ticks
pos <- unname(c(top = 1, bottom = 0, left = 0, right = 1)[position])
dir <- -2 * pos + 1
pos <- unit(rep(pos, 2 * n_breaks), "npc")
dir <- rep(vec_interleave(dir, 0), n_breaks) * rep(tick_len, each = 2)
tick <- pos + dir
# Build grob
flip_element_grob(
elements,
x = tick, y = mark,
id.lengths = rep(2, n_breaks),
flip = position %in% c("top", "bottom")
)
},
draw_early_exit = function(self, params, elements) {
zeroGrob()
},
add_title = function(gtable, title, position, just) {
if (is.zero(title)) {
return(gtable)
}
title_width_cm <- width_cm(title)
title_height_cm <- height_cm(title)
# Add extra row/col for title
gtable <- switch(
position,
top = gtable_add_rows(gtable, unit(title_height_cm, "cm"), pos = 0),
right = gtable_add_cols(gtable, unit(title_width_cm, "cm"), pos = -1),
bottom = gtable_add_rows(gtable, unit(title_height_cm, "cm"), pos = -1),
left = gtable_add_cols(gtable, unit(title_width_cm, "cm"), pos = 0)
)
# Add title
args <- switch(
position,
top = list(t = 1, l = 1, r = -1, b = 1),
right = list(t = 1, l = -1, r = -1, b = -1),
bottom = list(t = -1, l = 1, r = -1, b = -1),
left = list(t = 1, l = 1, r = 1, b = -1),
)
gtable <- inject(gtable_add_grob(
x = gtable, grobs = title, !!!args, z = -Inf, name = "title", clip = "off"
))
if (position %in% c("top", "bottom")) {
if (any(unitType(gtable$widths) == "null")) {
# Don't need to add extra title size for stretchy legends
return(gtable)
}
table_width <- sum(width_cm(gtable$widths))
extra_width <- max(0, title_width_cm - table_width)
if (extra_width == 0) {
return(gtable)
}
extra_width <- unit((c(1, -1) * just$hjust + c(0, 1)) * extra_width, "cm")
gtable <- gtable_add_cols(gtable, extra_width[1], pos = 0)
gtable <- gtable_add_cols(gtable, extra_width[2], pos = -1)
} else {
if (any(unitType(gtable$heights) == "null")) {
# Don't need to add extra title size for stretchy legends
return(gtable)
}
table_height <- sum(height_cm(gtable$heights))
extra_height <- max(0, title_height_cm - table_height)
if (extra_height == 0) {
return(gtable)
}
extra_height <- unit((c(-1, 1) * just$vjust + c(1, 0)) * extra_height, "cm")
gtable <- gtable_add_rows(gtable, extra_height[1], pos = 0)
gtable <- gtable_add_rows(gtable, extra_height[2], pos = -1)
}
gtable
}
)
# Helper function that may facilitate flipping theme elements by
# swapping x/y related arguments to `element_grob()`
flip_element_grob = function(..., flip = FALSE) {
if (!flip) {
ans <- element_grob(...)
return(ans)
}
args <- list(...)
translate <- names(args) %in% names(flip_names)
names(args)[translate] <- flip_names[names(args)[translate]]
do.call(element_grob, args)
}
# The flippable arguments for `flip_element_grob()`.
flip_names = c(
"x" = "y",
"y" = "x",
"width" = "height",
"height" = "width",
"hjust" = "vjust",
"vjust" = "hjust",
"margin_x" = "margin_y",
"margin_y" = "margin_x"
)
# Shortcut for position argument matching
.trbl <- c("top", "right", "bottom", "left")
opposite_position <- function(position) {
switch(
position,
top = "bottom",
bottom = "top",
left = "right",
right = "left"
)
}
# Ensure that labels aren't a list of expressions, but proper expressions
validate_labels <- function(labels) {
if (!is.list(labels)) {
return(labels)
}
if (any(vapply(labels, is.language, logical(1)))) {
do.call(expression, labels)
} else {
unlist(labels)
}
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.