Skip to content

Commit 3765b97

Browse files
authored
Custom guide (#5496)
* guides are named by their hash * Pumbing for custom guides * draft version * Document stuff * Add topic to pkgdown * prepend grid namespace to example * Adapt to new `Guide$draw()` formals * keep custom guides when there are no scales * sort grobs after drawing * Add news bullet
1 parent 6df5cd4 commit 3765b97

9 files changed

+287
-34
lines changed

DESCRIPTION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ Collate:
181181
'guide-bins.R'
182182
'guide-colorbar.R'
183183
'guide-colorsteps.R'
184+
'guide-custom.R'
184185
'layer.R'
185186
'guide-none.R'
186187
'guide-old.R'

NAMESPACE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ export(GuideAxisLogticks)
217217
export(GuideBins)
218218
export(GuideColourbar)
219219
export(GuideColoursteps)
220+
export(GuideCustom)
220221
export(GuideLegend)
221222
export(GuideNone)
222223
export(GuideOld)
@@ -429,6 +430,7 @@ export(guide_colorbar)
429430
export(guide_colorsteps)
430431
export(guide_colourbar)
431432
export(guide_coloursteps)
433+
export(guide_custom)
432434
export(guide_gengrob)
433435
export(guide_geom)
434436
export(guide_legend)

NEWS.md

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

3+
* New `guide_custom()` function for drawing custom graphical objects (grobs)
4+
unrelated to scales in legend positions (#5416).
5+
36
* `theme()` now supports splicing a list of arguments (#5542).
47

58
* Contour functions will not fail when `options("OutDec")` is not `.` (@eliocamp, #5555).

R/guide-custom.R

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
#' Custom guides
2+
#'
3+
#' This is a special guide that can be used to display any graphical object
4+
#' (grob) along with the regular guides. This guide has no associated scale.
5+
#'
6+
#' @param grob A grob to display.
7+
#' @param width,height The allocated width and height to display the grob, given
8+
#' in [grid::unit()]s.
9+
#' @param title A character string or expression indicating the title of guide.
10+
#' If `NULL` (default), no title is shown.
11+
#' @param title.position A character string indicating the position of a title.
12+
#' One of `"top"` (default), `"bottom"`, `"left"` or `"right"`.
13+
#' @param margin Margins around the guide. See [margin()] for more details. If
14+
#' `NULL` (default), margins are taken from the `legend.margin` theme setting.
15+
#' @param position Currently not in use.
16+
#' @inheritParams guide_legend
17+
#'
18+
#' @export
19+
#'
20+
#' @examples
21+
#' # A standard plot
22+
#' p <- ggplot(mpg, aes(displ, hwy)) +
23+
#' geom_point()
24+
#'
25+
#' # Define a graphical object
26+
#' circle <- grid::circleGrob()
27+
#'
28+
#' # Rendering a grob as a guide
29+
#' p + guides(custom = guide_custom(circle, title = "My circle"))
30+
#'
31+
#' # Controlling the size of the grob defined in relative units
32+
#' p + guides(custom = guide_custom(
33+
#' circle, title = "My circle",
34+
#' width = unit(2, "cm"), height = unit(2, "cm"))
35+
#' )
36+
#'
37+
#' # Size of grobs in absolute units is taken directly without the need to
38+
#' # set these manually
39+
#' p + guides(custom = guide_custom(
40+
#' title = "My circle",
41+
#' grob = grid::circleGrob(r = unit(1, "cm"))
42+
#' ))
43+
guide_custom <- function(
44+
grob, width = grobWidth(grob), height = grobHeight(grob),
45+
title = NULL, title.position = "top", margin = NULL,
46+
position = waiver(), order = 0
47+
) {
48+
check_object(grob, is.grob, "a {.cls grob} object")
49+
check_object(width, is.unit, "a {.cls unit} object")
50+
check_object(height, is.unit, "a {.cls unit} object")
51+
check_object(margin, is.margin, "a {.cls margin} object", allow_null = TRUE)
52+
if (length(width) != 1) {
53+
cli::cli_abort("{.arg width} must be a single {.cls unit}, not a unit vector.")
54+
}
55+
if (length(height) != 1) {
56+
cli::cli_abort("{.arg height} must be a single {.cls unit}, not a unit vector.")
57+
}
58+
title.position <- arg_match0(title.position, .trbl)
59+
60+
new_guide(
61+
grob = grob,
62+
width = width,
63+
height = height,
64+
title = title,
65+
title.position = title.position,
66+
margin = margin,
67+
hash = hash(list(title, grob)), # hash is already known
68+
position = position,
69+
order = order,
70+
available_aes = "any",
71+
super = GuideCustom
72+
)
73+
}
74+
75+
#' @rdname ggplot2-ggproto
76+
#' @format NULL
77+
#' @usage NULL
78+
#' @export
79+
GuideCustom <- ggproto(
80+
"GuideCustom", Guide,
81+
82+
params = c(Guide$params, list(
83+
grob = NULL, width = NULL, height = NULL,
84+
margin = NULL,
85+
title = NULL,
86+
title.position = "top"
87+
)),
88+
89+
hashables = exprs(title, grob),
90+
91+
elements = list(
92+
background = "legend.background",
93+
theme.margin = "legend.margin",
94+
theme.title = "legend.title"
95+
),
96+
97+
train = function(...) {
98+
params
99+
},
100+
101+
transform = function(...) {
102+
params
103+
},
104+
105+
override_elements = function(params, elements, theme) {
106+
elements$title <- elements$theme.title
107+
elements$margin <- params$margin %||% elements$theme.margin
108+
elements
109+
},
110+
111+
draw = function(self, theme, position = NULL, direction = NULL,
112+
params = self$params) {
113+
114+
# Render title
115+
elems <- self$setup_elements(params, self$elements, theme)
116+
elems <- self$override_elements(params, elems, theme)
117+
if (!is.waive(params$title) && !is.null(params$title)) {
118+
title <- self$build_title(params$title, elems, params)
119+
} else {
120+
title <- zeroGrob()
121+
}
122+
title.position <- params$title.position
123+
if (is.zero(title)) {
124+
title.position <- "none"
125+
}
126+
127+
width <- convertWidth(params$width, "cm")
128+
height <- convertHeight(params$height, "cm")
129+
gt <- gtable(widths = width, heights = height)
130+
gt <- gtable_add_grob(gt, params$grob, t = 1, l = 1, clip = "off")
131+
132+
if (params$title.position == "top") {
133+
gt <- gtable_add_rows(gt, elems$margin[1], pos = 0)
134+
gt <- gtable_add_rows(gt, unit(height_cm(title), "cm"), pos = 0)
135+
gt <- gtable_add_grob(gt, title, t = 1, l = 1, name = "title", clip = "off")
136+
} else if (params$title.position == "bottom") {
137+
gt <- gtable_add_rows(gt, elems$margin[3], pos = -1)
138+
gt <- gtable_add_rows(gt, unit(height_cm(title), "cm"), pos = -1)
139+
gt <- gtable_add_grob(gt, title, t = -1, l = 1, name = "title", clip = "off")
140+
} else if (params$title.position == "left") {
141+
gt <- gtable_add_cols(gt, elems$margin[4], pos = 0)
142+
gt <- gtable_add_cols(gt, unit(width_cm(title), "cm"), pos = 0)
143+
gt <- gtable_add_grob(gt, title, t = 1, l = 1, name = "title", clip = "off")
144+
} else if (params$title.position == "right") {
145+
gt <- gtable_add_cols(gt, elems$margin[2], pos = -1)
146+
gt <- gtable_add_cols(gt, unit(width_cm(title), "cm"), pos = 0)
147+
gt <- gtable_add_grob(gt, title, t = 1, l = -1, name = "title", clip = "off")
148+
}
149+
gt <- gtable_add_padding(gt, elems$margin)
150+
151+
background <- element_grob(elems$background)
152+
gt <- gtable_add_grob(
153+
gt, background,
154+
t = 1, l = 1, r = -1, b = -1,
155+
z = -Inf, clip = "off"
156+
)
157+
gt
158+
}
159+
)

R/guides-.R

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,18 @@ Guides <- ggproto(
248248
)
249249
},
250250

251+
get_custom = function(self) {
252+
custom <- vapply(self$guides, inherits, logical(1), what = "GuideCustom")
253+
n_custom <- sum(custom)
254+
if (n_custom < 1) {
255+
return(guides_list())
256+
}
257+
custom <- guides_list(self$guides[custom])
258+
custom$params <- lapply(custom$guides, `[[`, "params")
259+
custom$merge()
260+
custom
261+
},
262+
251263
## Building ------------------------------------------------------------------
252264

253265
# The `Guides$build()` method is called in ggplotGrob (plot-build.R) and makes
@@ -281,7 +293,8 @@ Guides <- ggproto(
281293
build = function(self, scales, layers, labels, layer_data) {
282294

283295
# Empty guides list
284-
no_guides <- guides_list()
296+
custom <- self$get_custom()
297+
no_guides <- custom
285298

286299
# Extract the non-position scales
287300
scales <- scales$non_position_scales()$scales
@@ -308,6 +321,10 @@ Guides <- ggproto(
308321
if (length(guides$guides) == 0) {
309322
return(no_guides)
310323
}
324+
325+
guides$guides <- c(guides$guides, custom$guides)
326+
guides$params <- c(guides$params, custom$params)
327+
311328
guides
312329
},
313330

@@ -413,22 +430,23 @@ Guides <- ggproto(
413430
# Bundle together guides and their parameters
414431
pairs <- Map(list, guide = self$guides, params = self$params)
415432

416-
# If there is only one guide, we can exit early, because nothing to merge
417-
if (length(pairs) == 1) {
418-
return()
419-
}
420-
421433
# The `{order}_{hash}` combination determines groups of guides
422434
orders <- vapply(self$params, `[[`, 0, "order")
423435
orders[orders == 0] <- 99
424436
orders <- sprintf("%02d", orders)
425437
hashes <- vapply(self$params, `[[`, "", "hash")
426438
hashes <- paste(orders, hashes, sep = "_")
427439

440+
# If there is only one guide, we can exit early, because nothing to merge
441+
if (length(pairs) == 1) {
442+
names(self$guides) <- hashes
443+
return()
444+
}
445+
428446
# Split by hashes
429447
indices <- split(seq_along(pairs), hashes)
430448
indices <- vapply(indices, `[[`, 0L, 1L, USE.NAMES = FALSE) # First index
431-
groups <- unname(split(pairs, hashes))
449+
groups <- split(pairs, hashes)
432450
lens <- lengths(groups)
433451

434452
# Merge groups with >1 member
@@ -495,6 +513,7 @@ Guides <- ggproto(
495513
if (length(grobs) < 1) {
496514
return(zeroGrob())
497515
}
516+
grobs <- grobs[order(names(grobs))]
498517

499518
# Set spacing
500519
theme$legend.spacing <- theme$legend.spacing %||% unit(0.5, "lines")

R/plot-build.R

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ ggplot_build.ggplot <- function(plot) {
9494
plot$guides <- plot$guides$build(npscales, plot$layers, plot$labels, data)
9595
data <- lapply(data, npscales$map_df)
9696
} else {
97-
# Assign empty guides if there are no non-position scales
98-
plot$guides <- guides_list()
97+
# Only keep custom guides if there are no non-position scales
98+
plot$guides <- plot$guides$get_custom()
9999
}
100100

101101
# Fill in defaults etc.

_pkgdown.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ reference:
130130
- guide_axis_theta
131131
- guide_bins
132132
- guide_coloursteps
133+
- guide_custom
133134
- guide_none
134135
- guides
135136
- sec_axis

man/ggplot2-ggproto.Rd

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

0 commit comments

Comments
 (0)