diff --git a/DESCRIPTION b/DESCRIPTION index ae879c8d..99988d8c 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -33,8 +33,7 @@ Suggests: ggplot2, hms (>= 0.5.0), stringi, - testthat (>= 3.0.0), - waldo (>= 0.4.0) + testthat (>= 3.0.0) Config/Needs/website: tidyverse/tidytemplate Config/testthat/edition: 3 Encoding: UTF-8 diff --git a/NAMESPACE b/NAMESPACE index f6964735..221150ac 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -12,6 +12,7 @@ S3method(offset_by,numeric) S3method(plot,trans) S3method(print,trans) S3method(rescale,"NULL") +S3method(rescale,AsIs) S3method(rescale,Date) S3method(rescale,POSIXt) S3method(rescale,difftime) @@ -20,6 +21,7 @@ S3method(rescale,integer64) S3method(rescale,logical) S3method(rescale,numeric) S3method(rescale_mid,"NULL") +S3method(rescale_mid,AsIs) S3method(rescale_mid,Date) S3method(rescale_mid,POSIXt) S3method(rescale_mid,dist) @@ -35,12 +37,14 @@ export(abs_area) export(alpha) export(area_pal) export(as.trans) +export(asinh_trans) export(asn_trans) export(atanh_trans) export(boxcox_trans) export(breaks_extended) export(breaks_log) export(breaks_pretty) +export(breaks_timespan) export(breaks_width) export(brewer_pal) export(cbreaks) @@ -66,6 +70,7 @@ export(demo_datetime) export(demo_discrete) export(demo_log10) export(demo_time) +export(demo_timespan) export(dichromat_pal) export(discard) export(div_gradient_pal) @@ -86,6 +91,7 @@ export(identity_trans) export(is.trans) export(label_bytes) export(label_comma) +export(label_currency) export(label_date) export(label_date_short) export(label_dollar) @@ -100,6 +106,7 @@ export(label_percent) export(label_pvalue) export(label_scientific) export(label_time) +export(label_timespan) export(label_wrap) export(linetype_pal) export(log10_trans) @@ -157,6 +164,7 @@ export(squish) export(squish_infinite) export(time_format) export(time_trans) +export(timespan_trans) export(train_continuous) export(train_discrete) export(trans_breaks) diff --git a/NEWS.md b/NEWS.md index 34aab923..f4e9c49e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -8,12 +8,27 @@ vectors during training. Mixing of character and factor data will still lead to different results depending on the training order. * Add a rescale method for `difftime` objects (#382) +* `rescale(I(x), ...)` and `rescale_mid(I(x), ...)` return `I(x)` unaltered + (@teunbrand, #403). * The `scale_cut` argument in `number()` now works as advertised for values below the lowest cut value (#346) * Added a new option to the `style_positive` argument in `label_*()` functions. Setting this to `"space"` will add a figure space in front of the number to make it easier to align positive and negative values as figure space takes up the same amount of space as `-` (#366) +* `label_dollar()` has been superseeded by `label_currency()` for clarity (#344) +* `sqrt_trans()` no longer returns an inverse for values outside of its domain + (#214) +* Add better support for `difftime` objects. `label_timespan()` adds + functionality for adding correct unit suffix to timespan data, + `breaks_timespan()` adds functionality for finding pleasant breakpoints across + the various bases in time units, while `timespan_trans()` wraps it all + together and provides an alternative to `hms_trans()` (#212) +* Add an inverse (area) hyperbolic sine transformation `asinh_trans()`, which + provides a logarithm-like transformation of a space, but which accommodates + negative values (#297) +* Transformation objects can optionally include the derivatives of the transform + and the inverse transform (@mjskay, #322). # scales 1.2.1 diff --git a/R/bounds.R b/R/bounds.R index 20ebdf94..01160258 100644 --- a/R/bounds.R +++ b/R/bounds.R @@ -5,6 +5,9 @@ #' @param from input range (vector of length two). If not given, is #' calculated from the range of `x` #' @param ... other arguments passed on to methods +#' @details +#' Objects of class `` are returned unaltered. +#' #' @keywords manip #' @export #' @examples @@ -62,6 +65,9 @@ rescale.integer64 <- function(x, to = c(0, 1), from = range(x, na.rm = TRUE), .. #' @export rescale.difftime <- rescale.numeric +#' @rdname rescale +#' @export +rescale.AsIs <- function(x, to, from, ...) x #' Rescale vector to have specified minimum, midpoint, and maximum #' @@ -72,6 +78,8 @@ rescale.difftime <- rescale.numeric #' calculated from the range of `x` #' @param mid mid-point of input range #' @param ... other arguments passed on to methods +#' @details +#' Objects of class `` are returned unaltered. #' @examples #' rescale_mid(1:100, mid = 50.5) #' rescale_mid(runif(50), mid = 0.5) @@ -133,6 +141,10 @@ rescale_mid.integer64 <- function(x, to = c(0, 1), from = range(x, na.rm = TRUE) } +#' @rdname rescale_mid +#' @export +rescale_mid.AsIs <- function(x, to, from, ...) x + #' Rescale numeric vector to have specified maximum #' #' @export diff --git a/R/breaks.R b/R/breaks.R index 439580f7..947f3aa6 100644 --- a/R/breaks.R +++ b/R/breaks.R @@ -140,3 +140,45 @@ breaks_pretty <- function(n = 5, ...) { #' @export #' @inheritParams breaks_pretty pretty_breaks <- breaks_pretty + +#' Breaks for timespan data +#' +#' As timespan units span a variety of bases (1000 below seconds, 60 for second +#' and minutes, 24 for hours, and 7 for days), the range of the input data +#' determines the base used for calculating breaks +#' +#' @param unit The unit used to interpret numeric data input +#' @inheritParams breaks_extended +#' @export +#' @examples +#' demo_timespan(seq(0, 100), breaks = breaks_timespan()) +#' +breaks_timespan <- function(unit = c("secs", "mins", "hours", "days", "weeks"), n = 5) { + unit <- arg_match(unit) + force(n) + function(x) { + x <- as.numeric(as.difftime(x, units = unit), units = "secs") + rng <- range(x) + diff <- rng[2] - rng[1] + + if (diff <= 2 * 60) { + scale <- 1 + } else if (diff <= 2 * 3600) { + scale <- 60 + } else if (diff <= 2 * 86400) { + scale <- 3600 + } else if (diff <= 2 * 604800) { + scale <- 86400 + } else { + scale <- 604800 + } + + rng <- rng / scale + breaks <- labeling::extended( + rng[1], rng[2], n, + Q = c(1, 2, 1.5, 4, 3), + only.loose = FALSE + ) + as.difftime(breaks * scale, units = "secs") + } +} diff --git a/R/label-dollar.R b/R/label-currency.R similarity index 56% rename from R/label-dollar.R rename to R/label-currency.R index 25485eaa..d141670d 100644 --- a/R/label-dollar.R +++ b/R/label-currency.R @@ -1,59 +1,57 @@ -#' Label currencies ($100, $2.50, etc) +#' Label currencies ($100, €2.50, etc) #' -#' Format numbers as currency, rounding values to dollars or cents using -#' a convenient heuristic. +#' Format numbers as currency, rounding values to monetary or fractional +#' monetary using unit a convenient heuristic. #' #' @inherit label_number return params -#' @param accuracy,largest_with_cents Number to round to. If `NULL`, the default, -#' values will be rounded to the nearest integer, unless any of the -#' values has non-zero fractional component (e.g. cents) and the largest -#' value is less than `largest_with_cents` which by default is 100,000. +#' @param accuracy,largest_with_fractional Number to round +#' to. If `NULL`, the default, values will be rounded to the nearest integer, +#' unless any of the values has non-zero fractional component (e.g. cents) and +#' the largest value is less than `largest_with_fractional` which by default +#' is 100,000. #' @param prefix,suffix Symbols to display before and after value. -#' @param negative_parens `r lifecycle::badge("deprecated")` Use -#' `style_negative = "parens"` instead. #' @inheritDotParams number #' @export #' @family labels for continuous scales #' @examples -#' demo_continuous(c(0, 1), labels = label_dollar()) -#' demo_continuous(c(1, 100), labels = label_dollar()) +#' demo_continuous(c(0, 1), labels = label_currency()) +#' demo_continuous(c(1, 100), labels = label_currency()) #' #' # Customise currency display with prefix and suffix -#' demo_continuous(c(1, 100), labels = label_dollar(prefix = "USD ")) -#' euro <- label_dollar( -#' prefix = "", -#' suffix = "\u20ac", +#' demo_continuous(c(1, 100), labels = label_currency(prefix = "USD ")) +#' yen <- label_currency( +#' prefix = "¥", +#' suffix = "", #' big.mark = ".", #' decimal.mark = "," #' ) -#' demo_continuous(c(1000, 1100), labels = euro) +#' demo_continuous(c(1000, 1100), labels = yen) #' -#' # Use negative_parens = TRUE for finance style display -#' demo_continuous(c(-100, 100), labels = label_dollar(style_negative = "parens")) +#' # Use style_negative = "parens" for finance style display +#' demo_continuous(c(-100, 100), labels = label_currency(style_negative = "parens")) #' #' # Use scale_cut to use K/M/B where appropriate #' demo_log10(c(1, 1e16), #' breaks = log_breaks(7, 1e3), -#' labels = label_dollar(scale_cut = cut_short_scale()) +#' labels = label_currency(scale_cut = cut_short_scale()) #' ) #' # cut_short_scale() uses B = one thousand million #' # cut_long_scale() uses B = one million million #' demo_log10(c(1, 1e16), #' breaks = log_breaks(7, 1e3), -#' labels = label_dollar(scale_cut = cut_long_scale()) +#' labels = label_currency(scale_cut = cut_long_scale()) #' ) #' #' # You can also define your own breaks -#' gbp <- label_dollar( +#' gbp <- label_currency( #' prefix = "\u00a3", #' scale_cut = c(0, k = 1e3, m = 1e6, bn = 1e9, tn = 1e12) #' ) #' demo_log10(c(1, 1e12), breaks = log_breaks(5, 1e3), labels = gbp) -label_dollar <- function(accuracy = NULL, scale = 1, prefix = "$", - suffix = "", big.mark = ",", decimal.mark = ".", - trim = TRUE, largest_with_cents = 100000, - negative_parens = deprecated(), - ...) { +label_currency <- function(accuracy = NULL, scale = 1, prefix = "$", + suffix = "", big.mark = ",", decimal.mark = ".", + trim = TRUE, largest_with_fractional = 100000, + ...) { force_all( accuracy, scale, @@ -62,8 +60,7 @@ label_dollar <- function(accuracy = NULL, scale = 1, prefix = "$", big.mark, decimal.mark, trim, - largest_with_cents, - negative_parens, + largest_with_fractional, ... ) function(x) { @@ -76,8 +73,7 @@ label_dollar <- function(accuracy = NULL, scale = 1, prefix = "$", big.mark = big.mark, decimal.mark = decimal.mark, trim = trim, - largest_with_cents = largest_with_cents, - negative_parens = negative_parens, + largest_with_cents = largest_with_fractional, ... ) } @@ -95,18 +91,55 @@ needs_cents <- function(x, threshold) { !all(x == floor(x), na.rm = TRUE) } -#' Superseded interface to `label_dollar()` +#' Superseded interface to `label_currency()` #' #' @description #' `r lifecycle::badge("superseded")` #' #' These functions are kept for backward compatibility; you should switch -#' to [label_dollar()] for new code. +#' to [label_currency()] for new code. #' #' @keywords internal #' @export -#' @inheritParams label_dollar -dollar_format <- label_dollar +#' @inheritParams label_currency +#' @param largest_with_cents Like `largest_with_fractional()` in +#' [label_currency()] +#' @param negative_parens `r lifecycle::badge("deprecated")` Use +#' `style_negative = "parens"` instead. +dollar_format <- function(accuracy = NULL, scale = 1, prefix = "$", + suffix = "", big.mark = ",", decimal.mark = ".", + trim = TRUE, largest_with_cents = 100000, + negative_parens = deprecated(), + ...) { + force_all( + accuracy, + scale, + prefix, + suffix, + big.mark, + decimal.mark, + trim, + largest_with_cents, + negative_parens, + ... + ) + function(x) { + dollar( + x, + accuracy = accuracy, + scale = scale, + prefix = prefix, + suffix = suffix, + big.mark = big.mark, + decimal.mark = decimal.mark, + trim = trim, + largest_with_cents = largest_with_cents, + negative_parens = negative_parens, + ... + ) + } +} + #' @export #' @rdname dollar_format @@ -151,3 +184,7 @@ dollar <- function(x, accuracy = NULL, scale = 1, prefix = "$", ... ) } + +#' @export +#' @rdname dollar_format +label_dollar <- dollar_format diff --git a/R/label-date.R b/R/label-date.R index af89dc20..0a64e177 100644 --- a/R/label-date.R +++ b/R/label-date.R @@ -7,6 +7,8 @@ #' but uses a slightly different approach: `ConciseDateFormatter` formats #' "firsts" (e.g. first day of month, first day of day) specially; #' `date_short()` formats changes (e.g. new month, new year) specially. +#' `label_timespan()` is intended to show time passed and adds common time units +#' suffix to the input (ns, `r "\u03BC"`s, ms, s, m, h, d, w). #' #' @inherit label_number return #' @param format For `date_format()` and `time_format()` a date/time format @@ -114,6 +116,34 @@ label_time <- function(format = "%H:%M:%S", tz = "UTC", locale = NULL) { } } +#' @export +#' @rdname label_date +#' @param unit The unit used to interpret numeric input +#' @inheritDotParams number accuracy scale prefix suffix big.mark decimal.mark style_positive style_negative trim +label_timespan <- function(unit = c("secs", "mins", "hours", "days", "weeks"), + ...) { + unit <- arg_match(unit) + force_all(...) + function(x) { + x <- as.numeric(as.difftime(x, units = unit), units = "secs") + number( + x, + scale_cut = c( + 0, + "ns" = 1e-9, + "\u03BCs" = 1e-6, + "ms" = 1e-3, + "s" = 1, + "m" = 60, + "h" = 3600, + "d" = 24 * 3600, + "w" = 7 * 24 * 3600 + ), + ... + ) + } +} + format_dt <- function(x, format, tz = "UTC", locale = NULL) { if (is.null(locale)) { format(x, format = format, tz = tz) diff --git a/R/trans-compose.R b/R/trans-compose.R index 26b85da2..252d4af2 100644 --- a/R/trans-compose.R +++ b/R/trans-compose.R @@ -27,11 +27,16 @@ compose_trans <- function(...) { names <- vapply(trans_list, "[[", "name", FUN.VALUE = character(1)) + has_d_transform <- all(lengths(lapply(trans_list, "[[", "d_transform")) > 0) + has_d_inverse <- all(lengths(lapply(trans_list, "[[", "d_inverse")) > 0) + trans_new( paste0("composition(", paste0(names, collapse = ","), ")"), - transform = function(x) compose_fwd(x, trans_list), - inverse = function(x) compose_rev(x, trans_list), - breaks = function(x) trans_list[[1]]$breaks(x), + transform = function(x) compose_fwd(x, trans_list), + inverse = function(x) compose_rev(x, trans_list), + d_transform = if (has_d_transform) function(x) compose_deriv_fwd(x, trans_list), + d_inverse = if (has_d_inverse) function(x) compose_deriv_rev(x, trans_list), + breaks = function(x) trans_list[[1]]$breaks(x), domain = domain ) } @@ -49,3 +54,21 @@ compose_rev <- function(x, trans_list) { } x } + +compose_deriv_fwd <- function(x, trans_list) { + x_deriv <- 1 + for (trans in trans_list) { + x_deriv <- trans$d_transform(x) * x_deriv + x <- trans$transform(x) + } + x_deriv +} + +compose_deriv_rev <- function(x, trans_list) { + x_deriv <- 1 + for (trans in rev(trans_list)) { + x_deriv <- trans$d_inverse(x) * x_deriv + x <- trans$inverse(x) + } + x_deriv +} diff --git a/R/trans-date.R b/R/trans-date.R index 51424b36..9a17ce0c 100644 --- a/R/trans-date.R +++ b/R/trans-date.R @@ -61,15 +61,50 @@ time_trans <- function(tz = NULL) { #' Transformation for times (class hms) #' +#' `timespan_trans()` provides transformations for data encoding time passed +#' along with breaks and label formatting showing standard unit of time fitting +#' the range of the data. `hms_trans()` provides the same but using standard hms +#' idioms and formatting. +#' +#' @inheritParams label_timespan #' @export #' @examples +#' # timespan_trans allows you to specify the time unit numeric data is +#' # interpreted in +#' min_trans <- timespan_trans("mins") +#' demo_timespan(seq(0, 100), trans = min_trans) +#' # Input already in difftime format is interpreted correctly +#' demo_timespan(as.difftime(seq(0, 100), units = "secs"), trans = min_trans) +#' #' if (require("hms")) { +#' # hms_trans always assumes seconds #' hms <- round(runif(10) * 86400) #' t <- hms_trans() #' t$transform(hms) #' t$inverse(t$transform(hms)) #' t$breaks(hms) +#' # The break labels also follow the hms format +#' demo_timespan(hms, trans = t) #' } +#' +timespan_trans <- function(unit = c("secs", "mins", "hours", "days", "weeks")) { + unit <- arg_match(unit) + trans_new( + "timespan", + transform = function(x) { + structure(as.numeric(as.difftime(x, units = unit), units = "secs"), names = names(x)) + }, + inverse = function(x) { + x <- as.difftime(x, units = "secs") + units(x) <- unit + x + }, + breaks = breaks_timespan(unit), + format = label_timespan(unit) + ) +} +#' @rdname timespan_trans +#' @export hms_trans <- function() { trans_new( "hms", @@ -77,32 +112,13 @@ hms_trans <- function() { structure(as.numeric(x), names = names(x)) }, inverse = hms::as_hms, - breaks = time_breaks() + breaks = breaks_hms() ) } -time_breaks <- function(n = 5) { - force(n) +breaks_hms <- function(n = 5) { + base_breaks <- breaks_timespan("secs", n) function(x) { - rng <- as.numeric(range(x)) - diff <- rng[2] - rng[1] - - if (diff <= 2 * 60) { - scale <- 1 - } else if (diff <= 2 * 3600) { - scale <- 60 - } else if (diff <= 2 * 86400) { - scale <- 3600 - } else { - scale <- 86400 - } - - rng <- rng / scale - breaks <- labeling::extended( - rng[1], rng[2], n, - Q = c(1, 2, 1.5, 4, 3), - only.loose = FALSE - ) - hms::as_hms(breaks * scale) + hms::as_hms(base_breaks(x)) } } diff --git a/R/trans-numeric.R b/R/trans-numeric.R index dd718d2d..301e15b0 100644 --- a/R/trans-numeric.R +++ b/R/trans-numeric.R @@ -11,6 +11,8 @@ asn_trans <- function() { "asn", function(x) 2 * asin(sqrt(x)), function(x) sin(x / 2)^2, + d_transform = function(x) 1 / sqrt(x - x^2), + d_inverse = function(x) sin(x) / 2, domain = c(0, 1) ) } @@ -21,7 +23,29 @@ asn_trans <- function() { #' @examples #' plot(atanh_trans(), xlim = c(-1, 1)) atanh_trans <- function() { - trans_new("atanh", "atanh", "tanh") + trans_new( + "atanh", + "atanh", + "tanh", + d_transform = function(x) 1 / (1 - x^2), + d_inverse = function(x) 1 / cosh(x)^2, + domain = c(-1, 1) + ) +} + +#' Inverse Hyperbolic Sine transformation +#' +#' @export +#' @examples +#' plot(asinh_trans(), xlim = c(-1e2, 1e2)) +asinh_trans <- function() { + trans_new( + "asinh", + transform = asinh, + inverse = sinh, + d_transform = function(x) 1 / sqrt(x^2 + 1), + d_inverse = cosh + ) } #' Box-Cox & modulus transformations @@ -67,30 +91,35 @@ atanh_trans <- function() { #' plot(modulus_trans(1), xlim = c(-10, 10)) #' plot(modulus_trans(2), xlim = c(-10, 10)) boxcox_trans <- function(p, offset = 0) { - trans <- function(x) { + if (abs(p) < 1e-07) { + trans <- function(x) log(x + offset) + inv <- function(x) exp(x) - offset + d_trans <- function(x) 1 / (x + offset) + d_inv <- "exp" + } else { + trans <- function(x) ((x + offset)^p - 1) / p + inv <- function(x) (x * p + 1)^(1 / p) - offset + d_trans <- function(x) (x + offset)^(p - 1) + d_inv <- function(x) (x * p + 1)^(1 / p - 1) + } + + trans_with_check <- function(x) { if (any((x + offset) < 0, na.rm = TRUE)) { cli::cli_abort(c( "{.fun boxcox_trans} must be given only positive values", i = "Consider using {.fun modulus_trans} instead?" )) } - if (abs(p) < 1e-07) { - log(x + offset) - } else { - ((x + offset)^p - 1) / p - } - } - - inv <- function(x) { - if (abs(p) < 1e-07) { - exp(x) - offset - } else { - (x * p + 1)^(1 / p) - offset - } + trans(x) } trans_new( - paste0("pow-", format(p)), trans, inv, domain = c(0, Inf) + paste0("pow-", format(p)), + trans_with_check, + inv, + d_transform = d_trans, + d_inverse = d_inv, + domain = c(0, Inf) ) } @@ -100,12 +129,17 @@ modulus_trans <- function(p, offset = 1) { if (abs(p) < 1e-07) { trans <- function(x) sign(x) * log(abs(x) + offset) inv <- function(x) sign(x) * (exp(abs(x)) - offset) + d_trans <- function(x) 1 / (abs(x) + offset) + d_inv <- function(x) exp(abs(x)) } else { trans <- function(x) sign(x) * ((abs(x) + offset)^p - 1) / p inv <- function(x) sign(x) * ((abs(x) * p + 1)^(1 / p) - offset) + d_trans <- function(x) (abs(x) + offset)^(p - 1) + d_inv <- function(x) (abs(x) * p + 1)^(1 / p - 1) } trans_new( - paste0("mt-pow-", format(p)), trans, inv + paste0("mt-pow-", format(p)), trans, inv, + d_transform = d_trans, d_inverse = d_inv ) } @@ -140,34 +174,44 @@ yj_trans <- function(p) { eps <- 1e-7 if (abs(p) < eps) { - trans_pos <- function(x) log(x + 1) - inv_pos <- function(x) exp(x) - 1 + trans_pos <- log1p + inv_pos <- expm1 + d_trans_pos <- function(x) 1 / (1 + x) + d_inv_pos <- exp } else { trans_pos <- function(x) ((x + 1)^p - 1) / p inv_pos <- function(x) (p * x + 1)^(1 / p) - 1 + d_trans_pos <- function(x) (x + 1)^(p - 1) + d_inv_pos <- function(x) (p * x + 1)^(1 / p - 1) } if (abs(2 - p) < eps) { - trans_neg <- function(x) -log(-x + 1) + trans_neg <- function(x) -log1p(-x) inv_neg <- function(x) 1 - exp(-x) + d_trans_neg <- function(x) 1 / (1 - x) + d_inv_new <- function(x) exp(-x) } else { trans_neg <- function(x) -((-x + 1)^(2 - p) - 1) / (2 - p) inv_neg <- function(x) 1 - (-(2 - p) * x + 1)^(1 / (2 - p)) + d_trans_neg <- function(x) (1 - x)^(1 - p) + d_inv_neg <- function(x) (-(2 - p) * x + 1)^(1 / (2 - p) - 1) } trans_new( paste0("yeo-johnson-", format(p)), function(x) trans_two_sided(x, trans_pos, trans_neg), - function(x) trans_two_sided(x, inv_pos, inv_neg) + function(x) trans_two_sided(x, inv_pos, inv_neg), + d_transform = function(x) trans_two_sided(x, d_trans_pos, d_trans_neg, f_at_0 = 1), + d_inverse = function(x) trans_two_sided(x, d_inv_pos, d_inv_neg, f_at_0 = 1) ) } -trans_two_sided <- function(x, pos, neg) { +trans_two_sided <- function(x, pos, neg, f_at_0 = 0) { out <- rep(NA_real_, length(x)) present <- !is.na(x) out[present & x > 0] <- pos(x[present & x > 0]) out[present & x < 0] <- neg(x[present & x < 0]) - out[present & x == 0] <- 0 + out[present & x == 0] <- f_at_0 out } @@ -185,7 +229,9 @@ exp_trans <- function(base = exp(1)) { trans_new( paste0("power-", format(base)), function(x) base^x, - function(x) log(x, base = base) + function(x) log(x, base = base), + d_transform = function(x) base^x * log(base), + d_inverse = function(x) 1 / x / log(base) ) } @@ -195,7 +241,13 @@ exp_trans <- function(base = exp(1)) { #' @examples #' plot(identity_trans(), xlim = c(-1, 1)) identity_trans <- function() { - trans_new("identity", "force", "force") + trans_new( + "identity", + "force", + "force", + d_transform = function(x) rep(1, length(x)), + d_inverse = function(x) rep(1, length(x)) + ) } @@ -224,11 +276,13 @@ identity_trans <- function() { #' lines(log_trans(), xlim = c(1, 20), col = "red") log_trans <- function(base = exp(1)) { force(base) - trans <- function(x) log(x, base) - inv <- function(x) base^x - - trans_new(paste0("log-", format(base)), trans, inv, - log_breaks(base = base), + trans_new( + paste0("log-", format(base)), + function(x) log(x, base), + function(x) base^x, + d_transform = function(x) 1 / x / log(base), + d_inverse = function(x) base^x * log(base), + breaks = log_breaks(base = base), domain = c(1e-100, Inf) ) } @@ -248,7 +302,11 @@ log2_trans <- function() { #' @export log1p_trans <- function() { trans_new( - "log1p", "log1p", "expm1", + "log1p", + "log1p", + "expm1", + d_transform = function(x) 1 / (1 + x), + d_inverse = "exp", domain = c(-1 + .Machine$double.eps, Inf) ) } @@ -260,15 +318,18 @@ pseudo_log_trans <- function(sigma = 1, base = exp(1)) { trans_new( "pseudo_log", function(x) asinh(x / (2 * sigma)) / log(base), - function(x) 2 * sigma * sinh(x * log(base)) + function(x) 2 * sigma * sinh(x * log(base)), + d_transform = function(x) 1 / (sqrt(4 + x^2/sigma^2) * sigma * log(base)), + d_inverse = function(x) 2 * sigma * cosh(x * log(base)) * log(base) ) } #' Probability transformation #' #' @param distribution probability distribution. Should be standard R -#' abbreviation so that "p" + distribution is a valid probability density -#' function, and "q" + distribution is a valid quantile function. +#' abbreviation so that "p" + distribution is a valid cumulative distribution +#' function, "q" + distribution is a valid quantile function, and +#' "d" + distribution is a valid probability density function. #' @param ... other arguments passed on to distribution and quantile functions #' @export #' @examples @@ -277,11 +338,14 @@ pseudo_log_trans <- function(sigma = 1, base = exp(1)) { probability_trans <- function(distribution, ...) { qfun <- match.fun(paste0("q", distribution)) pfun <- match.fun(paste0("p", distribution)) + dfun <- match.fun(paste0("d", distribution)) trans_new( paste0("prob-", distribution), function(x) qfun(x, ...), function(x) pfun(x, ...), + d_transform = function(x) 1 / dfun(qfun(x, ...), ...), + d_inverse = function(x) dfun(x, ...), domain = c(0, 1) ) } @@ -301,7 +365,9 @@ reciprocal_trans <- function() { trans_new( "reciprocal", function(x) 1 / x, - function(x) 1 / x + function(x) 1 / x, + d_transform = function(x) -1 / x^2, + d_inverse = function(x) -1 / x^2 ) } @@ -319,6 +385,8 @@ reverse_trans <- function() { "reverse", function(x) -x, function(x) -x, + d_transform = function(x) rep(-1, length(x)), + d_inverse = function(x) rep(-1, length(x)), minor_breaks = regular_minor_breaks(reverse = TRUE) ) } @@ -335,7 +403,9 @@ sqrt_trans <- function() { trans_new( "sqrt", "sqrt", - function(x) x^2, + function(x) ifelse(x < 0, NA_real_, x ^ 2), + d_transform = function(x) 0.5 / sqrt(x), + d_inverse = function(x) 2 * x, domain = c(0, Inf) ) } diff --git a/R/trans.R b/R/trans.R index 23a93804..38e43b69 100644 --- a/R/trans.R +++ b/R/trans.R @@ -3,14 +3,19 @@ #' A transformation encapsulates a transformation and its inverse, as well #' as the information needed to create pleasing breaks and labels. The `breaks()` #' function is applied on the un-transformed range of the data, and the -#' `format()` function takes the output of the `breaks()` function and return -#' well-formatted labels. +#' `format()` function takes the output of the `breaks()` function and returns +#' well-formatted labels. Transformations may also include the derivatives of the +#' transformation and its inverse, but are not required to. #' #' @param name transformation name #' @param transform function, or name of function, that performs the #' transformation #' @param inverse function, or name of function, that performs the #' inverse of the transformation +#' @param d_transform Optional function, or name of function, that gives the +#' derivative of the transformation. May be `NULL`. +#' @param d_inverse Optional function, or name of function, that gives the +#' derivative of the inverse of the transformation. May be `NULL`. #' @param breaks default breaks function for this transformation. The breaks #' function is applied to the un-transformed data. #' @param minor_breaks default minor breaks function for this transformation. @@ -23,17 +28,23 @@ #' @export #' @keywords internal #' @aliases trans -trans_new <- function(name, transform, inverse, breaks = extended_breaks(), +trans_new <- function(name, transform, inverse, + d_transform = NULL, d_inverse = NULL, + breaks = extended_breaks(), minor_breaks = regular_minor_breaks(), format = format_format(), domain = c(-Inf, Inf)) { if (is.character(transform)) transform <- match.fun(transform) if (is.character(inverse)) inverse <- match.fun(inverse) + if (is.character(d_transform)) d_transform <- match.fun(d_transform) + if (is.character(d_inverse)) d_inverse <- match.fun(d_inverse) structure( list( name = name, transform = transform, inverse = inverse, + d_transform = d_transform, + d_inverse = d_inverse, breaks = breaks, minor_breaks = minor_breaks, format = format, diff --git a/R/utils.R b/R/utils.R index e1928597..27ae7021 100644 --- a/R/utils.R +++ b/R/utils.R @@ -55,3 +55,9 @@ demo_datetime <- function(x, ...) { demo_time <- function(x, ...) { demo_ggplot(x, "scale_x_time", ...) } + +#' @rdname demo_continuous +#' @export +demo_timespan <- function(x, ...) { + demo_ggplot(x, "scale_x_continuous", ...) +} diff --git a/man/asinh_trans.Rd b/man/asinh_trans.Rd new file mode 100644 index 00000000..10b4e8be --- /dev/null +++ b/man/asinh_trans.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/trans-numeric.R +\name{asinh_trans} +\alias{asinh_trans} +\title{Inverse Hyperbolic Sine transformation} +\usage{ +asinh_trans() +} +\description{ +Inverse Hyperbolic Sine transformation +} +\examples{ +plot(asinh_trans(), xlim = c(-1e2, 1e2)) +} diff --git a/man/breaks_timespan.Rd b/man/breaks_timespan.Rd new file mode 100644 index 00000000..dc750729 --- /dev/null +++ b/man/breaks_timespan.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/breaks.R +\name{breaks_timespan} +\alias{breaks_timespan} +\title{Breaks for timespan data} +\usage{ +breaks_timespan(unit = c("secs", "mins", "hours", "days", "weeks"), n = 5) +} +\arguments{ +\item{unit}{The unit used to interpret numeric data input} + +\item{n}{Desired number of breaks. You may get slightly more or fewer +breaks that requested.} +} +\description{ +As timespan units span a variety of bases (1000 below seconds, 60 for second +and minutes, 24 for hours, and 7 for days), the range of the input data +determines the base used for calculating breaks +} +\examples{ +demo_timespan(seq(0, 100), breaks = breaks_timespan()) + +} diff --git a/man/demo_continuous.Rd b/man/demo_continuous.Rd index 94576fe3..d062595f 100644 --- a/man/demo_continuous.Rd +++ b/man/demo_continuous.Rd @@ -6,6 +6,7 @@ \alias{demo_discrete} \alias{demo_datetime} \alias{demo_time} +\alias{demo_timespan} \title{Demonstrate scales functions with ggplot2 code} \usage{ demo_continuous(x, ...) @@ -17,6 +18,8 @@ demo_discrete(x, ...) demo_datetime(x, ...) demo_time(x, ...) + +demo_timespan(x, ...) } \arguments{ \item{x}{A vector of data} diff --git a/man/dollar_format.Rd b/man/dollar_format.Rd index fdf73383..fc65b2df 100644 --- a/man/dollar_format.Rd +++ b/man/dollar_format.Rd @@ -1,9 +1,10 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/label-dollar.R +% Please edit documentation in R/label-currency.R \name{dollar_format} \alias{dollar_format} \alias{dollar} -\title{Superseded interface to \code{label_dollar()}} +\alias{label_dollar} +\title{Superseded interface to \code{label_currency()}} \usage{ dollar_format( accuracy = NULL, @@ -33,13 +34,21 @@ dollar( scale_cut = NULL, ... ) + +label_dollar( + accuracy = NULL, + scale = 1, + prefix = "$", + suffix = "", + big.mark = ",", + decimal.mark = ".", + trim = TRUE, + largest_with_cents = 1e+05, + negative_parens = deprecated(), + ... +) } \arguments{ -\item{accuracy, largest_with_cents}{Number to round to. If \code{NULL}, the default, -values will be rounded to the nearest integer, unless any of the -values has non-zero fractional component (e.g. cents) and the largest -value is less than \code{largest_with_cents} which by default is 100,000.} - \item{scale}{A scaling factor: \code{x} will be multiplied by \code{scale} before formatting. This is useful if the underlying data is very small or very large.} @@ -54,6 +63,9 @@ decimal point.} \item{trim}{Logical, if \code{FALSE}, values are right-justified to a common width (see \code{\link[base:format]{base::format()}}).} +\item{largest_with_cents}{Like \code{largest_with_fractional()} in +\code{\link[=label_currency]{label_currency()}}} + \item{negative_parens}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Use \code{style_negative = "parens"} instead.} @@ -65,6 +77,6 @@ width (see \code{\link[base:format]{base::format()}}).} \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#superseded}{\figure{lifecycle-superseded.svg}{options: alt='[Superseded]'}}}{\strong{[Superseded]}} These functions are kept for backward compatibility; you should switch -to \code{\link[=label_dollar]{label_dollar()}} for new code. +to \code{\link[=label_currency]{label_currency()}} for new code. } \keyword{internal} diff --git a/man/hms_trans.Rd b/man/hms_trans.Rd deleted file mode 100644 index e01bfa2a..00000000 --- a/man/hms_trans.Rd +++ /dev/null @@ -1,20 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/trans-date.R -\name{hms_trans} -\alias{hms_trans} -\title{Transformation for times (class hms)} -\usage{ -hms_trans() -} -\description{ -Transformation for times (class hms) -} -\examples{ -if (require("hms")) { - hms <- round(runif(10) * 86400) - t <- hms_trans() - t$transform(hms) - t$inverse(t$transform(hms)) - t$breaks(hms) -} -} diff --git a/man/label_bytes.Rd b/man/label_bytes.Rd index 35120545..d25cae27 100644 --- a/man/label_bytes.Rd +++ b/man/label_bytes.Rd @@ -97,7 +97,7 @@ demo_continuous(c(1, 1024^2), } \seealso{ Other labels for continuous scales: -\code{\link{label_dollar}()}, +\code{\link{label_currency}()}, \code{\link{label_number_auto}()}, \code{\link{label_number_si}()}, \code{\link{label_ordinal}()}, diff --git a/man/label_dollar.Rd b/man/label_currency.Rd similarity index 72% rename from man/label_dollar.Rd rename to man/label_currency.Rd index ef5cee8b..40bead59 100644 --- a/man/label_dollar.Rd +++ b/man/label_currency.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/label-dollar.R -\name{label_dollar} -\alias{label_dollar} -\title{Label currencies ($100, $2.50, etc)} +% Please edit documentation in R/label-currency.R +\name{label_currency} +\alias{label_currency} +\title{Label currencies ($100, €2.50, etc)} \usage{ -label_dollar( +label_currency( accuracy = NULL, scale = 1, prefix = "$", @@ -12,16 +12,16 @@ label_dollar( big.mark = ",", decimal.mark = ".", trim = TRUE, - largest_with_cents = 1e+05, - negative_parens = deprecated(), + largest_with_fractional = 1e+05, ... ) } \arguments{ -\item{accuracy, largest_with_cents}{Number to round to. If \code{NULL}, the default, -values will be rounded to the nearest integer, unless any of the -values has non-zero fractional component (e.g. cents) and the largest -value is less than \code{largest_with_cents} which by default is 100,000.} +\item{accuracy, largest_with_fractional}{Number to round +to. If \code{NULL}, the default, values will be rounded to the nearest integer, +unless any of the values has non-zero fractional component (e.g. cents) and +the largest value is less than \code{largest_with_fractional} which by default +is 100,000.} \item{scale}{A scaling factor: \code{x} will be multiplied by \code{scale} before formatting. This is useful if the underlying data is very small or very @@ -37,9 +37,6 @@ decimal point.} \item{trim}{Logical, if \code{FALSE}, values are right-justified to a common width (see \code{\link[base:format]{base::format()}}).} -\item{negative_parens}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} Use -\code{style_negative = "parens"} instead.} - \item{...}{ Arguments passed on to \code{\link[=number]{number}} \describe{ @@ -81,40 +78,40 @@ they work similarly for all scales, including those that generate legends rather than axes. } \description{ -Format numbers as currency, rounding values to dollars or cents using -a convenient heuristic. +Format numbers as currency, rounding values to monetary or fractional +monetary using unit a convenient heuristic. } \examples{ -demo_continuous(c(0, 1), labels = label_dollar()) -demo_continuous(c(1, 100), labels = label_dollar()) +demo_continuous(c(0, 1), labels = label_currency()) +demo_continuous(c(1, 100), labels = label_currency()) # Customise currency display with prefix and suffix -demo_continuous(c(1, 100), labels = label_dollar(prefix = "USD ")) -euro <- label_dollar( - prefix = "", - suffix = "\u20ac", +demo_continuous(c(1, 100), labels = label_currency(prefix = "USD ")) +yen <- label_currency( + prefix = "¥", + suffix = "", big.mark = ".", decimal.mark = "," ) -demo_continuous(c(1000, 1100), labels = euro) +demo_continuous(c(1000, 1100), labels = yen) -# Use negative_parens = TRUE for finance style display -demo_continuous(c(-100, 100), labels = label_dollar(style_negative = "parens")) +# Use style_negative = "parens" for finance style display +demo_continuous(c(-100, 100), labels = label_currency(style_negative = "parens")) # Use scale_cut to use K/M/B where appropriate demo_log10(c(1, 1e16), breaks = log_breaks(7, 1e3), - labels = label_dollar(scale_cut = cut_short_scale()) + labels = label_currency(scale_cut = cut_short_scale()) ) # cut_short_scale() uses B = one thousand million # cut_long_scale() uses B = one million million demo_log10(c(1, 1e16), breaks = log_breaks(7, 1e3), - labels = label_dollar(scale_cut = cut_long_scale()) + labels = label_currency(scale_cut = cut_long_scale()) ) # You can also define your own breaks -gbp <- label_dollar( +gbp <- label_currency( prefix = "\u00a3", scale_cut = c(0, k = 1e3, m = 1e6, bn = 1e9, tn = 1e12) ) diff --git a/man/label_date.Rd b/man/label_date.Rd index 427f52b2..9c510d4a 100644 --- a/man/label_date.Rd +++ b/man/label_date.Rd @@ -4,6 +4,7 @@ \alias{label_date} \alias{label_date_short} \alias{label_time} +\alias{label_timespan} \title{Label date/times} \usage{ label_date(format = "\%Y-\%m-\%d", tz = "UTC", locale = NULL) @@ -11,6 +12,8 @@ label_date(format = "\%Y-\%m-\%d", tz = "UTC", locale = NULL) label_date_short(format = c("\%Y", "\%b", "\%d", "\%H:\%M"), sep = "\\n") label_time(format = "\%H:\%M:\%S", tz = "UTC", locale = NULL) + +label_timespan(unit = c("secs", "mins", "hours", "days", "weeks"), ...) } \arguments{ \item{format}{For \code{date_format()} and \code{time_format()} a date/time format @@ -28,6 +31,44 @@ can see a complete list of supported locales with \code{\link[stringi:stri_locale_list]{stringi::stri_locale_list()}}.} \item{sep}{Separator to use when combining date formats into a single string.} + +\item{unit}{The unit used to interpret numeric input} + +\item{...}{ + Arguments passed on to \code{\link[=number]{number}} + \describe{ + \item{\code{accuracy}}{A number to round to. Use (e.g.) \code{0.01} to show 2 decimal +places of precision. If \code{NULL}, the default, uses a heuristic that should +ensure breaks have the minimum number of digits needed to show the +difference between adjacent values. + +Applied to rescaled data.} + \item{\code{scale}}{A scaling factor: \code{x} will be multiplied by \code{scale} before +formatting. This is useful if the underlying data is very small or very +large.} + \item{\code{prefix}}{Additional text to display before the number. The suffix is +applied to absolute value before \code{style_positive} and \code{style_negative} are +processed so that \code{prefix = "$"} will yield (e.g.) \verb{-$1} and \verb{($1)}.} + \item{\code{suffix}}{Additional text to display after the number.} + \item{\code{big.mark}}{Character used between every 3 digits to separate thousands.} + \item{\code{decimal.mark}}{The character to be used to indicate the numeric +decimal point.} + \item{\code{style_positive}}{A string that determines the style of positive numbers: +\itemize{ +\item \code{"none"} (the default): no change, e.g. \code{1}. +\item \code{"plus"}: preceded by \code{+}, e.g. \code{+1}. +}} + \item{\code{style_negative}}{A string that determines the style of negative numbers: +\itemize{ +\item \code{"hyphen"} (the default): preceded by a standard hypen \code{-}, e.g. \code{-1}. +\item \code{"minus"}, uses a proper Unicode minus symbol. This is a typographical +nicety that ensures \code{-} aligns with the horizontal bar of the +the horizontal bar of \code{+}. +\item \code{"parens"}, wrapped in parentheses, e.g. \code{(1)}. +}} + \item{\code{trim}}{Logical, if \code{FALSE}, values are right-justified to a common +width (see \code{\link[base:format]{base::format()}}).} + }} } \value{ All \code{label_()} functions return a "labelling" function, i.e. a function that @@ -47,6 +88,8 @@ sufficient to uniquely identify labels. It's inspired by matplotlib's but uses a slightly different approach: \code{ConciseDateFormatter} formats "firsts" (e.g. first day of month, first day of day) specially; \code{date_short()} formats changes (e.g. new month, new year) specially. +\code{label_timespan()} is intended to show time passed and adds common time units +suffix to the input (ns, μs, ms, s, m, h, d, w). } \examples{ date_range <- function(start, days) { diff --git a/man/label_number_auto.Rd b/man/label_number_auto.Rd index 220dfe55..a660cbec 100644 --- a/man/label_number_auto.Rd +++ b/man/label_number_auto.Rd @@ -29,7 +29,7 @@ demo_log10(c(1, 1e7), labels = label_number_auto()) \seealso{ Other labels for continuous scales: \code{\link{label_bytes}()}, -\code{\link{label_dollar}()}, +\code{\link{label_currency}()}, \code{\link{label_number_si}()}, \code{\link{label_ordinal}()}, \code{\link{label_parse}()}, diff --git a/man/label_number_si.Rd b/man/label_number_si.Rd index df2d0d57..85019af4 100644 --- a/man/label_number_si.Rd +++ b/man/label_number_si.Rd @@ -94,7 +94,7 @@ demo_continuous(c(1, 1e9), labels = label_number(scale_cut = cut_short_scale())) \seealso{ Other labels for continuous scales: \code{\link{label_bytes}()}, -\code{\link{label_dollar}()}, +\code{\link{label_currency}()}, \code{\link{label_number_auto}()}, \code{\link{label_ordinal}()}, \code{\link{label_parse}()}, diff --git a/man/label_ordinal.Rd b/man/label_ordinal.Rd index 68be92f5..62945380 100644 --- a/man/label_ordinal.Rd +++ b/man/label_ordinal.Rd @@ -110,7 +110,7 @@ demo_continuous(c(1, 10), \seealso{ Other labels for continuous scales: \code{\link{label_bytes}()}, -\code{\link{label_dollar}()}, +\code{\link{label_currency}()}, \code{\link{label_number_auto}()}, \code{\link{label_number_si}()}, \code{\link{label_parse}()}, diff --git a/man/label_parse.Rd b/man/label_parse.Rd index 5636233c..7dd7d29d 100644 --- a/man/label_parse.Rd +++ b/man/label_parse.Rd @@ -47,7 +47,7 @@ demo_continuous(c(1, 5), labels = label_math()) Other labels for continuous scales: \code{\link{label_bytes}()}, -\code{\link{label_dollar}()}, +\code{\link{label_currency}()}, \code{\link{label_number_auto}()}, \code{\link{label_number_si}()}, \code{\link{label_ordinal}()}, diff --git a/man/label_percent.Rd b/man/label_percent.Rd index ffb018ea..ffef1e54 100644 --- a/man/label_percent.Rd +++ b/man/label_percent.Rd @@ -98,7 +98,7 @@ demo_continuous(c(0, .01), labels = french_percent) \seealso{ Other labels for continuous scales: \code{\link{label_bytes}()}, -\code{\link{label_dollar}()}, +\code{\link{label_currency}()}, \code{\link{label_number_auto}()}, \code{\link{label_number_si}()}, \code{\link{label_ordinal}()}, diff --git a/man/label_pvalue.Rd b/man/label_pvalue.Rd index 1cff3fce..ae9e2b83 100644 --- a/man/label_pvalue.Rd +++ b/man/label_pvalue.Rd @@ -54,7 +54,7 @@ demo_continuous(c(0, 1), labels = label_pvalue(prefix = prefix)) \seealso{ Other labels for continuous scales: \code{\link{label_bytes}()}, -\code{\link{label_dollar}()}, +\code{\link{label_currency}()}, \code{\link{label_number_auto}()}, \code{\link{label_number_si}()}, \code{\link{label_ordinal}()}, diff --git a/man/label_scientific.Rd b/man/label_scientific.Rd index 0447c010..97db3367 100644 --- a/man/label_scientific.Rd +++ b/man/label_scientific.Rd @@ -54,7 +54,7 @@ demo_log10(c(1, 1e9)) \seealso{ Other labels for continuous scales: \code{\link{label_bytes}()}, -\code{\link{label_dollar}()}, +\code{\link{label_currency}()}, \code{\link{label_number_auto}()}, \code{\link{label_number_si}()}, \code{\link{label_ordinal}()}, diff --git a/man/probability_trans.Rd b/man/probability_trans.Rd index 67ca1771..e0a4e82d 100644 --- a/man/probability_trans.Rd +++ b/man/probability_trans.Rd @@ -14,8 +14,9 @@ probit_trans() } \arguments{ \item{distribution}{probability distribution. Should be standard R -abbreviation so that "p" + distribution is a valid probability density -function, and "q" + distribution is a valid quantile function.} +abbreviation so that "p" + distribution is a valid cumulative distribution +function, "q" + distribution is a valid quantile function, and +"d" + distribution is a valid probability density function.} \item{...}{other arguments passed on to distribution and quantile functions} } diff --git a/man/rescale.Rd b/man/rescale.Rd index b271bc99..4a9b66d5 100644 --- a/man/rescale.Rd +++ b/man/rescale.Rd @@ -9,6 +9,7 @@ \alias{rescale.Date} \alias{rescale.integer64} \alias{rescale.difftime} +\alias{rescale.AsIs} \title{Rescale continuous vector to have specified minimum and maximum} \usage{ rescale(x, to, from, ...) @@ -26,6 +27,8 @@ rescale(x, to, from, ...) \method{rescale}{integer64}(x, to = c(0, 1), from = range(x, na.rm = TRUE), ...) \method{rescale}{difftime}(x, to = c(0, 1), from = range(x, na.rm = TRUE, finite = TRUE), ...) + +\method{rescale}{AsIs}(x, to, from, ...) } \arguments{ \item{x}{continuous vector of values to manipulate.} @@ -40,6 +43,9 @@ calculated from the range of \code{x}} \description{ Rescale continuous vector to have specified minimum and maximum } +\details{ +Objects of class \verb{} are returned unaltered. +} \examples{ rescale(1:100) rescale(runif(50)) diff --git a/man/rescale_mid.Rd b/man/rescale_mid.Rd index c9bf7392..d3133ab0 100644 --- a/man/rescale_mid.Rd +++ b/man/rescale_mid.Rd @@ -8,6 +8,7 @@ \alias{rescale_mid.POSIXt} \alias{rescale_mid.Date} \alias{rescale_mid.integer64} +\alias{rescale_mid.AsIs} \title{Rescale vector to have specified minimum, midpoint, and maximum} \usage{ rescale_mid(x, to, from, mid, ...) @@ -23,6 +24,8 @@ rescale_mid(x, to, from, mid, ...) \method{rescale_mid}{Date}(x, to = c(0, 1), from = range(x, na.rm = TRUE), mid, ...) \method{rescale_mid}{integer64}(x, to = c(0, 1), from = range(x, na.rm = TRUE), mid = 0, ...) + +\method{rescale_mid}{AsIs}(x, to, from, ...) } \arguments{ \item{x}{vector of values to manipulate.} @@ -39,6 +42,9 @@ calculated from the range of \code{x}} \description{ Rescale vector to have specified minimum, midpoint, and maximum } +\details{ +Objects of class \verb{} are returned unaltered. +} \examples{ rescale_mid(1:100, mid = 50.5) rescale_mid(runif(50), mid = 0.5) diff --git a/man/timespan_trans.Rd b/man/timespan_trans.Rd new file mode 100644 index 00000000..6ead6f0d --- /dev/null +++ b/man/timespan_trans.Rd @@ -0,0 +1,40 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/trans-date.R +\name{timespan_trans} +\alias{timespan_trans} +\alias{hms_trans} +\title{Transformation for times (class hms)} +\usage{ +timespan_trans(unit = c("secs", "mins", "hours", "days", "weeks")) + +hms_trans() +} +\arguments{ +\item{unit}{The unit used to interpret numeric input} +} +\description{ +\code{timespan_trans()} provides transformations for data encoding time passed +along with breaks and label formatting showing standard unit of time fitting +the range of the data. \code{hms_trans()} provides the same but using standard hms +idioms and formatting. +} +\examples{ +# timespan_trans allows you to specify the time unit numeric data is +# interpreted in +min_trans <- timespan_trans("mins") +demo_timespan(seq(0, 100), trans = min_trans) +# Input already in difftime format is interpreted correctly +demo_timespan(as.difftime(seq(0, 100), units = "secs"), trans = min_trans) + +if (require("hms")) { + # hms_trans always assumes seconds + hms <- round(runif(10) * 86400) + t <- hms_trans() + t$transform(hms) + t$inverse(t$transform(hms)) + t$breaks(hms) + # The break labels also follow the hms format + demo_timespan(hms, trans = t) +} + +} diff --git a/man/trans_new.Rd b/man/trans_new.Rd index 1e6b8fa1..64ae23ad 100644 --- a/man/trans_new.Rd +++ b/man/trans_new.Rd @@ -11,6 +11,8 @@ trans_new( name, transform, inverse, + d_transform = NULL, + d_inverse = NULL, breaks = extended_breaks(), minor_breaks = regular_minor_breaks(), format = format_format(), @@ -30,6 +32,12 @@ transformation} \item{inverse}{function, or name of function, that performs the inverse of the transformation} +\item{d_transform}{Optional function, or name of function, that gives the +derivative of the transformation. May be \code{NULL}.} + +\item{d_inverse}{Optional function, or name of function, that gives the +derivative of the inverse of the transformation. May be \code{NULL}.} + \item{breaks}{default breaks function for this transformation. The breaks function is applied to the un-transformed data.} @@ -46,8 +54,9 @@ argument.} A transformation encapsulates a transformation and its inverse, as well as the information needed to create pleasing breaks and labels. The \code{breaks()} function is applied on the un-transformed range of the data, and the -\code{format()} function takes the output of the \code{breaks()} function and return -well-formatted labels. +\code{format()} function takes the output of the \code{breaks()} function and returns +well-formatted labels. Transformations may also include the derivatives of the +transformation and its inverse, but are not required to. } \seealso{ \Sexpr[results=rd,stage=build]{scales:::seealso_trans()} diff --git a/tests/testthat/test-bounds.R b/tests/testthat/test-bounds.R index 931f1fba..01be2823 100644 --- a/tests/testthat/test-bounds.R +++ b/tests/testthat/test-bounds.R @@ -71,6 +71,11 @@ test_that("scaling is possible with NULL values", { expect_null(rescale_mid(NULL)) }) +test_that("rescaling does not alter AsIs objects", { + expect_identical(I(1:3), rescale(I(1:3), from = c(0, 4))) + expect_identical(I(1:3), rescale_mid(I(1:3), from = c(0, 4), mid = 1)) +}) + test_that("scaling is possible with logical values", { expect_equal(rescale(c(FALSE, TRUE)), c(0, 1)) expect_equal(rescale_mid(c(FALSE, TRUE), mid = 0.5), c(0, 1)) diff --git a/tests/testthat/test-label-dollar.R b/tests/testthat/test-label-currency.R similarity index 58% rename from tests/testthat/test-label-dollar.R rename to tests/testthat/test-label-currency.R index 9b804638..cdb23d7c 100644 --- a/tests/testthat/test-label-dollar.R +++ b/tests/testthat/test-label-currency.R @@ -1,5 +1,5 @@ test_that("negative comes before prefix", { - expect_equal(label_dollar()(-1), "-$1") + expect_equal(label_currency()(-1), "-$1") }) test_that("negative_parens is deprecated", { @@ -10,21 +10,21 @@ test_that("negative_parens is deprecated", { }) test_that("preserves NAs", { - expect_equal(label_dollar()(NA_real_), NA_character_) + expect_equal(label_currency()(NA_real_), NA_character_) }) test_that("preserves names", { - expect_named(label_dollar()(c(a = 1)), "a") + expect_named(label_currency()(c(a = 1)), "a") }) test_that("decimal.mark could be modified", { - expect_equal(label_dollar(decimal.mark = ",")(123.45), "$123,45") + expect_equal(label_currency(decimal.mark = ",")(123.45), "$123,45") }) test_that("can rescale with scale_cut", { - lab <- label_dollar(scale_cut = cut_short_scale()) + lab <- label_currency(scale_cut = cut_short_scale()) expect_equal(lab(c(1, 1e3, 1e6)), c("$1", "$1K", "$1M")) - lab <- label_dollar(scale_cut = cut_short_scale(), prefix = "", suffix = " USD") + lab <- label_currency(scale_cut = cut_short_scale(), prefix = "", suffix = " USD") expect_equal(lab(c(1, 1e3, 1e6)), c("1 USD", "1K USD", "1M USD")) }) diff --git a/tests/testthat/test-trans-compose.R b/tests/testthat/test-trans-compose.R index d564fd4f..4a49dbc5 100644 --- a/tests/testthat/test-trans-compose.R +++ b/tests/testthat/test-trans-compose.R @@ -4,6 +4,18 @@ test_that("composes transforms correctly", { expect_equal(t$inverse(-2), 100) }) +test_that("composes derivatives correctly", { + t <- compose_trans("sqrt", "reciprocal", "reverse") + expect_equal(t$d_transform(0.25), 4) + expect_equal(t$d_inverse(-2), 0.25) +}) + +test_that("produces NULL derivatives if not all transforms have derivatives", { + t <- compose_trans("sqrt", trans_new("no_deriv", identity, identity)) + expect_null(t$d_transform) + expect_null(t$d_inverse) +}) + test_that("uses breaks from first transformer", { t <- compose_trans("log10", "reverse") expect_equal(t$breaks(c(1, 1000)), log_breaks()(c(1, 1000))) diff --git a/tests/testthat/test-trans-numeric.R b/tests/testthat/test-trans-numeric.R index d20083a4..a7f5cbf8 100644 --- a/tests/testthat/test-trans-numeric.R +++ b/tests/testthat/test-trans-numeric.R @@ -116,3 +116,161 @@ test_that("Yeo-Johnson transform works", { expect_equal(yj_trans(lambdas[2])$transform(x[[2]]), expected_data[[2]]) expect_equal(yj_trans(lambdas[3])$transform(x[[3]]), expected_data[[3]]) }) + +test_that("probability transforms have domain (0,1)", { + expect_equal(logit_trans()$domain, c(0, 1)) + expect_equal(probit_trans()$domain, c(0, 1)) +}) + +# Derivatives ------------------------------------------------------------- + +test_that("asn_trans derivatives work", { + trans <- asn_trans() + expect_equal(trans$d_transform(c(0, 0.5, 1)), c(Inf, 2, Inf)) + expect_equal(trans$d_inverse(c(0, pi/2, pi)), c(0, 0.5, 0)) + x <- seq(0.1, 0.9, length.out = 10) + expect_equal(trans$d_transform(x), 1 / trans$d_inverse(trans$transform(x))) + expect_equal(trans$d_inverse(x), 1 / trans$d_transform(trans$inverse(x))) +}) + +test_that("atanh_trans derivatives work", { + trans <- atanh_trans() + expect_equal(trans$d_transform(c(-1, 0, 1)), c(Inf, 1, Inf)) + expect_equal(trans$d_inverse(c(-log(2), 0, log(2))), c(0.64, 1, 0.64)) + x <- seq(-0.9, 0.9, length.out = 10) + expect_equal(trans$d_transform(x), 1 / trans$d_inverse(trans$transform(x))) + expect_equal(trans$d_inverse(x), 1 / trans$d_transform(trans$inverse(x))) +}) + +test_that("asinh_trans derivatives work", { + trans <- asinh_trans() + expect_equal(trans$d_transform(c(-1, 0, 1)), c(sqrt(2) / 2, 1, sqrt(2) / 2)) + expect_equal(trans$d_inverse(c(-log(2), 0, log(2))), c(1.25, 1, 1.25)) + x <- seq(-0.9, 0.9, length.out = 10) + expect_equal(trans$d_transform(x), 1 / trans$d_inverse(trans$transform(x))) + expect_equal(trans$d_inverse(x), 1 / trans$d_transform(trans$inverse(x))) +}) + +test_that("boxcox_trans derivatives work", { + trans <- boxcox_trans(p = 0, offset = 1) + expect_equal(trans$d_transform(c(0, 1, 2)), c(1, 1/2, 1/3)) + expect_equal(trans$d_inverse(c(0, 1, 2)), exp(c(0, 1, 2))) + x <- 0:10 + expect_equal(trans$d_transform(x), 1 / trans$d_inverse(trans$transform(x))) + expect_equal(trans$d_inverse(x), 1 / trans$d_transform(trans$inverse(x))) + + trans <- boxcox_trans(p = 2, offset = 2) + expect_equal(trans$d_transform(c(0, 1, 2)), c(2, 3, 4)) + expect_equal(trans$d_inverse(c(0, 0.5, 4)), c(1, sqrt(2) / 2, 1/3)) + expect_equal(trans$d_transform(x), 1 / trans$d_inverse(trans$transform(x))) + expect_equal(trans$d_inverse(x), 1 / trans$d_transform(trans$inverse(x))) +}) + +test_that("modulus_trans derivatives work", { + trans <- modulus_trans(p = 0, offset = 1) + expect_equal(trans$d_transform(c(-2, -1, 1, 2)), c(1/3, 1/2, 1/2, 1/3)) + expect_equal(trans$d_inverse(c(-2, -1, 1, 2)), exp(c(2, 1, 1, 2))) + x <- c(-10:-2, 2:10) + expect_equal(trans$d_transform(x), 1 / trans$d_inverse(trans$transform(x))) + expect_equal(trans$d_inverse(x), 1 / trans$d_transform(trans$inverse(x))) + + trans <- modulus_trans(p = 2, offset = 2) + expect_equal(trans$d_transform(c(-2, -1, 1, 2)), c(4, 3, 3, 4)) + expect_equal(trans$d_inverse(c(-4, -0.5, 0.5, 4)), c(1/3, sqrt(2) / 2, sqrt(2) / 2, 1/3)) + expect_equal(trans$d_transform(x), 1 / trans$d_inverse(trans$transform(x))) + expect_equal(trans$d_inverse(x), 1 / trans$d_transform(trans$inverse(x))) +}) + +test_that("yj_trans derivatives work", { + trans <- yj_trans(p = 0) + expect_equal(trans$d_transform(c(-2, -1, 1, 2)), c(3, 2, 0.5, 1/3)) + expect_equal(trans$d_inverse(c(-1/2, 1, 2)), c(sqrt(2) / 2, exp(1), exp(2))) + x <- c(-10:10) + expect_equal(trans$d_transform(x), 1 / trans$d_inverse(trans$transform(x))) + expect_equal(trans$d_inverse(x), 1 / trans$d_transform(trans$inverse(x))) + + trans <- yj_trans(p = 3) + expect_equal(trans$d_transform(c(-2, -1, 1, 2)), c(1/9, 1/4, 4, 9)) + expect_equal(trans$d_inverse(c(-4, -0.5, 1)), c(1/9, 4, (1/16)^(1/3))) + expect_equal(trans$d_transform(x), 1 / trans$d_inverse(trans$transform(x))) + expect_equal(trans$d_inverse(0:10), 1 / trans$d_transform(trans$inverse(0:10))) +}) + +test_that("exp_trans derivatives work", { + trans <- exp_trans(10) + expect_equal(trans$d_transform(c(0, 1, 2)), c(1, 10, 100) * log(10)) + expect_equal(trans$d_inverse(c(0.1, 1, 10) / log(10)), c(10, 1, 0.1)) + x <- 1:10 + expect_equal(trans$d_transform(x), 1 / trans$d_inverse(trans$transform(x))) + expect_equal(trans$d_inverse(x), 1 / trans$d_transform(trans$inverse(x))) +}) + +test_that("identity_trans derivatives work", { + trans <- identity_trans() + expect_equal(trans$d_transform(numeric(0)), numeric(0)) + expect_equal(trans$d_transform(c(0, 1, 2)), c(1, 1, 1)) + expect_equal(trans$d_inverse(numeric(0)), numeric(0)) + expect_equal(trans$d_inverse(c(0, 1, 2)), c(1, 1, 1)) +}) + +test_that("log_trans derivatives work", { + trans <- log_trans(10) + expect_equal(trans$d_transform(c(0.1, 1, 10) / log(10)), c(10, 1, 0.1)) + expect_equal(trans$d_inverse(c(0, 1, 2)), c(1, 10, 100) * log(10)) + x <- 1:10 + expect_equal(trans$d_transform(x), 1 / trans$d_inverse(trans$transform(x))) + expect_equal(trans$d_inverse(x), 1 / trans$d_transform(trans$inverse(x))) +}) + +test_that("log1p_trans derivatives work", { + trans <- log1p_trans() + expect_equal(trans$d_transform(c(0, 1, 2)), c(1, 1/2, 1/3)) + expect_equal(trans$d_inverse(c(0, 1, 2)), exp(c(0, 1, 2))) + x <- 0:10 + expect_equal(trans$d_transform(x), 1 / trans$d_inverse(trans$transform(x))) + expect_equal(trans$d_inverse(x), 1 / trans$d_transform(trans$inverse(x))) +}) + +test_that("pseudo_log_trans derivatives work", { + trans <- pseudo_log_trans(0.5) + expect_equal(trans$d_transform(c(0, 1)), c(1, sqrt(2) / 2)) + expect_equal(trans$d_inverse(c(0, 1)), c(1, cosh(1))) + x <- 1:10 + expect_equal(trans$d_transform(x), 1 / trans$d_inverse(trans$transform(x))) + expect_equal(trans$d_inverse(x), 1 / trans$d_transform(trans$inverse(x))) +}) + +test_that("logit_trans derivatives work", { + trans <- logit_trans() + expect_equal(trans$d_transform(c(0.1, 0.5, 0.8)), c(100/9, 4, 6.25)) + expect_equal(trans$d_inverse(c(0, 1, 2)), dlogis(c(0, 1, 2))) + x <- seq(0.1, 0.9, length.out = 10) + expect_equal(trans$d_transform(x), 1 / trans$d_inverse(trans$transform(x))) + expect_equal(trans$d_inverse(x), 1 / trans$d_transform(trans$inverse(x))) +}) + +test_that("reciprocal_trans derivatives work", { + trans <- reciprocal_trans() + expect_equal(trans$d_transform(c(0.1, 1, 10)), c(-100, -1, -0.01)) + expect_equal(trans$d_inverse(c(0.1, 1, 10)), c(-100, -1, -0.01)) + x <- (1:20)/10 + expect_equal(trans$d_transform(x), 1 / trans$d_inverse(trans$transform(x))) + expect_equal(trans$d_inverse(x), 1 / trans$d_transform(trans$inverse(x))) +}) + +test_that("reverse_trans derivatives work", { + trans <- reverse_trans() + expect_equal(trans$d_transform(numeric(0)), numeric(0)) + expect_equal(trans$d_transform(c(-1, 1, 2)), c(-1, -1, -1)) + expect_equal(trans$d_inverse(numeric(0)), numeric(0)) + expect_equal(trans$d_inverse(c(-1, 1, 2)), c(-1, -1, -1)) +}) + +test_that("sqrt_trans derivatives work", { + trans <- sqrt_trans() + expect_equal(trans$d_transform(c(1, 4, 9)), c(1/2, 1/4, 1/6)) + expect_equal(trans$d_inverse(c(1, 2, 3)), c(2, 4, 6)) + x <- 1:10 + expect_equal(trans$d_transform(x), 1 / trans$d_inverse(trans$transform(x))) + expect_equal(trans$d_inverse(x), 1 / trans$d_transform(trans$inverse(x))) +}) diff --git a/tests/testthat/test-trans.R b/tests/testthat/test-trans.R index d07a29b1..7030d43f 100644 --- a/tests/testthat/test-trans.R +++ b/tests/testthat/test-trans.R @@ -27,3 +27,7 @@ test_that("trans has useful print method", { trans_new("test", transform = identity, inverse = identity) }) }) + +test_that("inverse of trans_sqrt() returns NA for values outside of range", { + expect_equal(sqrt_trans()$inverse(-2), NA_real_) +})