diff --git a/NEWS.md b/NEWS.md
index f0996c9bbb..5ecace8835 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,5 +1,8 @@
# ggplot2 (development version)
+* Fix various issues with how `labels`, `breaks`, `limits`, and `show.limits`
+ interact in the different binning guides (@thomasp85, #4831)
+
* Automatic break calculation now squishes the scale limits to the domain
of the transformation. This allows `scale_{x/y}_sqrt()` to find breaks at 0
when appropriate (@teunbrand, #980).
diff --git a/R/guide-bins.R b/R/guide-bins.R
index 6f700a29bb..edf5d4f6e5 100644
--- a/R/guide-bins.R
+++ b/R/guide-bins.R
@@ -14,7 +14,10 @@
#' @param axis.arrow A call to `arrow()` to specify arrows at the end of the
#' axis line, thus showing an open interval.
#' @param show.limits Logical. Should the limits of the scale be shown with
-#' labels and ticks.
+#' labels and ticks. Default is `NULL` meaning it will take the value from the
+#' scale. This argument is ignored if `labels` is given as a vector of
+#' values. If one or both of the limits is also given in `breaks` it will be
+#' shown irrespective of the value of `show.limits`.
#'
#' @section Use with discrete scale:
#' This guide is intended to show binned data and work together with ggplot2's
@@ -137,6 +140,14 @@ guide_train.bins <- function(guide, scale, aesthetic = NULL) {
if (length(breaks) == 0 || all(is.na(breaks))) {
return()
}
+ show_limits <- guide$show.limits %||% scale$show.limits %||% FALSE
+ if (show_limits && (is.character(scale$labels) || is.numeric(scale$labels))) {
+ cli::cli_warn(c(
+ "{.arg show.limits} is ignored when {.arg labels} are given as a character vector",
+ "i" = "Either add the limits to {.arg breaks} or provide a function for {.arg labels}"
+ ))
+ show_limits <- FALSE
+ }
# in the key data frame, use either the aesthetic provided as
# argument to this function or, as a fall back, the first in the vector
# of possible aesthetics handled by the scale
@@ -144,8 +155,10 @@ guide_train.bins <- function(guide, scale, aesthetic = NULL) {
if (is.numeric(breaks)) {
limits <- scale$get_limits()
- breaks <- breaks[!breaks %in% limits]
- all_breaks <- c(limits[1], breaks, limits[2])
+ if (!is.numeric(scale$breaks)) {
+ breaks <- breaks[!breaks %in% limits]
+ }
+ all_breaks <- unique(c(limits[1], breaks, limits[2]))
bin_at <- all_breaks[-1] - diff(all_breaks) / 2
} else {
# 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) {
))
}
all_breaks <- breaks[c(1, seq_along(bin_at) * 2)]
+ limits <- all_breaks[c(1, length(all_breaks))]
+ breaks <- all_breaks[-c(1, length(all_breaks))]
}
key <- new_data_frame(setNames(list(c(scale$map(bin_at), NA)), aes_column_name))
- key$.label <- scale$get_labels(all_breaks)
- guide$show.limits <- guide$show.limits %||% scale$show_limits %||% FALSE
+ labels <- scale$get_labels(breaks)
+ show_limits <- rep(show_limits, 2)
+ if (is.character(scale$labels) || is.numeric(scale$labels)) {
+ limit_lab <- c(NA, NA)
+ } else {
+ limit_lab <- scale$get_labels(limits)
+ }
+ if (!breaks[1] %in% limits) {
+ labels <- c(limit_lab[1], labels)
+ } else {
+ show_limits[1] <- TRUE
+ }
+ if (!breaks[length(breaks)] %in% limits) {
+ labels <- c(labels, limit_lab[2])
+ } else {
+ show_limits[2] <- TRUE
+ }
+ key$.label <- labels
+ guide$show.limits <- show_limits
if (guide$reverse) {
key <- key[rev(seq_len(nrow(key))), ]
@@ -245,9 +277,7 @@ guide_geom.bins <- function(guide, layers, default_mapping) {
#' @export
guide_gengrob.bins <- function(guide, theme) {
- if (!guide$show.limits) {
- guide$key$.label[c(1, nrow(guide$key))] <- NA
- }
+ guide$key$.label[c(1, nrow(guide$key))[!guide$show.limits]] <- NA
# default setting
if (guide$direction == "horizontal") {
@@ -332,9 +362,7 @@ guide_gengrob.bins <- function(guide, theme) {
)
ggname("guide.label", g)
})
- if (!guide$show.limits) {
- grob.labels[c(1, length(grob.labels))] <- list(zeroGrob())
- }
+ grob.labels[c(1, length(grob.labels))[!guide$show.limits]] <- list(zeroGrob())
}
label_widths <- width_cm(grob.labels)
@@ -514,9 +542,8 @@ guide_gengrob.bins <- function(guide, theme) {
)
}
grob.ticks <- rep_len(list(grob.ticks), length(grob.labels))
- if (!guide$show.limits) {
- grob.ticks[c(1, length(grob.ticks))] <- list(zeroGrob())
- }
+ grob.ticks[c(1, length(grob.ticks))[!guide$show.limits]] <- list(zeroGrob())
+
# Create the gtable for the legend
gt <- gtable(widths = unit(widths, "cm"), heights = unit(heights, "cm"))
gt <- gtable_add_grob(
diff --git a/R/guide-colorsteps.R b/R/guide-colorsteps.R
index 3222e5d1fa..f9096fb77b 100644
--- a/R/guide-colorsteps.R
+++ b/R/guide-colorsteps.R
@@ -6,8 +6,11 @@
#'
#' @param even.steps Should the rendered size of the bins be equal, or should
#' they be proportional to their length in the data space? Defaults to `TRUE`
-#' @param show.limits Should labels for the outer limits of the bins be printed?
-#' Default is `NULL` which makes the guide use the setting from the scale
+#' @param show.limits Logical. Should the limits of the scale be shown with
+#' labels and ticks. Default is `NULL` meaning it will take the value from the
+#' scale. This argument is ignored if `labels` is given as a vector of
+#' values. If one or both of the limits is also given in `breaks` it will be
+#' shown irrespective of the value of `show.limits`.
#' @param ticks A logical specifying if tick marks on the colourbar should be
#' visible.
#' @inheritDotParams guide_colourbar -nbin -raster -ticks -available_aes
@@ -58,14 +61,24 @@ guide_colorsteps <- guide_coloursteps
guide_train.colorsteps <- function(guide, scale, aesthetic = NULL) {
breaks <- scale$get_breaks()
breaks <- breaks[!is.na(breaks)]
+ show_limits <- guide$show.limits %||% scale$show.limits %||% FALSE
+ if (show_limits && (is.character(scale$labels) || is.numeric(scale$labels))) {
+ cli::cli_warn(c(
+ "{.arg show.limits} is ignored when {.arg labels} are given as a character vector",
+ "i" = "Either add the limits to {.arg breaks} or provide a function for {.arg labels}"
+ ))
+ show_limits <- FALSE
+ }
if (guide$even.steps || !is.numeric(breaks)) {
if (length(breaks) == 0 || all(is.na(breaks))) {
return()
}
if (is.numeric(breaks)) {
limits <- scale$get_limits()
- breaks <- breaks[!breaks %in% limits]
- all_breaks <- c(limits[1], breaks, limits[2])
+ if (!is.numeric(scale$breaks)) {
+ breaks <- breaks[!breaks %in% limits]
+ }
+ all_breaks <- unique(c(limits[1], breaks, limits[2]))
bin_at <- all_breaks[-1] - diff(all_breaks) / 2
} else {
# 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) {
ticks <- new_data_frame(setNames(list(scale$map(breaks)), aesthetic %||% scale$aesthetics[1]))
ticks$.value <- seq_along(breaks) - 0.5
ticks$.label <- scale$get_labels(breaks)
- guide$nbin <- length(breaks) + 1
+ guide$nbin <- length(breaks) + 1L
+ if (breaks[1] %in% limits) {
+ ticks$.value <- ticks$.value - 1L
+ ticks[[1]][1] <- NA
+ guide$nbin <- guide$nbin - 1L
+ }
+ if (breaks[length(breaks)] %in% limits) {
+ ticks[[1]][nrow(ticks)] <- NA
+ guide$nbin <- guide$nbin - 1L
+ }
guide$key <- ticks
guide$bar <- new_data_frame(list(colour = scale$map(bin_at), value = seq_along(bin_at) - 1), n = length(bin_at))
@@ -104,12 +126,18 @@ guide_train.colorsteps <- function(guide, scale, aesthetic = NULL) {
guide <- NextMethod()
limits <- scale$get_limits()
}
- if (guide$show.limits %||% scale$show.limits %||% FALSE) {
+ if (show_limits) {
edges <- rescale(c(0, 1), to = guide$bar$value[c(1, nrow(guide$bar))], from = c(0.5, guide$nbin - 0.5) / guide$nbin)
if (guide$reverse) edges <- rev(edges)
guide$key <- guide$key[c(NA, seq_len(nrow(guide$key)), NA), , drop = FALSE]
guide$key$.value[c(1, nrow(guide$key))] <- edges
guide$key$.label[c(1, nrow(guide$key))] <- scale$get_labels(limits)
+ if (guide$key$.value[1] == guide$key$.value[2]) {
+ guide$key <- guide$key[-1,]
+ }
+ if (guide$key$.value[nrow(guide$key)-1] == guide$key$.value[nrow(guide$key)]) {
+ guide$key <- guide$key[-nrow(guide$key),]
+ }
}
guide
}
diff --git a/man/guide_bins.Rd b/man/guide_bins.Rd
index 8cfac27945..80746cad25 100644
--- a/man/guide_bins.Rd
+++ b/man/guide_bins.Rd
@@ -101,7 +101,10 @@ multiple guides are displayed, not the contents of the guide itself.
If 0 (default), the order is determined by a secret algorithm.}
\item{show.limits}{Logical. Should the limits of the scale be shown with
-labels and ticks.}
+labels and ticks. Default is \code{NULL} meaning it will take the value from the
+scale. This argument is ignored if \code{labels} is given as a vector of
+values. If one or both of the limits is also given in \code{breaks} it will be
+shown irrespective of the value of \code{show.limits}.}
\item{...}{ignored.}
}
diff --git a/man/guide_coloursteps.Rd b/man/guide_coloursteps.Rd
index 7ab1f8ff36..f6e77e0e47 100644
--- a/man/guide_coloursteps.Rd
+++ b/man/guide_coloursteps.Rd
@@ -13,8 +13,11 @@ guide_colorsteps(even.steps = TRUE, show.limits = NULL, ticks = FALSE, ...)
\item{even.steps}{Should the rendered size of the bins be equal, or should
they be proportional to their length in the data space? Defaults to \code{TRUE}}
-\item{show.limits}{Should labels for the outer limits of the bins be printed?
-Default is \code{NULL} which makes the guide use the setting from the scale}
+\item{show.limits}{Logical. Should the limits of the scale be shown with
+labels and ticks. Default is \code{NULL} meaning it will take the value from the
+scale. This argument is ignored if \code{labels} is given as a vector of
+values. If one or both of the limits is also given in \code{breaks} it will be
+shown irrespective of the value of \code{show.limits}.}
\item{ticks}{A logical specifying if tick marks on the colourbar should be
visible.}
diff --git a/tests/testthat/_snaps/guides.md b/tests/testthat/_snaps/guides.md
index 9fd2d9f7bd..4b9f4b317b 100644
--- a/tests/testthat/_snaps/guides.md
+++ b/tests/testthat/_snaps/guides.md
@@ -1,3 +1,13 @@
+# binning scales understand the different combinations of limits, breaks, labels, and show.limits
+
+ `show.limits` is ignored when `labels` are given as a character vector
+ i Either add the limits to `breaks` or provide a function for `labels`
+
+---
+
+ `show.limits` is ignored when `labels` are given as a character vector
+ i Either add the limits to `breaks` or provide a function for `labels`
+
# axis_label_element_overrides errors when angles are outside the range [0, 90]
Unrecognized `axis_position`: "test"
diff --git a/tests/testthat/_snaps/guides/guide-bins-can-show-limits.svg b/tests/testthat/_snaps/guides/guide-bins-can-show-limits.svg
index 4b6e1addad..dee77908af 100644
--- a/tests/testthat/_snaps/guides/guide-bins-can-show-limits.svg
+++ b/tests/testthat/_snaps/guides/guide-bins-can-show-limits.svg
@@ -71,11 +71,11 @@
-1.0
+11.52.02.5
-3.0
+3guide_bins can show limits
diff --git a/tests/testthat/_snaps/guides/guide-bins-sets-labels-when-limits-is-in-breaks.svg b/tests/testthat/_snaps/guides/guide-bins-sets-labels-when-limits-is-in-breaks.svg
new file mode 100644
index 0000000000..e2d0a86b9c
--- /dev/null
+++ b/tests/testthat/_snaps/guides/guide-bins-sets-labels-when-limits-is-in-breaks.svg
@@ -0,0 +1,312 @@
+
+
diff --git a/tests/testthat/_snaps/guides/guide-bins-understands-coinciding-limits-and-bins-2.svg b/tests/testthat/_snaps/guides/guide-bins-understands-coinciding-limits-and-bins-2.svg
new file mode 100644
index 0000000000..4630e3449e
--- /dev/null
+++ b/tests/testthat/_snaps/guides/guide-bins-understands-coinciding-limits-and-bins-2.svg
@@ -0,0 +1,312 @@
+
+
diff --git a/tests/testthat/_snaps/guides/guide-bins-understands-coinciding-limits-and-bins-showing-limits.svg b/tests/testthat/_snaps/guides/guide-bins-understands-coinciding-limits-and-bins-showing-limits.svg
new file mode 100644
index 0000000000..2e66733d33
--- /dev/null
+++ b/tests/testthat/_snaps/guides/guide-bins-understands-coinciding-limits-and-bins-showing-limits.svg
@@ -0,0 +1,314 @@
+
+
diff --git a/tests/testthat/_snaps/guides/guide-bins-understands-coinciding-limits-and-bins.svg b/tests/testthat/_snaps/guides/guide-bins-understands-coinciding-limits-and-bins.svg
new file mode 100644
index 0000000000..82f71991a0
--- /dev/null
+++ b/tests/testthat/_snaps/guides/guide-bins-understands-coinciding-limits-and-bins.svg
@@ -0,0 +1,312 @@
+
+
diff --git a/tests/testthat/_snaps/guides/guide-colorsteps-sets-labels-when-limits-is-in-breaks.svg b/tests/testthat/_snaps/guides/guide-colorsteps-sets-labels-when-limits-is-in-breaks.svg
new file mode 100644
index 0000000000..b38018f911
--- /dev/null
+++ b/tests/testthat/_snaps/guides/guide-colorsteps-sets-labels-when-limits-is-in-breaks.svg
@@ -0,0 +1,301 @@
+
+
diff --git a/tests/testthat/_snaps/guides/guide-colorsteps-understands-coinciding-limits-and-bins-2.svg b/tests/testthat/_snaps/guides/guide-colorsteps-understands-coinciding-limits-and-bins-2.svg
new file mode 100644
index 0000000000..9427333e92
--- /dev/null
+++ b/tests/testthat/_snaps/guides/guide-colorsteps-understands-coinciding-limits-and-bins-2.svg
@@ -0,0 +1,301 @@
+
+
diff --git a/tests/testthat/_snaps/guides/guide-colorsteps-understands-coinciding-limits-and-bins-showing-limits.svg b/tests/testthat/_snaps/guides/guide-colorsteps-understands-coinciding-limits-and-bins-showing-limits.svg
new file mode 100644
index 0000000000..528e875db3
--- /dev/null
+++ b/tests/testthat/_snaps/guides/guide-colorsteps-understands-coinciding-limits-and-bins-showing-limits.svg
@@ -0,0 +1,302 @@
+
+
diff --git a/tests/testthat/_snaps/guides/guide-colorsteps-understands-coinciding-limits-and-bins.svg b/tests/testthat/_snaps/guides/guide-colorsteps-understands-coinciding-limits-and-bins.svg
new file mode 100644
index 0000000000..e4e230d71a
--- /dev/null
+++ b/tests/testthat/_snaps/guides/guide-colorsteps-understands-coinciding-limits-and-bins.svg
@@ -0,0 +1,301 @@
+
+
diff --git a/tests/testthat/test-guides.R b/tests/testthat/test-guides.R
index 5f0fcf7ff5..bef1042caa 100644
--- a/tests/testthat/test-guides.R
+++ b/tests/testthat/test-guides.R
@@ -594,6 +594,53 @@ test_that("coloursteps guide can be styled correctly", {
)
})
+test_that("binning scales understand the different combinations of limits, breaks, labels, and show.limits", {
+ p <- ggplot(mpg, aes(cty, hwy, color = year)) +
+ geom_point()
+
+ expect_doppelganger("guide_bins understands coinciding limits and bins",
+ p + scale_color_binned(limits = c(1999, 2008),
+ breaks = c(1999, 2000, 2002, 2004, 2006),
+ guide = 'bins')
+ )
+ expect_doppelganger("guide_bins understands coinciding limits and bins 2",
+ p + scale_color_binned(limits = c(1999, 2008),
+ breaks = c(2000, 2002, 2004, 2006, 2008),
+ guide = 'bins')
+ )
+ expect_doppelganger("guide_bins understands coinciding limits and bins showing limits",
+ p + scale_color_binned(limits = c(1999, 2008),
+ breaks = c(1999, 2000, 2002, 2004, 2006),
+ guide = 'bins', show.limits = TRUE)
+ )
+ expect_doppelganger("guide_bins sets labels when limits is in breaks",
+ p + scale_color_binned(limits = c(1999, 2008),
+ breaks = c(1999, 2000, 2002, 2004, 2006),
+ labels = 1:5, guide = 'bins')
+ )
+ expect_snapshot_warning(ggplotGrob(p + scale_color_binned(labels = 1:4, show.limits = TRUE, guide = "bins")))
+
+ expect_doppelganger("guide_colorsteps understands coinciding limits and bins",
+ p + scale_color_binned(limits = c(1999, 2008),
+ breaks = c(1999, 2000, 2002, 2004, 2006))
+ )
+ expect_doppelganger("guide_colorsteps understands coinciding limits and bins 2",
+ p + scale_color_binned(limits = c(1999, 2008),
+ breaks = c(2000, 2002, 2004, 2006, 2008))
+ )
+ expect_doppelganger("guide_colorsteps understands coinciding limits and bins showing limits",
+ p + scale_color_binned(limits = c(1999, 2008),
+ breaks = c(1999, 2000, 2002, 2004, 2006),
+ show.limits = TRUE)
+ )
+ expect_doppelganger("guide_colorsteps sets labels when limits is in breaks",
+ p + scale_color_binned(limits = c(1999, 2008),
+ breaks = c(1999, 2000, 2002, 2004, 2006),
+ labels = 1:5)
+ )
+ expect_snapshot_warning(ggplotGrob(p + scale_color_binned(labels = 1:4, show.limits = TRUE)))
+})
+
test_that("a warning is generated when guides( = FALSE) is specified", {
df <- data_frame(x = c(1, 2, 4),
y = c(6, 5, 7))