diff --git a/DESCRIPTION b/DESCRIPTION index f4b63c3a9f..279469d660 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -226,6 +226,7 @@ Collate: 'scale-linetype.R' 'scale-linewidth.R' 'scale-manual.R' + 'scale-partial.R' 'scale-shape.R' 'scale-size.R' 'scale-steps.R' diff --git a/NAMESPACE b/NAMESPACE index eb67c79182..a70f6511ce 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -238,6 +238,7 @@ export(ScaleContinuousPosition) export(ScaleDiscrete) export(ScaleDiscreteIdentity) export(ScaleDiscretePosition) +export(ScalePartial) export(Stat) export(StatAlign) export(StatBin) @@ -578,6 +579,7 @@ export(scale_linewidth_discrete) export(scale_linewidth_identity) export(scale_linewidth_manual) export(scale_linewidth_ordinal) +export(scale_partial) export(scale_radius) export(scale_shape) export(scale_shape_binned) @@ -598,6 +600,7 @@ export(scale_size_identity) export(scale_size_manual) export(scale_size_ordinal) export(scale_type) +export(scale_x) export(scale_x_binned) export(scale_x_continuous) export(scale_x_date) @@ -607,6 +610,7 @@ export(scale_x_log10) export(scale_x_reverse) export(scale_x_sqrt) export(scale_x_time) +export(scale_y) export(scale_y_binned) export(scale_y_continuous) export(scale_y_date) diff --git a/R/layout.R b/R/layout.R index 6e2124e8be..dac59310ca 100644 --- a/R/layout.R +++ b/R/layout.R @@ -129,6 +129,10 @@ Layout <- ggproto("Layout", NULL, train_position = function(self, data, x_scale, y_scale) { # Initialise scales if needed, and possible. layout <- self$layout + + x_scale <- resolve_partial(x_scale) + y_scale <- resolve_partial(y_scale) + if (is.null(self$panel_scales_x)) { self$panel_scales_x <- self$facet$init_scales(layout, x_scale = x_scale, params = self$facet_params)$x diff --git a/R/limits.R b/R/limits.R index 727df98326..843b0e0fbd 100644 --- a/R/limits.R +++ b/R/limits.R @@ -90,13 +90,13 @@ lims <- function(...) { #' @export #' @rdname lims xlim <- function(...) { - limits(c(...), "x", call = current_call()) + scale_x(limits = c(...), call = current_call()) } #' @export #' @rdname lims ylim <- function(...) { - limits(c(...), "y", call = current_call()) + scale_y(limits = c(...), call = current_call()) } #' Generate correct scale type for specified limits diff --git a/R/scale-.R b/R/scale-.R index eb4248048d..e26775585d 100644 --- a/R/scale-.R +++ b/R/scale-.R @@ -409,6 +409,9 @@ binned_scale <- function(aesthetics, scale_name = deprecated(), palette, name = #' - `make_sec_title()` Hook to modify the title for the second axis that is calculated #' when the `Layout` calculates the x and y labels. #' +#' - `fields` A character vector naming parameters that can be updated by +#' partial scales. +#' #' @rdname ggplot2-ggproto #' @format NULL #' @usage NULL @@ -558,6 +561,47 @@ Scale <- ggproto("Scale", NULL, make_sec_title = function(title) { title + }, + + fields = character(0), + + validate = function(self, fields = self$fields) { + if (any(c("breaks", "labels") %in% fields)) { + check_breaks_labels(self$breaks, self$labels, call = self$call) + } + return() + }, + + update_params = function(self, params, default = FALSE, call = NULL) { + + fields <- intersect(self$fields, names(params)) + extra <- setdiff(names(params), fields) + + if (length(extra) > 0) { + cli::cli_warn( + "Ignoring unknown scale parameter{?s}: {.and {.field {extra}}}.", + call = call + ) + } + + if (!default) { + # Don't update fields that were already defined in non-default scale + fields <- setdiff(fields, call_args_names(self$call)) + } else { + self$call <- call + } + + if (length(fields) < 1) { + # Nothing to update here + return() + } + + # Update parameters + for (field in fields) { + self[[field]] <- params[[field]] + } + self$validate(fields) + return() } ) @@ -841,6 +885,59 @@ ScaleContinuous <- ggproto("ScaleContinuous", Scale, ) }, + fields = setdiff( + fn_fmls_names(continuous_scale), + c("aesthetics", "scale_name", "super", "call", "position") + ), + + update_params = function(self, params, default = FALSE, call = NULL) { + + if ("trans" %in% names(params)) { + # We're using the old transform to revert the limits to input data, so + # that new transform returns valid limits. + if (!is.null(self$limits) && !is.function(self$limits)) { + self$limits <- self$trans$inverse(self$limits) + } + } + ggproto_parent(Scale, self)$update_params(params, default = default, call = call) + return() + }, + + validate = function(self, fields = self$fields) { + + ggproto_parent(Scale, self)$validate(fields) + + limits <- self$limits + if ("trans" %in% fields) { + self$trans <- as.trans(self$trans) + if (!is.null(limits) && !is.function(limits)) { + self$limits <- self$trans$transform(limits) + } + } + if ("limits" %in% fields && !is.null(limits) && !is.function(limits)) { + if (is.discrete(limits)) { + cli::cli_abort( + "Discrete limits supplied to continuous scale.", + call = self$call + ) + } + if (length(limits) != 2 || !vec_is(limits)) { + cli::cli_abort( + "{.arg limits} must a vector of length 2.", + call = self$call + ) + } + self$limits <- self$trans$transform(limits) + } + if ("rescaler" %in% fields) { + check_function(self$rescaler, call = self$call, arg = "rescaler") + } + if ("oob" %in% fields) { + check_function(self$oob, call = self$call, arg = "oob") + } + return() + }, + print = function(self, ...) { show_range <- function(x) paste0(formatC(x, digits = 3), collapse = " -- ") @@ -1040,7 +1137,24 @@ ScaleDiscrete <- ggproto("ScaleDiscrete", Scale, major_source = major, minor_source = NULL ) - } + }, + + fields = setdiff( + fn_fmls_names(discrete_scale), + c("aesthetics", "scale_name", "super", "call", "position") + ), + + validate = function(self, fields = self$fields) { + + ggproto_parent(Scale, self)$validate(fields) + + if ("limits" %in% fields) { + if (!is.discrete(self$limits)) { + cli::cli_abort("Continuous limits supplied to discrete scale.") + } + } + return() + }, ) #' @rdname ggplot2-ggproto @@ -1271,7 +1385,12 @@ ScaleBinned <- ggproto("ScaleBinned", Scale, list(range = range, labels = labels, major = pal, minor = NULL, major_source = major, minor_source = NULL) - } + }, + + fields = setdiff( + fn_fmls_names(binned_scale), + c("aesthetics", "scale_name", "super", "call", "position") + ) ) # In place modification of a scale to change the primary axis diff --git a/R/scale-continuous.R b/R/scale-continuous.R index 73b026a678..7c794600f2 100644 --- a/R/scale-continuous.R +++ b/R/scale-continuous.R @@ -161,7 +161,8 @@ ScaleContinuousPosition <- ggproto("ScaleContinuousPosition", ScaleContinuous, } else { ggproto_parent(ScaleContinuous, self)$make_sec_title(title) } - } + }, + fields = union(setdiff(ScaleContinuous$fields, "palette"), "position") ) # Transformed scales --------------------------------------------------------- diff --git a/R/scale-discrete-.R b/R/scale-discrete-.R index 339df10122..50dfadf440 100644 --- a/R/scale-discrete-.R +++ b/R/scale-discrete-.R @@ -140,7 +140,9 @@ ScaleDiscretePosition <- ggproto("ScaleDiscretePosition", ScaleDiscrete, new$range <- DiscreteRange$new() new$range_c <- ContinuousRange$new() new - } + }, + + fields = union(setdiff(ScaleDiscrete$fields, "palette"), "position") ) # Can't use vctrs - vctrs is too restrictive for mapped_discrete diff --git a/R/scale-partial.R b/R/scale-partial.R new file mode 100644 index 0000000000..6feac7629c --- /dev/null +++ b/R/scale-partial.R @@ -0,0 +1,103 @@ +#' Partial scales +#' +#' Partial scales are useful for setting some scale parameters without +#' committing to any particular scale yet. Partial scales can be added to a plot +#' and will be combined with full scales. +#' +#' @param aesthetic A string specifying an aesthetic to create a partial +#' scale for. +#' @param ... Arguments passed onto full scales. +#' @param call A `` to report in warning and error messages. +#' +#' @return A `` object that can be added to a plot. +#' @export +#' @name partial-scales +#' +#' @examples +#' # A standard plot +#' p <- ggplot(mpg, aes(displ, hwy)) + +#' geom_point() +#' +#' # Adding a partial scale +#' p + scale_y(trans = "sqrt") +#' +#' # Partial scales can be stacked +#' p + scale_y(trans = "sqrt") + scale_y(breaks = seq(15, 45, by = 5)) +#' +#' # When two scales declare the same parameter, the latter overrules the first +#' p + scale_y_continuous(name = "Highway Miles") + +#' scale_y(name = "Title from partial scale") +#' +#' # But other parameters are kept and not overruled +#' p + scale_y(name = "Highway Miles", +#' breaks = c(20, 30, 40), +#' labels = c("A", "B", "C")) + +#' scale_y_continuous(name = "Title from full scale") +scale_partial <- function(aesthetic, ..., call = caller_call()) { + + check_string(aesthetic, allow_empty = FALSE) + aesthetic <- standardise_aes_names(aesthetic) + + args <- dots_list(..., .homonyms = "error") + if (!is_named(args)) { + cli::cli_abort("All arguments in {.code ...} must be named.", call = call) + } + + args <- args[!vapply(args, is.waive, logical(1))] + + lambdas <- intersect( + names(args), + c("limits", "breaks", "labels", "rescaler", "oob", "minor_breaks") + ) + args[lambdas] <- lapply(args[lambdas], allow_lambda) + + call <- call %||% current_call() + + ggproto( + NULL, ScalePartial, + call = call, + aesthetics = aesthetic, + params = args + ) +} + +#' @export +#' @rdname partial-scales +scale_x <- function(...) scale_partial(aesthetic = "x", ...) +#' @export +#' @rdname partial-scales +scale_y <- function(...) scale_partial(aesthetic = "y", ...) + +#' @rdname ggplot2-ggproto +#' @format NULL +#' @usage NULL +#' @export +ScalePartial <- ggproto( + "ScalePartial", Scale, + + aesthetics = character(), + params = list(), + call = NULL, + + update_params = function(self, params, default = FALSE, call = self$call) { + self$params <- defaults(params, self$params) + }, + + clone = function(self) { + ggproto(NULL, self) + }, + + reset = function(self) NULL +) + +resolve_partial <- function(scale) { + if (!inherits(scale, "ScalePartial")) { + return(scale) + } + if (is.null(scale$params$limits)) { + return(NULL) + } + new <- limits(scale$params$limits, scale$aesthetics[1]) + new$update_params(scale$params, default = TRUE) + new +} diff --git a/R/scales-.R b/R/scales-.R index 73c490c8a2..a25b7498f3 100644 --- a/R/scales-.R +++ b/R/scales-.R @@ -17,22 +17,48 @@ ScalesList <- ggproto("ScalesList", NULL, any(self$find(aesthetic)) }, - add = function(self, scale) { + add = function(self, scale, default = FALSE) { if (is.null(scale)) { return() } prev_aes <- self$find(scale$aesthetics) - if (any(prev_aes)) { - # Get only the first aesthetic name in the returned vector -- it can - # sometimes be c("x", "xmin", "xmax", ....) - scalename <- self$scales[prev_aes][[1]]$aesthetics[1] - cli::cli_inform(c( - "Scale for {.field {scalename}} is already present.", - "Adding another scale for {.field {scalename}}, which will replace the existing scale." - )) + if (!any(prev_aes)) { + self$scales <- c(self$scales, list(scale)) + return() + } + + prev_scale <- self$scales[prev_aes][[1]] + + if (inherits(scale, "ScalePartial")) { + # Clone scale to avoid state changes + prev_scale <- prev_scale$clone() + # `default = TRUE` here because upon adding a partial scale, + # we need to override parameters, regardless of whether `prev_scale` is + # a full or partial scale. + prev_scale$update_params(scale$params, default = TRUE, call = scale$call) + self$scales <- c(self$scales[!prev_aes], list(prev_scale)) + return() } + if (inherits(prev_scale, "ScalePartial")) { + if (!default) { + scale <- scale$clone() + } + scale$update_params(prev_scale$params, default = default, + call = prev_scale$call) + self$scales <- c(self$scales[!prev_aes], list(scale)) + return() + } + + # Get only the first aesthetic name in the returned vector -- it can + # sometimes be c("x", "xmin", "xmax", ....) + scalename <- self$scales[prev_aes][[1]]$aesthetics[1] + cli::cli_inform(c( + "Scale for {.field {scalename}} is already present.", + "Adding another scale for {.field {scalename}}, which will replace the existing scale." + )) + # Remove old scale for this aesthetic (if it exists) self$scales <- c(self$scales[!prev_aes], list(scale)) }, @@ -42,7 +68,8 @@ ScalesList <- ggproto("ScalesList", NULL, }, input = function(self) { - unlist(lapply(self$scales, "[[", "aesthetics")) + idx <- !vapply(self$scales, inherits, logical(1), what = "ScalePartial") + unlist(lapply(self$scales[idx], "[[", "aesthetics")) }, # This actually makes a descendant of self, which is functionally the same @@ -157,7 +184,7 @@ ScalesList <- ggproto("ScalesList", NULL, data_cols <- compact(data_cols) for (aes in names(data_cols)) { - self$add(find_scale(aes, data_cols[[aes]], env)) + self$add(find_scale(aes, data_cols[[aes]], env), default = TRUE) } }, @@ -168,7 +195,11 @@ ScalesList <- ggproto("ScalesList", NULL, for (aes in aesthetics) { scale_name <- paste("scale", aes, "continuous", sep = "_") - self$add(find_global(scale_name, env, mode = "function")()) + scale <- find_global(scale_name, env, mode = "function")() + if (!is.null(scale)) { + scale$call <- call(scale_name) + self$add(scale, default = TRUE) + } } } ) diff --git a/man/ggplot2-ggproto.Rd b/man/ggplot2-ggproto.Rd index 37a042dd68..38ce36601c 100644 --- a/man/ggplot2-ggproto.Rd +++ b/man/ggplot2-ggproto.Rd @@ -18,13 +18,14 @@ % R/position-dodge.R, R/position-dodge2.R, R/position-identity.R, % R/position-jitter.R, R/position-jitterdodge.R, R/position-nudge.R, % R/position-stack.R, R/scale-.R, R/scale-binned.R, R/scale-continuous.R, -% R/scale-date.R, R/scale-discrete-.R, R/scale-identity.R, R/stat-align.R, -% R/stat-bin.R, R/stat-bin2d.R, R/stat-bindot.R, R/stat-binhex.R, -% R/stat-boxplot.R, R/stat-contour.R, R/stat-count.R, R/stat-density-2d.R, -% R/stat-density.R, R/stat-ecdf.R, R/stat-ellipse.R, R/stat-function.R, -% R/stat-identity.R, R/stat-qq-line.R, R/stat-qq.R, R/stat-quantilemethods.R, -% R/stat-smooth.R, R/stat-sum.R, R/stat-summary-2d.R, R/stat-summary-bin.R, -% R/stat-summary-hex.R, R/stat-summary.R, R/stat-unique.R, R/stat-ydensity.R +% R/scale-date.R, R/scale-discrete-.R, R/scale-identity.R, +% R/scale-partial.R, R/stat-align.R, R/stat-bin.R, R/stat-bin2d.R, +% R/stat-bindot.R, R/stat-binhex.R, R/stat-boxplot.R, R/stat-contour.R, +% R/stat-count.R, R/stat-density-2d.R, R/stat-density.R, R/stat-ecdf.R, +% R/stat-ellipse.R, R/stat-function.R, R/stat-identity.R, R/stat-qq-line.R, +% R/stat-qq.R, R/stat-quantilemethods.R, R/stat-smooth.R, R/stat-sum.R, +% R/stat-summary-2d.R, R/stat-summary-bin.R, R/stat-summary-hex.R, +% R/stat-summary.R, R/stat-unique.R, R/stat-ydensity.R \docType{data} \name{ggplot2-ggproto} \alias{ggplot2-ggproto} @@ -116,6 +117,7 @@ \alias{ScaleDiscretePosition} \alias{ScaleDiscreteIdentity} \alias{ScaleContinuousIdentity} +\alias{ScalePartial} \alias{StatAlign} \alias{StatBin} \alias{StatBin2d} @@ -563,6 +565,8 @@ for discrete scales), \code{major} (the rescaled value of \code{major_source}, i \item \code{axis_order()} One of \code{c("primary", "secondary")} or \code{c("secondary", "primary")} \item \code{make_sec_title()} Hook to modify the title for the second axis that is calculated when the \code{Layout} calculates the x and y labels. +\item \code{fields} A character vector naming parameters that can be updated by +partial scales. } } diff --git a/man/partial-scales.Rd b/man/partial-scales.Rd new file mode 100644 index 0000000000..22a5c9dd01 --- /dev/null +++ b/man/partial-scales.Rd @@ -0,0 +1,52 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/scale-partial.R +\name{partial-scales} +\alias{partial-scales} +\alias{scale_partial} +\alias{scale_x} +\alias{scale_y} +\title{Partial scales} +\usage{ +scale_partial(aesthetic, ..., call = caller_call()) + +scale_x(...) + +scale_y(...) +} +\arguments{ +\item{aesthetic}{A string specifying an aesthetic to create a partial +scale for.} + +\item{...}{Arguments passed onto full scales.} + +\item{call}{A \verb{} to report in warning and error messages.} +} +\value{ +A \verb{} object that can be added to a plot. +} +\description{ +Partial scales are useful for setting some scale parameters without +committing to any particular scale yet. Partial scales can be added to a plot +and will be combined with full scales. +} +\examples{ +# A standard plot +p <- ggplot(mpg, aes(displ, hwy)) + + geom_point() + +# Adding a partial scale +p + scale_y(trans = "sqrt") + +# Partial scales can be stacked +p + scale_y(trans = "sqrt") + scale_y(breaks = seq(15, 45, by = 5)) + +# When two scales declare the same parameter, the latter overrules the first +p + scale_y_continuous(name = "Highway Miles") + + scale_y(name = "Title from partial scale") + +# But other parameters are kept and not overruled +p + scale_y(name = "Highway Miles", + breaks = c(20, 30, 40), + labels = c("A", "B", "C")) + + scale_y_continuous(name = "Title from full scale") +} diff --git a/tests/testthat/_snaps/coord-cartesian.md b/tests/testthat/_snaps/coord-cartesian.md index 7da67ba9c9..f5517f046d 100644 --- a/tests/testthat/_snaps/coord-cartesian.md +++ b/tests/testthat/_snaps/coord-cartesian.md @@ -1,6 +1,6 @@ # cartesian coords throws error when limits are badly specified - `xlim` must be a vector of length 2, not a object. + `xlim` must be a vector of length 2, not a object. --- diff --git a/tests/testthat/_snaps/coord-flip.md b/tests/testthat/_snaps/coord-flip.md index b7717a7381..cfc24bcbc7 100644 --- a/tests/testthat/_snaps/coord-flip.md +++ b/tests/testthat/_snaps/coord-flip.md @@ -1,6 +1,6 @@ # flip coords throws error when limits are badly specified - `xlim` must be a vector of length 2, not a object. + `xlim` must be a vector of length 2, not a object. --- diff --git a/tests/testthat/_snaps/coord-map.md b/tests/testthat/_snaps/coord-map.md index 2afa61e0a7..f418d31db2 100644 --- a/tests/testthat/_snaps/coord-map.md +++ b/tests/testthat/_snaps/coord-map.md @@ -1,6 +1,6 @@ # coord map throws error when limits are badly specified - `xlim` must be a vector of length 2, not a object. + `xlim` must be a vector of length 2, not a object. --- diff --git a/tests/testthat/_snaps/coord-transform.md b/tests/testthat/_snaps/coord-transform.md index 14be4bd125..f6923f792a 100644 --- a/tests/testthat/_snaps/coord-transform.md +++ b/tests/testthat/_snaps/coord-transform.md @@ -1,6 +1,6 @@ # coord_trans() throws error when limits are badly specified - `xlim` must be a vector of length 2, not a object. + `xlim` must be a vector of length 2, not a object. --- diff --git a/tests/testthat/_snaps/coord_sf.md b/tests/testthat/_snaps/coord_sf.md index c53025f074..3cb44dabc3 100644 --- a/tests/testthat/_snaps/coord_sf.md +++ b/tests/testthat/_snaps/coord_sf.md @@ -21,7 +21,7 @@ # coord_sf() throws error when limits are badly specified - `xlim` must be a vector of length 2, not a object. + `xlim` must be a vector of length 2, not a object. --- diff --git a/tests/testthat/_snaps/scale-partial.md b/tests/testthat/_snaps/scale-partial.md new file mode 100644 index 0000000000..139c52c6d2 --- /dev/null +++ b/tests/testthat/_snaps/scale-partial.md @@ -0,0 +1,5 @@ +# partial scale input is checked + + Arguments in `...` must have unique names. + x Multiple arguments named `limits` at positions 1 and 2. + diff --git a/tests/testthat/test-scale-partial.R b/tests/testthat/test-scale-partial.R new file mode 100644 index 0000000000..39be2b6b80 --- /dev/null +++ b/tests/testthat/test-scale-partial.R @@ -0,0 +1,148 @@ + +test_that("partial scales can be updated", { + + # Partial scales + xscale1 <- scale_x(name = "foobar", limits = c(0, 10)) + xscale2 <- scale_x(limits = c(0, 1)) + + plot1 <- ggplot() + xscale1 + + expect_s3_class( + plot1$scales$get_scales("x"), + "ScalePartial" + ) + expect_equal( + plot1$scales$get_scales("x")$params, + list(name = "foobar", limits = c(0, 10)) + ) + + # Update scale + plot2 <- plot1 + xscale2 + + # Check update was successful + expect_equal( + plot2$scales$get_scales("x")$params, + list(limits = c(0, 1), name = "foobar") + ) + # Check state of plot1 hasn't changed + expect_equal( + plot1$scales$get_scales("x")$params, + list(name = "foobar", limits = c(0, 10)) + ) + # Check state of xscale1 is unchanged + expect_equal(xscale1$params, list(name = "foobar", limits = c(0, 10))) + # Check state of xscale2 is unchanged + expect_equal(xscale2$params, list(limits = c(0, 1))) + + # Add default scales + plot2$scales$add_missing(c("x", "y"), env = current_env()) + + full <- plot2$scales$get_scales("x") + expect_s3_class(full, "ScaleContinuousPosition") + expect_equal(full$name, "foobar") + expect_equal(full$limits, c(0, 1)) +}) + +test_that("partial scale input is checked", { + + expect_error(scale_x(10), "must be named") + expect_snapshot_error( + scale_x(limits = c(0, 10), limits = c(1, 2)) + ) + + # Check for nonsense arguments + p <- ggplot() + scale_x(foo = "bar", limits = c(0, 1)) + + expect_warning( + p$scales$add_missing(c("x", "y"), env = current_env()), + "Ignoring unknown scale parameter" + ) + + # Check incompatible arguments + p <- ggplot() + scale_x(breaks = c(1, 2), labels = c("A", "B", "C")) + expect_error( + p$scales$add_missing(c("x", "y"), env = current_env()), + "must have the same length" + ) + + expect_error( + ggplot() + scale_x(limits = c(0, 10)) + scale_x_discrete(), + "Continuous limits supplied to discrete scale" + ) + expect_error( + ggplot() + scale_x(limits = c("A", "B")) + scale_x_continuous(), + "Discrete limits supplied to continuous scale" + ) + +}) + +test_that("scale override mechanics are correct", { + + # Full overrides partial + full <- scale_x_continuous(limits = c(0, 10)) + part <- scale_x(name = "foo", limits = c(0, 1)) + p1 <- ggplot() + part + full + + # Check inheritance is correct + resolved <- p1$scales$get_scales("x") + expect_equal(resolved$name, "foo") + expect_equal(resolved$limits, c(0, 10)) + + # Check state hasn't changed + expect_equal(full$name, waiver()) + expect_equal(full$limits, c(0, 10)) + expect_equal(part$params, list(name = "foo", limits = c(0, 1))) + + # Partial overrides full + full <- scale_x_continuous(name = "foo", limits = c(0, 10)) + part <- scale_x(limits = c(0, 1)) + p2 <- ggplot() + full + part + + # Check inheritance is correct + resolved <- p2$scales$get_scales("x") + expect_equal(resolved$name, "foo") + expect_equal(resolved$limits, c(0, 1)) + + # Check state hasn't changed + expect_equal(full$name, "foo") + expect_equal(full$limits, c(0, 10)) + expect_equal(part$params, list(limits = c(0, 1))) +}) + +test_that("limits are updated with transformations", { + + p1 <- ggplot() + scale_x_log10(limits = c(1, 100)) + + expect_equal(p1$scales$get_scales("x")$limits, c(0, 2)) + + p2 <- p1 + scale_x(trans = "sqrt") + + expect_equal(p2$scales$get_scales("x")$limits, c(1, 10)) + + p3 <- p2 + scale_x(trans = "identity") + + expect_equal(p3$scales$get_scales("x")$limits, c(1, 100)) + + p1 <- ggplot() + scale_x(limits = c(1, 100)) + + p2 <- p1 + scale_x_log10() + + expect_equal(p2$scales$get_scales("x")$limits, c(0, 2)) + +}) + +test_that("partial scales may be resolved", { + + s <- resolve_partial(scale_x()) + expect_null(s) + + s <- resolve_partial(scale_x(limits = LETTERS[1:3])) + expect_s3_class(s, "ScaleDiscrete") + + s <- resolve_partial(scale_x(limits = c(0, 10))) + expect_s3_class(s, "ScaleContinuous") + + s <- resolve_partial(scale_partial("colour", limits = c(0, 10))) + expect_equal(s$guide, "colourbar") + +}) diff --git a/tests/testthat/test-scales.R b/tests/testthat/test-scales.R index bedf7fb94b..9e28f61f9d 100644 --- a/tests/testthat/test-scales.R +++ b/tests/testthat/test-scales.R @@ -675,6 +675,13 @@ test_that("find_scale appends appropriate calls", { }) +test_that("scales generated by ScaleList$add_missing() have appropriate calls", { + sc <- scales_list() + sc$add_missing(c("x", "y"), current_env()) + expect_equal(as_label(sc$get_scales("x")$call), "scale_x_continuous()") + expect_equal(as_label(sc$get_scales("y")$call), "scale_y_continuous()") +}) + test_that("Using `scale_name` prompts deprecation message", { expect_snapshot_warning(continuous_scale("x", "foobar", identity_pal()))