diff --git a/NEWS.md b/NEWS.md index 4c9fb2f09c..ab4b2080fd 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,8 @@ # ggplot2 (development version) +* Discrete scales now support `minor_breaks`. This may only make sense in + discrete position scales, where it affects the placement of minor ticks + and minor gridlines (#5434). * Discrete position scales now expose the `palette` argument, which can be used to customise spacings between levels (@teunbrand, #5770). * The default `se` parameter in layers with `geom = "smooth"` will be `TRUE` diff --git a/R/guide-axis.R b/R/guide-axis.R index 8eafccf114..dc57ffd23e 100644 --- a/R/guide-axis.R +++ b/R/guide-axis.R @@ -136,6 +136,11 @@ GuideAxis <- ggproto( if (nrow(major) > 0) { major$.type <- "major" + if (!vec_is(minor$.value, major$.value)) { + # If we have mixed types of values, which may happen in discrete scales, + # discard minor values in favour of the major values. + minor$.value <- NULL + } vec_rbind(major, minor) } else { minor diff --git a/R/scale-.R b/R/scale-.R index 6233caf54f..9eaa153590 100644 --- a/R/scale-.R +++ b/R/scale-.R @@ -23,13 +23,13 @@ #' Also accepts rlang [lambda][rlang::as_function()] function notation. #' @param minor_breaks One of: #' - `NULL` for no minor breaks -#' - `waiver()` for the default breaks (one minor break between -#' each major break) +#' - `waiver()` for the default breaks (none for discrete, one minor break +#' between each major break for continuous) #' - A numeric vector of positions #' - A function that given the limits returns a vector of minor breaks. Also #' accepts rlang [lambda][rlang::as_function()] function notation. When #' the function has two arguments, it will be given the limits and major -#' breaks. +#' break positions. #' @param n.breaks An integer guiding the number of major breaks. The algorithm #' may choose a slightly different number to ensure nice break labels. Will #' only have an effect if `breaks = waiver()`. Use `NULL` to use the default @@ -200,7 +200,8 @@ continuous_scale <- function(aesthetics, scale_name = deprecated(), palette, nam #' The `r link_book("new scales section", "extensions#sec-new-scales")` #' @keywords internal discrete_scale <- function(aesthetics, scale_name = deprecated(), palette, name = waiver(), - breaks = waiver(), labels = waiver(), limits = NULL, expand = waiver(), + breaks = waiver(), minor_breaks = waiver(), + labels = waiver(), limits = NULL, expand = waiver(), na.translate = TRUE, na.value = NA, drop = TRUE, guide = "legend", position = "left", call = caller_call(), @@ -218,6 +219,7 @@ discrete_scale <- function(aesthetics, scale_name = deprecated(), palette, name limits <- allow_lambda(limits) breaks <- allow_lambda(breaks) labels <- allow_lambda(labels) + minor_breaks <- allow_lambda(minor_breaks) if (!is.function(limits) && (length(limits) > 0) && !is.discrete(limits)) { cli::cli_warn(c( @@ -247,6 +249,7 @@ discrete_scale <- function(aesthetics, scale_name = deprecated(), palette, name name = name, breaks = breaks, + minor_breaks = minor_breaks, labels = labels, drop = drop, guide = guide, @@ -1022,7 +1025,34 @@ ScaleDiscrete <- ggproto("ScaleDiscrete", Scale, structure(in_domain, pos = match(in_domain, breaks)) }, - get_breaks_minor = function(...) NULL, + get_breaks_minor = function(self, n = 2, b = self$break_positions(), + limits = self$get_limits()) { + breaks <- self$minor_breaks + # The default is to draw no minor ticks + if (is.null(breaks %|W|% NULL)) { + return(NULL) + } + if (is.function(breaks)) { + # Ensure function gets supplied numeric limits and breaks + if (!is.numeric(b)) { + b <- self$map(b) + } + if (!is.numeric(limits)) { + limits <- self$map(limits) + limits <- self$dimension(self$expand, limits) + } + + # Allow for two types of minor breaks specifications + break_fun <- fetch_ggproto(self, "minor_breaks") + arg_names <- fn_fmls_names(break_fun) + if (length(arg_names) == 1L) { + breaks <- break_fun(limits) + } else { + breaks <- break_fun(limits, b) + } + } + breaks + }, get_labels = function(self, breaks = self$get_breaks()) { if (self$is_empty()) { diff --git a/R/scale-view.R b/R/scale-view.R index 3a068ea81c..de78ebffb6 100644 --- a/R/scale-view.R +++ b/R/scale-view.R @@ -15,17 +15,16 @@ view_scale_primary <- function(scale, limits = scale$get_limits(), continuous_range = scale$dimension(limits = limits)) { + # continuous_range can be specified in arbitrary order, but + # scales expect the one in ascending order. + continuous_scale_sorted <- sort(continuous_range) if(!scale$is_discrete()) { - # continuous_range can be specified in arbitrary order, but - # continuous scales expect the one in ascending order. - continuous_scale_sorted <- sort(continuous_range) breaks <- scale$get_breaks(continuous_scale_sorted) - minor_breaks <- scale$get_breaks_minor(b = breaks, limits = continuous_scale_sorted) breaks <- censor(breaks, continuous_scale_sorted, only.finite = FALSE) } else { breaks <- scale$get_breaks(limits) - minor_breaks <- scale$get_breaks_minor(b = breaks, limits = limits) } + minor_breaks <- scale$get_breaks_minor(b = breaks, limits = continuous_scale_sorted) minor_breaks <- censor(minor_breaks, continuous_range, only.finite = FALSE) ggproto(NULL, ViewScale, diff --git a/man/continuous_scale.Rd b/man/continuous_scale.Rd index dc4abffbce..a572ec2bf7 100644 --- a/man/continuous_scale.Rd +++ b/man/continuous_scale.Rd @@ -56,13 +56,13 @@ Also accepts rlang \link[rlang:as_function]{lambda} function notation. \item{minor_breaks}{One of: \itemize{ \item \code{NULL} for no minor breaks -\item \code{waiver()} for the default breaks (one minor break between -each major break) +\item \code{waiver()} for the default breaks (none for discrete, one minor break +between each major break for continuous) \item A numeric vector of positions \item A function that given the limits returns a vector of minor breaks. Also accepts rlang \link[rlang:as_function]{lambda} function notation. When the function has two arguments, it will be given the limits and major -breaks. +break positions. }} \item{n.breaks}{An integer guiding the number of major breaks. The algorithm diff --git a/man/discrete_scale.Rd b/man/discrete_scale.Rd index 0fd213cd30..ae349cdf1f 100644 --- a/man/discrete_scale.Rd +++ b/man/discrete_scale.Rd @@ -10,6 +10,7 @@ discrete_scale( palette, name = waiver(), breaks = waiver(), + minor_breaks = waiver(), labels = waiver(), limits = NULL, expand = waiver(), @@ -47,6 +48,18 @@ as output. Also accepts rlang \link[rlang:as_function]{lambda} function notation. }} +\item{minor_breaks}{One of: +\itemize{ +\item \code{NULL} for no minor breaks +\item \code{waiver()} for the default breaks (none for discrete, one minor break +between each major break for continuous) +\item A numeric vector of positions +\item A function that given the limits returns a vector of minor breaks. Also +accepts rlang \link[rlang:as_function]{lambda} function notation. When +the function has two arguments, it will be given the limits and major +break positions. +}} + \item{labels}{One of: \itemize{ \item \code{NULL} for no labels diff --git a/man/scale_continuous.Rd b/man/scale_continuous.Rd index 56c23639a6..153bb325f5 100644 --- a/man/scale_continuous.Rd +++ b/man/scale_continuous.Rd @@ -78,13 +78,13 @@ Also accepts rlang \link[rlang:as_function]{lambda} function notation. \item{minor_breaks}{One of: \itemize{ \item \code{NULL} for no minor breaks -\item \code{waiver()} for the default breaks (one minor break between -each major break) +\item \code{waiver()} for the default breaks (none for discrete, one minor break +between each major break for continuous) \item A numeric vector of positions \item A function that given the limits returns a vector of minor breaks. Also accepts rlang \link[rlang:as_function]{lambda} function notation. When the function has two arguments, it will be given the limits and major -breaks. +break positions. }} \item{n.breaks}{An integer guiding the number of major breaks. The algorithm diff --git a/man/scale_discrete.Rd b/man/scale_discrete.Rd index 381c09254e..0bab3ad985 100644 --- a/man/scale_discrete.Rd +++ b/man/scale_discrete.Rd @@ -63,6 +63,17 @@ from a discrete scale, specify \code{na.translate = FALSE}.} missing values be displayed as? Does not apply to position scales where \code{NA} is always placed at the far right.} \item{\code{aesthetics}}{The names of the aesthetics that this scale works with.} + \item{\code{minor_breaks}}{One of: +\itemize{ +\item \code{NULL} for no minor breaks +\item \code{waiver()} for the default breaks (none for discrete, one minor break +between each major break for continuous) +\item A numeric vector of positions +\item A function that given the limits returns a vector of minor breaks. Also +accepts rlang \link[rlang:as_function]{lambda} function notation. When +the function has two arguments, it will be given the limits and major +break positions. +}} \item{\code{labels}}{One of: \itemize{ \item \code{NULL} for no labels diff --git a/man/scale_gradient.Rd b/man/scale_gradient.Rd index cbc5427282..368ef16509 100644 --- a/man/scale_gradient.Rd +++ b/man/scale_gradient.Rd @@ -120,13 +120,13 @@ Also accepts rlang \link[rlang:as_function]{lambda} function notation. \item{\code{minor_breaks}}{One of: \itemize{ \item \code{NULL} for no minor breaks -\item \code{waiver()} for the default breaks (one minor break between -each major break) +\item \code{waiver()} for the default breaks (none for discrete, one minor break +between each major break for continuous) \item A numeric vector of positions \item A function that given the limits returns a vector of minor breaks. Also accepts rlang \link[rlang:as_function]{lambda} function notation. When the function has two arguments, it will be given the limits and major -breaks. +break positions. }} \item{\code{n.breaks}}{An integer guiding the number of major breaks. The algorithm may choose a slightly different number to ensure nice break labels. Will diff --git a/man/scale_grey.Rd b/man/scale_grey.Rd index e085fa0016..6b04f40688 100644 --- a/man/scale_grey.Rd +++ b/man/scale_grey.Rd @@ -61,6 +61,17 @@ every level in a legend, the layer should use \code{show.legend = TRUE}.} \item{\code{na.translate}}{Unlike continuous scales, discrete scales can easily show missing values, and do so by default. If you want to remove missing values from a discrete scale, specify \code{na.translate = FALSE}.} + \item{\code{minor_breaks}}{One of: +\itemize{ +\item \code{NULL} for no minor breaks +\item \code{waiver()} for the default breaks (none for discrete, one minor break +between each major break for continuous) +\item A numeric vector of positions +\item A function that given the limits returns a vector of minor breaks. Also +accepts rlang \link[rlang:as_function]{lambda} function notation. When +the function has two arguments, it will be given the limits and major +break positions. +}} \item{\code{labels}}{One of: \itemize{ \item \code{NULL} for no labels diff --git a/man/scale_hue.Rd b/man/scale_hue.Rd index 0ec49fd123..99776d95d3 100644 --- a/man/scale_hue.Rd +++ b/man/scale_hue.Rd @@ -67,6 +67,17 @@ every level in a legend, the layer should use \code{show.legend = TRUE}.} \item{\code{na.translate}}{Unlike continuous scales, discrete scales can easily show missing values, and do so by default. If you want to remove missing values from a discrete scale, specify \code{na.translate = FALSE}.} + \item{\code{minor_breaks}}{One of: +\itemize{ +\item \code{NULL} for no minor breaks +\item \code{waiver()} for the default breaks (none for discrete, one minor break +between each major break for continuous) +\item A numeric vector of positions +\item A function that given the limits returns a vector of minor breaks. Also +accepts rlang \link[rlang:as_function]{lambda} function notation. When +the function has two arguments, it will be given the limits and major +break positions. +}} \item{\code{labels}}{One of: \itemize{ \item \code{NULL} for no labels diff --git a/man/scale_linetype.Rd b/man/scale_linetype.Rd index 94eeb93927..da4cf2b7c3 100644 --- a/man/scale_linetype.Rd +++ b/man/scale_linetype.Rd @@ -53,6 +53,17 @@ every level in a legend, the layer should use \code{show.legend = TRUE}.} missing values, and do so by default. If you want to remove missing values from a discrete scale, specify \code{na.translate = FALSE}.} \item{\code{aesthetics}}{The names of the aesthetics that this scale works with.} + \item{\code{minor_breaks}}{One of: +\itemize{ +\item \code{NULL} for no minor breaks +\item \code{waiver()} for the default breaks (none for discrete, one minor break +between each major break for continuous) +\item A numeric vector of positions +\item A function that given the limits returns a vector of minor breaks. Also +accepts rlang \link[rlang:as_function]{lambda} function notation. When +the function has two arguments, it will be given the limits and major +break positions. +}} \item{\code{labels}}{One of: \itemize{ \item \code{NULL} for no labels diff --git a/man/scale_manual.Rd b/man/scale_manual.Rd index 9857dcbc65..92205f3013 100644 --- a/man/scale_manual.Rd +++ b/man/scale_manual.Rd @@ -64,6 +64,17 @@ from a discrete scale, specify \code{na.translate = FALSE}.} \code{waiver()}, the default, the name of the scale is taken from the first mapping used for that aesthetic. If \code{NULL}, the legend title will be omitted.} + \item{\code{minor_breaks}}{One of: +\itemize{ +\item \code{NULL} for no minor breaks +\item \code{waiver()} for the default breaks (none for discrete, one minor break +between each major break for continuous) +\item A numeric vector of positions +\item A function that given the limits returns a vector of minor breaks. Also +accepts rlang \link[rlang:as_function]{lambda} function notation. When +the function has two arguments, it will be given the limits and major +break positions. +}} \item{\code{labels}}{One of: \itemize{ \item \code{NULL} for no labels diff --git a/man/scale_shape.Rd b/man/scale_shape.Rd index 65deec3ea2..ffbb381481 100644 --- a/man/scale_shape.Rd +++ b/man/scale_shape.Rd @@ -53,6 +53,17 @@ from a discrete scale, specify \code{na.translate = FALSE}.} missing values be displayed as? Does not apply to position scales where \code{NA} is always placed at the far right.} \item{\code{aesthetics}}{The names of the aesthetics that this scale works with.} + \item{\code{minor_breaks}}{One of: +\itemize{ +\item \code{NULL} for no minor breaks +\item \code{waiver()} for the default breaks (none for discrete, one minor break +between each major break for continuous) +\item A numeric vector of positions +\item A function that given the limits returns a vector of minor breaks. Also +accepts rlang \link[rlang:as_function]{lambda} function notation. When +the function has two arguments, it will be given the limits and major +break positions. +}} \item{\code{labels}}{One of: \itemize{ \item \code{NULL} for no labels diff --git a/man/scale_size.Rd b/man/scale_size.Rd index c1c7d8dd05..753ecfa790 100644 --- a/man/scale_size.Rd +++ b/man/scale_size.Rd @@ -134,13 +134,13 @@ breaks are given explicitly.} \item{\code{minor_breaks}}{One of: \itemize{ \item \code{NULL} for no minor breaks -\item \code{waiver()} for the default breaks (one minor break between -each major break) +\item \code{waiver()} for the default breaks (none for discrete, one minor break +between each major break for continuous) \item A numeric vector of positions \item A function that given the limits returns a vector of minor breaks. Also accepts rlang \link[rlang:as_function]{lambda} function notation. When the function has two arguments, it will be given the limits and major -breaks. +break positions. }} \item{\code{oob}}{One of: \itemize{ diff --git a/tests/testthat/_snaps/prohibited-functions.md b/tests/testthat/_snaps/prohibited-functions.md index 87c26e26a8..f2aa9bf4d2 100644 --- a/tests/testthat/_snaps/prohibited-functions.md +++ b/tests/testthat/_snaps/prohibited-functions.md @@ -29,7 +29,7 @@ [4] "date_minor_breaks" $discrete_scale - [1] "scale_name" + [1] "scale_name" "minor_breaks" $geom_density2d [1] "contour_var"