Skip to content

Commit 123b26e

Browse files
authored
Violin quantiles are based on observations (#5912)
* plumbing for `draw_quantiles` in `stat_ydensity()` * stat computes quantiles * geom draws quantiles, not compute them * migrate docs * add test * accept snapshot changes * add news bullet * stat param is named `quantiles` * quantile drawing is controlled by graphical params * adapt tests * document * add news bullets * dedup news bullets * Deprecation of the `draw_quantiles` parameter coming from `geom_violin()` parity
1 parent 9eeeafe commit 123b26e

22 files changed

+272
-164
lines changed

NEWS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@
5151
(@teunbrand, #4320)
5252
* `geom_boxplot()` gains additional arguments to style the colour, linetype and
5353
linewidths of the box, whiskers, median line and staples (@teunbrand, #5126)
54+
* `geom_violin()` gains additional arguments to style the colour, linetype and
55+
linewidths of the quantiles, which replace the now-deprecated `draw_quantiles`
56+
argument (#5912).
57+
* (breaking) `geom_violin(quantiles)` now has actual quantiles based on
58+
the data, rather than inferred quantiles based on the computed density. The
59+
`quantiles` parameter that replaces `draw_quantiles` now belongs to
60+
`stat_ydensity()` instead of `geom_violin()` (@teunbrand, #4120).
5461
* (internal) Using `after_scale()` in the `Geom*$default_aes()` field is now
5562
evaluated in the context of data (@teunbrand, #6135)
5663
* Fixed bug where binned scales wouldn't simultaneously accept transformations

R/geom-violin.R

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
#' @eval rd_aesthetics("geom", "violin")
1111
#' @inheritParams layer
1212
#' @inheritParams geom_bar
13-
#' @param draw_quantiles If `not(NULL)` (default), draw horizontal lines
14-
#' at the given quantiles of the density estimate.
1513
#' @param trim If `TRUE` (default), trim the tails of the violins
1614
#' to the range of the data. If `FALSE`, don't trim the tails.
1715
#' @param geom,stat Use to override the default connection between
@@ -23,6 +21,12 @@
2321
#' finite, boundary effect of default density estimation will be corrected by
2422
#' reflecting tails outside `bounds` around their closest edge. Data points
2523
#' outside of bounds are removed with a warning.
24+
#' @param quantile.colour,quantile.color,quantile.linewidth,quantile.linetype
25+
#' Default aesthetics for the quantile lines. Set to `NULL` to inherit from
26+
#' the data's aesthetics. By default, quantile lines are hidden and can be
27+
#' turned on by changing `quantile.linetype`.
28+
#' @param draw_quantiles `r lifecycle::badge("deprecated")` Previous
29+
#' specification of drawing quantiles.
2630
#' @export
2731
#' @references Hintze, J. L., Nelson, R. D. (1998) Violin Plots: A Box
2832
#' Plot-Density Trace Synergism. The American Statistician 52, 181-184.
@@ -91,14 +95,46 @@
9195
geom_violin <- function(mapping = NULL, data = NULL,
9296
stat = "ydensity", position = "dodge",
9397
...,
94-
draw_quantiles = NULL,
9598
trim = TRUE,
9699
bounds = c(-Inf, Inf),
100+
quantile.colour = NULL,
101+
quantile.color = NULL,
102+
quantile.linetype = 0L,
103+
quantile.linewidth = NULL,
104+
draw_quantiles = deprecated(),
97105
scale = "area",
98106
na.rm = FALSE,
99107
orientation = NA,
100108
show.legend = NA,
101109
inherit.aes = TRUE) {
110+
111+
extra <- list()
112+
if (lifecycle::is_present(draw_quantiles)) {
113+
deprecate_soft0(
114+
"3.6.0",
115+
what = "geom_violin(draw_quantiles)",
116+
with = "geom_violin(quantiles.linetype)"
117+
)
118+
check_numeric(draw_quantiles)
119+
120+
# Pass on to stat when stat accepts 'quantiles'
121+
stat <- check_subclass(stat, "Stat", current_call(), caller_env())
122+
if ("quantiles" %in% stat$parameters()) {
123+
extra$quantiles <- draw_quantiles
124+
}
125+
126+
# Turn on quantile lines
127+
if (!is.null(quantile.linetype)) {
128+
quantile.linetype <- max(quantile.linetype, 1)
129+
}
130+
}
131+
132+
quantile_gp <- list(
133+
colour = quantile.color %||% quantile.colour,
134+
linetype = quantile.linetype,
135+
linewidth = quantile.linewidth
136+
)
137+
102138
layer(
103139
data = data,
104140
mapping = mapping,
@@ -110,10 +146,11 @@ geom_violin <- function(mapping = NULL, data = NULL,
110146
params = list2(
111147
trim = trim,
112148
scale = scale,
113-
draw_quantiles = draw_quantiles,
114149
na.rm = na.rm,
115150
orientation = orientation,
116151
bounds = bounds,
152+
quantile_gp = quantile_gp,
153+
!!!extra,
117154
...
118155
)
119156
)
@@ -146,7 +183,7 @@ GeomViolin <- ggproto("GeomViolin", Geom,
146183
flip_data(data, params$flipped_aes)
147184
},
148185

149-
draw_group = function(self, data, ..., draw_quantiles = NULL, flipped_aes = FALSE) {
186+
draw_group = function(self, data, ..., quantile_gp = list(linetype = 0), flipped_aes = FALSE) {
150187
data <- flip_data(data, flipped_aes)
151188
# Find the points for the line to go all the way around
152189
data <- transform(data,
@@ -165,36 +202,28 @@ GeomViolin <- ggproto("GeomViolin", Geom,
165202
newdata <- vec_rbind0(newdata, newdata[1,])
166203
newdata <- flip_data(newdata, flipped_aes)
167204

205+
violin_grob <- GeomPolygon$draw_panel(newdata, ...)
206+
207+
if (!"quantile" %in% names(newdata) ||
208+
all(quantile_gp$linetype == 0) ||
209+
all(quantile_gp$linetype == "blank")) {
210+
return(ggname("geom_violin", violin_grob))
211+
}
212+
168213
# Draw quantiles if requested, so long as there is non-zero y range
169-
if (length(draw_quantiles) > 0 & !scales::zero_range(range(data$y))) {
170-
if (!(all(draw_quantiles >= 0) && all(draw_quantiles <= 1))) {
171-
cli::cli_abort("{.arg draw_quantiles} must be between 0 and 1.")
172-
}
173-
174-
# Compute the quantile segments and combine with existing aesthetics
175-
quantiles <- create_quantile_segment_frame(data, draw_quantiles)
176-
aesthetics <- data[
177-
rep(1, nrow(quantiles)),
178-
setdiff(names(data), c("x", "y", "group")),
179-
drop = FALSE
180-
]
181-
aesthetics$alpha <- rep(1, nrow(quantiles))
182-
both <- vec_cbind(quantiles, aesthetics)
183-
both <- both[!is.na(both$group), , drop = FALSE]
184-
both <- flip_data(both, flipped_aes)
185-
quantile_grob <- if (nrow(both) == 0) {
186-
zeroGrob()
187-
} else {
188-
GeomPath$draw_panel(both, ...)
189-
}
190-
191-
ggname("geom_violin", grobTree(
192-
GeomPolygon$draw_panel(newdata, ...),
193-
quantile_grob)
194-
)
214+
quantiles <- newdata[!is.na(newdata$quantile),]
215+
quantiles$group <- match(quantiles$quantile, unique(quantiles$quantile))
216+
quantiles$linetype <- quantile_gp$linetype %||% quantiles$linetype
217+
quantiles$linewidth <- quantile_gp$linewidth %||% quantiles$linewidth
218+
quantiles$colour <- quantile_gp$colour %||% quantiles$colour
219+
220+
quantile_grob <- if (nrow(quantiles) == 0) {
221+
zeroGrob()
195222
} else {
196-
ggname("geom_violin", GeomPolygon$draw_panel(newdata, ...))
223+
GeomPath$draw_panel(quantiles, ...)
197224
}
225+
226+
ggname("geom_violin", grobTree(violin_grob, quantile_grob))
198227
},
199228

200229
draw_key = draw_key_polygon,

R/stat-ydensity.R

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
#' @param drop Whether to discard groups with less than 2 observations
88
#' (`TRUE`, default) or keep such groups for position adjustment purposes
99
#' (`FALSE`).
10+
#' @param quantiles If not `NULL` (default), compute the `quantile` variable
11+
#' and draw horizontal lines at the given quantiles in `geom_violin()`.
1012
#'
1113
#' @eval rd_computed_vars(
1214
#' density = "Density estimate.",
@@ -16,7 +18,8 @@
1618
#' violinwidth = "Density scaled for the violin plot, according to area,
1719
#' counts or to a constant maximum width.",
1820
#' n = "Number of points.",
19-
#' width = "Width of violin bounding box."
21+
#' width = "Width of violin bounding box.",
22+
#' quantile = "Whether the row is part of the `quantiles` computation."
2023
#' )
2124
#'
2225
#' @seealso [geom_violin()] for examples, and [stat_density()]
@@ -26,6 +29,7 @@
2629
stat_ydensity <- function(mapping = NULL, data = NULL,
2730
geom = "violin", position = "dodge",
2831
...,
32+
quantiles = c(0.25, 0.50, 0.75),
2933
bw = "nrd0",
3034
adjust = 1,
3135
kernel = "gaussian",
@@ -56,6 +60,7 @@ stat_ydensity <- function(mapping = NULL, data = NULL,
5660
drop = drop,
5761
na.rm = na.rm,
5862
bounds = bounds,
63+
quantiles = quantiles,
5964
...
6065
)
6166
)
@@ -73,14 +78,26 @@ StatYdensity <- ggproto("StatYdensity", Stat,
7378
setup_params = function(data, params) {
7479
params$flipped_aes <- has_flipped_aes(data, params, main_is_orthogonal = TRUE, group_has_equal = TRUE)
7580

81+
if (!is.null(params$draw_quantiles)) {
82+
deprecate_soft0(
83+
"3.6.0",
84+
what = "stat_ydensity(draw_quantiles)",
85+
with = "stat_ydensity(quantiles)"
86+
)
87+
params$quantiles <- params$draw_quantiles
88+
check_numeric(params$quantiles, arg = "quantiles")
89+
}
90+
7691
params
7792
},
7893

79-
extra_params = c("na.rm", "orientation"),
94+
# `draw_quantiles` is here for deprecation repair reasons
95+
extra_params = c("na.rm", "orientation", "draw_quantiles"),
8096

8197
compute_group = function(self, data, scales, width = NULL, bw = "nrd0", adjust = 1,
8298
kernel = "gaussian", trim = TRUE, na.rm = FALSE,
83-
drop = TRUE, flipped_aes = FALSE, bounds = c(-Inf, Inf)) {
99+
drop = TRUE, flipped_aes = FALSE, bounds = c(-Inf, Inf),
100+
quantiles = c(0.25, 0.50, 0.75)) {
84101
if (nrow(data) < 2) {
85102
if (isTRUE(drop)) {
86103
cli::cli_warn(c(
@@ -115,17 +132,43 @@ StatYdensity <- ggproto("StatYdensity", Stat,
115132
}
116133
dens$width <- width
117134

135+
if (!is.null(quantiles)) {
136+
if (!(all(quantiles >= 0) && all(quantiles <= 1))) {
137+
cli::cli_abort("{.arg quantiles} must be between 0 and 1.")
138+
}
139+
if (!is.null(data[["weight"]]) || !all(data[["weight"]] == 1)) {
140+
cli::cli_warn(
141+
"{.arg quantiles} for weighted data is not implemented."
142+
)
143+
}
144+
quants <- quantile(data$y, probs = quantiles)
145+
quants <- data_frame0(
146+
y = unname(quants),
147+
quantile = quantiles
148+
)
149+
150+
# Interpolate other metrics
151+
for (var in setdiff(names(dens), names(quants))) {
152+
quants[[var]] <-
153+
approx(dens$y, dens[[var]], xout = quants$y, ties = "ordered")$y
154+
}
155+
156+
dens <- vec_slice(dens, !dens$y %in% quants$y)
157+
dens <- vec_c(dens, quants)
158+
}
159+
118160
dens
119161
},
120162

121163
compute_panel = function(self, data, scales, width = NULL, bw = "nrd0", adjust = 1,
122164
kernel = "gaussian", trim = TRUE, na.rm = FALSE,
123165
scale = "area", flipped_aes = FALSE, drop = TRUE,
124-
bounds = c(-Inf, Inf)) {
166+
bounds = c(-Inf, Inf), quantiles = c(0.25, 0.50, 0.75)) {
125167
data <- flip_data(data, flipped_aes)
126168
data <- ggproto_parent(Stat, self)$compute_panel(
127169
data, scales, width = width, bw = bw, adjust = adjust, kernel = kernel,
128170
trim = trim, na.rm = na.rm, drop = drop, bounds = bounds,
171+
quantiles = quantiles
129172
)
130173
if (!drop && any(data$n < 2)) {
131174
cli::cli_warn(

man/geom_violin.Rd

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

tests/testthat/_snaps/geom-violin.md

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
# quantiles fails outside 0-1 bound
22

3-
Problem while converting geom to grob.
4-
i Error occurred in the 1st layer.
5-
Caused by error in `draw_group()`:
6-
! `draw_quantiles` must be between 0 and 1.
3+
Computation failed in `stat_ydensity()`.
4+
Caused by error in `compute_group()`:
5+
! `quantiles` must be between 0 and 1.
76

87
---
98

10-
Problem while converting geom to grob.
11-
i Error occurred in the 1st layer.
12-
Caused by error in `draw_group()`:
13-
! `draw_quantiles` must be between 0 and 1.
9+
Computation failed in `stat_ydensity()`.
10+
Caused by error in `compute_group()`:
11+
! `quantiles` must be between 0 and 1.
1412

0 commit comments

Comments
 (0)