Skip to content

Commit a92042d

Browse files
econandrewthomasp85
authored andcommitted
Allow functional limits in continuous scales (#2334)
1 parent 8724c8e commit a92042d

File tree

9 files changed

+168
-22
lines changed

9 files changed

+168
-22
lines changed

NEWS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ core developer team.
5353

5454
* `x0` and `y0` are now recognized positional aesthetics so they will get scaled
5555
if used in extension geoms and stats (@thomasp85, #3168)
56+
57+
* Continuous scale limits now accept functions which accept the default
58+
limits and return adjusted limits. This makes it possible to write
59+
a function that e.g. ensures the limits are always a multiple of 100,
60+
regardless of the data (@econandrew, #2307).
5661

5762
## Minor improvements and bug fixes
5863

R/scale-.r

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
Scale <- ggproto("Scale", NULL,
1515

1616
call = NULL,
17-
1817
aesthetics = aes(),
1918
scale_name = NULL,
2019
palette = function() {
@@ -107,16 +106,20 @@ Scale <- ggproto("Scale", NULL,
107106
stop("Not implemented", call. = FALSE)
108107
},
109108

110-
# if scale contains a NULL, use the default scale range
109+
# if scale is a function, apply it to the default (inverted) scale range
110+
# if scale is NULL, use the default scale range
111111
# if scale contains a NA, use the default range for that axis, otherwise
112112
# use the user defined limit for that axis
113113
get_limits = function(self) {
114114
if (self$is_empty()) return(c(0, 1))
115115

116-
if (!is.null(self$limits)) {
117-
ifelse(!is.na(self$limits), self$limits, self$range$range)
118-
} else {
116+
if (is.null(self$limits)) {
119117
self$range$range
118+
} else if (is.function(self$limits)) {
119+
# if limits is a function, it expects to work in data space
120+
self$trans$transform(self$limits(self$trans$inverse(self$range$range)))
121+
} else {
122+
ifelse(is.na(self$limits), self$range$range, self$limits)
120123
}
121124
},
122125

@@ -526,8 +529,12 @@ ScaleDiscrete <- ggproto("ScaleDiscrete", Scale,
526529
#' - A character vector giving labels (must be same length as `breaks`)
527530
#' - A function that takes the breaks as input and returns labels
528531
#' as output
529-
#' @param limits A numeric vector of length two providing limits of the scale.
530-
#' Use `NA` to refer to the existing minimum or maximum.
532+
#' @param limits One of:
533+
#' - `NULL` to use the default scale range
534+
#' - A numeric vector of length two providing limits of the scale.
535+
#' Use `NA` to refer to the existing minimum or maximum
536+
#' - A function that accepts the existing (automatic) limits and returns
537+
#' new limits
531538
#' @param rescaler Used by diverging and n colour gradients
532539
#' (i.e. [scale_colour_gradient2()], [scale_colour_gradientn()]).
533540
#' A function used to scale the input values to the range \[0, 1].
@@ -538,7 +545,7 @@ ScaleDiscrete <- ggproto("ScaleDiscrete", Scale,
538545
#' @param trans Either the name of a transformation object, or the
539546
#' object itself. Built-in transformations include "asn", "atanh",
540547
#' "boxcox", "date", "exp", "hms", "identity", "log", "log10", "log1p", "log2",
541-
#' "logit", "modulus", "probability", "probit", "pseudo_log", "reciprocal",
548+
#' "logit", "modulus", "probability", "probit", "pseudo_log", "reciprocal",
542549
#' "reverse", "sqrt" and "time".
543550
#'
544551
#' A transformation object bundles together a transform, its inverse,
@@ -569,7 +576,7 @@ continuous_scale <- function(aesthetics, scale_name, palette, name = waiver(),
569576
}
570577

571578
trans <- as.trans(trans)
572-
if (!is.null(limits)) {
579+
if (!is.null(limits) && !is.function(limits)) {
573580
limits <- trans$transform(limits)
574581
}
575582

man/continuous_scale.Rd

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

man/scale_continuous.Rd

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

man/scale_date.Rd

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

man/scale_gradient.Rd

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

man/scale_size.Rd

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

tests/testthat/test-scales-breaks-labels.r

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,6 @@ test_that("scale_breaks with explicit NA options (deprecated)", {
202202
expect_error(scc$get_breaks())
203203
})
204204

205-
206205
test_that("breaks can be specified by names of labels", {
207206
labels <- setNames(LETTERS[1:4], letters[1:4])
208207

@@ -241,6 +240,12 @@ test_that("minor breaks are transformed by scales", {
241240
expect_equal(sc$get_breaks_minor(), c(0, 1, 2))
242241
})
243242

243+
test_that("continuous limits accepts functions", {
244+
p <- ggplot(mpg, aes(class, hwy)) +
245+
scale_y_continuous(limits = function(lims) (c(lims[1] - 10, lims[2] + 100)))
246+
247+
expect_equal(layer_scales(p)$y$get_limits(), c(range(mpg$hwy)[1] - 10, range(mpg$hwy)[2] + 100))
248+
})
244249

245250
# Visual tests ------------------------------------------------------------
246251

@@ -324,3 +329,18 @@ test_that("scale breaks can be removed", {
324329
ggplot(dat, aes(x = 1, y = y, colour = x)) + geom_point() + scale_colour_continuous(breaks = NULL)
325330
)
326331
})
332+
333+
test_that("functional limits work for continuous scales", {
334+
limiter <- function(by) {
335+
function(limits) {
336+
low <- floor(limits[1] / by) * by
337+
high <- ceiling(limits[2] / by) * by
338+
c(low, high)
339+
}
340+
}
341+
342+
expect_doppelganger(
343+
"functional limits",
344+
ggplot(mpg, aes(class)) + geom_bar(aes(fill = drv)) + scale_y_continuous(limits = limiter(50))
345+
)
346+
})

0 commit comments

Comments
 (0)