From d887912ab66ae2fa1ca69c655f4d44dd4f27b456 Mon Sep 17 00:00:00 2001 From: munoztd0 Date: Thu, 12 Mar 2026 11:22:19 +0100 Subject: [PATCH 1/5] update: geom_boxplot to add quantile type argument --- R/geom-boxplot.R | 1 + R/stat-boxplot.R | 6 ++-- man/geom_boxplot.Rd | 3 ++ .../test-geom-boxplot-quantile-type.R | 28 +++++++++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 tests/testthat/test-geom-boxplot-quantile-type.R diff --git a/R/geom-boxplot.R b/R/geom-boxplot.R index 69aad3046c..5b0e55c58d 100644 --- a/R/geom-boxplot.R +++ b/R/geom-boxplot.R @@ -68,6 +68,7 @@ #' `TRUE`, boxes are drawn with widths proportional to the #' square-roots of the number of observations in the groups (possibly #' weighted, using the `weight` aesthetic). +#' @param quantile_type Passed to `stats::quantile(type = )`, defaults to `7` #' @note In the unlikely event you specify both US and UK spellings of colour, #' the US spelling will take precedence. #' diff --git a/R/stat-boxplot.R b/R/stat-boxplot.R index cfc137d4c3..ec06ccf440 100644 --- a/R/stat-boxplot.R +++ b/R/stat-boxplot.R @@ -53,7 +53,7 @@ StatBoxplot <- ggproto("StatBoxplot", Stat, extra_params = c("na.rm", "orientation"), - compute_group = function(data, scales, width = NULL, na.rm = FALSE, coef = 1.5, flipped_aes = FALSE) { + compute_group = function(data, scales, width = NULL, na.rm = FALSE, coef = 1.5, flipped_aes = FALSE, quantile_type = 7) { data <- flip_data(data, flipped_aes) qs <- c(0, 0.25, 0.5, 0.75, 1) @@ -61,7 +61,8 @@ StatBoxplot <- ggproto("StatBoxplot", Stat, mod <- quantreg::rq(y ~ 1, weights = weight, data = data, tau = qs) stats <- as.numeric(stats::coef(mod)) } else { - stats <- as.numeric(stats::quantile(data$y, qs)) + # Follow base R default (type = 7) unless overridden by user + stats <- as.numeric(stats::quantile(data$y, qs, type = quantile_type)) } names(stats) <- c("ymin", "lower", "middle", "upper", "ymax") iqr <- diff(stats[c(2, 4)]) @@ -99,6 +100,7 @@ StatBoxplot <- ggproto("StatBoxplot", Stat, #' @rdname geom_boxplot #' @param coef Length of the whiskers as multiple of IQR. Defaults to 1.5. +#' @param quantile_type Passed to `stats::quantile(type = )`, defaults to `7` #' @inheritParams shared_layer_parameters #' @export #' @eval rd_computed_vars( diff --git a/man/geom_boxplot.Rd b/man/geom_boxplot.Rd index 5f9962f6a0..81a525ae31 100644 --- a/man/geom_boxplot.Rd +++ b/man/geom_boxplot.Rd @@ -53,6 +53,7 @@ stat_boxplot( ..., orientation = NA, coef = 1.5, + quantile_type = 7, na.rm = FALSE, show.legend = NA, inherit.aes = TRUE @@ -180,6 +181,8 @@ overriding these connections, see how the \link[=layer_stats]{stat} and \link[=layer_geoms]{geom} arguments work.} \item{coef}{Length of the whiskers as multiple of IQR. Defaults to 1.5.} + +\item{quantile_type}{Passed to \code{stats::quantile(type = )}, defaults to \code{7}} } \description{ The boxplot compactly displays the distribution of a continuous variable. diff --git a/tests/testthat/test-geom-boxplot-quantile-type.R b/tests/testthat/test-geom-boxplot-quantile-type.R new file mode 100644 index 0000000000..e4bed1022c --- /dev/null +++ b/tests/testthat/test-geom-boxplot-quantile-type.R @@ -0,0 +1,28 @@ +test_that("quantile_type changes hinges for small samples (unweighted)", { + df <- data_frame(x = 1, y = c(1, 2, 3, 4)) + + p_default <- ggplot(df, aes(x, y)) + stat_boxplot() + d_default <- get_layer_data(p_default) + + p_t2 <- ggplot(df, aes(x, y)) + stat_boxplot(quantile_type = 2) + d_t2 <- get_layer_data(p_t2) + + # Lower/upper hinges should differ under different quantile definitions + expect_false(isTRUE(all.equal(d_default$lower, d_t2$lower))) + expect_false(isTRUE(all.equal(d_default$upper, d_t2$upper))) +}) + +test_that("quantile_type = 7 matches default behavior (backward compatible)", { + set.seed(123) + df <- data_frame(x = 1, y = rnorm(25)) + + p_default <- ggplot(df, aes(x, y)) + stat_boxplot() + d_default <- get_layer_data(p_default) + + p_t7 <- ggplot(df, aes(x, y)) + stat_boxplot(quantile_type = 7) + d_t7 <- get_layer_data(p_t7) + + expect_equal(d_default$lower, d_t7$lower) + expect_equal(d_default$middle, d_t7$middle) + expect_equal(d_default$upper, d_t7$upper) +}) From c682a7285fab97df4b34f538c6f679a504ea0d3c Mon Sep 17 00:00:00 2001 From: munoztd0 Date: Thu, 12 Mar 2026 15:43:37 +0100 Subject: [PATCH 2/5] update: change quantile_type to quantile.type --- R/geom-boxplot.R | 2 +- R/stat-boxplot.R | 6 ++-- .../test-geom-boxplot-quantile-type.R | 28 ------------------ tests/testthat/test-geom-boxplot.R | 29 +++++++++++++++++++ 4 files changed, 33 insertions(+), 32 deletions(-) delete mode 100644 tests/testthat/test-geom-boxplot-quantile-type.R diff --git a/R/geom-boxplot.R b/R/geom-boxplot.R index 5b0e55c58d..33b7c2930f 100644 --- a/R/geom-boxplot.R +++ b/R/geom-boxplot.R @@ -68,7 +68,7 @@ #' `TRUE`, boxes are drawn with widths proportional to the #' square-roots of the number of observations in the groups (possibly #' weighted, using the `weight` aesthetic). -#' @param quantile_type Passed to `stats::quantile(type = )`, defaults to `7` +#' @param quantile.type Passed to `stats::quantile(type = )`, defaults to `7` #' @note In the unlikely event you specify both US and UK spellings of colour, #' the US spelling will take precedence. #' diff --git a/R/stat-boxplot.R b/R/stat-boxplot.R index ec06ccf440..e71a457fe9 100644 --- a/R/stat-boxplot.R +++ b/R/stat-boxplot.R @@ -53,7 +53,7 @@ StatBoxplot <- ggproto("StatBoxplot", Stat, extra_params = c("na.rm", "orientation"), - compute_group = function(data, scales, width = NULL, na.rm = FALSE, coef = 1.5, flipped_aes = FALSE, quantile_type = 7) { + compute_group = function(data, scales, width = NULL, na.rm = FALSE, coef = 1.5, flipped_aes = FALSE, quantile.type = 7) { data <- flip_data(data, flipped_aes) qs <- c(0, 0.25, 0.5, 0.75, 1) @@ -62,7 +62,7 @@ StatBoxplot <- ggproto("StatBoxplot", Stat, stats <- as.numeric(stats::coef(mod)) } else { # Follow base R default (type = 7) unless overridden by user - stats <- as.numeric(stats::quantile(data$y, qs, type = quantile_type)) + stats <- as.numeric(stats::quantile(data$y, qs, type = quantile.type)) } names(stats) <- c("ymin", "lower", "middle", "upper", "ymax") iqr <- diff(stats[c(2, 4)]) @@ -100,7 +100,7 @@ StatBoxplot <- ggproto("StatBoxplot", Stat, #' @rdname geom_boxplot #' @param coef Length of the whiskers as multiple of IQR. Defaults to 1.5. -#' @param quantile_type Passed to `stats::quantile(type = )`, defaults to `7` +#' @param quantile.type Passed to `stats::quantile(type = )`, defaults to `7` #' @inheritParams shared_layer_parameters #' @export #' @eval rd_computed_vars( diff --git a/tests/testthat/test-geom-boxplot-quantile-type.R b/tests/testthat/test-geom-boxplot-quantile-type.R deleted file mode 100644 index e4bed1022c..0000000000 --- a/tests/testthat/test-geom-boxplot-quantile-type.R +++ /dev/null @@ -1,28 +0,0 @@ -test_that("quantile_type changes hinges for small samples (unweighted)", { - df <- data_frame(x = 1, y = c(1, 2, 3, 4)) - - p_default <- ggplot(df, aes(x, y)) + stat_boxplot() - d_default <- get_layer_data(p_default) - - p_t2 <- ggplot(df, aes(x, y)) + stat_boxplot(quantile_type = 2) - d_t2 <- get_layer_data(p_t2) - - # Lower/upper hinges should differ under different quantile definitions - expect_false(isTRUE(all.equal(d_default$lower, d_t2$lower))) - expect_false(isTRUE(all.equal(d_default$upper, d_t2$upper))) -}) - -test_that("quantile_type = 7 matches default behavior (backward compatible)", { - set.seed(123) - df <- data_frame(x = 1, y = rnorm(25)) - - p_default <- ggplot(df, aes(x, y)) + stat_boxplot() - d_default <- get_layer_data(p_default) - - p_t7 <- ggplot(df, aes(x, y)) + stat_boxplot(quantile_type = 7) - d_t7 <- get_layer_data(p_t7) - - expect_equal(d_default$lower, d_t7$lower) - expect_equal(d_default$middle, d_t7$middle) - expect_equal(d_default$upper, d_t7$upper) -}) diff --git a/tests/testthat/test-geom-boxplot.R b/tests/testthat/test-geom-boxplot.R index 82c45c3fb7..21fafc40ee 100644 --- a/tests/testthat/test-geom-boxplot.R +++ b/tests/testthat/test-geom-boxplot.R @@ -109,3 +109,32 @@ test_that("boxplot draws correctly", { ) ) }) + +test_that("quantile.type changes hinges for small samples (unweighted)", { + df <- data_frame(x = 1, y = c(1, 2, 3, 4)) + + p_default <- ggplot(df, aes(x, y)) + stat_boxplot() + d_default <- get_layer_data(p_default) + + p_t2 <- ggplot(df, aes(x, y)) + stat_boxplot(quantile.type = 2) + d_t2 <- get_layer_data(p_t2) + + # Lower/upper hinges should differ under different quantile definitions + expect_false(isTRUE(all.equal(d_default$lower, d_t2$lower))) + expect_false(isTRUE(all.equal(d_default$upper, d_t2$upper))) +}) + +test_that("quantile.type = 7 matches default behavior (backward compatible)", { + set.seed(123) + df <- data_frame(x = 1, y = rnorm(25)) + + p_default <- ggplot(df, aes(x, y)) + stat_boxplot() + d_default <- get_layer_data(p_default) + + p_t7 <- ggplot(df, aes(x, y)) + stat_boxplot(quantile.type = 7) + d_t7 <- get_layer_data(p_t7) + + expect_equal(d_default$lower, d_t7$lower) + expect_equal(d_default$middle, d_t7$middle) + expect_equal(d_default$upper, d_t7$upper) +}) From 0a9e9562c4bf5350eb4feed0846c8b45e531f09f Mon Sep 17 00:00:00 2001 From: munoztd0 Date: Thu, 12 Mar 2026 15:51:38 +0100 Subject: [PATCH 3/5] update: NEWS.md --- NEWS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NEWS.md b/NEWS.md index 2b7a252179..6a9d993fe8 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # ggplot2 (development version) +* `geom_boxplot()`/`stat_boxplot()` gain a `quantile.type` parameter (default `7`) + to control the percentile definition used for hinges and median; set `quantile.type = 2` + to match SAS's default `PCTLDEF = 5`, enabling parity with SAS boxplots out-of-the-box. + (@munoztd0, #6819) * `make_constructor()` no longer captures `rlang::list2()` at build time. * The `arrow` and `arrow.fill` arguments are now available in `geom_linerange()` and `geom_pointrange()` layers (@teunbrand, #6481). From 0a6caaffe32aad2218662c6fd0713d4ccc06f60e Mon Sep 17 00:00:00 2001 From: munoztd0 Date: Thu, 12 Mar 2026 16:28:25 +0100 Subject: [PATCH 4/5] update: document() --- man/geom_boxplot.Rd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/man/geom_boxplot.Rd b/man/geom_boxplot.Rd index 81a525ae31..bfd5ee14b1 100644 --- a/man/geom_boxplot.Rd +++ b/man/geom_boxplot.Rd @@ -53,7 +53,7 @@ stat_boxplot( ..., orientation = NA, coef = 1.5, - quantile_type = 7, + quantile.type = 7, na.rm = FALSE, show.legend = NA, inherit.aes = TRUE @@ -182,7 +182,7 @@ overriding these connections, see how the \link[=layer_stats]{stat} and \item{coef}{Length of the whiskers as multiple of IQR. Defaults to 1.5.} -\item{quantile_type}{Passed to \code{stats::quantile(type = )}, defaults to \code{7}} +\item{quantile.type}{Passed to \code{stats::quantile(type = )}, defaults to \code{7}} } \description{ The boxplot compactly displays the distribution of a continuous variable. From 6f37afbbece7a8c8ec090f1afa08548e57a4cd71 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Wed, 18 Mar 2026 09:44:09 +0100 Subject: [PATCH 5/5] tiny doc polish --- R/geom-boxplot.R | 1 - R/stat-boxplot.R | 3 ++- man/geom_boxplot.Rd | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/R/geom-boxplot.R b/R/geom-boxplot.R index 33b7c2930f..69aad3046c 100644 --- a/R/geom-boxplot.R +++ b/R/geom-boxplot.R @@ -68,7 +68,6 @@ #' `TRUE`, boxes are drawn with widths proportional to the #' square-roots of the number of observations in the groups (possibly #' weighted, using the `weight` aesthetic). -#' @param quantile.type Passed to `stats::quantile(type = )`, defaults to `7` #' @note In the unlikely event you specify both US and UK spellings of colour, #' the US spelling will take precedence. #' diff --git a/R/stat-boxplot.R b/R/stat-boxplot.R index e71a457fe9..5f199511f5 100644 --- a/R/stat-boxplot.R +++ b/R/stat-boxplot.R @@ -100,7 +100,8 @@ StatBoxplot <- ggproto("StatBoxplot", Stat, #' @rdname geom_boxplot #' @param coef Length of the whiskers as multiple of IQR. Defaults to 1.5. -#' @param quantile.type Passed to `stats::quantile(type = )`, defaults to `7` +#' @param quantile.type An integer between 1 and 9 setting the quantile algorithm +#' per [`stats::quantile(type)`][stats::quantile]. Defaults to `7` #' @inheritParams shared_layer_parameters #' @export #' @eval rd_computed_vars( diff --git a/man/geom_boxplot.Rd b/man/geom_boxplot.Rd index bfd5ee14b1..f747628213 100644 --- a/man/geom_boxplot.Rd +++ b/man/geom_boxplot.Rd @@ -182,7 +182,8 @@ overriding these connections, see how the \link[=layer_stats]{stat} and \item{coef}{Length of the whiskers as multiple of IQR. Defaults to 1.5.} -\item{quantile.type}{Passed to \code{stats::quantile(type = )}, defaults to \code{7}} +\item{quantile.type}{An integer between 1 and 9 setting the quantile algorithm +per \code{\link[stats:quantile]{stats::quantile(type)}}. Defaults to \code{7}} } \description{ The boxplot compactly displays the distribution of a continuous variable.