Skip to content

Commit 4fbc857

Browse files
authored
Mask stage() at the expression level (#6110)
* evaluation helper * use helper * generalise `substitute_aes()` * substitute expressions instead of wrangling data masks * add test * make a comment for the next person to trip over this * add news bullet
1 parent 5184f5e commit 4fbc857

File tree

6 files changed

+79
-25
lines changed

6 files changed

+79
-25
lines changed

NEWS.md

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

3+
* Fixed bug where the `ggplot2::`-prefix did not work with `stage()`
4+
(@teunbrand, #6104).
35
* New `get_labs()` function for retrieving completed plot labels
46
(@teunbrand, #6008).
57
* Built-in `theme_*()` functions now have `ink` and `paper` arguments to control

R/aes-evaluation.R

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,3 +358,39 @@ make_labels <- function(mapping) {
358358
}
359359
Map(default_label, names(mapping), mapping)
360360
}
361+
362+
eval_aesthetics <- function(aesthetics, data, mask = NULL) {
363+
364+
env <- child_env(base_env())
365+
366+
# Here we mask functions, often to replace `stage()` with context appropriate
367+
# functions `stage_calculated()`/`stage_scaled()`.
368+
if (length(mask) > 0) {
369+
aesthetics <- substitute_aes(aesthetics, mask_function, mask = mask)
370+
}
371+
372+
evaled <- lapply(aesthetics, eval_tidy, data = data, env = env)
373+
names(evaled) <- names(aesthetics)
374+
compact(rename_aes(evaled))
375+
}
376+
377+
# `mask` is a list of functions where `names(mask)` indicate names of functions
378+
# that need to be replaced, and `mask[[i]]` is the function to replace it
379+
# with.
380+
mask_function <- function(x, mask) {
381+
if (!is.call(x)) {
382+
return(x)
383+
}
384+
nms <- names(mask)
385+
x[-1] <- lapply(x[-1], mask_function, mask = mask)
386+
if (!is_call(x, nms)) {
387+
return(x)
388+
}
389+
for (nm in nms) {
390+
if (is_call(x, nm)) {
391+
x[[1]] <- mask[[nm]]
392+
return(x)
393+
}
394+
}
395+
}
396+

R/aes.R

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,9 +198,12 @@ rename_aes <- function(x) {
198198
}
199199
x
200200
}
201-
substitute_aes <- function(x) {
201+
202+
# `x` is assumed to be a strict list of quosures;
203+
# it should have no non-quosure constants in it, even though `aes()` allows it.
204+
substitute_aes <- function(x, fun = standardise_aes_symbols, ...) {
202205
x <- lapply(x, function(aesthetic) {
203-
as_quosure(standardise_aes_symbols(quo_get_expr(aesthetic)), env = environment(aesthetic))
206+
as_quosure(fun(quo_get_expr(aesthetic), ...), env = environment(aesthetic))
204207
})
205208
class(x) <- "uneval"
206209
x

R/geom-.R

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,10 @@ Geom <- ggproto("Geom",
157157
# This order means that they will have access to all default aesthetics
158158
if (length(modifiers) != 0) {
159159
# Set up evaluation environment
160-
env <- child_env(baseenv(), after_scale = after_scale)
161-
# Mask stage with stage_scaled so it returns the correct expression
162-
stage_mask <- child_env(emptyenv(), stage = stage_scaled)
163-
mask <- new_data_mask(as_environment(data, stage_mask), stage_mask)
164-
mask$.data <- as_data_pronoun(mask)
165-
modified_aes <- lapply(substitute_aes(modifiers), eval_tidy, mask, env)
160+
modified_aes <- eval_aesthetics(
161+
substitute_aes(modifiers), data,
162+
mask = list(stage = stage_scaled)
163+
)
166164

167165
# Check that all output are valid data
168166
nondata_modified <- check_nondata_cols(modified_aes)
@@ -177,11 +175,9 @@ Geom <- ggproto("Geom",
177175
))
178176
}
179177

180-
names(modified_aes) <- names(rename_aes(modifiers))
181-
182178
modified_aes <- cleanup_mismatched_data(modified_aes, nrow(data), "after_scale")
183179

184-
modified_aes <- data_frame0(!!!compact(modified_aes))
180+
modified_aes <- data_frame0(!!!modified_aes)
185181

186182
data <- data_frame0(!!!defaults(modified_aes, data))
187183
}

R/layer.R

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -307,10 +307,7 @@ Layer <- ggproto("Layer", NULL,
307307
}
308308

309309
# Evaluate aesthetics
310-
env <- child_env(baseenv(), stage = stage)
311-
evaled <- lapply(aesthetics, eval_tidy, data = data, env = env)
312-
evaled <- compact(evaled)
313-
310+
evaled <- eval_aesthetics(aesthetics, data)
314311
plot$scales$add_defaults(evaled, plot$plot_env)
315312

316313
# Check for discouraged usage in mapping
@@ -390,14 +387,10 @@ Layer <- ggproto("Layer", NULL,
390387
data_orig <- plot$scales$backtransform_df(data)
391388

392389
# Add map stat output to aesthetics
393-
env <- child_env(baseenv(), stat = stat, after_stat = after_stat)
394-
stage_mask <- child_env(emptyenv(), stage = stage_calculated)
395-
mask <- new_data_mask(as_environment(data_orig, stage_mask), stage_mask)
396-
mask$.data <- as_data_pronoun(mask)
397-
398-
new <- substitute_aes(new)
399-
stat_data <- lapply(new, eval_tidy, mask, env)
400-
390+
stat_data <- eval_aesthetics(
391+
substitute_aes(new), data_orig,
392+
mask = list(stage = stage_calculated)
393+
)
401394
# Check that all columns in aesthetic stats are valid data
402395
nondata_stat_cols <- check_nondata_cols(stat_data)
403396
if (length(nondata_stat_cols) > 0) {
@@ -411,8 +404,7 @@ Layer <- ggproto("Layer", NULL,
411404
))
412405
}
413406

414-
names(stat_data) <- names(new)
415-
stat_data <- data_frame0(!!!compact(stat_data))
407+
stat_data <- data_frame0(!!!stat_data)
416408

417409
# Add any new scales, if needed
418410
plot$scales$add_defaults(stat_data, plot$plot_env)

tests/testthat/test-aes-calculated.R

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,28 @@ test_that("A deprecated warning is issued when stat(var) or ..var.. is used", {
9999
p2 <- ggplot(NULL, aes(..bar..))
100100
expect_snapshot_warning(b2 <- ggplot_build(p2))
101101
})
102+
103+
test_that("functions can be masked", {
104+
105+
foo <- function(x) x + 10
106+
bar <- function(x) x * 10
107+
108+
data <- data.frame(val = 10)
109+
mapping <- aes(x = val, y = foo(20))
110+
111+
evaled <- eval_aesthetics(mapping, data = data, mask = list())
112+
expect_equal(evaled, list(x = 10, y = 30))
113+
114+
evaled <- eval_aesthetics(mapping, data = data, mask = list(foo = bar))
115+
expect_equal(evaled, list(x = 10, y = 200))
116+
117+
# Test namespace-prefixed evaluation (#6104)
118+
mapping <- aes(x = val, y = ggplot2::stage(10, 20, 30))
119+
evaled <- eval_aesthetics(mapping, data = data, mask = list())
120+
expect_equal(evaled, list(x = 10, y = 10))
121+
evaled <- eval_aesthetics(mapping, data = data, mask = list(stage = stage_calculated))
122+
expect_equal(evaled, list(x = 10, y = 20))
123+
evaled <- eval_aesthetics(mapping, data = data, mask = list(stage = stage_scaled))
124+
expect_equal(evaled, list(x = 10, y = 30))
125+
126+
})

0 commit comments

Comments
 (0)