Skip to content

Commit fc60051

Browse files
authored
Finer control over aesthetic evaluation (#3534)
1 parent 5d1d773 commit fc60051

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+421
-218
lines changed

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ Collate:
6464
'ggproto.r'
6565
'ggplot-global.R'
6666
'aaa-.r'
67-
'aes-calculated.r'
6867
'aes-colour-fill-alpha.r'
68+
'aes-evaluation.r'
6969
'aes-group-order.r'
7070
'aes-linetype-size-shape.r'
7171
'aes-position.r'

NAMESPACE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,8 @@ export(aes_all)
252252
export(aes_auto)
253253
export(aes_q)
254254
export(aes_string)
255+
export(after_scale)
256+
export(after_stat)
255257
export(alpha)
256258
export(annotate)
257259
export(annotation_custom)
@@ -570,6 +572,7 @@ export(scale_y_time)
570572
export(sec_axis)
571573
export(set_last_plot)
572574
export(should_stop)
575+
export(stage)
573576
export(standardise_aes_names)
574577
export(stat)
575578
export(stat_bin)

NEWS.md

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

3+
* The evaluation time of aesthetics can now be controlled to a finer degree.
4+
`after_stat()` superseeds the use of `stat()` and `..var..`-notation, ad is
5+
joined by `after_scale()` to allow for mapping to scaled aesthetic values.
6+
Remapping of the same aesthetic is now supported with `stage()`, so you can
7+
map a data variable to a stat aesthetic, and remap the same aesthetic to
8+
something else after statistical transformation (@thomasp85, #3534)
9+
310
* ggplot2 no longer depends on reshape2, which means that it no longer
411
(recursively) needs plyr, stringr, or stringi packages.
512

R/aes-calculated.r

Lines changed: 0 additions & 113 deletions
This file was deleted.

R/aes-evaluation.r

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
#' Control aesthetic evaluation
2+
#'
3+
#' Most aesthetics are mapped from variables found in the data. Sometimes,
4+
#' however, you want to delay the mapping until later in the rendering process.
5+
#' ggplot2 has three stages of the data that you can map aesthetics from. The
6+
#' default is to map at the beginning, using the layer data provided by the
7+
#' user. The second stage is after the data has been transformed by the layer
8+
#' stat. The third and last stage is after the data has been transformed and
9+
#' mapped by the plot scales. The most common example of mapping from stat
10+
#' transformed data is the height of bars in [geom_histogram()]:
11+
#' the height does not come from a variable in the underlying data, but
12+
#' is instead mapped to the `count` computed by [stat_bin()]. An example of
13+
#' mapping from scaled data could be to use a desaturated version of the stroke
14+
#' colour for fill. If you want to map directly from the layer data you should
15+
#' not do anything special. In order to map from stat transformed data you
16+
#' should use the `after_stat()` function to flag that evaluation of the
17+
#' aesthetic mapping should be postponed until after stat transformation.
18+
#' Similarly, you should use `after_scale()` to flag evaluation of mapping for
19+
#' after data has been scaled. If you want to map the same aesthetic multiple
20+
#' times, e.g. map `x` to a data column for the stat, but remap it for the geom,
21+
#' you can use the `stage()` function to collect multiple mappings.
22+
#'
23+
#' `after_stat()` replaces the old approaches of using either `stat()` or
24+
#' surrounding the variable names with `..`.
25+
#'
26+
#' @note Evaluation after stat transformation will only have access to the
27+
#' variables calculated by the stat. Evaluation after scaling will only have
28+
#' access to the final aesthetics of the layer (including non-mapped, default
29+
#' aesthetics). The original layer data can only be accessed at the first stage.
30+
#'
31+
#' @param x An aesthetic expression using variables calculated by the stat
32+
#' (`after_stat()`) or layer aesthetics (`after_scale()`).
33+
#' @param start An aesthetic expression using variables from the layer data.
34+
#' @param after_stat An aesthetic expression using variables calculated by the
35+
#' stat.
36+
#' @param after_scale An aesthetic expression using layer aesthetics.
37+
#'
38+
#' @rdname aes_eval
39+
#' @name aes_eval
40+
#'
41+
#' @examples
42+
#' # Default histogram display
43+
#' ggplot(mpg, aes(displ)) +
44+
#' geom_histogram(aes(y = after_stat(count)))
45+
#'
46+
#' # Scale tallest bin to 1
47+
#' ggplot(mpg, aes(displ)) +
48+
#' geom_histogram(aes(y = after_stat(count / max(count))))
49+
#'
50+
#' # Use a transparent version of colour for fill
51+
#' ggplot(mpg, aes(class, hwy)) +
52+
#' geom_boxplot(aes(colour = class, fill = after_scale(alpha(colour, 0.4))))
53+
#'
54+
#' # Use stage to modify the scaled fill
55+
#' ggplot(mpg, aes(class, hwy)) +
56+
#' geom_boxplot(aes(fill = stage(class, after_scale = alpha(fill, 0.4))))
57+
NULL
58+
59+
#' @rdname aes_eval
60+
#' @export
61+
after_stat <- function(x) {
62+
x
63+
}
64+
#' @rdname aes_eval
65+
#' @usage NULL
66+
#' @export
67+
stat <- function(x) {
68+
x
69+
}
70+
#' @rdname aes_eval
71+
#' @export
72+
after_scale <- function(x) {
73+
x
74+
}
75+
#' @rdname aes_eval
76+
#' @export
77+
stage <- function(start = NULL, after_stat = NULL, after_scale = NULL) {
78+
start
79+
}
80+
stage_calculated <- function(start = NULL, after_stat = NULL, after_scale = NULL) {
81+
after_stat
82+
}
83+
stage_scaled <- function(start = NULL, after_stat = NULL, after_scale = NULL) {
84+
after_scale
85+
}
86+
87+
# Regex to determine if an identifier refers to a calculated aesthetic
88+
match_calculated_aes <- "^\\.\\.([a-zA-Z._]+)\\.\\.$"
89+
90+
is_dotted_var <- function(x) {
91+
grepl(match_calculated_aes, x)
92+
}
93+
94+
# Determine if aesthetic is calculated
95+
is_calculated_aes <- function(aesthetics) {
96+
vapply(aesthetics, is_calculated, logical(1), USE.NAMES = FALSE)
97+
}
98+
is_scaled_aes <- function(aesthetics) {
99+
vapply(aesthetics, is_scaled, logical(1), USE.NAMES = FALSE)
100+
}
101+
is_staged_aes <- function(aesthetics) {
102+
vapply(aesthetics, is_staged, logical(1), USE.NAMES = FALSE)
103+
}
104+
is_calculated <- function(x) {
105+
if (is_call(get_expr(x), "after_stat")) {
106+
return(TRUE)
107+
}
108+
# Support of old recursive behaviour
109+
if (is.atomic(x)) {
110+
FALSE
111+
} else if (is.symbol(x)) {
112+
is_dotted_var(as.character(x))
113+
} else if (is_quosure(x)) {
114+
is_calculated(quo_get_expr(x))
115+
} else if (is.call(x)) {
116+
if (identical(x[[1]], quote(stat))) {
117+
TRUE
118+
} else {
119+
any(vapply(x, is_calculated, logical(1)))
120+
}
121+
} else if (is.pairlist(x)) {
122+
FALSE
123+
} else {
124+
stop("Unknown input:", class(x)[1])
125+
}
126+
}
127+
is_scaled <- function(x) {
128+
is_call(get_expr(x), "after_scale")
129+
}
130+
is_staged <- function(x) {
131+
is_call(get_expr(x), "stage")
132+
}
133+
134+
# Strip dots from expressions
135+
strip_dots <- function(expr) {
136+
if (is.atomic(expr)) {
137+
expr
138+
} else if (is.name(expr)) {
139+
expr_ch <- as.character(expr)
140+
if (nchar(expr_ch) > 0) {
141+
as.name(gsub(match_calculated_aes, "\\1", expr_ch))
142+
} else {
143+
expr
144+
}
145+
} else if (is_quosure(expr)) {
146+
# strip dots from quosure and reconstruct the quosure
147+
expr <- new_quosure(
148+
strip_dots(quo_get_expr(expr)),
149+
quo_get_env(expr)
150+
)
151+
} else if (is.call(expr)) {
152+
if (identical(expr[[1]], quote(stat))) {
153+
strip_dots(expr[[2]])
154+
} else {
155+
expr[-1] <- lapply(expr[-1], strip_dots)
156+
expr
157+
}
158+
} else if (is.pairlist(expr)) {
159+
# In the unlikely event of an anonymous function
160+
as.pairlist(lapply(expr, strip_dots))
161+
} else if (is.list(expr)) {
162+
# For list of aesthetics
163+
lapply(expr, strip_dots)
164+
} else {
165+
stop("Unknown input:", class(expr)[1])
166+
}
167+
}
168+
169+
strip_stage <- function(expr) {
170+
uq_expr <- get_expr(expr)
171+
if (is_call(uq_expr, c("after_stat", "after_scale"))) {
172+
uq_expr[[2]]
173+
} else if (is_call(uq_expr, "stage")) {
174+
# Prefer stat mapping if present, otherwise original mapping (fallback to
175+
# scale mapping) but there should always be two arguments to stage()
176+
uq_expr$after_stat %||% uq_expr$start %||% (if (is.null(uq_expr$after_scale)) uq_expr[[3]]) %||% uq_expr[[2]]
177+
} else {
178+
expr
179+
}
180+
}
181+
182+
# Convert aesthetic mapping into text labels
183+
make_labels <- function(mapping) {
184+
default_label <- function(aesthetic, mapping) {
185+
# e.g., geom_smooth(aes(colour = "loess")) or aes(y = NULL)
186+
if (is.atomic(mapping)) {
187+
return(aesthetic)
188+
}
189+
mapping <- strip_stage(mapping)
190+
mapping <- strip_dots(mapping)
191+
if (is_quosure(mapping) && quo_is_symbol(mapping)) {
192+
name <- as_string(quo_get_expr(mapping))
193+
} else {
194+
name <- quo_text(mapping)
195+
name <- gsub("\n.*$", "...", name)
196+
}
197+
name
198+
}
199+
Map(default_label, names(mapping), mapping)
200+
}

R/aes-group-order.r

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
#' f + geom_line(aes(linetype = variable))
3535
#'
3636
#' # Using facets
37-
#' k <- ggplot(diamonds, aes(carat, stat(density))) + geom_histogram(binwidth = 0.2)
37+
#' k <- ggplot(diamonds, aes(carat, after_stat(density))) +
38+
#' geom_histogram(binwidth = 0.2)
3839
#' k + facet_grid(. ~ cut)
3940
#'
4041
#' # There are three common cases where the default is not enough, and we

R/aes.r

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,28 @@ rename_aes <- function(x) {
174174
}
175175
x
176176
}
177+
substitute_aes <- function(x) {
178+
x <- lapply(x, function(aesthetic) {
179+
as_quosure(standardise_aes_symbols(quo_get_expr(aesthetic)), env = environment(aesthetic))
180+
})
181+
class(x) <- "uneval"
182+
x
183+
}
184+
# x is a quoted expression from inside aes()
185+
standardise_aes_symbols <- function(x) {
186+
if (is.symbol(x)) {
187+
name <- standardise_aes_names(as_string(x))
188+
return(sym(name))
189+
}
190+
if (!is.call(x)) {
191+
return(x)
192+
}
193+
194+
# Don't walk through function heads
195+
x[-1] <- lapply(x[-1], standardise_aes_symbols)
196+
197+
x
198+
}
177199

178200
# Look up the scale that should be used for a given aesthetic
179201
aes_to_scale <- function(var) {

0 commit comments

Comments
 (0)