From 83ab2c7c2592581b6f8c824bab87fdd9b70e06d9 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Tue, 7 Jan 2025 12:48:09 +0100 Subject: [PATCH 1/8] simplify expansion --- R/scale-expansion.R | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/R/scale-expansion.R b/R/scale-expansion.R index a132f5cd22..d8133288cc 100644 --- a/R/scale-expansion.R +++ b/R/scale-expansion.R @@ -81,9 +81,7 @@ expand_range4 <- function(limits, expand) { # Calculate separate range expansion for the lower and # upper range limits, and then combine them into one vector - lower <- expand_range(limits, expand[1], expand[2])[1] - upper <- expand_range(limits, expand[3], expand[4])[2] - c(lower, upper) + expand_range(limits, expand[c(1, 3)], expand[c(2, 4)]) } #' Calculate the default expansion for a scale From 0807f52d8b5d8053e47c0899a77f35cc9326eaa5 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Tue, 7 Jan 2025 12:53:26 +0100 Subject: [PATCH 2/8] add `continuous.limits` as scale argument --- R/scale-discrete-.R | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/R/scale-discrete-.R b/R/scale-discrete-.R index f6fc512f9c..1c1f531338 100644 --- a/R/scale-discrete-.R +++ b/R/scale-discrete-.R @@ -69,7 +69,8 @@ #' } scale_x_discrete <- function(name = waiver(), ..., palette = seq_len, expand = waiver(), guide = waiver(), - position = "bottom", sec.axis = waiver()) { + position = "bottom", sec.axis = waiver(), + continuous.limits = NULL) { sc <- discrete_scale( aesthetics = ggplot_global$x_aes, name = name, palette = palette, ..., @@ -78,13 +79,15 @@ scale_x_discrete <- function(name = waiver(), ..., palette = seq_len, ) sc$range_c <- ContinuousRange$new() + sc$continuous_limits <- continuous.limits set_sec_axis(sec.axis, sc) } #' @rdname scale_discrete #' @export scale_y_discrete <- function(name = waiver(), ..., palette = seq_len, expand = waiver(), guide = waiver(), - position = "left", sec.axis = waiver()) { + position = "left", sec.axis = waiver(), + continuous.limits = NULL) { sc <- discrete_scale( aesthetics = ggplot_global$y_aes, name = name, palette = palette, ..., @@ -93,6 +96,7 @@ scale_y_discrete <- function(name = waiver(), ..., palette = seq_len, ) sc$range_c <- ContinuousRange$new() + sc$continuous_limits <- continuous.limits set_sec_axis(sec.axis, sc) } @@ -106,6 +110,8 @@ scale_y_discrete <- function(name = waiver(), ..., palette = seq_len, #' @usage NULL #' @export ScaleDiscretePosition <- ggproto("ScaleDiscretePosition", ScaleDiscrete, + continuous_limits = NULL, + train = function(self, x) { if (is.discrete(x)) { self$range$train(x, drop = self$drop, na.rm = !self$na.translate) From 7c5a27d8c88dabb16f65befd4b5b0b25eb8c8d3c Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Tue, 7 Jan 2025 12:53:53 +0100 Subject: [PATCH 3/8] allow `scale$continuous_limits` to overrule expansion limits --- R/scale-expansion.R | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/R/scale-expansion.R b/R/scale-expansion.R index d8133288cc..75b7d70c3b 100644 --- a/R/scale-expansion.R +++ b/R/scale-expansion.R @@ -146,12 +146,20 @@ expand_limits_scale <- function(scale, expand = expansion(0, 0), limits = waiver limits <- limits %|W|% scale$get_limits() if (scale$is_discrete()) { + continuous_limits <- scale$continuous_limits + if (is.function(continuous_limits)) { + continuous_limits <- continuous_limits(limits) + } + if (!is.null(continuous_limits)) { + continuous_limits <- range(continuous_limits) + check_numeric(continuous_limits, call = scale$call, arg = "continuous.limits") + } coord_limits <- coord_limits %||% c(NA_real_, NA_real_) expand_limits_discrete( - scale$map(limits), + continuous_limits %||% scale$map(limits), expand, coord_limits, - range_continuous = scale$range_c$range + range_continuous = continuous_limits %||% scale$range_c$range ) } else { # using the inverse transform to resolve the NA value is needed for date/datetime/time From cb63477d34d783ffe63a0810f862aa83da5d2aba Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Tue, 7 Jan 2025 13:03:38 +0100 Subject: [PATCH 4/8] document --- R/scale-discrete-.R | 5 +++++ man/scale_discrete.Rd | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/R/scale-discrete-.R b/R/scale-discrete-.R index 1c1f531338..a301cac246 100644 --- a/R/scale-discrete-.R +++ b/R/scale-discrete-.R @@ -16,6 +16,11 @@ #' argument (the number of levels in the scale) returns the numerical values #' that they should take. #' @param sec.axis [dup_axis()] is used to specify a secondary axis. +#' @param continuous.limits One of: +#' * `NULL` to use the default scale range +#' * A numeric vector of length two providing a display range for the scale. +#' * A function that accepts the existing discrete limits and returns a +#' numeric vector of length two. #' @rdname scale_discrete #' @family position scales #' @seealso diff --git a/man/scale_discrete.Rd b/man/scale_discrete.Rd index 0bab3ad985..e21eacd1c4 100644 --- a/man/scale_discrete.Rd +++ b/man/scale_discrete.Rd @@ -12,7 +12,8 @@ scale_x_discrete( expand = waiver(), guide = waiver(), position = "bottom", - sec.axis = waiver() + sec.axis = waiver(), + continuous.limits = NULL ) scale_y_discrete( @@ -22,7 +23,8 @@ scale_y_discrete( expand = waiver(), guide = waiver(), position = "left", - sec.axis = waiver() + sec.axis = waiver(), + continuous.limits = NULL ) } \arguments{ @@ -107,6 +109,14 @@ expand the scale by 5\% on each side for continuous variables, and by \code{left} or \code{right} for y axes, \code{top} or \code{bottom} for x axes.} \item{sec.axis}{\code{\link[=dup_axis]{dup_axis()}} is used to specify a secondary axis.} + +\item{continuous.limits}{One of: +\itemize{ +\item \code{NULL} to use the default scale range +\item A numeric vector of length two providing a display range for the scale. +\item A function that accepts the existing discrete limits and returns a +numeric vector of length two. +}} } \description{ \code{scale_x_discrete()} and \code{scale_y_discrete()} are used to set the values for From fe13b78a927cf00e0a1531b1495bb8e9fa23a2a4 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Tue, 7 Jan 2025 13:16:53 +0100 Subject: [PATCH 5/8] confine and improve --- R/scale-expansion.R | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/R/scale-expansion.R b/R/scale-expansion.R index 75b7d70c3b..ce61a1c5e3 100644 --- a/R/scale-expansion.R +++ b/R/scale-expansion.R @@ -146,20 +146,13 @@ expand_limits_scale <- function(scale, expand = expansion(0, 0), limits = waiver limits <- limits %|W|% scale$get_limits() if (scale$is_discrete()) { - continuous_limits <- scale$continuous_limits - if (is.function(continuous_limits)) { - continuous_limits <- continuous_limits(limits) - } - if (!is.null(continuous_limits)) { - continuous_limits <- range(continuous_limits) - check_numeric(continuous_limits, call = scale$call, arg = "continuous.limits") - } coord_limits <- coord_limits %||% c(NA_real_, NA_real_) expand_limits_discrete( - continuous_limits %||% scale$map(limits), + scale$map(limits), expand, coord_limits, - range_continuous = continuous_limits %||% scale$range_c$range + range_continuous = scale$range_c$range, + continuous_limits = scale$continuous_limits ) } else { # using the inverse transform to resolve the NA value is needed for date/datetime/time @@ -176,7 +169,21 @@ expand_limits_continuous <- function(limits, expand = expansion(0, 0), coord_lim } expand_limits_discrete <- function(limits, expand = expansion(0, 0), coord_limits = c(NA, NA), - range_continuous = NULL) { + range_continuous = NULL, continuous_limits = NULL) { + if (is.function(continuous_limits)) { + continuous_limits <- continuous_limits(limits) + } + if (!is.null(continuous_limits)) { + if (!anyNA(continuous_limits)) { + continuous_limits <- range(continuous_limits) + } + check_numeric(continuous_limits, arg = "continuous.limits") + check_length(continuous_limits, 2L, arg = "continuous.limits") + missing <- is.na(continuous_limits) + limits <- range(ifelse(missing, limits, continuous_limits)) + coord_limits <- range(ifelse(missing, coord_limits, continuous_limits)) + } + limit_info <- expand_limits_discrete_trans( limits, expand, From 48b83f4837d35a661cea02b61b854fccefdd6b0d Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Tue, 7 Jan 2025 13:29:48 +0100 Subject: [PATCH 6/8] add news bullet --- NEWS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS.md b/NEWS.md index e19471d2e2..539dee258c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # ggplot2 (development version) +* `scale_{x/y}_discrete(continuous.limits)` is a new argument to control the + display range of discrete scales (@teunbrand, #4174, #6259). * `geom_ribbon()` now appropriately warns about, and removes, missing values (@teunbrand, #6243). * `guide_*()` can now accept two inside legend theme elements: From fb1f531909c1e63d868667ac93d331e9319c0701 Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Tue, 7 Jan 2025 13:35:21 +0100 Subject: [PATCH 7/8] add tests --- tests/testthat/test-scale-expansion.R | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/testthat/test-scale-expansion.R b/tests/testthat/test-scale-expansion.R index 41bd9430e7..331c6a651d 100644 --- a/tests/testthat/test-scale-expansion.R +++ b/tests/testthat/test-scale-expansion.R @@ -96,6 +96,27 @@ test_that("expand_limits_discrete() can override limits with a both discrete and expand_limits_discrete(c("one", "two"), coord_limits = c(0, NA), range_continuous = c(1, 2)), c(0, 2) ) + expect_identical( + expand_limits_discrete(1:2, range_continuous = c(1, 2), continuous_limits = c(0, 3)), + c(0, 3) + ) + expect_identical( + expand_limits_discrete(1:2, range_continuous = c(1, 2), continuous_limits = c(NA, 4)), + c(1, 4) + ) + expect_identical( + expand_limits_discrete(1:2, range_continuous = c(1, 2), continuous_limits = c(0, NA)), + c(0, 2) + ) + expect_identical( + expand_limits_discrete(1:2, range_continuous = c(1, 2), continuous_limits = c(NA_real_, NA_real_)), + c(1, 2) + ) + expect_identical( + expand_limits_discrete(1:2, range_continuous = 1:2, + continuous_limits = function(x) x + c(-1, 1)), + c(0, 3) + ) }) test_that("expand_limits_continuous_trans() works with inverted transformations", { From ad8e6c7d67b7bfbe136406f2ec79e3849c22237f Mon Sep 17 00:00:00 2001 From: Teun van den Brand Date: Tue, 7 Jan 2025 13:36:12 +0100 Subject: [PATCH 8/8] fix wrong description --- R/scale-discrete-.R | 2 +- man/scale_discrete.Rd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/R/scale-discrete-.R b/R/scale-discrete-.R index a301cac246..bb1518746f 100644 --- a/R/scale-discrete-.R +++ b/R/scale-discrete-.R @@ -19,7 +19,7 @@ #' @param continuous.limits One of: #' * `NULL` to use the default scale range #' * A numeric vector of length two providing a display range for the scale. -#' * A function that accepts the existing discrete limits and returns a +#' * A function that accepts the existing continuous limits and returns a #' numeric vector of length two. #' @rdname scale_discrete #' @family position scales diff --git a/man/scale_discrete.Rd b/man/scale_discrete.Rd index e21eacd1c4..7e6e251980 100644 --- a/man/scale_discrete.Rd +++ b/man/scale_discrete.Rd @@ -114,7 +114,7 @@ expand the scale by 5\% on each side for continuous variables, and by \itemize{ \item \code{NULL} to use the default scale range \item A numeric vector of length two providing a display range for the scale. -\item A function that accepts the existing discrete limits and returns a +\item A function that accepts the existing continuous limits and returns a numeric vector of length two. }} }