diff --git a/NEWS.md b/NEWS.md index ea6b04ca..53e3b94c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -15,7 +15,7 @@ where the formatting is also better._ implied factor levels. It also improves layering consistency via `plt_add()` and co. so that plots are identical, regardless of whether lines are layered on top of points, or vice versa. Note that you can still select into the - old behaviour by passing the (new) `xlevels = "asis"` argument as an + old behaviour by passing the (new) `xord = "asis"` argument as an explicit override; see "Other new features" below. (#683 @grantmcdermott) ### New features @@ -32,6 +32,14 @@ where the formatting is also better._ along the chosen margin by default. It also reverses the y-axis by default, so that the first row sits at the top (again matching `heatmap()`); pass an explicit `ylim` to override. (#677 @grantmcdermott) +- While not strictly a new plot type, `type_area()` gains a new `stack` argument + for drawing _stacked_ area plots, where each layer represents a discrete `by` + category group. This functionality is further enhanced by two (also new) + sister arguments. First, `byord` enables on-the-fly (re-)ordering of the + stacked `by` layers, via convenience keywords or custom functions (e.g., + `byord = "end"` ranks groups according to their largest final value). Second, + a `FUN` argument permits stacking of multi-observation data by collapsing + repeated `y` values. (#688 @grantmcdermott) #### Facet improvements @@ -66,28 +74,63 @@ Note that each of these `facet.args` arguments is paired with an equivalent to set this behaviour globally. This also means that they can be set as part of a (custom) theme, e.g. `tinytheme("clean", facet.axes = "outer")`. +#### Ordering and labelling categorical variables + +This release brings several enhancements for working with _categorical_ +variables, i.e. where `x`, `y`, or `by` are characters or factors with discrete +levels. This includes improvements to existing arguments, as well as the +provision of some new arguments that enable finer control over level ordering +and convenient label formatting. + +- `xlevels`, `ylevels`: these (type-level) arguments permit on-the-fly + reordering of a categorical variable via _literal_ specification, either a + character vector of level names (e.g., `c("C", "B", "A")`), or a numeric + vector of level indices (e.g., `3:1`). While this argument is not new---having + been supported by `type_barplot` and several other types for a while---we now + extend `xlevels` support to `type_points()`, `type_lines()`, + `type_errorbar()`, and `type_pointrange()`. (#683, #694 @grantmcdermott) +- `xord`, `yord`: these are new (type-level) arguments that provide an alternate + ordering interface to `x/ylevels`. Specifically, while `x/ylevels` require a + literal ordering, `x/yord` _computes_ the order on the fly, according to + (type-appropriate) convenience keywords or a custom ranking function. For + example, `"desc(ending)"`/`"asc(ending)"` orders by value, while `"asis"` + ignores factor levels and just takes the order of appearance in the data as + given. Among other things, this makes it possible to sort barplots by height + (e.g., `type_barplot(xord = "desc")`), or ridges by their spread (e.g., + `type_ridge(yord = "minvar")`) without relevelling the underlying factor by + hand. (#683, #694 @grantmcdermott) +- (Note: users should only supply one of the preceding sets of arguments. If + both `x/ylevels` and `x/yord` are provided, the former takes precedence as + the more explicit.) +- `tinylabel()` gains a dictionary form, i.e., a *named* character vector or + list that maps existing labels to new ones _a la_ + `c(old1 = "new1", old2 = "new2")`. Partial mapping is fine since the lookup is + by value rather than by position, so that some levels can be left unnamed. + Importantly, this behaviour extends to the rest of **tinyplot**'s + (re)labelling machinery---including `x/yaxl`, `type_text()`, and any function + with a `labeller` argument---since everything is routed through `tinylabel()`. + (#690 @grantmcdermott) +- The type-level `x/yaxlabels` arguments of `type_spineplot()` and + `type_barplot()` are deprecated in favour of the top-level `x/yaxl` arguments. + The type-level arguments predated their top-level cousins, which now offer the + same functionality via a consistent interface across _all_ types. The old + arguments still work (with a warning) for now. But we will be formally + removing them in a future release and, going forwards, encourage users to move + over to `xaxl` and `yaxl` as the idiomatic **tinyplot** way to relabel + and format axis ticks. (#692 @grantmcdermott) + +Beyond convenience, these improvements to categorical variable handling also +provide the scaffolding to eliminate some niggling inconsistencies; for example, +related to plot layering. See "Bug fixes" below. + #### Other new features -- `type_area()` gains a `stack` argument for stacked area plots. A sister - `byord` argument enables convenient, on-the-fly (re-)ordering of stacking - layers through convenience keywords or custom functions (e.g., - `byord = "end"` ranks groups according to their largest final value, while - `byord = "minvar"` puts the lowest variance group on the baseline, and - `byord = "rev"` simply reverses the existing level order). Custom - functions may additionally name an `x` argument to receive the group's `x` - values, as needed by any statistic that depends on their spacing. - Similarly, a new `FUN` argument permits stacking of multi-observation data by - collapsing repeated `y` values. (#688 @grantmcdermott) -- `type_points()`, `type_lines()`, `type_errorbar()`, and `type_pointrange()` - gain an `xlevels` argument for reordering a categorical `x` variable on the - fly (matching existing functionality for `type_barplot()` and several other - types). Values can be a character vector of level names, a numeric vector of - level indexes (e.g., `3:1`), or the new `"asis"` keyword, which takes the - categories in the order that they appear in the data. The latter option is - also the default for `type_errorbar()` and `type_pointrange()`, thus - preserving existing behaviour since these two types are typically fed - coefficient table data where the row order is intentional. - (#683 @grantmcdermott) +- `type_density()` gains an `echo.bw` argument for reporting the smoothing + bandwidth and the number of observations behind it, neither of which is + visible from the curve itself. Destinations are `"sub"`, `"cap"`, and + `"cat"` (console), in any combination; a destination the user has already + labelled is left alone. Shared bandwidths are reported once and named as + joint, individual bandwidths per group. (#287 @haomeng797-ship-it) - Custom plot types have more control over the surrounding plot machinery, via a new `type_hints` mechanism. A type can declare properties about itself---that it draws its own axes, needs a secondary right-hand axis, uses proportional @@ -97,12 +140,6 @@ a (custom) theme, e.g. `tinytheme("clean", facet.axes = "outer")`. custom types. See [Advanced customization](https://grantmcdermott.com/tinyplot/vignettes/types.html#type-hints) in the `Types` vignette for the list of supported hints. (#543 @grantmcdermott) -- `type_density()` gains an `echo.bw` argument for reporting the smoothing - bandwidth and the number of observations behind it, neither of which is - visible from the curve itself. Destinations are `"sub"`, `"cap"`, and - `"cat"` (console), in any combination; a destination the user has already - labelled is left alone. Shared bandwidths are reported once and named as - joint, individual bandwidths per group. (#287 @haomeng797-ship-it) - New `cex.xaxs` and `cex.yaxs` graphical parameters allow the x- and y-axis tick labels to be sized independently, e.g. `tpar(cex.yaxs = 0.6)` to shrink a long list of category names on the y-axis without also shrinking the x-axis. @@ -117,6 +154,25 @@ a (custom) theme, e.g. `tinytheme("clean", facet.axes = "outer")`. ### Bug fixes +- Layers added with `tinyplot_add()` now align correctly when the base plot type + coerces a numeric `x` variable to a factor, as `type_barplot()` and + `type_violin()` do. The base layer's categories are the coerced *labels*, + while the added layer still carried the raw values, so it was drawn at those + coordinates instead of at the category positions---often well outside the + plotting region. (#691 @grantmcdermott) +- Axis labellers no longer blow up the decimal precision when the breaks are + symmetric about zero, as they are for a centered barplot. `tinyplot(..., + center = TRUE, yaxl = "percent")` labelled its axis `80.00000%` rather than + `80%`. (#689 @grantmcdermott) +- The top-level `xaxl` / `yaxl` arguments now work for `type_spineplot()` and + `type_ridge()`. Both types draw their own axes, and so never reached the + standard path where those arguments are applied, meaning they were silently + ignored. (#694 @grantmcdermott) +- `xlevels` / `ylevels` no longer drop data silently. Naming a strict subset of + a variable's levels sent every other level to `NA`, quietly removing those + observations from the plot; this now warns. Supplying a value that matches no + level at all is now an error, rather than surfacing later as an unrelated + complaint about zero-length ranges. (#688, #694 @grantmcdermott) - `type_area()` now labels a categorical `x` axis with its factor levels, rather than falling back to the underlying integer positions. (#688 @grantmcdermott) diff --git a/R/align_layer.R b/R/align_layer.R index cc52d69d..c9cd4a76 100644 --- a/R/align_layer.R +++ b/R/align_layer.R @@ -22,8 +22,23 @@ align_layer = function(settings) { # Only adjust if original layer has named xlabs if (!is.null(names(xlabs_orig))) { - if (is.factor(settings$datapoints[["x"]])) { - # Case 1: relevel a factor (e.g., ribbon added to errorbars) + # The atomic branch of this condition covers a base layer that coerced a + # numeric/character x to a factor itself (bars, ridges): its categories are + # the *labels*, while the added layer still carries the raw values. Those + # values are releveled below just like a factor would be, and the resulting + # integer codes are the positions the base layer drew at. + # + # Both extra tests are load-bearing. Requiring the layer to have no named + # xlabs of its own leaves Case 2 owning layers that already converted -- + # otherwise a base whose categories are literally "1", "2", "3" would have + # the layer's integer *positions* misread as labels. Requiring every value + # to match leaves a partial overlap alone, rather than silently turning the + # unmatched rows into NA and dropping them from the plot. + if (is.factor(settings$datapoints[["x"]]) || + (is.null(names(xlabs_layer)) && + all(as.character(settings$datapoints[["x"]]) %in% names(xlabs_orig)))) { + # Case 1: relevel a factor (e.g., ribbon added to errorbars), or an + # atomic x whose values name the original layer's categories settings$datapoints[["x"]] = tryCatch( factor(settings$datapoints[["x"]], levels = names(xlabs_orig)), error = function(e) { diff --git a/R/assertions.R b/R/assertions.R index 0b9ab8f3..564a8297 100644 --- a/R/assertions.R +++ b/R/assertions.R @@ -103,16 +103,23 @@ is_string1 = function(x) { isTRUE(check_string(x)) && !is.na(x) } -# label formatter passed on to tinylabel(): a function, or one of its -# convenience strings (e.g. "percent"). With `list.ok`, several of them -- as a -# list or character vector -- are allowed too, e.g. one per facet variable. +# label formatter passed on to tinylabel(): a function, one of its convenience +# strings (e.g. "percent"), or a dictionary. With `list.ok`, several of them -- +# as a list or character vector -- are allowed too, e.g. one per facet variable. +# +# A dictionary is only recognised *inside* that list, never as `x` itself: at +# the top level a named vector is read as a per-variable mapping instead, which +# is what makes `list(Species = c(setosa = "SET"))` mean something different +# from a bare `c(setosa = "SET")`. match_facet_vars() enforces the top-level +# reading; this only has to let the nested one through. assert_labeller = function(x, name = as.character(substitute(x)), list.ok = FALSE) { if (is.null(x) || is_labeller(x)) return(invisible(TRUE)) if (isTRUE(list.ok) && (is.list(x) || is.character(x)) && length(x) >= 1L) { - if (all(vapply(x, is_labeller, logical(1L)))) return(invisible(TRUE)) + ok = vapply(x, function(xi) is_labeller(xi) || is_dict(xi), logical(1L)) + if (all(ok)) return(invisible(TRUE)) } msg = if (isTRUE(list.ok)) { - "`%s` must be a function or a `tinylabel()` convenience string, or a list of them (one per facet variable)." + "`%s` must be a function, a `tinylabel()` convenience string, or a dictionary of labels -- or a list of them, one per facet variable." } else { "`%s` must be a function, or a `tinylabel()` convenience string." } @@ -123,6 +130,18 @@ is_labeller = function(x) { is.function(x) || (is.character(x) && length(x) == 1L && !is.na(x)) } +# A tinylabel() dictionary: a *named* character vector, or a named list that +# flattens to one, mapping existing labels to their replacements. Deliberately +# mirrors what tinylabel() itself dispatches on (see R/tinylabel.R), so that +# what passes validation here is exactly what it can consume. Note the overlap +# with is_labeller() at length 1, where a one-entry dictionary and a +# convenience string cannot be told apart -- and need not be, since both are +# accepted either way. +is_dict = function(x) { + if (is.list(x) && !is.null(names(x))) x = unlist(x) + is.character(x) && !is.null(names(x)) +} + assert_length = function(x, len = 1, null.ok = FALSE, name = as.character(substitute(x))) { if (is.null(x) && isTRUE(null.ok)) { return(invisible(TRUE)) diff --git a/R/legend.R b/R/legend.R index 4922e78d..0dff41d1 100644 --- a/R/legend.R +++ b/R/legend.R @@ -542,7 +542,9 @@ prepare_legend = function(settings) { #' @param legend_args Additional legend arguments #' @param by_dep The (deparsed) "by" grouping variable name #' @param lgnd_labs The legend labels -#' @param labeller Character or function for formatting labels +#' @param labeller Function, character keyword, or dictionary (named vector or +#' list) for formatting or relabelling the labels. See [`tinylabel`] for the +#' accepted forms. #' @param type Plot type #' @param pch Plotting character(s) #' @param lty Line type(s) @@ -770,7 +772,9 @@ reverse_legend_keys = function(legend_args, n) { #' @param legend_args Additional legend arguments #' @param by_dep The (deparsed) "by" grouping variable name #' @param lgnd_labs The legend labels -#' @param labeller Character or function for formatting labels +#' @param labeller Function, character keyword, or dictionary (named vector or +#' list) for formatting or relabelling the labels. See [`tinylabel`] for the +#' accepted forms. #' @param type Plot type #' @param pch Plotting character(s) #' @param lty Line type(s) @@ -879,8 +883,9 @@ build_legend_env = function( #' \code{\link[graphics]{legend}}. #' @param by_dep The (deparsed) "by" grouping variable name. #' @param lgnd_labs The labels passed to `legend(legend = ...)`. -#' @param labeller Character or function for formatting the labels (`lgnd_labs`). -#' Passed down to [`tinylabel`]. +#' @param labeller Function, character keyword, or dictionary (named vector or +#' list) for formatting or relabelling the labels (`lgnd_labs`). See +#' [`tinylabel`] for the accepted forms. Passed down to [`tinylabel`]. #' @param type Plotting type(s), passed down from [tinyplot]. #' @param pch Plotting character(s), passed down from [tinyplot]. #' @param lty Plotting linetype(s), passed down from [tinyplot]. diff --git a/R/ordering_args.R b/R/ordering_args.R new file mode 100644 index 00000000..edd81cbf --- /dev/null +++ b/R/ordering_args.R @@ -0,0 +1,38 @@ +## Guards shared by the `*levels` / `*ord` argument pair. Both members control +## the same thing -- the order of a categorical variable's levels -- so these +## are named for that concept rather than for either argument. +## +## Note the pair is resolved by precedence rather than by a guard: `*levels` +## wins and `*ord` is skipped at the call site when both are given. That keeps +## the types free of supplied-vs-default bookkeeping, which would otherwise be +## needed because `type_errorbar()` and `type_pointrange()` default `xord` to +## "asis". + +## Warn when `*levels` / `*ord` were supplied for an axis that cannot be +## reordered. Types that coerce their categorical axis to a factor (barplot, +## ridge) never reach this -- there the arguments always apply. For the point +## and line family a numeric `x` is plotted at its own values, so the request +## is silently dropped, which is the failure mode worth surfacing. +## +## `supplied` is passed by the caller rather than inferred, because these +## arguments reach here through a closure: `type_errorbar()` and +## `type_pointrange()` default `xord` to "asis", and warning about a default +## the user never typed would fire on every numeric-x coefficient plot. +warn_ignored_ordering = function(v, xlevels, ord, supplied = TRUE) { + nms = c(deparse(substitute(xlevels)), deparse(substitute(ord))) + if (is.factor(v) || !isTRUE(supplied)) { + return(invisible(NULL)) + } + given = c(if (!is.null(xlevels)) nms[1L], if (!is.null(ord)) nms[2L]) + if (length(given) == 0L) { + return(invisible(NULL)) + } + warning( + sprintf( + "ignoring '%s': only categorical (factor or character) variables can be reordered.", + paste(given, collapse = "' and '") + ), + call. = FALSE + ) + invisible(NULL) +} diff --git a/R/sanitize_ord.R b/R/sanitize_ord.R index cde8f18c..b74bdbeb 100644 --- a/R/sanitize_ord.R +++ b/R/sanitize_ord.R @@ -5,16 +5,17 @@ ## - NULL: keep the existing factor levels (the default) ## - "asis": the categories in the order they appear in the data ## - "rev": the existing factor levels, reversed +## - "desc": rank by the group's ranking statistic, largest first +## - "asc": ditto, smallest first ## - "start": rank by the group's y value at the smallest x ## - "end": rank by the group's y value at the largest x -## - "total": rank by the group's summed y across every x ## - "minvar": rank by the group's variance, least variable first ## ## ... and, for anything else, a function that is handed each group's y values ## (ordered by x) and returns a single number to sort *ascending* on. So -## `function(y) -sum(y)` reproduces "total", and `function(y) sum(y)` reverses -## it. This is the escape hatch for the reverse direction, and for statistics we -## don't have a keyword for (`function(y) -median(y)`, etc.). +## `function(y) -sum(y)` reproduces a summed "desc", and `function(y) sum(y)` +## its "asc". This is the escape hatch for statistics we don't have a keyword +## for (`function(y) -median(y)`, etc.). ## ## A function that declares a formal named `x` also receives that group's x ## values, which is what any statistic depending on the spacing between @@ -24,19 +25,25 @@ ## from being silently handed x. x is passed by name, so the two arguments may ## be declared in either order. ## +## Only types whose categories span a real secondary axis pass one (`x = NULL` +## otherwise). Handing a barplot's ranking function the `by` level index would +## let `lm(y ~ x)` quietly return a number that means nothing, so asking for `x` +## where there is none is an error instead. +## ## "asis" and "rev" are the two keywords that consult no data at all -- they ## just permute the levels -- so they work when y is absent or non-numeric. ## "rev" is also the one thing a ranking function cannot express: a function is ## handed only its own group's y values, never its group identity or level ## index, so it has no way to say "put me where I already am, backwards". Note ## that it reverses the *existing* level order only; to reverse what another -## keyword computed, negate it with a function instead (`function(y) sum(y)` is -## the reverse of "total"). +## keyword computed, swap "asc" for "desc". ## -## The three size keywords rank largest first, i.e. into the first level, which -## is the bottom band of a stacked area. "minvar" ranks the *other* way -- -## smallest first -- because there the stable baseline is the calm group, not -## the big one. Both directions serve the same end. +## "start"/"end"/"minvar" each bake in a direction, since only one of the two +## is ever wanted: the size keywords rank largest first, i.e. into the first +## level, which is the bottom band of a stacked area, while "minvar" ranks the +## *other* way -- smallest first -- because there the stable baseline is the +## calm group, not the big one. Both directions serve the same end. Only +## "asc"/"desc" name a direction without naming a statistic; see `stat` below. ## ## Explicit level names or indexes are deliberately *not* accepted here -- that ## is what sanitize_xlevels() is for, and letting both arguments take the same @@ -54,24 +61,80 @@ ## category: in the degenerate case of a group literally called "end", set the ## factor levels beforehand instead. -ord_keywords = c("asis", "rev", "start", "end", "total", "minvar") +ord_keywords = c("asis", "rev", "start", "end", "asc", "desc", "minvar") + +## The three sets below track what a type's categories actually are, since that +## is what decides which keywords can mean anything: +## +## ord_keywords a series along a secondary axis (byord) +## ord_keywords_distribution a distribution, but no axis (points, ...) +## ord_keywords_scalar a single value (bars, spines) +## +## "start"/"end" name a position along a *secondary* axis, so they only mean +## what they say for categories that span one -- the `by` groups of a stacked +## area, say. Elsewhere they would silently collapse onto "desc" when there is +## no grouping, and silently re-read as "first/last `by` level" when there is. +ord_keywords_distribution = setdiff(ord_keywords, c("start", "end")) -sanitize_ord = function(v, y, x, ord, arg = "ord") { +## "minvar" then needs each category to carry a spread of its own: the scatter +## of points at an x position, the width of a ridge. A bar is a single +## aggregate and a spine a proportion of a count, so ranking either by variance +## would measure something the reader never sees -- cell values across +## `by`/facets for a bar (which stacking sums away and `beside` splits into +## separate bars), the supplied weights for a spine. +ord_keywords_scalar = setdiff(ord_keywords_distribution, "minvar") + +## Long-form spellings of the two direction keywords. Only "asc"/"desc" are +## documented and only they appear in the error below; these exist so that +## typing the word that comes naturally does not error. "inc"/"dec" are +## deliberately absent: in code they read first as increment/decrement. +ord_aliases = c( + ascending = "asc", increasing = "asc", + descending = "desc", decreasing = "desc" +) + +sanitize_ord = function(v, y, x, ord, arg = "ord", keywords = ord_keywords, stat = c("sum", "mean")) { + # A non-factor is not reordered at all, and the call sites already warn about + # that wholesale (see warn_ignored_ordering()), so there is nothing here to + # validate. Bailing out before the checks below also keeps us from raising a + # warning and an error over the same argument, which would leave the warning + # unmuffled by the caller's tryCatch(). if (is.null(ord) || !is.factor(v)) { return(v) } + stat = match.arg(stat) - keyword = is.character(ord) && length(ord) == 1L && ord %in% ord_keywords + # Normalise the long forms before anything else looks at `ord`, so that the + # keyword check, the error message and the branches below all see canonical + # spellings only. + if (is.character(ord) && length(ord) == 1L && ord %in% names(ord_aliases)) { + ord = unname(ord_aliases[[ord]]) + } + + keyword = is.character(ord) && length(ord) == 1L && ord %in% keywords if (!keyword && !is.function(ord)) { + hint = if (is.character(ord) && length(ord) == 1L && ord %in% ord_keywords) { + sprintf("\n \"%s\" is not available for this plot type.", ord) + } else { + "\n To set the level order explicitly, use factor(levels = ) on the variable beforehand." + } stop( sprintf( - "`%s` must be NULL, one of %s, or a function.\n To set the level order explicitly, use factor(levels = ) on the variable beforehand.", - arg, paste(sprintf('"%s"', ord_keywords), collapse = ", ") + "`%s` must be NULL, one of %s, or a function.%s", + arg, paste(sprintf('"%s"', keywords), collapse = ", "), hint ), call. = FALSE ) } + # One level has exactly one ordering, so there is no work left to do -- but + # only past the validation above, so that a typo is still caught on data that + # happens to hold a single category today and two tomorrow. The "minvar" + # degeneracy check below would misfire here too. + if (nlevels(v) < 2L) { + return(v) + } + # "asis" and "rev" need no y, and must work when y is absent or non-numeric. # factor() defaults `ordered` to is.ordered(v), so an ordered grouping stays # ordered (and keeps its sequential palette) through either. @@ -82,30 +145,93 @@ sanitize_ord = function(v, y, x, ord, arg = "ord") { return(factor(v, levels = rev(levels(v)))) } + # Everything below ranks on numbers. Reaching here with a non-numeric is + # almost always a transposed formula (e.g. a ridge plot called with the + # continuous variable on the categorical side), so say that rather than + # letting var()/sum() fail with something cryptic about factors. + if (!is.numeric(y)) { + # Name `ord` only when it is a keyword: sprintf() cannot coerce a closure, + # so interpolating a ranking function here would replace this diagnostic + # with an internal error about closures. + culprit = if (keyword) { + sprintf("`%s = \"%s\"`", arg, ord) + } else { + sprintf("the `%s` function", arg) + } + stop( + sprintf( + "%s ranks on a numeric variable, but was given %s.\n Only \"asis\" and \"rev\" work without one.", + culprit, class(y)[1L] + ), + call. = FALSE + ) + } + if (identical(ord, "minvar")) { # Ascending, i.e. *not* negated like the size keywords below: a stacked # baseline is steadiest when the least variable group sits on it, since # every band above inherits its movement. Groups too short to have a # variance give NA and sort last (to the top), which is the right place # for them anyway. - stat = tapply(y, v, function(z) var(z, na.rm = TRUE), default = NA_real_) + score = tapply(y, v, function(z) var(z, na.rm = TRUE), default = NA_real_) + # A variance that is NA everywhere (one observation per group) or identical + # everywhere (constant weights) cannot order anything, and would otherwise + # return the input untouched -- a silent no-op is the worst outcome here. + if (length(unique(score)) < 2L) { + stop( + sprintf( + "`%s = \"minvar\"` cannot order these groups: %s.", + arg, + if (all(is.na(score))) { + "each has fewer than two observations, so there is no variance to rank on" + } else { + "every group has the same variance" + } + ), + call. = FALSE + ) + } } else if (keyword) { - if (identical(ord, "total")) { - keep = rep.int(TRUE, length(x)) + # `stat` picks the summary the *reader* sees. Types whose categories carry + # one value each -- a bar's height, a spine's count, one band of a stacked + # area at a given x -- sum, so that pooling across `by`/facets adds up the + # way the drawing does. Types whose categories carry a whole distribution + # (a column of points, a ridge) average instead: summing there ranks by + # group size, so a category of many small values outranks one of few large + # ones even though every one of its observations is lower. + agg = if (identical(stat, "mean")) mean else sum + if (identical(ord, "asc") || identical(ord, "desc")) { + keep = rep.int(TRUE, length(y)) } else { edge = if (identical(ord, "start")) min(x, na.rm = TRUE) else max(x, na.rm = TRUE) keep = !is.na(x) & x == edge } - stat = tapply(y[keep], v[keep], function(z) sum(z, na.rm = TRUE), default = 0) - stat = -stat # largest group first, i.e. the bottom band + # An absent group has no mean, so it sorts last rather than to zero; under + # a sum, zero *is* its total and ranks it correctly among the others. + score = tapply( + y[keep], v[keep], function(z) agg(z, na.rm = TRUE), + default = if (identical(stat, "mean")) NA_real_ else 0 + ) + # "start"/"end" rank largest first, i.e. the bottom band; "asc" is the one + # keyword here that wants the raw ascending order. + if (!identical(ord, "asc")) score = -score } else { - xord = order(x) + xord = if (is.null(x)) seq_along(y) else order(x) grps = split(y[xord], v[xord]) # Hand over x too, but only to functions that ask for it by name; see the # note at the top of this file. want_x = "x" %in% names(formals(ord)) + if (want_x && is.null(x)) { + stop( + sprintf( + "the `%s` function asks for `x`, but this plot type has no secondary axis to supply.\n Its categories are a flat set, so drop the `x` argument and rank on `y` alone.", + arg + ), + call. = FALSE + ) + } xgrps = if (want_x) split(x[xord], v[xord]) else NULL - stat = vapply( + score = vapply( seq_along(grps), function(i) { z = grps[[i]] @@ -117,6 +243,6 @@ sanitize_ord = function(v, y, x, ord, arg = "ord") { } # seq_along() breaks ties on the existing level order; empty groups sort last - o = order(stat, seq_along(stat), na.last = TRUE) + o = order(score, seq_along(score), na.last = TRUE) factor(v, levels = levels(v)[o]) } diff --git a/R/sanitize_xlevels.R b/R/sanitize_xlevels.R index 0eb27f4a..a9431588 100644 --- a/R/sanitize_xlevels.R +++ b/R/sanitize_xlevels.R @@ -4,19 +4,18 @@ ## ## - NULL: keep the existing factor levels (the default everywhere ## except type_errorbar()/type_pointrange()) -## - "asis": take the categories in the order they appear in the data, -## i.e. skip the alphabetical sorting that factor() applies -## when coercing a character variable (cf. read.table's -## `as.is` argument) ## - character: the levels in the desired order ## - numeric: indexes into the existing levels, e.g. 3:1 ## ## Only affects factors (character variables have already been coerced by ## sanitize_datapoints() when this runs inside a type_data() function); any ## other class is returned untouched, so the argument is inert for numeric -## variables. A length-1 "asis" is always read as the keyword: in the -## degenerate case of a category literally named "asis", set the factor -## levels beforehand instead. +## variables. +## +## Data-derived orderings -- "asis", "rev", ranking by size or variance -- are +## deliberately *not* handled here. They belong to the sibling `*ord` arguments +## and sanitize_ord(); keeping the two vocabularies disjoint is what makes each +## argument name mean one thing. The two compose, `*levels` first. ## ## Site-specific follow-ups -- re-syncing `by` when it aliases the releveled ## variable (spineplot, ridge), or converting the factor to integer positions @@ -25,16 +24,41 @@ sanitize_xlevels = function(x, xlevels, arg = "xlevels") { if (is.null(xlevels) || !is.factor(x)) { return(x) } - if (identical(xlevels, "asis")) { - return(factor(x, levels = unique(x))) - } if (is.numeric(xlevels)) { xlevels = levels(x)[xlevels] } + v = substr(arg, 1, 1) + # Naming a strict subset silently sends every other level to NA, which drops + # those rows from the plot without a word. Ordering is all these arguments + # claim to do, so treat a shortfall as a mistake worth flagging -- and a + # complete miss (no supplied level matches at all) as fatal, since the + # all-NA factor it produces only surfaces later as an unrelated error about + # zero-length ranges. + # + # The fatal case has to be settled *before* either warning below, so that a + # complete miss aborts cleanly instead of warning on its way to the stop(). + # A warning raised en route to an error is not muffled by the caller's + # tryCatch/expect_error, so it escapes to R's deferred list and resurfaces, + # unattributed, at the end of whatever was running. + kept = intersect(levels(x), xlevels) + if (length(kept) == 0L) { + stop(sprintf( + "'%s' matches none of the levels of '%s'.\n Expected some of: %s", + arg, v, paste(sprintf('"%s"', levels(x)), collapse = ", ") + ), call. = FALSE) + } if (anyNA(xlevels) || !all(xlevels %in% levels(x))) { warning(sprintf( "not all '%s' correspond to levels of '%s'", - arg, substr(arg, 1, 1) + arg, v + )) + } + dropped = setdiff(levels(x), xlevels) + if (length(dropped) > 0L) { + warning(sprintf( + "'%s' omits %d of the %d levels of '%s' (%s); those observations will not be plotted", + arg, length(dropped), nlevels(x), v, + paste(sprintf('"%s"', dropped), collapse = ", ") )) } factor(x, levels = xlevels) diff --git a/R/tinylabel.R b/R/tinylabel.R index cd51e514..ce694ee2 100644 --- a/R/tinylabel.R +++ b/R/tinylabel.R @@ -4,12 +4,28 @@ #' ticks labels. This is what the top-level `xaxl` and `yaxl` arguments #' from [`tinyplot`] ultimately get passed to. #' @param x a numeric or character vector -#' @param labeller a formatting function to be applied to `x`, e.g. [`format`], -#' [`toupper`], [`abs`], or other custom function (including from the popular -#' **scales** package). Can also be one of the following convenience strings -#' (symbols), for which common formatting transformations are provided: -#' `"percent"` (`"%"`), `"comma"` (`","`), `"log"` (`"l"`), `"dollar"` -#' (`"$"`), `"euro"` (`"€"`), or `"sterling"` (`"£"`). +#' @param labeller how the labels should be relabelled or reformatted. One of: +#' +#' - a function to be applied to `x`, e.g. [`format`], [`toupper`], [`abs`], +#' or any other custom function (including from the popular **scales** +#' package). +#' - one of the following convenience strings (or their symbol equivalents), +#' for which common formatting transformations are provided: `"percent"` +#' (`"%"`), `"comma"` (`","`), `"log"` (`"l"`), `"dollar"` (`"$"`), `"euro"` +#' (`"€"`), or `"sterling"` (`"£"`). +#' - a dictionary, i.e. a *named* character vector or list of form +#' `c(old_lab = "new_lab", ...)`. Entries of `x` that match a name are +#' replaced by the corresponding value and the rest are left alone, so a +#' partial mapping is fine. The lookup is by value rather than by position, +#' which means it is unaffected by any reordering of the underlying +#' categories. +#' +#' Note that an *unnamed* character vector of length greater than one is an +#' error, rather than a positional replacement of the labels. Such a vector +#' cannot be distinguished from a formatting keyword when `x` is of length +#' one, and would not follow the categories if they were reordered. Pass a +#' function if you want to compute the labels positionally, e.g. +#' `function(x) LETTERS[seq_along(x)]`. #' @param na.ignore logical indicating whether the labelling function should #' ignore `NA` values in `x`. In other words, should the `NA` values be left #' as-is? Default is `TRUE`. @@ -25,6 +41,10 @@ #' tinylabel(x, "comma") #' tinylabel(x, ",") # same #' tinylabel(x, "$") # or "dollar" +#' +#' # a named vector acts as a dictionary: matched entries are replaced and the +#' # rest are left alone +#' tinylabel(c("setosa", "versicolor"), c(setosa = "SET")) #' #' # invoke tinylabel from a parent tinyplot call... #' # => x/yaxl for adjusting axes tick labels @@ -93,8 +113,32 @@ tinylabel = function(x, labeller = NULL, na.ignore = TRUE, na.rm = TRUE) { } else { seq_along(x) } + if (is.list(labeller) && !is.null(names(labeller))) { + labeller = unlist(labeller) + } if (is.character(labeller)) { - labeller = labeller_fun((labeller)) + # A *named* character vector is a dictionary: replace the labels it names + # and leave the rest alone. Names are what make this safe -- they cannot + # collide with the formatting keywords, they survive any `*ord`/`*levels` + # reordering (the mapping is by value, not by position), and a length + # mismatch is meaningless rather than silently recycled. + if (!is.null(names(labeller))) { + out = as.character(x) + hit = match(out, names(labeller)) + out[!is.na(hit)] = unname(labeller)[hit[!is.na(hit)]] + return(out) + } + # An unnamed vector is a formatting keyword, and only ever that. Positional + # replacement is deliberately not supported: it could not be told apart + # from a keyword on a one-tick axis, and it would not follow the categories + # when they are reordered. + if (length(labeller) != 1L) { + stop( + "a character `labeller` must be a single formatting keyword, or a named vector mapping old labels to new ones.\n For positional or computed labels, pass a function, e.g. `function(x) LETTERS[seq_along(x)]`.", + call. = FALSE + ) + } + labeller = labeller_fun(labeller) } # don't need to subset if everything is being used. DateTime also require # exception logic (e.g., date format needs to be consistent for whole vector) @@ -116,20 +160,18 @@ labeller_fun = function(label = "percent") { "\u00a3" = "sterling", "l" = "log" ) - if (label %in% names(labels)) { - label = labels[label] - } - - ## all labels plus absolute value version - # labels = c("percent", "comma", "dollar", "euro", "sterling") - labels = c(labels, paste0("abs_", labels)) - - ## match full label first, then store abs_ info separately - label = match.arg(label, labels) + ## Strip any "abs_" prefix *before* resolving a symbol to its full name. + ## Doing it the other way round leaves "abs_," unresolvable, which is exactly + ## what a centered barplot produces from a symbol keyword: it prepends the + ## prefix itself (see type_barplot()). abs_ = substr(label, 1L, 4L) == "abs_" if (abs_) { label = substr(label, 5L, nchar(label)) } + if (label %in% names(labels)) { + label = labels[label] + } + label = match.arg(label, unname(labels)) ## actual formatting functions @@ -137,7 +179,8 @@ labeller_fun = function(label = "percent") { # the unique values distinct, so a single consistent format can be applied to # the whole vector. Falls back to max_decimals. consistent_decimals = function(x, max_decimals = 5L) { - ux = unique(as.numeric(x)) + # Deduplicate at printed precision (#689) + ux = unique(round(as.numeric(x), max_decimals)) Find( function(d) { length(unique(sprintf(paste0("%.", d, "f"), ux))) == length(ux) diff --git a/R/tinyplot.R b/R/tinyplot.R index 2beefe67..81d94f11 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -89,8 +89,11 @@ #' `labeller = list(firm = toupper, yield = "%")`, with any variable left #' unnamed not formatted. While not recommended, unnamed values are matched #' positionally, according to the variable order in the `facet` formula -#' specification. Defaults to the value of `tpar("facet.labeller")`, which is -#' `NULL` (no formatting). +#' specification. Note that this per-variable naming claims the same slot that +#' a [`tinylabel`] dictionary would, so a dictionary has to be nested inside +#' it, e.g. `labeller = list(Species = c(setosa = "SET"))`; a bare named +#' vector is read as a per-variable mapping instead. Defaults to the value of +#' `tpar("facet.labeller")`, which is `NULL` (no formatting). #' - `prefix` a logical or character value for prefixing the facet titles with #' a descriptive name. Pass `TRUE` to prefix with the (deparsed) facet #' variable name(s), e.g. `"am = 0"` instead of just `"0"`. Alternatively, @@ -257,16 +260,12 @@ #' the break points at which the axis tick-marks are to be drawn. Break points #' outside the range of the data will be ignored if the associated axis #' variable is categorical, or an explicit `x/ylim` range is given. -#' @param xaxl,yaxl a function or a character keyword specifying the format of -#' the x- or y-axis tick labels. Note that this is a post-processing step that +#' @param xaxl,yaxl a function, character keyword, or dictionary (named vector +#' or list) for formatting or (re)labelling the x- or y-axis tick labels. +#' Passed to [`tinylabel`]; see the latter's help file for more detailed +#' documentation and examples. Note that this is a post-processing step that #' affects the _appearance_ of the tick labels only; use in conjunction with -#' `x/yaxb` if you would like to adjust the position of the tick marks too. In -#' addition to user-supplied formatting functions (e.g., [`format`], -#' [`toupper`], [`abs`], or other custom function), several convenience -#' keywords (or their symbol equivalents) are available for common formatting -#' transformations: `"percent"` (`"%"`), `"comma"` (`","`), `"log"` (`"l"`), -#' `"dollar"` (`"$"`), `"euro"` (`"€"`), or `"sterling"` (`"£"`). See the -#' [`tinylabel`] documentation for examples. +#' `x/yaxb` if you would like to adjust the position of the tick marks too. #' @param log a character string which contains `"x"` if the x axis is to be #' logarithmic, `"y"` if the y axis is to be logarithmic and `"xy"` or `"yx"` #' if both axes are to be logarithmic. @@ -1284,6 +1283,12 @@ tinyplot.default = function( par(mar = dynmar_computed + .whtsbp) } + # A "legend_reversed" type reads bottom-up, so its key is flipped to match. + # Under `flip = TRUE` the same groups run left-to-right instead, and a + # vertical key has no height to concur with -- reading it top-down against + # bands laid out left-to-right just runs it backwards. Drop the hint. + if (isTRUE(flip)) type_hints[["legend_reversed"]] = NULL + if (legend_draw_flag && !identical(legend_args[["x"]], "direct")) { if (!multi_legend) { ## simple case: single legend only diff --git a/R/tinyplot.matrix.R b/R/tinyplot.matrix.R index 03260bcd..f031ce2c 100644 --- a/R/tinyplot.matrix.R +++ b/R/tinyplot.matrix.R @@ -51,13 +51,20 @@ #' tinyplot(VADeaths, type = "b", legend = "direct", theme = "socviz") #' tinyplot(VADeaths, type = "b", legend = FALSE, facet = "by", theme = "socviz") #' -#' # equivalent plot to an example in `?matplot` +#' # digression: equivalent "o" plot to an example in `?matplot` #' sines = outer(1:20, 1:4, function(x, y) sin(x / 20 * pi * y)) #' tinyplot(sines, type = "o", pch = "by", lty = "by", col = rainbow(ncol(sines))) #' -#' # `"tile"` + `"heatmap"` types lay the matrix out as a grid instead +#' # back to VADeaths running example, we can pass down other types too... +#' +#' # heatmap #' tinyplot(VADeaths, type = "heatmap", theme = "heatmap", col = "white") #' +#' # barplot(s) +#' tinyplot(VADeaths, type = "barplot", beside = TRUE) +#' tinyplot(t(VADeaths), type = "barplot", beside = TRUE) +#' tinyplot(VADeaths, type = "barplot", facet = "by", legend = FALSE) +#' #' @export tinyplot.matrix = function(x, type = NULL, legend = NULL, facet = NULL, xlab = NULL, ylab = NULL, ...) { assert_choice(facet, "by", null.ok = TRUE) diff --git a/R/tpar.R b/R/tpar.R index dc6884c2..a481d42f 100644 --- a/R/tpar.R +++ b/R/tpar.R @@ -67,7 +67,7 @@ #' * `facet.cex`: Expansion factor for facet titles. Defaults to `1`. #' * `facet.col`: Character or integer specifying the facet text colour. If an integer, will correspond to the user's default global colour palette (see `palette`). Defaults to `NULL`, which is equivalent to "black". #' * `facet.font`: An integer corresponding to the desired font face for facet titles. For most font families and graphics devices, one of four possible values: `1` (regular), `2` (bold), `3` (italic), or `4` (bold italic). Defaults to `NULL`, which is equivalent to `1` (i.e., regular). -#' * `facet.labeller`: A formatting function (or [`tinylabel`] convenience string, e.g. `"percent"`) applied to the facet titles, or a list of them (or a character vector of convenience strings), optionally named for the facet variables they apply to, for formatting each facet variable differently. Defaults to `NULL` (no formatting). Applied to the underlying facet values, i.e. before any `facet.prefix` name is added. Equivalent to setting `tinyplot(..., facet.args = list(labeller = X))`, but globally. +#' * `facet.labeller`: A formatting function (or [`tinylabel`] convenience string, e.g. `"percent"`) applied to the facet titles, or a list of them (or a character vector of convenience strings), optionally named for the facet variables they apply to, for formatting each facet variable differently. Note that this per-variable naming claims the same slot that a [`tinylabel`] dictionary would, so a dictionary has to be nested inside it, e.g. `facet.labeller = list(Species = c(setosa = "SET"))`. Defaults to `NULL` (no formatting). Applied to the underlying facet values, i.e. before any `facet.prefix` name is added. Equivalent to setting `tinyplot(..., facet.args = list(labeller = X))`, but globally. #' * `facet.prefix`: Logical or character controlling whether facet titles are prefixed with their variable name, e.g. `"vs = 0"` rather than just `"0"`. `TRUE` uses the variable name(s), while a character string---or a vector or list of them, one element per facet variable, optionally named for the variables they apply to---supplies custom name(s) instead. Defaults to `NULL`, which is equivalent to `FALSE` (no prefix). Equivalent to setting `tinyplot(..., facet.args = list(prefix = X))`, but globally. #' * `facet.sep`: Character string separating the individual variables of a multi-variable facet title, e.g. `"\n"` to stack them on separate lines. Ignored for single-variable facets. Defaults to `NULL`, i.e. the `":"` that the variables were combined with, or `", "` if they are prefixed via `facet.prefix` (above). Equivalent to setting `tinyplot(..., facet.args = list(sep = X))`, but globally. #' * `file.height`: Numeric specifying the height (in inches) of any plot that is written to disk using the `tinyplot(..., file = X)` argument. Defaults to `7`. diff --git a/R/type_barplot.R b/R/type_barplot.R index e4589734..f396c54b 100644 --- a/R/type_barplot.R +++ b/R/type_barplot.R @@ -19,20 +19,45 @@ #' or the mid-way in the third category, respectively. #' @param FUN a function to compute the summary statistic for `y` within each #' group of `x` in case of using a two-sided formula `y ~ x` (default: mean). -#' @param xlevels a character or numeric vector specifying the ordering of the -#' levels of the `x` variable (if character) or the corresponding indexes -#' (if numeric) for the plot. The special keyword `"asis"` takes the -#' categories in the order that they appear in the data. Note that this -#' argument only affects categorical (i.e., factor or character) `x` -#' variables. -#' @param xaxlabels a character vector with the axis labels for the `x` variable, -#' defaulting to the levels of `x`. +#' @param xlevels,xord arguments controlling the order of the `x` variable, and +#' hence of the x-axis. Supply one or the other; if both arguments are +#' provided, `xlevels` takes precedence and `xord` is silently ignored. +#' +#' - `xlevels` specifies the levels _literally_, either a character vector of +#' level names in the desired order (e.g., `c("C", "B", "A")`), or a numeric +#' vector of the corresponding level indexes (e.g. `3:1`). +#' +#' - `xord` instead accepts a keyword or custom function, which then _derives_ +#' the order from the data. Options are: +#' +#' - `"desc(ending)"` and `"asc(ending)"` rank (sort) the categories by bar +#' height, tallest or shortest first. Both the abbreviated and long form +#' strings are permitted, as are the `"decreasing"` and `"increasing"` +#' aliases. Note that the ranking is applied to the *aggregated* bars, i.e. +#' whatever `FUN` produced, rather than the underlying rows. With `by` +#' groups or facets, a single ordering is computed and shared across all of +#' them, by summing each category's bars over every group and facet. For +#' stacked bars that sum is the height of the full stack; with +#' `beside = TRUE` it is the group total rather than any individual bar. (A +#' factor carries one level order, so a per-facet ranking is not +#' expressible.) +#' - `"asis"` or `"rev"` permute the existing levels without consulting the +#' data at all. The former takes the categories in the order that they +#' appear in the data, while the latter reverses the current level order. +#' - a custom function that determines both the ranking statistic and its +#' direction. The statistic is always sorted in ascending order, so +#' `function(y) -median(y)` ranks by median, largest first. +#' +#' Note that a numeric `x` is coerced to a factor before the bars are drawn, +#' so it is reordered like any other categorical variable. +#' Each argument defaults to `NULL`, i.e. keep the existing factor levels. #' @param offset optional specification for shifting bar baselines, accepting #' one of two distinct forms. See the Examples for illustrations of both. #' #' - *Positions* via an unnamed numeric scalar or vector. Bars start at the #' offset value(s) rather than zero, matched per x-level after any `xlevels` -#' reordering (a scalar is applied to all bars). Useful for waterfall charts. +#' or `xord` reordering (a scalar is applied to all bars). Useful for +#' waterfall charts. #' The positional form cannot be combined with `center`. #' - *Category* via a character vector such as `offset = "Unsure"`, or a #' named numeric vector such as `offset = c(Unsure = 1.1)`. The named @@ -48,43 +73,99 @@ #' series colour(s)? Default is `TRUE`, which keeps single- and multi-group #' displays consistent and lets the fill read cleanly over grid lines. Set to #' `FALSE` to use the fully-saturated palette colour(s) instead. +#' @param xaxlabels \[Deprecated\] a character vector with the axis labels for +#' the `x` variable. Use the top-level `xaxl` argument instead (see +#' [`tinylabel`]). This argument will be removed in a future release. #' #' @examples -#' # Basic examples of frequency tables (without y variable) -#' tinyplot(~ cyl, data = mtcars, type = "barplot") -#' tinyplot(~ cyl | vs, data = mtcars, type = "barplot") -#' tinyplot(~ cyl | vs, data = mtcars, type = "barplot", beside = TRUE) +#' # +#' ## Basic use (raw values) +#' +#' tinyplot(GNP ~ Year, data = longley, type = "barplot") +#' +#' tinyplot(demand ~ Time, data = BOD, type = "bar") # "bar" is a shorthand +#' tinyplot_add(type = "text", pos = 3, xpd = NA) # add y values as text +#' +#' # reordering (just to demonstrate; these aren't sensible for a time variable) +#' tinyplot(demand ~ Time, data = BOD, type = "bar", xord = "asc") +#' jumble = c("7","1","5","2","4","3") # note: Time = 6 is also missing +#' tinyplot(demand ~ Time, data = BOD, type = "bar", xlevels = jumble) #' -#' # Reorder x variable categories either by their character levels or numeric indexes -#' tinyplot(~ cyl, data = mtcars, type = "barplot", xlevels = c("8", "6", "4")) -#' tinyplot(~ cyl, data = mtcars, type = "barplot", xlevels = 3:1) +#' # +#' ## Aggregated vs grouped values (multiple ys per x) +#' +#' # each person receives two drugs +#' sleep2 = transform(sleep, drug = group) # less misleading name +#' +#' # default aggregation FUN is mean +#' tinyplot( +#' extra ~ ID, data = sleep2, +#' type = "barplot", +#' main = "Mean extra sleep from 2 soporific drugs" +#' ) +#' # switch to diff (answers a more relevant q: who benefits most from drug 2?) +#' tinyplot( +#' extra ~ ID, data = sleep2, +#' type = "barplot", FUN = diff, +#' main = "Sleep gain (drug 2 vs drug 1)" +#' ) +#' # we can sort in descending (or ascending) order too +#' tinyplot( +#' extra ~ ID, data = sleep2, +#' type = "barplot", FUN = diff, xord = "desc", +#' main = "Sleep gain (drug 2 vs drug 1), ordered" +#' ) #' -#' # Note: Above we used automatic argument passing for `beside`. But this -#' # wouldn't work for `width`, since it would conflict with the top-level +#' # of course, we don't have to aggregate if we specify groups (stacked or non) +#' tinyplot(extra ~ ID | drug, data = sleep2, type = "barplot", beside = TRUE) + +#' # Note: We used automatic argument passing for 'xord', `FUN`, etc. above. But +#' # this wouldn't work for `width`, since it would conflict with the top-level #' # `tinyplot(..., width = )` argument. It's safer to pass these args -#' # through the `type_barplot()` functional equivalent. +#' # through the `type_barplot()` functional equivalent... +#' #' tinyplot( -#' ~ cyl | vs, data = mtcars, -#' type = type_barplot(beside = TRUE, drop.zeros = TRUE, width = 0.65) +#' extra ~ ID | drug, data = sleep2, +#' type = type_barplot(beside = TRUE, xord = "desc", width = 0.5) #' ) #' -#' # Example for numeric y aggregated by x (default: FUN = mean) + facets +#' # speaking of top-level args, use xaxl to format the x labels, e.g. with a +#' # dictionary, keyword, or (here:) function +#' #' tinyplot( -#' extra ~ ID | group, facet = "by", data = sleep, -#' type = "barplot", -#' theme = "clean2" +#' extra ~ ID | drug, data = sleep2, +#' type = type_barplot(beside = TRUE, xord = "desc"), +#' xaxl = as.roman #' ) #' -#' # Fancy frequency table: +#' # +#' ## matrix method (no formula required) +#' +#' tinyplot(VADeaths, type = "barplot") +#' tinyplot(VADeaths, type = "barplot", beside = TRUE) +#' # etc. see ?tinyplot.matrix +#' +#' # +#' ## Frequency tables +#' +#' # No y variable (frequency calculated on the fly) +#' tinyplot(~ cyl, data = mtcars, type = "barplot") +#' tinyplot(~ cyl | vs, data = mtcars, type = "barplot") +#' tinyplot(~ cyl | vs, data = mtcars, type = "barplot", beside = TRUE) +#' +#' # Fancy frequency table (y = frequency aleady computed) #' tinyplot( #' Freq ~ Sex | Survived, data = as.data.frame(Titanic), #' facet = ~ Class, facet.args = list(nrow = 1), -#' type = "barplot", flip = TRUE, +#' type = "barplot", beside = TRUE, flip = TRUE, #' theme = "clean2" #' ) +#' +#' # +#' ## Centering #' -#' # Centered barplot for conditional proportions of dark (black/brown) vs. -#' # light (red/blond) hair color, conditional on eye color and sex. +#' # Centered barplot for conditional proportions of "dark" (black/brown) vs. +#' # "fair" (red/blond) hair color, conditional on eye color and sex. #' # Aside: use `lighten = FALSE` to avoid lightening the bar fill colors. #' hec = as.data.frame(proportions(HairEyeColor, 2:3)) #' hcols = c("black", "sienna", "indianred", "goldenrod") @@ -95,8 +176,10 @@ #' flip = TRUE, yaxl = "percent", #' theme = list("clean2", palette.qualitative = hcols) #' ) +#' tinyplot_add(type = "vline", col = "white") #' -#' # Use cases for the `offset` argument +#' # +#' ## Offset examples #' #' # 1. Waterfall plot #' d = data.frame(item = c("Sales", "Services", "Costs", "Returns", "TOTAL"), @@ -138,11 +221,20 @@ #' main = "Hypothetical Likert example with category offset" #' ) #' tinyplot_add(type = "vline") +#' tinyplot_add(type = "vline", v = 1, lty = 2) #' #' @export -type_barplot = function(width = 5/6, beside = FALSE, center = FALSE, offset = NULL, FUN = NULL, xlevels = NULL, xaxlabels = NULL, drop.zeros = FALSE, lighten = TRUE) { +type_barplot = function(width = 5/6, beside = FALSE, center = FALSE, offset = NULL, FUN = NULL, xlevels = NULL, xord = NULL, drop.zeros = FALSE, lighten = TRUE, xaxlabels = NULL) { + if (!is.null(xaxlabels)) { + warning( + "'xaxlabels' is deprecated; use the top-level 'xaxl' argument instead, ", + "e.g. tinyplot(..., xaxl = c(old = \"new\")) to rename particular ", + "categories, or xaxl = function(x) ... to compute the labels.", + call. = FALSE + ) + } out = list( - data = data_barplot(width = width, beside = beside, center = center, offset = offset, FUN = FUN, xlevels = xlevels, xaxlabels = xaxlabels, drop.zeros = drop.zeros, lighten = lighten), + data = data_barplot(width = width, beside = beside, center = center, offset = offset, FUN = FUN, xlevels = xlevels, xord = xord, xaxlabels = xaxlabels, drop.zeros = drop.zeros, lighten = lighten), draw = draw_rect(), name = "barplot" ) @@ -151,7 +243,7 @@ type_barplot = function(width = 5/6, beside = FALSE, center = FALSE, offset = NU } #' @importFrom stats aggregate -data_barplot = function(width = 5/6, beside = FALSE, center = FALSE, offset = NULL, FUN = NULL, xlevels = NULL, xaxlabels = NULL, drop.zeros = FALSE, lighten = TRUE) { +data_barplot = function(width = 5/6, beside = FALSE, center = FALSE, offset = NULL, FUN = NULL, xlevels = NULL, xord = NULL, xaxlabels = NULL, drop.zeros = FALSE, lighten = TRUE) { fun = function(settings, ...) { env2env( settings, @@ -175,11 +267,34 @@ data_barplot = function(width = 5/6, beside = FALSE, center = FALSE, offset = NU } if (!is.factor(datapoints$x)) datapoints$x = factor(datapoints$x) datapoints$x = sanitize_xlevels(datapoints$x, xlevels) + ## "asis" means "the order the categories appear in the data", and the + ## aggregate() below destroys that by sorting on the grouping columns. + ## It consults no `y`, so apply it here while the row order still + ## survives. The ranking keywords have the opposite requirement -- they + ## must see the aggregated bars -- and so stay below. + if (identical(xord, "asis") && is.null(xlevels)) { + datapoints$x = sanitize_ord( + datapoints$x, NULL, NULL, + xord, arg = "xord", keywords = ord_keywords_scalar + ) + } if (!is.null(xaxlabels)) levels(datapoints$x) = xaxlabels datapoints = aggregate(datapoints[, "y", drop = FALSE], datapoints[, c("x", "by", "facet")], FUN = FUN, drop = FALSE) datapoints$y[is.na(datapoints$y)] = 0 #FIXME: always?# if (!is.factor(datapoints$by)) datapoints$by = factor(datapoints$by) if (!is.factor(datapoints$facet)) datapoints$facet = factor(datapoints$facet) + + ## `xord` ranks on the *aggregated* bars, so it has to run after the + ## aggregate() above -- ranking the raw cells would sort on sums while + ## the plot draws whatever FUN produced. It also has to run before the + ## `offset` block below, which is keyed positionally by x-level. + if (!is.null(xord) && !identical(xord, "asis") && is.null(xlevels)) { + datapoints$x = sanitize_ord( + datapoints$x, datapoints$y, NULL, + xord, arg = "xord", keywords = ord_keywords_scalar + ) + datapoints = datapoints[order(datapoints$facet, datapoints$by, datapoints$x), , drop = FALSE] + } ## `offset` accepts two distinct forms: ## - unnamed numeric -> positional, keyed by x-level (waterfall) diff --git a/R/type_errorbar.R b/R/type_errorbar.R index 45d67ee6..c5e4ee95 100644 --- a/R/type_errorbar.R +++ b/R/type_errorbar.R @@ -4,17 +4,37 @@ #' #' @inheritParams dodge_positions #' @inheritParams graphics::arrows -#' @param xlevels a character or numeric vector specifying the order in which -#' the levels of the `x` variable should be plotted (as level names if -#' character, or level indexes if numeric, e.g. `3:1`). Note that this -#' argument only affects categorical (i.e., factor or character) `x` -#' variables; it is ignored for numeric `x`. Unlike most other plot types, -#' here it defaults to the special keyword `"asis"`, which takes the -#' categories in the order that they appear in the data: these types are -#' typically used for coefficient plots, where the row order of the data -#' (e.g., the terms of a model) is usually intentional. Set -#' `xlevels = NULL` to follow the factor levels instead, matching the other -#' plot types. +#' @param xlevels,xord arguments controlling the order of the `x` variable, and +#' hence of the x-axis. Supply one or the other; if both arguments are +#' provided, `xlevels` takes precedence and `xord` is silently ignored. +#' +#' - `xlevels` specifies the levels _literally_, either a character vector of +#' level names in the desired order (e.g., `c("C", "B", "A")`), or a numeric +#' vector of the corresponding level indexes (e.g. `3:1`). +#' +#' - `xord` instead accepts a keyword or custom function, which then _derives_ +#' the order from the data. Options are: +#' +#' - `"desc"` and `"asc"` rank the categories by their mean `y` value, +#' largest or smallest first. (Long forms like `"descending"` and `"increasing"` are also accepted.) +#' - `"minvar"` ranks them by variance, lowest first. This needs more than +#' one observation per category, so it does not apply to the usual +#' one-row-per-term coefficient table. +#' - `"asis"` or `"rev"` permute the existing levels without consulting the +#' data at all. The former takes the categories in the order that they +#' appear in the data, while the latter reverses the current level order. +#' - a custom function that determines both the ranking statistic and its +#' direction. The statistic is always sorted ascending, so +#' `function(y) -median(y)` ranks by median, largest first. +#' +#' Note that `x` is only reordered when it is categorical (i.e., factor or +#' character). A numeric `x` is plotted at its own values and cannot be +#' reordered, so supplying either argument there is ignored with a warning. +#' +#' Unlike most other plot types, `xord` defaults to `"asis"` here rather than +#' `NULL`: these types are typically used for coefficient plots, where the row +#' order of the data (e.g., the terms of a model) is usually intentional. Set +#' `xord = NULL` to follow the factor levels instead, matching the other types. #' @examples #' tinytheme("basic") #' @@ -97,10 +117,11 @@ #' tinytheme() # reset theme #' #' @export -type_errorbar = function(length = 0.05, dodge = 0, fixed.dodge = FALSE, xlevels = "asis") { +type_errorbar = function(length = 0.05, dodge = 0, fixed.dodge = FALSE, xlevels = NULL, xord = "asis") { + ord_supplied = !missing(xord) || !is.null(xlevels) out = list( draw = draw_errorbar(length = length), - data = data_pointrange(dodge = dodge, fixed.dodge = fixed.dodge, xlevels = xlevels), + data = data_pointrange(dodge = dodge, fixed.dodge = fixed.dodge, xlevels = xlevels, xord = xord, ord_supplied = ord_supplied), name = "p" ) class(out) = "tinyplot_type" diff --git a/R/type_lines.R b/R/type_lines.R index b5f29032..46a90f20 100644 --- a/R/type_lines.R +++ b/R/type_lines.R @@ -12,7 +12,7 @@ #' character) data according to the factor levels. Character variables are #' coerced with [factor()] and so end up in alphabetical order. To order the #' categories by their appearance in the data instead, use -#' `xlevels = "asis"`, or set the levels explicitly, e.g. +#' `xord = "asis"`, or set the levels explicitly, e.g. #' `factor(x, levels = unique(x))`. #' #' Note that the lines themselves are always drawn in the order that the rows @@ -48,10 +48,10 @@ #' ) #' #' @export -type_lines = function(type = "l", dodge = 0, fixed.dodge = FALSE, xlevels = NULL) { +type_lines = function(type = "l", dodge = 0, fixed.dodge = FALSE, xlevels = NULL, xord = NULL) { out = list( draw = draw_lines(type = type), - data = data_lines(dodge = dodge, fixed.dodge = fixed.dodge, xlevels = xlevels), + data = data_lines(dodge = dodge, fixed.dodge = fixed.dodge, xlevels = xlevels, xord = xord), name = type ) class(out) = "tinyplot_type" @@ -59,7 +59,7 @@ type_lines = function(type = "l", dodge = 0, fixed.dodge = FALSE, xlevels = NULL } -data_lines = function(dodge = 0, fixed.dodge = FALSE, xlevels = NULL) { +data_lines = function(dodge = 0, fixed.dodge = FALSE, xlevels = NULL, xord = NULL) { fun = function(settings, ...) { env2env(settings, environment(), "datapoints") @@ -69,6 +69,17 @@ data_lines = function(dodge = 0, fixed.dodge = FALSE, xlevels = NULL) { # `factor(x, levels = ...)` is honoured, and that layering a line type onto # a point type (or vice versa) lands on the same categories. #679 datapoints[["x"]] = sanitize_xlevels(datapoints[["x"]], xlevels) + warn_ignored_ordering(datapoints[["x"]], xlevels, xord) + # `xord` must run here, before the factor is collapsed to integer + # positions below -- once x is an integer there are no levels left to + # reorder. + if (!is.null(xord) && is.null(xlevels)) { + datapoints[["x"]] = sanitize_ord( + datapoints[["x"]], datapoints[["y"]], NULL, + xord, arg = "xord", keywords = ord_keywords_distribution, + stat = "mean" + ) + } if (is.factor(datapoints[["x"]])) { xlvls = levels(datapoints[["x"]]) xlabs = seq_along(xlvls) diff --git a/R/type_pointrange.R b/R/type_pointrange.R index 9f8cddd8..2e64df95 100644 --- a/R/type_pointrange.R +++ b/R/type_pointrange.R @@ -1,9 +1,10 @@ #' @rdname type_errorbar #' @export -type_pointrange = function(dodge = 0, fixed.dodge = FALSE, xlevels = "asis") { +type_pointrange = function(dodge = 0, fixed.dodge = FALSE, xlevels = NULL, xord = "asis") { + ord_supplied = !missing(xord) || !is.null(xlevels) out = list( draw = draw_pointrange(), - data = data_pointrange(dodge = dodge, fixed.dodge = fixed.dodge, xlevels = xlevels), + data = data_pointrange(dodge = dodge, fixed.dodge = fixed.dodge, xlevels = xlevels, xord = xord, ord_supplied = ord_supplied), name = "p" ) class(out) = "tinyplot_type" @@ -47,17 +48,26 @@ draw_pointrange = function() { } -data_pointrange = function(dodge, fixed.dodge, xlevels = "asis") { +data_pointrange = function(dodge, fixed.dodge, xlevels = NULL, xord = "asis", ord_supplied = TRUE) { fun = function(settings, ...) { env2env(settings, environment(), c("datapoints", "xlabs", "cex", "lty", "lwd")) if (is.character(datapoints$x)) { datapoints$x = as.factor(datapoints$x) } - ## default xlevels = "asis" preserves the row order of the data (i.e., no + ## default xord = "asis" preserves the row order of the data (i.e., no ## new sorting by factor), since these types are typically used for ## coefficient plots where that order is intentional + warn_ignored_ordering(datapoints$x, xlevels, xord, supplied = ord_supplied) datapoints$x = sanitize_xlevels(datapoints$x, xlevels) + # before the collapse to integer positions below + if (!is.null(xord) && is.null(xlevels)) { + datapoints$x = sanitize_ord( + datapoints$x, datapoints[["y"]], NULL, + xord, arg = "xord", keywords = ord_keywords_distribution, + stat = "mean" + ) + } if (is.factor(datapoints$x)) { xlvls = levels(datapoints$x) xlabs = seq_along(xlvls) diff --git a/R/type_points.R b/R/type_points.R index 557b48cc..31c2fc55 100644 --- a/R/type_points.R +++ b/R/type_points.R @@ -3,15 +3,32 @@ #' @description Type function for plotting points, i.e. a scatter plot. #' @param clim Numeric giving the lower and upper limits of the character #' expansion (`cex`) normalization for bubble charts. -#' @param xlevels a character or numeric vector specifying the order in which -#' the levels of the `x` variable should be plotted (as level names if -#' character, or level indexes if numeric, e.g. `3:1`). The special keyword -#' `"asis"` takes the categories in the order that they appear in the data, -#' i.e. skipping the alphabetical sort that is otherwise applied when -#' coercing a character variable to a factor. Note that this argument only -#' affects categorical (i.e., factor or character) `x` variables; it is -#' ignored for numeric `x`. The default `NULL` keeps the existing factor -#' levels (alphabetical for character variables). +#' @param xlevels,xord arguments controlling the order of the (categorical) `x` +#' variable, and hence of the x-axis. Supply one or the other; if both +#' arguments are provided, `xlevels` takes precedence and `xord` is silently +#' ignored. +#' +#' - `xlevels` specifies the levels _literally_, either a character vector of +#' level names in the desired order (e.g., `c("C", "B", "A")`), or a numeric +#' vector of the corresponding level indexes (e.g. `3:1`). +#' +#' - `xord` instead accepts a keyword or custom function, which then _derives_ +#' the order from the data. Options are: +#' +#' - `"desc"` and `"asc"` rank the categories by their mean `y` value, +#' largest or smallest first. (Long forms like `"descending"` and `"increasing"` are also accepted.) +#' - `"minvar"` ranks them by the variance of those values, lowest first. +#' - `"asis"` or `"rev"` permute the existing levels without consulting the +#' data at all. The former takes the categories in the order that they +#' appear in the data, while the latter reverses the current level order. +#' - a custom function that determines both the ranking statistic and its +#' direction. The statistic is always sorted ascending, so +#' `function(y) -median(y)` ranks by median, largest first. +#' +#' Note that `x` is only reordered when it is categorical (i.e., factor or +#' character). A numeric `x` is plotted at its own values and cannot be +#' reordered, so supplying either argument there is ignored with a warning. +#' Each argument defaults to `NULL`, i.e. keep the existing factor levels. #' @inheritParams dodge_positions #' #' @examples @@ -41,9 +58,9 @@ #' pch = 21, fill = 0.3) #' #' @export -type_points = function(clim = c(0.5, 2.5), dodge = 0, fixed.dodge = FALSE, xlevels = NULL) { +type_points = function(clim = c(0.5, 2.5), dodge = 0, fixed.dodge = FALSE, xlevels = NULL, xord = NULL) { out = list( - data = data_points(clim = clim, dodge = dodge, fixed.dodge = fixed.dodge, xlevels = xlevels), + data = data_points(clim = clim, dodge = dodge, fixed.dodge = fixed.dodge, xlevels = xlevels, xord = xord), draw = draw_points(), name = "p" ) @@ -51,7 +68,7 @@ type_points = function(clim = c(0.5, 2.5), dodge = 0, fixed.dodge = FALSE, xleve return(out) } -data_points = function(clim = c(0.5, 2.5), dodge = 0, fixed.dodge = FALSE, xlevels = NULL) { +data_points = function(clim = c(0.5, 2.5), dodge = 0, fixed.dodge = FALSE, xlevels = NULL, xord = NULL) { fun = function(settings, ...) { env2env(settings, environment(), "datapoints") @@ -60,6 +77,17 @@ data_points = function(clim = c(0.5, 2.5), dodge = 0, fixed.dodge = FALSE, xleve # catch for factors (we should still be able to "force" plot these with points) datapoints$x = sanitize_xlevels(datapoints$x, xlevels) + warn_ignored_ordering(datapoints$x, xlevels, xord) + # `xord` must run here, before the factor is collapsed to integer + # positions below -- once x is an integer there are no levels left to + # reorder. + if (!is.null(xord) && is.null(xlevels)) { + datapoints$x = sanitize_ord( + datapoints$x, datapoints[["y"]], NULL, + xord, arg = "xord", keywords = ord_keywords_distribution, + stat = "mean" + ) + } if (is.factor(datapoints$x)) { xlvls = levels(datapoints$x) xlabs = seq_along(xlvls) diff --git a/R/type_ribbon.R b/R/type_ribbon.R index 01765a9e..78c7e206 100644 --- a/R/type_ribbon.R +++ b/R/type_ribbon.R @@ -11,25 +11,28 @@ #' @param byord keyword string or function. Permits on-the-fly (re)ordering of #' the `by` group layers, thus controlling the order in which they stack. #' Options are: -#' -#' - `"start"`, `"end"`, and `"total"` are positional keywords that rank groups -#' according to their `y` values along the `x` axis. In each case, the group -#' with the largest value is stacked first as the bottom layer. -#' - `"minvar"` ranks by variance and puts the lowest variance group on the +#' +#' - `"desc"` and `"asc"` rank the groups by their summed `y` across the `x` +#' axis, largest or smallest first. With `"desc"` the biggest group is stacked +#' first, as the bottom layer. (Long forms like `"descending"` and `"increasing"` are also accepted.) +#' - `"start"` and `"end"` instead rank on the `y` values at the smallest and +#' largest `x` respectively, again stacking the largest group first. +#' - `"minvar"` ranks by variance, putting the lowest variance group on the #' baseline. -#' - `"asis"` and `"rev"` permute the existing levels without consulting the +#' - `"asis"` or `"rev"` permute the existing levels without consulting the #' data at all. The former takes the groups in the order that they appear in -#' the data, while `"rev"` reverses the current level order. -#' - custom function that determines both the ranking statistic and its -#' direction, e.g. `function(y) -median(y)` would layer by median `y` value, -#' from the biggest to the smallest. Note: if a function requires access to a -#' group's `x` values, then one of its arguments _must_ be named `x`, e.g. +#' the data, while the latter reverses the current level order. +#' - a custom function that determines both the ranking statistic and its +#' direction. The statistic is always sorted ascending, so +#' `function(y) -median(y)` layers by median `y` value, from the biggest to +#' the smallest. Note that if a function requires access to a group's `x` +#' values, then one of its arguments _must_ be named `x`, e.g. #' `function(y, x) coef(lm(y ~ x))[2]` would layer by trend. -#' -#' Default is `NULL`, in which case the existing factor level order is -#' retained; to set that order explicitly, call `factor(levels = ...)` on the -#' grouping variable beforehand. See Examples, as well as the "Stacked area -#' plots" section below. +#' +#' Defaults to `NULL`, i.e. keep the existing factor levels. To set that order +#' explicitly, call `factor(levels = ...)` on the grouping variable +#' beforehand. See Examples, as well as the "Stacked area plots" section +#' below. #' @param FUN a function for collapsing repeated `y` values within a group and #' `x` position, used only when `stack = TRUE`. Defaults to `mean`, matching #' [`type_barplot()`], so that the same data stacks to the same heights @@ -59,10 +62,11 @@ #' #' The `byord` argument is a helpful companion to stacked area plots, since it #' enables on-the-fly adjustment of the stacking order. For example, -#' three positional keywords---`"start"`, `"end"`, and `"total"`---rank the -#' stacked `by` groups according to their `y` values at the designated position -#' along the `x` axis. Following convention, the ranking runs in descending -#' order, so that the biggest group is drawn on the bottom layer. However, size +#' the size keywords---`"desc"`, `"start"`, and `"end"`---rank the +#' stacked `by` groups according to their `y` values, either summed across the +#' `x` axis or taken at one end of it. Following convention, the ranking runs in +#' descending order, so that the biggest group is drawn on the bottom layer +#' (use `"asc"` for the reverse). However, size #' is not the only route to a stable baseline. Because each band is #' drawn on top of the ones below it, they all inherit whatever movement the #' bottom layer has. A large but volatile group can therefore be a worse choice @@ -146,7 +150,7 @@ #' # `"minvar"` instead puts the *least variable* group on the baseline. Every #' # band inherits the movement of the ones below it, so a steady bottom layer #' # keeps the whole chart legible. Here that picks group B, which the default -#' # level order leaves in the middle and `"end"`/`"total"` push to the top. +#' # level order leaves in the middle and `"end"`/`"desc"` push to the top. #' #' tinyplot( #' val ~ year | grp, data = dat, diff --git a/R/type_ridge.R b/R/type_ridge.R index 915d12ae..ef33b613 100644 --- a/R/type_ridge.R +++ b/R/type_ridge.R @@ -27,9 +27,32 @@ #' at the specified `probs`. The quantiles are computed based on the density #' (rather than the raw original variable). Only one of `breaks` or #' `probs` must be specified. -#' @param ylevels a character or numeric vector specifying in which order -#' the levels of the y-variable should be plotted. The special keyword -#' `"asis"` takes the categories in the order that they appear in the data. +#' @param ylevels,yord arguments controlling the order of the `y` variable, and +#' hence of the y-axis. Supply one or the other; if both arguments are +#' provided, `ylevels` takes precedence and `yord` is silently ignored. +#' +#' - `ylevels` specifies the levels _literally_, either a character vector of +#' level names in the desired order (e.g., `c("C", "B", "A")`), or a numeric +#' vector of the corresponding level indexes (e.g. `3:1`). +#' +#' - `yord` instead accepts a keyword or custom function, which then _derives_ +#' the order from the data. Options are: +#' +#' - `"desc"` and `"asc"` rank the ridges by their mean `x` value, largest +#' or smallest first. (Long forms like `"descending"` and `"increasing"` are also accepted.) (Ridge plots have no +#' separate response, so the ranking runs on the continuous `x` variable.) +#' - `"minvar"` ranks them by the spread of each distribution, narrowest +#' first. +#' - `"asis"` or `"rev"` permute the existing levels without consulting the +#' data at all. The former takes the categories in the order that they +#' appear in the data, while the latter reverses the current level order. +#' - a custom function that determines both the ranking statistic and its +#' direction. The statistic is always sorted ascending, so +#' `function(y) -median(y)` ranks by median, largest first. +#' +#' Note that a numeric `y` is coerced to a factor before the ridges are +#' drawn, so it is reordered like any other categorical variable. +#' Each argument defaults to `NULL`, i.e. keep the existing factor levels. #' @inheritParams stats::density #' @param bw the smoothing \code{\link[stats:bw.nrd]{bandwidth}} to be used, #' see \code{\link[stats]{density}} for details and options. @@ -215,6 +238,7 @@ type_ridge = function( breaks = NULL, probs = NULL, ylevels = NULL, + yord = NULL, bw = "nrd0", joint.bw = c("mean", "full", "none"), adjust = 1, @@ -245,6 +269,7 @@ type_ridge = function( breaks = breaks, probs = probs, ylevels = ylevels, + yord = yord, raster = raster, col = col, alpha = alpha, @@ -265,14 +290,14 @@ data_ridge = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, gradient = FALSE, breaks = NULL, probs = NULL, - ylevels = NULL, + ylevels = NULL, yord = NULL, raster = FALSE, col = NULL, alpha = NULL, singletons = "warn" ) { fun = function(settings, ...) { - env2env(settings, environment(), c("datapoints", "yaxt", "xaxt", "null_by")) + env2env(settings, environment(), c("datapoints", "yaxt", "xaxt", "null_by", "yaxl")) # `col` may arrive either via the top-level `tinyplot(..., col =)` call # (stored in settings) or via the `type_ridge(col =)` constructor arg. The @@ -303,6 +328,17 @@ data_ridge = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, datapoints$y = sanitize_xlevels(datapoints$y, ylevels, arg = "ylevels") if (y_by) datapoints$by = datapoints$y } + ## `yord` ranks the ridges on the *continuous* variable, which for this + ## type is `x` -- there is no separate response to rank on. So "asc"/"desc" + ## order by mean x, "minvar" by the spread of each distribution. + if (!is.null(yord) && is.null(ylevels)) { + datapoints$y = sanitize_ord( + datapoints$y, datapoints$x, NULL, + yord, arg = "yord", keywords = ord_keywords_distribution, + stat = "mean" + ) + if (y_by) datapoints$by = datapoints$y + } ## datapoints = split(datapoints, list(datapoints$y, datapoints$by, datapoints$facet)) @@ -460,6 +496,10 @@ data_ridge = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, probs = probs, manbreaks = manbreaks, yaxt = yaxt_orig, + ## This type draws its own y-axis category labels (see `draws_own_axes`), + ## so it never reaches the standard path where `yaxl` is applied. Carry it + ## through for the tinyAxis() calls in draw_ridge() to use as a labeller. + yaxl = yaxl, raster = raster, ridge_theme = ridge_theme, x_by = x_by, @@ -580,6 +620,7 @@ draw_ridge = function() { if (ridge_theme) { if (keep_axis(2)) { tinyAxis(x = d$y, side = 2, at = val, labels = lab, type = type_info[["yaxt"]], + labeller = type_info[["yaxl"]], padj = 0, mgp = c(3, 1, 0) - c(0.5, 0.5 + 0.3, 0), tcl = 0) @@ -587,7 +628,8 @@ draw_ridge = function() { if (identical(.tpar[["tinytheme"]], "ridge2") && keep_axis(1)) axis(1, labels = FALSE) } else { if (keep_axis(2)) { - tinyAxis(x = d$y, side = 2, at = val, labels = lab, type = type_info[["yaxt"]]) + tinyAxis(x = d$y, side = 2, at = val, labels = lab, type = type_info[["yaxt"]], + labeller = type_info[["yaxl"]]) } } } diff --git a/R/type_spineplot.R b/R/type_spineplot.R index 2d5500f8..6056bf25 100644 --- a/R/type_spineplot.R +++ b/R/type_spineplot.R @@ -4,11 +4,34 @@ #' are modified versions of histograms or mosaic plots, and particularly #' useful for visualizing factor variables. Note that [`tinyplot`] defaults #' to `type_spineplot()` if `y` is a factor variable. -#' @param xlevels,ylevels a character or numeric vector specifying the ordering of the -#' levels of the `x` and `y` variables (if character) or the corresponding indexes -#' (if numeric) for the plot. The special keyword `"asis"` takes the -#' categories in the order that they appear in the data. Note that these -#' arguments only affect categorical (i.e., factor or character) variables. +#' @param xlevels,xord arguments controlling the order of the `x` variable, and +#' hence of the x-axis. Supply one or the other; if both arguments are +#' provided, `xlevels` takes precedence and `xord` is silently ignored. +#' +#' - `xlevels` specifies the levels _literally_, either a character vector of +#' level names in the desired order (e.g., `c("C", "B", "A")`), or a numeric +#' vector of the corresponding level indexes (e.g. `3:1`). +#' +#' - `xord` instead accepts a keyword or custom function, which then _derives_ +#' the order from the data. Options are: +#' +#' - `"desc"` and `"asc"` rank the categories by (weighted) frequency, +#' i.e. most or least common first. (Long forms like `"descending"` and `"increasing"` are also accepted.) +#' - `"asis"` or `"rev"` permute the existing levels without consulting the +#' data at all. The former takes the categories in the order that they +#' appear in the data, while the latter reverses the current level order. +#' - a custom function that determines both the ranking statistic and its +#' direction. The statistic is always sorted ascending, so +#' `function(y) -median(y)` ranks by median, largest first. +#' +#' Note that `x` is only reordered when it is categorical (i.e., factor or +#' character). Both arguments are thus ignored for spinograms, which have a +#' (binned) numeric `x` axis. Each argument defaults to `NULL`, i.e. keep the +#' existing factor levels. +#' @param ylevels,yord as for `xlevels` / `xord` above, but for the `y` +#' variable. Note that `y` is always coerced to a factor for spineplots and +#' spinograms, so these arguments are always binding if provided. Be aware +#' that a numeric `y` gives one level per distinct value. #' @inheritParams graphics::spineplot #' @param lighten logical. For grouped spineplots where the `y` variable is #' itself the grouping variable (i.e. `y == by`), should the fills use a @@ -20,6 +43,10 @@ #' the lighter tint. Note that `lighten` has no effect on other spineplot #' displays (single-group or `x == by`), which always use a sequential shading #' ramp of the base colour. +#' @param xaxlabels,yaxlabels \[Deprecated\] character vectors for annotation of +#' the x and y axis. Use the top-level `xaxl` / `yaxl` arguments instead, +#' which apply consistently across [`tinyplot`] types. These two type-specific +#' arguments will be removed in a future release. #' @examples #' # "spineplot" type convenience string #' tinyplot(Species ~ Sepal.Width, data = iris, type = "spineplot") @@ -94,10 +121,24 @@ #' ) #' #' @export -type_spineplot = function(breaks = NULL, tol.ylab = 0.05, off = NULL, xlevels = NULL, ylevels = NULL, col = NULL, xaxlabels = NULL, yaxlabels = NULL, weights = NULL, lighten = FALSE) { +type_spineplot = function(breaks = NULL, tol.ylab = 0.05, off = NULL, xlevels = NULL, xord = NULL, ylevels = NULL, yord = NULL, col = NULL, weights = NULL, lighten = FALSE, xaxlabels = NULL, yaxlabels = NULL) { col = col + dep = c(if (!is.null(xaxlabels)) "xaxlabels", if (!is.null(yaxlabels)) "yaxlabels") + if (length(dep)) { + warning( + sprintf( + "'%s' %s deprecated; ", + paste(dep, collapse = "' and '"), + if (length(dep) > 1L) "are" else "is" + ), + "use the top-level 'xaxl'/'yaxl' arguments instead, e.g. ", + "tinyplot(..., yaxl = c(old = \"new\")) to rename particular categories, ", + "or yaxl = function(x) ... to compute the labels.", + call. = FALSE + ) + } out = list( - data = data_spineplot(off = off, breaks = breaks, xlevels = xlevels, ylevels = ylevels, xaxlabels = xaxlabels, yaxlabels = yaxlabels, weights = weights, lighten = lighten), + data = data_spineplot(off = off, breaks = breaks, xlevels = xlevels, xord = xord, ylevels = ylevels, yord = yord, xaxlabels = xaxlabels, yaxlabels = yaxlabels, weights = weights, lighten = lighten), draw = draw_spineplot(tol.ylab = tol.ylab, off = off, col = col, xaxlabels = xaxlabels, yaxlabels = yaxlabels, lighten = lighten), name = "spineplot" ) @@ -106,9 +147,9 @@ type_spineplot = function(breaks = NULL, tol.ylab = 0.05, off = NULL, xlevels = } #' @importFrom grDevices nclass.Sturges -data_spineplot = function(off = NULL, breaks = NULL, xlevels = xlevels, ylevels = ylevels, xaxlabels = NULL, yaxlabels = NULL, weights = NULL, lighten = FALSE) { +data_spineplot = function(off = NULL, breaks = NULL, xlevels = xlevels, xord = NULL, ylevels = ylevels, yord = NULL, xaxlabels = NULL, yaxlabels = NULL, weights = NULL, lighten = FALSE) { fun = function(settings, ...) { - env2env(settings, environment(), c("datapoints", "xlim", "ylim", "facet", "facet.args", "by", "xaxb", "yaxb", "null_by", "null_facet", "col", "bg", "axes", "frame.plot", "xaxt", "yaxt", "lwd", "lty")) + env2env(settings, environment(), c("datapoints", "xlim", "ylim", "facet", "facet.args", "by", "xaxb", "yaxb", "xaxl", "yaxl", "null_by", "null_facet", "col", "bg", "axes", "frame.plot", "xaxt", "yaxt", "lwd", "lty")) settings[["lighten"]] = lighten ## process weights: a top-level `weights` column (carried on datapoints @@ -166,6 +207,24 @@ data_spineplot = function(off = NULL, breaks = NULL, xlevels = xlevels, ylevels datapoints$y = sanitize_xlevels(datapoints$y, ylevels, arg = "ylevels") if (y_by) datapoints$by = datapoints$y } + ## Both axes here are categorical, so there is no response to rank on: + ## the size keywords count observations instead (weighted, if given), + ## i.e. "asc"/"desc" order the categories by frequency. + spine_w = if (!is.null(weights)) weights else rep.int(1, nrow(datapoints)) + if (!is.null(xord) && is.null(xlevels) && x.categorical) { + datapoints$x = sanitize_ord( + datapoints$x, spine_w, NULL, + xord, arg = "xord", keywords = ord_keywords_scalar + ) + if (x_by) datapoints$by = datapoints$x + } + if (!is.null(yord) && is.null(ylevels)) { + datapoints$y = sanitize_ord( + datapoints$y, spine_w, NULL, + yord, arg = "yord", keywords = ord_keywords_scalar + ) + if (y_by) datapoints$by = datapoints$y + } x = datapoints$x y = datapoints$y @@ -265,6 +324,15 @@ data_spineplot = function(off = NULL, breaks = NULL, xlevels = xlevels, ylevels if (isTRUE(x_by)) datapoints$by = factor(rep(xaxlabels, each = ny)) # each x label extends over ny rows if (isTRUE(y_by)) datapoints$by = factor(rep_len(yaxlabels, nrow(datapoints))) + ## This type draws its own axes (see the `draws_own_axes` hint below), + ## so it never reaches the standard path where the top-level `xaxl` / + ## `yaxl` are applied. Apply them here instead, to the labels that + ## spine_axis() will actually draw. Deliberately after the `by` catch + ## above: `x/yaxl` are documented as affecting the tick labels only, so + ## a legend built from the same categories should be left alone. + if (!is.null(xaxl)) xaxlabels = tinylabel(xaxlabels, xaxl) + if (!is.null(yaxl)) yaxlabels = tinylabel(yaxlabels, yaxl) + x = c(datapoints$xmin, datapoints$xmax) y = c(datapoints$ymin, datapoints$ymax) ymin = datapoints$ymin diff --git a/R/type_text.R b/R/type_text.R index 803bec38..96bc88bc 100644 --- a/R/type_text.R +++ b/R/type_text.R @@ -9,11 +9,11 @@ #' a top-level [`tinyplot`] argument, which additionally supports non-standard #' evaluation against `data` and takes precedence if both are given. See #' Examples. -#' @param labeller A formatting function (or convenience string) passed to -#' [`tinylabel`] for formatting the `labels`. Useful for ensuring that the -#' text labels match the formatting of an axis, e.g. `labeller = "%"` to -#' display the labels as percentages. Default is `NULL`, i.e. no formatting. -#' See Examples. +#' @param labeller A formatting function, convenience string, or dictionary +#' (named vector or list) passed to [`tinylabel`] for formatting or +#' relabelling the `labels`. Useful for ensuring that the text labels match +#' the formatting of an axis, e.g. `labeller = "%"` to display the labels as +#' percentages. Default is `NULL`, i.e. no formatting. See Examples. #' @param family The name of a font family. Default of `NULL` means that the #' family will be the same as the main plot text, following #' \code{\link[graphics]{par}}. Note that if a `family` argument is provided, diff --git a/altdoc/pkgdown.yml b/altdoc/pkgdown.yml index dc63ad1f..c82f70d4 100644 --- a/altdoc/pkgdown.yml +++ b/altdoc/pkgdown.yml @@ -1,8 +1,8 @@ altdoc: 0.7.3 -pandoc: 3.8.3 +pandoc: 3.10.2 pkgdown: 2.1.3 pkgdown_sha: ~ -last_built: 2026-08-18T05:02:34+0000 +last_built: 2026-08-29T04:35:36+0000 urls: reference: https://grantmcdermott.com/tinyplot/man article: https://grantmcdermott.com/tinyplot/vignettes diff --git a/inst/tinytest/_tinysnapshot/area_stack_byord_asc.svg b/inst/tinytest/_tinysnapshot/area_stack_byord_asc.svg new file mode 100644 index 00000000..d6a029b2 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/area_stack_byord_asc.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + +grp +a +b + + + + + + + +x +y + + + + + + + + + + +1.0 +1.5 +2.0 +2.5 +3.0 +3.5 +4.0 + + + + + + + +0 +20 +40 +60 +80 +100 + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/area_stack_byord_desc.svg b/inst/tinytest/_tinysnapshot/area_stack_byord_desc.svg new file mode 100644 index 00000000..e6d08924 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/area_stack_byord_desc.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + +grp +b +a + + + + + + + +x +y + + + + + + + + + + +1.0 +1.5 +2.0 +2.5 +3.0 +3.5 +4.0 + + + + + + + +0 +20 +40 +60 +80 +100 + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/area_stack_byord_start.svg b/inst/tinytest/_tinysnapshot/area_stack_byord_start.svg new file mode 100644 index 00000000..ec6009f1 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/area_stack_byord_start.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + +grp +A +B +C + + + + + + + +year +val + + + + + + + + +2000 +2005 +2010 +2015 +2020 + + + + + + + + +0 +2 +4 +6 +8 +10 +12 + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/area_stack_flip.svg b/inst/tinytest/_tinysnapshot/area_stack_flip.svg index 6836bee9..0a2f8e5c 100644 --- a/inst/tinytest/_tinysnapshot/area_stack_flip.svg +++ b/inst/tinytest/_tinysnapshot/area_stack_flip.svg @@ -26,13 +26,13 @@ - + - + grp -C +A B -A +C diff --git a/inst/tinytest/_tinysnapshot/barplot_xaxl_dict.svg b/inst/tinytest/_tinysnapshot/barplot_xaxl_dict.svg new file mode 100644 index 00000000..b43df53a --- /dev/null +++ b/inst/tinytest/_tinysnapshot/barplot_xaxl_dict.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + +cyl +Count +four +six +eight + + + + + + + + + +0 +2 +4 +6 +8 +10 +12 +14 + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/barplot_xord_aggregated_mean.svg b/inst/tinytest/_tinysnapshot/barplot_xord_aggregated_mean.svg new file mode 100644 index 00000000..20baca9a --- /dev/null +++ b/inst/tinytest/_tinysnapshot/barplot_xord_aggregated_mean.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + +g +v +few +mid +many + + + + + + + +0 +2 +4 +6 +8 +10 + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/barplot_xord_aggregated_sum.svg b/inst/tinytest/_tinysnapshot/barplot_xord_aggregated_sum.svg new file mode 100644 index 00000000..9e13c9a0 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/barplot_xord_aggregated_sum.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + +g +v +many +mid +few + + + + + +0 +5 +10 +15 + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/barplot_xord_ascending.svg b/inst/tinytest/_tinysnapshot/barplot_xord_ascending.svg new file mode 100644 index 00000000..ace55a4c --- /dev/null +++ b/inst/tinytest/_tinysnapshot/barplot_xord_ascending.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + +cyl +Count +6 +4 +8 + + + + + + + + + +0 +2 +4 +6 +8 +10 +12 +14 + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/barplot_xord_asis.svg b/inst/tinytest/_tinysnapshot/barplot_xord_asis.svg new file mode 100644 index 00000000..72887bb7 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/barplot_xord_asis.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + +g +v +z +a +m + + + + + + + +0 +1 +2 +3 +4 +5 + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/barplot_xord_desc.svg b/inst/tinytest/_tinysnapshot/barplot_xord_desc.svg new file mode 100644 index 00000000..d6550adf --- /dev/null +++ b/inst/tinytest/_tinysnapshot/barplot_xord_desc.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + +cyl +Count +8 +4 +6 + + + + + + + + + +0 +2 +4 +6 +8 +10 +12 +14 + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/barplot_xord_rev.svg b/inst/tinytest/_tinysnapshot/barplot_xord_rev.svg new file mode 100644 index 00000000..bd58dafc --- /dev/null +++ b/inst/tinytest/_tinysnapshot/barplot_xord_rev.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + +cyl +Count +8 +6 +4 + + + + + + + + + +0 +2 +4 +6 +8 +10 +12 +14 + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/facet_labeller_dict.svg b/inst/tinytest/_tinysnapshot/facet_labeller_dict.svg new file mode 100644 index 00000000..2100a5f3 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/facet_labeller_dict.svg @@ -0,0 +1,328 @@ + + + + + + + + + + + + + +Petal.Length +Sepal.Length + + + + + + + + + + + + + + + + + +1 +2 +3 +4 +5 +6 +7 + + + + + + + + + +4.5 +5.0 +5.5 +6.0 +6.5 +7.0 +7.5 +8.0 + +SET + + + + + + + + + + + + + + + + + + +1 +2 +3 +4 +5 +6 +7 + + + + + + + + + +4.5 +5.0 +5.5 +6.0 +6.5 +7.0 +7.5 +8.0 + +versicolor + + + + + + + + + + + + + + + + + + +1 +2 +3 +4 +5 +6 +7 + + + + + + + + + +4.5 +5.0 +5.5 +6.0 +6.5 +7.0 +7.5 +8.0 + +VIR + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/lines_xord_rev.svg b/inst/tinytest/_tinysnapshot/lines_xord_rev.svg new file mode 100644 index 00000000..abee4dc7 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/lines_xord_rev.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + +name +runtime + + + + +Two Towers +Return +Fellowship + + + + + + +180 +185 +190 +195 +200 + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/points_xord_desc.svg b/inst/tinytest/_tinysnapshot/points_xord_desc.svg new file mode 100644 index 00000000..0f16a85b --- /dev/null +++ b/inst/tinytest/_tinysnapshot/points_xord_desc.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + +factor(cyl) +mpg + + + + +4 +6 +8 + + + + + + +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/points_xord_rev.svg b/inst/tinytest/_tinysnapshot/points_xord_rev.svg new file mode 100644 index 00000000..ae47da1e --- /dev/null +++ b/inst/tinytest/_tinysnapshot/points_xord_rev.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + +factor(cyl) +mpg + + + + +8 +6 +4 + + + + + + +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/ridge_yaxl_dict.svg b/inst/tinytest/_tinysnapshot/ridge_yaxl_dict.svg new file mode 100644 index 00000000..23993016 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/ridge_yaxl_dict.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + +Sepal.Width +Species + + + + + + + + +1.5 +2.0 +2.5 +3.0 +3.5 +4.0 +4.5 + + + + + + + + + + + + + + + + + + + + +SET +versicolor +VIR + + + diff --git a/inst/tinytest/_tinysnapshot/ridge_yaxl_toupper.svg b/inst/tinytest/_tinysnapshot/ridge_yaxl_toupper.svg new file mode 100644 index 00000000..ad718faf --- /dev/null +++ b/inst/tinytest/_tinysnapshot/ridge_yaxl_toupper.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + +Sepal.Width +Species + + + + + + + + +1.5 +2.0 +2.5 +3.0 +3.5 +4.0 +4.5 + + + + + + + + + + + + + + + + + + + + +SETOSA +VERSICOLOR +VIRGINICA + + + diff --git a/inst/tinytest/_tinysnapshot/ridge_yord_minvar.svg b/inst/tinytest/_tinysnapshot/ridge_yord_minvar.svg new file mode 100644 index 00000000..f0cab425 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/ridge_yord_minvar.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + +Sepal.Length +Species + + + + + + +4 +5 +6 +7 +8 + + + + + + + + + + + + + + + + + + + + +setosa +versicolor +virginica + + + diff --git a/inst/tinytest/_tinysnapshot/ridge_yord_rev.svg b/inst/tinytest/_tinysnapshot/ridge_yord_rev.svg new file mode 100644 index 00000000..91929b3d --- /dev/null +++ b/inst/tinytest/_tinysnapshot/ridge_yord_rev.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + +Sepal.Length +Species + + + + + + +4 +5 +6 +7 +8 + + + + + + + + + + + + + + + + + + + + +virginica +versicolor +setosa + + + diff --git a/inst/tinytest/_tinysnapshot/spineplot_xaxl_toupper.svg b/inst/tinytest/_tinysnapshot/spineplot_xaxl_toupper.svg new file mode 100644 index 00000000..fb35196e --- /dev/null +++ b/inst/tinytest/_tinysnapshot/spineplot_xaxl_toupper.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + +grp +resp + + + + + + + + + + + + + +ALPHA +BETA +hi +lo + + + + + + + +0.0 +0.2 +0.4 +0.6 +0.8 +1.0 + + + diff --git a/inst/tinytest/_tinysnapshot/spineplot_xord_desc.svg b/inst/tinytest/_tinysnapshot/spineplot_xord_desc.svg new file mode 100644 index 00000000..2283b780 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/spineplot_xord_desc.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + +cut(Sepal.Length, 3) +Species + + + + + + + + + + + + + + + + + + +(5.5,6.7] +(4.3,5.5] +(6.7,7.9] +virginica +versicolor +setosa + + + + + + + +0.0 +0.2 +0.4 +0.6 +0.8 +1.0 + + + diff --git a/inst/tinytest/_tinysnapshot/spineplot_yaxl_dict.svg b/inst/tinytest/_tinysnapshot/spineplot_yaxl_dict.svg new file mode 100644 index 00000000..85c38f30 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/spineplot_yaxl_dict.svg @@ -0,0 +1,118 @@ + + + + + + + + + + + + + +Sepal.Width +Species + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +2 +2.4 +2.6 +2.8 +3 +3.2 +3.4 +3.6 +4 +VIR +versicolor +SET + + + + + + + +0.0 +0.2 +0.4 +0.6 +0.8 +1.0 + + + + diff --git a/inst/tinytest/_tinysnapshot/spineplot_yaxl_toupper.svg b/inst/tinytest/_tinysnapshot/spineplot_yaxl_toupper.svg new file mode 100644 index 00000000..a4208d52 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/spineplot_yaxl_toupper.svg @@ -0,0 +1,118 @@ + + + + + + + + + + + + + +Sepal.Width +Species + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +2 +2.4 +2.6 +2.8 +3 +3.2 +3.4 +3.6 +4 +VIRGINICA +VERSICOLOR +SETOSA + + + + + + + +0.0 +0.2 +0.4 +0.6 +0.8 +1.0 + + + + diff --git a/inst/tinytest/_tinysnapshot/spineplot_yord_rev.svg b/inst/tinytest/_tinysnapshot/spineplot_yord_rev.svg new file mode 100644 index 00000000..9cd28fc5 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/spineplot_yord_rev.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + +cut(Sepal.Length, 3) +Species + + + + + + + + + + + + + + + + + + +(4.3,5.5] +(5.5,6.7] +(6.7,7.9] +setosa +versicolor +virginica + + + + + + + +0.0 +0.2 +0.4 +0.6 +0.8 +1.0 + + + diff --git a/inst/tinytest/_tinysnapshot/tinylabel_center_percent.svg b/inst/tinytest/_tinysnapshot/tinylabel_center_percent.svg new file mode 100644 index 00000000..c6412636 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinylabel_center_percent.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + +grp +down +up + + + + + + + +v +g + + + + + + + + + + +60% +40% +20% +0% +20% +40% +60% +a +b + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinyplot_add_numeric_x_barplot.svg b/inst/tinytest/_tinysnapshot/tinyplot_add_numeric_x_barplot.svg new file mode 100644 index 00000000..f3d930ea --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinyplot_add_numeric_x_barplot.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + +Time +demand +1 +2 +3 +4 +5 +7 + + + + + + +0 +5 +10 +15 +20 + + + + + + + + + + + + + + + +8.3 +10.3 +19 +16 +15.6 +19.8 + + + diff --git a/inst/tinytest/_tinysnapshot/tinyplot_add_numeric_x_violin.svg b/inst/tinytest/_tinysnapshot/tinyplot_add_numeric_x_violin.svg new file mode 100644 index 00000000..6fda13b3 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinyplot_add_numeric_x_violin.svg @@ -0,0 +1,122 @@ + + + + + + + + + + + + + +dose +len + + + + +0.5 +1 +2 + + + + + + +0 +10 +20 +30 +40 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/test-facet.R b/inst/tinytest/test-facet.R index ce6ef115..4f0f8462 100644 --- a/inst/tinytest/test-facet.R +++ b/inst/tinytest/test-facet.R @@ -749,6 +749,18 @@ f = function() { } expect_snapshot_plot(f, label = "facet_labeller_list") +# A dictionary can be nested inside that per-variable list, which is the only +# way to reach one here: a bare named vector claims the same slot and is read +# as a per-variable mapping instead. Partial mapping is fine -- "versicolor" +# is not named, so it comes through untouched. +f = function() { + tinyplot( + Sepal.Length ~ Petal.Length, data = iris, facet = ~Species, + facet.args = list(labeller = list(Species = c(setosa = "SET", virginica = "VIR"))) + ) +} +expect_snapshot_plot(f, label = "facet_labeller_dict") + # All of the facet title arguments at once: a named `prefix` (so the order it # is written in doesn't matter), a `labeller`, and a `sep` to stack the two # variables. Note that the labeller sees each variable's own values rather than diff --git a/inst/tinytest/test-tinylabel.R b/inst/tinytest/test-tinylabel.R index a20bd079..55aa101b 100644 --- a/inst/tinytest/test-tinylabel.R +++ b/inst/tinytest/test-tinylabel.R @@ -53,3 +53,72 @@ expect_equal( # coercion" warnings (#622). spp = c("Adelie", "Gentoo", "Chinstrap") expect_equal(tinylabel(spp, ","), spp) + +# `xaxl`/`yaxl` accept a dictionary mapping old labels to new ones. Unmatched +# labels are left alone, the mapping is by value rather than position (so it +# survives reordering), and a named list works as well as a named vector. +expect_equal(tinylabel(c("a", "b", "c"), c(b = "Bee")), c("a", "Bee", "c")) +expect_equal(tinylabel(c("c", "a", "b"), c(a = "A", b = "B", c = "C")), c("C", "A", "B")) +expect_equal(tinylabel(c("a", "b"), list(a = "Alpha", b = "Beta")), c("Alpha", "Beta")) + +# names remove the keyword collision that a bare vector could never resolve +expect_equal(tinylabel(c("log", "x"), c(log = "Log scale")), c("Log scale", "x")) + +# an unnamed multi-element vector is not positional replacement +expect_error(tinylabel(c("a", "b"), c("X", "Y")), pattern = "single formatting keyword") + +# "abs_" must be stripped before a symbol keyword is resolved; a centered +# barplot prepends it itself, which previously made `yaxl = ","` unusable there +expect_equal(tinylabel(c(-1000, 2000), "abs_,"), c("1,000", "2,000")) +expect_equal(tinylabel(c(-1000, 2000), "abs_comma"), c("1,000", "2,000")) + + +# +## symmetric breaks must not blow up the precision ----- + +# A centered barplot's breaks are symmetric, so the "abs_" wrapper hands the +# formatter genuine duplicates -- but abs(-0.4) * 100 and 0.4 * 100 differ in +# their last bits, so unique() keeps both. consistent_decimals() was then asked +# for a precision that prints two identical numbers distinctly, failed at every +# candidate, and fell back to its 5-decimal maximum ("80.00000%" not "80%"). +# seq() rather than typed-out constants: literals are exact, so abs() maps them +# onto bit-equal values that unique() collapses cleanly and the bug never fires. +# Real axis breaks are computed, and carry the noise that defeats unique(). The +# guard below asserts that condition holds, so this cannot quietly go vacuous. +brks = seq(-0.8, 0.8, by = 0.2) +expect_true( + length(unique(abs(brks) * 100)) > length(unique(round(abs(brks) * 100, 5))) +) +expect_equal( + tinylabel(brks, "abs_percent"), + c("80%", "60%", "40%", "20%", "0%", "20%", "40%", "60%", "80%") +) + +# ...while precision still adapts where the values genuinely need it +expect_equal(tinylabel(c(0.00011, 0.00012), "percent"), c("0.011%", "0.012%")) + +# The deduplication runs at max_decimals rather than at a fixed number of +# significant digits: significance is relative while max_decimals is absolute, +# so any signif() threshold merges values still distinguishable at the 5th +# decimal once they grow large enough. This case fails under signif(., 6), +# (., 8) and (., 12) alike. +expect_equal( + tinylabel(c(10000000.00001, 10000000.00002), "comma"), + c("10,000,000.00001", "10,000,000.00002") +) + +# ...and end to end, since the axis is where it surfaced: a centered stacked +# barplot is the simplest thing that produces breaks symmetric about zero +props = data.frame( + g = factor(rep(c("a", "b"), each = 2)), + grp = factor(rep(c("up", "down"), 2)), + v = c(0.6, 0.4, 0.3, 0.7) +) +f = function() { + tinyplot( + v ~ g | grp, data = props, + type = type_barplot(center = TRUE), yaxl = "percent", + flip = TRUE + ) +} +expect_snapshot_plot(f, label = "tinylabel_center_percent") diff --git a/inst/tinytest/test-tinyplot_add.R b/inst/tinytest/test-tinyplot_add.R index e1e67e11..dd9eae48 100644 --- a/inst/tinytest/test-tinyplot_add.R +++ b/inst/tinytest/test-tinyplot_add.R @@ -142,3 +142,19 @@ f = function() { tinyplot_add(y ~ g, data = d2, type = "p", col = "red", pch = 16) } expect_snapshot_plot(f, label = "tinyplot_add_layer_category_alignment") + +# A base layer that coerces a numeric x to a factor itself (bars, violins, ...) +# leaves its categories named by the *labels*, while the added layer still +# carries the raw values. Those used to be plotted at their own coordinates, +# landing far outside a panel that spans the factor positions. (#691) +f = function() { + tinyplot(demand ~ Time, data = BOD, type = "barplot") + tinyplot_add(type = "text", pos = 3, xpd = NA) +} +expect_snapshot_plot(f, label = "tinyplot_add_numeric_x_barplot") + +f = function() { + tinyplot(len ~ dose, data = ToothGrowth, fill = 0.2, type = "violin") + tinyplot_add(type = type_summary(median, type = "p")) +} +expect_snapshot_plot(f, label = "tinyplot_add_numeric_x_violin") diff --git a/inst/tinytest/test-type_area.R b/inst/tinytest/test-type_area.R index 37eba7b0..6ae39d6b 100644 --- a/inst/tinytest/test-type_area.R +++ b/inst/tinytest/test-type_area.R @@ -40,6 +40,9 @@ f = function() { } expect_snapshot_plot(f, label = "area_stack_alpha") +# flipping lays the bands out left-to-right, so the bottom-up reading the +# reversed legend key exists to match no longer applies: the key runs in +# level order here, not reversed f = function() { tinyplot(val ~ year | grp, data = dat, type = type_area(stack = TRUE), flip = TRUE) } @@ -139,14 +142,31 @@ expect_error( pattern = "must be NULL" ) -# a one-argument function keeps working unchanged, and a second argument that -# is *not* named `x` (e.g. a tuning parameter with a default) must not be fed -# the x values by mistake -dp = function(byord) { - d = data.frame(x = rep(1:4, 2), y = c(1, 2, 3, 100, 4, 4, 4, 4), - by = factor(rep(c("a", "b"), each = 4)), facet = "f") - levels(tinyplot:::sanitize_ord(d$by, d$y, d$x, byord)) +# `a` carries the larger values, so "desc" stacks it first and "asc" reverses +# that. Every case below is asserted against one of these two references. +ord = data.frame(x = rep(1:4, 2), y = c(1, 2, 3, 100, 4, 4, 4, 4), + grp = factor(rep(c("a", "b"), each = 4))) +byo = function(byord) { + function() { + tinyplot(y ~ x | grp, data = ord, type = type_area(stack = TRUE, byord = byord)) + } } -expect_equal(dp(function(y) -median(y)), c("b", "a")) -expect_equal(dp(function(y, p = 0.9) -as.numeric(quantile(y, p))), c("a", "b")) -expect_equal(dp(function(y, x) coef(lm(y ~ x))[2]), c("b", "a")) +expect_snapshot_plot(byo("desc"), label = "area_stack_byord_desc") +expect_snapshot_plot(byo("asc"), label = "area_stack_byord_asc") + +# a second argument that is *not* named `x` is a tuning parameter, and must not +# be fed the x values by mistake. Dispatching on the count of formals rather +# than their names handed `p` the vector c(1, 2, 3, 4), and quantile() then +# errored on probabilities outside [0, 1] -- so this failing at all is the +# regression, whatever the resulting order. +expect_snapshot_plot( + byo(function(y, p = 0.9) -as.numeric(quantile(y, p))), + label = "area_stack_byord_desc" +) + +# ...but "start"/"end" remain available for `byord`, where the groups really do +# span a secondary axis +f = function() { + tinyplot(val ~ year | grp, data = dat, type = type_area(stack = TRUE, byord = "start")) +} +expect_snapshot_plot(f, label = "area_stack_byord_start") diff --git a/inst/tinytest/test-type_barplot.R b/inst/tinytest/test-type_barplot.R index a994a37a..08230193 100644 --- a/inst/tinytest/test-type_barplot.R +++ b/inst/tinytest/test-type_barplot.R @@ -192,3 +192,112 @@ f = function() { type = type_barplot(lighten = FALSE), theme = "clean2") } expect_snapshot_plot(f, label = "barplot_group_lighten_false") + + +# +## xord ----- + +# sort bars by height -- not previously possible without relevelling by hand +f = function() tinyplot(~ cyl, data = mtcars, type = type_barplot(xord = "desc")) +expect_snapshot_plot(f, label = "barplot_xord_desc") + +f = function() tinyplot(~ cyl, data = mtcars, type = type_barplot(xord = "rev")) +expect_snapshot_plot(f, label = "barplot_xord_rev") + +# `xord` must rank the *aggregated* bars, not the raw cells. With unequal cell +# counts these two order the bars oppositely (means: few>mid>many; sums: +# many>mid>few), so the pair pins the ranking to whatever FUN actually drew. +bars = data.frame( + g = factor(rep(c("few", "many", "mid"), times = c(1, 6, 3))), + v = c(10, rep(3, 6), rep(5, 3)) +) + +f = function() tinyplot(v ~ g, data = bars, type = type_barplot(xord = "desc")) +expect_snapshot_plot(f, label = "barplot_xord_aggregated_mean") + +f = function() tinyplot(v ~ g, data = bars, type = type_barplot(xord = "desc", FUN = sum)) +expect_snapshot_plot(f, label = "barplot_xord_aggregated_sum") + + +# `xord` no longer accepts explicit levels; that is what `xlevels` is for +expect_error( + tinyplot(~ cyl, data = mtcars, type = type_barplot(xord = c("8", "6", "4"))), + pattern = "must be NULL" +) +# and `xlevels` no longer accepts the ord keywords: a keyword matches none of +# the levels, which is fatal rather than a silent all-NA factor +expect_error( + tinyplot(~ cyl, data = mtcars, type = type_barplot(xlevels = "asis")), + pattern = "matches none of the levels" +) + +# `xord` is validated even where the ordering itself is a no-op, so that a typo +# does not lie dormant until the data gains a second category +one_level = data.frame(g = factor("only"), v = 1) +expect_error( + tinyplot(v ~ g, data = one_level, type = type_barplot(xord = "des")), + pattern = "must be NULL" +) + +# "start"/"end" name a position along a secondary axis, which x-categories do +# not have; offering them here would silently alias "desc" (ungrouped) or +# silently re-read as "first/last `by` level" (grouped) +expect_error( + tinyplot(~ cyl, data = mtcars, type = type_barplot(xord = "end")), + pattern = "not available for this plot type" +) + +# a bar is a single aggregate, so it has no variance to rank on: "minvar" is +# not part of this type's vocabulary at all +expect_error( + tinyplot(~ cyl | vs, data = mtcars, type = type_barplot(xord = "minvar")), + pattern = "must be NULL" +) + +# a ranking function may not ask for `x` here: bar categories are a flat set, +# so the only thing to hand over would be the `by` level index -- a nominal +# code that lm() would happily regress on and return a meaningless number +expect_error( + tinyplot(~ cyl | vs, data = mtcars, + type = type_barplot(xord = function(y, x) coef(lm(y ~ x))[2])), + pattern = "no secondary axis" +) +# ...but a plain function is fine. "asc" is now the direct route to ascending +# order, and must agree with the function that used to be the only way there +f = function() tinyplot(~ cyl, data = mtcars, type = type_barplot(xord = function(y) sum(y))) +expect_snapshot_plot(f, label = "barplot_xord_ascending") + +f = function() tinyplot(~ cyl, data = mtcars, type = type_barplot(xord = "asc")) +expect_snapshot_plot(f, label = "barplot_xord_ascending") + +# naming a strict subset of levels silently dropped the rest to NA, taking +# those observations out of the plot without a word (#645 follow-up) +expect_warning( + tinyplot(~ cyl, data = mtcars, type = type_barplot(xlevels = c("8", "4"))), + pattern = "omits 1 of the 3 levels" +) + +asis_dat = data.frame( + g = factor(c("z", "z", "a", "m", "m", "m")), # appearance z,a,m; levels a,m,z + v = c(5, 5, 1, 3, 3, 3) +) +# what "asis" computes: appearance order, not level order +expect_equal( + levels(tinyplot:::sanitize_ord(asis_dat$g, NULL, NULL, "asis", keywords = tinyplot:::ord_keywords_scalar)), + c("z", "a", "m") +) +# ...and *where* barplot applies it, which the unit call above cannot check: +# "asis" has to run before aggregate(), which sorts on the grouping columns and +# would otherwise leave it returning plain level order. The ranking keywords +# have the opposite requirement, so the two are applied at different points. +f = function() tinyplot(v ~ g, data = asis_dat, type = type_barplot(xord = "asis")) +expect_snapshot_plot(f, label = "barplot_xord_asis") + + +# xaxl dictionary relabelling (replaces deprecated, type-specific xaxalabels) +f = function() { + tinyplot(~ cyl, data = mtcars, type = "barplot", + xaxl = c("4" = "four", "6" = "six", "8" = "eight")) +} +expect_snapshot_plot(f, label = "barplot_xaxl_dict") + diff --git a/inst/tinytest/test-type_lines.R b/inst/tinytest/test-type_lines.R index ea8d8278..1419da7e 100644 --- a/inst/tinytest/test-type_lines.R +++ b/inst/tinytest/test-type_lines.R @@ -44,10 +44,14 @@ expect_snapshot_plot(f, label = "type_lines_layer_h_p") # "asis" keyword takes the categories in the order they appear in the data, # restoring the pre-fix behaviour on demand; forwarded automatically from the # top-level call. -f = function() tinyplot(runtime ~ name, data = LOTR, type = "b", xlevels = "asis") +f = function() tinyplot(runtime ~ name, data = LOTR, type = "b", xord = "asis") expect_snapshot_plot(f, label = "type_lines_xlevels_asis") # numeric indexes into the existing levels, via the constructor f = function() tinyplot(runtime ~ name, data = LOTR, type = type_points(xlevels = 3:1)) expect_snapshot_plot(f, label = "type_points_xlevels_idx") + +# `xord` composes with `xlevels` and covers the keyword vocabulary +f = function() tinyplot(runtime ~ name, data = LOTR, type = "b", xord = "rev") +expect_snapshot_plot(f, label = "lines_xord_rev") diff --git a/inst/tinytest/test-type_pointrange.R b/inst/tinytest/test-type_pointrange.R index 9715b3ea..a0e4a2ce 100644 --- a/inst/tinytest/test-type_pointrange.R +++ b/inst/tinytest/test-type_pointrange.R @@ -72,7 +72,7 @@ fun = function() { } expect_snapshot_plot(fun, label = "pointrange_with_layers_flipped") -# xlevels = NULL overrides the "asis" default, ordering the terms by their +# xord = NULL overrides the "asis" default, ordering the terms by their # factor levels (alphabetical here) instead of their row order (#679) fun = function() { with( @@ -82,8 +82,20 @@ fun = function() { y = y, ymin = ymin, ymax = ymax, - type = type_pointrange(xlevels = NULL) + type = type_pointrange(xord = NULL) ) ) } expect_snapshot_plot(fun, label = "pointrange_xlevels_null") + +# `xord` defaults to "asis" here, so the ignored-argument warning must key on +# whether the user actually supplied it -- otherwise every numeric-x +# coefficient plot would warn on its own default +cf2 = data.frame(x = c(1, 2, 3), lo = c(0, 1, 2), hi = c(2, 3, 4)) +expect_silent( + tinyplot(x ~ x, data = cf2, ymin = lo, ymax = hi, type = type_pointrange()) +) +expect_warning( + tinyplot(x ~ x, data = cf2, ymin = lo, ymax = hi, type = type_pointrange(xord = "rev")), + pattern = "only categorical" +) diff --git a/inst/tinytest/test-type_points.R b/inst/tinytest/test-type_points.R new file mode 100644 index 00000000..1d9d2191 --- /dev/null +++ b/inst/tinytest/test-type_points.R @@ -0,0 +1,37 @@ +source("helpers.R") +using("tinysnapshot") + + +# +## xord ----- + +f = function() tinyplot(mpg ~ factor(cyl), data = mtcars, type = type_points(xord = "desc")) +expect_snapshot_plot(f, label = "points_xord_desc") + +f = function() tinyplot(mpg ~ factor(cyl), data = mtcars, type = type_points(xord = "rev")) +expect_snapshot_plot(f, label = "points_xord_rev") + +# The ranking statistic follows what the type draws. A distribution type ranks +# on the mean, so a category of many small values must not outrank one of few +# large ones -- which is exactly what summing would do here. +d = data.frame(g = factor(rep(c("A", "B"), c(100, 5))), y = c(rep(1, 100), rep(10, 5))) +expect_equal(levels(tinyplot:::sanitize_ord(d$g, d$y, NULL, "desc", stat = "mean")), c("B", "A")) +expect_equal(levels(tinyplot:::sanitize_ord(d$g, d$y, NULL, "desc", stat = "sum")), c("A", "B")) + + +# +## numeric x cannot be reordered ----- + +# a numeric x is plotted at its own values, so the request is dropped -- but +# silently dropping it is the failure mode worth surfacing +expect_warning( + tinyplot(mpg ~ cyl, data = mtcars, type = type_points(xord = "desc")), + pattern = "only categorical" +) +expect_warning( + tinyplot(mpg ~ cyl, data = mtcars, type = type_points(xlevels = c("4", "6"))), + pattern = "only categorical" +) +# ...but a factor x is fine, and so is not asking in the first place +expect_silent(tinyplot(mpg ~ factor(cyl), data = mtcars, type = type_points(xord = "desc"))) +expect_silent(tinyplot(mpg ~ cyl, data = mtcars, type = type_points())) diff --git a/inst/tinytest/test-type_ridge.R b/inst/tinytest/test-type_ridge.R index bce0c812..0ed6fb47 100644 --- a/inst/tinytest/test-type_ridge.R +++ b/inst/tinytest/test-type_ridge.R @@ -190,3 +190,48 @@ expect_error( pattern = "at least 2 data points" ) expect_error(type_ridge(singletons = "nope")) + + +# +## yord ----- + +# ridges rank on the continuous `x`, since there is no separate response +f = function() tinyplot(Species ~ Sepal.Length, data = iris, type = type_ridge(yord = "minvar")) +expect_snapshot_plot(f, label = "ridge_yord_minvar") + +f = function() tinyplot(Species ~ Sepal.Length, data = iris, type = type_ridge(yord = "rev")) +expect_snapshot_plot(f, label = "ridge_yord_rev") + +# a transposed formula leaves nothing numeric to rank on; say so plainly +expect_error( + tinyplot(Sepal.Length ~ Species, data = iris, type = type_ridge(yord = "minvar")), + pattern = "ranks on a numeric variable" +) +# ...including when the ranking is a function, which cannot be interpolated +# into the message the way a keyword can +expect_error( + tinyplot(Sepal.Length ~ Species, data = iris, + type = type_ridge(yord = function(z) -mean(z))), + pattern = "ranks on a numeric variable" +) + +expect_error( + tinyplot(Species ~ Sepal.Length, data = iris, type = type_ridge(yord = "start")), + pattern = "not available for this plot type" +) + + +# +## yaxl ----- + +# ridge draws its own y-axis category labels, so `yaxl` has to be carried +# through `type_info` to the tinyAxis() calls rather than picked up by the +# standard axis path +f = function() tinyplot(Species ~ Sepal.Width, data = iris, type = "ridge", yaxl = toupper) +expect_snapshot_plot(f, label = "ridge_yaxl_toupper") + +f = function() { + tinyplot(Species ~ Sepal.Width, data = iris, type = "ridge", + yaxl = c(setosa = "SET", virginica = "VIR")) +} +expect_snapshot_plot(f, label = "ridge_yaxl_dict") diff --git a/inst/tinytest/test-type_spineplot.R b/inst/tinytest/test-type_spineplot.R index f47466a6..3876856c 100644 --- a/inst/tinytest/test-type_spineplot.R +++ b/inst/tinytest/test-type_spineplot.R @@ -127,3 +127,51 @@ f = function() { ) } expect_snapshot_plot(f, label = "spineplot_yby_lighten_true") + + +# +## xord / yord ----- + +# both spineplot axes are categorical, so the size keywords rank on frequency +f = function() { + tinyplot(Species ~ cut(Sepal.Length, 3), data = iris, type = "spineplot", xord = "desc") +} +expect_snapshot_plot(f, label = "spineplot_xord_desc") + +f = function() { + tinyplot(Species ~ cut(Sepal.Length, 3), data = iris, type = "spineplot", yord = "rev") +} +expect_snapshot_plot(f, label = "spineplot_yord_rev") + +# a spine is a proportion of a count, with no dispersion of its own, so +# "minvar" is not part of this type's vocabulary +expect_error( + tinyplot(Species ~ cut(Sepal.Length, 3), data = iris, type = "spineplot", xord = "minvar"), + pattern = "must be NULL" +) + + +# +## xaxl / yaxl ----- + +# this type draws its own axes, so it never reaches the standard path where +# `xaxl`/`yaxl` are applied; they are applied inside data_spineplot() instead +f = function() { + tinyplot(Species ~ Sepal.Width, data = iris, type = "spineplot", yaxl = toupper) +} +expect_snapshot_plot(f, label = "spineplot_yaxl_toupper") + +f = function() { + tinyplot(Species ~ Sepal.Width, data = iris, type = "spineplot", + yaxl = c(setosa = "SET", virginica = "VIR")) +} +expect_snapshot_plot(f, label = "spineplot_yaxl_dict") + +# categorical x, and a numeric x whose breaks take a formatting keyword +spine_d = data.frame( + grp = factor(rep(c("alpha", "beta"), each = 50)), + resp = factor(rep(c("lo", "hi"), 50), levels = c("lo", "hi")) +) +f = function() tinyplot(resp ~ grp, data = spine_d, type = "spineplot", xaxl = toupper) +expect_snapshot_plot(f, label = "spineplot_xaxl_toupper") + diff --git a/man/build_legend_args.Rd b/man/build_legend_args.Rd index 1e1ea297..28492eee 100644 --- a/man/build_legend_args.Rd +++ b/man/build_legend_args.Rd @@ -32,7 +32,9 @@ build_legend_args( \item{lgnd_labs}{The legend labels} -\item{labeller}{Character or function for formatting labels} +\item{labeller}{Function, character keyword, or dictionary (named vector or +list) for formatting or relabelling the labels. See \code{\link{tinylabel}} for the +accepted forms.} \item{type}{Plot type} diff --git a/man/build_legend_env.Rd b/man/build_legend_env.Rd index 9deb42e9..62a162b3 100644 --- a/man/build_legend_env.Rd +++ b/man/build_legend_env.Rd @@ -36,7 +36,9 @@ build_legend_env( \item{lgnd_labs}{The legend labels} -\item{labeller}{Character or function for formatting labels} +\item{labeller}{Function, character keyword, or dictionary (named vector or +list) for formatting or relabelling the labels. See \code{\link{tinylabel}} for the +accepted forms.} \item{type}{Plot type} diff --git a/man/draw_legend.Rd b/man/draw_legend.Rd index b604375c..412ba7b5 100644 --- a/man/draw_legend.Rd +++ b/man/draw_legend.Rd @@ -39,8 +39,9 @@ draw_legend( \item{lgnd_labs}{The labels passed to \code{legend(legend = ...)}.} -\item{labeller}{Character or function for formatting the labels (\code{lgnd_labs}). -Passed down to \code{\link{tinylabel}}.} +\item{labeller}{Function, character keyword, or dictionary (named vector or +list) for formatting or relabelling the labels (\code{lgnd_labs}). See +\code{\link{tinylabel}} for the accepted forms. Passed down to \code{\link{tinylabel}}.} \item{type}{Plotting type(s), passed down from \link{tinyplot}.} diff --git a/man/tinyAxis.Rd b/man/tinyAxis.Rd index d052c848..baacbad0 100644 --- a/man/tinyAxis.Rd +++ b/man/tinyAxis.Rd @@ -20,12 +20,29 @@ arguments of the parent \code{\link[=tinyplot]{tinyplot()}} call. One of either: labels without ticks and axis line), or \code{"axis"} (only axis line and labels but no ticks). Partial matching is allowed, e.g. \code{type = "s"}.} -\item{labeller}{a formatting function to be applied to \code{x}, e.g. \code{\link{format}}, -\code{\link{toupper}}, \code{\link{abs}}, or other custom function (including from the popular -\strong{scales} package). Can also be one of the following convenience strings -(symbols), for which common formatting transformations are provided: -\code{"percent"} (\code{"\%"}), \code{"comma"} (\code{","}), \code{"log"} (\code{"l"}), \code{"dollar"} -(\code{"$"}), \code{"euro"} (\code{"€"}), or \code{"sterling"} (\code{"£"}).} +\item{labeller}{how the labels should be relabelled or reformatted. One of: +\itemize{ +\item a function to be applied to \code{x}, e.g. \code{\link{format}}, \code{\link{toupper}}, \code{\link{abs}}, +or any other custom function (including from the popular \strong{scales} +package). +\item one of the following convenience strings (or their symbol equivalents), +for which common formatting transformations are provided: \code{"percent"} +(\code{"\%"}), \code{"comma"} (\code{","}), \code{"log"} (\code{"l"}), \code{"dollar"} (\code{"$"}), \code{"euro"} +(\code{"€"}), or \code{"sterling"} (\code{"£"}). +\item a dictionary, i.e. a \emph{named} character vector or list of form +\code{c(old_lab = "new_lab", ...)}. Entries of \code{x} that match a name are +replaced by the corresponding value and the rest are left alone, so a +partial mapping is fine. The lookup is by value rather than by position, +which means it is unaffected by any reordering of the underlying +categories. +} + +Note that an \emph{unnamed} character vector of length greater than one is an +error, rather than a positional replacement of the labels. Such a vector +cannot be distinguished from a formatting keyword when \code{x} is of length +one, and would not follow the categories if they were reordered. Pass a +function if you want to compute the labels positionally, e.g. +\code{function(x) LETTERS[seq_along(x)]}.} } \description{ Internal function used for adding an axis to a \code{\link{tinyplot}} diff --git a/man/tinylabel.Rd b/man/tinylabel.Rd index 7150eef7..4ebbe088 100644 --- a/man/tinylabel.Rd +++ b/man/tinylabel.Rd @@ -9,12 +9,29 @@ tinylabel(x, labeller = NULL, na.ignore = TRUE, na.rm = TRUE) \arguments{ \item{x}{a numeric or character vector} -\item{labeller}{a formatting function to be applied to \code{x}, e.g. \code{\link{format}}, -\code{\link{toupper}}, \code{\link{abs}}, or other custom function (including from the popular -\strong{scales} package). Can also be one of the following convenience strings -(symbols), for which common formatting transformations are provided: -\code{"percent"} (\code{"\%"}), \code{"comma"} (\code{","}), \code{"log"} (\code{"l"}), \code{"dollar"} -(\code{"$"}), \code{"euro"} (\code{"€"}), or \code{"sterling"} (\code{"£"}).} +\item{labeller}{how the labels should be relabelled or reformatted. One of: +\itemize{ +\item a function to be applied to \code{x}, e.g. \code{\link{format}}, \code{\link{toupper}}, \code{\link{abs}}, +or any other custom function (including from the popular \strong{scales} +package). +\item one of the following convenience strings (or their symbol equivalents), +for which common formatting transformations are provided: \code{"percent"} +(\code{"\%"}), \code{"comma"} (\code{","}), \code{"log"} (\code{"l"}), \code{"dollar"} (\code{"$"}), \code{"euro"} +(\code{"€"}), or \code{"sterling"} (\code{"£"}). +\item a dictionary, i.e. a \emph{named} character vector or list of form +\code{c(old_lab = "new_lab", ...)}. Entries of \code{x} that match a name are +replaced by the corresponding value and the rest are left alone, so a +partial mapping is fine. The lookup is by value rather than by position, +which means it is unaffected by any reordering of the underlying +categories. +} + +Note that an \emph{unnamed} character vector of length greater than one is an +error, rather than a positional replacement of the labels. Such a vector +cannot be distinguished from a formatting keyword when \code{x} is of length +one, and would not follow the categories if they were reordered. Pass a +function if you want to compute the labels positionally, e.g. +\code{function(x) LETTERS[seq_along(x)]}.} \item{na.ignore}{logical indicating whether the labelling function should ignore \code{NA} values in \code{x}. In other words, should the \code{NA} values be left @@ -39,6 +56,10 @@ tinylabel(x, "comma") tinylabel(x, ",") # same tinylabel(x, "$") # or "dollar" +# a named vector acts as a dictionary: matched entries are replaced and the +# rest are left alone +tinylabel(c("setosa", "versicolor"), c(setosa = "SET")) + # invoke tinylabel from a parent tinyplot call... # => x/yaxl for adjusting axes tick labels # => legend = list(labeller = ...) for adjusting the legend labels diff --git a/man/tinyplot.Rd b/man/tinyplot.Rd index 609420bb..8b804b53 100644 --- a/man/tinyplot.Rd +++ b/man/tinyplot.Rd @@ -220,8 +220,11 @@ format each variable differently, e.g. \code{labeller = list(firm = toupper, yield = "\%")}, with any variable left unnamed not formatted. While not recommended, unnamed values are matched positionally, according to the variable order in the \code{facet} formula -specification. Defaults to the value of \code{tpar("facet.labeller")}, which is -\code{NULL} (no formatting). +specification. Note that this per-variable naming claims the same slot that +a \code{\link{tinylabel}} dictionary would, so a dictionary has to be nested inside +it, e.g. \code{labeller = list(Species = c(setosa = "SET"))}; a bare named +vector is read as a per-variable mapping instead. Defaults to the value of +\code{tpar("facet.labeller")}, which is \code{NULL} (no formatting). \item \code{prefix} a logical or character value for prefixing the facet titles with a descriptive name. Pass \code{TRUE} to prefix with the (deparsed) facet variable name(s), e.g. \code{"am = 0"} instead of just \code{"0"}. Alternatively, @@ -417,16 +420,12 @@ the break points at which the axis tick-marks are to be drawn. Break points outside the range of the data will be ignored if the associated axis variable is categorical, or an explicit \code{x/ylim} range is given.} -\item{xaxl, yaxl}{a function or a character keyword specifying the format of -the x- or y-axis tick labels. Note that this is a post-processing step that +\item{xaxl, yaxl}{a function, character keyword, or dictionary (named vector +or list) for formatting or (re)labelling the x- or y-axis tick labels. +Passed to \code{\link{tinylabel}}; see the latter's help file for more detailed +documentation and examples. Note that this is a post-processing step that affects the \emph{appearance} of the tick labels only; use in conjunction with -\code{x/yaxb} if you would like to adjust the position of the tick marks too. In -addition to user-supplied formatting functions (e.g., \code{\link{format}}, -\code{\link{toupper}}, \code{\link{abs}}, or other custom function), several convenience -keywords (or their symbol equivalents) are available for common formatting -transformations: \code{"percent"} (\code{"\%"}), \code{"comma"} (\code{","}), \code{"log"} (\code{"l"}), -\code{"dollar"} (\code{"$"}), \code{"euro"} (\code{"€"}), or \code{"sterling"} (\code{"£"}). See the -\code{\link{tinylabel}} documentation for examples.} +\code{x/yaxb} if you would like to adjust the position of the tick marks too.} \item{log}{a character string which contains \code{"x"} if the x axis is to be logarithmic, \code{"y"} if the y axis is to be logarithmic and \code{"xy"} or \code{"yx"} diff --git a/man/tinyplot.matrix.Rd b/man/tinyplot.matrix.Rd index e39f60a4..ad908448 100644 --- a/man/tinyplot.matrix.Rd +++ b/man/tinyplot.matrix.Rd @@ -72,13 +72,20 @@ tinyplot(VADeaths, type = "b") tinyplot(VADeaths, type = "b", legend = "direct", theme = "socviz") tinyplot(VADeaths, type = "b", legend = FALSE, facet = "by", theme = "socviz") -# equivalent plot to an example in `?matplot` +# digression: equivalent "o" plot to an example in `?matplot` sines = outer(1:20, 1:4, function(x, y) sin(x / 20 * pi * y)) tinyplot(sines, type = "o", pch = "by", lty = "by", col = rainbow(ncol(sines))) -# `"tile"` + `"heatmap"` types lay the matrix out as a grid instead +# back to VADeaths running example, we can pass down other types too... + +# heatmap tinyplot(VADeaths, type = "heatmap", theme = "heatmap", col = "white") +# barplot(s) +tinyplot(VADeaths, type = "barplot", beside = TRUE) +tinyplot(t(VADeaths), type = "barplot", beside = TRUE) +tinyplot(VADeaths, type = "barplot", facet = "by", legend = FALSE) + } \seealso{ \code{\link[graphics]{matplot}} diff --git a/man/tpar.Rd b/man/tpar.Rd index 47ebd4a4..b4cc1866 100644 --- a/man/tpar.Rd +++ b/man/tpar.Rd @@ -78,7 +78,7 @@ you should rather use \code{par()} instead. \item \code{facet.cex}: Expansion factor for facet titles. Defaults to \code{1}. \item \code{facet.col}: Character or integer specifying the facet text colour. If an integer, will correspond to the user's default global colour palette (see \code{palette}). Defaults to \code{NULL}, which is equivalent to "black". \item \code{facet.font}: An integer corresponding to the desired font face for facet titles. For most font families and graphics devices, one of four possible values: \code{1} (regular), \code{2} (bold), \code{3} (italic), or \code{4} (bold italic). Defaults to \code{NULL}, which is equivalent to \code{1} (i.e., regular). -\item \code{facet.labeller}: A formatting function (or \code{\link{tinylabel}} convenience string, e.g. \code{"percent"}) applied to the facet titles, or a list of them (or a character vector of convenience strings), optionally named for the facet variables they apply to, for formatting each facet variable differently. Defaults to \code{NULL} (no formatting). Applied to the underlying facet values, i.e. before any \code{facet.prefix} name is added. Equivalent to setting \code{tinyplot(..., facet.args = list(labeller = X))}, but globally. +\item \code{facet.labeller}: A formatting function (or \code{\link{tinylabel}} convenience string, e.g. \code{"percent"}) applied to the facet titles, or a list of them (or a character vector of convenience strings), optionally named for the facet variables they apply to, for formatting each facet variable differently. Note that this per-variable naming claims the same slot that a \code{\link{tinylabel}} dictionary would, so a dictionary has to be nested inside it, e.g. \code{facet.labeller = list(Species = c(setosa = "SET"))}. Defaults to \code{NULL} (no formatting). Applied to the underlying facet values, i.e. before any \code{facet.prefix} name is added. Equivalent to setting \code{tinyplot(..., facet.args = list(labeller = X))}, but globally. \item \code{facet.prefix}: Logical or character controlling whether facet titles are prefixed with their variable name, e.g. \code{"vs = 0"} rather than just \code{"0"}. \code{TRUE} uses the variable name(s), while a character string---or a vector or list of them, one element per facet variable, optionally named for the variables they apply to---supplies custom name(s) instead. Defaults to \code{NULL}, which is equivalent to \code{FALSE} (no prefix). Equivalent to setting \code{tinyplot(..., facet.args = list(prefix = X))}, but globally. \item \code{facet.sep}: Character string separating the individual variables of a multi-variable facet title, e.g. \code{"\\n"} to stack them on separate lines. Ignored for single-variable facets. Defaults to \code{NULL}, i.e. the \code{":"} that the variables were combined with, or \code{", "} if they are prefixed via \code{facet.prefix} (above). Equivalent to setting \code{tinyplot(..., facet.args = list(sep = X))}, but globally. \item \code{file.height}: Numeric specifying the height (in inches) of any plot that is written to disk using the \code{tinyplot(..., file = X)} argument. Defaults to \code{7}. diff --git a/man/type_barplot.Rd b/man/type_barplot.Rd index 5e3b72ee..d47299bd 100644 --- a/man/type_barplot.Rd +++ b/man/type_barplot.Rd @@ -11,9 +11,10 @@ type_barplot( offset = NULL, FUN = NULL, xlevels = NULL, - xaxlabels = NULL, + xord = NULL, drop.zeros = FALSE, - lighten = TRUE + lighten = TRUE, + xaxlabels = NULL ) } \arguments{ @@ -36,7 +37,8 @@ one of two distinct forms. See the Examples for illustrations of both. \itemize{ \item \emph{Positions} via an unnamed numeric scalar or vector. Bars start at the offset value(s) rather than zero, matched per x-level after any \code{xlevels} -reordering (a scalar is applied to all bars). Useful for waterfall charts. +or \code{xord} reordering (a scalar is applied to all bars). Useful for +waterfall charts. The positional form cannot be combined with \code{center}. \item \emph{Category} via a character vector such as \code{offset = "Unsure"}, or a named numeric vector such as \code{offset = c(Unsure = 1.1)}. The named @@ -50,15 +52,39 @@ grouping and \code{beside = FALSE}, but can be combined with \code{center}. \item{FUN}{a function to compute the summary statistic for \code{y} within each group of \code{x} in case of using a two-sided formula \code{y ~ x} (default: mean).} -\item{xlevels}{a character or numeric vector specifying the ordering of the -levels of the \code{x} variable (if character) or the corresponding indexes -(if numeric) for the plot. The special keyword \code{"asis"} takes the -categories in the order that they appear in the data. Note that this -argument only affects categorical (i.e., factor or character) \code{x} -variables.} +\item{xlevels, xord}{arguments controlling the order of the \code{x} variable, and +hence of the x-axis. Supply one or the other; if both arguments are +provided, \code{xlevels} takes precedence and \code{xord} is silently ignored. +\itemize{ +\item \code{xlevels} specifies the levels \emph{literally}, either a character vector of +level names in the desired order (e.g., \code{c("C", "B", "A")}), or a numeric +vector of the corresponding level indexes (e.g. \code{3:1}). +\item \code{xord} instead accepts a keyword or custom function, which then \emph{derives} +the order from the data. Options are: +\itemize{ +\item \code{"desc(ending)"} and \code{"asc(ending)"} rank (sort) the categories by bar +height, tallest or shortest first. Both the abbreviated and long form +strings are permitted, as are the \code{"decreasing"} and \code{"increasing"} +aliases. Note that the ranking is applied to the \emph{aggregated} bars, i.e. +whatever \code{FUN} produced, rather than the underlying rows. With \code{by} +groups or facets, a single ordering is computed and shared across all of +them, by summing each category's bars over every group and facet. For +stacked bars that sum is the height of the full stack; with +\code{beside = TRUE} it is the group total rather than any individual bar. (A +factor carries one level order, so a per-facet ranking is not +expressible.) +\item \code{"asis"} or \code{"rev"} permute the existing levels without consulting the +data at all. The former takes the categories in the order that they +appear in the data, while the latter reverses the current level order. +\item a custom function that determines both the ranking statistic and its +direction. The statistic is always sorted in ascending order, so +\code{function(y) -median(y)} ranks by median, largest first. +} +} -\item{xaxlabels}{a character vector with the axis labels for the \code{x} variable, -defaulting to the levels of \code{x}.} +Note that a numeric \code{x} is coerced to a factor before the bars are drawn, +so it is reordered like any other categorical variable. +Each argument defaults to \code{NULL}, i.e. keep the existing factor levels.} \item{drop.zeros}{logical. Should bars with zero height be dropped? If set to \code{FALSE} (default) a zero height bar is still drawn for which the border @@ -68,6 +94,10 @@ lines will still be visible.} series colour(s)? Default is \code{TRUE}, which keeps single- and multi-group displays consistent and lets the fill read cleanly over grid lines. Set to \code{FALSE} to use the fully-saturated palette colour(s) instead.} + +\item{xaxlabels}{[Deprecated] a character vector with the axis labels for +the \code{x} variable. Use the top-level \code{xaxl} argument instead (see +\code{\link{tinylabel}}). This argument will be removed in a future release.} } \description{ Type function for producing barplots. For formulas of type @@ -77,41 +107,93 @@ of \code{y} within each level of \code{x} is visualized, if necessary aggregated using some function (default: mean). } \examples{ -# Basic examples of frequency tables (without y variable) -tinyplot(~ cyl, data = mtcars, type = "barplot") -tinyplot(~ cyl | vs, data = mtcars, type = "barplot") -tinyplot(~ cyl | vs, data = mtcars, type = "barplot", beside = TRUE) +# +## Basic use (raw values) -# Reorder x variable categories either by their character levels or numeric indexes -tinyplot(~ cyl, data = mtcars, type = "barplot", xlevels = c("8", "6", "4")) -tinyplot(~ cyl, data = mtcars, type = "barplot", xlevels = 3:1) +tinyplot(GNP ~ Year, data = longley, type = "barplot") + +tinyplot(demand ~ Time, data = BOD, type = "bar") # "bar" is a shorthand +tinyplot_add(type = "text", pos = 3, xpd = NA) # add y values as text + +# reordering (just to demonstrate; these aren't sensible for a time variable) +tinyplot(demand ~ Time, data = BOD, type = "bar", xord = "asc") +jumble = c("7","1","5","2","4","3") # note: Time = 6 is also missing +tinyplot(demand ~ Time, data = BOD, type = "bar", xlevels = jumble) + +# +## Aggregated vs grouped values (multiple ys per x) + +# each person receives two drugs +sleep2 = transform(sleep, drug = group) # less misleading name + +# default aggregation FUN is mean +tinyplot( + extra ~ ID, data = sleep2, + type = "barplot", + main = "Mean extra sleep from 2 soporific drugs" +) +# switch to diff (answers a more relevant q: who benefits most from drug 2?) +tinyplot( + extra ~ ID, data = sleep2, + type = "barplot", FUN = diff, + main = "Sleep gain (drug 2 vs drug 1)" +) +# we can sort in descending (or ascending) order too +tinyplot( + extra ~ ID, data = sleep2, + type = "barplot", FUN = diff, xord = "desc", + main = "Sleep gain (drug 2 vs drug 1), ordered" +) -# Note: Above we used automatic argument passing for `beside`. But this -# wouldn't work for `width`, since it would conflict with the top-level +# of course, we don't have to aggregate if we specify groups (stacked or non) +tinyplot(extra ~ ID | drug, data = sleep2, type = "barplot", beside = TRUE) +# Note: We used automatic argument passing for 'xord', `FUN`, etc. above. But +# this wouldn't work for `width`, since it would conflict with the top-level # `tinyplot(..., width = )` argument. It's safer to pass these args -# through the `type_barplot()` functional equivalent. +# through the `type_barplot()` functional equivalent... + tinyplot( - ~ cyl | vs, data = mtcars, - type = type_barplot(beside = TRUE, drop.zeros = TRUE, width = 0.65) + extra ~ ID | drug, data = sleep2, + type = type_barplot(beside = TRUE, xord = "desc", width = 0.5) ) -# Example for numeric y aggregated by x (default: FUN = mean) + facets +# speaking of top-level args, use xaxl to format the x labels, e.g. with a +# dictionary, keyword, or (here:) function + tinyplot( - extra ~ ID | group, facet = "by", data = sleep, - type = "barplot", - theme = "clean2" + extra ~ ID | drug, data = sleep2, + type = type_barplot(beside = TRUE, xord = "desc"), + xaxl = as.roman ) -# Fancy frequency table: +# +## matrix method (no formula required) + +tinyplot(VADeaths, type = "barplot") +tinyplot(VADeaths, type = "barplot", beside = TRUE) +# etc. see ?tinyplot.matrix + +# +## Frequency tables + +# No y variable (frequency calculated on the fly) +tinyplot(~ cyl, data = mtcars, type = "barplot") +tinyplot(~ cyl | vs, data = mtcars, type = "barplot") +tinyplot(~ cyl | vs, data = mtcars, type = "barplot", beside = TRUE) + +# Fancy frequency table (y = frequency aleady computed) tinyplot( Freq ~ Sex | Survived, data = as.data.frame(Titanic), facet = ~ Class, facet.args = list(nrow = 1), - type = "barplot", flip = TRUE, + type = "barplot", beside = TRUE, flip = TRUE, theme = "clean2" ) -# Centered barplot for conditional proportions of dark (black/brown) vs. -# light (red/blond) hair color, conditional on eye color and sex. +# +## Centering + +# Centered barplot for conditional proportions of "dark" (black/brown) vs. +# "fair" (red/blond) hair color, conditional on eye color and sex. # Aside: use `lighten = FALSE` to avoid lightening the bar fill colors. hec = as.data.frame(proportions(HairEyeColor, 2:3)) hcols = c("black", "sienna", "indianred", "goldenrod") @@ -122,8 +204,10 @@ tinyplot( flip = TRUE, yaxl = "percent", theme = list("clean2", palette.qualitative = hcols) ) +tinyplot_add(type = "vline", col = "white") -# Use cases for the `offset` argument +# +## Offset examples # 1. Waterfall plot d = data.frame(item = c("Sales", "Services", "Costs", "Returns", "TOTAL"), @@ -165,5 +249,6 @@ tinyplot( main = "Hypothetical Likert example with category offset" ) tinyplot_add(type = "vline") +tinyplot_add(type = "vline", v = 1, lty = 2) } diff --git a/man/type_errorbar.Rd b/man/type_errorbar.Rd index 3d59cc6b..b1e4bfe9 100644 --- a/man/type_errorbar.Rd +++ b/man/type_errorbar.Rd @@ -5,9 +5,15 @@ \alias{type_pointrange} \title{Error bar and pointrange plot types} \usage{ -type_errorbar(length = 0.05, dodge = 0, fixed.dodge = FALSE, xlevels = "asis") +type_errorbar( + length = 0.05, + dodge = 0, + fixed.dodge = FALSE, + xlevels = NULL, + xord = "asis" +) -type_pointrange(dodge = 0, fixed.dodge = FALSE, xlevels = "asis") +type_pointrange(dodge = 0, fixed.dodge = FALSE, xlevels = NULL, xord = "asis") } \arguments{ \item{length}{length of the edges of the arrow head (in inches).} @@ -34,17 +40,38 @@ present at that position. If \code{TRUE}, dodge positions are based on all groups, ensuring "fixed" spacing across x-axis breaks (i.e., even if some groups are missing for a particular \code{x} value).} -\item{xlevels}{a character or numeric vector specifying the order in which -the levels of the \code{x} variable should be plotted (as level names if -character, or level indexes if numeric, e.g. \code{3:1}). Note that this -argument only affects categorical (i.e., factor or character) \code{x} -variables; it is ignored for numeric \code{x}. Unlike most other plot types, -here it defaults to the special keyword \code{"asis"}, which takes the -categories in the order that they appear in the data: these types are -typically used for coefficient plots, where the row order of the data -(e.g., the terms of a model) is usually intentional. Set -\code{xlevels = NULL} to follow the factor levels instead, matching the other -plot types.} +\item{xlevels, xord}{arguments controlling the order of the \code{x} variable, and +hence of the x-axis. Supply one or the other; if both arguments are +provided, \code{xlevels} takes precedence and \code{xord} is silently ignored. +\itemize{ +\item \code{xlevels} specifies the levels \emph{literally}, either a character vector of +level names in the desired order (e.g., \code{c("C", "B", "A")}), or a numeric +vector of the corresponding level indexes (e.g. \code{3:1}). +\item \code{xord} instead accepts a keyword or custom function, which then \emph{derives} +the order from the data. Options are: +\itemize{ +\item \code{"desc"} and \code{"asc"} rank the categories by their mean \code{y} value, +largest or smallest first. (Long forms like \code{"descending"} and \code{"increasing"} are also accepted.) +\item \code{"minvar"} ranks them by variance, lowest first. This needs more than +one observation per category, so it does not apply to the usual +one-row-per-term coefficient table. +\item \code{"asis"} or \code{"rev"} permute the existing levels without consulting the +data at all. The former takes the categories in the order that they +appear in the data, while the latter reverses the current level order. +\item a custom function that determines both the ranking statistic and its +direction. The statistic is always sorted ascending, so +\code{function(y) -median(y)} ranks by median, largest first. +} +} + +Note that \code{x} is only reordered when it is categorical (i.e., factor or +character). A numeric \code{x} is plotted at its own values and cannot be +reordered, so supplying either argument there is ignored with a warning. + +Unlike most other plot types, \code{xord} defaults to \code{"asis"} here rather than +\code{NULL}: these types are typically used for coefficient plots, where the row +order of the data (e.g., the terms of a model) is usually intentional. Set +\code{xord = NULL} to follow the factor levels instead, matching the other types.} } \description{ Type function(s) for producing error bar and pointrange plots. diff --git a/man/type_lines.Rd b/man/type_lines.Rd index 79acd24d..f9be54a1 100644 --- a/man/type_lines.Rd +++ b/man/type_lines.Rd @@ -4,7 +4,13 @@ \alias{type_lines} \title{Lines plot type} \usage{ -type_lines(type = "l", dodge = 0, fixed.dodge = FALSE, xlevels = NULL) +type_lines( + type = "l", + dodge = 0, + fixed.dodge = FALSE, + xlevels = NULL, + xord = NULL +) } \arguments{ \item{type}{1-character string giving the type of plot desired. The @@ -40,15 +46,33 @@ present at that position. If \code{TRUE}, dodge positions are based on all groups, ensuring "fixed" spacing across x-axis breaks (i.e., even if some groups are missing for a particular \code{x} value).} -\item{xlevels}{a character or numeric vector specifying the order in which -the levels of the \code{x} variable should be plotted (as level names if -character, or level indexes if numeric, e.g. \code{3:1}). The special keyword -\code{"asis"} takes the categories in the order that they appear in the data, -i.e. skipping the alphabetical sort that is otherwise applied when -coercing a character variable to a factor. Note that this argument only -affects categorical (i.e., factor or character) \code{x} variables; it is -ignored for numeric \code{x}. The default \code{NULL} keeps the existing factor -levels (alphabetical for character variables).} +\item{xlevels, xord}{arguments controlling the order of the (categorical) \code{x} +variable, and hence of the x-axis. Supply one or the other; if both +arguments are provided, \code{xlevels} takes precedence and \code{xord} is silently +ignored. +\itemize{ +\item \code{xlevels} specifies the levels \emph{literally}, either a character vector of +level names in the desired order (e.g., \code{c("C", "B", "A")}), or a numeric +vector of the corresponding level indexes (e.g. \code{3:1}). +\item \code{xord} instead accepts a keyword or custom function, which then \emph{derives} +the order from the data. Options are: +\itemize{ +\item \code{"desc"} and \code{"asc"} rank the categories by their mean \code{y} value, +largest or smallest first. (Long forms like \code{"descending"} and \code{"increasing"} are also accepted.) +\item \code{"minvar"} ranks them by the variance of those values, lowest first. +\item \code{"asis"} or \code{"rev"} permute the existing levels without consulting the +data at all. The former takes the categories in the order that they +appear in the data, while the latter reverses the current level order. +\item a custom function that determines both the ranking statistic and its +direction. The statistic is always sorted ascending, so +\code{function(y) -median(y)} ranks by median, largest first. +} +} + +Note that \code{x} is only reordered when it is categorical (i.e., factor or +character). A numeric \code{x} is plotted at its own values and cannot be +reordered, so supplying either argument there is ignored with a warning. +Each argument defaults to \code{NULL}, i.e. keep the existing factor levels.} } \description{ Type function for plotting lines. @@ -60,7 +84,7 @@ Like the other plot types, \code{type_lines()} places categorical (factor or character) data according to the factor levels. Character variables are coerced with \code{\link[=factor]{factor()}} and so end up in alphabetical order. To order the categories by their appearance in the data instead, use -\code{xlevels = "asis"}, or set the levels explicitly, e.g. +\code{xord = "asis"}, or set the levels explicitly, e.g. \code{factor(x, levels = unique(x))}. Note that the lines themselves are always drawn in the order that the rows diff --git a/man/type_points.Rd b/man/type_points.Rd index 205c4cbb..4ee9342e 100644 --- a/man/type_points.Rd +++ b/man/type_points.Rd @@ -4,7 +4,13 @@ \alias{type_points} \title{Points plot type} \usage{ -type_points(clim = c(0.5, 2.5), dodge = 0, fixed.dodge = FALSE, xlevels = NULL) +type_points( + clim = c(0.5, 2.5), + dodge = 0, + fixed.dodge = FALSE, + xlevels = NULL, + xord = NULL +) } \arguments{ \item{clim}{Numeric giving the lower and upper limits of the character @@ -32,15 +38,33 @@ present at that position. If \code{TRUE}, dodge positions are based on all groups, ensuring "fixed" spacing across x-axis breaks (i.e., even if some groups are missing for a particular \code{x} value).} -\item{xlevels}{a character or numeric vector specifying the order in which -the levels of the \code{x} variable should be plotted (as level names if -character, or level indexes if numeric, e.g. \code{3:1}). The special keyword -\code{"asis"} takes the categories in the order that they appear in the data, -i.e. skipping the alphabetical sort that is otherwise applied when -coercing a character variable to a factor. Note that this argument only -affects categorical (i.e., factor or character) \code{x} variables; it is -ignored for numeric \code{x}. The default \code{NULL} keeps the existing factor -levels (alphabetical for character variables).} +\item{xlevels, xord}{arguments controlling the order of the (categorical) \code{x} +variable, and hence of the x-axis. Supply one or the other; if both +arguments are provided, \code{xlevels} takes precedence and \code{xord} is silently +ignored. +\itemize{ +\item \code{xlevels} specifies the levels \emph{literally}, either a character vector of +level names in the desired order (e.g., \code{c("C", "B", "A")}), or a numeric +vector of the corresponding level indexes (e.g. \code{3:1}). +\item \code{xord} instead accepts a keyword or custom function, which then \emph{derives} +the order from the data. Options are: +\itemize{ +\item \code{"desc"} and \code{"asc"} rank the categories by their mean \code{y} value, +largest or smallest first. (Long forms like \code{"descending"} and \code{"increasing"} are also accepted.) +\item \code{"minvar"} ranks them by the variance of those values, lowest first. +\item \code{"asis"} or \code{"rev"} permute the existing levels without consulting the +data at all. The former takes the categories in the order that they +appear in the data, while the latter reverses the current level order. +\item a custom function that determines both the ranking statistic and its +direction. The statistic is always sorted ascending, so +\code{function(y) -median(y)} ranks by median, largest first. +} +} + +Note that \code{x} is only reordered when it is categorical (i.e., factor or +character). A numeric \code{x} is plotted at its own values and cannot be +reordered, so supplying either argument there is ignored with a warning. +Each argument defaults to \code{NULL}, i.e. keep the existing factor levels.} } \description{ Type function for plotting points, i.e. a scatter plot. diff --git a/man/type_ribbon.Rd b/man/type_ribbon.Rd index 22d1ab7d..282b063a 100644 --- a/man/type_ribbon.Rd +++ b/man/type_ribbon.Rd @@ -24,25 +24,28 @@ area plots" section below.} the \code{by} group layers, thus controlling the order in which they stack. Options are: \itemize{ -\item \code{"start"}, \code{"end"}, and \code{"total"} are positional keywords that rank groups -according to their \code{y} values along the \code{x} axis. In each case, the group -with the largest value is stacked first as the bottom layer. -\item \code{"minvar"} ranks by variance and puts the lowest variance group on the +\item \code{"desc"} and \code{"asc"} rank the groups by their summed \code{y} across the \code{x} +axis, largest or smallest first. With \code{"desc"} the biggest group is stacked +first, as the bottom layer. (Long forms like \code{"descending"} and \code{"increasing"} are also accepted.) +\item \code{"start"} and \code{"end"} instead rank on the \code{y} values at the smallest and +largest \code{x} respectively, again stacking the largest group first. +\item \code{"minvar"} ranks by variance, putting the lowest variance group on the baseline. -\item \code{"asis"} and \code{"rev"} permute the existing levels without consulting the +\item \code{"asis"} or \code{"rev"} permute the existing levels without consulting the data at all. The former takes the groups in the order that they appear in -the data, while \code{"rev"} reverses the current level order. -\item custom function that determines both the ranking statistic and its -direction, e.g. \code{function(y) -median(y)} would layer by median \code{y} value, -from the biggest to the smallest. Note: if a function requires access to a -group's \code{x} values, then one of its arguments \emph{must} be named \code{x}, e.g. +the data, while the latter reverses the current level order. +\item a custom function that determines both the ranking statistic and its +direction. The statistic is always sorted ascending, so +\code{function(y) -median(y)} layers by median \code{y} value, from the biggest to +the smallest. Note that if a function requires access to a group's \code{x} +values, then one of its arguments \emph{must} be named \code{x}, e.g. \code{function(y, x) coef(lm(y ~ x))[2]} would layer by trend. } -Default is \code{NULL}, in which case the existing factor level order is -retained; to set that order explicitly, call \code{factor(levels = ...)} on the -grouping variable beforehand. See Examples, as well as the "Stacked area -plots" section below.} +Defaults to \code{NULL}, i.e. keep the existing factor levels. To set that order +explicitly, call \code{factor(levels = ...)} on the grouping variable +beforehand. See Examples, as well as the "Stacked area plots" section +below.} \item{FUN}{a function for collapsing repeated \code{y} values within a group and \code{x} position, used only when \code{stack = TRUE}. Defaults to \code{mean}, matching @@ -98,10 +101,11 @@ separately within each facet. The \code{byord} argument is a helpful companion to stacked area plots, since it enables on-the-fly adjustment of the stacking order. For example, -three positional keywords---\code{"start"}, \code{"end"}, and \code{"total"}---rank the -stacked \code{by} groups according to their \code{y} values at the designated position -along the \code{x} axis. Following convention, the ranking runs in descending -order, so that the biggest group is drawn on the bottom layer. However, size +the size keywords---\code{"desc"}, \code{"start"}, and \code{"end"}---rank the +stacked \code{by} groups according to their \code{y} values, either summed across the +\code{x} axis or taken at one end of it. Following convention, the ranking runs in +descending order, so that the biggest group is drawn on the bottom layer +(use \code{"asc"} for the reverse). However, size is not the only route to a stable baseline. Because each band is drawn on top of the ones below it, they all inherit whatever movement the bottom layer has. A large but volatile group can therefore be a worse choice @@ -184,7 +188,7 @@ tinyplot( # `"minvar"` instead puts the *least variable* group on the baseline. Every # band inherits the movement of the ones below it, so a steady bottom layer # keeps the whole chart legible. Here that picks group B, which the default -# level order leaves in the middle and `"end"`/`"total"` push to the top. +# level order leaves in the middle and `"end"`/`"desc"` push to the top. tinyplot( val ~ year | grp, data = dat, diff --git a/man/type_ridge.Rd b/man/type_ridge.Rd index a6207864..0ca4dd4a 100644 --- a/man/type_ridge.Rd +++ b/man/type_ridge.Rd @@ -10,6 +10,7 @@ type_ridge( breaks = NULL, probs = NULL, ylevels = NULL, + yord = NULL, bw = "nrd0", joint.bw = c("mean", "full", "none"), adjust = 1, @@ -47,9 +48,33 @@ at the specified \code{probs}. The quantiles are computed based on the density (rather than the raw original variable). Only one of \code{breaks} or \code{probs} must be specified.} -\item{ylevels}{a character or numeric vector specifying in which order -the levels of the y-variable should be plotted. The special keyword -\code{"asis"} takes the categories in the order that they appear in the data.} +\item{ylevels, yord}{arguments controlling the order of the \code{y} variable, and +hence of the y-axis. Supply one or the other; if both arguments are +provided, \code{ylevels} takes precedence and \code{yord} is silently ignored. +\itemize{ +\item \code{ylevels} specifies the levels \emph{literally}, either a character vector of +level names in the desired order (e.g., \code{c("C", "B", "A")}), or a numeric +vector of the corresponding level indexes (e.g. \code{3:1}). +\item \code{yord} instead accepts a keyword or custom function, which then \emph{derives} +the order from the data. Options are: +\itemize{ +\item \code{"desc"} and \code{"asc"} rank the ridges by their mean \code{x} value, largest +or smallest first. (Long forms like \code{"descending"} and \code{"increasing"} are also accepted.) (Ridge plots have no +separate response, so the ranking runs on the continuous \code{x} variable.) +\item \code{"minvar"} ranks them by the spread of each distribution, narrowest +first. +\item \code{"asis"} or \code{"rev"} permute the existing levels without consulting the +data at all. The former takes the categories in the order that they +appear in the data, while the latter reverses the current level order. +\item a custom function that determines both the ranking statistic and its +direction. The statistic is always sorted ascending, so +\code{function(y) -median(y)} ranks by median, largest first. +} +} + +Note that a numeric \code{y} is coerced to a factor before the ridges are +drawn, so it is reordered like any other categorical variable. +Each argument defaults to \code{NULL}, i.e. keep the existing factor levels.} \item{bw}{the smoothing \code{\link[stats:bw.nrd]{bandwidth}} to be used, see \code{\link[stats]{density}} for details and options.} diff --git a/man/type_spineplot.Rd b/man/type_spineplot.Rd index 9540520d..7fb8c48b 100644 --- a/man/type_spineplot.Rd +++ b/man/type_spineplot.Rd @@ -9,12 +9,14 @@ type_spineplot( tol.ylab = 0.05, off = NULL, xlevels = NULL, + xord = NULL, ylevels = NULL, + yord = NULL, col = NULL, - xaxlabels = NULL, - yaxlabels = NULL, weights = NULL, - lighten = FALSE + lighten = FALSE, + xaxlabels = NULL, + yaxlabels = NULL ) } \arguments{ @@ -29,20 +31,40 @@ type_spineplot( \item{off}{vertical offset between the bars (in per cent). It is fixed to \code{0} for spinograms and defaults to \code{2} for spine plots.} -\item{xlevels, ylevels}{a character or numeric vector specifying the ordering of the -levels of the \code{x} and \code{y} variables (if character) or the corresponding indexes -(if numeric) for the plot. The special keyword \code{"asis"} takes the -categories in the order that they appear in the data. Note that these -arguments only affect categorical (i.e., factor or character) variables.} +\item{xlevels, xord}{arguments controlling the order of the \code{x} variable, and +hence of the x-axis. Supply one or the other; if both arguments are +provided, \code{xlevels} takes precedence and \code{xord} is silently ignored. +\itemize{ +\item \code{xlevels} specifies the levels \emph{literally}, either a character vector of +level names in the desired order (e.g., \code{c("C", "B", "A")}), or a numeric +vector of the corresponding level indexes (e.g. \code{3:1}). +\item \code{xord} instead accepts a keyword or custom function, which then \emph{derives} +the order from the data. Options are: +\itemize{ +\item \code{"desc"} and \code{"asc"} rank the categories by (weighted) frequency, +i.e. most or least common first. (Long forms like \code{"descending"} and \code{"increasing"} are also accepted.) +\item \code{"asis"} or \code{"rev"} permute the existing levels without consulting the +data at all. The former takes the categories in the order that they +appear in the data, while the latter reverses the current level order. +\item a custom function that determines both the ranking statistic and its +direction. The statistic is always sorted ascending, so +\code{function(y) -median(y)} ranks by median, largest first. +} +} + +Note that \code{x} is only reordered when it is categorical (i.e., factor or +character). Both arguments are thus ignored for spinograms, which have a +(binned) numeric \code{x} axis. Each argument defaults to \code{NULL}, i.e. keep the +existing factor levels.} + +\item{ylevels, yord}{as for \code{xlevels} / \code{xord} above, but for the \code{y} +variable. Note that \code{y} is always coerced to a factor for spineplots and +spinograms, so these arguments are always binding if provided. Be aware +that a numeric \code{y} gives one level per distinct value.} \item{col}{a vector of fill colors of the same length as \code{levels(y)}. The default is to call \code{\link{gray.colors}}.} -\item{xaxlabels, yaxlabels}{character vectors for annotation of x and y axis. - Default to \code{levels(y)} and \code{levels(x)}, respectively for the - spine plot. For \code{xaxlabels} in the spinogram, the breaks are - used.} - \item{weights}{numeric. A vector of frequency weights for each observation in the data. If \code{NULL} all weights are implicitly assumed to be 1. If \code{x} is already a 2-way table, the weights @@ -58,6 +80,11 @@ better against their matching border colours.) Set to \code{TRUE} to opt in to the lighter tint. Note that \code{lighten} has no effect on other spineplot displays (single-group or \code{x == by}), which always use a sequential shading ramp of the base colour.} + +\item{xaxlabels, yaxlabels}{[Deprecated] character vectors for annotation of +the x and y axis. Use the top-level \code{xaxl} / \code{yaxl} arguments instead, +which apply consistently across \code{\link{tinyplot}} types. These two type-specific +arguments will be removed in a future release.} } \description{ Type function(s) for producing spineplots and spinograms, which diff --git a/man/type_text.Rd b/man/type_text.Rd index ccb105d7..0cfc1505 100644 --- a/man/type_text.Rd +++ b/man/type_text.Rd @@ -27,11 +27,11 @@ a top-level \code{\link{tinyplot}} argument, which additionally supports non-sta evaluation against \code{data} and takes precedence if both are given. See Examples.} -\item{labeller}{A formatting function (or convenience string) passed to -\code{\link{tinylabel}} for formatting the \code{labels}. Useful for ensuring that the -text labels match the formatting of an axis, e.g. \code{labeller = "\%"} to -display the labels as percentages. Default is \code{NULL}, i.e. no formatting. -See Examples.} +\item{labeller}{A formatting function, convenience string, or dictionary +(named vector or list) passed to \code{\link{tinylabel}} for formatting or +relabelling the \code{labels}. Useful for ensuring that the text labels match +the formatting of an axis, e.g. \code{labeller = "\%"} to display the labels as +percentages. Default is \code{NULL}, i.e. no formatting. See Examples.} \item{adj}{one or two values in \eqn{[0, 1]} which specify the x (and optionally y) adjustment (\sQuote{justification}) of the