Skip to content

Commit 3c9c504

Browse files
authored
Merge pull request #2491 from tidyverse/tidyeval-facets
Port facets to tidy eval
2 parents cab8a54 + 449bc03 commit 3c9c504

File tree

13 files changed

+594
-107
lines changed

13 files changed

+594
-107
lines changed

NAMESPACE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,7 @@ export(unit)
548548
export(update_geom_defaults)
549549
export(update_labels)
550550
export(update_stat_defaults)
551+
export(vars)
551552
export(waiver)
552553
export(wrap_dims)
553554
export(xlab)
@@ -559,7 +560,6 @@ import(grid)
559560
import(gtable)
560561
import(scales)
561562
importFrom(lazyeval,f_eval)
562-
importFrom(plyr,as.quoted)
563563
importFrom(plyr,defaults)
564564
importFrom(rlang,.data)
565565
importFrom(rlang,enexpr)

NEWS.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,26 @@
66
and `:=`. This replaces `aes_()` and `aes_string()` which are now
77
soft-deprecated (but will remain around for a long time).
88

9+
* `facet_wrap()` and `facet_grid()` now support `vars()` inputs. Like
10+
`dplyr::vars()`, this helper quotes its inputs and supports
11+
quasiquotation. For instance you can now supply facetting variables
12+
like this: `facet_wrap(vars(am, cyl))` instead of `facet_wrap(~am +
13+
cyl)`. Note that the formula interface is not going away and will
14+
not be deprecated. `vars()` is simply meant to make it easier to
15+
create functions around `facet_wrap()` and `facet_grid()`.
16+
17+
The first two arguments of `facet_grid()` become `rows` and `cols`
18+
and now support `vars()` inputs. Note however that we took special
19+
care to ensure complete backward compatibility. With this change
20+
`facet_grid(vars(cyl), vars(am, vs))` is equivalent to
21+
`facet_grid(cyl ~ am + vs)` and `facet_grid(cols = vars(am, vs))` is
22+
equivalent to `facet_grid(. ~ am + vs)`.
23+
24+
One nice aspect of the new interface is that you can now easily
25+
supply names: `facet_grid(vars(Cylinder = cyl), labeller =
26+
label_both)` will give nice label titles to the facets. Of course
27+
those names can be unquoted with the usual tidy eval syntax.
28+
929
* ggplot2 now works on R 3.1 onwards, and uses the
1030
[vdiffr](https://github.com/lionel-/vdiffr) package for visual testing.
1131

R/aes.r

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ NULL
4747
#' @param x,y,... List of name value pairs giving aesthetics to map to
4848
#' variables. The names for x and y aesthetics are typically omitted because
4949
#' they are so common; all other aesthetics must be named.
50+
#' @seealso [vars()] for another quoting function designed for
51+
#' faceting specifications.
5052
#' @export
5153
#' @examples
5254
#' aes(x = mpg, y = wt)

R/facet-.r

Lines changed: 224 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,67 @@ Facet <- ggproto("Facet", NULL,
170170

171171
# Helpers -----------------------------------------------------------------
172172

173+
#' Quote faceting variables
174+
#'
175+
#' @description
176+
#'
177+
#' Just like [aes()], `vars()` is a [quoting function][rlang::quotation]
178+
#' that takes inputs to be evaluated in the context of a dataset.
179+
#' These inputs can be:
180+
#'
181+
#' * variable names
182+
#' * complex expressions
183+
#'
184+
#' In both cases, the results (the vectors that the variable
185+
#' represents or the results of the expressions) are used to form
186+
#' faceting groups.
187+
#'
188+
#' @param ... Variables or expressions automatically quoted. These are
189+
#' evaluated in the context of the data to form faceting groups. Can
190+
#' be named (the names are passed to a [labeller][labellers]).
191+
#'
192+
#' @seealso [aes()], [facet_wrap()], [facet_grid()]
193+
#' @export
194+
#' @examples
195+
#' p <- ggplot(diamonds) + geom_point(aes(carat, price))
196+
#' p + facet_wrap(vars(cut, clarity))
197+
#'
198+
#' # vars() makes it easy to pass variables from wrapper functions:
199+
#' wrap_by <- function(...) {
200+
#' facet_wrap(vars(...), labeller = label_both)
201+
#' }
202+
#' p + wrap_by(cut)
203+
#' p + wrap_by(cut, clarity)
204+
#'
205+
#'
206+
#' # You can also supply expressions to vars(). In this case it's often a
207+
#' # good idea to supply a name as well:
208+
#' p + wrap_by(depth = cut_number(depth, 3))
209+
#'
210+
#' # Let's create another function for cutting and wrapping a
211+
#' # variable. This time it will take a named argument instead of dots,
212+
#' # so we'll have to use the "enquote and unquote" pattern:
213+
#' wrap_cut <- function(var, n = 3) {
214+
#' # Let's enquote the named argument `var` to make it auto-quoting:
215+
#' var <- enquo(var)
216+
#'
217+
#' # `quo_name()` will create a nice default name:
218+
#' nm <- quo_name(var)
219+
#'
220+
#' # Now let's unquote everything at the right place. Note that we also
221+
#' # unquote `n` just in case the data frame has a column named
222+
#' # `n`. The latter would have precedence over our local variable
223+
#' # because the data is always masking the environment.
224+
#' wrap_by(!!nm := cut_number(!!var, !!n))
225+
#' }
226+
#'
227+
#' # Thanks to tidy eval idioms we now have another useful wrapper:
228+
#' p + wrap_cut(depth)
229+
vars <- function(...) {
230+
rlang::quos(...)
231+
}
232+
233+
173234
#' Is this object a faceting specification?
174235
#'
175236
#' @param x object to test
@@ -203,38 +264,180 @@ df.grid <- function(a, b) {
203264
))
204265
}
205266

