Skip to content

Commit 77f6aaf

Browse files
authored
Merge branch 'main' into binned_limits
2 parents eb79313 + bee7034 commit 77f6aaf

Some content is hidden

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

56 files changed

+805
-387
lines changed

NEWS.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
* Automatic breaks in `scale_*_binned()` should no longer be out-of-bounds,
44
and automatic limits expand to include these (@teunbrand, #5095, #5100).
5+
* Renamed computed aesthetic in `stat_ecdf()` to `ecdf`, to prevent incorrect
6+
scale transformations (@teunbrand, #5113 and #5112).
7+
* Fixed misbehaviour of `draw_key_boxplot()` and `draw_key_crossbar()` with
8+
skewed key aspect ratio (@teunbrand, #5082).
9+
* `scale_*_binned()` handles zero-range limits more gracefully (@teunbrand,
10+
#5066)
11+
* Binned scales are now compatible with `trans = "date"` and `trans = "time"`
12+
(@teunbrand, #4217).
13+
* `ggsave()` warns when multiple `filename`s are given, and only writes to the
14+
first file (@teunbrand, #5114)
515
* Fixed a regression in `geom_hex()` where aesthetics were replicated across
616
bins (@thomasp85, #5037 and #5044)
717
* Fixed spurious warning when `weight` aesthetic was used in `stat_smooth()`
@@ -10,7 +20,7 @@
1020
(@teunbrand based on @clauswilke's suggestion #5051).
1121
* Fixed a regression in `Coord$train_panel_guides()` where names of guides were
1222
dropped (@maxsutton, #5063)
13-
23+
1424
# ggplot2 3.4.0
1525
This is a minor release focusing on tightening up the internals and ironing out
1626
some inconsistencies in the API. The biggest change is the addition of the

R/aes-evaluation.r

Lines changed: 152 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,114 @@
11
#' Control aesthetic evaluation
22
#'
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
3+
#' @description
4+
#' Most [aesthetics][aes()] are mapped from variables found in the data.
5+
#' Sometimes, however, you want to delay the mapping until later in the
6+
#' rendering process. ggplot2 has three stages of the data that you can map
7+
#' aesthetics from, and three functions to control at which stage aesthetics
8+
#' should be evaluated.
9+
#'
10+
#' @description
11+
#' `after_stat()` replaces the old approaches of using either `stat()`, e.g.
12+
#' `stat(density)`, or surrounding the variable names with `..`, e.g.
13+
#' `..density..`.
14+
#'
15+
#' @usage
16+
#' # These functions can be used inside the `aes()` function
17+
#' # used as the `mapping` argument in layers, for example:
18+
#' # geom_density(mapping = aes(y = after_stat(scaled)))
19+
#'
20+
#' @param x <[`data-masking`][rlang::topic-data-mask]> An aesthetic expression
21+
#' using variables calculated by the stat (`after_stat()`) or layer aesthetics
22+
#' (`after_scale()`).
23+
#' @param start <[`data-masking`][rlang::topic-data-mask]> An aesthetic
24+
#' expression using variables from the layer data.
25+
#' @param after_stat <[`data-masking`][rlang::topic-data-mask]> An aesthetic
26+
#' expression using variables calculated by the stat.
27+
#' @param after_scale <[`data-masking`][rlang::topic-data-mask]> An aesthetic
28+
#' expression using layer aesthetics.
29+
#'
30+
#' @details
31+
#' # Staging
32+
#' Below follows an overview of the three stages of evaluation and how aesthetic
33+
#' evaluation can be controlled.
34+
#'
35+
#' ## Stage 1: direct input
36+
#' The default is to map at the beginning, using the layer data provided by
37+
#' the user. If you want to map directly from the layer data you should not do
38+
#' anything special. This is the only stage where the original layer data can
39+
#' be accessed.
40+
#'
41+
#' ```r
42+
#' # 'x' and 'y' are mapped directly
43+
#' ggplot(mtcars) + geom_point(aes(x = mpg, y = disp))
44+
#' ```
45+
#'
46+
#' ## Stage 2: after stat transformation
47+
#' The second stage is after the data has been transformed by the layer
48+
#' stat. The most common example of mapping from stat transformed data is the
49+
#' height of bars in [geom_histogram()]: the height does not come from a
50+
#' variable in the underlying data, but is instead mapped to the `count`
51+
#' computed by [stat_bin()]. In order to map from stat transformed data you
1652
#' should use the `after_stat()` function to flag that evaluation of the
1753
#' 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 have access to the
27-
#' variables calculated by the stat, not the original mapped values. Evaluation
28-
#' after scaling will only have access to the final aesthetics of the layer
29-
#' (including non-mapped, default aesthetics). The original layer data can only
30-
#' be accessed at the first stage.
31-
#'
32-
#' @param x An aesthetic expression using variables calculated by the stat
33-
#' (`after_stat()`) or layer aesthetics (`after_scale()`).
34-
#' @param start An aesthetic expression using variables from the layer data.
35-
#' @param after_stat An aesthetic expression using variables calculated by the
36-
#' stat.
37-
#' @param after_scale An aesthetic expression using layer aesthetics.
54+
#' Evaluation after stat transformation will have access to the variables
55+
#' calculated by the stat, not the original mapped values. The 'computed
56+
#' variables' section in each stat lists which variables are available to
57+
#' access.
58+
#'
59+
#' ```r
60+
#' # The 'y' values for the histogram are computed by the stat
61+
#' ggplot(faithful, aes(x = waiting)) +
62+
#' geom_histogram()
63+
#'
64+
#' # Choosing a different computed variable to display, matching up the
65+
#' # histogram with the density plot
66+
#' ggplot(faithful, aes(x = waiting)) +
67+
#' geom_histogram(aes(y = after_stat(density))) +
68+
#' geom_density()
69+
#' ```
70+
#'
71+
#' ## Stage 3: after scale transformation
72+
#' The third and last stage is after the data has been transformed and
73+
#' mapped by the plot scales. An example of mapping from scaled data could
74+
#' be to use a desaturated version of the stroke colour for fill. You should
75+
#' use `after_scale()` to flag evaluation of mapping for after data has been
76+
#' scaled. Evaluation after scaling will only have access to the final
77+
#' aesthetics of the layer (including non-mapped, default aesthetics).
78+
#'
79+
#' ```r
80+
#' # The exact colour is known after scale transformation
81+
#' ggplot(mpg, aes(cty, colour = factor(cyl))) +
82+
#' geom_density()
3883
#'
84+
#' # We re-use colour properties for the fill without a separate fill scale
85+
#' ggplot(mpg, aes(cty, colour = factor(cyl))) +
86+
#' geom_density(aes(fill = after_scale(alpha(colour, 0.3))))
87+
#' ```
88+
#'
89+
#' ## Complex staging
90+
#' If you want to map the same aesthetic multiple times, e.g. map `x` to a
91+
#' data column for the stat, but remap it for the geom, you can use the
92+
#' `stage()` function to collect multiple mappings.
93+
#'
94+
#' ```r
95+
#' # Use stage to modify the scaled fill
96+
#' ggplot(mpg, aes(class, hwy)) +
97+
#' geom_boxplot(aes(fill = stage(class, after_scale = alpha(fill, 0.4))))
98+
#'
99+
#' # Using data for computing summary, but placing label elsewhere.
100+
#' # Also, we're making our own computed variable to use for the label.
101+
#' ggplot(mpg, aes(class, displ)) +
102+
#' geom_violin() +
103+
#' stat_summary(
104+
#' aes(
105+
#' y = stage(displ, after_stat = 8),
106+
#' label = after_stat(paste(mean, "±", sd))
107+
#' ),
108+
#' geom = "text",
109+
#' fun.data = ~ round(data.frame(mean = mean(.x), sd = sd(.x)), 2)
110+
#' )
111+
#' ```
39112
#' @rdname aes_eval
40113
#' @name aes_eval
41114
#'
@@ -55,6 +128,52 @@
55128
#' # Use stage to modify the scaled fill
56129
#' ggplot(mpg, aes(class, hwy)) +
57130
#' geom_boxplot(aes(fill = stage(class, after_scale = alpha(fill, 0.4))))
131+
#'
132+
#' # Making a proportional stacked density plot
133+
#' ggplot(mpg, aes(cty)) +
134+
#' geom_density(
135+
#' aes(
136+
#' colour = factor(cyl),
137+
#' fill = after_scale(alpha(colour, 0.3)),
138+
#' y = after_stat(count / sum(n[!duplicated(group)]))
139+
#' ),
140+
#' position = "stack", bw = 1
141+
#' ) +
142+
#' geom_density(bw = 1)
143+
#'
144+
#' # Imitating a ridgeline plot
145+
#' ggplot(mpg, aes(cty, colour = factor(cyl))) +
146+
#' geom_ribbon(
147+
#' stat = "density", outline.type = "upper",
148+
#' aes(
149+
#' fill = after_scale(alpha(colour, 0.3)),
150+
#' ymin = after_stat(group),
151+
#' ymax = after_stat(group + ndensity)
152+
#' )
153+
#' )
154+
#'
155+
#' # Labelling a bar plot
156+
#' ggplot(mpg, aes(class)) +
157+
#' geom_bar() +
158+
#' geom_text(
159+
#' aes(
160+
#' y = after_stat(count + 2),
161+
#' label = after_stat(count)
162+
#' ),
163+
#' stat = "count"
164+
#' )
165+
#'
166+
#' # Labelling the upper hinge of a boxplot,
167+
#' # inspired by June Choe
168+
#' ggplot(mpg, aes(displ, class)) +
169+
#' geom_boxplot(outlier.shape = NA) +
170+
#' geom_text(
171+
#' aes(
172+
#' label = after_stat(xmax),
173+
#' x = stage(displ, after_stat = xmax)
174+
#' ),
175+
#' stat = "boxplot", hjust = -0.5
176+
#' )
58177
NULL
59178

60179
#' @rdname aes_eval

R/aes.r

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ NULL
3131
#' are typically omitted because they are so common; all other aesthetics must be named.
3232
#' @seealso [vars()] for another quoting function designed for
3333
#' faceting specifications.
34+
#'
35+
#' [Delayed evaluation][aes_eval] for working with computed variables.
3436
#' @return A list with class `uneval`. Components of the list are either
3537
#' quosures or constants.
3638
#' @export

R/geom-.r

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,12 @@ Geom <- ggproto("Geom",
7878
# Trim off extra parameters
7979
params <- params[intersect(names(params), self$parameters())]
8080

81-
lapply(split(data, data$PANEL), function(data) {
81+
if (nlevels(as.factor(data$PANEL)) > 1L) {
82+
data_panels <- split(data, data$PANEL)
83+
} else {
84+
data_panels <- list(data)
85+
}
86+
lapply(data_panels, function(data) {
8287
if (empty(data)) return(zeroGrob())
8388

8489
panel_params <- layout$panel_params[[data$PANEL[1]]]

R/geom-dotplot.r

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,17 @@
1717
#' to match the number of dots.
1818
#'
1919
#' @eval rd_aesthetics("geom", "dotplot")
20-
#' @section Computed variables:
21-
#' \describe{
22-
#' \item{x}{center of each bin, if binaxis is "x"}
23-
#' \item{y}{center of each bin, if binaxis is "x"}
24-
#' \item{binwidth}{max width of each bin if method is "dotdensity";
25-
#' width of each bin if method is "histodot"}
26-
#' \item{count}{number of points in bin}
27-
#' \item{ncount}{count, scaled to maximum of 1}
28-
#' \item{density}{density of points in bin, scaled to integrate to 1,
29-
#' if method is "histodot"}
30-
#' \item{ndensity}{density, scaled to maximum of 1, if method is "histodot"}
31-
#' }
20+
#' @eval rd_computed_vars(
21+
#' x = 'center of each bin, if `binaxis` is `"x"`.',
22+
#' y = 'center of each bin, if `binaxis` is `"x"`.',
23+
#' binwidth = 'maximum width of each bin if method is `"dotdensity"`;
24+
#' width of each bin if method is `"histodot"`.',
25+
#' count = "number of points in bin.",
26+
#' ncount = "count, scaled to a maximum of 1.",
27+
#' density = 'density of points in bin, scaled to integrate to 1, if method
28+
#' is `"histodot"`.',
29+
#' ndensity = 'density, scaled to maximum of 1, if method is `"histodot"`.'
30+
#' )
3231
#'
3332
#' @inheritParams layer
3433
#' @inheritParams geom_point

R/legend-draw.r

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -98,39 +98,58 @@ draw_key_blank <- function(data, params, size) {
9898
#' @export
9999
#' @rdname draw_key
100100
draw_key_boxplot <- function(data, params, size) {
101-
grobTree(
102-
linesGrob(0.5, c(0.1, 0.25)),
103-
linesGrob(0.5, c(0.75, 0.9)),
104-
rectGrob(height = 0.5, width = 0.75),
105-
linesGrob(c(0.125, 0.875), 0.5),
106-
gp = gpar(
107-
col = data$colour %||% "grey20",
108-
fill = alpha(data$fill %||% "white", data$alpha),
109-
lwd = (data$linewidth %||% 0.5) * .pt,
110-
lty = data$linetype %||% 1,
111-
lineend = params$lineend %||% "butt",
112-
linejoin = params$linejoin %||% "mitre"
113-
),
114-
vp = if (isTRUE(params$flipped_aes)) viewport(angle = -90)
101+
gp <- gpar(
102+
col = data$colour %||% "grey20",
103+
fill = alpha(data$fill %||% "white", data$alpha),
104+
lwd = (data$linewidth %||% 0.5) * .pt,
105+
lty = data$linetype %||% 1,
106+
lineend = params$lineend %||% "butt",
107+
linejoin = params$linejoin %||% "mitre"
115108
)
109+
110+
if (isTRUE(params$flipped_aes)) {
111+
grobTree(
112+
linesGrob(c(0.1, 0.25), 0.5),
113+
linesGrob(c(0.75, 0.9), 0.5),
114+
rectGrob(width = 0.5, height = 0.75),
115+
linesGrob(0.5, c(0.125, 0.875)),
116+
gp = gp
117+
)
118+
} else {
119+
grobTree(
120+
linesGrob(0.5, c(0.1, 0.25)),
121+
linesGrob(0.5, c(0.75, 0.9)),
122+
rectGrob(height = 0.5, width = 0.75),
123+
linesGrob(c(0.125, 0.875), 0.5),
124+
gp = gp
125+
)
126+
}
116127
}
117128

118129
#' @export
119130
#' @rdname draw_key
120131
draw_key_crossbar <- function(data, params, size) {
121-
grobTree(
122-
rectGrob(height = 0.5, width = 0.75),
123-
linesGrob(c(0.125, 0.875), 0.5),
124-
gp = gpar(
125-
col = data$colour %||% "grey20",
126-
fill = alpha(data$fill %||% "white", data$alpha),
127-
lwd = (data$linewidth %||% 0.5) * .pt,
128-
lty = data$linetype %||% 1,
129-
lineend = params$lineend %||% "butt",
130-
linejoin = params$linejoin %||% "mitre"
131-
),
132-
vp = if (isTRUE(params$flipped_aes)) viewport(angle = -90)
132+
gp <- gpar(
133+
col = data$colour %||% "grey20",
134+
fill = alpha(data$fill %||% "white", data$alpha),
135+
lwd = (data$linewidth %||% 0.5) * .pt,
136+
lty = data$linetype %||% 1,
137+
lineend = params$lineend %||% "butt",
138+
linejoin = params$linejoin %||% "mitre"
133139
)
140+
if (isTRUE(params$flipped_aes)) {
141+
grobTree(
142+
rectGrob(height = 0.75, width = 0.5),
143+
linesGrob(0.5, c(0.125, 0.875)),
144+
gp = gp
145+
)
146+
} else {
147+
grobTree(
148+
rectGrob(height = 0.5, width = 0.75),
149+
linesGrob(c(0.125, 0.875), 0.5),
150+
gp = gp
151+
)
152+
}
134153
}
135154

136155
#' @export

R/save.r

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,17 @@ ggsave <- function(filename, plot = last_plot(),
7777
device = NULL, path = NULL, scale = 1,
7878
width = NA, height = NA, units = c("in", "cm", "mm", "px"),
7979
dpi = 300, limitsize = TRUE, bg = NULL, ...) {
80+
if (length(filename) != 1) {
81+
if (length(filename) == 0) {
82+
cli::cli_abort("{.arg filename} cannot be empty.")
83+
}
84+
len <- length(filename)
85+
filename <- filename[1]
86+
cli::cli_warn(c(
87+
"{.arg filename} must have length 1, not length {len}.",
88+
"!" = "Only the first, {.file {filename}}, will be used."
89+
))
90+
}
8091

8192
dpi <- parse_dpi(dpi)
8293
dev <- plot_dev(device, filename, dpi = dpi)

0 commit comments

Comments
 (0)