From f9618ac25facd47b9f5ac083985fbd9a894785bb Mon Sep 17 00:00:00 2001 From: Carl Suster Date: Fri, 1 Nov 2024 16:41:03 +1100 Subject: [PATCH 1/5] POC bin breaks derived from scale breaks --- R/scale-.R | 23 ++++++++++++++++++++++- R/stat-bin.R | 8 +++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/R/scale-.R b/R/scale-.R index d7c0f42252..4a3777d2be 100644 --- a/R/scale-.R +++ b/R/scale-.R @@ -657,6 +657,9 @@ ScaleContinuous <- ggproto("ScaleContinuous", Scale, minor_breaks = waiver(), n.breaks = NULL, trans = transform_identity(), + freeze_breaks = FALSE, + frozen_breaks = NULL, + frozen_minor_breaks = NULL, is_discrete = function() FALSE, @@ -749,10 +752,14 @@ ScaleContinuous <- ggproto("ScaleContinuous", Scale, ) } + breaks_are_frozen <- !is.null(self$frozen_breaks) + # Compute `zero_range()` in transformed space in case `limits` in data space # don't support conversion to numeric (#5304) if (zero_range(as.numeric(transformation$transform(limits)))) { breaks <- limits[1] + } else if (self$freeze_breaks && breaks_are_frozen) { + breaks <- self$frozen_breaks } else if (is.waive(self$breaks)) { if (!is.null(self$n.breaks) && trans_support_nbreaks(transformation)) { breaks <- transformation$breaks(limits, self$n.breaks) @@ -771,6 +778,10 @@ ScaleContinuous <- ggproto("ScaleContinuous", Scale, breaks <- self$breaks } + if (self$freeze_breaks && !breaks_are_frozen) { + self$frozen_breaks <- breaks + } + # Breaks in data space need to be converted back to transformed space transformation$transform(breaks) }, @@ -794,8 +805,12 @@ ScaleContinuous <- ggproto("ScaleContinuous", Scale, # some transforms assume finite major breaks b <- b[is.finite(b)] + breaks_are_frozen <- !is.null(self$frozen_minor_breaks) + transformation <- self$get_transformation() - if (is.waive(self$minor_breaks)) { + if (self$freeze_breaks && breaks_are_frozen) { + breaks <- self$frozen_minor_breaks + } else if (is.waive(self$minor_breaks)) { if (is.null(b)) { breaks <- NULL } else { @@ -819,6 +834,10 @@ ScaleContinuous <- ggproto("ScaleContinuous", Scale, breaks <- transformation$transform(self$minor_breaks) } + if (self$freeze_breaks && !breaks_are_frozen) { + self$frozen_minor_breaks <- breaks + } + # Any minor breaks outside the dimensions need to be thrown away discard(breaks, limits) }, @@ -875,6 +894,8 @@ ScaleContinuous <- ggproto("ScaleContinuous", Scale, clone = function(self) { new <- ggproto(NULL, self) new$range <- ContinuousRange$new() + new$frozen_breaks <- NULL + new$frozen_minor_breaks <- NULL new }, diff --git a/R/stat-bin.R b/R/stat-bin.R index 9c571ae519..b9dcfdb403 100644 --- a/R/stat-bin.R +++ b/R/stat-bin.R @@ -59,6 +59,7 @@ stat_bin <- function(mapping = NULL, data = NULL, closed = c("right", "left"), pad = FALSE, na.rm = FALSE, + follow.scale = FALSE, keep.zeroes = "all", orientation = NA, show.legend = NA, @@ -81,6 +82,7 @@ stat_bin <- function(mapping = NULL, data = NULL, closed = closed, pad = pad, na.rm = na.rm, + follow.scale = follow.scale, orientation = orientation, keep.zeroes = keep.zeroes, ... @@ -150,11 +152,15 @@ StatBin <- ggproto("StatBin", Stat, center = NULL, boundary = NULL, closed = c("right", "left"), pad = FALSE, breaks = NULL, flipped_aes = FALSE, keep.zeroes = "all", + follow.scale = FALSE, # The following arguments are not used, but must # be listed so parameters are computed correctly origin = NULL, right = NULL, drop = NULL) { x <- flipped_names(flipped_aes)$x - if (!is.null(breaks)) { + if (follow.scale) { + breaks <- scales[[x]]$get_breaks() + bins <- bin_breaks(breaks, closed) + } else if (!is.null(breaks)) { if (is.function(breaks)) { breaks <- breaks(data[[x]]) } From 93ff4562594ec59c4079f518dc1d6c07fc6aa60d Mon Sep 17 00:00:00 2001 From: Carl Suster Date: Mon, 16 Dec 2024 09:06:41 +1100 Subject: [PATCH 2/5] Revert break freezing functionality --- R/scale-.R | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/R/scale-.R b/R/scale-.R index 4a3777d2be..d7c0f42252 100644 --- a/R/scale-.R +++ b/R/scale-.R @@ -657,9 +657,6 @@ ScaleContinuous <- ggproto("ScaleContinuous", Scale, minor_breaks = waiver(), n.breaks = NULL, trans = transform_identity(), - freeze_breaks = FALSE, - frozen_breaks = NULL, - frozen_minor_breaks = NULL, is_discrete = function() FALSE, @@ -752,14 +749,10 @@ ScaleContinuous <- ggproto("ScaleContinuous", Scale, ) } - breaks_are_frozen <- !is.null(self$frozen_breaks) - # Compute `zero_range()` in transformed space in case `limits` in data space # don't support conversion to numeric (#5304) if (zero_range(as.numeric(transformation$transform(limits)))) { breaks <- limits[1] - } else if (self$freeze_breaks && breaks_are_frozen) { - breaks <- self$frozen_breaks } else if (is.waive(self$breaks)) { if (!is.null(self$n.breaks) && trans_support_nbreaks(transformation)) { breaks <- transformation$breaks(limits, self$n.breaks) @@ -778,10 +771,6 @@ ScaleContinuous <- ggproto("ScaleContinuous", Scale, breaks <- self$breaks } - if (self$freeze_breaks && !breaks_are_frozen) { - self$frozen_breaks <- breaks - } - # Breaks in data space need to be converted back to transformed space transformation$transform(breaks) }, @@ -805,12 +794,8 @@ ScaleContinuous <- ggproto("ScaleContinuous", Scale, # some transforms assume finite major breaks b <- b[is.finite(b)] - breaks_are_frozen <- !is.null(self$frozen_minor_breaks) - transformation <- self$get_transformation() - if (self$freeze_breaks && breaks_are_frozen) { - breaks <- self$frozen_minor_breaks - } else if (is.waive(self$minor_breaks)) { + if (is.waive(self$minor_breaks)) { if (is.null(b)) { breaks <- NULL } else { @@ -834,10 +819,6 @@ ScaleContinuous <- ggproto("ScaleContinuous", Scale, breaks <- transformation$transform(self$minor_breaks) } - if (self$freeze_breaks && !breaks_are_frozen) { - self$frozen_minor_breaks <- breaks - } - # Any minor breaks outside the dimensions need to be thrown away discard(breaks, limits) }, @@ -894,8 +875,6 @@ ScaleContinuous <- ggproto("ScaleContinuous", Scale, clone = function(self) { new <- ggproto(NULL, self) new$range <- ContinuousRange$new() - new$frozen_breaks <- NULL - new$frozen_minor_breaks <- NULL new }, From 0b601ee760d64f93d7729e99f011a2ef474b8a8f Mon Sep 17 00:00:00 2001 From: Carl Suster Date: Mon, 16 Dec 2024 09:19:26 +1100 Subject: [PATCH 3/5] Add breaks_cached() implementation --- DESCRIPTION | 1 + NAMESPACE | 3 +++ R/breaks_cached.R | 53 ++++++++++++++++++++++++++++++++++++++++++++ man/breaks_cached.Rd | 24 ++++++++++++++++++++ 4 files changed, 81 insertions(+) create mode 100644 R/breaks_cached.R create mode 100644 man/breaks_cached.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 77755980b7..3318cb4f81 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -109,6 +109,7 @@ Collate: 'backports.R' 'bench.R' 'bin.R' + 'breaks_cached.R' 'coord-.R' 'coord-cartesian-.R' 'coord-fixed.R' diff --git a/NAMESPACE b/NAMESPACE index 852cb97600..c4fdbdf884 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -22,6 +22,7 @@ S3method(element_grob,element_blank) S3method(element_grob,element_line) S3method(element_grob,element_rect) S3method(element_grob,element_text) +S3method(format,ggplot2_cached_breaks) S3method(format,ggproto) S3method(format,ggproto_method) S3method(fortify,"NULL") @@ -106,6 +107,7 @@ S3method(predictdf,loess) S3method(print,element) S3method(print,ggplot) S3method(print,ggplot2_bins) +S3method(print,ggplot2_cached_breaks) S3method(print,ggproto) S3method(print,ggproto_method) S3method(print,rel) @@ -298,6 +300,7 @@ export(autoplot) export(benchplot) export(binned_scale) export(borders) +export(breaks_cached) export(calc_element) export(check_device) export(combine_vars) diff --git a/R/breaks_cached.R b/R/breaks_cached.R new file mode 100644 index 0000000000..fddc8a8536 --- /dev/null +++ b/R/breaks_cached.R @@ -0,0 +1,53 @@ +#' Caching scale breaks +#' +#' This helper caches the output of another breaks function the first time it is +#' evaluated. All subsequent calls will return the same breaks vector +#' regardless of the provided limits. In general this is not what you want +#' since the breaks should change when the limits change. It is helpful in the +#' specific case that you are using `follow.scale` on `stat_bin()` and related +#' binning stats, because it ensures that the breaks are not recomputed after +#' they are used to define the bin edges. +#' +#' @param breaks A function that takes the limits as input and returns breaks +#' as output. See `ggplot2::continuous_scale` for details. +#' +#' @return A wrapped breaks function suitable for use with ggplot scales. +#' @export +breaks_cached <- function(breaks) { + if (! rlang::is_function(breaks)) { + cli::cli_abort("{.arg breaks} must be a function") + } + + cached <- ggplot2::ggproto( + "BreaksCached", NULL, + fn = breaks, + cached = NULL, + get_breaks = function(self, limits) { + if (is.null(self$cached)) self$cached <- self$fn(limits) + self$cached + } + )$get_breaks + + class(cached) <- c("ggplot2_cached_breaks", class(cached)) + cached +} + +#' @export +format.ggplot2_cached_breaks <- function(x, ...) { + bc <- environment(x)$self + inner <- environment(bc$fn)$f + + paste0( + "\n", + ifelse( + is.null(bc$cached), + paste0(" ", format(inner), collapse = "\n"), + paste0(" [", class(bc$cached), "] ", paste0(format(bc$cached), collapse = " ")) + ) + ) +} + +#' @export +print.ggplot2_cached_breaks <- function(x, ...) { + cat(format(x), sep = "") +} diff --git a/man/breaks_cached.Rd b/man/breaks_cached.Rd new file mode 100644 index 0000000000..2460d1648e --- /dev/null +++ b/man/breaks_cached.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/breaks_cached.R +\name{breaks_cached} +\alias{breaks_cached} +\title{Caching scale breaks} +\usage{ +breaks_cached(breaks) +} +\arguments{ +\item{breaks}{A function that takes the limits as input and returns breaks +as output. See \code{ggplot2::continuous_scale} for details.} +} +\value{ +A wrapped breaks function suitable for use with ggplot scales. +} +\description{ +This helper caches the output of another breaks function the first time it is +evaluated. All subsequent calls will return the same breaks vector +regardless of the provided limits. In general this is not what you want +since the breaks should change when the limits change. It is helpful in the +specific case that you are using \code{follow.scale} on \code{stat_bin()} and related +binning stats, because it ensures that the breaks are not recomputed after +they are used to define the bin edges. +} From 4a7ce186e72b38aab55dee959a475337f6436e93 Mon Sep 17 00:00:00 2001 From: Carl Suster Date: Mon, 16 Dec 2024 11:53:29 +1100 Subject: [PATCH 4/5] Complete follow.scale feature for stat_bin() --- R/breaks_cached.R | 22 ++++++++++++++++++---- R/geom-histogram.R | 7 +++++++ R/stat-bin.R | 28 ++++++++++++++++++++++------ _pkgdown.yml | 1 + man/breaks_cached.Rd | 21 ++++++++++++++++++--- man/geom_histogram.Rd | 11 +++++++++++ 6 files changed, 77 insertions(+), 13 deletions(-) diff --git a/R/breaks_cached.R b/R/breaks_cached.R index fddc8a8536..f2c0e26737 100644 --- a/R/breaks_cached.R +++ b/R/breaks_cached.R @@ -1,18 +1,32 @@ -#' Caching scale breaks +#' Cache scale breaks #' #' This helper caches the output of another breaks function the first time it is #' evaluated. All subsequent calls will return the same breaks vector #' regardless of the provided limits. In general this is not what you want #' since the breaks should change when the limits change. It is helpful in the -#' specific case that you are using `follow.scale` on `stat_bin()` and related +#' specific case that you are using `follow.scale` on [stat_bin()] and related #' binning stats, because it ensures that the breaks are not recomputed after #' they are used to define the bin edges. #' +#' @export #' @param breaks A function that takes the limits as input and returns breaks -#' as output. See `ggplot2::continuous_scale` for details. +#' as output. See [continuous_scale()] for details. #' #' @return A wrapped breaks function suitable for use with ggplot scales. -#' @export +#' @examples +#' discoveries_df <- data.frame( +#' year = unlist(mapply(rep, time(discoveries), discoveries)) +#' ) +#' p <- ggplot(discoveries_df, aes(year)) + +#' geom_histogram(follow.scale = "minor") +#' +#' # Using follow.scale with function breaks can cause misalignment as the scale +#' # can update the breaks after the bin edges are fixed by the stat +#' p + scale_x_continuous(breaks = scales::breaks_extended()) +#' +#' # Wrapping the same breaks function avoids this issue but can leave you with +#' # sub-optimal breaks since they are no longer updated after stats +#' p + scale_x_continuous(breaks = breaks_cached(scales::breaks_extended())) breaks_cached <- function(breaks) { if (! rlang::is_function(breaks)) { cli::cli_abort("{.arg breaks} must be a function") diff --git a/R/geom-histogram.R b/R/geom-histogram.R index 7bd832b611..3f27ab1a80 100644 --- a/R/geom-histogram.R +++ b/R/geom-histogram.R @@ -133,6 +133,13 @@ #' ggplot(economics_long, aes(value)) + #' facet_wrap(~variable, scales = 'free_x') + #' geom_histogram(binwidth = function(x) 2 * IQR(x) / (length(x)^(1/3))) +#' +#' # If you've already got your scale breaks set up how you want them, you can +#' # tell stat_bin() to align bin edges with the breaks. This works best when +#' # your scale uses fixed breaks, otherwise the breaks can be updated later +#' ggplot(diamonds, aes(carat)) + +#' geom_histogram(follow.scale = "minor") + +#' scale_x_continuous(breaks = seq(0, 5, 0.5)) geom_histogram <- function(mapping = NULL, data = NULL, stat = "bin", position = "stack", ..., diff --git a/R/stat-bin.R b/R/stat-bin.R index b9dcfdb403..e1aa9bf2a6 100644 --- a/R/stat-bin.R +++ b/R/stat-bin.R @@ -22,6 +22,12 @@ #' @param breaks Alternatively, you can supply a numeric vector giving #' the bin boundaries. Overrides `binwidth`, `bins`, `center`, #' and `boundary`. Can also be a function that takes group-wise values as input and returns bin boundaries. +#' @param follow.scale Alternatively, the bin edges can be copied from the scale +#' breaks, either `"major"` or `"minor"`. Ignored when `"off"`. Note that if +#. the scale's limits are updated by other layers or expansions then its +#. breaks are recomputed and might end up different to the value copied for +#. the bin edges. This is not an issue when the scale uses a fixed breaks +#. vector. #' @param closed One of `"right"` or `"left"` indicating whether right #' or left edges of bins are included in the bin. #' @param pad If `TRUE`, adds empty bins at either end of x. This ensures @@ -58,8 +64,8 @@ stat_bin <- function(mapping = NULL, data = NULL, breaks = NULL, closed = c("right", "left"), pad = FALSE, + follow.scale = c("off", "minor", "major"), na.rm = FALSE, - follow.scale = FALSE, keep.zeroes = "all", orientation = NA, show.legend = NA, @@ -81,8 +87,8 @@ stat_bin <- function(mapping = NULL, data = NULL, breaks = breaks, closed = closed, pad = pad, - na.rm = na.rm, follow.scale = follow.scale, + na.rm = na.rm, orientation = orientation, keep.zeroes = keep.zeroes, ... @@ -138,7 +144,15 @@ StatBin <- ggproto("StatBin", Stat, cli::cli_abort("Only one of {.arg boundary} and {.arg center} may be specified in {.fn {snake_class(self)}}.") } - if (is.null(params$breaks) && is.null(params$binwidth) && is.null(params$bins)) { + if (!is.null(params$follow.scale)) { + params$follow.scale <- match.arg(params$follow.scale, c("off", "minor", "major")) + if (params$follow.scale == "off") params$follow.scale <- NULL + } + if (!is.null(params$follow.scale) && !is.null(params$breaks)) { + cli::cli_abort("Only one of {.arg follow.scale} and {.arg breaks} may be specified in {.fn {snake_class(self)}}.") + } + + if (is.null(params$breaks) && is.null(params$binwidth) && is.null(params$bins) && is.null(params$follow.scale)) { cli::cli_inform("{.fn {snake_class(self)}} using {.code bins = 30}. Pick better value with {.arg binwidth}.") params$bins <- 30 } @@ -152,13 +166,15 @@ StatBin <- ggproto("StatBin", Stat, center = NULL, boundary = NULL, closed = c("right", "left"), pad = FALSE, breaks = NULL, flipped_aes = FALSE, keep.zeroes = "all", - follow.scale = FALSE, + follow.scale = NULL, # The following arguments are not used, but must # be listed so parameters are computed correctly origin = NULL, right = NULL, drop = NULL) { x <- flipped_names(flipped_aes)$x - if (follow.scale) { - breaks <- scales[[x]]$get_breaks() + if (!is.null(follow.scale)) { + breaks <- switch(follow.scale, + minor = scales[[x]]$get_breaks_minor(), + major = scales[[x]]$get_breaks()) bins <- bin_breaks(breaks, closed) } else if (!is.null(breaks)) { if (is.function(breaks)) { diff --git a/_pkgdown.yml b/_pkgdown.yml index 1e4ea6a727..4508f87291 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -116,6 +116,7 @@ reference: - expansion - starts_with("scale_") - get_alt_text + - breaks_cached - title: "Guides: axes and legends" desc: > diff --git a/man/breaks_cached.Rd b/man/breaks_cached.Rd index 2460d1648e..e1c97a9596 100644 --- a/man/breaks_cached.Rd +++ b/man/breaks_cached.Rd @@ -2,13 +2,13 @@ % Please edit documentation in R/breaks_cached.R \name{breaks_cached} \alias{breaks_cached} -\title{Caching scale breaks} +\title{Cache scale breaks} \usage{ breaks_cached(breaks) } \arguments{ \item{breaks}{A function that takes the limits as input and returns breaks -as output. See \code{ggplot2::continuous_scale} for details.} +as output. See \code{\link[=continuous_scale]{continuous_scale()}} for details.} } \value{ A wrapped breaks function suitable for use with ggplot scales. @@ -18,7 +18,22 @@ This helper caches the output of another breaks function the first time it is evaluated. All subsequent calls will return the same breaks vector regardless of the provided limits. In general this is not what you want since the breaks should change when the limits change. It is helpful in the -specific case that you are using \code{follow.scale} on \code{stat_bin()} and related +specific case that you are using \code{follow.scale} on \code{\link[=stat_bin]{stat_bin()}} and related binning stats, because it ensures that the breaks are not recomputed after they are used to define the bin edges. } +\examples{ +discoveries_df <- data.frame( + year = unlist(mapply(rep, time(discoveries), discoveries)) +) +p <- ggplot(discoveries_df, aes(year)) + + geom_histogram(follow.scale = "minor") + +# Using follow.scale with function breaks can cause misalignment as the scale +# can update the breaks after the bin edges are fixed by the stat +p + scale_x_continuous(breaks = scales::breaks_extended()) + +# Wrapping the same breaks function avoids this issue but can leave you with +# sub-optimal breaks since they are no longer updated after stats +p + scale_x_continuous(breaks = breaks_cached(scales::breaks_extended())) +} diff --git a/man/geom_histogram.Rd b/man/geom_histogram.Rd index 32f9c39610..eeed9fa4f8 100644 --- a/man/geom_histogram.Rd +++ b/man/geom_histogram.Rd @@ -45,6 +45,7 @@ stat_bin( breaks = NULL, closed = c("right", "left"), pad = FALSE, + follow.scale = c("off", "minor", "major"), na.rm = FALSE, keep.zeroes = "all", orientation = NA, @@ -174,6 +175,9 @@ or left edges of bins are included in the bin.} \item{pad}{If \code{TRUE}, adds empty bins at either end of x. This ensures frequency polygons touch 0. Defaults to \code{FALSE}.} +\item{follow.scale}{Alternatively, the bin edges can be copied from the scale +breaks, either \code{"major"} or \code{"minor"}. Ignored when \code{"off"}. Note that if} + \item{keep.zeroes}{Treatment of zero count bins. If \code{"all"} (default), such bins are kept as-is. If \code{"none"}, all zero count bins are filtered out. If \code{"inner"} only zero count bins at the flanks are filtered out, but not @@ -331,6 +335,13 @@ m + ggplot(economics_long, aes(value)) + facet_wrap(~variable, scales = 'free_x') + geom_histogram(binwidth = function(x) 2 * IQR(x) / (length(x)^(1/3))) + +# If you've already got your scale breaks set up how you want them, you can +# tell stat_bin() to align bin edges with the breaks. This works best when +# your scale uses fixed breaks, otherwise the breaks can be updated later +ggplot(diamonds, aes(carat)) + + geom_histogram(follow.scale = "minor") + + scale_x_continuous(breaks = seq(0, 5, 0.5)) } \seealso{ \code{\link[=stat_count]{stat_count()}}, which counts the number of cases at each x From d6e536eefc43b1a25db1451b5b3ab6d8f7f62e4c Mon Sep 17 00:00:00 2001 From: Carl Suster Date: Mon, 16 Dec 2024 12:15:18 +1100 Subject: [PATCH 5/5] Fix default follow.scale inconsistency --- R/stat-bin.R | 24 +++++++++--------------- man/geom_histogram.Rd | 8 ++++++-- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/R/stat-bin.R b/R/stat-bin.R index e1aa9bf2a6..bb3970f27e 100644 --- a/R/stat-bin.R +++ b/R/stat-bin.R @@ -24,10 +24,10 @@ #' and `boundary`. Can also be a function that takes group-wise values as input and returns bin boundaries. #' @param follow.scale Alternatively, the bin edges can be copied from the scale #' breaks, either `"major"` or `"minor"`. Ignored when `"off"`. Note that if -#. the scale's limits are updated by other layers or expansions then its -#. breaks are recomputed and might end up different to the value copied for -#. the bin edges. This is not an issue when the scale uses a fixed breaks -#. vector. +#' the scale's limits are updated by other layers or expansions then its +#' breaks are recomputed and might end up different to the value copied for +#' the bin edges. This is not an issue when the scale uses a fixed breaks +#' vector. #' @param closed One of `"right"` or `"left"` indicating whether right #' or left edges of bins are included in the bin. #' @param pad If `TRUE`, adds empty bins at either end of x. This ensures @@ -64,7 +64,7 @@ stat_bin <- function(mapping = NULL, data = NULL, breaks = NULL, closed = c("right", "left"), pad = FALSE, - follow.scale = c("off", "minor", "major"), + follow.scale = "off", na.rm = FALSE, keep.zeroes = "all", orientation = NA, @@ -144,15 +144,9 @@ StatBin <- ggproto("StatBin", Stat, cli::cli_abort("Only one of {.arg boundary} and {.arg center} may be specified in {.fn {snake_class(self)}}.") } - if (!is.null(params$follow.scale)) { - params$follow.scale <- match.arg(params$follow.scale, c("off", "minor", "major")) - if (params$follow.scale == "off") params$follow.scale <- NULL - } - if (!is.null(params$follow.scale) && !is.null(params$breaks)) { - cli::cli_abort("Only one of {.arg follow.scale} and {.arg breaks} may be specified in {.fn {snake_class(self)}}.") - } + params$follow.scale <- match.arg(params$follow.scale, c("off", "minor", "major")) - if (is.null(params$breaks) && is.null(params$binwidth) && is.null(params$bins) && is.null(params$follow.scale)) { + if (is.null(params$breaks) && is.null(params$binwidth) && is.null(params$bins) && (params$follow.scale == "off")) { cli::cli_inform("{.fn {snake_class(self)}} using {.code bins = 30}. Pick better value with {.arg binwidth}.") params$bins <- 30 } @@ -166,12 +160,12 @@ StatBin <- ggproto("StatBin", Stat, center = NULL, boundary = NULL, closed = c("right", "left"), pad = FALSE, breaks = NULL, flipped_aes = FALSE, keep.zeroes = "all", - follow.scale = NULL, + follow.scale = "off", # The following arguments are not used, but must # be listed so parameters are computed correctly origin = NULL, right = NULL, drop = NULL) { x <- flipped_names(flipped_aes)$x - if (!is.null(follow.scale)) { + if (follow.scale != "off") { breaks <- switch(follow.scale, minor = scales[[x]]$get_breaks_minor(), major = scales[[x]]$get_breaks()) diff --git a/man/geom_histogram.Rd b/man/geom_histogram.Rd index eeed9fa4f8..94ea8be60b 100644 --- a/man/geom_histogram.Rd +++ b/man/geom_histogram.Rd @@ -45,7 +45,7 @@ stat_bin( breaks = NULL, closed = c("right", "left"), pad = FALSE, - follow.scale = c("off", "minor", "major"), + follow.scale = "off", na.rm = FALSE, keep.zeroes = "all", orientation = NA, @@ -176,7 +176,11 @@ or left edges of bins are included in the bin.} frequency polygons touch 0. Defaults to \code{FALSE}.} \item{follow.scale}{Alternatively, the bin edges can be copied from the scale -breaks, either \code{"major"} or \code{"minor"}. Ignored when \code{"off"}. Note that if} +breaks, either \code{"major"} or \code{"minor"}. Ignored when \code{"off"}. Note that if +the scale's limits are updated by other layers or expansions then its +breaks are recomputed and might end up different to the value copied for +the bin edges. This is not an issue when the scale uses a fixed breaks +vector.} \item{keep.zeroes}{Treatment of zero count bins. If \code{"all"} (default), such bins are kept as-is. If \code{"none"}, all zero count bins are filtered out.