267+
# A facets spec is a list of facets. A grid facetting needs two facets
268+
# while a wrap facetting flattens all dimensions and thus accepts any
269+
# number of facets.
270+
#
271+
# A facets is a list of grouping variables. They are typically
272+
# supplied as variable names but can be expressions.
273+
#
274+
# as_facets() is complex due to historical baggage but its main
275+
# purpose is to create a facets spec from a formula: a + b ~ c + d
276+
# creates a facets list with two components, each of which bundles two
277+
# facetting variables.
278+
279+
as_facets_list <- function(x) {
280+
if (inherits(x, "mapping")) {
281+
stop("Please use `vars()` to supply facet variables")
282+
}
283+
if (inherits(x, "quosures")) {
284+
x <- rlang::quos_auto_name(x)
285+
return(list(x))
286+
}
287+
288+
# This needs to happen early because we might get a formula.
289+
# facet_grid() directly converted strings to a formula while
290+
# facet_wrap() called as.quoted(). Hence this is a little more
291+
# complicated for backward compatibility.
292+
if (rlang::is_string(x)) {
293+
x <- rlang::parse_expr(x)
294+
}
295+
296+
# At this level formulas are coerced to lists of lists for backward
297+
# compatibility with facet_grid(). The LHS and RHS are treated as
298+
# distinct facet dimensions and `+` defines multiple facet variables
299+
# inside each dimension.
300+
if (rlang::is_formula(x)) {
301+
return(f_as_facets_list(x))
302+
}
303+
304+
# For backward-compatibility with facet_wrap()
305+
if (!rlang::is_bare_list(x)) {
306+
x <- as_quoted(x)
307+
}
308+
309+
# If we have a list there are two possibilities. We may already have
310+
# a proper facet spec structure. Otherwise we coerce each element
311+
# with as_quoted() for backward compatibility with facet_grid().
312+
if (is.list(x)) {
313+
x <- lapply(x, as_facets)
314+
}
315+
316+
if (sum(vapply(x, length, integer(1))) == 0L) {
317+
stop("Must specify at least one variable to facet by", call. = FALSE)
318+
}
319+
320+
x
321+
}
322+
323+
# Compatibility with plyr::as.quoted()
324+
as_quoted <- function(x) {
325+
if (is.character(x)) {
326+
return(rlang::parse_exprs(x))
327+
}
328+
if (is.null(x)) {
329+
return(list())
330+
}
331+
if (rlang::is_formula(x)) {
332+
return(simplify(x))
333+
}
334+
list(x)
335+
}
336+
# From plyr:::as.quoted.formula
337+
simplify <- function(x) {
338+
if (length(x) == 2 && rlang::is_symbol(x[[1]], "~")) {
339+
return(simplify(x[[2]]))
340+
}
341+
if (length(x) < 3) {
342+
return(list(x))
343+
}
344+
op <- x[[1]]; a <- x[[2]]; b <- x[[3]]
345+
346+
if (rlang::is_symbol(op, c("+", "*", "~"))) {
347+
c(simplify(a), simplify(b))
348+
} else if (rlang::is_symbol(op, "-")) {
349+
c(simplify(a), expr(-!!simplify(b)))
350+
} else {
351+
list(x)
352+
}
353+
}
354+
355+
f_as_facets_list <- function(f) {
356+
lhs <- function(x) if (length(x) == 2) NULL else x[-3]
357+
rhs <- function(x) if (length(x) == 2) x else x[-2]
358+
359+
rows <- f_as_facets(lhs(f))
360+
cols <- f_as_facets(rhs(f))
361+
362+
if (length(rows) + length(cols) == 0) {
363+
stop("Must specify at least one variable to facet by", call. = FALSE)
364+
}
365+
366+
if (length(rows)) {
367+
list(rows, cols)
368+
} else {
369+
list(cols)
370+
}
371+
}
372+
373+
as_facets <- function(x) {
374+
if (is_facets(x)) {
375+
return(x)
376+
}
377+
378+
if (rlang::is_formula(x)) {
379+
# Use different formula method because plyr's does not handle the
380+
# environment correctly.
381+
f_as_facets(x)
382+
} else {
383+
vars <- as_quoted(x)
384+
rlang::as_quosures(vars, globalenv(), named = TRUE)
385+
}
386+
}
387+
f_as_facets <- function(f) {
388+
if (is.null(f)) {
389+
return(rlang::as_quosures(list()))
390+
}
391+
392+
env <- rlang::f_env(f) %||% globalenv()
393+
394+
# as.quoted() handles `+` specifications
395+
vars <- plyr::as.quoted(f)
396+
397+
# `.` in formulas is ignored
398+
vars <- discard_dots(vars)
399+
400+
rlang::as_quosures(vars, env, named = TRUE)
401+
}
402+
discard_dots <- function(x) {
403+
x[!vapply(x, identical, logical(1), as.name("."))]
404+
}
405+
406+
is_facets <- function(x) {
407+
if (!is.list(x)) {
408+
return(FALSE)
409+
}
410+
if (!length(x)) {
411+
return(FALSE)
412+
}
413+
all(vapply(x, rlang::is_quosure, logical(1)))
414+
}
415+
416+
206417
# When evaluating variables in a facet specification, we evaluate bare
207418
# variables and expressions slightly differently. Bare variables should
208419
# always succeed, even if the variable doesn't exist in the data frame:
209420
# that makes it possible to repeat data across multiple factors. But
210421
# when evaluating an expression, you want to see any errors. That does
211422
# mean you can't have background data when faceting by an expression,
212423
# 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)
424+
eval_facets <- function(facets, data, env = globalenv()) {
425+
vars <- compact(lapply(facets, eval_facet, data, env = env))
426+
tibble::as_tibble(vars)
222427
}
428+
eval_facet <- function(facet, data, env = emptyenv()) {
429+
if (rlang::quo_is_symbol(facet)) {
430+
facet <- as.character(rlang::quo_get_expr(facet))
223431

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]]
432+
if (facet %in% names(data)) {
433+
out <- data[[facet]]
229434
} else {
230-
NULL
435+
out <- NULL
231436
}
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)
437+
return(out)
237438
}
439+
440+
rlang::eval_tidy(facet, data, env)
238441
}
239442

240443
layout_null <- function() {
@@ -325,7 +528,7 @@ combine_vars <- function(data, env = emptyenv(), vars = NULL, drop = TRUE) {
325528
if (length(vars) == 0) return(data.frame())
326529

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

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

0 commit comments

Comments
 (0)