Skip to content

Commit 47a0618

Browse files
authored
Add function for registering of new theme elements. (#3678)
* Add functions specifically for registering of new theme elements. Fixes #3677 * remove element tree from theme. * minor fixes * add one more regression test
1 parent d05e437 commit 47a0618

File tree

12 files changed

+222
-179
lines changed

12 files changed

+222
-179
lines changed

NAMESPACE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ export(geom_text)
374374
export(geom_tile)
375375
export(geom_violin)
376376
export(geom_vline)
377+
export(get_element_tree)
377378
export(gg_dep)
378379
export(ggplot)
379380
export(ggplotGrob)
@@ -444,10 +445,12 @@ export(quickplot)
444445
export(quo)
445446
export(quo_name)
446447
export(quos)
448+
export(register_theme_elements)
447449
export(rel)
448450
export(remove_missing)
449451
export(render_axes)
450452
export(render_strips)
453+
export(reset_theme_settings)
451454
export(resolution)
452455
export(scale_alpha)
453456
export(scale_alpha_continuous)

NEWS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@
3535
* `Geom` now gains a `setup_params()` method in line with the other ggproto
3636
classes (@thomasp85, #3509)
3737

38-
* Themes can now modify the theme element tree, via the
39-
`element_tree` argument. This allows extension packages to add functionality that
40-
alters the element tree (@clauswilke, #2540).
38+
* The newly added function `register_theme_elements()` now allows developers
39+
of extension packages to define their own new theme elements and place them
40+
into the ggplot2 element tree (@clauswilke, #2540).
4141

4242
* `element_text()` now issues a warning when vectorized arguments are provided, as in
4343
`colour = c("red", "green", "blue")`. Such use is discouraged and not officially supported

R/theme-current.R

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,6 @@ theme_get <- function() {
7272
#' @param new new theme (a list of theme elements)
7373
#' @export
7474
theme_set <- function(new) {
75-
missing <- setdiff(names(ggplot_global$theme_grey), names(new))
76-
if (length(missing) > 0) {
77-
warn(glue(
78-
"New theme missing the following elements: ",
79-
glue_collapse(missing, ", ", last = " and ")
80-
))
81-
}
82-
8375
old <- ggplot_global$theme_current
8476
ggplot_global$theme_current <- new
8577
invisible(old)
@@ -107,16 +99,11 @@ theme_replace <- function(...) {
10799
# Can't use modifyList here since it works recursively and drops NULLs
108100
e1[names(e2)] <- e2
109101

110-
# Merge element trees if provided
111-
attr(e1, "element_tree") <- defaults(
112-
attr(e2, "element_tree", exact = TRUE),
113-
attr(e1, "element_tree", exact = TRUE)
114-
)
115-
116102
# comment by @clauswilke:
117103
# `complete` and `validate` are currently ignored,
118104
# which means they are taken from e1. Is this correct?
119105
# I'm not sure how `%+replace%` should handle them.
120106

121107
e1
122108
}
109+

R/theme-defaults.r

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -651,9 +651,13 @@ theme_test <- function(base_size = 11, base_family = "",
651651
}
652652

653653
theme_all_null <- function() {
654-
# set all elements in the element tree to NULL
654+
# Set all elements in the element tree to NULL.
655+
656+
# We read from `.element_tree` instead of `ggplot_global$element_tree`
657+
# because we don't want to change our results just because a user
658+
# has defined new theme elements.
655659
elements <- sapply(
656-
names(ggplot_global$element_tree),
660+
names(.element_tree),
657661
function(x) NULL,
658662
simplify = FALSE, USE.NAMES = TRUE
659663
)

R/theme-elements.r

Lines changed: 78 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -266,23 +266,23 @@ element_grob.element_line <- function(element, x = 0:1, y = 0:1,
266266
)
267267
}
268268

269-
270-
271-
#' Define new elements for a theme's element tree
269+
#' Define and register new theme elements
272270
#'
273-
#' Each theme has an element tree that defines which theme elements inherit
274-
#' theme parameters from which other elements. The function `el_def()` can be used
275-
#' to define new or modified elements for this tree.
271+
#' The underlying structure of a ggplot2 theme is defined via the element tree, which
272+
#' specifies for each theme element what type it should have and whether it inherits from
273+
#' a parent element. In some use cases, it may be necessary to modify or extend this
274+
#' element tree and provide default settings for newly defined theme elements.
276275
#'
277-
#' @param class The name of the element class. Examples are "element_line" or
278-
#' "element_text" or "unit", or one of the two reserved keywords "character" or
279-
#' "margin". The reserved keyword "character" implies a character
280-
#' or numeric vector, not a class called "character". The keyword
281-
#' "margin" implies a unit vector of length 4, as created by [margin()].
282-
#' @param inherit A vector of strings, naming the elements that this
283-
#' element inherits from.
284-
#' @param description An optional character vector providing a description
285-
#' for the element.
276+
#' The function `register_theme_elements()` provides the option to globally register new
277+
#' theme elements with ggplot2. In general, for each new theme element both an element
278+
#' definition and a corresponding entry in the element tree should be provided. See
279+
#' examples for details. For extension package that use this functionality, it is
280+
#' recommended to call `register_theme_elements()` from the `.onLoad()` function.
281+
#' @param ... Element specifications
282+
#' @param element_tree Addition of or modification to the element tree, which specifies the
283+
#' inheritance relationship of the theme elements. The element tree must be provided as
284+
#' a list of named element definitions created with el_def().
285+
#' @param complete If `TRUE` (the default), elements are set to inherit from blank elements.
286286
#' @examples
287287
#' # define a new coord that includes a panel annotation
288288
#' coord_annotate <- function(label = "panel annotation") {
@@ -297,9 +297,8 @@ element_grob.element_line <- function(element, x = 0:1, y = 0:1,
297297
#' )
298298
#' }
299299
#'
300-
#' # update the default theme by adding a new `panel.annotation`
301-
#' # theme element
302-
#' old <- theme_update(
300+
#' # register a new theme element `panel.annotation`
301+
#' register_theme_elements(
303302
#' panel.annotation = element_text(color = "blue", hjust = 0.95, vjust = 0.05),
304303
#' element_tree = list(panel.annotation = el_def("element_text", "text"))
305304
#' )
@@ -309,18 +308,74 @@ element_grob.element_line <- function(element, x = 0:1, y = 0:1,
309308
#' geom_point() +
310309
#' coord_annotate("annotation in blue")
311310
#'
312-
#' # revert to original default theme
313-
#' theme_set(old)
311+
#' # revert to original ggplot2 settings
312+
#' reset_theme_settings()
313+
#' @keywords internal
314+
#' @export
315+
register_theme_elements <- function(..., element_tree = NULL, complete = TRUE) {
316+
old <- ggplot_global$theme_default
317+
t <- theme(..., complete = complete)
318+
ggplot_global$theme_default <- ggplot_global$theme_default %+replace% t
319+
320+
# Merge element trees
321+
ggplot_global$element_tree <- defaults(element_tree, ggplot_global$element_tree)
322+
323+
invisible(old)
324+
}
325+
326+
#' @rdname register_theme_elements
327+
#' @details
328+
#' The function `reset_theme_settings()` restores the default element tree, discards
329+
#' all new element definitions, and (unless turned off) resets the currently active
330+
#' theme to the default.
331+
#' @param reset_current If `TRUE` (the default), the currently active theme is
332+
#' reset to the default theme.
333+
#' @keywords internal
334+
#' @export
335+
reset_theme_settings <- function(reset_current = TRUE) {
336+
ggplot_global$element_tree <- .element_tree
337+
338+
# reset the underlying fallback default theme
339+
ggplot_global$theme_default <- theme_grey()
340+
341+
if (isTRUE(reset_current)) {
342+
# reset the currently active theme
343+
ggplot_global$theme_current <- ggplot_global$theme_default
344+
}
345+
}
346+
347+
#' @rdname register_theme_elements
348+
#' @details
349+
#' The function `get_element_tree()` returns the currently active element tree.
350+
#' @keywords internal
351+
#' @export
352+
get_element_tree <- function() {
353+
ggplot_global$element_tree
354+
}
355+
356+
#' @rdname register_theme_elements
357+
#' @details
358+
#' The function `el_def()` is used to define new or modified element types and
359+
#' element inheritance relationships for the element tree.
360+
#' @param class The name of the element class. Examples are "element_line" or
361+
#' "element_text" or "unit", or one of the two reserved keywords "character" or
362+
#' "margin". The reserved keyword "character" implies a character
363+
#' or numeric vector, not a class called "character". The keyword
364+
#' "margin" implies a unit vector of length 4, as created by [margin()].
365+
#' @param inherit A vector of strings, naming the elements that this
366+
#' element inherits from.
367+
#' @param description An optional character vector providing a description
368+
#' for the element.
314369
#' @keywords internal
315370
#' @export
316371
el_def <- function(class = NULL, inherit = NULL, description = NULL) {
317372
list(class = class, inherit = inherit, description = description)
318373
}
319374

320375

321-
# This data structure represents the theme elements and the inheritance
322-
# among them. (In the future, .element_tree should be removed in favor
323-
# of direct assignment to ggplot_global$element_tree, see below.)
376+
# This data structure represents the default theme elements and the inheritance
377+
# among them. It should not be read from directly, since users may modify the
378+
# current element tree stored in ggplot_global$element_tree
324379
.element_tree <- list(
325380
line = el_def("element_line"),
326381
rect = el_def("element_rect"),
@@ -424,8 +479,6 @@ el_def <- function(class = NULL, inherit = NULL, description = NULL) {
424479
aspect.ratio = el_def("character")
425480
)
426481

427-
ggplot_global$element_tree <- .element_tree
428-
429482
# Check that an element object has the proper class
430483
#
431484
# Given an element object and the name of the element, this function

R/theme.r

Lines changed: 13 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -156,19 +156,14 @@
156156
#' @param strip.switch.pad.wrap space between strips and axes when strips are
157157
#' switched (`unit`)
158158
#'
159-
#' @param ... additional element specifications not part of base ggplot2. If
160-
#' supplied `validate` needs to be set to `FALSE`.
159+
#' @param ... additional element specifications not part of base ggplot2. In general,
160+
#' these should also be defined in the `element tree` argument.
161161
#' @param complete set this to `TRUE` if this is a complete theme, such as
162162
#' the one returned by [theme_grey()]. Complete themes behave
163163
#' differently when added to a ggplot object. Also, when setting
164164
#' `complete = TRUE` all elements will be set to inherit from blank
165165
#' elements.
166166
#' @param validate `TRUE` to run `validate_element()`, `FALSE` to bypass checks.
167-
#' @param element_tree optional addition or modification to the element tree,
168-
#' which specifies the inheritance relationship of the theme elements. The element
169-
#' tree should be provided as a list of named element definitions created with
170-
#' [`el_def()`]. See [`el_def()`] for more details.
171-
#'
172167
#' @seealso
173168
#' [+.gg()] and [%+replace%],
174169
#' [element_blank()], [element_line()],
@@ -362,10 +357,9 @@ theme <- function(line,
362357
strip.switch.pad.wrap,
363358
...,
364359
complete = FALSE,
365-
validate = TRUE,
366-
element_tree = NULL
360+
validate = TRUE
367361
) {
368-
elements <- find_args(..., complete = NULL, validate = NULL, element_tree = NULL)
362+
elements <- find_args(..., complete = NULL, validate = NULL)
369363

370364
if (!is.null(elements$axis.ticks.margin)) {
371365
warn("`axis.ticks.margin` is deprecated. Please set `margin` property of `axis.text` instead")
@@ -405,8 +399,7 @@ theme <- function(line,
405399
elements,
406400
class = c("theme", "gg"),
407401
complete = complete,
408-
validate = validate,
409-
element_tree = element_tree
402+
validate = validate
410403
)
411404
}
412405

@@ -422,24 +415,6 @@ is_theme_validate <- function(x) {
422415
isTRUE(validate)
423416
}
424417

425-
# obtain the full element tree from a theme,
426-
# substituting the defaults if needed
427-
complete_element_tree <- function(theme) {
428-
element_tree <- attr(theme, "element_tree", exact = TRUE)
429-
430-
# we fill in the element tree first from the current default theme,
431-
# and then from the internal element tree if necessary
432-
# this makes it easy for extension packages to provide modified
433-
# default element trees
434-
defaults(
435-
defaults(
436-
element_tree,
437-
attr(theme_get(), "element_tree", exact = TRUE)
438-
),
439-
ggplot_global$element_tree
440-
)
441-
}
442-
443418
# Combine plot defaults with current theme to get complete theme for a plot
444419
plot_theme <- function(x, default = theme_get()) {
445420
theme <- x$theme
@@ -455,15 +430,15 @@ plot_theme <- function(x, default = theme_get()) {
455430
theme <- default + theme
456431
}
457432

458-
# complete the element tree and save back to the theme
459-
element_tree <- complete_element_tree(theme)
460-
attr(theme, "element_tree") <- element_tree
433+
# if we're still missing elements relative to fallback default, fill in those
434+
missing <- setdiff(names(ggplot_global$theme_default), names(theme))
435+
theme[missing] <- ggplot_global$theme_default[missing]
461436

462437
# Check that all elements have the correct class (element_text, unit, etc)
463438
if (is_theme_validate(theme)) {
464439
mapply(
465440
validate_element, theme, names(theme),
466-
MoreArgs = list(element_tree = element_tree)
441+
MoreArgs = list(element_tree = get_element_tree())
467442
)
468443
}
469444

@@ -504,12 +479,6 @@ add_theme <- function(t1, t2, t2name) {
504479
attr(t1, "validate") <-
505480
is_theme_validate(t1) && is_theme_validate(t2)
506481

507-
# Merge element trees if provided
508-
attr(t1, "element_tree") <- defaults(
509-
attr(t2, "element_tree", exact = TRUE),
510-
attr(t1, "element_tree", exact = TRUE)
511-
)
512-
513482
t1
514483
}
515484

@@ -552,13 +521,8 @@ calc_element <- function(element, theme, verbose = FALSE, skip_blank = FALSE) {
552521
}
553522
}
554523

555-
# Obtain the element tree and check that the element is in it
556-
# If not, try to retrieve the complete element tree. This is
557-
# needed for backwards compatibility and certain unit tests.
558-
element_tree <- attr(theme, "element_tree", exact = TRUE)
559-
if (!element %in% names(element_tree)) {
560-
element_tree <- complete_element_tree(theme)
561-
}
524+
# Obtain the element tree
525+
element_tree <- get_element_tree()
562526

563527
# If the element is defined (and not just inherited), check that
564528
# it is of the class specified in element_tree
@@ -580,8 +544,8 @@ calc_element <- function(element, theme, verbose = FALSE, skip_blank = FALSE) {
580544
return(el_out) # no null properties, return element as is
581545
}
582546

583-
# if we have null properties, try to fill in from theme_grey()
584-
el_out <- combine_elements(el_out, ggplot_global$theme_grey[[element]])
547+
# if we have null properties, try to fill in from ggplot_global$theme_default
548+
el_out <- combine_elements(el_out, ggplot_global$theme_default[[element]])
585549
nullprops <- vapply(el_out, is.null, logical(1))
586550
if (!any(nullprops)) {
587551
return(el_out) # no null properties remaining, return element

R/zzz.r

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ pathGrob <- NULL
3333

3434
.zeroGrob <<- grob(cl = "zeroGrob", name = "NULL")
3535

36-
# create default theme, store for later use, and set as current theme
37-
ggplot_global$theme_all_null <- theme_all_null() # required by theme_grey()
38-
ggplot_global$theme_grey <- theme_grey()
39-
ggplot_global$theme_current <- ggplot_global$theme_grey
36+
# create the global variables holding all the theme settings
37+
ggplot_global$theme_all_null <- theme_all_null() # cache all null theme, required by theme_grey()
38+
ggplot_global$theme_current <- NULL # the current theme applied to plots if none is specified
39+
ggplot_global$theme_default <- NULL # the underlying fallback default theme
40+
ggplot_global$element_tree <- NULL # the current element tree for themes
41+
reset_theme_settings() # sets the preceding three global variables to their actual defaults
4042

4143
# Used by rbind_dfs
4244
date <- Sys.Date()

0 commit comments

Comments
 (0)