Skip to content

Commit 466344a

Browse files
authored
Miscellaneous improvements to guides (#5345)
* Handle `labels = NULL` better * Convert `guides()` error to warning * Ignore no guides * Swap old train order * Fix `even.steps`/`show.limits` interaction * Change to soft deprecation * Fix old guide title * Fix `draw_axis()` with `NULL` labels * Default old guide title is `waiver()` * `guide_for_position` becomes <Guides> method * GuideColoursteps is a named class * `guide_colourbar()` rejects discrete scales * Fix test TODO * Use `vec_slice()` to preserve attributes * Document extension points * Handle hashing in `train()`
1 parent cd7199d commit 466344a

File tree

11 files changed

+256
-83
lines changed

11 files changed

+256
-83
lines changed

R/coord-cartesian-.R

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -147,37 +147,9 @@ view_scales_from_scale <- function(scale, coord_limits = NULL, expand = TRUE) {
147147
}
148148

149149
panel_guides_grob <- function(guides, position, theme) {
150-
pair <- guide_for_position(guides, position) %||%
151-
list(guide = guide_none(), params = NULL)
152-
pair$guide$draw(theme, pair$params)
153-
}
154-
155-
guide_for_position <- function(guides, position) {
156-
params <- guides$params
157-
has_position <- vapply(
158-
params, function(p) identical(p$position, position), logical(1)
159-
)
160-
if (!any(has_position)) {
161-
return(NULL)
162-
}
163-
164-
# Subset guides and parameters
165-
guides <- guides$get_guide(has_position)
166-
params <- params[has_position]
167-
# Pair up guides with parameters
168-
pairs <- Map(list, guide = guides, params = params)
169-
170-
# Early exit, nothing to merge
171-
if (length(pairs) == 1) {
172-
return(pairs[[1]])
150+
if (!inherits(guides, "Guides")) {
151+
return(zeroGrob())
173152
}
174-
175-
# TODO: There must be a smarter way to merge these
176-
order <- order(vapply(params, function(p) as.numeric(p$order), numeric(1)))
177-
Reduce(
178-
function(old, new) {
179-
old$guide$merge(old$params, new$guide, new$params)
180-
},
181-
pairs[order]
182-
)
153+
pair <- guides$get_position(position)
154+
pair$guide$draw(theme, pair$params)
183155
}

R/guide-.R

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,79 @@ new_guide <- function(..., available_aes = "any", super) {
7474
#' To create a new type of Guide object, you typically will want to override
7575
#' one or more of the following:
7676
#'
77-
#' TODO: Fill this in properly
77+
#' Properties:
78+
#'
79+
#' - `available_aes` A `character` vector with aesthetics that this guide
80+
#' supports. The value `"any"` indicates all non-position aesthetics.
81+
#'
82+
#' - `params` A named `list` of parameters that the guide needs to function.
83+
#' It has the following roles:
84+
#'
85+
#' - `params` provides the defaults for a guide.
86+
#' - `names(params)` determines what are valid arguments to `new_guide()`.
87+
#' Some parameters are *required* to render the guide. These are: `title`,
88+
#' `name`, `position`, `direction`, `order` and `hash`.
89+
#' - During build stages, `params` holds information about the guide.
90+
#'
91+
#' - `elements` A named list of `character`s, giving the name of theme elements
92+
#' that should be retrieved automatically, for example `"legend.text"`.
93+
#'
94+
#' - `hashables` An `expression` that can be evaluated in the context of
95+
#' `params`. The hash of the evaluated expression determines the merge
96+
#' compatibility of guides, and is stored in `params$hash`.
97+
#'
98+
#' Methods:
99+
#'
100+
#' - `extract_key()` Returns a `data.frame` with (mapped) breaks and labels
101+
#' extracted from the scale, which will be stored in `params$key`.
102+
#'
103+
#' - `extract_decor()` Returns a `data.frame` containing other structured
104+
#' information extracted from the scale, which will be stored in
105+
#' `params$decor`. The `decor` has a guide-specific meaning: it is the bar in
106+
#' `guide_colourbar()`, but specifies the `axis.line` in `guide_axis()`.
107+
#'
108+
#' - `extract_params()` Updates the `params` with other, unstructured
109+
#' information from the scale. An example of this is inheriting the guide's
110+
#' title from the `scale$name` field.
111+
#'
112+
#' - `transform()` Updates the `params$key` based on the coordinates. This
113+
#' applies to position guides, as it rescales the aesthetic to the \[0, 1\]
114+
#' range.
115+
#'
116+
#' - `merge()` Combines information from multiple guides with the same
117+
#' `params$hash`. This ensures that e.g. `guide_legend()` can display both
118+
#' `shape` and `colour` in the same guide.
119+
#'
120+
#' - `get_layer_key()` Extract information from layers. This can be used to
121+
#' check that the guide's aesthetic is actually in use, or to gather
122+
#' information about how legend keys should be displayed.
123+
#'
124+
#' - `setup_params()` Set up parameters at the beginning of drawing stages.
125+
#' It can be used to overrule user-supplied parameters or perform checks on
126+
#' the `params` property.
127+
#'
128+
#' - `override_elements()` Take populated theme elements derived from the
129+
#' `elements` property and allows overriding these theme settings.
130+
#'
131+
#' - `build_title()` Render the guide's title.
132+
#'
133+
#' - `build_labels()` Render the guide's labels.
134+
#'
135+
#' - `build_decor()` Render the `params$decor`, which is different for every
136+
#' guide.
137+
#'
138+
#' - `build_ticks()` Render tick marks.
139+
#'
140+
#' - `measure_grobs()` Measure dimensions of the graphical objects produced
141+
#' by the `build_*()` methods to be used in the layout or assembly.
142+
#'
143+
#' - `arrange_layout()` Set up a layout for how graphical objects produced by
144+
#' the `build_*()` methods should be arranged.
145+
#'
146+
#' - `assemble_drawing()` Take the graphical objects produced by the `build_*()`
147+
#' methods, the measurements from `measure_grobs()` and layout from
148+
#' `arrange_layout()` to finalise the guide.
149+
#'
78150
#' @rdname ggplot2-ggproto
79151
#' @format NULL
80152
#' @usage NULL
@@ -117,14 +189,15 @@ Guide <- ggproto(
117189
return(NULL)
118190
}
119191
params$decor <- inject(self$extract_decor(scale, !!!params))
120-
self$extract_params(scale, params, self$hashables, ...)
192+
params <- self$extract_params(scale, params, ...)
193+
# Make hash
194+
# TODO: Maybe we only need the hash on demand during merging?
195+
params$hash <- hash(lapply(unname(self$hashables), eval_tidy, data = params))
196+
params
121197
},
122198

123199
# Setup parameters that are only available after training
124-
# TODO: Maybe we only need the hash on demand during merging?
125-
extract_params = function(scale, params, hashables, ...) {
126-
# Make hash
127-
params$hash <- hash(lapply(unname(hashables), eval_tidy, data = params))
200+
extract_params = function(scale, params, ...) {
128201
params
129202
},
130203

@@ -137,13 +210,18 @@ Guide <- ggproto(
137210

138211
mapped <- scale$map(breaks)
139212
labels <- scale$get_labels(breaks)
213+
# {vctrs} doesn't play nice with expressions, convert to list.
214+
# see also https://github.com/r-lib/vctrs/issues/559
215+
if (is.expression(labels)) {
216+
labels <- as.list(labels)
217+
}
140218

141219
key <- data_frame(mapped, .name_repair = ~ aesthetic)
142220
key$.value <- breaks
143221
key$.label <- labels
144222

145223
if (is.numeric(breaks)) {
146-
key[is.finite(breaks), , drop = FALSE]
224+
vec_slice(key, is.finite(breaks))
147225
} else {
148226
key
149227
}
@@ -342,3 +420,14 @@ flip_names = c(
342420
# Shortcut for position argument matching
343421
.trbl <- c("top", "right", "bottom", "left")
344422

423+
# Ensure that labels aren't a list of expressions, but proper expressions
424+
validate_labels <- function(labels) {
425+
if (!is.list(labels)) {
426+
return(labels)
427+
}
428+
if (any(vapply(labels, is.language, logical(1)))) {
429+
do.call(expression, labels)
430+
} else {
431+
unlist(labels)
432+
}
433+
}

R/guide-axis.R

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,9 @@ GuideAxis <- ggproto(
103103
ticks_length = "axis.ticks.length"
104104
),
105105

106-
extract_params = function(scale, params, hashables, ...) {
106+
extract_params = function(scale, params, ...) {
107107
params$name <- paste0(params$name, "_", params$aesthetic)
108-
Guide$extract_params(scale, params, hashables)
108+
params
109109
},
110110

111111
extract_decor = function(scale, aesthetic, position, key, cap = "none", ...) {
@@ -281,22 +281,14 @@ GuideAxis <- ggproto(
281281
},
282282

283283
build_labels = function(key, elements, params) {
284-
labels <- key$.label
284+
labels <- validate_labels(key$.label)
285285
n_labels <- length(labels)
286286

287287
if (n_labels < 1) {
288288
return(list(zeroGrob()))
289289
}
290290

291-
pos <- key[[params$aes]]
292-
293-
if (is.list(labels)) {
294-
if (any(vapply(labels, is.language, logical(1)))) {
295-
labels <- do.call(expression, labels)
296-
} else {
297-
labels <- unlist(labels)
298-
}
299-
}
291+
pos <- key[[params$aes]]
300292

301293
dodge_pos <- rep(seq_len(params$n.dodge %||% 1), length.out = n_labels)
302294
dodge_indices <- unname(split(seq_len(n_labels), dodge_pos))
@@ -432,9 +424,10 @@ draw_axis <- function(break_positions, break_labels, axis_position, theme,
432424
aes <- if (axis_position %in% c("top", "bottom")) "x" else "y"
433425
opp <- setdiff(c("x", "y"), aes)
434426
opp_value <- if (axis_position %in% c("top", "right")) 0 else 1
435-
key <- data_frame(
436-
break_positions, break_positions, break_labels,
437-
.name_repair = ~ c(aes, ".value", ".label")
427+
key <- data_frame0(
428+
!!aes := break_positions,
429+
.value = break_positions,
430+
.label = break_labels
438431
)
439432
params$key <- key
440433
params$decor <- data_frame0(

R/guide-bins.R

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ GuideBins <- ggproto(
266266
return(key)
267267
},
268268

269-
extract_params = function(scale, params, hashables,
269+
extract_params = function(scale, params,
270270
title = waiver(), direction = NULL, ...) {
271271

272272
show.limits <- params$show.limits %||% scale$show.limits %||% FALSE
@@ -320,8 +320,7 @@ GuideBins <- ggproto(
320320
"not {.val {params$label.position}}."
321321
))
322322
}
323-
324-
Guide$extract_params(scale, params, hashables)
323+
params
325324
},
326325

327326
setup_params = function(params) {
@@ -340,7 +339,11 @@ GuideBins <- ggproto(
340339
},
341340

342341
build_labels = function(key, elements, params) {
343-
key$.label[c(1, nrow(key))[!params$show.limits]] <- ""
342+
n_labels <- length(key$.label)
343+
if (n_labels < 1) {
344+
return(list(labels = zeroGrob()))
345+
}
346+
key$.label[c(1, n_labels)[!params$show.limits]] <- ""
344347

345348
just <- if (params$direction == "horizontal") {
346349
elements$text$vjust

R/guide-colorbar.R

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,14 @@ GuideColourbar <- ggproto(
317317
theme.title = "legend.title"
318318
),
319319

320+
extract_key = function(scale, aesthetic, ...) {
321+
if (scale$is_discrete()) {
322+
cli::cli_warn("{.fn guide_colourbar} needs continuous scales.")
323+
return(NULL)
324+
}
325+
Guide$extract_key(scale, aesthetic, ...)
326+
},
327+
320328
extract_decor = function(scale, aesthetic, nbin = 300, reverse = FALSE, ...) {
321329

322330
limits <- scale$get_limits()
@@ -335,7 +343,7 @@ GuideColourbar <- ggproto(
335343
return(bar)
336344
},
337345

338-
extract_params = function(scale, params, hashables,
346+
extract_params = function(scale, params,
339347
title = waiver(), direction = "vertical", ...) {
340348
params$title <- scale$make_title(
341349
params$title %|W|% scale$name %|W|% title
@@ -364,7 +372,7 @@ GuideColourbar <- ggproto(
364372
c(0.5, params$nbin - 0.5) / params$nbin,
365373
limits
366374
)
367-
Guide$extract_params(scale, params, hashables)
375+
params
368376
},
369377

370378
merge = function(self, params, new_guide, new_params) {
@@ -414,6 +422,11 @@ GuideColourbar <- ggproto(
414422
},
415423

416424
build_labels = function(key, elements, params) {
425+
n_labels <- length(key$.label)
426+
if (n_labels < 1) {
427+
return(list(labels = zeroGrob()))
428+
}
429+
417430
just <- if (params$direction == "horizontal") {
418431
elements$text$vjust
419432
} else {
@@ -422,7 +435,7 @@ GuideColourbar <- ggproto(
422435

423436
list(labels = flip_element_grob(
424437
elements$text,
425-
label = key$.label,
438+
label = validate_labels(key$.label),
426439
x = unit(key$.value, "npc"),
427440
y = rep(just, nrow(key)),
428441
margin_x = FALSE,

R/guide-colorsteps.R

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ guide_colorsteps <- guide_coloursteps
7474
#' @usage NULL
7575
#' @export
7676
GuideColoursteps <- ggproto(
77-
NULL, GuideColourbar,
77+
"GuideColoursteps", GuideColourbar,
7878

7979
params = c(
8080
list(even.steps = TRUE, show.limits = NULL),
@@ -135,7 +135,7 @@ GuideColoursteps <- ggproto(
135135
return(bar)
136136
},
137137

138-
extract_params = function(scale, params, hashables, ...) {
138+
extract_params = function(scale, params, ...) {
139139

140140
if (params$even.steps) {
141141
params$nbin <- nbin <- sum(!is.na(params$key[[1]])) + 1
@@ -164,7 +164,7 @@ GuideColoursteps <- ggproto(
164164
from = c(0.5, nbin - 0.5) / nbin
165165
)
166166
key <- params$key
167-
limits <- attr(key, "limits", TRUE)
167+
limits <- attr(key, "limits", TRUE) %||% scale$get_limits()
168168
key <- key[c(NA, seq_len(nrow(key)), NA), , drop = FALSE]
169169
key$.value[c(1, nrow(key))] <- edges
170170
key$.label[c(1, nrow(key))] <- scale$get_labels(limits)
@@ -177,6 +177,6 @@ GuideColoursteps <- ggproto(
177177
params$key <- key
178178
}
179179

180-
GuideColourbar$extract_params(scale, params, hashables, ...)
180+
GuideColourbar$extract_params(scale, params, ...)
181181
}
182182
)

R/guide-legend.R

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ GuideLegend <- ggproto(
259259
theme.title = "legend.title"
260260
),
261261

262-
extract_params = function(scale, params, hashables,
262+
extract_params = function(scale, params,
263263
title = waiver(), direction = NULL, ...) {
264264
params$title <- scale$make_title(
265265
params$title %|W|% scale$name %|W|% title
@@ -271,8 +271,7 @@ GuideLegend <- ggproto(
271271
if (isTRUE(params$reverse %||% FALSE)) {
272272
params$key <- params$key[nrow(params$key):1, , drop = FALSE]
273273
}
274-
275-
Guide$extract_params(scale, params, hashables)
274+
params
276275
},
277276

278277
merge = function(self, params, new_guide, new_params) {
@@ -476,6 +475,11 @@ GuideLegend <- ggproto(
476475
},
477476

478477
build_labels = function(key, elements, params) {
478+
n_labels <- length(key$.label)
479+
if (n_labels < 1) {
480+
out <- rep(list(zeroGrob()), nrow(key))
481+
return(out)
482+
}
479483
lapply(key$.label, function(lab) {
480484
ggname(
481485
"guide.label",

0 commit comments

Comments
 (0)