diff --git a/NEWS.md b/NEWS.md index 4a1bede8..4a6804e6 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,7 @@ # scales (development version) +* Range training now supports custom classes. Continuous classes require a + `range()` method that returns numeric values. Discrete classes require a + `levels()` method (and optionally a `droplevels()` method) (#480). * New `label_glue()` labelling function for interpolated strings (#457). * `fullseq()` and by extension `breaks_width()` can now deal with unsorted ranges (#435). diff --git a/R/scale-continuous.R b/R/scale-continuous.R index d2ccd0a0..30673c94 100644 --- a/R/scale-continuous.R +++ b/R/scale-continuous.R @@ -41,6 +41,11 @@ train_continuous <- function(new, existing = NULL, call = caller_env()) { return(existing) } + new <- try_fetch( + range(new, na.rm = TRUE), + error = function(cnd) new + ) + if (is.factor(new) || !typeof(new) %in% c("integer", "double")) { example <- unique(new) example <- example[seq_len(pmin(length(example), 5))] diff --git a/R/scale-discrete.R b/R/scale-discrete.R index 8957583a..35ec3c8b 100644 --- a/R/scale-discrete.R +++ b/R/scale-discrete.R @@ -15,7 +15,7 @@ dscale <- function(x, palette, na.value = NA) { } is.discrete <- function(x) { - is.factor(x) || is.character(x) || is.logical(x) + !is.null(levels(x)) || is.character(x) || is.logical(x) } #' Train (update) a discrete scale @@ -90,8 +90,8 @@ discrete_range <- function(old, new, drop = FALSE, na.rm = FALSE, fct = NA) { clevels <- function(x, drop = FALSE, na.rm = FALSE) { if (is.null(x)) { character() - } else if (is.factor(x)) { - if (drop) x <- factor(x) + } else if (!is.null(levels(x))) { + if (drop) x <- droplevels(x) values <- levels(x) if (na.rm) { diff --git a/tests/testthat/_snaps/scale-continuous.md b/tests/testthat/_snaps/scale-continuous.md index a3d4dcf6..f237c0d6 100644 --- a/tests/testthat/_snaps/scale-continuous.md +++ b/tests/testthat/_snaps/scale-continuous.md @@ -5,5 +5,5 @@ Condition Error: ! Discrete value supplied to a continuous scale. - i Example values: "A", "B", "C", "D", and "E". + i Example values: "A" and "E". diff --git a/tests/testthat/test-scale-continuous.R b/tests/testthat/test-scale-continuous.R index 0682c061..45a99af9 100644 --- a/tests/testthat/test-scale-continuous.R +++ b/tests/testthat/test-scale-continuous.R @@ -34,3 +34,13 @@ test_that("train_continuous works with integer64", { c(1, 10) ) }) + +test_that("train_continuous can train on S3 classes", { + my_obj <- structure(c("IX", "CM", "X", "IV"), class = "bar") + range.bar <- function(x, ...) range(.romans[x], ...) + registerS3method("range", "bar", method = range.bar) + expect_equal( + train_continuous(my_obj), + c(4, 900) + ) +}) diff --git a/tests/testthat/test-scale-discrete.R b/tests/testthat/test-scale-discrete.R index c158f794..23f374e9 100644 --- a/tests/testthat/test-scale-discrete.R +++ b/tests/testthat/test-scale-discrete.R @@ -27,3 +27,13 @@ test_that("na.rm = TRUE drops NA", { expect_equal(train_discrete(x2, na.rm = TRUE), "a") expect_equal(train_discrete(x3, na.rm = TRUE), "a") }) + +test_that("discrete ranges can be trained on S3 classes", { + my_obj <- structure(list("A", c("B", "C")), class = "foo") + levels.foo <- function(x) unique(unlist(x)) + registerS3method("levels", "foo", method = levels.foo) + expect_equal( + train_discrete(my_obj), + LETTERS[1:3] + ) +})