Skip to content

Commit d6f5bf4

Browse files
authored
Fix labels/breaks/limits interactions in bin guides (tidyverse#4849)
1 parent e0d54f6 commit d6f5bf4

16 files changed

+2601
-25
lines changed

NEWS.md

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

3+
* Fix various issues with how `labels`, `breaks`, `limits`, and `show.limits`
4+
interact in the different binning guides (@thomasp85, #4831)
5+
36
* Automatic break calculation now squishes the scale limits to the domain
47
of the transformation. This allows `scale_{x/y}_sqrt()` to find breaks at 0
58
when appropriate (@teunbrand, #980).

R/guide-bins.R

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
#' @param axis.arrow A call to `arrow()` to specify arrows at the end of the
1515
#' axis line, thus showing an open interval.
1616
#' @param show.limits Logical. Should the limits of the scale be shown with
17-
#' labels and ticks.
17+
#' labels and ticks. Default is `NULL` meaning it will take the value from the
18+
#' scale. This argument is ignored if `labels` is given as a vector of
19+
#' values. If one or both of the limits is also given in `breaks` it will be
20+
#' shown irrespective of the value of `show.limits`.
1821
#'
1922
#' @section Use with discrete scale:
2023
#' This guide is intended to show binned data and work together with ggplot2's
@@ -137,15 +140,25 @@ guide_train.bins <- function(guide, scale, aesthetic = NULL) {
137140
if (length(breaks) == 0 || all(is.na(breaks))) {
138141
return()
139142
}
143+
show_limits <- guide$show.limits %||% scale$show.limits %||% FALSE
144+
if (show_limits && (is.character(scale$labels) || is.numeric(scale$labels))) {
145+
cli::cli_warn(c(
146+
"{.arg show.limits} is ignored when {.arg labels} are given as a character vector",
147+
"i" = "Either add the limits to {.arg breaks} or provide a function for {.arg labels}"
148+
))
149+
show_limits <- FALSE
150+
}
140151
# in the key data frame, use either the aesthetic provided as
141152
# argument to this function or, as a fall back, the first in the vector
142153
# of possible aesthetics handled by the scale
143154
aes_column_name <- aesthetic %||% scale$aesthetics[1]
144155

145156
if (is.numeric(breaks)) {
146157
limits <- scale$get_limits()
147-
breaks <- breaks[!breaks %in% limits]
148-
all_breaks <- c(limits[1], breaks, limits[2])
158+
if (!is.numeric(scale$breaks)) {
159+
breaks <- breaks[!breaks %in% limits]
160+
}
161+
all_breaks <- unique(c(limits[1], breaks, limits[2]))
149162
bin_at <- all_breaks[-1] - diff(all_breaks) / 2
150163
} else {
151164
# If the breaks are not numeric it is used with a discrete scale. We check
@@ -162,10 +175,29 @@ guide_train.bins <- function(guide, scale, aesthetic = NULL) {
162175
))
163176
}
164177
all_breaks <- breaks[c(1, seq_along(bin_at) * 2)]
178+
limits <- all_breaks[c(1, length(all_breaks))]
179+
breaks <- all_breaks[-c(1, length(all_breaks))]
165180
}
166181
key <- new_data_frame(setNames(list(c(scale$map(bin_at), NA)), aes_column_name))
167-
key$.label <- scale$get_labels(all_breaks)
168-
guide$show.limits <- guide$show.limits %||% scale$show_limits %||% FALSE
182+
labels <- scale$get_labels(breaks)
183+
show_limits <- rep(show_limits, 2)
184+
if (is.character(scale$labels) || is.numeric(scale$labels)) {
185+
limit_lab <- c(NA, NA)
186+
} else {
187+
limit_lab <- scale$get_labels(limits)
188+
}
189+
if (!breaks[1] %in% limits) {
190+
labels <- c(limit_lab[1], labels)
191+
} else {
192+
show_limits[1] <- TRUE
193+
}
194+
if (!breaks[length(breaks)] %in% limits) {
195+
labels <- c(labels, limit_lab[2])
196+
} else {
197+
show_limits[2] <- TRUE
198+
}
199+
key$.label <- labels
200+
guide$show.limits <- show_limits
169201

170202
if (guide$reverse) {
171203
key <- key[rev(seq_len(nrow(key))), ]
@@ -245,9 +277,7 @@ guide_geom.bins <- function(guide, layers, default_mapping) {
245277

246278
#' @export
247279
guide_gengrob.bins <- function(guide, theme) {
248-
if (!guide$show.limits) {
249-
guide$key$.label[c(1, nrow(guide$key))] <- NA
250-
}
280+
guide$key$.label[c(1, nrow(guide$key))[!guide$show.limits]] <- NA
251281

252282
# default setting
253283
if (guide$direction == "horizontal") {
@@ -332,9 +362,7 @@ guide_gengrob.bins <- function(guide, theme) {
332362
)
333363
ggname("guide.label", g)
334364
})
335-
if (!guide$show.limits) {
336-
grob.labels[c(1, length(grob.labels))] <- list(zeroGrob())
337-
}
365+
grob.labels[c(1, length(grob.labels))[!guide$show.limits]] <- list(zeroGrob())
338366
}
339367

340368
label_widths <- width_cm(grob.labels)
@@ -514,9 +542,8 @@ guide_gengrob.bins <- function(guide, theme) {
514542
)
515543
}
516544
grob.ticks <- rep_len(list(grob.ticks), length(grob.labels))
517-
if (!guide$show.limits) {
518-
grob.ticks[c(1, length(grob.ticks))] <- list(zeroGrob())
519-
}
545+
grob.ticks[c(1, length(grob.ticks))[!guide$show.limits]] <- list(zeroGrob())
546+
520547
# Create the gtable for the legend
521548
gt <- gtable(widths = unit(widths, "cm"), heights = unit(heights, "cm"))
522549
gt <- gtable_add_grob(

R/guide-colorsteps.R

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
#'
77
#' @param even.steps Should the rendered size of the bins be equal, or should
88
#' they be proportional to their length in the data space? Defaults to `TRUE`
9-
#' @param show.limits Should labels for the outer limits of the bins be printed?
10-
#' Default is `NULL` which makes the guide use the setting from the scale
9+
#' @param show.limits Logical. Should the limits of the scale be shown with
10+
#' labels and ticks. Default is `NULL` meaning it will take the value from the
11+
#' scale. This argument is ignored if `labels` is given as a vector of
12+
#' values. If one or both of the limits is also given in `breaks` it will be
13+
#' shown irrespective of the value of `show.limits`.
1114
#' @param ticks A logical specifying if tick marks on the colourbar should be
1215
#' visible.
1316
#' @inheritDotParams guide_colourbar -nbin -raster -ticks -available_aes
@@ -58,14 +61,24 @@ guide_colorsteps <- guide_coloursteps
5861
guide_train.colorsteps <- function(guide, scale, aesthetic = NULL) {
5962
breaks <- scale$get_breaks()
6063
breaks <- breaks[!is.na(breaks)]
64+
show_limits <- guide$show.limits %||% scale$show.limits %||% FALSE
65+
if (show_limits && (is.character(scale$labels) || is.numeric(scale$labels))) {
66+
cli::cli_warn(c(
67+
"{.arg show.limits} is ignored when {.arg labels} are given as a character vector",
68+
"i" = "Either add the limits to {.arg breaks} or provide a function for {.arg labels}"
69+
))
70+
show_limits <- FALSE
71+
}
6172
if (guide$even.steps || !is.numeric(breaks)) {
6273
if (length(breaks) == 0 || all(is.na(breaks))) {
6374
return()
6475
}
6576
if (is.numeric(breaks)) {
6677
limits <- scale$get_limits()
67-
breaks <- breaks[!breaks %in% limits]
68-
all_breaks <- c(limits[1], breaks, limits[2])
78+
if (!is.numeric(scale$breaks)) {
79+
breaks <- breaks[!breaks %in% limits]
80+
}
81+
all_breaks <- unique(c(limits[1], breaks, limits[2]))
6982
bin_at <- all_breaks[-1] - diff(all_breaks) / 2
7083
} else {
7184
# If the breaks are not numeric it is used with a discrete scale. We check
@@ -91,7 +104,16 @@ guide_train.colorsteps <- function(guide, scale, aesthetic = NULL) {
91104
ticks <- new_data_frame(setNames(list(scale$map(breaks)), aesthetic %||% scale$aesthetics[1]))
92105
ticks$.value <- seq_along(breaks) - 0.5
93106
ticks$.label <- scale$get_labels(breaks)
94-
guide$nbin <- length(breaks) + 1
107+
guide$nbin <- length(breaks) + 1L
108+
if (breaks[1] %in% limits) {
109+
ticks$.value <- ticks$.value - 1L
110+
ticks[[1]][1] <- NA
111+
guide$nbin <- guide$nbin - 1L
112+
}
113+
if (breaks[length(breaks)] %in% limits) {
114+
ticks[[1]][nrow(ticks)] <- NA
115+
guide$nbin <- guide$nbin - 1L
116+
}
95117
guide$key <- ticks
96118
guide$bar <- new_data_frame(list(colour = scale$map(bin_at), value = seq_along(bin_at) - 1), n = length(bin_at))
97119

@@ -104,12 +126,18 @@ guide_train.colorsteps <- function(guide, scale, aesthetic = NULL) {
104126
guide <- NextMethod()
105127
limits <- scale$get_limits()
106128
}
107-
if (guide$show.limits %||% scale$show.limits %||% FALSE) {
129+
if (show_limits) {
108130
edges <- rescale(c(0, 1), to = guide$bar$value[c(1, nrow(guide$bar))], from = c(0.5, guide$nbin - 0.5) / guide$nbin)
109131
if (guide$reverse) edges <- rev(edges)
110132
guide$key <- guide$key[c(NA, seq_len(nrow(guide$key)), NA), , drop = FALSE]
111133
guide$key$.value[c(1, nrow(guide$key))] <- edges
112134
guide$key$.label[c(1, nrow(guide$key))] <- scale$get_labels(limits)
135+
if (guide$key$.value[1] == guide$key$.value[2]) {
136+
guide$key <- guide$key[-1,]
137+
}
138+
if (guide$key$.value[nrow(guide$key)-1] == guide$key$.value[nrow(guide$key)]) {
139+
guide$key <- guide$key[-nrow(guide$key),]
140+
}
113141
}
114142
guide
115143
}

man/guide_bins.Rd

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

man/guide_coloursteps.Rd

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

tests/testthat/_snaps/guides.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
# binning scales understand the different combinations of limits, breaks, labels, and show.limits
2+
3+
`show.limits` is ignored when `labels` are given as a character vector
4+
i Either add the limits to `breaks` or provide a function for `labels`
5+
6+
---
7+
8+
`show.limits` is ignored when `labels` are given as a character vector
9+
i Either add the limits to `breaks` or provide a function for `labels`
10+
111
# axis_label_element_overrides errors when angles are outside the range [0, 90]
212

313
Unrecognized `axis_position`: "test"

tests/testthat/_snaps/guides/guide-bins-can-show-limits.svg

Lines changed: 2 additions & 2 deletions
Loading

0 commit comments

Comments
 (0)