Skip to content

Commit 9b8a85d

Browse files
committed
Support quosures in facets specs
1 parent 30f366d commit 9b8a85d

File tree

4 files changed

+215
-55
lines changed

4 files changed

+215
-55
lines changed

R/facet-.r

Lines changed: 134 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -203,38 +203,151 @@ df.grid <- function(a, b) {
203203
))
204204
}
205205

206+
# A facets spec is a list of facets. A grid facetting needs two facets
207+
# while a wrap facetting flattens all dimensions and thus accepts any
208+
# number of facets.
209+
#
210+
# A facets is a list of grouping variables. They are typically
211+
# supplied as variable names but can be expressions.
212+
#
213+
# as_facets() is complex due to historical baggage but its main
214+
# purpose is to create a facets spec from a formula: a + b ~ c + d
215+
# creates a facets list with two components, each of which bundles two
216+
# facetting variables.
217+
218+
as_facets_spec <- function(x) {
219+
if (inherits(x, "mapping")) {
220+
stop("todo mapping")
221+
}
222+
223+
# This needs to happen early because we might get a formula.
224+
# facet_grid() directly converted strings to a formula while
225+
# facet_wrap() called as.quoted(). Hence this is a little more
226+
# complicated for backward compatibility.
227+
if (rlang::is_string(x)) {
228+
x <- plyr::as.quoted(x)
229+
if (rlang::is_formula(x[[1]])) {
230+
x <- x[[1]]
231+
}
232+
}
233+
234+
# At this level formulas are coerced to lists of lists for backward
235+
# compatibility with facet_grid(). The LHS and RHS are treated as
236+
# distinct facet dimensions and `+` defines multiple facet variables
237+
# inside each dimension.
238+
if (rlang::is_formula(x)) {
239+
return(f_as_facets_spec(x))
240+
}
241+
242+
# For backward-compatibility with facet_wrap()
243+
if (!rlang::is_bare_list(x)) {
244+
x <- plyr::as.quoted(x)
245+
attributes(x) <- NULL
246+
}
247+
248+
# If we have a list there are two possibilities. We may already have
249+
# a proper facet spec structure. Otherwise we coerce each element
250+
# with plyr::as.quoted() for backward compatibility with facet_grid().
251+
if (is.list(x)) {
252+
x <- lapply(x, as_facets)
253+
}
254+
255+
if (sum(vapply(x, length, integer(1))) == 0L) {
256+
stop("Must specify at least one variable to facet by", call. = FALSE)
257+
}
258+
259+
x
260+
}
261+
262+
f_as_facets_spec <- function(f) {
263+
lhs <- function(x) if (length(x) == 2) NULL else x[-3]
264+
rhs <- function(x) if (length(x) == 2) x else x[-2]
265+
266+
rows <- f_as_facets(lhs(f))
267+
cols <- f_as_facets(rhs(f))
268+
269+
if (length(rows) + length(cols) == 0) {
270+
stop("Must specify at least one variable to facet by", call. = FALSE)
271+
}
272+
273+
if (length(rows)) {
274+
list(rows, cols)
275+
} else {
276+
list(cols)
277+
}
278+
}
279+
280+
281+
as_facets <- function(x) {
282+
if (is_facets(x)) {
283+
return(x)
284+
}
285+
286+
if (rlang::is_formula(x)) {
287+
# Use different formula method because plyr's does not handle the
288+
# environment correctly.
289+
f_as_facets(x)
290+
} else {
291+
vars <- plyr::as.quoted(x)
292+
vars <- lapply(vars, rlang::new_quosure, env = globalenv())
293+
rlang::quos_auto_name(vars)
294+
}
295+
}
296+
f_as_facets <- function(f) {
297+
if (is.null(f)) {
298+
return(list())
299+
}
300+
301+
env <- rlang::f_env(f) %||% globalenv()
302+
303+
# as.quoted() handles `+` specifications
304+
vars <- plyr::as.quoted(f)
305+
306+
# `.` in formulas is ignored
307+
vars <- discard_dots(vars)
308+
309+
vars <- lapply(vars, rlang::new_quosure, env)
310+
rlang::quos_auto_name(vars)
311+
}
312+
discard_dots <- function(x) {
313+
x[!vapply(x, identical, logical(1), as.name("."))]
314+
}
315+
316+
is_facets <- function(x) {
317+
if (!is.list(x)) {
318+
return(FALSE)
319+
}
320+
if (!length(x)) {
321+
return(FALSE)
322+
}
323+
all(vapply(x, rlang::is_quosure, logical(1)))
324+
}
325+
326+
206327
# When evaluating variables in a facet specification, we evaluate bare
207328
# variables and expressions slightly differently. Bare variables should
208329
# always succeed, even if the variable doesn't exist in the data frame:
209330
# that makes it possible to repeat data across multiple factors. But
210331
# when evaluating an expression, you want to see any errors. That does
211332
# mean you can't have background data when faceting by an expression,
212333
# but that seems like a reasonable tradeoff.
213-
eval_facet_vars <- function(vars, data, env = emptyenv()) {
214-
nms <- names(vars)
215-
out <- list()
216-
217-
for (i in seq_along(vars)) {
218-
out[[ nms[[i]] ]] <- eval_facet_var(vars[[i]], data, env = env)
219-
}
220-
221-
tibble::as_tibble(out)
334+
eval_facets <- function(facets, data, env = globalenv()) {
335+
vars <- compact(lapply(facets, eval_facet, data, env = env))
336+
tibble::as_tibble(vars)
222337
}
338+
eval_facet <- function(facet, data, env = emptyenv()) {
339+
if (rlang::quo_is_symbol(facet)) {
340+
facet <- as.character(rlang::quo_get_expr(facet))
223341

224-
eval_facet_var <- function(var, data, env = emptyenv()) {
225-
if (is.name(var)) {
226-
var <- as.character(var)
227-
if (var %in% names(data)) {
228-
data[[var]]
342+
if (facet %in% names(data)) {
343+
out <- data[[facet]]
229344
} else {
230-
NULL
345+
out <- NULL
231346
}
232-
} else if (is.call(var)) {
233-
eval(var, envir = data, enclos = env)
234-
} else {
235-
stop("Must use either variable name or expression when faceting",
236-
call. = FALSE)
347+
return(out)
237348
}
349+
350+
rlang::eval_tidy(facet, data, env)
238351
}
239352

240353
layout_null <- function() {
@@ -325,7 +438,7 @@ combine_vars <- function(data, env = emptyenv(), vars = NULL, drop = TRUE) {
325438
if (length(vars) == 0) return(data.frame())
326439

327440
# For each layer, compute the facet values
328-
values <- compact(plyr::llply(data, eval_facet_vars, vars = vars, env = env))
441+
values <- compact(plyr::llply(data, eval_facets, facets = vars, env = env))
329442

330443
# Form the base data frame which contains all combinations of faceting
331444
# variables that appear in the data

R/facet-grid-.r

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -146,26 +146,21 @@ facet_grid <- function(facets, margins = FALSE, scales = "fixed", space = "fixed
146146
stop("switch must be either 'both', 'x', or 'y'", call. = FALSE)
147147
}
148148

149-
# Facets can either be a formula, a string, or a list of things to be
150-
# convert to quoted
151-
if (is.character(facets)) {
152-
facets <- stats::as.formula(facets)
153-
}
154-
if (is.formula(facets)) {
155-
lhs <- function(x) if (length(x) == 2) NULL else x[-3]
156-
rhs <- function(x) if (length(x) == 2) x else x[-2]
157-
158-
rows <- as.quoted(lhs(facets))
159-
rows <- rows[!sapply(rows, identical, as.name("."))]
160-
cols <- as.quoted(rhs(facets))
161-
cols <- cols[!sapply(cols, identical, as.name("."))]
149+
facets <- as_facets_spec(facets)
150+
151+
n <- length(facets)
152+
if (!n) {
153+
stop("FIXME: Internal error grid?")
162154
}
163-
if (is.list(facets)) {
164-
rows <- as.quoted(facets[[1]])
165-
cols <- as.quoted(facets[[2]])
155+
if (n > 2L) {
156+
stop("A grid facet specification can't have more than two dimensions", call. = FALSE)
166157
}
167-
if (length(rows) + length(cols) == 0) {
168-
stop("Must specify at least one variable to facet by", call. = FALSE)
158+
if (n == 1L) {
159+
rows <- list()
160+
cols <- facets[[1]]
161+
} else {
162+
rows <- facets[[1]]
163+
cols <- facets[[2]]
169164
}
170165

171166
# Check for deprecated labellers
@@ -187,8 +182,8 @@ FacetGrid <- ggproto("FacetGrid", Facet,
187182
shrink = TRUE,
188183

189184
compute_layout = function(data, params) {
190-
rows <- as.quoted(params$rows)
191-
cols <- as.quoted(params$cols)
185+
rows <- params$rows
186+
cols <- params$cols
192187

193188
dups <- intersect(names(rows), names(cols))
194189
if (length(dups) > 0) {
@@ -234,16 +229,16 @@ FacetGrid <- ggproto("FacetGrid", Facet,
234229
return(cbind(data, PANEL = integer(0)))
235230
}
236231

237-
rows <- as.quoted(params$rows)
238-
cols <- as.quoted(params$cols)
232+
rows <- params$rows
233+
cols <- params$cols
239234
vars <- c(names(rows), names(cols))
240235

241236
# Compute faceting values and add margins
242237
margin_vars <- list(intersect(names(rows), names(data)),
243238
intersect(names(cols), names(data)))
244239
data <- reshape2::add_margins(data, margin_vars, params$margins)
245240

246-
facet_vals <- eval_facet_vars(c(rows, cols), data, params$plot_env)
241+
facet_vals <- eval_facets(c(rows, cols), data, params$plot_env)
247242

248243
# If any faceting variables are missing, add them in by
249244
# duplicating the data
@@ -405,7 +400,7 @@ FacetGrid <- ggproto("FacetGrid", Facet,
405400
panel_table
406401
},
407402
vars = function(self) {
408-
vapply(c(self$params$rows, self$params$cols), as.character, character(1))
403+
names(c(self$params$rows, self$params$cols))
409404
}
410405
)
411406

R/facet-wrap.r

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,22 @@ facet_wrap <- function(facets, nrow = NULL, ncol = NULL, scales = "fixed",
105105
# Check for deprecated labellers
106106
labeller <- check_labeller(labeller)
107107

108+
# Flatten all facets dimensions into a single one
109+
facets <- rlang::flatten(as_facets_spec(facets))
110+
108111
ggproto(NULL, FacetWrap,
109112
shrink = shrink,
110-
params = list(facets = as.quoted(facets), free = free,
111-
as.table = as.table, strip.position = strip.position,
112-
drop = drop, ncol = ncol, nrow = nrow,
113-
labeller = labeller,
114-
dir = dir)
113+
params = list(
114+
facets = facets,
115+
free = free,
116+
as.table = as.table,
117+
strip.position = strip.position,
118+
drop = drop,
119+
ncol = ncol,
120+
nrow = nrow,
121+
labeller = labeller,
122+
dir = dir
123+
)
115124
)
116125
}
117126

@@ -123,8 +132,10 @@ FacetWrap <- ggproto("FacetWrap", Facet,
123132
shrink = TRUE,
124133

125134
compute_layout = function(data, params) {
126-
vars <- as.quoted(params$facets)
127-
if (length(vars) == 0) return(layout_null())
135+
vars <- params$facets
136+
if (length(vars) == 0) {
137+
return(layout_null())
138+
}
128139

129140
base <- plyr::unrowname(
130141
combine_vars(data, params$plot_env, vars, drop = params$drop)
@@ -162,9 +173,9 @@ FacetWrap <- ggproto("FacetWrap", Facet,
162173
if (empty(data)) {
163174
return(cbind(data, PANEL = integer(0)))
164175
}
165-
vars <- as.quoted(params$facets)
176+
vars <- params$facets
166177

167-
facet_vals <- eval_facet_vars(vars, data, params$plot_env)
178+
facet_vals <- eval_facets(vars, data, params$plot_env)
168179
facet_vals[] <- lapply(facet_vals[], as.factor)
169180

170181
missing_facets <- setdiff(names(vars), names(facet_vals))
@@ -338,7 +349,7 @@ FacetWrap <- ggproto("FacetWrap", Facet,
338349
panel_table
339350
},
340351
vars = function(self) {
341-
vapply(self$params$facets, as.character, character(1))
352+
names(self$params$facets)
342353
}
343354
)
344355

tests/testthat/test-facet-.r

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,46 @@
11
context("Facetting")
22

3+
quo <- rlang::quo
4+
quoted_obj <- structure(list(), class = "quoted_obj")
5+
as.quoted.quoted_obj <- function(...) plyr::as.quoted(quote(dispatched), globalenv())
6+
assign("as.quoted.quoted_obj", as.quoted.quoted_obj, envir = globalenv())
7+
8+
test_that("as_facets_spec() coerces formulas", {
9+
expect_identical(as_facets_spec(~foo), list(list(foo = quo(foo))))
10+
expect_identical(as_facets_spec(~foo + bar), list(list(foo = quo(foo), bar = quo(bar))))
11+
12+
expect_identical(as_facets_spec(foo ~ bar), list(list(foo = quo(foo)), list(bar = quo(bar))))
13+
14+
exp <- list(list(foo = quo(foo), bar = quo(bar)), list(baz = quo(baz), bam = quo(bam)))
15+
expect_identical(as_facets_spec(foo + bar ~ baz + bam), exp)
16+
17+
exp <- list(list(`foo()`= quo(foo()), `bar()` = quo(bar())), list(`baz()` = quo(baz()), `bam()` = quo(bam())))
18+
expect_identical(as_facets_spec(foo() + bar() ~ baz() + bam()), exp)
19+
})
20+
21+
test_that("as_facets_spec() coerces strings containing formulas", {
22+
expect_identical(as_facets_spec("foo ~ bar"), as_facets_spec(local(foo ~ bar, globalenv())))
23+
})
24+
25+
test_that("as_facets_spec() coerces character vectors", {
26+
expect_identical(as_facets_spec("foo"), as_facets_spec(local(~foo, globalenv())))
27+
expect_identical(as_facets_spec(c("foo", "bar")), as_facets_spec(local(foo ~ bar, globalenv())))
28+
})
29+
30+
test_that("as_facets_spec() coerces lists", {
31+
out <- as_facets_spec(list(quote(foo), c("foo", "bar"), NULL, quoted_obj))
32+
exp <- c(as_facets_spec(quote(foo)), list(rlang::flatten(as_facets_spec(c("foo", "bar")))), list(list()), as_facets_spec(quoted_obj))
33+
expect_identical(out, exp)
34+
})
35+
36+
test_that("as_facets_spec() errors with empty specs", {
37+
expect_error(as_facets_spec(list()), "at least one variable to facet by")
38+
expect_error(as_facets_spec(. ~ .), "at least one variable to facet by")
39+
expect_error(as_facets_spec(list(. ~ .)), "at least one variable to facet by")
40+
expect_error(as_facets_spec(list(NULL)), "at least one variable to facet by")
41+
})
42+
43+
344
df <- data.frame(x = 1:3, y = 3:1, z = letters[1:3])
445

546
test_that("facets split up the data", {

0 commit comments

Comments
 (0)