Skip to content

Commit 5e6b1e5

Browse files
Allow expressions in facet specs even when some variables are not available on some layers (#3757)
1 parent f72102f commit 5e6b1e5

File tree

5 files changed

+50
-7
lines changed

5 files changed

+50
-7
lines changed

NEWS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,9 @@ fail.
168168
stepwise addition of individual partial themes is now equivalent to
169169
addition of multple theme elements at once (@clauswilke, #3039).
170170

171+
* Facets now don't fail even when some variable in the spec are not available
172+
in all layers (@yutannihilation, #2963).
173+
171174
# ggplot2 3.2.1
172175

173176
This is a patch release fixing a few regressions introduced in 3.2.0 as well as

R/facet-.r

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ Facet <- ggproto("Facet", NULL,
155155
panels
156156
},
157157
setup_params = function(data, params) {
158+
params$.possible_columns <- unique(unlist(lapply(data, names)))
158159
params
159160
},
160161
setup_data = function(data, params) {
@@ -417,11 +418,13 @@ is_facets <- function(x) {
417418
# when evaluating an expression, you want to see any errors. That does
418419
# mean you can't have background data when faceting by an expression,
419420
# but that seems like a reasonable tradeoff.
420-
eval_facets <- function(facets, data, env = globalenv()) {
421-
vars <- compact(lapply(facets, eval_facet, data, env = env))
421+
eval_facets <- function(facets, data, possible_columns = NULL) {
422+
vars <- compact(lapply(facets, eval_facet, data, possible_columns = possible_columns))
422423
new_data_frame(tibble::as_tibble(vars))
423424
}
424-
eval_facet <- function(facet, data, env = emptyenv()) {
425+
eval_facet <- function(facet, data, possible_columns = NULL) {
426+
# Treat the case when `facet` is a quosure of a symbol specifically
427+
# to issue a friendlier warning
425428
if (quo_is_symbol(facet)) {
426429
facet <- as.character(quo_get_expr(facet))
427430

@@ -433,7 +436,22 @@ eval_facet <- function(facet, data, env = emptyenv()) {
433436
return(out)
434437
}
435438

436-
eval_tidy(facet, data, env)
439+
# Key idea: use active bindings so that column names missing in this layer
440+
# but present in others raise a custom error
441+
env <- new_environment(data)
442+
missing_columns <- setdiff(possible_columns, names(data))
443+
undefined_error <- function(e) abort("", class = "ggplot2_missing_facet_var")
444+
bindings <- rep_named(missing_columns, list(undefined_error))
445+
env_bind_active(env, !!!bindings)
446+
447+
# Create a data mask and install a data pronoun manually (see ?new_data_mask)
448+
mask <- new_data_mask(env)
449+
mask$.data <- as_data_pronoun(mask)
450+
451+
tryCatch(
452+
eval_tidy(facet, mask),
453+
ggplot2_missing_facet_var = function(e) NULL
454+
)
437455
}
438456

439457
layout_null <- function() {
@@ -524,10 +542,11 @@ panel_rows <- function(table) {
524542
#' @keywords internal
525543
#' @export
526544
combine_vars <- function(data, env = emptyenv(), vars = NULL, drop = TRUE) {
545+
possible_columns <- unique(unlist(lapply(data, names)))
527546
if (length(vars) == 0) return(new_data_frame())
528547

529548
# For each layer, compute the facet values
530-
values <- compact(lapply(data, eval_facets, facets = vars, env = env))
549+
values <- compact(lapply(data, eval_facets, facets = vars, possible_columns = possible_columns))
531550

532551
# Form the base data.frame which contains all combinations of faceting
533552
# variables that appear in the data

R/facet-grid-.r

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ FacetGrid <- ggproto("FacetGrid", Facet,
252252
intersect(names(cols), names(data)))
253253
data <- reshape_add_margins(data, margin_vars, params$margins)
254254

255-
facet_vals <- eval_facets(c(rows, cols), data, params$plot_env)
255+
facet_vals <- eval_facets(c(rows, cols), data, params$.possible_columns)
256256

257257
# If any faceting variables are missing, add them in by
258258
# duplicating the data

R/facet-wrap.r

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ FacetWrap <- ggproto("FacetWrap", Facet,
187187
return(data)
188188
}
189189

190-
facet_vals <- eval_facets(vars, data, params$plot_env)
190+
facet_vals <- eval_facets(vars, data, params$.possible_columns)
191191
facet_vals[] <- lapply(facet_vals[], as.factor)
192192

193193
missing_facets <- setdiff(names(vars), names(facet_vals))

tests/testthat/test-facet-.r

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,27 @@ test_that("combine_vars() generates the correct combinations with multiple data
302302
)
303303
})
304304

305+
test_that("eval_facet() is tolerant for missing columns (#2963)", {
306+
expect_null(eval_facet(quo(2 * x), data_frame(foo = 1), possible_columns = c("x")))
307+
expect_null(eval_facet(quo(2 * .data$x), data_frame(foo = 1), possible_columns = c("x")))
308+
309+
# Even if there's the same name of external variable, eval_facet() returns NULL before
310+
# reaching to the variable
311+
bar <- 2
312+
expect_null(eval_facet(quo(2 * bar), data_frame(foo = 1), possible_columns = c("bar")))
313+
# If there's no same name of columns, the external variable is used
314+
expect_equal(
315+
eval_facet(quo(2 * bar), data_frame(foo = 1), possible_columns = c("x")),
316+
4
317+
)
318+
319+
# If the expression contains any non-existent variable, it fails
320+
expect_error(
321+
eval_facet(quo(no_such_variable * x), data_frame(foo = 1), possible_columns = c("x")),
322+
"object 'no_such_variable' not found"
323+
)
324+
})
325+
305326
# Visual tests ------------------------------------------------------------
306327

307328
test_that("facet labels respect both justification and margin arguments", {

0 commit comments

Comments
 (0)