Skip to content

Commit a208db9

Browse files
yutannihilationthomasp85
authored andcommitted
allow empty facet specs (#3162)
1 parent 00d7315 commit a208db9

File tree

5 files changed

+114
-56
lines changed

5 files changed

+114
-56
lines changed

NEWS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ core developer team.
108108
* `stat_bin()` will now error when the number of bins exceeds 1e6 to avoid
109109
accidentally freezing the user session (@thomasp85).
110110

111+
* `facet_wrap()` and `facet_grid()` now automatically remove NULL from facet
112+
specs, and accept empty specs (@yutannihilation, #3070, #2986).
113+
111114
* `stat_bin()` now handles data with only one unique value (@yutannihilation
112115
#3047).
113116

R/facet-.r

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -275,10 +275,10 @@ df.grid <- function(a, b) {
275275
# facetting variables.
276276

277277
as_facets_list <- function(x) {
278-
if (inherits(x, "mapping")) {
279-
stop("Please use `vars()` to supply facet variables")
278+
if (inherits(x, "uneval")) {
279+
stop("Please use `vars()` to supply facet variables", call. = FALSE)
280280
}
281-
if (inherits(x, "quosures")) {
281+
if (rlang::is_quosures(x)) {
282282
x <- rlang::quos_auto_name(x)
283283
return(list(x))
284284
}
@@ -311,13 +311,16 @@ as_facets_list <- function(x) {
311311
x <- lapply(x, as_facets)
312312
}
313313

314-
if (sum(vapply(x, length, integer(1))) == 0L) {
315-
stop("Must specify at least one variable to facet by", call. = FALSE)
316-
}
317-
318314
x
319315
}
320316

317+
# Flatten a list of quosures objects to a quosures object, and compact it
318+
compact_facets <- function(x) {
319+
x <- rlang::flatten_if(x, rlang::is_list)
320+
null <- vapply(x, rlang::quo_is_null, logical(1))
321+
rlang::new_quosures(x[!null])
322+
}
323+
321324
# Compatibility with plyr::as.quoted()
322325
as_quoted <- function(x) {
323326
if (is.character(x)) {
@@ -360,15 +363,7 @@ f_as_facets_list <- function(f) {
360363
rows <- f_as_facets(lhs(f))
361364
cols <- f_as_facets(rhs(f))
362365

363-
if (length(rows) + length(cols) == 0) {
364-
stop("Must specify at least one variable to facet by", call. = FALSE)
365-
}
366-
367-
if (length(rows)) {
368-
list(rows, cols)
369-
} else {
370-
list(cols)
371-
}
366+
list(rows, cols)
372367
}
373368

374369
as_facets <- function(x) {

R/facet-grid-.r

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -145,54 +145,46 @@ facet_grid <- function(rows = NULL, cols = NULL, scales = "fixed",
145145
}
146146

147147
facets_list <- grid_as_facets_list(rows, cols)
148-
n <- length(facets_list)
149-
if (n > 2L) {
150-
stop("A grid facet specification can't have more than two dimensions", call. = FALSE)
151-
}
152-
if (n == 1L) {
153-
rows <- rlang::quos()
154-
cols <- facets_list[[1]]
155-
} else {
156-
rows <- facets_list[[1]]
157-
cols <- facets_list[[2]]
158-
}
159148

160149
# Check for deprecated labellers
161150
labeller <- check_labeller(labeller)
162151

163152
ggproto(NULL, FacetGrid,
164153
shrink = shrink,
165-
params = list(rows = rows, cols = cols, margins = margins,
154+
params = list(rows = facets_list$rows, cols = facets_list$cols, margins = margins,
166155
free = free, space_free = space_free, labeller = labeller,
167156
as.table = as.table, switch = switch, drop = drop)
168157
)
169158
}
159+
160+
# Returns a list of quosures objects. The list has exactly two elements, `rows` and `cols`.
170161
grid_as_facets_list <- function(rows, cols) {
171162
is_rows_vars <- is.null(rows) || rlang::is_quosures(rows)
172163
if (!is_rows_vars) {
173164
if (!is.null(cols)) {
174165
stop("`rows` must be `NULL` or a `vars()` list if `cols` is a `vars()` list", call. = FALSE)
175166
}
176-
return(as_facets_list(rows))
167+
# For backward-compatibility
168+
facets_list <- as_facets_list(rows)
169+
if (length(facets_list) > 2L) {
170+
stop("A grid facet specification can't have more than two dimensions", call. = FALSE)
171+
}
172+
# Fill with empty quosures
173+
facets <- list(rows = rlang::quos(), cols = rlang::quos())
174+
facets[seq_along(facets_list)] <- facets_list
175+
# Do not compact the legacy specs
176+
return(facets)
177177
}
178178

179179
is_cols_vars <- is.null(cols) || rlang::is_quosures(cols)
180180
if (!is_cols_vars) {
181181
stop("`cols` must be `NULL` or a `vars()` specification", call. = FALSE)
182182
}
183183

184-
if (is.null(rows)) {
185-
rows <- rlang::quos()
186-
} else {
187-
rows <- rlang::quos_auto_name(rows)
188-
}
189-
if (is.null(cols)) {
190-
cols <- rlang::quos()
191-
} else {
192-
cols <- rlang::quos_auto_name(cols)
193-
}
194-
195-
list(rows, cols)
184+
list(
185+
rows = compact_facets(as_facets_list(rows)),
186+
cols = compact_facets(as_facets_list(cols))
187+
)
196188
}
197189

198190
#' @rdname ggplot2-ggproto
@@ -223,6 +215,10 @@ FacetGrid <- ggproto("FacetGrid", Facet,
223215
base_cols <- combine_vars(data, params$plot_env, cols, drop = params$drop)
224216
base <- df.grid(base_rows, base_cols)
225217

218+
if (nrow(base) == 0) {
219+
return(new_data_frame(list(PANEL = 1L, ROW = 1L, COL = 1L, SCALE_X = 1L, SCALE_Y = 1L)))
220+
}
221+
226222
# Add margins
227223
base <- reshape2::add_margins(base, list(names(rows), names(cols)), params$margins)
228224
# Work around bug in reshape2
@@ -253,6 +249,11 @@ FacetGrid <- ggproto("FacetGrid", Facet,
253249
cols <- params$cols
254250
vars <- c(names(rows), names(cols))
255251

252+
if (length(vars) == 0) {
253+
data$PANEL <- layout$PANEL
254+
return(data)
255+
}
256+
256257
# Compute faceting values and add margins
257258
margin_vars <- list(intersect(names(rows), names(data)),
258259
intersect(names(cols), names(data)))

R/facet-wrap.r

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,7 @@ facet_wrap <- function(facets, nrow = NULL, ncol = NULL, scales = "fixed",
109109
labeller <- check_labeller(labeller)
110110

111111
# Flatten all facets dimensions into a single one
112-
facets_list <- as_facets_list(facets)
113-
facets <- rlang::flatten_if(facets_list, rlang::is_list)
112+
facets <- wrap_as_facets_list(facets)
114113

115114
ggproto(NULL, FacetWrap,
116115
shrink = shrink,
@@ -128,6 +127,12 @@ facet_wrap <- function(facets, nrow = NULL, ncol = NULL, scales = "fixed",
128127
)
129128
}
130129

130+
# Returns a quosures object
131+
wrap_as_facets_list <- function(x) {
132+
facets_list <- as_facets_list(x)
133+
compact_facets(facets_list)
134+
}
135+
131136
#' @rdname ggplot2-ggproto
132137
#' @format NULL
133138
#' @usage NULL
@@ -177,8 +182,14 @@ FacetWrap <- ggproto("FacetWrap", Facet,
177182
if (empty(data)) {
178183
return(cbind(data, PANEL = integer(0)))
179184
}
185+
180186
vars <- params$facets
181187

188+
if (length(vars) == 0) {
189+
data$PANEL <- 1L
190+
return(data)
191+
}
192+
182193
facet_vals <- eval_facets(vars, data, params$plot_env)
183194
facet_vals[] <- lapply(facet_vals[], as.factor)
184195

@@ -229,7 +240,12 @@ FacetWrap <- ggproto("FacetWrap", Facet,
229240

230241
axes <- render_axes(ranges, ranges, coord, theme, transpose = TRUE)
231242

232-
labels_df <- layout[names(params$facets)]
243+
if (length(params$facets) == 0) {
244+
# Add a dummy label
245+
labels_df <- new_data_frame(list("(all)" = "(all)"), n = 1)
246+
} else {
247+
labels_df <- layout[names(params$facets)]
248+
}
233249
attr(labels_df, "facet") <- "wrap"
234250
strips <- render_strips(
235251
structure(labels_df, type = "rows"),

tests/testthat/test-facet-.r

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
context("Facetting")
22

33
test_that("as_facets_list() coerces formulas", {
4-
expect_identical(as_facets_list(~foo), list(quos(foo = foo)))
5-
expect_identical(as_facets_list(~foo + bar), list(quos(foo = foo, bar = bar)))
6-
4+
expect_identical(as_facets_list(~foo), list(quos(), quos(foo = foo)))
5+
expect_identical(as_facets_list(~foo + bar), list(quos(), quos(foo = foo, bar = bar)))
76
expect_identical(as_facets_list(foo ~ bar), list(quos(foo = foo), quos(bar = bar)))
87

98
exp <- list(quos(foo = foo, bar = bar), quos(baz = baz, bam = bam))
@@ -18,8 +17,13 @@ test_that("as_facets_list() coerces strings containing formulas", {
1817
})
1918

2019
test_that("as_facets_list() coerces character vectors", {
21-
expect_identical(as_facets_list("foo"), as_facets_list(local(~foo, globalenv())))
22-
expect_identical(as_facets_list(c("foo", "bar")), as_facets_list(local(foo ~ bar, globalenv())))
20+
foo <- rlang::new_quosure(quote(foo), globalenv())
21+
bar <- rlang::new_quosure(quote(bar), globalenv())
22+
foobar <- rlang::as_quosures(list(foo, bar), named = TRUE)
23+
24+
expect_identical(as_facets_list("foo"), list(foobar[1]))
25+
expect_identical(as_facets_list(c("foo", "bar")), list(foobar[1], foobar[2]))
26+
expect_identical(wrap_as_facets_list(c("foo", "bar")), foobar)
2327
})
2428

2529
test_that("as_facets_list() coerces lists", {
@@ -36,17 +40,39 @@ test_that("as_facets_list() coerces lists", {
3640
expect_identical(out, exp)
3741
})
3842

39-
test_that("as_facets_list() errors with empty specs", {
40-
expect_error(as_facets_list(list()), "at least one variable to facet by")
41-
expect_error(as_facets_list(. ~ .), "at least one variable to facet by")
42-
expect_error(as_facets_list(list(. ~ .)), "at least one variable to facet by")
43-
expect_error(as_facets_list(list(NULL)), "at least one variable to facet by")
43+
test_that("as_facets_list() coerces quosures objectss", {
44+
expect_identical(as_facets_list(vars(foo)), list(quos(foo = foo)))
45+
})
46+
47+
test_that("facets reject aes()", {
48+
expect_error(facet_wrap(aes(foo)), "Please use `vars()` to supply facet variables", fixed = TRUE)
49+
expect_error(facet_grid(aes(foo)), "Please use `vars()` to supply facet variables", fixed = TRUE)
4450
})
4551

46-
test_that("as_facets_list() coerces quosure lists", {
47-
expect_identical(as_facets_list(vars(foo)), list(rlang::quos(foo = foo)))
52+
test_that("wrap_as_facets_list() returns a quosures object with compacted", {
53+
expect_identical(wrap_as_facets_list(vars(foo)), quos(foo = foo))
54+
expect_identical(wrap_as_facets_list(~foo + bar), quos(foo = foo, bar = bar))
55+
expect_identical(wrap_as_facets_list(vars(foo, NULL, bar)), quos(foo = foo, bar = bar))
4856
})
4957

58+
test_that("grid_as_facets_list() returns a list of quosures objects with compacted", {
59+
expect_identical(grid_as_facets_list(vars(foo), NULL), list(rows = quos(foo = foo), cols = quos()))
60+
expect_identical(grid_as_facets_list(~foo, NULL), list(rows = quos(), cols = quos(foo = foo)))
61+
expect_identical(grid_as_facets_list(vars(foo, NULL, bar), NULL), list(rows = quos(foo = foo, bar = bar), cols = quos()))
62+
})
63+
64+
test_that("wrap_as_facets_list() and grid_as_facets_list() accept empty specs", {
65+
expect_identical(wrap_as_facets_list(NULL), quos())
66+
expect_identical(wrap_as_facets_list(list()), quos())
67+
expect_identical(wrap_as_facets_list(. ~ .), quos())
68+
expect_identical(wrap_as_facets_list(list(. ~ .)), quos())
69+
expect_identical(wrap_as_facets_list(list(NULL)), quos())
70+
71+
expect_identical(grid_as_facets_list(list(), NULL), list(rows = quos(), cols = quos()))
72+
expect_identical(grid_as_facets_list(. ~ ., NULL), list(rows = quos(), cols = quos()))
73+
expect_identical(grid_as_facets_list(list(. ~ .), NULL), list(rows = quos(), cols = quos()))
74+
expect_identical(grid_as_facets_list(list(NULL), NULL), list(rows = quos(), cols = quos()))
75+
})
5076

5177
df <- data_frame(x = 1:3, y = 3:1, z = letters[1:3])
5278

@@ -110,6 +136,23 @@ test_that("vars() accepts optional names", {
110136
expect_named(wrap$params$facets, c("A", "b"))
111137
})
112138

139+
test_that("facets_wrap() compacts the facet spec and accept empty spec", {
140+
p <- ggplot(df, aes(x, y)) + geom_point() + facet_wrap(vars(NULL))
141+
d <- layer_data(p)
142+
143+
expect_equal(d$PANEL, c(1L, 1L, 1L))
144+
expect_equal(d$group, c(-1L, -1L, -1L))
145+
})
146+
147+
test_that("facets_grid() compacts the facet spec and accept empty spec", {
148+
p <- ggplot(df, aes(x, y)) + geom_point() + facet_grid(vars(NULL))
149+
d <- layer_data(p)
150+
151+
expect_equal(d$PANEL, c(1L, 1L, 1L))
152+
expect_equal(d$group, c(-1L, -1L, -1L))
153+
})
154+
155+
113156
test_that("facets with free scales scale independently", {
114157
l1 <- ggplot(df, aes(x, y)) + geom_point() +
115158
facet_wrap(~z, scales = "free")

0 commit comments

Comments
 (0)