Skip to content

Commit 32534e9

Browse files
Drop non-constant aesthetics more thoroughly (#4917)
1 parent 5ebe979 commit 32534e9

File tree

2 files changed

+87
-11
lines changed

2 files changed

+87
-11
lines changed

R/stat-.r

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,30 +120,61 @@ Stat <- ggproto("Stat",
120120
self$compute_group(data = group, scales = scales, ...)
121121
})
122122

123+
# Record columns that are not constant within groups. We will drop them later.
124+
non_constant_columns <- character(0)
125+
123126
stats <- mapply(function(new, old) {
127+
# In this function,
128+
#
129+
# - `new` is the computed result. All the variables will be picked.
130+
# - `old` is the original data. There are 3 types of variables:
131+
# 1) If the variable is already included in `new`, it's ignored
132+
# because the values of `new` will be used.
133+
# 2) If the variable is not included in `new` and the value is
134+
# constant within the group, it will be picked.
135+
# 3) If the variable is not included in `new` and the value is not
136+
# constant within the group, it will be dropped. We need to record
137+
# the dropped columns to drop it consistently later.
138+
124139
if (empty(new)) return(data_frame0())
125-
unique <- uniquecols(old)
126-
missing <- !(names(unique) %in% names(new))
140+
141+
# First, filter out the columns already included `new` (type 1).
142+
old <- old[, !(names(old) %in% names(new)), drop = FALSE]
143+
144+
# Then, check whether the rest of the columns have constant values (type 2)
145+
# or not (type 3).
146+
non_constant <- vapply(old, function(x) length(unique0(x)) > 1, logical(1L))
147+
148+
# Record the non-constant columns.
149+
non_constant_columns <<- c(non_constant_columns, names(old)[non_constant])
150+
127151
vec_cbind(
128152
new,
129-
unique[rep(1, nrow(new)), missing,drop = FALSE]
153+
# Note that, while the non-constant columns should be dropped, we don't
154+
# do this here because it can be filled by vec_rbind() later if either
155+
# one of the group has a constant value (see #4394 for the details).
156+
old[rep(1, nrow(new)), , drop = FALSE]
130157
)
131158
}, stats, groups, SIMPLIFY = FALSE)
132159

133-
data_new <- vec_rbind(!!!stats)
160+
non_constant_columns <- unique0(non_constant_columns)
134161

135-
# The above code will drop columns that are not constant within groups and not
162+
# We are going to drop columns that are not constant within groups and not
136163
# carried over/recreated by the stat. This can produce unexpected results,
137-
# and hence we warn about it.
138-
dropped <- base::setdiff(names(data), base::union(self$dropped_aes, names(data_new)))
164+
# and hence we warn about it (variables in dropped_aes are expected so
165+
# ignored here).
166+
dropped <- non_constant_columns[!non_constant_columns %in% self$dropped_aes]
139167
if (length(dropped) > 0) {
140168
cli::cli_warn(c(
141169
"The following aesthetics were dropped during statistical transformation: {.field {glue_collapse(dropped, sep = ', ')}}",
142170
"i" = "This can happen when ggplot fails to infer the correct grouping structure in the data.",
143171
"i" = "Did you forget to specify a {.code group} aesthetic or to convert a numerical variable into a factor?"
144172
))
145173
}
146-
data_new
174+
175+
# Finally, combine the results and drop columns that are not constant.
176+
data_new <- vec_rbind(!!!stats)
177+
data_new[, !names(data_new) %in% non_constant_columns, drop = FALSE]
147178
},
148179

149180
compute_group = function(self, data, scales) {

tests/testthat/test-stats.r

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,59 @@ test_that("error message is thrown when aesthetics are missing", {
2121
})
2222

2323
test_that("erroneously dropped aesthetics are found and issue a warning", {
24-
df <- data_frame(
24+
25+
# case 1) dropped completely
26+
27+
df1 <- data_frame(
2528
x = c( # arbitrary random numbers
2629
0.42986445, 1.11153170, -1.22318013, 0.90982003,
2730
0.46454276, -0.42300004, -1.76139834, -0.75060412,
2831
0.01635474, -0.63202159
2932
),
3033
g = rep(1:2, each = 5)
3134
)
32-
p <- ggplot(df, aes(x, fill = g)) + geom_density()
33-
expect_warning(ggplot_build(p), "aesthetics were dropped")
35+
p1 <- ggplot(df1, aes(x, fill = g)) + geom_density()
36+
expect_warning(ggplot_build(p1), "aesthetics were dropped")
37+
38+
# case 2-1) dropped partially
39+
40+
df2 <- data_frame(
41+
id = c("a", "a", "b", "b", "c"),
42+
colour = c( 0, 1, 10, 10, 20), # a should be dropped
43+
fill = c( 0, 0, 10, 11, 20) # b should be dropped
44+
)
45+
46+
p2 <- ggplot(df2, aes(id, colour = colour, fill = fill)) + geom_bar()
47+
expect_warning(
48+
b2 <- ggplot_build(p2),
49+
"The following aesthetics were dropped during statistical transformation: .*colour.*, .*fill.*"
50+
)
51+
52+
# colour is dropped because group a's colour is not constant (GeomBar$default_aes$colour is NA)
53+
expect_true(all(is.na(b2$data[[1]]$colour)))
54+
# fill is dropped because group b's fill is not constant
55+
expect_true(all(b2$data[[1]]$fill == GeomBar$default_aes$fill))
56+
57+
# case 2-1) dropped partially with NA
58+
59+
df3 <- data_frame(
60+
id = c("a", "a", "b", "b", "c"),
61+
colour = c( 0, NA, 10, 10, 20), # a should be dropped
62+
fill = c( NA, NA, 10, 10, 20) # a should not be dropped
63+
)
64+
65+
p3 <- ggplot(df3, aes(id, colour = colour, fill = fill)) + geom_bar() +
66+
scale_fill_continuous(na.value = "#123")
67+
expect_warning(
68+
b3 <- ggplot_build(p3),
69+
"The following aesthetics were dropped during statistical transformation: .*colour.*"
70+
)
71+
72+
# colour is dropped because group a's colour is not constant (GeomBar$default_aes$colour is NA)
73+
expect_true(all(is.na(b3$data[[1]]$colour)))
74+
# fill is NOT dropped. Group a's fill is na.value, but others are mapped.
75+
expect_equal(
76+
b3$data[[1]]$fill == "#123",
77+
c(TRUE, FALSE, FALSE)
78+
)
3479
})

0 commit comments

Comments
 (0)