diff --git a/NAMESPACE b/NAMESPACE index 50dbb621..2fdba310 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -41,6 +41,7 @@ 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 +67,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) @@ -101,6 +103,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) @@ -158,6 +161,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 d4778712..5d94c33b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -13,6 +13,11 @@ * `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) # scales 1.2.1 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-date.R b/R/label-date.R index af89dc20..730fb7b5 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, µ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, + "µs" = 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-date.R b/R/trans-date.R index 51424b36..7bf67874 100644 --- a/R/trans-date.R +++ b/R/trans-date.R @@ -61,15 +61,49 @@ 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. +#' #' @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 +111,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/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/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/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_date.Rd b/man/label_date.Rd index 427f52b2..eb00a960 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/timespan_trans.Rd b/man/timespan_trans.Rd new file mode 100644 index 00000000..adc94257 --- /dev/null +++ b/man/timespan_trans.Rd @@ -0,0 +1,37 @@ +% 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() +} +\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) +} + +}