Skip to content

WIP: Allow expressions in facet specs even when some variables are not available on some layers #3735

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

29 changes: 24 additions & 5 deletions R/facet-.r
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ Facet <- ggproto("Facet", NULL,
panels
},
setup_params = function(data, params) {
params$.possible_columns <- unique(unlist(lapply(data, names)))
params
},
setup_data = function(data, params) {
Expand Down Expand Up @@ -417,11 +418,13 @@ is_facets <- function(x) {
# when evaluating an expression, you want to see any errors. That does
# mean you can't have background data when faceting by an expression,
# but that seems like a reasonable tradeoff.
eval_facets <- function(facets, data, env = globalenv()) {
vars <- compact(lapply(facets, eval_facet, data, env = env))
eval_facets <- function(facets, data, possible_columns = NULL) {
vars <- compact(lapply(facets, eval_facet, data, possible_columns = possible_columns))
new_data_frame(tibble::as_tibble(vars))
}
eval_facet <- function(facet, data, env = emptyenv()) {
eval_facet <- function(facet, data, possible_columns = NULL) {
# Treat the case when `facet` is a quosure of a symbol specifically
# to issue a friendlier warning
if (quo_is_symbol(facet)) {
facet <- as.character(quo_get_expr(facet))

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

eval_tidy(facet, data, env)
# Key idea: use active bindings so that column names missing in this layer
# but present in others raise a custom error
env <- new_environment(data)
missing_columns <- setdiff(possible_columns, names(data))
undefined_error <- function(e) abort("", class = "ggplot2_missing_facet_var")
bindings <- rep_named(missing_columns, list(undefined_error))
env_bind_active(env, !!!bindings)

# Create a data mask and install a data pronoun manually (see ?new_data_mask)
mask <- new_data_mask(env)
mask$.data <- as_data_pronoun(mask)

tryCatch(
eval_tidy(facet, mask),
ggplot2_missing_facet_var = function(e) NULL
)
}

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

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

# Form the base data.frame which contains all combinations of faceting
# variables that appear in the data
Expand Down
2 changes: 1 addition & 1 deletion R/facet-grid-.r
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ FacetGrid <- ggproto("FacetGrid", Facet,
intersect(names(cols), names(data)))
data <- reshape_add_margins(data, margin_vars, params$margins)

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

# If any faceting variables are missing, add them in by
# duplicating the data
Expand Down
2 changes: 1 addition & 1 deletion R/facet-wrap.r
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ FacetWrap <- ggproto("FacetWrap", Facet,
return(data)
}

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

missing_facets <- setdiff(names(vars), names(facet_vals))
Expand Down
21 changes: 21 additions & 0 deletions tests/testthat/test-facet-.r
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,27 @@ test_that("combine_vars() generates the correct combinations with multiple data
)
})

test_that("eval_facet() is tolerant for missing columns (#2963)", {
expect_null(eval_facet(quo(2 * x), data_frame(foo = 1), possible_columns = c("x")))
expect_null(eval_facet(quo(2 * .data$x), data_frame(foo = 1), possible_columns = c("x")))

# Even if there's the same name of external variable, eval_facet() returns NULL before
# reaching to the variable
bar <- 2
expect_null(eval_facet(quo(2 * bar), data_frame(foo = 1), possible_columns = c("bar")))
# If there's no same name of columns, the external variable is used
expect_equal(
eval_facet(quo(2 * bar), data_frame(foo = 1), possible_columns = c("x")),
4
)

# If the expression contains any non-existent variable, it fails
expect_error(
eval_facet(quo(no_such_variable * x), data_frame(foo = 1), possible_columns = c("x")),
"object 'no_such_variable' not found"
)
})

# Visual tests ------------------------------------------------------------

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