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: diff --git a/R/scale-discrete-.R b/R/scale-discrete-.R index f6fc512f9c..a5ff3289a4 100644 --- a/R/scale-discrete-.R +++ b/R/scale-discrete-.R @@ -16,6 +16,12 @@ #' 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. +#' Use `NA` to refer to the existing minimum or maximum. +#' * A function that accepts the limits and returns a numeric vector of +#' length two. #' @rdname scale_discrete #' @family position scales #' @seealso @@ -69,7 +75,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 +85,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 +102,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 +116,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) diff --git a/R/scale-expansion.R b/R/scale-expansion.R index a132f5cd22..9c682eeaa6 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 @@ -153,7 +151,8 @@ expand_limits_scale <- function(scale, expand = expansion(0, 0), limits = waiver scale$map(limits), expand, coord_limits, - range_continuous = 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 @@ -170,7 +169,20 @@ 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 <- ifelse(missing, range(limits), continuous_limits) + } + limit_info <- expand_limits_discrete_trans( limits, expand, diff --git a/man/scale_discrete.Rd b/man/scale_discrete.Rd index 0bab3ad985..19e1df99fb 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,15 @@ 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. +Use \code{NA} to refer to the existing minimum or maximum. +\item A function that accepts the 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 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", {