Skip to content

Commit 47d65dd

Browse files
dpseidelthomasp85
authored andcommitted
Fix tick misalignment and revert transformation inheritance in sec.axis (#2978) (#3040)
1 parent b871d57 commit 47d65dd

12 files changed

+1107
-80
lines changed

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ core developer team.
107107

108108
* `stat_bin()` will now error when the number of bins exceeds 1e6 to avoid
109109
accidentally freezing the user session (@thomasp85).
110+
111+
* `sec_axis()` now places ticks accurately when using nonlinear transformations (@dpseidel, #2978).
110112

111113
* `facet_wrap()` and `facet_grid()` now automatically remove NULL from facet
112114
specs, and accept empty specs (@yutannihilation, #3070, #2986).

R/axis-secondary.R

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@
2929
#' `dup_axis` is provide as a shorthand for creating a secondary axis that
3030
#' is a duplication of the primary axis, effectively mirroring the primary axis.
3131
#'
32+
#' As of v3.1, date and datetime scales have limited secondary axis capabilities.
33+
#' Unlike other continuous scales, secondary axis transformations for date and datetime scales
34+
#' must respect their primary POSIX data structure.
35+
#' This means they may only be transformed via addition or subtraction, e.g.
36+
#' `~. + hms::hms(days = 8)`, or
37+
#' `~.- 8*60*60`. Nonlinear transformations will return an error.
38+
#' To produce a time-since-event secondary axis in this context, users
39+
#' may consider adapting secondary axis labels.
40+
#'
3241
#' @examples
3342
#' p <- ggplot(mtcars, aes(cyl, mpg)) +
3443
#' geom_point()
@@ -56,7 +65,7 @@
5665
#' price = seq(20, 200000, length.out = 10)
5766
#' )
5867
#'
59-
#' # useful for labelling different time scales in the same plot
68+
#' # This may useful for labelling different time scales in the same plot
6069
#' ggplot(df, aes(x = dx, y = price)) + geom_line() +
6170
#' scale_x_datetime("Date", date_labels = "%b %d",
6271
#' date_breaks = "6 hour",
@@ -136,6 +145,7 @@ AxisSecondary <- ggproto("AxisSecondary", NULL,
136145
if (!is.formula(self$trans)) stop("transformation for secondary axes must be a formula", call. = FALSE)
137146
if (is.derived(self$name) && !is.waive(scale$name)) self$name <- scale$name
138147
if (is.derived(self$breaks)) self$breaks <- scale$breaks
148+
if (is.waive(self$breaks)) self$breaks <- scale$trans$breaks
139149
if (is.derived(self$labels)) self$labels <- scale$labels
140150
},
141151

@@ -148,37 +158,66 @@ AxisSecondary <- ggproto("AxisSecondary", NULL,
148158
)
149159
},
150160

151-
break_info = function(self, range, scale) {
152-
if (self$empty()) return()
153-
154-
# Get original range before transformation
155-
inv_range <- scale$trans$inverse(range)
161+
mono_test = function(self, scale){
162+
range <- scale$range$range
163+
along_range <- seq(range[1], range[2], length.out = self$detail)
164+
old_range <- scale$trans$inverse(along_range)
156165

157166
# Create mapping between primary and secondary range
158-
old_range <- seq(inv_range[1], inv_range[2], length.out = self$detail)
159167
full_range <- self$transform_range(old_range)
160168

161169
# Test for monotonicity
162170
if (length(unique(sign(diff(full_range)))) != 1)
163171
stop("transformation for secondary axes must be monotonic")
172+
},
173+
174+
break_info = function(self, range, scale) {
175+
if (self$empty()) return()
176+
177+
# Test for monotonicity on unexpanded range
178+
self$mono_test(scale)
179+
180+
# Get scale's original range before transformation
181+
along_range <- seq(range[1], range[2], length.out = self$detail)
182+
old_range <- scale$trans$inverse(along_range)
183+
184+
# Create mapping between primary and secondary range
185+
full_range <- self$transform_range(old_range)
164186

165187
# Get break info for the secondary axis
166-
new_range <- range(scale$transform(full_range), na.rm = TRUE)
167-
sec_scale <- self$create_scale(new_range, scale)
168-
range_info <- sec_scale$break_info()
188+
new_range <- range(full_range, na.rm = TRUE)
189+
190+
# patch for date and datetime scales just to maintain functionality
191+
# works only for linear secondary transforms that respect the time or date transform
192+
if (scale$trans$name %in% c("date", "time")){
193+
temp_scale <- self$create_scale(new_range, trans = scale$trans)
194+
range_info <- temp_scale$break_info()
195+
names(range_info) <- paste0("sec.", names(range_info))
196+
return(range_info)
197+
}
198+
199+
temp_scale <- self$create_scale(new_range)
200+
range_info <- temp_scale$break_info()
201+
202+
# Map the break values back to their correct position on the primary scale
203+
old_val <- lapply(range_info$major_source, function(x) which.min(abs(full_range - x)))
204+
old_val <- old_range[unlist(old_val)]
205+
old_val_trans <- scale$trans$transform(old_val)
206+
range_info$major[] <- round(rescale(scale$map(old_val_trans, range(old_val_trans)), from = range), digits = 3)
207+
169208
names(range_info) <- paste0("sec.", names(range_info))
170209
range_info
171210
},
172211

173212
# Temporary scale for the purpose of calling break_info()
174-
create_scale = function(self, range, primary) {
213+
create_scale = function(self, range, trans = identity_trans()) {
175214
scale <- ggproto(NULL, ScaleContinuousPosition,
176-
name = self$name,
177-
breaks = self$breaks,
178-
labels = self$labels,
179-
limits = range,
180-
expand = c(0, 0),
181-
trans = primary$trans
215+
name = self$name,
216+
breaks = self$breaks,
217+
labels = self$labels,
218+
limits = range,
219+
expand = c(0, 0),
220+
trans = trans
182221
)
183222
scale$train(range)
184223
scale

man/sec_axis.Rd

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

tests/figs/sec-axis/sec-axis-custom-transform.svg

Lines changed: 104 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)