Skip to content

Commit 5b99d3c

Browse files
authored
Stability of layer data attributes (#6194)
* prevent dropping attributes * restore attributes after layer methods * add test * add news bullet * include scale transforms * add test
1 parent 5fe3c2a commit 5b99d3c

File tree

7 files changed

+75
-19
lines changed

7 files changed

+75
-19
lines changed

NEWS.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# ggplot2 (development version)
22

3+
* (internal) layer data can be attenuated with parameter attributes
4+
(@teunbrand, #3175).
35
* Date scales silently coerce <POSIXct> to <Date> and datetime scales silently
46
coerce <Date> to <POSIXct> (@laurabrianna, #3533)
57
* New parameters for `geom_label()` (@teunbrand and @steveharoz, #5365):

R/geom-.R

+1-4
Original file line numberDiff line numberDiff line change
@@ -182,10 +182,7 @@ Geom <- ggproto("Geom",
182182
)
183183

184184
modified_aes <- cleanup_mismatched_data(modified_aes, nrow(data), "after_scale")
185-
186-
modified_aes <- data_frame0(!!!modified_aes)
187-
188-
data <- data_frame0(!!!defaults(modified_aes, data))
185+
data[names(modified_aes)] <- modified_aes
189186
}
190187

191188
# Override mappings with params

R/layer.R

+16-9
Original file line numberDiff line numberDiff line change
@@ -347,12 +347,13 @@ Layer <- ggproto("Layer", NULL,
347347
},
348348

349349
compute_statistic = function(self, data, layout) {
350-
if (empty(data))
351-
return(data_frame0())
350+
if (empty(data)) return(data_frame0())
352351

352+
ptype <- vec_ptype(data)
353353
self$computed_stat_params <- self$stat$setup_params(data, self$stat_params)
354354
data <- self$stat$setup_data(data, self$computed_stat_params)
355-
self$stat$compute_layer(data, self$computed_stat_params, layout)
355+
data <- self$stat$compute_layer(data, self$computed_stat_params, layout)
356+
merge_attrs(data, ptype)
356357
},
357358

358359
map_statistic = function(self, data, plot) {
@@ -396,30 +397,32 @@ Layer <- ggproto("Layer", NULL,
396397
stat_data <- plot$scales$transform_df(stat_data)
397398
}
398399
stat_data <- cleanup_mismatched_data(stat_data, nrow(data), "after_stat")
399-
400-
data_frame0(!!!defaults(stat_data, data))
400+
data[names(stat_data)] <- stat_data
401+
data
401402
},
402403

403404
compute_geom_1 = function(self, data) {
404405
if (empty(data)) return(data_frame0())
406+
ptype <- vec_ptype(data)
405407

406408
check_required_aesthetics(
407409
self$geom$required_aes,
408410
c(names(data), names(self$aes_params)),
409411
snake_class(self$geom)
410412
)
411413
self$computed_geom_params <- self$geom$setup_params(data, c(self$geom_params, self$aes_params))
412-
self$geom$setup_data(data, self$computed_geom_params)
414+
data <- self$geom$setup_data(data, self$computed_geom_params)
415+
merge_attrs(data, ptype)
413416
},
414417

415418
compute_position = function(self, data, layout) {
416419
if (empty(data)) return(data_frame0())
417-
420+
ptype <- vec_ptype(data)
418421
data <- self$position$use_defaults(data, self$aes_params)
419422
params <- self$position$setup_params(data)
420423
data <- self$position$setup_data(data, params)
421-
422-
self$position$compute_layer(data, params, layout)
424+
data <- self$position$compute_layer(data, params, layout)
425+
merge_attrs(data, ptype)
423426
},
424427

425428
compute_geom_2 = function(self, data, params = self$aes_params, theme = NULL, ...) {
@@ -484,6 +487,10 @@ set_draw_key <- function(geom, draw_key = NULL) {
484487
}
485488

486489
cleanup_mismatched_data <- function(data, n, fun) {
490+
if (vec_duplicate_any(names(data))) {
491+
data <- data[unique0(names(data))]
492+
}
493+
487494
failed <- !lengths(data) %in% c(0, 1, n)
488495
if (!any(failed)) {
489496
return(data)

R/scales-.R

+6-6
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ ScalesList <- ggproto("ScalesList", NULL,
7878
function(scale) scale$map_df(df = df)
7979
), recursive = FALSE)
8080

81-
data_frame0(!!!mapped, df[setdiff(names(df), names(mapped))])
81+
df[names(mapped)] <- mapped
82+
df
8283
},
8384

8485
transform_df = function(self, df) {
@@ -104,7 +105,8 @@ ScalesList <- ggproto("ScalesList", NULL,
104105
function(scale) scale$transform_df(df = df)
105106
), recursive = FALSE)
106107

107-
data_frame0(!!!transformed, df[setdiff(names(df), names(transformed))])
108+
df[names(transformed)] <- transformed
109+
df
108110
},
109111

110112
backtransform_df = function(self, df) {
@@ -139,10 +141,8 @@ ScalesList <- ggproto("ScalesList", NULL,
139141
}
140142
), recursive = FALSE)
141143

142-
data_frame0(
143-
!!!backtransformed,
144-
df[setdiff(names(df), names(backtransformed))]
145-
)
144+
df[names(backtransformed)] <- backtransformed
145+
df
146146
},
147147

148148
# `aesthetics` is a list of aesthetic-variable mappings. The name of each

R/utilities.R

+8
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,14 @@ toupper <- function(x) {
249249
cli::cli_abort("Please use {.fn to_upper_ascii}, which works fine in all locales.")
250250
}
251251

252+
merge_attrs <- function(new, old) {
253+
new_attr <- attributes(new)
254+
new <- vec_restore(new, old) # copies old attributes to new
255+
new_attr <- new_attr[setdiff(names(new_attr), names(attributes(new)))]
256+
attributes(new) <- c(attributes(new), new_attr)
257+
new
258+
}
259+
252260
# Convert a snake_case string to camelCase
253261
camelize <- function(x, first = FALSE) {
254262
x <- gsub("_(.)", "\\U\\1", x, perl = TRUE)

tests/testthat/test-layer.R

+23
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,29 @@ test_that("layer names can be resolved", {
154154
expect_snapshot(p + l + l, error = TRUE)
155155
})
156156

157+
test_that("attributes on layer data are preserved", {
158+
# This is a good layer for testing because:
159+
# * It needs to compute a statistic at the group level
160+
# * It needs to setup data to reshape x/y/width/height into xmin/xmax/ymin/ymax
161+
# * It needs to use a position adjustment
162+
# * It has an `after_stat()` so it enters the map_statistic method
163+
old <- stat_summary(
164+
aes(fill = after_stat(y)),
165+
fun = mean, geom = "col", position = "dodge"
166+
)
167+
# We modify the compute aesthetics method to append a test attribute
168+
new <- ggproto(NULL, old, compute_aesthetics = function(self, data, plot) {
169+
data <- ggproto_parent(old, self)$compute_aesthetics(data, plot)
170+
attr(data, "test") <- "preserve me"
171+
data
172+
})
173+
# At the end of plot building, we want to retrieve that metric
174+
ld <- layer_data(
175+
ggplot(mpg, aes(drv, hwy, colour = factor(year))) + new + facet_grid(~year) +
176+
scale_y_sqrt()
177+
)
178+
expect_equal(attr(ld, "test"), "preserve me")
179+
})
157180

158181
# Data extraction ---------------------------------------------------------
159182

tests/testthat/test-stats.R

+19
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,22 @@ test_that("erroneously dropped aesthetics are found and issue a warning", {
6969
c(TRUE, FALSE, FALSE)
7070
)
7171
})
72+
73+
test_that("stats can modify persistent attributes", {
74+
75+
StatTest <- ggproto(
76+
"StatTest", Stat,
77+
compute_layer = function(self, data, params, layout) {
78+
attr(data, "foo") <- "bar"
79+
data
80+
}
81+
)
82+
83+
p <- ggplot(mtcars, aes(disp, mpg)) +
84+
geom_point(stat = StatTest) +
85+
facet_wrap(~cyl)
86+
87+
ld <- layer_data(p)
88+
expect_equal(attr(ld, "foo"), "bar")
89+
90+
})

0 commit comments

Comments
 (0)