Skip to content

Commit a8964b2

Browse files
authored
Merge branch 'main' into bump_deprecations
2 parents e2bae39 + 5b99d3c commit a8964b2

19 files changed

+157
-32
lines changed

NEWS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
of warnings.
55
* Functions and arguments that were soft-deprecated up to ggplot2 3.4.0 now
66
throw warnings.
7+
* (internal) layer data can be attenuated with parameter attributes
8+
(@teunbrand, #3175).
9+
* Date scales silently coerce <POSIXct> to <Date> and datetime scales silently
10+
coerce <Date> to <POSIXct> (@laurabrianna, #3533)
711
* New parameters for `geom_label()` (@teunbrand and @steveharoz, #5365):
812
* The `linewidth` aesthetic is now applied and replaces the `label.size`
913
argument.
@@ -306,10 +310,14 @@
306310
particularly for data-points with a low radius near the center
307311
(@teunbrand, #5023).
308312
* All scales now expose the `aesthetics` parameter (@teunbrand, #5841)
313+
* Staged expressions are handled more gracefully if legends cannot resolve them
314+
(@teunbrand, #6264).
309315
* New `theme(legend.key.justification)` to control the alignment of legend keys
310316
(@teunbrand, #3669).
311317
* Added `scale_{x/y}_time(date_breaks, date_minor_breaks, date_labels)`
312318
(@teunbrand, #4335).
319+
* `ggsave()` can write a multi-page pdf file when provided with a list of plots
320+
(@teunbrand, #5093).
313321

314322
# ggplot2 3.5.1
315323

R/geom-.R

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,15 @@ Geom <- ggproto("Geom",
163163
# If any after_scale mappings are detected they will be resolved here
164164
# This order means that they will have access to all default aesthetics
165165
if (length(modifiers) != 0) {
166-
# Set up evaluation environment
167-
modified_aes <- eval_aesthetics(
168-
substitute_aes(modifiers), data,
169-
mask = list(stage = stage_scaled)
166+
modified_aes <- try_fetch(
167+
eval_aesthetics(
168+
substitute_aes(modifiers), data,
169+
mask = list(stage = stage_scaled)
170+
),
171+
error = function(cnd) {
172+
cli::cli_warn("Unable to apply staged modifications.", parent = cnd)
173+
data_frame0()
174+
}
170175
)
171176

172177
# Check that all output are valid data
@@ -177,10 +182,7 @@ Geom <- ggproto("Geom",
177182
)
178183

179184
modified_aes <- cleanup_mismatched_data(modified_aes, nrow(data), "after_scale")
180-
181-
modified_aes <- data_frame0(!!!modified_aes)
182-
183-
data <- data_frame0(!!!defaults(modified_aes, data))
185+
data[names(modified_aes)] <- modified_aes
184186
}
185187

186188
# Override mappings with params

R/geom-crossbar.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ geom_crossbar <- function(mapping = NULL, data = NULL,
6060
#' @export
6161
GeomCrossbar <- ggproto("GeomCrossbar", Geom,
6262
setup_params = function(data, params) {
63-
if (lifecycle::is_present(params$fatten)) {
63+
if (lifecycle::is_present(params$fatten %||% deprecated())) {
6464
deprecate_soft0(
6565
"3.6.0", "geom_crossbar(fatten)",
6666
"geom_crossbar(middle.linewidth)"

R/geom-pointrange.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ GeomPointrange <- ggproto("GeomPointrange", Geom,
4242
required_aes = c("x", "y", "ymin|xmin", "ymax|xmax"),
4343

4444
setup_params = function(data, params) {
45-
if (lifecycle::is_present(params$fatten)) {
45+
if (lifecycle::is_present(params$fatten %||% deprecated())) {
4646
deprecate_soft0("3.6.0", "geom_pointrange(fatten)", I("the `size` aesthetic"))
4747
} else {
4848
# For backward compatibility reasons

R/geom-ribbon.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ GeomRibbon <- ggproto("GeomRibbon", Geom,
183183
if ((length(aes$fill) > 1 || length(aes$alpha) > 1)) {
184184
transformed <- coord$transform(flip_data(data, flipped_aes), panel_params)
185185
if (flipped_aes) {
186-
keep <- is.finite(tranformed$y)
186+
keep <- is.finite(transformed$y)
187187
args <- list(
188188
colours = alpha(data$fill, data$alpha)[keep],
189189
stops = rescale(transformed$y)[keep],

R/layer.R

Lines changed: 16 additions & 9 deletions
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/position-nudge.R

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@
2727
#' ggplot(df, aes(x, y)) +
2828
#' geom_point() +
2929
#' geom_text(aes(label = y), nudge_y = -0.1)
30+
#'
31+
#' # For each text individually
32+
#' ggplot(df, aes(x, y)) +
33+
#' geom_point() +
34+
#' geom_text(aes(label = y, nudge_y = c(-0.1, 0.1, -0.1, 0.1)))
3035
position_nudge <- function(x = NULL, y = NULL) {
3136
ggproto(NULL, PositionNudge,
3237
x = x,

R/save.R

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,17 +101,18 @@ ggsave <- function(filename, plot = get_last_plot(),
101101
dev <- validate_device(device, filename, dpi = dpi)
102102
dim <- plot_dim(c(width, height), scale = scale, units = units,
103103
limitsize = limitsize, dpi = dpi)
104+
bg <- get_plot_background(plot, bg)
104105

105-
if (is_null(bg)) {
106-
bg <- calc_element("plot.background", plot_theme(plot))$fill %||% "transparent"
107-
}
108106
old_dev <- grDevices::dev.cur()
109107
dev(filename = filename, width = dim[1], height = dim[2], bg = bg, ...)
110108
on.exit(utils::capture.output({
111109
grDevices::dev.off()
112110
if (old_dev > 1) grDevices::dev.set(old_dev) # restore old device unless null device
113111
}))
114-
grid.draw(plot)
112+
if (!is_bare_list(plot)) {
113+
plot <- list(plot)
114+
}
115+
lapply(plot, grid.draw)
115116

116117
invisible(filename)
117118
}
@@ -235,6 +236,17 @@ plot_dim <- function(dim = c(NA, NA), scale = 1, units = "in",
235236
dim
236237
}
237238

239+
get_plot_background <- function(plot, bg = NULL, default = "transparent") {
240+
if (!is.null(bg)) {
241+
return(bg)
242+
}
243+
plot <- if (is_bare_list(plot)) plot[[1]] else plot
244+
if (!is.ggplot(plot)) {
245+
return(default)
246+
}
247+
calc_element("plot.background", plot_theme(plot))$fill %||% default
248+
}
249+
238250
validate_device <- function(device, filename = NULL, dpi = 300, call = caller_env()) {
239251
force(filename)
240252
force(dpi)

R/scale-date.R

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,9 @@ ScaleContinuousDatetime <- ggproto("ScaleContinuousDatetime", ScaleContinuous,
394394
i = "The value was converted to {obj_type_friendly(x)}."
395395
), call = self$call)
396396
}
397+
if (inherits(x, "Date")) {
398+
x <- as.POSIXct(x)
399+
}
397400
ggproto_parent(ScaleContinuous, self)$transform(x)
398401
},
399402
map = function(self, x, limits = self$get_limits()) {
@@ -441,6 +444,9 @@ ScaleContinuousDate <- ggproto("ScaleContinuousDate", ScaleContinuous,
441444
i = "The value was converted to {obj_type_friendly(x)}."
442445
), call = self$call)
443446
}
447+
if (inherits(x, "POSIXct")) {
448+
x <- as.Date(x)
449+
}
444450
ggproto_parent(ScaleContinuous, self)$transform(x)
445451
},
446452
get_breaks = function(self, limits = self$get_limits()) {

R/scale-linewidth.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#'
33
#' `scale_linewidth` scales the width of lines and polygon strokes. Due to
44
#' historical reasons, it is also possible to control this with the `size`
5-
#' aesthetic, but using `linewidth` is encourage to clearly differentiate area
5+
#' aesthetic, but using `linewidth` is encouraged to clearly differentiate area
66
#' aesthetics from stroke width aesthetics.
77
#'
88
#' @name scale_linewidth

R/scales-.R

Lines changed: 6 additions & 6 deletions
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

Lines changed: 8 additions & 0 deletions
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)

man/position_nudge.Rd

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/scale_linewidth.Rd

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/_snaps/guide-legend.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# unresolved, modified expressions throw a warning (#6264)
2+
3+
Unable to apply staged modifications.
4+
Caused by error:
5+
! object 'prop' not found
6+

tests/testthat/test-guide-legend.R

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,15 @@ test_that("legends can be forced to display unrelated geoms", {
136136
)
137137
})
138138

139+
test_that("unresolved, modified expressions throw a warning (#6264)", {
140+
# Snapshot is unstable in lesser R versions
141+
skip_if_not(getRversion() >= "4.3.0")
142+
p <- ggplot(mpg, aes(drv)) +
143+
geom_bar(
144+
aes(fill = stage(drv, after_scale = alpha(fill, prop)))
145+
)
146+
expect_snapshot_warning(ggplot_build(p))
147+
})
139148

140149
# Visual tests ------------------------------------------------------------
141150

tests/testthat/test-layer.R

Lines changed: 23 additions & 0 deletions
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-scale_date.R

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,19 @@
11

2+
test_that("date(time) scales coerce data types", {
3+
4+
date <- as.Date("2024-11-11")
5+
datetime <- as.POSIXct(date)
6+
7+
sc <- scale_x_datetime()
8+
df <- sc$transform_df(data_frame0(x = date))
9+
expect_equal(df$x, as.numeric(datetime))
10+
11+
sc <- scale_x_date()
12+
df <- sc$transform_df(data_frame0(x = datetime))
13+
expect_equal(df$x, as.numeric(date))
14+
15+
})
16+
217
# Visual tests ------------------------------------------------------------
318

419
test_that("date scale draws correctly", {

tests/testthat/test-stats.R

Lines changed: 19 additions & 0 deletions
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)