Skip to content

Commit 7f6ca67

Browse files
authored
Discrete secondary axes (#5620)
* set secondary axes * add missing `sec_name` method * only allow identity transforms * map discrete breaks * breaks plumbing * default to identity transform * document `sec.axis` argument * only test when non-waiver * Don't derive discrete breaks from transformation * add test * add news bullet
1 parent 9af5d81 commit 7f6ca67

File tree

6 files changed

+69
-14
lines changed

6 files changed

+69
-14
lines changed

NEWS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
(@teunbrand, #5856).
2323
* New helper function `ggpar()` to translate ggplot2's interpretation of
2424
graphical parameters to {grid}'s interpretation (@teunbrand, #5866).
25+
* `scale_{x/y}_discrete()` can now accept a `sec.axis`. It is recommended to
26+
only use `dup_axis()` to set custom breaks or labels, as discrete variables
27+
cannot be transformed (@teunbrand, #3171).
2528

2629
# ggplot2 3.5.1
2730

R/axis-secondary.R

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ sec_axis <- function(transform = NULL,
119119
#' @rdname sec_axis
120120
#'
121121
#' @export
122-
dup_axis <- function(transform = ~., name = derive(), breaks = derive(),
122+
dup_axis <- function(transform = identity, name = derive(), breaks = derive(),
123123
labels = derive(), guide = derive(), trans = deprecated()) {
124124
sec_axis(transform, trans = trans, name, breaks, labels, guide)
125125
}
@@ -130,6 +130,11 @@ is.sec_axis <- function(x) {
130130

131131
set_sec_axis <- function(sec.axis, scale) {
132132
if (!is.waive(sec.axis)) {
133+
if (scale$is_discrete()) {
134+
if (!identical(.subset2(sec.axis, "trans"), identity)) {
135+
cli::cli_abort("Discrete secondary axes must have the {.fn identity} transformation.")
136+
}
137+
}
133138
if (is.formula(sec.axis)) sec.axis <- sec_axis(sec.axis)
134139
if (!is.sec_axis(sec.axis)) {
135140
cli::cli_abort("Secondary axes must be specified using {.fn sec_axis}.")
@@ -179,7 +184,13 @@ AxisSecondary <- ggproto("AxisSecondary", NULL,
179184
}
180185
if (is.derived(self$name) && !is.waive(scale$name)) self$name <- scale$name
181186
if (is.derived(self$breaks)) self$breaks <- scale$breaks
182-
if (is.waive(self$breaks)) self$breaks <- scale$get_transformation()$breaks
187+
if (is.waive(self$breaks)) {
188+
if (scale$is_discrete()) {
189+
self$breaks <- scale$get_breaks()
190+
} else {
191+
self$breaks <- scale$get_transformation()$breaks
192+
}
193+
}
183194
if (is.derived(self$labels)) self$labels <- scale$labels
184195
if (is.derived(self$guide)) self$guide <- scale$guide
185196
},
@@ -214,10 +225,15 @@ AxisSecondary <- ggproto("AxisSecondary", NULL,
214225
if (self$empty()) return()
215226

216227
# Test for monotonicity on unexpanded range
217-
self$mono_test(scale)
228+
if (!scale$is_discrete()) {
229+
self$mono_test(scale)
230+
breaks <- self$breaks
231+
} else {
232+
breaks <- scale$map(self$breaks)
233+
}
218234

219235
# Get scale's original range before transformation
220-
transformation <- scale$get_transformation()
236+
transformation <- scale$get_transformation() %||% transform_identity()
221237
along_range <- seq(range[1], range[2], length.out = self$detail)
222238
old_range <- transformation$inverse(along_range)
223239

@@ -245,7 +261,7 @@ AxisSecondary <- ggproto("AxisSecondary", NULL,
245261
old_val_trans <- rescale(range_info$major, from = c(0, 1), to = range)
246262
old_val_minor_trans <- rescale(range_info$minor, from = c(0, 1), to = range)
247263
} else {
248-
temp_scale <- self$create_scale(new_range)
264+
temp_scale <- self$create_scale(new_range, breaks = breaks)
249265
range_info <- temp_scale$break_info()
250266

251267
# Map the break values back to their correct position on the primary scale
@@ -294,10 +310,11 @@ AxisSecondary <- ggproto("AxisSecondary", NULL,
294310
},
295311

296312
# Temporary scale for the purpose of calling break_info()
297-
create_scale = function(self, range, transformation = transform_identity()) {
313+
create_scale = function(self, range, transformation = transform_identity(),
314+
breaks = self$breaks) {
298315
scale <- ggproto(NULL, ScaleContinuousPosition,
299316
name = self$name,
300-
breaks = self$breaks,
317+
breaks = breaks,
301318
labels = self$labels,
302319
limits = range,
303320
expand = c(0, 0),

R/scale-discrete-.R

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#'
1313
#' @inheritDotParams discrete_scale -scale_name
1414
#' @inheritParams discrete_scale
15+
#' @param sec.axis [dup_axis()] is used to specify a secondary axis.
1516
#' @rdname scale_discrete
1617
#' @family position scales
1718
#' @seealso
@@ -64,7 +65,8 @@
6465
#' scale_x_discrete(labels = abbreviate)
6566
#' }
6667
scale_x_discrete <- function(name = waiver(), ..., expand = waiver(),
67-
guide = waiver(), position = "bottom") {
68+
guide = waiver(), position = "bottom",
69+
sec.axis = waiver()) {
6870
sc <- discrete_scale(
6971
aesthetics = c("x", "xmin", "xmax", "xend"), name = name,
7072
palette = identity, ...,
@@ -73,12 +75,13 @@ scale_x_discrete <- function(name = waiver(), ..., expand = waiver(),
7375
)
7476

7577
sc$range_c <- ContinuousRange$new()
76-
sc
78+
set_sec_axis(sec.axis, sc)
7779
}
7880
#' @rdname scale_discrete
7981
#' @export
8082
scale_y_discrete <- function(name = waiver(), ..., expand = waiver(),
81-
guide = waiver(), position = "left") {
83+
guide = waiver(), position = "left",
84+
sec.axis = waiver()) {
8285
sc <- discrete_scale(
8386
aesthetics = c("y", "ymin", "ymax", "yend"), name = name,
8487
palette = identity, ...,
@@ -87,7 +90,7 @@ scale_y_discrete <- function(name = waiver(), ..., expand = waiver(),
8790
)
8891

8992
sc$range_c <- ContinuousRange$new()
90-
sc
93+
set_sec_axis(sec.axis, sc)
9194
}
9295

9396
# The discrete position scale maintains two separate ranges - one for
@@ -147,6 +150,14 @@ ScaleDiscretePosition <- ggproto("ScaleDiscretePosition", ScaleDiscrete,
147150
expand_limits_scale(self, expand, limits)
148151
},
149152

153+
sec_name = function(self) {
154+
if (is.waive(self$secondary.axis)) {
155+
waiver()
156+
} else {
157+
self$secondary.axis$name
158+
}
159+
},
160+
150161
clone = function(self) {
151162
new <- ggproto(NULL, self)
152163
new$range <- DiscreteRange$new()

man/scale_discrete.Rd

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

man/sec_axis.Rd

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-sec-axis.R

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,3 +380,23 @@ test_that("sec_axis() works for power transformations (monotonicity test doesn't
380380
breaks <- scale$break_info()
381381
expect_equal(breaks$major, breaks$sec.major, tolerance = .001)
382382
})
383+
384+
test_that("discrete scales can have secondary axes", {
385+
386+
data <- data.frame(x = c("A", "B", "C"), y = c("D", "E", "F"))
387+
p <- ggplot(data, aes(x, y)) +
388+
geom_point() +
389+
scale_x_discrete(sec.axis = dup_axis(labels = c("foo", "bar", "baz"))) +
390+
scale_y_discrete(sec.axis = dup_axis(
391+
breaks = c(1.5, 2.5), labels = c("grault", "garply")
392+
))
393+
b <- ggplot_build(p)
394+
395+
x <- get_guide_data(b, "x.sec")
396+
expect_equal(x$.value, 1:3, ignore_attr = TRUE)
397+
expect_equal(x$.label, c("foo", "bar", "baz"))
398+
399+
y <- get_guide_data(b, "y.sec")
400+
expect_equal(y$.value, c(1.5, 2.5), ignore_attr = TRUE)
401+
expect_equal(y$.label, c("grault", "garply"))
402+
})

0 commit comments

Comments
 (0)