From 1bb923099e5d44b21615431f07bb99894e7a7dfd Mon Sep 17 00:00:00 2001 From: Teun van den Brand <49372158+teunbrand@users.noreply.github.com> Date: Mon, 2 Dec 2024 11:14:57 +0100 Subject: [PATCH] Scale palettes from theme (#5946) * add palette theme elements * set palettes * expand options for numeric palettes * backward compatibility for `options()` default scales * use explicit scales in tests * standard colour scales have `palette = NULL` * document * properly document this time * misc fixes * more palettes in theme * use binned versions of discrete palettes for continuous linetype/shape * Set default palettes to `NULL` * tweak test to populate palettes * try registered theme palettes * document * remove redundant call to `plot_theme()` * simplify `fallback_palette()` args * Put in shims for scales/#427 * Streamline `ScalesList$set_palettes()` method * try to match first non-null aesthetic * add news bullet --- NEWS.md | 3 + R/geom-text.R | 2 +- R/plot-build.R | 1 + R/scale-.R | 1 + R/scale-alpha.R | 35 +-- R/scale-colour.R | 216 ++++++++++-------- R/scale-hue.R | 57 ++--- R/scale-linetype.R | 4 +- R/scale-linewidth.R | 35 +-- R/scale-shape.R | 8 +- R/scale-size.R | 38 +-- R/scales-.R | 33 +++ R/theme-elements.R | 15 ++ R/utilities.R | 62 ++++- man/geom_text.Rd | 2 +- man/scale_alpha.Rd | 8 +- man/scale_colour_continuous.Rd | 45 +++- man/scale_colour_discrete.Rd | 20 +- man/scale_linewidth.Rd | 4 +- man/scale_shape.Rd | 2 +- man/scale_size.Rd | 4 +- .../_snaps/scale-colour-continuous.md | 2 - tests/testthat/_snaps/scale-discrete.md | 8 +- tests/testthat/test-guide-colorbar.R | 2 +- tests/testthat/test-guide-legend.R | 13 +- tests/testthat/test-scales.R | 24 ++ 26 files changed, 427 insertions(+), 217 deletions(-) diff --git a/NEWS.md b/NEWS.md index 36ab09933e..2bd655a5c1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -215,6 +215,9 @@ * Added `keep.zeroes` argument to `stat_bin()` (@teunbrand, #3449) * `coord_sf()` no longer errors when dealing with empty graticules (@teunbrand, #6052) * Added `theme_transparent()` with transparent backgrounds (@topepo). +* New theme elements `palette.{aes}.discrete` and `palette.{aes}.continuous`. + Theme palettes replace palettes in scales where `palette = NULL`, which is + the new default in many scales (@teunbrand, #4696). # ggplot2 3.5.1 diff --git a/R/geom-text.R b/R/geom-text.R index ed378734ba..78e601f8f9 100644 --- a/R/geom-text.R +++ b/R/geom-text.R @@ -95,7 +95,7 @@ #' # Add aesthetic mappings #' p + geom_text(aes(colour = factor(cyl))) #' p + geom_text(aes(colour = factor(cyl))) + -#' scale_colour_discrete(l = 40) +#' scale_colour_hue(l = 40) #' p + geom_label(aes(fill = factor(cyl)), colour = "white", fontface = "bold") #' #' # Scale size of text, and change legend key glyph from a to point diff --git a/R/plot-build.R b/R/plot-build.R index 873f79a32c..b55c599198 100644 --- a/R/plot-build.R +++ b/R/plot-build.R @@ -106,6 +106,7 @@ ggplot_build.ggplot <- function(plot) { # Train and map non-position scales and guides npscales <- scales$non_position_scales() if (npscales$n() > 0) { + npscales$set_palettes(plot$theme) lapply(data, npscales$train_df) plot$guides <- plot$guides$build(npscales, plot$layers, plot$labels, data, plot$theme) data <- lapply(data, npscales$map_df) diff --git a/R/scale-.R b/R/scale-.R index 33752f9ec2..f345310e4b 100644 --- a/R/scale-.R +++ b/R/scale-.R @@ -528,6 +528,7 @@ Scale <- ggproto("Scale", NULL, if (empty(df)) { return() } + self$palette <- self$palette %||% fallback_palette(self) aesthetics <- intersect(self$aesthetics, names(df)) names(aesthetics) <- aesthetics diff --git a/R/scale-alpha.R b/R/scale-alpha.R index 53344f23be..5e22937e88 100644 --- a/R/scale-alpha.R +++ b/R/scale-alpha.R @@ -31,8 +31,9 @@ #' #' # Changing the title #' p + scale_alpha("cylinders") -scale_alpha <- function(name = waiver(), ..., range = c(0.1, 1)) { - continuous_scale("alpha", name = name, palette = pal_rescale(range), ...) +scale_alpha <- function(name = waiver(), ..., range = NULL) { + palette <- if (!is.null(range)) pal_rescale(range) else NULL + continuous_scale("alpha", name = name, palette = palette, ...) } #' @rdname scale_alpha @@ -41,8 +42,9 @@ scale_alpha_continuous <- scale_alpha #' @rdname scale_alpha #' @export -scale_alpha_binned <- function(name = waiver(), ..., range = c(0.1, 1)) { - binned_scale("alpha", name = name, palette = pal_rescale(range), ...) +scale_alpha_binned <- function(name = waiver(), ..., range = NULL) { + palette <- if (!is.null(range)) pal_rescale(range) else NULL + binned_scale("alpha", name = name, palette = palette, ...) } #' @rdname scale_alpha @@ -56,32 +58,33 @@ scale_alpha_discrete <- function(...) { #' @rdname scale_alpha #' @export -scale_alpha_ordinal <- function(name = waiver(), ..., range = c(0.1, 1)) { - discrete_scale( - "alpha", name = name, - palette = function(n) seq(range[1], range[2], length.out = n), - ... - ) +scale_alpha_ordinal <- function(name = waiver(), ..., range = NULL) { + palette <- if (!is.null(range)) { + function(n) seq(range[1], range[2], length.out = n) + } else { + NULL + } + discrete_scale("alpha", name = name, palette = palette, ...) } #' @rdname scale_alpha #' @export #' @usage NULL -scale_alpha_datetime <- function(name = waiver(), ..., range = c(0.1, 1)) { +scale_alpha_datetime <- function(name = waiver(), ..., range = NULL) { + palette <- if (!is.null(range)) pal_rescale(range) else NULL datetime_scale( aesthetics = "alpha", transform = "time", name = name, - palette = pal_rescale(range), - ... + palette = palette, ... ) } #' @rdname scale_alpha #' @export #' @usage NULL -scale_alpha_date <- function(name = waiver(), ..., range = c(0.1, 1)){ +scale_alpha_date <- function(name = waiver(), ..., range = NULL){ + palette <- if (!is.null(range)) pal_rescale(range) else NULL datetime_scale( aesthetics = "alpha", transform = "date", name = name, - palette = pal_rescale(range), - ... + palette = palette, ... ) } diff --git a/R/scale-colour.R b/R/scale-colour.R index 19cdda1396..a17d872dbe 100644 --- a/R/scale-colour.R +++ b/R/scale-colour.R @@ -27,6 +27,7 @@ #' you want to manually set the colors of a scale, consider using #' [scale_colour_gradient()] or [scale_colour_steps()]. #' +#' @inheritParams continuous_scale #' @param ... Additional parameters passed on to the scale type #' @param type One of the following: #' * "gradient" (the default) @@ -77,122 +78,81 @@ #' v #' options(ggplot2.continuous.fill = tmp) # restore previous setting #' @export -scale_colour_continuous <- function(..., +scale_colour_continuous <- function(..., aesthetics = "colour", + guide = "colourbar", na.value = "grey50", type = getOption("ggplot2.continuous.colour")) { - type <- type %||% "gradient" - args <- list2(...) - args$call <- args$call %||% current_call() - if (is.function(type)) { - if (!any(c("...", "call") %in% fn_fmls_names(type))) { - args$call <- NULL - } - check_scale_type(exec(type, !!!args), "scale_colour_continuous", "colour") - } else if (identical(type, "gradient")) { - exec(scale_colour_gradient, !!!args) - } else if (identical(type, "viridis")) { - exec(scale_colour_viridis_c, !!!args) - } else { - cli::cli_abort(c( - "Unknown scale type: {.val {type}}", - "i" = "Use either {.val gradient} or {.val viridis}." - )) + if (!is.null(type)) { + scale <- scale_backward_compatibility( + ..., guide = guide, na.value = na.value, scale = type, + aesthetic = "colour", type = "continuous" + ) + return(scale) } + + continuous_scale( + aesthetics, palette = NULL, guide = guide, na.value = na.value, + ... + ) } #' @rdname scale_colour_continuous #' @export -scale_fill_continuous <- function(..., +scale_fill_continuous <- function(..., aesthetics = "fill", guide = "colourbar", + na.value = "grey50", type = getOption("ggplot2.continuous.fill")) { - type <- type %||% "gradient" - args <- list2(...) - args$call <- args$call %||% current_call() - if (is.function(type)) { - if (!any(c("...", "call") %in% fn_fmls_names(type))) { - args$call <- NULL - } - check_scale_type(exec(type, !!!args), "scale_fill_continuous", "fill") - } else if (identical(type, "gradient")) { - exec(scale_fill_gradient, !!!args) - } else if (identical(type, "viridis")) { - exec(scale_fill_viridis_c, !!!args) - } else { - cli::cli_abort(c( - "Unknown scale type: {.val {type}}", - "i" = "Use either {.val gradient} or {.val viridis}." - )) + if (!is.null(type)) { + scale <- scale_backward_compatibility( + ..., guide = guide, na.value = na.value, scale = type, + aesthetic = "fill", type = "continuous" + ) + return(scale) } + + continuous_scale( + aesthetics, palette = NULL, guide = guide, na.value = na.value, + ... + ) } #' @export #' @rdname scale_colour_continuous -scale_colour_binned <- function(..., +scale_colour_binned <- function(..., aesthetics = "colour", guide = "coloursteps", + na.value = "grey50", type = getOption("ggplot2.binned.colour")) { - args <- list2(...) - args$call <- args$call %||% current_call() - if (is.function(type)) { - if (!any(c("...", "call") %in% fn_fmls_names(type))) { - args$call <- NULL - } - check_scale_type(exec(type, !!!args), "scale_colour_binned", "colour") - } else { - type_fallback <- getOption("ggplot2.continuous.colour", default = "gradient") - # don't use fallback from scale_colour_continuous() if it is - # a function, since that would change the type of the color - # scale from binned to continuous - if (is.function(type_fallback)) { - type_fallback <- "gradient" - } - type <- type %||% type_fallback - - if (identical(type, "gradient")) { - exec(scale_colour_steps, !!!args) - } else if (identical(type, "viridis")) { - exec(scale_colour_viridis_b, !!!args) - } else { - cli::cli_abort(c( - "Unknown scale type: {.val {type}}", - "i" = "Use either {.val gradient} or {.val viridis}." - )) - } + if (!is.null(type)) { + scale <- scale_backward_compatibility( + ..., guide = guide, na.value = na.value, scale = type, + aesthetic = "colour", type = "binned" + ) + return(scale) } + + binned_scale( + aesthetics, palette = NULL, guide = guide, na.value = na.value, + ... + ) } #' @export #' @rdname scale_colour_continuous -scale_fill_binned <- function(..., +scale_fill_binned <- function(..., aesthetics = "fill", guide = "coloursteps", + na.value = "grey50", type = getOption("ggplot2.binned.fill")) { - args <- list2(...) - args$call <- args$call %||% current_call() - if (is.function(type)) { - if (!any(c("...", "call") %in% fn_fmls_names(type))) { - args$call <- NULL - } - check_scale_type(exec(type, !!!args), "scale_fill_binned", "fill") - } else { - type_fallback <- getOption("ggplot2.continuous.fill", default = "gradient") - # don't use fallback from scale_colour_continuous() if it is - # a function, since that would change the type of the color - # scale from binned to continuous - if (is.function(type_fallback)) { - type_fallback <- "gradient" - } - type <- type %||% type_fallback - - if (identical(type, "gradient")) { - exec(scale_fill_steps, !!!args) - } else if (identical(type, "viridis")) { - exec(scale_fill_viridis_b, !!!args) - } else { - cli::cli_abort(c( - "Unknown scale type: {.val {type}}", - "i" = "Use either {.val gradient} or {.val viridis}." - )) - } + if (!is.null(type)) { + scale <- scale_backward_compatibility( + ..., guide = guide, na.value = na.value, scale = type, + aesthetic = "fill", type = "binned" + ) + return(scale) } -} + binned_scale( + aesthetics, palette = NULL, guide = guide, na.value = na.value, + ... + ) +} # helper function to make sure that the provided scale is of the correct # type (i.e., is continuous and works with the provided aesthetic) @@ -222,3 +182,73 @@ check_scale_type <- function(scale, name, aesthetic, scale_is_discrete = FALSE, scale } + +# helper function for backwards compatibility through setting defaults +# scales through `options()` instead of `theme()`. +scale_backward_compatibility <- function(..., scale, aesthetic, type) { + aesthetic <- standardise_aes_names(aesthetic[1]) + + args <- list2(...) + args$call <- args$call %||% caller_call() %||% current_call() + + if (type == "binned") { + fallback <- getOption( + paste("ggplot2", type, aesthetic, sep = "."), + default = "gradient" + ) + if (is.function(fallback)) { + fallback <- "gradient" + } + scale <- scale %||% fallback + } + + if (is_bare_string(scale)) { + if (scale == "continuous") { + scale <- "gradient" + } + if (scale == "discrete") { + scale <- "hue" + } + if (scale == "viridis") { + scale <- switch( + type, discrete = "viridis_d", binned = "viridis_b", "viridis_c" + ) + } + + candidates <- paste("scale", aesthetic, scale, sep = "_") + for (candi in candidates) { + f <- find_global(candi, env = caller_env(), mode = "function") + if (!is.null(f)) { + scale <- f + break + } + } + } + + if (!is.function(scale) && type == "discrete") { + args$type <- scale + scale <- switch( + aesthetic, + colour = scale_colour_qualitative, + fill = scale_fill_qualitative + ) + } + + if (is.function(scale)) { + if (!any(c("...", "call") %in% fn_fmls_names(scale))) { + args$call <- NULL + } + if (!"..." %in% fn_fmls_names(scale)) { + args <- args[intersect(names(args), fn_fmls_names(scale))] + } + scale <- check_scale_type( + exec(scale, !!!args), + paste("scale", aesthetic, type, sep = "_"), + aesthetic, + scale_is_discrete = type == "discrete" + ) + return(scale) + } + + cli::cli_abort("Unknown scale type: {.val {scale}}") +} diff --git a/R/scale-hue.R b/R/scale-hue.R index 414f10864e..b95938ceb6 100644 --- a/R/scale-hue.R +++ b/R/scale-hue.R @@ -86,6 +86,7 @@ scale_fill_hue <- function(name = waiver(), ..., h = c(0, 360) + 15, c = 100, #' is specified. #' #' @param ... Additional parameters passed on to the scale type, +#' @inheritParams discrete_scale #' @param type One of the following: #' * A character vector of color codes. The codes are used for a 'manual' color #' scale as long as the number of codes exceeds the number of data levels @@ -134,48 +135,36 @@ scale_fill_hue <- function(name = waiver(), ..., h = c(0, 360) + 15, c = 100, #' print(cty_by_var(fl)) #' }) #' -scale_colour_discrete <- function(..., type = getOption("ggplot2.discrete.colour")) { - # TODO: eventually `type` should default to a set of colour-blind safe color codes (e.g. Okabe-Ito) - type <- type %||% scale_colour_hue - args <- list2(...) - args$call <- args$call %||% current_call() - - if (is.function(type)) { - if (!any(c("...", "call") %in% fn_fmls_names(type))) { - args$call <- NULL - } - check_scale_type( - exec(type, !!!args), - "scale_colour_discrete", - "colour", - scale_is_discrete = TRUE +scale_colour_discrete <- function(..., aesthetics = "colour", na.value = "grey50", + type = getOption("ggplot2.discrete.colour")) { + if (!is.null(type)) { + scale <- scale_backward_compatibility( + ..., na.value = na.value, scale = type, + aesthetic = "colour", type = "discrete" ) - } else { - exec(scale_colour_qualitative, !!!args, type = type) + return(scale) } + discrete_scale( + aesthetics, palette = NULL, na.value = na.value, + ... + ) } #' @rdname scale_colour_discrete #' @export -scale_fill_discrete <- function(..., type = getOption("ggplot2.discrete.fill")) { - # TODO: eventually `type` should default to a set of colour-blind safe color codes (e.g. Okabe-Ito) - type <- type %||% scale_fill_hue - args <- list2(...) - args$call <- args$call %||% current_call() - - if (is.function(type)) { - if (!any(c("...", "call") %in% fn_fmls_names(type))) { - args$call <- NULL - } - check_scale_type( - exec(type, !!!args), - "scale_fill_discrete", - "fill", - scale_is_discrete = TRUE +scale_fill_discrete <- function(..., aesthetics = "fill", na.value = "grey50", + type = getOption("ggplot2.discrete.fill")) { + if (!is.null(type)) { + scale <- scale_backward_compatibility( + ..., na.value = na.value, scale = type, + aesthetic = "fill", type = "discrete" ) - } else { - exec(scale_fill_qualitative, !!!args, type = type) + return(scale) } + discrete_scale( + aesthetics, palette = NULL, na.value = na.value, + ... + ) } scale_colour_qualitative <- function(name = waiver(), ..., type = NULL, diff --git a/R/scale-linetype.R b/R/scale-linetype.R index d4ea6df26d..8f8c62e30e 100644 --- a/R/scale-linetype.R +++ b/R/scale-linetype.R @@ -38,7 +38,7 @@ scale_linetype <- function(name = waiver(), ..., na.value = NA) { discrete_scale( "linetype", name = name, - palette = pal_linetype(), + palette = NULL, na.value = na.value, ... ) @@ -49,7 +49,7 @@ scale_linetype <- function(name = waiver(), ..., na.value = NA) { scale_linetype_binned <- function(name = waiver(), ..., na.value = NA) { binned_scale( "linetype", name = name, - palette = pal_binned(pal_linetype()), + palette = NULL, na.value = na.value, ... ) diff --git a/R/scale-linewidth.R b/R/scale-linewidth.R index 801df22b3a..9bf05b3913 100644 --- a/R/scale-linewidth.R +++ b/R/scale-linewidth.R @@ -31,10 +31,11 @@ NULL #' @usage NULL scale_linewidth_continuous <- function(name = waiver(), breaks = waiver(), labels = waiver(), limits = NULL, - range = c(1, 6), transform = "identity", + range = NULL, transform = "identity", trans = deprecated(), guide = "legend") { - continuous_scale("linewidth", palette = pal_rescale(range), name = name, + palette <- if (!is.null(range)) pal_rescale(range) else NULL + continuous_scale("linewidth", palette = palette, name = name, breaks = breaks, labels = labels, limits = limits, transform = transform, trans = trans, guide = guide) } @@ -46,10 +47,11 @@ scale_linewidth <- scale_linewidth_continuous #' @rdname scale_linewidth #' @export scale_linewidth_binned <- function(name = waiver(), breaks = waiver(), labels = waiver(), - limits = NULL, range = c(1, 6), n.breaks = NULL, + limits = NULL, range = NULL, n.breaks = NULL, nice.breaks = TRUE, transform = "identity", trans = deprecated(), guide = "bins") { - binned_scale("linewidth", palette = pal_rescale(range), name = name, + palette <- if (!is.null(range)) pal_rescale(range) else NULL + binned_scale("linewidth", palette = palette, name = name, breaks = breaks, labels = labels, limits = limits, transform = transform, trans = trans, n.breaks = n.breaks, nice.breaks = nice.breaks, guide = guide) @@ -68,32 +70,33 @@ scale_linewidth_discrete <- function(...) { #' @rdname scale_linewidth #' @export #' @usage NULL -scale_linewidth_ordinal <- function(name = waiver(), ..., range = c(2, 6)) { - force(range) - - discrete_scale( - "linewidth", name = name, - palette = function(n) seq(range[1], range[2], length.out = n), - ... - ) +scale_linewidth_ordinal <- function(name = waiver(), ..., range = NULL) { + palette <- if (!is.null(range)) { + function(n) seq(range[1], range[2], length.out = n) + } else { + NULL + } + discrete_scale("linewidth", name = name, palette = palette, ...) } #' @rdname scale_linewidth #' @export #' @usage NULL -scale_linewidth_datetime <- function(name = waiver(), ..., range = c(1, 6)) { +scale_linewidth_datetime <- function(name = waiver(), ..., range = NULL) { + palette <- if (!is.null(range)) pal_rescale(range) else NULL datetime_scale( "linewidth", transform = "time", name = name, - palette = pal_rescale(range), ... + palette = palette, ... ) } #' @rdname scale_linewidth #' @export #' @usage NULL -scale_linewidth_date <- function(name = waiver(), ..., range = c(1, 6)) { +scale_linewidth_date <- function(name = waiver(), ..., range = NULL) { + palette <- if (!is.null(range)) pal_rescale(range) else NULL datetime_scale( "linewidth", transform = "date", name = name, - palette = pal_rescale(range), ... + palette = palette, ... ) } diff --git a/R/scale-shape.R b/R/scale-shape.R index ecb8a2a2a1..effa0a0b2f 100644 --- a/R/scale-shape.R +++ b/R/scale-shape.R @@ -42,14 +42,16 @@ #' scale_shape_identity() + #' facet_wrap(~shape) + #' theme_void() -scale_shape <- function(name = waiver(), ..., solid = TRUE) { - discrete_scale("shape", name = name, palette = pal_shape(solid), ...) +scale_shape <- function(name = waiver(), ..., solid = NULL) { + palette <- if (!is.null(solid)) pal_shape(solid) else NULL + discrete_scale("shape", name = name, palette = palette, ...) } #' @rdname scale_shape #' @export scale_shape_binned <- function(name = waiver(), ..., solid = TRUE) { - binned_scale("shape", name = name, palette = pal_binned(pal_shape(solid)), ...) + palette <- if (!is.null(solid)) pal_binned(pal_shape(solid)) else NULL + binned_scale("shape", name = name, palette = palette, ...) } #' @rdname scale_shape diff --git a/R/scale-size.R b/R/scale-size.R index 33f14d4834..525f378e15 100644 --- a/R/scale-size.R +++ b/R/scale-size.R @@ -52,11 +52,12 @@ NULL #' @export #' @usage NULL scale_size_continuous <- function(name = waiver(), breaks = waiver(), labels = waiver(), - limits = NULL, range = c(1, 6), + limits = NULL, range = NULL, transform = "identity", trans = deprecated(), guide = "legend") { - continuous_scale("size", palette = pal_area(range), name = name, + palette <- if (!is.null(range)) pal_area(range) else NULL + continuous_scale("size", palette = palette, name = name, breaks = breaks, labels = labels, limits = limits, transform = transform, trans = trans, guide = guide) } @@ -79,10 +80,11 @@ scale_radius <- function(name = waiver(), breaks = waiver(), labels = waiver(), #' @rdname scale_size #' @export scale_size_binned <- function(name = waiver(), breaks = waiver(), labels = waiver(), - limits = NULL, range = c(1, 6), n.breaks = NULL, + limits = NULL, range = NULL, n.breaks = NULL, nice.breaks = TRUE, transform = "identity", trans = deprecated(), guide = "bins") { - binned_scale("size", palette = pal_area(range), name = name, + palette <- if (!is.null(range)) pal_area(range) else NULL + binned_scale("size", palette = palette, name = name, breaks = breaks, labels = labels, limits = limits, transform = transform, trans = trans, n.breaks = n.breaks, nice.breaks = nice.breaks, guide = guide) @@ -101,17 +103,13 @@ scale_size_discrete <- function(...) { #' @rdname scale_size #' @export #' @usage NULL -scale_size_ordinal <- function(name = waiver(), ..., range = c(2, 6)) { - force(range) - - discrete_scale( - "size", name = name, - palette = function(n) { - area <- seq(range[1] ^ 2, range[2] ^ 2, length.out = n) - sqrt(area) - }, - ... - ) +scale_size_ordinal <- function(name = waiver(), ..., range = NULL) { + palette <- if (!is.null(range)) { + function(n) sqrt(seq(range[1]^2, range[2]^2, length.out = n)) + } else { + NULL + } + discrete_scale("size", name = name, palette = palette, ...) } #' @inheritDotParams continuous_scale -aesthetics -scale_name -palette -rescaler -expand -position @@ -139,13 +137,15 @@ scale_size_binned_area <- function(name = waiver(), ..., max_size = 6) { #' @rdname scale_size #' @export #' @usage NULL -scale_size_datetime <- function(name = waiver(), ..., range = c(1, 6)) { - datetime_scale("size", "time", name = name, palette = pal_area(range), ...) +scale_size_datetime <- function(name = waiver(), ..., range = NULL) { + palette <- if (!is.null(range)) pal_area(range) else NULL + datetime_scale("size", "time", name = name, palette = palette, ...) } #' @rdname scale_size #' @export #' @usage NULL -scale_size_date <- function(name = waiver(), ..., range = c(1, 6)) { - datetime_scale("size", "date", name = name, palette = pal_area(range), ...) +scale_size_date <- function(name = waiver(), ..., range = NULL) { + palette <- if (!is.null(range)) pal_area(range) else NULL + datetime_scale("size", "date", name = name, palette = palette, ...) } diff --git a/R/scales-.R b/R/scales-.R index e62eb0e8cb..769613a2d8 100644 --- a/R/scales-.R +++ b/R/scales-.R @@ -168,6 +168,39 @@ ScalesList <- ggproto("ScalesList", NULL, scale_name <- paste("scale", aes, "continuous", sep = "_") self$add(find_global(scale_name, env, mode = "function")()) } + }, + + set_palettes = function(self, theme) { + for (scale in self$scales) { + if (!is.null(scale$palette)) { + next + } + + # Resolve palette theme setting for this scale + type <- if (scale$is_discrete()) "discrete" else "continuous" + elem <- paste0("palette.", scale$aesthetics, ".", type) + elem <- compact(lapply(elem, calc_element, theme))[1][[1]] + + # Resolve the palette itself + elem <- elem %||% fallback_palette(scale) + palette <- switch( + type, + discrete = as_discrete_pal(elem), + continuous = as_continuous_pal(elem) + ) + if (!is.function(palette)) { + cli::cli_warn( + "Failed to find palette for {.field {scale$aesthetics[1]}} scale." + ) + } + + # Set palette to scale + # Note: while direct assignment is not ideal, we've already cloned the + # scale at the beginning of the plot build method, so it doesn't affect + # other plots + scale$palette <- palette + invisible() + } } ) diff --git a/R/theme-elements.R b/R/theme-elements.R index fb3341bf49..c51134216f 100644 --- a/R/theme-elements.R +++ b/R/theme-elements.R @@ -666,6 +666,21 @@ el_def <- function(class = NULL, inherit = NULL, description = NULL) { plot.tag.location = el_def("character"), plot.margin = el_def(c("margin", "unit", "rel"), "margins"), + palette.colour.discrete = el_def(c("character", "function")), + palette.colour.continuous = el_def(c("character", "function")), + palette.fill.discrete = el_def(c("character", "function"), "palette.colour.discrete"), + palette.fill.continuous = el_def(c("character", "function"), "palette.colour.continuous"), + palette.alpha.discrete = el_def(c("character", "numeric", "integer", "function")), + palette.alpha.continuous = el_def(c("character", "numeric", "integer", "function")), + palette.linewidth.discrete = el_def(c("character", "numeric", "integer", "function")), + palette.linewidth.continuous = el_def(c("character", "numeric", "integer", "function")), + palette.size.discrete = el_def(c("character", "numeric", "integer", "function")), + palette.size.continuous = el_def(c("character", "numeric", "integer", "function")), + palette.shape.discrete = el_def(c("character", "numeric", "integer", "function")), + palette.shape.continuous = el_def(c("character", "numeric", "integer", "function")), + palette.linetype.discrete = el_def(c("character", "numeric", "integer", "function")), + palette.linetype.continuous = el_def(c("character", "numeric", "integer", "function")), + aspect.ratio = el_def(c("numeric", "integer")) ) diff --git a/R/utilities.R b/R/utilities.R index 039376f4df..cbb403da4e 100644 --- a/R/utilities.R +++ b/R/utilities.R @@ -781,6 +781,34 @@ as_unordered_factor <- function(x) { x } +fallback_palette <- function(scale) { + aes <- scale$aesthetics[1] + discrete <- scale$is_discrete() + if (discrete) { + pal <- switch( + aes, + colour = , fill = pal_hue(), + alpha = function(n) seq(0.1, 1, length.out = n), + linewidth = function(n) seq(2, 6, length.out = n), + linetype = pal_linetype(), + shape = pal_shape(), + size = function(n) sqrt(seq(4, 36, length.out = n)), + ggplot_global$theme_default[[paste0("palette.", aes, ".discrete")]] + ) + return(pal) + } + switch( + aes, + colour = , fill = pal_seq_gradient("#132B43", "#56B1F7"), + alpha = pal_rescale(c(0.1, 1)), + linewidth = pal_rescale(c(1, 6)), + linetype = pal_binned(pal_linetype()), + shape = pal_binned(pal_shape()), + size = pal_area(), + ggplot_global$theme_default[[paste0("palette.", aes, ".continuous")]] + ) +} + warn_dots_used <- function(env = caller_env(), call = caller_env()) { check_dots_used( env = env, call = call, @@ -793,6 +821,8 @@ warn_dots_used <- function(env = caller_env(), call = caller_env()) { ) } +# TODO: delete shims when {scales} releases >1.3.0.9000 +# and bump {scales} version requirements # Shim for scales/#424 col_mix <- function(a, b, amount = 0.5) { input <- vec_recycle_common(a = a, b = b, amount = amount) @@ -805,10 +835,40 @@ col_mix <- function(a, b, amount = 0.5) { ) } +# Shim for scales/#427 +as_discrete_pal <- function(x, ...) { + if (is.function(x)) { + return(x) + } + pal_manual(x) +} + +# Shim for scales/#427 +as_continuous_pal <- function(x, ...) { + if (is.function(x)) { + return(x) + } + is_color <- grepl("^#(([[:xdigit:]]{2}){3,4}|([[:xdigit:]]){3,4})$", x) | + x %in% grDevices::colours() + if (all(is_color)) { + colour_ramp(x) + } else { + approxfun(seq(0, 1, length.out = length(x)), x) + } +} + +# Replace shims by actual scales function when available on_load({ - if ("col_mix" %in% getNamespaceExports("scales")) { + nse <- getNamespaceExports("scales") + if ("col_mix" %in% nse) { col_mix <- scales::col_mix } + if ("as_discrete_pal" %in% nse) { + as_discrete_pal <- scales::as_discrete_pal + } + if ("as_continuous_pal" %in% nse) { + as_continuous_pal <- scales::as_continuous_pal + } }) # TODO: Replace me if rlang/#1730 gets implemented diff --git a/man/geom_text.Rd b/man/geom_text.Rd index 1b06e73ae1..e88e45a0e1 100644 --- a/man/geom_text.Rd +++ b/man/geom_text.Rd @@ -246,7 +246,7 @@ p + # Add aesthetic mappings p + geom_text(aes(colour = factor(cyl))) p + geom_text(aes(colour = factor(cyl))) + - scale_colour_discrete(l = 40) + scale_colour_hue(l = 40) p + geom_label(aes(fill = factor(cyl)), colour = "white", fontface = "bold") # Scale size of text, and change legend key glyph from a to point diff --git a/man/scale_alpha.Rd b/man/scale_alpha.Rd index 28defef0de..6833a08002 100644 --- a/man/scale_alpha.Rd +++ b/man/scale_alpha.Rd @@ -10,15 +10,15 @@ \alias{scale_alpha_date} \title{Alpha transparency scales} \usage{ -scale_alpha(name = waiver(), ..., range = c(0.1, 1)) +scale_alpha(name = waiver(), ..., range = NULL) -scale_alpha_continuous(name = waiver(), ..., range = c(0.1, 1)) +scale_alpha_continuous(name = waiver(), ..., range = NULL) -scale_alpha_binned(name = waiver(), ..., range = c(0.1, 1)) +scale_alpha_binned(name = waiver(), ..., range = NULL) scale_alpha_discrete(...) -scale_alpha_ordinal(name = waiver(), ..., range = c(0.1, 1)) +scale_alpha_ordinal(name = waiver(), ..., range = NULL) } \arguments{ \item{name}{The name of the scale. Used as the axis or legend title. If diff --git a/man/scale_colour_continuous.Rd b/man/scale_colour_continuous.Rd index 36f3427746..d88a74f399 100644 --- a/man/scale_colour_continuous.Rd +++ b/man/scale_colour_continuous.Rd @@ -9,17 +9,48 @@ \alias{scale_color_binned} \title{Continuous and binned colour scales} \usage{ -scale_colour_continuous(..., type = getOption("ggplot2.continuous.colour")) - -scale_fill_continuous(..., type = getOption("ggplot2.continuous.fill")) - -scale_colour_binned(..., type = getOption("ggplot2.binned.colour")) - -scale_fill_binned(..., type = getOption("ggplot2.binned.fill")) +scale_colour_continuous( + ..., + aesthetics = "colour", + guide = "colourbar", + na.value = "grey50", + type = getOption("ggplot2.continuous.colour") +) + +scale_fill_continuous( + ..., + aesthetics = "fill", + guide = "colourbar", + na.value = "grey50", + type = getOption("ggplot2.continuous.fill") +) + +scale_colour_binned( + ..., + aesthetics = "colour", + guide = "coloursteps", + na.value = "grey50", + type = getOption("ggplot2.binned.colour") +) + +scale_fill_binned( + ..., + aesthetics = "fill", + guide = "coloursteps", + na.value = "grey50", + type = getOption("ggplot2.binned.fill") +) } \arguments{ \item{...}{Additional parameters passed on to the scale type} +\item{aesthetics}{The names of the aesthetics that this scale works with.} + +\item{guide}{A function used to create a guide or its name. See +\code{\link[=guides]{guides()}} for more information.} + +\item{na.value}{Missing values will be replaced with this value.} + \item{type}{One of the following: \itemize{ \item "gradient" (the default) diff --git a/man/scale_colour_discrete.Rd b/man/scale_colour_discrete.Rd index 0c7883fb6e..ff8fe3f9e7 100644 --- a/man/scale_colour_discrete.Rd +++ b/man/scale_colour_discrete.Rd @@ -6,13 +6,29 @@ \alias{scale_color_discrete} \title{Discrete colour scales} \usage{ -scale_colour_discrete(..., type = getOption("ggplot2.discrete.colour")) +scale_colour_discrete( + ..., + aesthetics = "colour", + na.value = "grey50", + type = getOption("ggplot2.discrete.colour") +) -scale_fill_discrete(..., type = getOption("ggplot2.discrete.fill")) +scale_fill_discrete( + ..., + aesthetics = "fill", + na.value = "grey50", + type = getOption("ggplot2.discrete.fill") +) } \arguments{ \item{...}{Additional parameters passed on to the scale type,} +\item{aesthetics}{The names of the aesthetics that this scale works with.} + +\item{na.value}{If \code{na.translate = TRUE}, what aesthetic value should the +missing values be displayed as? Does not apply to position scales +where \code{NA} is always placed at the far right.} + \item{type}{One of the following: \itemize{ \item A character vector of color codes. The codes are used for a 'manual' color diff --git a/man/scale_linewidth.Rd b/man/scale_linewidth.Rd index 275f860582..5c9a842da9 100644 --- a/man/scale_linewidth.Rd +++ b/man/scale_linewidth.Rd @@ -15,7 +15,7 @@ scale_linewidth( breaks = waiver(), labels = waiver(), limits = NULL, - range = c(1, 6), + range = NULL, transform = "identity", trans = deprecated(), guide = "legend" @@ -26,7 +26,7 @@ scale_linewidth_binned( breaks = waiver(), labels = waiver(), limits = NULL, - range = c(1, 6), + range = NULL, n.breaks = NULL, nice.breaks = TRUE, transform = "identity", diff --git a/man/scale_shape.Rd b/man/scale_shape.Rd index 8c8b8320fc..85b991fd9a 100644 --- a/man/scale_shape.Rd +++ b/man/scale_shape.Rd @@ -8,7 +8,7 @@ \alias{scale_shape_continuous} \title{Scales for shapes, aka glyphs} \usage{ -scale_shape(name = waiver(), ..., solid = TRUE) +scale_shape(name = waiver(), ..., solid = NULL) scale_shape_binned(name = waiver(), ..., solid = TRUE) } diff --git a/man/scale_size.Rd b/man/scale_size.Rd index 753ecfa790..2ba6a1e295 100644 --- a/man/scale_size.Rd +++ b/man/scale_size.Rd @@ -18,7 +18,7 @@ scale_size( breaks = waiver(), labels = waiver(), limits = NULL, - range = c(1, 6), + range = NULL, transform = "identity", trans = deprecated(), guide = "legend" @@ -40,7 +40,7 @@ scale_size_binned( breaks = waiver(), labels = waiver(), limits = NULL, - range = c(1, 6), + range = NULL, n.breaks = NULL, nice.breaks = TRUE, transform = "identity", diff --git a/tests/testthat/_snaps/scale-colour-continuous.md b/tests/testthat/_snaps/scale-colour-continuous.md index a5410a8799..14d6e6a95d 100644 --- a/tests/testthat/_snaps/scale-colour-continuous.md +++ b/tests/testthat/_snaps/scale-colour-continuous.md @@ -21,10 +21,8 @@ --- Unknown scale type: "abc" - i Use either "gradient" or "viridis". --- Unknown scale type: "abc" - i Use either "gradient" or "viridis". diff --git a/tests/testthat/_snaps/scale-discrete.md b/tests/testthat/_snaps/scale-discrete.md index a3251c4c4e..6a49fa9fdf 100644 --- a/tests/testthat/_snaps/scale-discrete.md +++ b/tests/testthat/_snaps/scale-discrete.md @@ -3,7 +3,7 @@ Code scale_colour_discrete(type = scale_colour_gradient) Condition - Error in `scale_colour_discrete()`: + Error in `scale_backward_compatibility()`: ! The `type` argument must return a discrete scale for the colour aesthetic. x The provided scale is continuous. @@ -12,7 +12,7 @@ Code scale_fill_discrete(type = scale_fill_gradient) Condition - Error in `scale_fill_discrete()`: + Error in `scale_backward_compatibility()`: ! The `type` argument must return a discrete scale for the fill aesthetic. x The provided scale is continuous. @@ -21,7 +21,7 @@ Code scale_colour_discrete(type = scale_fill_hue) Condition - Error in `scale_colour_discrete()`: + Error in `scale_backward_compatibility()`: ! The `type` argument must return a continuous scale for the colour aesthetic. x The provided scale works with the following aesthetics: fill. @@ -30,7 +30,7 @@ Code scale_fill_discrete(type = scale_colour_hue) Condition - Error in `scale_fill_discrete()`: + Error in `scale_backward_compatibility()`: ! The `type` argument must return a continuous scale for the fill aesthetic. x The provided scale works with the following aesthetics: colour. diff --git a/tests/testthat/test-guide-colorbar.R b/tests/testthat/test-guide-colorbar.R index 9f44adb371..7cfd96a2f1 100644 --- a/tests/testthat/test-guide-colorbar.R +++ b/tests/testthat/test-guide-colorbar.R @@ -2,7 +2,7 @@ skip_on_cran() # This test suite is long-running (on cran) and is skipped test_that("colourbar trains without labels", { g <- guide_colorbar() - sc <- scale_colour_continuous(limits = c(0, 4), labels = NULL) + sc <- scale_colour_gradient(limits = c(0, 4), labels = NULL) out <- g$train(scale = sc) expect_named(out$key, c("colour", ".value")) diff --git a/tests/testthat/test-guide-legend.R b/tests/testthat/test-guide-legend.R index ff79bd7aa7..d4a47c145e 100644 --- a/tests/testthat/test-guide-legend.R +++ b/tests/testthat/test-guide-legend.R @@ -38,6 +38,7 @@ test_that("guide merging for guide_legend() works as expected", { scales <- scales_list() scales$add(scale1) scales$add(scale2) + scales$set_palettes(NULL) scales <- scales$scales aesthetics <- lapply(scales, `[[`, "aesthetics") @@ -52,34 +53,34 @@ test_that("guide merging for guide_legend() works as expected", { } different_limits <- merge_test_guides( - scale_colour_discrete(limits = c("a", "b", "c", "d")), + scale_colour_hue(limits = c("a", "b", "c", "d")), scale_linetype_discrete(limits = c("a", "b", "c")) ) expect_length(different_limits, 2) same_limits <- merge_test_guides( - scale_colour_discrete(limits = c("a", "b", "c")), + scale_colour_hue(limits = c("a", "b", "c")), scale_linetype_discrete(limits = c("a", "b", "c")) ) expect_length(same_limits, 1) expect_equal(same_limits[[1]]$key$.label, c("a", "b", "c")) same_labels_different_limits <- merge_test_guides( - scale_colour_discrete(limits = c("a", "b", "c")), + scale_colour_hue(limits = c("a", "b", "c")), scale_linetype_discrete(limits = c("one", "two", "three"), labels = c("a", "b", "c")) ) expect_length(same_labels_different_limits, 1) expect_equal(same_labels_different_limits[[1]]$key$.label, c("a", "b", "c")) same_labels_different_scale <- merge_test_guides( - scale_colour_continuous(limits = c(0, 4), breaks = 1:3, labels = c("a", "b", "c")), + scale_colour_gradient(limits = c(0, 4), breaks = 1:3, labels = c("a", "b", "c")), scale_linetype_discrete(limits = c("a", "b", "c")) ) expect_length(same_labels_different_scale, 1) expect_equal(same_labels_different_scale[[1]]$key$.label, c("a", "b", "c")) repeated_identical_labels <- merge_test_guides( - scale_colour_discrete(limits = c("one", "two", "three"), labels = c("label1", "label1", "label2")), + scale_colour_hue(limits = c("one", "two", "three"), labels = c("label1", "label1", "label2")), scale_linetype_discrete(limits = c("1", "2", "3"), labels = c("label1", "label1", "label2")) ) expect_length(repeated_identical_labels, 1) @@ -99,7 +100,7 @@ test_that("size = NA doesn't throw rendering errors", { test_that("legend reverse argument reverses the key", { - scale <- scale_colour_discrete() + scale <- scale_colour_hue() scale$train(LETTERS[1:4]) guides <- guides_list(NULL) diff --git a/tests/testthat/test-scales.R b/tests/testthat/test-scales.R index d9286b513f..514cb392a3 100644 --- a/tests/testthat/test-scales.R +++ b/tests/testthat/test-scales.R @@ -734,6 +734,30 @@ test_that("continuous scales warn about faulty `limits`", { expect_snapshot(scale_x_continuous(limits = 1:3), error = TRUE) }) +test_that("populating palettes works", { + + scl <- scales_list() + scl$add(scale_colour_discrete(aesthetics = c("colour", "fill"))) + + my_theme <- theme( + palette.colour.discrete = c("white", "black"), + palette.fill.discrete = c("red", "blue") + ) + + scl$set_palettes(my_theme) + expect_equal(scl$scales[[1]]$palette(2), c("white", "black")) + + # Scales with >1 aesthetic + scl <- scales_list() + scl$add(scale_colour_discrete(aesthetics = c("colour", "fill"))) + + my_theme$palette.colour.discrete <- NULL + + scl$set_palettes(my_theme) + expect_equal(scl$scales[[1]]$palette(2), c("red", "blue")) + +}) + test_that("discrete scales work with NAs in arbitrary positions", { # Prevents intermediate caching of palettes map <- function(x, limits) {