Skip to content

Commit 09ef058

Browse files
Backtransform data before mapping statistics (#4194)
1 parent 3375667 commit 09ef058

File tree

5 files changed

+56
-3
lines changed

5 files changed

+56
-3
lines changed

.github/workflows/R-CMD-check.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ jobs:
5959

6060
- uses: r-lib/actions/setup-r-dependencies@v2
6161
with:
62+
cache-version: 2
6263
extra-packages: >
6364
any::rcmdcheck,
6465
maps=?ignore-before-r=3.5.0,

NEWS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# ggplot2 (development version)
22

3+
* `stage()` now properly refers to the values without scale transformations for
4+
the stage of `after_stat`. If your code requires the scaled version of the
5+
values for some reason, you have to apply the same transformation by yourself,
6+
e.g. `sqrt()` for `scale_{x,y}_sqrt()` (@yutannihilation and @teunbrand, #4155).
7+
38
* A `linewidth` aesthetic has been introduced and supersedes the `size`
49
aesthetic for scaling the width of lines in line based geoms. `size` will
510
remain functioning but deprecated for these geoms and it is recommended to

R/layer.r

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,10 +349,13 @@ Layer <- ggproto("Layer", NULL,
349349
new <- strip_dots(aesthetics[is_calculated_aes(aesthetics) | is_staged_aes(aesthetics)])
350350
if (length(new) == 0) return(data)
351351

352+
# data needs to be non-scaled
353+
data_orig <- scales_backtransform_df(plot$scales, data)
354+
352355
# Add map stat output to aesthetics
353356
env <- child_env(baseenv(), stat = stat, after_stat = after_stat)
354357
stage_mask <- child_env(emptyenv(), stage = stage_calculated)
355-
mask <- new_data_mask(as_environment(data, stage_mask), stage_mask)
358+
mask <- new_data_mask(as_environment(data_orig, stage_mask), stage_mask)
356359
mask$.data <- as_data_pronoun(mask)
357360

358361
new <- substitute_aes(new)

R/scales-.r

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,48 @@ scales_map_df <- function(scales, df) {
8080

8181
# Transform values to cardinal representation
8282
scales_transform_df <- function(scales, df) {
83-
if (empty(df) || length(scales$scales) == 0) return(df)
83+
if (empty(df)) return(df)
84+
85+
# if the scale contains no trans or the trans is of identity, it doesn't need
86+
# to be transformed.
87+
idx_skip <- vapply(scales$scales, function(x) {
88+
is.null(x$trans) ||
89+
identical(x$trans$transform, identity)
90+
}, logical(1L))
91+
scale_list <- scales$scales[!idx_skip]
92+
93+
if (length(scale_list) == 0L) return(df)
8494

85-
transformed <- unlist(lapply(scales$scales, function(s) s$transform_df(df = df)),
95+
transformed <- unlist(lapply(scale_list, function(s) s$transform_df(df = df)),
8696
recursive = FALSE)
8797
new_data_frame(c(transformed, df[setdiff(names(df), names(transformed))]))
8898
}
8999

100+
scales_backtransform_df <- function(scales, df) {
101+
# NOTE: no need to check empty(data) because it should be already checked
102+
# before this function is called.
103+
104+
# if the scale contains no trans or the trans is of identity, it doesn't need
105+
# to be backtransformed.
106+
idx_skip <- vapply(scales$scales, function(x) {
107+
is.null(x$trans) ||
108+
identical(x$trans$inverse, identity)
109+
}, logical(1L))
110+
scale_list <- scales$scales[!idx_skip]
111+
112+
if (length(scale_list) == 0L) return(df)
113+
114+
backtransformed <- unlist(lapply(scale_list, function(scale) {
115+
aesthetics <- intersect(scale$aesthetics, names(df))
116+
117+
if (length(aesthetics) == 0) return()
118+
119+
lapply(df[aesthetics], scale$trans$inverse)
120+
}), recursive = FALSE)
121+
122+
new_data_frame(c(backtransformed, df[setdiff(names(df), names(backtransformed))]))
123+
}
124+
90125
# @param aesthetics A list of aesthetic-variable mappings. The name of each
91126
# item is the aesthetic, and the value of each item is the variable in data.
92127
scales_add_defaults <- function(scales, data, aesthetics, env) {

tests/testthat/test-scales.r

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,3 +459,12 @@ test_that("breaks and labels are correctly checked", {
459459
p <- ggplot(mtcars) + geom_bar(aes(mpg)) + scale_x_binned(labels = function(x) 1:2)
460460
expect_snapshot_error(ggplotGrob(p))
461461
})
462+
463+
test_that("staged aesthetics are backtransformed properly (#4155)", {
464+
p <- ggplot(data.frame(value = 16)) +
465+
geom_point(aes(stage(value, after_stat = x / 2), 0)) +
466+
scale_x_sqrt(limits = c(0, 16), breaks = c(2, 4, 8))
467+
468+
# x / 2 should be 16 / 2 = 8, thus the result should be sqrt(8) on scale_x_sqrt()
469+
expect_equal(layer_data(p)$x, sqrt(8))
470+
})

0 commit comments

Comments
 (0)