diff --git a/NEWS.md b/NEWS.md index 140d7dd25b..3d9a9f7f99 100644 --- a/NEWS.md +++ b/NEWS.md @@ -51,6 +51,13 @@ (@teunbrand, #4320) * `geom_boxplot()` gains additional arguments to style the colour, linetype and linewidths of the box, whiskers, median line and staples (@teunbrand, #5126) +* `geom_violin()` gains additional arguments to style the colour, linetype and + linewidths of the quantiles, which replace the now-deprecated `draw_quantiles` + argument (#5912). +* (breaking) `geom_violin(quantiles)` now has actual quantiles based on + the data, rather than inferred quantiles based on the computed density. The + `quantiles` parameter that replaces `draw_quantiles` now belongs to + `stat_ydensity()` instead of `geom_violin()` (@teunbrand, #4120). * (internal) Using `after_scale()` in the `Geom*$default_aes()` field is now evaluated in the context of data (@teunbrand, #6135) * Fixed bug where binned scales wouldn't simultaneously accept transformations diff --git a/R/geom-violin.R b/R/geom-violin.R index e53c29396f..8c3d23ad71 100644 --- a/R/geom-violin.R +++ b/R/geom-violin.R @@ -10,8 +10,6 @@ #' @eval rd_aesthetics("geom", "violin") #' @inheritParams layer #' @inheritParams geom_bar -#' @param draw_quantiles If `not(NULL)` (default), draw horizontal lines -#' at the given quantiles of the density estimate. #' @param trim If `TRUE` (default), trim the tails of the violins #' to the range of the data. If `FALSE`, don't trim the tails. #' @param geom,stat Use to override the default connection between @@ -23,6 +21,12 @@ #' finite, boundary effect of default density estimation will be corrected by #' reflecting tails outside `bounds` around their closest edge. Data points #' outside of bounds are removed with a warning. +#' @param quantile.colour,quantile.color,quantile.linewidth,quantile.linetype +#' Default aesthetics for the quantile lines. Set to `NULL` to inherit from +#' the data's aesthetics. By default, quantile lines are hidden and can be +#' turned on by changing `quantile.linetype`. +#' @param draw_quantiles `r lifecycle::badge("deprecated")` Previous +#' specification of drawing quantiles. #' @export #' @references Hintze, J. L., Nelson, R. D. (1998) Violin Plots: A Box #' Plot-Density Trace Synergism. The American Statistician 52, 181-184. @@ -91,14 +95,46 @@ geom_violin <- function(mapping = NULL, data = NULL, stat = "ydensity", position = "dodge", ..., - draw_quantiles = NULL, trim = TRUE, bounds = c(-Inf, Inf), + quantile.colour = NULL, + quantile.color = NULL, + quantile.linetype = 0L, + quantile.linewidth = NULL, + draw_quantiles = deprecated(), scale = "area", na.rm = FALSE, orientation = NA, show.legend = NA, inherit.aes = TRUE) { + + extra <- list() + if (lifecycle::is_present(draw_quantiles)) { + deprecate_soft0( + "3.6.0", + what = "geom_violin(draw_quantiles)", + with = "geom_violin(quantiles.linetype)" + ) + check_numeric(draw_quantiles) + + # Pass on to stat when stat accepts 'quantiles' + stat <- check_subclass(stat, "Stat", current_call(), caller_env()) + if ("quantiles" %in% stat$parameters()) { + extra$quantiles <- draw_quantiles + } + + # Turn on quantile lines + if (!is.null(quantile.linetype)) { + quantile.linetype <- max(quantile.linetype, 1) + } + } + + quantile_gp <- list( + colour = quantile.color %||% quantile.colour, + linetype = quantile.linetype, + linewidth = quantile.linewidth + ) + layer( data = data, mapping = mapping, @@ -110,10 +146,11 @@ geom_violin <- function(mapping = NULL, data = NULL, params = list2( trim = trim, scale = scale, - draw_quantiles = draw_quantiles, na.rm = na.rm, orientation = orientation, bounds = bounds, + quantile_gp = quantile_gp, + !!!extra, ... ) ) @@ -146,7 +183,7 @@ GeomViolin <- ggproto("GeomViolin", Geom, flip_data(data, params$flipped_aes) }, - draw_group = function(self, data, ..., draw_quantiles = NULL, flipped_aes = FALSE) { + draw_group = function(self, data, ..., quantile_gp = list(linetype = 0), flipped_aes = FALSE) { data <- flip_data(data, flipped_aes) # Find the points for the line to go all the way around data <- transform(data, @@ -165,36 +202,28 @@ GeomViolin <- ggproto("GeomViolin", Geom, newdata <- vec_rbind0(newdata, newdata[1,]) newdata <- flip_data(newdata, flipped_aes) + violin_grob <- GeomPolygon$draw_panel(newdata, ...) + + if (!"quantile" %in% names(newdata) || + all(quantile_gp$linetype == 0) || + all(quantile_gp$linetype == "blank")) { + return(ggname("geom_violin", violin_grob)) + } + # Draw quantiles if requested, so long as there is non-zero y range - if (length(draw_quantiles) > 0 & !scales::zero_range(range(data$y))) { - if (!(all(draw_quantiles >= 0) && all(draw_quantiles <= 1))) { - cli::cli_abort("{.arg draw_quantiles} must be between 0 and 1.") - } - - # Compute the quantile segments and combine with existing aesthetics - quantiles <- create_quantile_segment_frame(data, draw_quantiles) - aesthetics <- data[ - rep(1, nrow(quantiles)), - setdiff(names(data), c("x", "y", "group")), - drop = FALSE - ] - aesthetics$alpha <- rep(1, nrow(quantiles)) - both <- vec_cbind(quantiles, aesthetics) - both <- both[!is.na(both$group), , drop = FALSE] - both <- flip_data(both, flipped_aes) - quantile_grob <- if (nrow(both) == 0) { - zeroGrob() - } else { - GeomPath$draw_panel(both, ...) - } - - ggname("geom_violin", grobTree( - GeomPolygon$draw_panel(newdata, ...), - quantile_grob) - ) + quantiles <- newdata[!is.na(newdata$quantile),] + quantiles$group <- match(quantiles$quantile, unique(quantiles$quantile)) + quantiles$linetype <- quantile_gp$linetype %||% quantiles$linetype + quantiles$linewidth <- quantile_gp$linewidth %||% quantiles$linewidth + quantiles$colour <- quantile_gp$colour %||% quantiles$colour + + quantile_grob <- if (nrow(quantiles) == 0) { + zeroGrob() } else { - ggname("geom_violin", GeomPolygon$draw_panel(newdata, ...)) + GeomPath$draw_panel(quantiles, ...) } + + ggname("geom_violin", grobTree(violin_grob, quantile_grob)) }, draw_key = draw_key_polygon, diff --git a/R/stat-ydensity.R b/R/stat-ydensity.R index 4eadd8ca58..6b0e4f0ff8 100644 --- a/R/stat-ydensity.R +++ b/R/stat-ydensity.R @@ -7,6 +7,8 @@ #' @param drop Whether to discard groups with less than 2 observations #' (`TRUE`, default) or keep such groups for position adjustment purposes #' (`FALSE`). +#' @param quantiles If not `NULL` (default), compute the `quantile` variable +#' and draw horizontal lines at the given quantiles in `geom_violin()`. #' #' @eval rd_computed_vars( #' density = "Density estimate.", @@ -16,7 +18,8 @@ #' violinwidth = "Density scaled for the violin plot, according to area, #' counts or to a constant maximum width.", #' n = "Number of points.", -#' width = "Width of violin bounding box." +#' width = "Width of violin bounding box.", +#' quantile = "Whether the row is part of the `quantiles` computation." #' ) #' #' @seealso [geom_violin()] for examples, and [stat_density()] @@ -26,6 +29,7 @@ stat_ydensity <- function(mapping = NULL, data = NULL, geom = "violin", position = "dodge", ..., + quantiles = c(0.25, 0.50, 0.75), bw = "nrd0", adjust = 1, kernel = "gaussian", @@ -56,6 +60,7 @@ stat_ydensity <- function(mapping = NULL, data = NULL, drop = drop, na.rm = na.rm, bounds = bounds, + quantiles = quantiles, ... ) ) @@ -73,14 +78,26 @@ StatYdensity <- ggproto("StatYdensity", Stat, setup_params = function(data, params) { params$flipped_aes <- has_flipped_aes(data, params, main_is_orthogonal = TRUE, group_has_equal = TRUE) + if (!is.null(params$draw_quantiles)) { + deprecate_soft0( + "3.6.0", + what = "stat_ydensity(draw_quantiles)", + with = "stat_ydensity(quantiles)" + ) + params$quantiles <- params$draw_quantiles + check_numeric(params$quantiles, arg = "quantiles") + } + params }, - extra_params = c("na.rm", "orientation"), + # `draw_quantiles` is here for deprecation repair reasons + extra_params = c("na.rm", "orientation", "draw_quantiles"), compute_group = function(self, data, scales, width = NULL, bw = "nrd0", adjust = 1, kernel = "gaussian", trim = TRUE, na.rm = FALSE, - drop = TRUE, flipped_aes = FALSE, bounds = c(-Inf, Inf)) { + drop = TRUE, flipped_aes = FALSE, bounds = c(-Inf, Inf), + quantiles = c(0.25, 0.50, 0.75)) { if (nrow(data) < 2) { if (isTRUE(drop)) { cli::cli_warn(c( @@ -115,17 +132,43 @@ StatYdensity <- ggproto("StatYdensity", Stat, } dens$width <- width + if (!is.null(quantiles)) { + if (!(all(quantiles >= 0) && all(quantiles <= 1))) { + cli::cli_abort("{.arg quantiles} must be between 0 and 1.") + } + if (!is.null(data[["weight"]]) || !all(data[["weight"]] == 1)) { + cli::cli_warn( + "{.arg quantiles} for weighted data is not implemented." + ) + } + quants <- quantile(data$y, probs = quantiles) + quants <- data_frame0( + y = unname(quants), + quantile = quantiles + ) + + # Interpolate other metrics + for (var in setdiff(names(dens), names(quants))) { + quants[[var]] <- + approx(dens$y, dens[[var]], xout = quants$y, ties = "ordered")$y + } + + dens <- vec_slice(dens, !dens$y %in% quants$y) + dens <- vec_c(dens, quants) + } + dens }, compute_panel = function(self, data, scales, width = NULL, bw = "nrd0", adjust = 1, kernel = "gaussian", trim = TRUE, na.rm = FALSE, scale = "area", flipped_aes = FALSE, drop = TRUE, - bounds = c(-Inf, Inf)) { + bounds = c(-Inf, Inf), quantiles = c(0.25, 0.50, 0.75)) { data <- flip_data(data, flipped_aes) data <- ggproto_parent(Stat, self)$compute_panel( data, scales, width = width, bw = bw, adjust = adjust, kernel = kernel, trim = trim, na.rm = na.rm, drop = drop, bounds = bounds, + quantiles = quantiles ) if (!drop && any(data$n < 2)) { cli::cli_warn( diff --git a/man/geom_violin.Rd b/man/geom_violin.Rd index 974d1c5bdc..244a7ac7ea 100644 --- a/man/geom_violin.Rd +++ b/man/geom_violin.Rd @@ -11,9 +11,13 @@ geom_violin( stat = "ydensity", position = "dodge", ..., - draw_quantiles = NULL, trim = TRUE, bounds = c(-Inf, Inf), + quantile.colour = NULL, + quantile.color = NULL, + quantile.linetype = 0L, + quantile.linewidth = NULL, + draw_quantiles = deprecated(), scale = "area", na.rm = FALSE, orientation = NA, @@ -27,6 +31,7 @@ stat_ydensity( geom = "violin", position = "dodge", ..., + quantiles = c(0.25, 0.5, 0.75), bw = "nrd0", adjust = 1, kernel = "gaussian", @@ -102,9 +107,6 @@ lists which parameters it can accept. \link[=draw_key]{key glyphs}, to change the display of the layer in the legend. }} -\item{draw_quantiles}{If \code{not(NULL)} (default), draw horizontal lines -at the given quantiles of the density estimate.} - \item{trim}{If \code{TRUE} (default), trim the tails of the violins to the range of the data. If \code{FALSE}, don't trim the tails.} @@ -114,6 +116,13 @@ finite, boundary effect of default density estimation will be corrected by reflecting tails outside \code{bounds} around their closest edge. Data points outside of bounds are removed with a warning.} +\item{quantile.colour, quantile.color, quantile.linewidth, quantile.linetype}{Default aesthetics for the quantile lines. Set to \code{NULL} to inherit from +the data's aesthetics. By default, quantile lines are hidden and can be +turned on by changing \code{quantile.linetype}.} + +\item{draw_quantiles}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Previous +specification of drawing quantiles.} + \item{scale}{if "area" (default), all violins have the same area (before trimming the tails). If "count", areas are scaled proportionally to the number of observations. If "width", all violins have the same maximum width.} @@ -144,6 +153,9 @@ the default plot specification, e.g. \code{\link[=borders]{borders()}}.} overriding these connections, see how the \link[=layer_stats]{stat} and \link[=layer_geoms]{geom} arguments work.} +\item{quantiles}{If not \code{NULL} (default), compute the \code{quantile} variable +and draw horizontal lines at the given quantiles in \code{geom_violin()}.} + \item{bw}{The smoothing bandwidth to be used. If numeric, the standard deviation of the smoothing kernel. If character, a rule to choose the bandwidth, as listed in @@ -198,6 +210,7 @@ These are calculated by the 'stat' part of layers and can be accessed with \link \item \code{after_stat(violinwidth)}\cr Density scaled for the violin plot, according to area, counts or to a constant maximum width. \item \code{after_stat(n)}\cr Number of points. \item \code{after_stat(width)}\cr Width of violin bounding box. +\item \code{after_stat(quantile)}\cr Whether the row is part of the \code{quantiles} computation. } } diff --git a/tests/testthat/_snaps/geom-violin.md b/tests/testthat/_snaps/geom-violin.md index 80da5aad02..68cc4c1c5a 100644 --- a/tests/testthat/_snaps/geom-violin.md +++ b/tests/testthat/_snaps/geom-violin.md @@ -1,14 +1,12 @@ # quantiles fails outside 0-1 bound - Problem while converting geom to grob. - i Error occurred in the 1st layer. - Caused by error in `draw_group()`: - ! `draw_quantiles` must be between 0 and 1. + Computation failed in `stat_ydensity()`. + Caused by error in `compute_group()`: + ! `quantiles` must be between 0 and 1. --- - Problem while converting geom to grob. - i Error occurred in the 1st layer. - Caused by error in `draw_group()`: - ! `draw_quantiles` must be between 0 and 1. + Computation failed in `stat_ydensity()`. + Caused by error in `compute_group()`: + ! `quantiles` must be between 0 and 1. diff --git a/tests/testthat/_snaps/geom-violin/basic.svg b/tests/testthat/_snaps/geom-violin/basic.svg index 206a6b4626..16e7518c21 100644 --- a/tests/testthat/_snaps/geom-violin/basic.svg +++ b/tests/testthat/_snaps/geom-violin/basic.svg @@ -27,9 +27,9 @@ - - - + + + diff --git a/tests/testthat/_snaps/geom-violin/continuous-x-axis-many-groups-center-should-be-at-2-0.svg b/tests/testthat/_snaps/geom-violin/continuous-x-axis-many-groups-center-should-be-at-2-0.svg index f737690144..611f73f969 100644 --- a/tests/testthat/_snaps/geom-violin/continuous-x-axis-many-groups-center-should-be-at-2-0.svg +++ b/tests/testthat/_snaps/geom-violin/continuous-x-axis-many-groups-center-should-be-at-2-0.svg @@ -27,7 +27,7 @@ - + diff --git a/tests/testthat/_snaps/geom-violin/continuous-x-axis-single-group-center-should-be-at-1-0.svg b/tests/testthat/_snaps/geom-violin/continuous-x-axis-single-group-center-should-be-at-1-0.svg index f11a934abb..74fc5ed64e 100644 --- a/tests/testthat/_snaps/geom-violin/continuous-x-axis-single-group-center-should-be-at-1-0.svg +++ b/tests/testthat/_snaps/geom-violin/continuous-x-axis-single-group-center-should-be-at-1-0.svg @@ -27,7 +27,7 @@ - + diff --git a/tests/testthat/_snaps/geom-violin/coord-flip.svg b/tests/testthat/_snaps/geom-violin/coord-flip.svg index 434afe96c8..59f095248a 100644 --- a/tests/testthat/_snaps/geom-violin/coord-flip.svg +++ b/tests/testthat/_snaps/geom-violin/coord-flip.svg @@ -27,9 +27,9 @@ - - - + + + diff --git a/tests/testthat/_snaps/geom-violin/coord-polar.svg b/tests/testthat/_snaps/geom-violin/coord-polar.svg index e70e3b11f3..02ae1107df 100644 --- a/tests/testthat/_snaps/geom-violin/coord-polar.svg +++ b/tests/testthat/_snaps/geom-violin/coord-polar.svg @@ -36,9 +36,9 @@ - - - + + + A B C diff --git a/tests/testthat/_snaps/geom-violin/dodging-and-coord-flip.svg b/tests/testthat/_snaps/geom-violin/dodging-and-coord-flip.svg index 86a328e5b5..6af10a6faa 100644 --- a/tests/testthat/_snaps/geom-violin/dodging-and-coord-flip.svg +++ b/tests/testthat/_snaps/geom-violin/dodging-and-coord-flip.svg @@ -27,9 +27,9 @@ - - - + + + diff --git a/tests/testthat/_snaps/geom-violin/dodging.svg b/tests/testthat/_snaps/geom-violin/dodging.svg index c1ccf480ce..d1d537e3b2 100644 --- a/tests/testthat/_snaps/geom-violin/dodging.svg +++ b/tests/testthat/_snaps/geom-violin/dodging.svg @@ -27,9 +27,9 @@ - - - + + + diff --git a/tests/testthat/_snaps/geom-violin/grouping-on-x-and-fill-dodge-width-0-5.svg b/tests/testthat/_snaps/geom-violin/grouping-on-x-and-fill-dodge-width-0-5.svg index 17142781de..fcf5700ada 100644 --- a/tests/testthat/_snaps/geom-violin/grouping-on-x-and-fill-dodge-width-0-5.svg +++ b/tests/testthat/_snaps/geom-violin/grouping-on-x-and-fill-dodge-width-0-5.svg @@ -27,12 +27,12 @@ - - - - - - + + + + + + diff --git a/tests/testthat/_snaps/geom-violin/grouping-on-x-and-fill.svg b/tests/testthat/_snaps/geom-violin/grouping-on-x-and-fill.svg index 56049d8ef6..477f9a02c5 100644 --- a/tests/testthat/_snaps/geom-violin/grouping-on-x-and-fill.svg +++ b/tests/testthat/_snaps/geom-violin/grouping-on-x-and-fill.svg @@ -27,12 +27,12 @@ - - - - - - + + + + + + diff --git a/tests/testthat/_snaps/geom-violin/narrower-width-5.svg b/tests/testthat/_snaps/geom-violin/narrower-width-5.svg index d7a23e057b..d233183697 100644 --- a/tests/testthat/_snaps/geom-violin/narrower-width-5.svg +++ b/tests/testthat/_snaps/geom-violin/narrower-width-5.svg @@ -27,9 +27,9 @@ - - - + + + diff --git a/tests/testthat/_snaps/geom-violin/quantiles.svg b/tests/testthat/_snaps/geom-violin/quantiles.svg deleted file mode 100644 index 8bec1ac1a6..0000000000 --- a/tests/testthat/_snaps/geom-violin/quantiles.svg +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --3 --2 --1 -0 -1 -2 -3 - - - - - - - - - - -A -B -C -x -y -quantiles - - diff --git a/tests/testthat/_snaps/geom-violin/scale-area-to-sample-size-c-is-smaller.svg b/tests/testthat/_snaps/geom-violin/scale-area-to-sample-size-c-is-smaller.svg index 1c0bf845b4..ca9f1bf889 100644 --- a/tests/testthat/_snaps/geom-violin/scale-area-to-sample-size-c-is-smaller.svg +++ b/tests/testthat/_snaps/geom-violin/scale-area-to-sample-size-c-is-smaller.svg @@ -27,9 +27,9 @@ - - - + + + diff --git a/tests/testthat/_snaps/geom-violin/styled-quantiles.svg b/tests/testthat/_snaps/geom-violin/styled-quantiles.svg new file mode 100644 index 0000000000..0b8d55329f --- /dev/null +++ b/tests/testthat/_snaps/geom-violin/styled-quantiles.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +-3 +-2 +-1 +0 +1 +2 +3 + + + + + + + + + + +A +B +C +x +y +styled quantiles + + diff --git a/tests/testthat/_snaps/geom-violin/with-smaller-bandwidth-and-points.svg b/tests/testthat/_snaps/geom-violin/with-smaller-bandwidth-and-points.svg index 1494c6bd08..3dc573d465 100644 --- a/tests/testthat/_snaps/geom-violin/with-smaller-bandwidth-and-points.svg +++ b/tests/testthat/_snaps/geom-violin/with-smaller-bandwidth-and-points.svg @@ -27,9 +27,9 @@ - - - + + + diff --git a/tests/testthat/_snaps/geom-violin/with-tails-and-points.svg b/tests/testthat/_snaps/geom-violin/with-tails-and-points.svg index 1db22dd441..d109c20fbc 100644 --- a/tests/testthat/_snaps/geom-violin/with-tails-and-points.svg +++ b/tests/testthat/_snaps/geom-violin/with-tails-and-points.svg @@ -27,9 +27,9 @@ - - - + + + diff --git a/tests/testthat/test-geom-violin.R b/tests/testthat/test-geom-violin.R index a93a534b40..ff3cae8de8 100644 --- a/tests/testthat/test-geom-violin.R +++ b/tests/testthat/test-geom-violin.R @@ -40,7 +40,8 @@ test_that("create_quantile_segment_frame functions for 3 quantiles", { test_that("quantiles do not fail on zero-range data", { zero.range.data <- data_frame(y = rep(1,3)) - p <- ggplot(zero.range.data) + geom_violin(aes(1, y), draw_quantiles = 0.5) + p <- ggplot(zero.range.data) + + geom_violin(aes(1, y), quantiles = 0.5, quantile.linetype = NULL) # This should return without error and have length one expect_length(get_layer_grob(p), 1) @@ -48,10 +49,10 @@ test_that("quantiles do not fail on zero-range data", { test_that("quantiles fails outside 0-1 bound", { p <- ggplot(mtcars) + - geom_violin(aes(as.factor(gear), mpg), draw_quantiles = c(-1, 0.5)) + geom_violin(aes(as.factor(gear), mpg), quantiles = c(-1, 0.5)) expect_snapshot_error(ggplotGrob(p)) p <- ggplot(mtcars) + - geom_violin(aes(as.factor(gear), mpg), draw_quantiles = c(0.5, 2)) + geom_violin(aes(as.factor(gear), mpg), quantiles = c(0.5, 2)) expect_snapshot_error(ggplotGrob(p)) }) @@ -70,7 +71,7 @@ test_that("quantiles do not issue warning", { data <- data_frame(x = 1, y = c(0, 0.25, 0.5, 0.75, 5)) p <- ggplot(data, aes(x = x, y = y)) + - geom_violin(draw_quantiles = 0.5) + geom_violin(quantiles = 0.5, quantile.linetype = NULL) expect_silent(plot(p)) }) @@ -116,8 +117,13 @@ test_that("geom_violin draws correctly", { expect_doppelganger("continuous x axis, single group (center should be at 1.0)", ggplot(dat, aes(x = as.numeric(1), y = y)) + geom_violin() ) - expect_doppelganger("quantiles", - ggplot(dat, aes(x=x, y=y)) + geom_violin(draw_quantiles=c(0.25,0.5,0.75)) + expect_doppelganger("styled quantiles", + ggplot(dat, aes(x=x, y=y)) + + geom_violin( + quantile.colour = "red", + quantile.linetype = "dotted", + quantile.linewidth = 2 + ) ) dat2 <- data_frame(x = rep(factor(LETTERS[1:3]), 30), y = rnorm(90), g = rep(factor(letters[5:6]), 45)) diff --git a/tests/testthat/test-stat-ydensity.R b/tests/testthat/test-stat-ydensity.R index d43fbcc0e3..fb5d39c036 100644 --- a/tests/testthat/test-stat-ydensity.R +++ b/tests/testthat/test-stat-ydensity.R @@ -39,3 +39,15 @@ test_that("mapped_discrete class is preserved", { expect_s3_class(ld$x, "mapped_discrete") expect_equal(unique(ld$x), c(1, 3)) }) + +test_that("quantiles are based on actual data (#4120)", { + + df <- data.frame(y = 0:10) + q <- seq(0.1, 0.9, by = 0.1) + + p <- ggplot(df, aes("X", y)) + + stat_ydensity(quantiles = q) + ld <- get_layer_data(p) + + expect_equal(ld$y[!is.na(ld$quantile)], 1:9) +})