Skip to content

Commit 7a006da

Browse files
authored
Palettes for discrete scales (#5771)
* Discrete position scales use palette * Plumbing for `palette` argument * discrete range derived from mapped limits * adjust test * add tests * palettes take `n` just like other scales * reoxygenate * rephrase palette arg * add news bullet
1 parent ae02c68 commit 7a006da

7 files changed

+89
-22
lines changed

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# ggplot2 (development version)
22

3+
* Discrete position scales now expose the `palette` argument, which can be used
4+
to customise spacings between levels (@teunbrand, #5770).
35
* The default `se` parameter in layers with `geom = "smooth"` will be `TRUE`
46
when the data has `ymin` and `ymax` parameters and `FALSE` if these are
57
absent. Note that this does not affect the default of `geom_smooth()` or

R/coord-transform.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ view_scales_from_scale_with_coord_trans <- function(scale, coord_limits, trans,
198198

199199
if (scale$is_discrete()) {
200200
continuous_ranges <- expand_limits_discrete_trans(
201-
scale_limits,
201+
scale$map(scale_limits),
202202
expansion,
203203
coord_limits,
204204
trans,

R/scale-discrete-.R

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
#'
1313
#' @inheritDotParams discrete_scale -scale_name
1414
#' @inheritParams discrete_scale
15+
#' @param palette A palette function that when called with a single integer
16+
#' argument (the number of levels in the scale) returns the numerical values
17+
#' that they should take.
1518
#' @param sec.axis [dup_axis()] is used to specify a secondary axis.
1619
#' @rdname scale_discrete
1720
#' @family position scales
@@ -64,12 +67,12 @@
6467
#' geom_point() +
6568
#' scale_x_discrete(labels = abbreviate)
6669
#' }
67-
scale_x_discrete <- function(name = waiver(), ..., expand = waiver(),
68-
guide = waiver(), position = "bottom",
69-
sec.axis = waiver()) {
70+
scale_x_discrete <- function(name = waiver(), ..., palette = seq_len,
71+
expand = waiver(), guide = waiver(),
72+
position = "bottom", sec.axis = waiver()) {
7073
sc <- discrete_scale(
7174
aesthetics = c("x", "xmin", "xmax", "xend"), name = name,
72-
palette = identity, ...,
75+
palette = palette, ...,
7376
expand = expand, guide = guide, position = position,
7477
super = ScaleDiscretePosition
7578
)
@@ -79,12 +82,12 @@ scale_x_discrete <- function(name = waiver(), ..., expand = waiver(),
7982
}
8083
#' @rdname scale_discrete
8184
#' @export
82-
scale_y_discrete <- function(name = waiver(), ..., expand = waiver(),
83-
guide = waiver(), position = "left",
84-
sec.axis = waiver()) {
85+
scale_y_discrete <- function(name = waiver(), ..., palette = seq_len,
86+
expand = waiver(), guide = waiver(),
87+
position = "left", sec.axis = waiver()) {
8588
sc <- discrete_scale(
8689
aesthetics = c("y", "ymin", "ymax", "yend"), name = name,
87-
palette = identity, ...,
90+
palette = palette, ...,
8891
expand = expand, guide = guide, position = position,
8992
super = ScaleDiscretePosition
9093
)
@@ -137,7 +140,21 @@ ScaleDiscretePosition <- ggproto("ScaleDiscretePosition", ScaleDiscrete,
137140

138141
map = function(self, x, limits = self$get_limits()) {
139142
if (is.discrete(x)) {
140-
x <- seq_along(limits)[match(as.character(x), limits)]
143+
values <- self$palette(length(limits))
144+
if (!is.numeric(values)) {
145+
cli::cli_abort(
146+
"The {.arg palette} function must return a {.cls numeric} vector.",
147+
call = self$call
148+
)
149+
}
150+
if (length(values) < length(limits)) {
151+
cli::cli_abort(
152+
"The {.arg palette} function must return at least \\
153+
{length(limits)} values.",
154+
call = self$call
155+
)
156+
}
157+
x <- values[match(as.character(x), limits)]
141158
}
142159
mapped_discrete(x)
143160
},

R/scale-expansion.R

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ expand_limits_scale <- function(scale, expand = expansion(0, 0), limits = waiver
137137
if (scale$is_discrete()) {
138138
coord_limits <- coord_limits %||% c(NA_real_, NA_real_)
139139
expand_limits_discrete(
140-
limits,
140+
scale$map(limits),
141141
expand,
142142
coord_limits,
143143
range_continuous = scale$range_c$range
@@ -201,25 +201,28 @@ expand_limits_continuous_trans <- function(limits, expand = expansion(0, 0),
201201
expand_limits_discrete_trans <- function(limits, expand = expansion(0, 0),
202202
coord_limits = c(NA, NA), trans = transform_identity(),
203203
range_continuous = NULL) {
204-
if (is.discrete(limits)) {
205-
n_discrete_limits <- length(limits)
206-
} else {
207-
n_discrete_limits <- 0
204+
discrete_limits <- NULL
205+
if (length(limits) > 0) {
206+
if (is.discrete(limits)) {
207+
discrete_limits <- c(1, length(limits)) # for backward compatibility
208+
} else {
209+
discrete_limits <- range(limits)
210+
}
208211
}
209212

210213
is_empty <- is.null(limits) && is.null(range_continuous)
211-
is_only_continuous <- n_discrete_limits == 0
214+
is_only_continuous <- is.null(discrete_limits)
212215
is_only_discrete <- is.null(range_continuous)
213216

214217
if (is_empty) {
215218
expand_limits_continuous_trans(c(0, 1), expand, coord_limits, trans)
216219
} else if (is_only_continuous) {
217220
expand_limits_continuous_trans(range_continuous, expand, coord_limits, trans)
218221
} else if (is_only_discrete) {
219-
expand_limits_continuous_trans(c(1, n_discrete_limits), expand, coord_limits, trans)
222+
expand_limits_continuous_trans(discrete_limits, expand, coord_limits, trans)
220223
} else {
221224
# continuous and discrete
222-
limit_info_discrete <- expand_limits_continuous_trans(c(1, n_discrete_limits), expand, coord_limits, trans)
225+
limit_info_discrete <- expand_limits_continuous_trans(discrete_limits, expand, coord_limits, trans)
223226

224227
# don't expand continuous range if there is also a discrete range
225228
limit_info_continuous <- expand_limits_continuous_trans(

man/scale_discrete.Rd

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

tests/testthat/test-scale-discrete.R

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,45 @@ test_that("mapped_discrete vectors behaves as predicted", {
162162
x[5:7] <- mapped_discrete(seq_len(3))
163163
expect_s3_class(x, "mapped_discrete")
164164
})
165+
166+
# Palettes ----------------------------------------------------------------
167+
168+
test_that("palettes work for discrete scales", {
169+
170+
df <- data.frame(x = c("A", "B", "C"), y = 1:3)
171+
values <- c(1, 10, 100)
172+
173+
p <- ggplot(df, aes(x, y)) +
174+
geom_point() +
175+
scale_x_discrete(palette = function(x) values)
176+
177+
# Check limits are translated to correct values
178+
ld <- layer_data(p)
179+
expect_equal(ld$x, values, ignore_attr = TRUE)
180+
181+
# Check discsrete expansion is applied
182+
b <- ggplot_build(p)
183+
expect_equal(
184+
b$layout$panel_params[[1]]$x.range,
185+
range(values) + c(-0.6, 0.6)
186+
)
187+
})
188+
189+
test_that("invalid palettes trigger errors", {
190+
191+
df <- data.frame(x = c("A", "B", "C"), y = 1:3)
192+
193+
p <- ggplot(df, aes(x, y)) +
194+
geom_point()
195+
196+
expect_error(
197+
ggplot_build(p + scale_x_discrete(palette = function(x) LETTERS[1:3])),
198+
"must return a .+ vector\\."
199+
)
200+
201+
expect_error(
202+
ggplot_build(p + scale_x_discrete(palette = function(x) 1:2)),
203+
"must return at least 3 values"
204+
)
205+
})
206+

tests/testthat/test-scale-expansion.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,6 @@ test_that("expand_limits_scale_discrete() begrudgingly handles numeric limits",
116116
coord_limits = c(NA, NA),
117117
range_continuous = c(-15, -2)
118118
),
119-
c(-15, -2)
119+
c(-16, -1)
120120
)
121121
})

0 commit comments

Comments
 (0)