From d494c47ccfd2fd025e9375536658f0541c776ee4 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Wed, 26 Aug 2026 16:50:43 -0700 Subject: [PATCH 01/23] xyord --- R/ordering_args.R | 38 +++++++++++++++++++ R/sanitize_ord.R | 87 +++++++++++++++++++++++++++++++++++++++---- R/sanitize_xlevels.R | 40 ++++++++++++++------ R/tinyplot.R | 6 +++ R/type_barplot.R | 80 ++++++++++++++++++++++++++++++++++----- R/type_errorbar.R | 46 ++++++++++++++++------- R/type_lines.R | 18 +++++++-- R/type_pointrange.R | 17 +++++++-- R/type_points.R | 51 +++++++++++++++++++------ R/type_ridge.R | 44 ++++++++++++++++++++-- R/type_spineplot.R | 53 ++++++++++++++++++++++---- man/type_barplot.Rd | 53 ++++++++++++++++++++++---- man/type_errorbar.Rd | 52 +++++++++++++++++++------- man/type_lines.Rd | 46 +++++++++++++++++------ man/type_points.Rd | 44 +++++++++++++++++----- man/type_ridge.Rd | 32 ++++++++++++++-- man/type_spineplot.Rd | 33 +++++++++++++--- 17 files changed, 618 insertions(+), 122 deletions(-) create mode 100644 R/ordering_args.R 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..cb6ded53 100644 --- a/R/sanitize_ord.R +++ b/R/sanitize_ord.R @@ -24,6 +24,11 @@ ## 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 @@ -56,17 +61,45 @@ ord_keywords = c("asis", "rev", "start", "end", "total", "minvar") -sanitize_ord = function(v, y, x, ord, arg = "ord") { - if (is.null(ord) || !is.factor(v)) { +## 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 "total" 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")) + +## "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") + +sanitize_ord = function(v, y, x, ord, arg = "ord", keywords = ord_keywords) { + # nlevels < 2 has exactly one ordering, so skip the work (and the degeneracy + # check below, which a single level would otherwise trip). + if (is.null(ord) || !is.factor(v) || nlevels(v) < 2L) { return(v) } - keyword = is.character(ord) && length(ord) == 1L && ord %in% ord_keywords + 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 ) @@ -82,6 +115,20 @@ 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)) { + stop( + sprintf( + "`%s = \"%s\"` ranks on a numeric variable, but was given %s.\n Only \"asis\" and \"rev\" work without one.", + arg, ord, 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 @@ -89,9 +136,26 @@ sanitize_ord = function(v, y, x, ord, arg = "ord") { # 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_) + # 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(stat)) < 2L) { + stop( + sprintf( + "`%s = \"minvar\"` cannot order these groups: %s.", + arg, + if (all(is.na(stat))) { + "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)) + 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 @@ -99,11 +163,20 @@ sanitize_ord = function(v, y, x, ord, arg = "ord") { 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 } 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( seq_along(grps), diff --git a/R/sanitize_xlevels.R b/R/sanitize_xlevels.R index 0eb27f4a..bb96125b 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,35 @@ 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) 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 + )) + } + # 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. + 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) + } + 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/tinyplot.R b/R/tinyplot.R index 2beefe67..881b4d30 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -1284,6 +1284,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/type_barplot.R b/R/type_barplot.R index e4589734..4b83a2e3 100644 --- a/R/type_barplot.R +++ b/R/type_barplot.R @@ -19,12 +19,32 @@ #' 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 xlevels,xord two ways to control the order of the `x` variable, and +#' hence of the axis. Supply one or the other; if both are given, `xlevels` +#' takes precedence and `xord` is ignored. Note that a numeric `x` is coerced +#' to a factor before the bars are drawn, so it is reordered like any other +#' categorical variable. +#' +#' `xlevels` names the levels literally: a character vector of level names in +#' the desired order, or a numeric vector of the corresponding level indexes +#' (e.g. `3:1`). +#' +#' `xord` instead derives the order from the data, via a keyword or a +#' function. Options are: +#' +#' - `"total"` ranks the categories by value, largest first. In practice this +#' is the keyword most reach for, since it sorts the bars by height. Note that +#' it ranks the *aggregated* bars, i.e. whatever `FUN` produced, rather than +#' the underlying rows. +#' - `"asis"` and `"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 `"rev"` 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) sum(y)` +#' reverses `"total"`, and `function(y) -median(y)` ranks by median rather +#' than by sum. +#' +#' Both default to `NULL`, i.e. keep the existing factor levels. #' @param xaxlabels a character vector with the axis labels for the `x` variable, #' defaulting to the levels of `x`. #' @param offset optional specification for shifting bar baselines, accepting @@ -32,7 +52,8 @@ #' #' - *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 @@ -59,6 +80,22 @@ #' tinyplot(~ cyl, data = mtcars, type = "barplot", xlevels = c("8", "6", "4")) #' tinyplot(~ cyl, data = mtcars, type = "barplot", xlevels = 3:1) #' +#' # Or let the data decide the order, rather than naming it. `xord = "total"` +#' # sorts the bars by height; the ordering is shared across groups and facets. +#' tinyplot(~ cyl, data = mtcars, type = "barplot", xord = "total") +#' tinyplot(~ cyl | vs, data = mtcars, type = "barplot", xord = "total") +#' +#' # The ranking statistic is always sorted ascending, so passing a function is +#' # how you get the reverse: `sum` undoes what `"total"` does. +#' tinyplot(~ cyl, data = mtcars, type = "barplot", xord = function(y) sum(y)) +#' +#' # The two arguments compose, `xlevels` first: here we fix an explicit order +#' # and then flip it. +#' tinyplot( +#' ~ cyl, data = mtcars, type = "barplot", +#' xlevels = c("8", "6", "4"), xord = "rev" +#' ) +#' #' # Note: Above we used automatic argument passing for `beside`. 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 @@ -140,9 +177,9 @@ #' tinyplot_add(type = "vline") #' #' @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, xaxlabels = NULL, drop.zeros = FALSE, lighten = TRUE) { 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 +188,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 +212,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..57db4274 100644 --- a/R/type_errorbar.R +++ b/R/type_errorbar.R @@ -4,17 +4,36 @@ #' #' @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 two ways to control the order of the `x` variable, and +#' hence of the axis. Supply one or the other; if both are given, `xlevels` +#' takes precedence and `xord` is ignored. Both only affect categorical (i.e., +#' factor or character) `x` variables; a numeric `x` is plotted at its own +#' values and cannot be reordered, so supplying either there is ignored with a +#' warning. +#' +#' `xlevels` names the levels literally: a character vector of level names in +#' the desired order, or a numeric vector of the corresponding level indexes +#' (e.g. `3:1`). Default is `NULL`. +#' +#' `xord` instead derives the order from the data, via a keyword or a +#' function. Options are: +#' +#' - `"total"` ranks the categories by their `y` values, largest first. +#' - `"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"` and `"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 `"rev"` 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) sum(y)` +#' reverses `"total"`, and `function(y) -median(y)` ranks by median rather +#' than by sum. +#' +#' 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 +116,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..af80a3e3 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,16 @@ 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 + ) + } 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..8d6c6887 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,25 @@ 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 + ) + } 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..61173d37 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 two ways to control the order of the `x` variable, and +#' hence of the axis. Supply one or the other; if both are given, `xlevels` +#' takes precedence and `xord` is ignored. Both only affect categorical (i.e., +#' factor or character) `x` variables; a numeric `x` is plotted at its own +#' values and cannot be reordered, so supplying either there is ignored with a +#' warning. +#' +#' `xlevels` names the levels literally: a character vector of level names in the +#' desired order, or a numeric vector of the corresponding level indexes +#' (e.g. `3:1`). +#' +#' `xord` instead derives the order from the data, via a keyword or a function. +#' Options are: +#' +#' - `"total"` ranks the categories by the `y` values observed at each one, +#' largest first. +#' - `"minvar"` ranks them by the variance of those values, lowest first. +#' - `"asis"` and `"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 `"rev"` 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) sum(y)` +#' reverses `"total"`, and `function(y) -median(y)` ranks by median rather +#' than by sum. +#' +#' Both default 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,16 @@ 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 + ) + } if (is.factor(datapoints$x)) { xlvls = levels(datapoints$x) xlabs = seq_along(xlvls) diff --git a/R/type_ridge.R b/R/type_ridge.R index 915d12ae..276149b9 100644 --- a/R/type_ridge.R +++ b/R/type_ridge.R @@ -27,9 +27,33 @@ #' 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 two ways to control the order of the `y` variable, and +#' hence of the axis. Supply one or the other; if both are given, `ylevels` +#' takes precedence and `yord` is ignored. Note that a numeric `y` is coerced +#' to a factor before the ridges are drawn, so it is reordered like any other +#' categorical variable. +#' +#' `ylevels` names the levels literally: a character vector of level names in +#' the desired order, or a numeric vector of the corresponding level indexes +#' (e.g. `3:1`). +#' +#' `yord` instead derives the order from the data, via a keyword or a +#' function. Options are: +#' +#' - `"total"` ranks the ridges by summed `x`, largest first. (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"` and `"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 `"rev"` 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) sum(y)` +#' reverses `"total"`, and `function(y) -median(y)` ranks by median rather +#' than by sum. +#' +#' Both default 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 +239,7 @@ type_ridge = function( breaks = NULL, probs = NULL, ylevels = NULL, + yord = NULL, bw = "nrd0", joint.bw = c("mean", "full", "none"), adjust = 1, @@ -245,6 +270,7 @@ type_ridge = function( breaks = breaks, probs = probs, ylevels = ylevels, + yord = yord, raster = raster, col = col, alpha = alpha, @@ -265,7 +291,7 @@ 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, @@ -303,6 +329,16 @@ 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 "total" + ## orders by summed 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 + ) + if (y_by) datapoints$by = datapoints$y + } ## datapoints = split(datapoints, list(datapoints$y, datapoints$by, datapoints$facet)) diff --git a/R/type_spineplot.R b/R/type_spineplot.R index 2d5500f8..f8f02fbd 100644 --- a/R/type_spineplot.R +++ b/R/type_spineplot.R @@ -4,11 +4,30 @@ #' 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 two ways to control the order of the `x` variable, and +#' hence of the axis. Supply one or the other; if both are given, `xlevels` +#' takes precedence and `xord` is ignored. Both only affect categorical (i.e., +#' factor or character) variables. +#' +#' `xlevels` names the levels literally: a character vector of level names in +#' the desired order, or a numeric vector of the corresponding level indexes +#' (e.g. `3:1`). +#' +#' `xord` instead derives the order from the data, via a keyword or a +#' function. Options are: +#' +#' - `"total"` ranks the categories by frequency, most common first. (Both +#' axes of a spineplot are categorical, so there is no response to rank on and +#' observations are counted instead, weighted if `weights` is given.) +#' - `"asis"` and `"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 `"rev"` 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) sum(y)` +#' reverses `"total"`. +#' +#' Both default to `NULL`, i.e. keep the existing factor levels. +#' @param ylevels,yord as for `xlevels` / `xord` above, but for the `y` variable. #' @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 @@ -94,10 +113,10 @@ #' ) #' #' @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, xaxlabels = NULL, yaxlabels = NULL, weights = NULL, lighten = FALSE) { col = col 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,7 +125,7 @@ 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")) settings[["lighten"]] = lighten @@ -166,6 +185,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. "total" orders 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 diff --git a/man/type_barplot.Rd b/man/type_barplot.Rd index 5e3b72ee..8498d62c 100644 --- a/man/type_barplot.Rd +++ b/man/type_barplot.Rd @@ -11,6 +11,7 @@ type_barplot( offset = NULL, FUN = NULL, xlevels = NULL, + xord = NULL, xaxlabels = NULL, drop.zeros = FALSE, lighten = TRUE @@ -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,12 +52,33 @@ 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}{two ways to control the order of the \code{x} variable, and +hence of the axis. Supply one or the other; if both are given, \code{xlevels} +takes precedence and \code{xord} is ignored. 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. + +\code{xlevels} names the levels literally: a character vector of level names in +the desired order, or a numeric vector of the corresponding level indexes +(e.g. \code{3:1}). + +\code{xord} instead derives the order from the data, via a keyword or a +function. Options are: +\itemize{ +\item \code{"total"} ranks the categories by value, largest first. In practice this +is the keyword most reach for, since it sorts the bars by height. Note that +it ranks the \emph{aggregated} bars, i.e. whatever \code{FUN} produced, rather than +the underlying rows. +\item \code{"asis"} and \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 \code{"rev"} 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) sum(y)} +reverses \code{"total"}, and \code{function(y) -median(y)} ranks by median rather +than by sum. +} + +Both default to \code{NULL}, i.e. keep the existing factor levels.} \item{xaxlabels}{a character vector with the axis labels for the \code{x} variable, defaulting to the levels of \code{x}.} @@ -86,6 +109,22 @@ tinyplot(~ cyl | vs, data = mtcars, type = "barplot", beside = TRUE) tinyplot(~ cyl, data = mtcars, type = "barplot", xlevels = c("8", "6", "4")) tinyplot(~ cyl, data = mtcars, type = "barplot", xlevels = 3:1) +# Or let the data decide the order, rather than naming it. `xord = "total"` +# sorts the bars by height; the ordering is shared across groups and facets. +tinyplot(~ cyl, data = mtcars, type = "barplot", xord = "total") +tinyplot(~ cyl | vs, data = mtcars, type = "barplot", xord = "total") + +# The ranking statistic is always sorted ascending, so passing a function is +# how you get the reverse: `sum` undoes what `"total"` does. +tinyplot(~ cyl, data = mtcars, type = "barplot", xord = function(y) sum(y)) + +# The two arguments compose, `xlevels` first: here we fix an explicit order +# and then flip it. +tinyplot( + ~ cyl, data = mtcars, type = "barplot", + xlevels = c("8", "6", "4"), xord = "rev" +) + # Note: Above we used automatic argument passing for `beside`. 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 diff --git a/man/type_errorbar.Rd b/man/type_errorbar.Rd index 3d59cc6b..05cfcd38 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,37 @@ 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}{two ways to control the order of the \code{x} variable, and +hence of the axis. Supply one or the other; if both are given, \code{xlevels} +takes precedence and \code{xord} is ignored. Both only affect categorical (i.e., +factor or character) \code{x} variables; a numeric \code{x} is plotted at its own +values and cannot be reordered, so supplying either there is ignored with a +warning. + +\code{xlevels} names the levels literally: a character vector of level names in +the desired order, or a numeric vector of the corresponding level indexes +(e.g. \code{3:1}). Default is \code{NULL}. + +\code{xord} instead derives the order from the data, via a keyword or a +function. Options are: +\itemize{ +\item \code{"total"} ranks the categories by their \code{y} values, largest first. +\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"} and \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 \code{"rev"} 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) sum(y)} +reverses \code{"total"}, and \code{function(y) -median(y)} ranks by median rather +than by sum. +} + +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..b21be42a 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}{two ways to control the order of the \code{x} variable, and +hence of the axis. Supply one or the other; if both are given, \code{xlevels} +takes precedence and \code{xord} is ignored. Both only affect categorical (i.e., +factor or character) \code{x} variables; a numeric \code{x} is plotted at its own +values and cannot be reordered, so supplying either there is ignored with a +warning. + +\code{xlevels} names the levels literally: a character vector of level names in the +desired order, or a numeric vector of the corresponding level indexes +(e.g. \code{3:1}). + +\code{xord} instead derives the order from the data, via a keyword or a function. +Options are: +\itemize{ +\item \code{"total"} ranks the categories by the \code{y} values observed at each one, +largest first. +\item \code{"minvar"} ranks them by the variance of those values, lowest first. +\item \code{"asis"} and \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 \code{"rev"} 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) sum(y)} +reverses \code{"total"}, and \code{function(y) -median(y)} ranks by median rather +than by sum. +} + +Both default 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..4b907dc1 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}{two ways to control the order of the \code{x} variable, and +hence of the axis. Supply one or the other; if both are given, \code{xlevels} +takes precedence and \code{xord} is ignored. Both only affect categorical (i.e., +factor or character) \code{x} variables; a numeric \code{x} is plotted at its own +values and cannot be reordered, so supplying either there is ignored with a +warning. + +\code{xlevels} names the levels literally: a character vector of level names in the +desired order, or a numeric vector of the corresponding level indexes +(e.g. \code{3:1}). + +\code{xord} instead derives the order from the data, via a keyword or a function. +Options are: +\itemize{ +\item \code{"total"} ranks the categories by the \code{y} values observed at each one, +largest first. +\item \code{"minvar"} ranks them by the variance of those values, lowest first. +\item \code{"asis"} and \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 \code{"rev"} 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) sum(y)} +reverses \code{"total"}, and \code{function(y) -median(y)} ranks by median rather +than by sum. +} + +Both default 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_ridge.Rd b/man/type_ridge.Rd index a6207864..7dd471fe 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,34 @@ 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}{two ways to control the order of the \code{y} variable, and +hence of the axis. Supply one or the other; if both are given, \code{ylevels} +takes precedence and \code{yord} is ignored. 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. + +\code{ylevels} names the levels literally: a character vector of level names in +the desired order, or a numeric vector of the corresponding level indexes +(e.g. \code{3:1}). + +\code{yord} instead derives the order from the data, via a keyword or a +function. Options are: +\itemize{ +\item \code{"total"} ranks the ridges by summed \code{x}, largest first. (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"} and \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 \code{"rev"} 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) sum(y)} +reverses \code{"total"}, and \code{function(y) -median(y)} ranks by median rather +than by sum. +} + +Both default 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..9d6d86dc 100644 --- a/man/type_spineplot.Rd +++ b/man/type_spineplot.Rd @@ -9,7 +9,9 @@ type_spineplot( tol.ylab = 0.05, off = NULL, xlevels = NULL, + xord = NULL, ylevels = NULL, + yord = NULL, col = NULL, xaxlabels = NULL, yaxlabels = NULL, @@ -29,11 +31,32 @@ 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}{two ways to control the order of the \code{x} variable, and +hence of the axis. Supply one or the other; if both are given, \code{xlevels} +takes precedence and \code{xord} is ignored. Both only affect categorical (i.e., +factor or character) variables. + +\code{xlevels} names the levels literally: a character vector of level names in +the desired order, or a numeric vector of the corresponding level indexes +(e.g. \code{3:1}). + +\code{xord} instead derives the order from the data, via a keyword or a +function. Options are: +\itemize{ +\item \code{"total"} ranks the categories by frequency, most common first. (Both +axes of a spineplot are categorical, so there is no response to rank on and +observations are counted instead, weighted if \code{weights} is given.) +\item \code{"asis"} and \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 \code{"rev"} 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) sum(y)} +reverses \code{"total"}. +} + +Both default 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.} \item{col}{a vector of fill colors of the same length as \code{levels(y)}. The default is to call \code{\link{gray.colors}}.} From 745386b72ddfb50bcf88b8987901a922ebf7450d Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Wed, 26 Aug 2026 20:33:24 -0700 Subject: [PATCH 02/23] tinylabel accepts dict of named vector or list --- R/legend.R | 10 ++++--- R/tinylabel.R | 75 +++++++++++++++++++++++++++++++++++++++------------ R/tinyplot.R | 16 +++++------ R/type_text.R | 10 +++---- 4 files changed, 76 insertions(+), 35 deletions(-) diff --git a/R/legend.R b/R/legend.R index 4922e78d..eebb0ede 100644 --- a/R/legend.R +++ b/R/legend.R @@ -542,7 +542,8 @@ 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 named vector 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 +771,8 @@ 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 named vector 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,7 +881,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`). +#' @param labeller Function, character keyword, or named vector 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]. diff --git a/R/tinylabel.R b/R/tinylabel.R index cd51e514..9e3afec3 100644 --- a/R/tinylabel.R +++ b/R/tinylabel.R @@ -4,12 +4,27 @@ #' 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 *named* character vector or list, acting as a dictionary, e.g. +#' `c(setosa = "SET")`. 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 +#' could not be told apart 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 +40,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 +112,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 +159,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 diff --git a/R/tinyplot.R b/R/tinyplot.R index 881b4d30..2e279ad1 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -257,16 +257,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 -#' 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. +#' @param xaxl,yaxl a function, character keyword, or named vector 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. #' @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. diff --git a/R/type_text.R b/R/type_text.R index 803bec38..1e990859 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 named vector +#' 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, From 0a297732eaf6ae0a9559e5f13ec30f2a2766e22f Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Wed, 26 Aug 2026 20:36:05 -0700 Subject: [PATCH 03/23] x/yaxlabels -> x/yaxl - deprecate remaining type-level x/yaxlabels arguments in favour of tope-level x-yaxl args --- R/type_barplot.R | 14 ++++++++++++-- R/type_ridge.R | 10 ++++++++-- R/type_spineplot.R | 29 ++++++++++++++++++++++++++++- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/R/type_barplot.R b/R/type_barplot.R index 4b83a2e3..709c2587 100644 --- a/R/type_barplot.R +++ b/R/type_barplot.R @@ -45,8 +45,10 @@ #' than by sum. #' #' Both default to `NULL`, i.e. keep the existing factor levels. -#' @param xaxlabels a character vector with the axis labels for the `x` variable, -#' defaulting to the levels of `x`. +#' @param xaxlabels \[Deprecated\] a character vector with the axis labels for +#' the `x` variable. Use the top-level `xaxl` argument instead, which now +#' accepts a named vector mapping old labels to new ones, and applies +#' consistently across plot types. #' @param offset optional specification for shifting bar baselines, accepting #' one of two distinct forms. See the Examples for illustrations of both. #' @@ -178,6 +180,14 @@ #' #' @export type_barplot = function(width = 5/6, beside = FALSE, center = FALSE, offset = NULL, FUN = NULL, xlevels = NULL, xord = NULL, xaxlabels = NULL, drop.zeros = FALSE, lighten = TRUE) { + 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, xord = xord, xaxlabels = xaxlabels, drop.zeros = drop.zeros, lighten = lighten), draw = draw_rect(), diff --git a/R/type_ridge.R b/R/type_ridge.R index 276149b9..2f328e2a 100644 --- a/R/type_ridge.R +++ b/R/type_ridge.R @@ -298,7 +298,7 @@ data_ridge = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, 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 @@ -496,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, @@ -616,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) @@ -623,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 f8f02fbd..75f10065 100644 --- a/R/type_spineplot.R +++ b/R/type_spineplot.R @@ -28,6 +28,10 @@ #' #' Both default to `NULL`, i.e. keep the existing factor levels. #' @param ylevels,yord as for `xlevels` / `xord` above, but for the `y` variable. +#' @param xaxlabels,yaxlabels \[Deprecated\] character vectors for annotation of +#' the x and y axis. Use the top-level `xaxl` / `yaxl` arguments instead, +#' which accept a named vector mapping old labels to new ones, and apply +#' consistently across plot types. #' @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 @@ -115,6 +119,20 @@ #' @export type_spineplot = function(breaks = NULL, tol.ylab = 0.05, off = NULL, xlevels = NULL, xord = NULL, ylevels = NULL, yord = NULL, col = NULL, xaxlabels = NULL, yaxlabels = NULL, weights = NULL, lighten = FALSE) { 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, 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), @@ -127,7 +145,7 @@ 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, 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 @@ -302,6 +320,15 @@ data_spineplot = function(off = NULL, breaks = NULL, xlevels = xlevels, xord = N 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 From b07386c056d9f2882e16578594eac05f58489fd1 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Wed, 26 Aug 2026 20:36:40 -0700 Subject: [PATCH 04/23] docs --- man/build_legend_args.Rd | 3 ++- man/build_legend_env.Rd | 3 ++- man/draw_legend.Rd | 4 +++- man/tinyAxis.Rd | 28 ++++++++++++++++++++++------ man/tinylabel.Rd | 32 ++++++++++++++++++++++++++------ man/tinyplot.Rd | 16 ++++++---------- man/type_barplot.Rd | 6 ++++-- man/type_text.Rd | 10 +++++----- 8 files changed, 70 insertions(+), 32 deletions(-) diff --git a/man/build_legend_args.Rd b/man/build_legend_args.Rd index 1e1ea297..4f093e7a 100644 --- a/man/build_legend_args.Rd +++ b/man/build_legend_args.Rd @@ -32,7 +32,8 @@ build_legend_args( \item{lgnd_labs}{The legend labels} -\item{labeller}{Character or function for formatting labels} +\item{labeller}{Function, character keyword, or named vector 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..69d9b0cf 100644 --- a/man/build_legend_env.Rd +++ b/man/build_legend_env.Rd @@ -36,7 +36,8 @@ build_legend_env( \item{lgnd_labs}{The legend labels} -\item{labeller}{Character or function for formatting labels} +\item{labeller}{Function, character keyword, or named vector 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..145b5ed9 100644 --- a/man/draw_legend.Rd +++ b/man/draw_legend.Rd @@ -39,7 +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}). +\item{labeller}{Function, character keyword, or named vector 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..f5182168 100644 --- a/man/tinyAxis.Rd +++ b/man/tinyAxis.Rd @@ -20,12 +20,28 @@ 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 \emph{named} character vector or list, acting as a dictionary, e.g. +\code{c(setosa = "SET")}. 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 +could not be told apart 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..2997d05c 100644 --- a/man/tinylabel.Rd +++ b/man/tinylabel.Rd @@ -9,12 +9,28 @@ 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 \emph{named} character vector or list, acting as a dictionary, e.g. +\code{c(setosa = "SET")}. 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 +could not be told apart 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 +55,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..ba643546 100644 --- a/man/tinyplot.Rd +++ b/man/tinyplot.Rd @@ -417,16 +417,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 -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.} +\item{xaxl, yaxl}{a function, character keyword, or named vector 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.} \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/type_barplot.Rd b/man/type_barplot.Rd index 8498d62c..cfeaa91d 100644 --- a/man/type_barplot.Rd +++ b/man/type_barplot.Rd @@ -80,8 +80,10 @@ than by sum. Both default to \code{NULL}, i.e. keep the existing factor levels.} -\item{xaxlabels}{a character vector with the axis labels for the \code{x} variable, -defaulting to the levels of \code{x}.} +\item{xaxlabels}{[Deprecated] a character vector with the axis labels for +the \code{x} variable. Use the top-level \code{xaxl} argument instead, which now +accepts a named vector mapping old labels to new ones, and applies +consistently across plot types.} \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 diff --git a/man/type_text.Rd b/man/type_text.Rd index ccb105d7..b8705c81 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 named vector +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 From d2aeb59795c827c1895874abd68baf3b986e3e14 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Thu, 27 Aug 2026 15:21:06 -0700 Subject: [PATCH 05/23] doc tweaks --- R/type_barplot.R | 55 +++++++++++++++++++------------------- R/type_errorbar.R | 51 ++++++++++++++++++----------------- R/type_points.R | 45 ++++++++++++++++--------------- R/type_ribbon.R | 33 ++++++++++++----------- R/type_ridge.R | 54 ++++++++++++++++++------------------- R/type_spineplot.R | 54 ++++++++++++++++++++----------------- man/type_barplot.Rd | 61 +++++++++++++++++++++--------------------- man/type_errorbar.Rd | 45 ++++++++++++++++--------------- man/type_lines.Rd | 41 ++++++++++++++-------------- man/type_points.Rd | 41 ++++++++++++++-------------- man/type_ribbon.Rd | 29 ++++++++++---------- man/type_ridge.Rd | 38 +++++++++++++------------- man/type_spineplot.Rd | 62 +++++++++++++++++++++++-------------------- 13 files changed, 313 insertions(+), 296 deletions(-) diff --git a/R/type_barplot.R b/R/type_barplot.R index 709c2587..8ce2c823 100644 --- a/R/type_barplot.R +++ b/R/type_barplot.R @@ -19,36 +19,32 @@ #' 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,xord two ways to control the order of the `x` variable, and -#' hence of the axis. Supply one or the other; if both are given, `xlevels` -#' takes precedence and `xord` is ignored. Note that a numeric `x` is coerced -#' to a factor before the bars are drawn, so it is reordered like any other -#' categorical variable. +#' @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` names the levels literally: a character vector of level names in -#' the desired order, or a numeric vector of the corresponding level indexes -#' (e.g. `3:1`). +#' - `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 derives the order from the data, via a keyword or a -#' function. Options are: +#' - `xord` instead accepts a keyword or custom function, which then _derives_ +#' the order from the data. Options are: #' -#' - `"total"` ranks the categories by value, largest first. In practice this -#' is the keyword most reach for, since it sorts the bars by height. Note that -#' it ranks the *aggregated* bars, i.e. whatever `FUN` produced, rather than -#' the underlying rows. -#' - `"asis"` and `"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 `"rev"` 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) sum(y)` -#' reverses `"total"`, and `function(y) -median(y)` ranks by median rather -#' than by sum. +#' - `"total"` ranks the categories by value, largest first. In practice +#' this is the keyword most reach for, since it sorts the bars by height. +#' Note that it ranks the *aggregated* bars, i.e. whatever `FUN` produced, +#' rather than the underlying rows. +#' - `"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) sum(y)` reverses `"total"`, and `function(y) -median(y)` +#' ranks by median rather than by sum. #' -#' Both default to `NULL`, i.e. keep the existing factor levels. -#' @param xaxlabels \[Deprecated\] a character vector with the axis labels for -#' the `x` variable. Use the top-level `xaxl` argument instead, which now -#' accepts a named vector mapping old labels to new ones, and applies -#' consistently across plot types. +#' 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. #' @@ -71,6 +67,11 @@ #' 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, which now +#' accepts a named vector mapping old labels to new ones, and applies +#' consistently across plot types. This argument will be removed in a future +#' release. #' #' @examples #' # Basic examples of frequency tables (without y variable) @@ -179,7 +180,7 @@ #' tinyplot_add(type = "vline") #' #' @export -type_barplot = function(width = 5/6, beside = FALSE, center = FALSE, offset = NULL, FUN = NULL, xlevels = NULL, xord = 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, ", diff --git a/R/type_errorbar.R b/R/type_errorbar.R index 57db4274..27d246a0 100644 --- a/R/type_errorbar.R +++ b/R/type_errorbar.R @@ -4,31 +4,32 @@ #' #' @inheritParams dodge_positions #' @inheritParams graphics::arrows -#' @param xlevels,xord two ways to control the order of the `x` variable, and -#' hence of the axis. Supply one or the other; if both are given, `xlevels` -#' takes precedence and `xord` is ignored. Both only affect categorical (i.e., -#' factor or character) `x` variables; a numeric `x` is plotted at its own -#' values and cannot be reordered, so supplying either there is ignored with a -#' warning. -#' -#' `xlevels` names the levels literally: a character vector of level names in -#' the desired order, or a numeric vector of the corresponding level indexes -#' (e.g. `3:1`). Default is `NULL`. -#' -#' `xord` instead derives the order from the data, via a keyword or a -#' function. Options are: -#' -#' - `"total"` ranks the categories by their `y` values, largest first. -#' - `"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"` and `"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 `"rev"` 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) sum(y)` -#' reverses `"total"`, and `function(y) -median(y)` ranks by median rather -#' than by sum. +#' @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: +#' +#' - `"total"` ranks the categories by their `y` values, largest first. +#' - `"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) sum(y)` reverses `"total"`, and `function(y) -median(y)` +#' ranks by median rather than by sum. +#' +#' 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 diff --git a/R/type_points.R b/R/type_points.R index 61173d37..e1cf7e93 100644 --- a/R/type_points.R +++ b/R/type_points.R @@ -3,32 +3,33 @@ #' @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,xord two ways to control the order of the `x` variable, and -#' hence of the axis. Supply one or the other; if both are given, `xlevels` -#' takes precedence and `xord` is ignored. Both only affect categorical (i.e., -#' factor or character) `x` variables; a numeric `x` is plotted at its own -#' values and cannot be reordered, so supplying either there is ignored with a -#' warning. +#' @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` names the levels literally: a character vector of level names in the -#' desired order, or a numeric vector of the corresponding level indexes -#' (e.g. `3:1`). +#' - `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 derives the order from the data, via a keyword or a function. -#' Options are: +#' - `xord` instead accepts a keyword or custom function, which then _derives_ +#' the order from the data. Options are: #' -#' - `"total"` ranks the categories by the `y` values observed at each one, -#' largest first. -#' - `"minvar"` ranks them by the variance of those values, lowest first. -#' - `"asis"` and `"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 `"rev"` 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) sum(y)` -#' reverses `"total"`, and `function(y) -median(y)` ranks by median rather -#' than by sum. +#' - `"total"` ranks the categories by the `y` values observed at each one, +#' largest first. +#' - `"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) sum(y)` reverses `"total"`, and `function(y) -median(y)` +#' ranks by median rather than by sum. #' -#' Both default to `NULL`, i.e. keep the existing factor levels. +#' 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 diff --git a/R/type_ribbon.R b/R/type_ribbon.R index 01765a9e..23b4fda7 100644 --- a/R/type_ribbon.R +++ b/R/type_ribbon.R @@ -11,25 +11,26 @@ #' @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 +#' +#' - `"start"`, `"end"` and `"total"` are positional keywords that rank the +#' groups by 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, 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 diff --git a/R/type_ridge.R b/R/type_ridge.R index 2f328e2a..37fcf6e9 100644 --- a/R/type_ridge.R +++ b/R/type_ridge.R @@ -27,33 +27,33 @@ #' 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,yord two ways to control the order of the `y` variable, and -#' hence of the axis. Supply one or the other; if both are given, `ylevels` -#' takes precedence and `yord` is ignored. Note that a numeric `y` is coerced -#' to a factor before the ridges are drawn, so it is reordered like any other -#' categorical variable. -#' -#' `ylevels` names the levels literally: a character vector of level names in -#' the desired order, or a numeric vector of the corresponding level indexes -#' (e.g. `3:1`). -#' -#' `yord` instead derives the order from the data, via a keyword or a -#' function. Options are: -#' -#' - `"total"` ranks the ridges by summed `x`, largest first. (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"` and `"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 `"rev"` 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) sum(y)` -#' reverses `"total"`, and `function(y) -median(y)` ranks by median rather -#' than by sum. -#' -#' Both default to `NULL`, i.e. keep the existing factor levels. +#' @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: +#' +#' - `"total"` ranks the ridges by summed `x`, largest first. (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) sum(y)` reverses `"total"`, and `function(y) -median(y)` +#' ranks by median rather than by sum. +#' +#' 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. diff --git a/R/type_spineplot.R b/R/type_spineplot.R index 75f10065..48256bfb 100644 --- a/R/type_spineplot.R +++ b/R/type_spineplot.R @@ -4,34 +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,xord two ways to control the order of the `x` variable, and -#' hence of the axis. Supply one or the other; if both are given, `xlevels` -#' takes precedence and `xord` is ignored. Both 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` names the levels literally: a character vector of level names in -#' the desired order, or a numeric vector of the corresponding level indexes -#' (e.g. `3:1`). +#' - `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 derives the order from the data, via a keyword or a -#' function. Options are: +#' - `xord` instead accepts a keyword or custom function, which then _derives_ +#' the order from the data. Options are: #' -#' - `"total"` ranks the categories by frequency, most common first. (Both -#' axes of a spineplot are categorical, so there is no response to rank on and -#' observations are counted instead, weighted if `weights` is given.) -#' - `"asis"` and `"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 `"rev"` 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) sum(y)` -#' reverses `"total"`. +#' - `"total"` ranks the categories by (weighted) frequency, i.e. most +#' common 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) sum(y)` reverses `"total"`. #' -#' Both default to `NULL`, i.e. keep the existing factor levels. -#' @param ylevels,yord as for `xlevels` / `xord` above, but for the `y` variable. -#' @param xaxlabels,yaxlabels \[Deprecated\] character vectors for annotation of -#' the x and y axis. Use the top-level `xaxl` / `yaxl` arguments instead, -#' which accept a named vector mapping old labels to new ones, and apply -#' consistently across plot types. +#' 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 @@ -43,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") @@ -117,7 +121,7 @@ #' ) #' #' @export -type_spineplot = function(breaks = NULL, tol.ylab = 0.05, off = NULL, xlevels = NULL, xord = NULL, ylevels = NULL, yord = 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)) { diff --git a/man/type_barplot.Rd b/man/type_barplot.Rd index cfeaa91d..dcd1af77 100644 --- a/man/type_barplot.Rd +++ b/man/type_barplot.Rd @@ -12,9 +12,9 @@ type_barplot( FUN = NULL, xlevels = NULL, xord = NULL, - xaxlabels = NULL, drop.zeros = FALSE, - lighten = TRUE + lighten = TRUE, + xaxlabels = NULL ) } \arguments{ @@ -52,38 +52,33 @@ 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, xord}{two ways to control the order of the \code{x} variable, and -hence of the axis. Supply one or the other; if both are given, \code{xlevels} -takes precedence and \code{xord} is ignored. 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. - -\code{xlevels} names the levels literally: a character vector of level names in -the desired order, or a numeric vector of the corresponding level indexes -(e.g. \code{3:1}). - -\code{xord} instead derives the order from the data, via a keyword or a -function. Options are: +\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{"total"} ranks the categories by value, largest first. In practice this -is the keyword most reach for, since it sorts the bars by height. Note that -it ranks the \emph{aggregated} bars, i.e. whatever \code{FUN} produced, rather than -the underlying rows. -\item \code{"asis"} and \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 \code{"rev"} reverses the current level order. +\item \code{"total"} ranks the categories by value, largest first. In practice +this is the keyword most reach for, since it sorts the bars by height. +Note that it ranks the \emph{aggregated} bars, i.e. whatever \code{FUN} produced, +rather than the underlying rows. +\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) sum(y)} -reverses \code{"total"}, and \code{function(y) -median(y)} ranks by median rather -than by sum. +direction. The statistic is always sorted in ascending order, so +\code{function(y) sum(y)} reverses \code{"total"}, and \code{function(y) -median(y)} +ranks by median rather than by sum. +} } -Both default to \code{NULL}, i.e. keep the existing factor levels.} - -\item{xaxlabels}{[Deprecated] a character vector with the axis labels for -the \code{x} variable. Use the top-level \code{xaxl} argument instead, which now -accepts a named vector mapping old labels to new ones, and applies -consistently across plot types.} +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 @@ -93,6 +88,12 @@ 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, which now +accepts a named vector mapping old labels to new ones, and applies +consistently across plot types. This argument will be removed in a future +release.} } \description{ Type function for producing barplots. For formulas of type diff --git a/man/type_errorbar.Rd b/man/type_errorbar.Rd index 05cfcd38..b35a6725 100644 --- a/man/type_errorbar.Rd +++ b/man/type_errorbar.Rd @@ -40,33 +40,34 @@ 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, xord}{two ways to control the order of the \code{x} variable, and -hence of the axis. Supply one or the other; if both are given, \code{xlevels} -takes precedence and \code{xord} is ignored. Both only affect categorical (i.e., -factor or character) \code{x} variables; a numeric \code{x} is plotted at its own -values and cannot be reordered, so supplying either there is ignored with a -warning. - -\code{xlevels} names the levels literally: a character vector of level names in -the desired order, or a numeric vector of the corresponding level indexes -(e.g. \code{3:1}). Default is \code{NULL}. - -\code{xord} instead derives the order from the data, via a keyword or a -function. Options are: +\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{"total"} ranks the categories by their \code{y} values, largest first. -\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"} and \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 \code{"rev"} reverses the current level order. +\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) sum(y)} -reverses \code{"total"}, and \code{function(y) -median(y)} ranks by median rather -than by sum. +direction. The statistic is always sorted ascending, so +\code{function(y) sum(y)} reverses \code{"total"}, and \code{function(y) -median(y)} +ranks by median rather than by sum. +} } +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 diff --git a/man/type_lines.Rd b/man/type_lines.Rd index b21be42a..c62addb9 100644 --- a/man/type_lines.Rd +++ b/man/type_lines.Rd @@ -46,33 +46,34 @@ 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, xord}{two ways to control the order of the \code{x} variable, and -hence of the axis. Supply one or the other; if both are given, \code{xlevels} -takes precedence and \code{xord} is ignored. Both only affect categorical (i.e., -factor or character) \code{x} variables; a numeric \code{x} is plotted at its own -values and cannot be reordered, so supplying either there is ignored with a -warning. - -\code{xlevels} names the levels literally: a character vector of level names in the -desired order, or a numeric vector of the corresponding level indexes -(e.g. \code{3:1}). - -\code{xord} instead derives the order from the data, via a keyword or a function. -Options are: +\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{"total"} ranks the categories by the \code{y} values observed at each one, largest first. \item \code{"minvar"} ranks them by the variance of those values, lowest first. -\item \code{"asis"} and \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 \code{"rev"} reverses the current level order. +\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) sum(y)} -reverses \code{"total"}, and \code{function(y) -median(y)} ranks by median rather -than by sum. +direction. The statistic is always sorted ascending, so +\code{function(y) sum(y)} reverses \code{"total"}, and \code{function(y) -median(y)} +ranks by median rather than by sum. +} } -Both default to \code{NULL}, i.e. keep the existing factor levels.} +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. diff --git a/man/type_points.Rd b/man/type_points.Rd index 4b907dc1..3d238069 100644 --- a/man/type_points.Rd +++ b/man/type_points.Rd @@ -38,33 +38,34 @@ 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, xord}{two ways to control the order of the \code{x} variable, and -hence of the axis. Supply one or the other; if both are given, \code{xlevels} -takes precedence and \code{xord} is ignored. Both only affect categorical (i.e., -factor or character) \code{x} variables; a numeric \code{x} is plotted at its own -values and cannot be reordered, so supplying either there is ignored with a -warning. - -\code{xlevels} names the levels literally: a character vector of level names in the -desired order, or a numeric vector of the corresponding level indexes -(e.g. \code{3:1}). - -\code{xord} instead derives the order from the data, via a keyword or a function. -Options are: +\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{"total"} ranks the categories by the \code{y} values observed at each one, largest first. \item \code{"minvar"} ranks them by the variance of those values, lowest first. -\item \code{"asis"} and \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 \code{"rev"} reverses the current level order. +\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) sum(y)} -reverses \code{"total"}, and \code{function(y) -median(y)} ranks by median rather -than by sum. +direction. The statistic is always sorted ascending, so +\code{function(y) sum(y)} reverses \code{"total"}, and \code{function(y) -median(y)} +ranks by median rather than by sum. +} } -Both default to \code{NULL}, i.e. keep the existing factor levels.} +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..590ec153 100644 --- a/man/type_ribbon.Rd +++ b/man/type_ribbon.Rd @@ -24,25 +24,26 @@ 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{"start"}, \code{"end"} and \code{"total"} are positional keywords that rank the +groups by 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, 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 diff --git a/man/type_ridge.Rd b/man/type_ridge.Rd index 7dd471fe..ce60fab1 100644 --- a/man/type_ridge.Rd +++ b/man/type_ridge.Rd @@ -48,34 +48,34 @@ 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, yord}{two ways to control the order of the \code{y} variable, and -hence of the axis. Supply one or the other; if both are given, \code{ylevels} -takes precedence and \code{yord} is ignored. 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. - -\code{ylevels} names the levels literally: a character vector of level names in -the desired order, or a numeric vector of the corresponding level indexes -(e.g. \code{3:1}). - -\code{yord} instead derives the order from the data, via a keyword or a -function. Options are: +\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{"total"} ranks the ridges by summed \code{x}, largest first. (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"} and \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 \code{"rev"} reverses the current level order. +\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) sum(y)} -reverses \code{"total"}, and \code{function(y) -median(y)} ranks by median rather -than by sum. +direction. The statistic is always sorted ascending, so +\code{function(y) sum(y)} reverses \code{"total"}, and \code{function(y) -median(y)} +ranks by median rather than by sum. +} } -Both default to \code{NULL}, i.e. keep the existing factor levels.} +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 9d6d86dc..d30fa45a 100644 --- a/man/type_spineplot.Rd +++ b/man/type_spineplot.Rd @@ -13,10 +13,10 @@ type_spineplot( ylevels = NULL, yord = NULL, col = NULL, - xaxlabels = NULL, - yaxlabels = NULL, weights = NULL, - lighten = FALSE + lighten = FALSE, + xaxlabels = NULL, + yaxlabels = NULL ) } \arguments{ @@ -31,41 +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, xord}{two ways to control the order of the \code{x} variable, and -hence of the axis. Supply one or the other; if both are given, \code{xlevels} -takes precedence and \code{xord} is ignored. Both only affect categorical (i.e., -factor or character) variables. - -\code{xlevels} names the levels literally: a character vector of level names in -the desired order, or a numeric vector of the corresponding level indexes -(e.g. \code{3:1}). - -\code{xord} instead derives the order from the data, via a keyword or a -function. Options are: +\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{"total"} ranks the categories by frequency, most common first. (Both -axes of a spineplot are categorical, so there is no response to rank on and -observations are counted instead, weighted if \code{weights} is given.) -\item \code{"asis"} and \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 \code{"rev"} reverses the current level order. +\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{"total"} ranks the categories by (weighted) frequency, i.e. most +common 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) sum(y)} -reverses \code{"total"}. +direction. The statistic is always sorted ascending, so +\code{function(y) sum(y)} reverses \code{"total"}. +} } -Both default to \code{NULL}, i.e. keep the existing factor levels.} +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.} +\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 @@ -81,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 From 7a7ad16964f282d0973e676420c5c4eee62a5b14 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Thu, 27 Aug 2026 18:40:08 -0700 Subject: [PATCH 06/23] xord: "total" -> "ascending" / "descending" - drop the ambiguous "total" xord keyword and rather use explicit directional ranking descriptions like "asc(ending)" and "desc(ending)". --- R/sanitize_ord.R | 76 ++++++++++++++++++++++++++----------- R/type_barplot.R | 87 ++++++++++++++++++++++++------------------- R/type_errorbar.R | 6 +-- R/type_lines.R | 3 +- R/type_pointrange.R | 3 +- R/type_points.R | 10 ++--- R/type_ribbon.R | 19 ++++++---- R/type_ridge.R | 16 ++++---- R/type_spineplot.R | 8 ++-- man/type_barplot.Rd | 87 ++++++++++++++++++++++++------------------- man/type_errorbar.Rd | 6 +-- man/type_lines.Rd | 7 ++-- man/type_points.Rd | 7 ++-- man/type_ribbon.Rd | 19 ++++++---- man/type_ridge.Rd | 9 ++--- man/type_spineplot.Rd | 6 +-- 16 files changed, 215 insertions(+), 154 deletions(-) diff --git a/R/sanitize_ord.R b/R/sanitize_ord.R index cb6ded53..179f982a 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 @@ -35,13 +36,14 @@ ## 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 @@ -59,7 +61,7 @@ ## 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: @@ -70,7 +72,7 @@ ord_keywords = c("asis", "rev", "start", "end", "total", "minvar") ## ## "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 "total" when there is +## 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")) @@ -82,12 +84,29 @@ ord_keywords_distribution = setdiff(ord_keywords, c("start", "end")) ## separate bars), the supplied weights for a spine. ord_keywords_scalar = setdiff(ord_keywords_distribution, "minvar") -sanitize_ord = function(v, y, x, ord, arg = "ord", keywords = ord_keywords) { +## 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")) { # nlevels < 2 has exactly one ordering, so skip the work (and the degeneracy # check below, which a single level would otherwise trip). if (is.null(ord) || !is.factor(v) || nlevels(v) < 2L) { return(v) } + stat = match.arg(stat) + + # 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)) { @@ -135,16 +154,16 @@ sanitize_ord = function(v, y, x, ord, arg = "ord", keywords = ord_keywords) { # 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(stat)) < 2L) { + if (length(unique(score)) < 2L) { stop( sprintf( "`%s = \"minvar\"` cannot order these groups: %s.", arg, - if (all(is.na(stat))) { + 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" @@ -154,14 +173,29 @@ sanitize_ord = function(v, y, x, ord, arg = "ord", keywords = ord_keywords) { ) } } else if (keyword) { - if (identical(ord, "total")) { + # `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 = if (is.null(x)) seq_along(y) else order(x) grps = split(y[xord], v[xord]) @@ -178,7 +212,7 @@ sanitize_ord = function(v, y, x, ord, arg = "ord", keywords = ord_keywords) { ) } xgrps = if (want_x) split(x[xord], v[xord]) else NULL - stat = vapply( + score = vapply( seq_along(grps), function(i) { z = grps[[i]] @@ -190,6 +224,6 @@ sanitize_ord = function(v, y, x, ord, arg = "ord", keywords = ord_keywords) { } # 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/type_barplot.R b/R/type_barplot.R index 8ce2c823..3bf21dd7 100644 --- a/R/type_barplot.R +++ b/R/type_barplot.R @@ -30,17 +30,23 @@ #' - `xord` instead accepts a keyword or custom function, which then _derives_ #' the order from the data. Options are: #' -#' - `"total"` ranks the categories by value, largest first. In practice -#' this is the keyword most reach for, since it sorts the bars by height. -#' Note that it ranks the *aggregated* bars, i.e. whatever `FUN` produced, -#' rather than the underlying rows. +#' - `"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) sum(y)` reverses `"total"`, and `function(y) -median(y)` -#' ranks by median rather than by sum. +#' `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. @@ -74,54 +80,58 @@ #' 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 #' -#' # 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) +#' sleep2 = transform(sleep, drug = group) # less misleading name (same people) #' -#' # Or let the data decide the order, rather than naming it. `xord = "total"` -#' # sorts the bars by height; the ordering is shared across groups and facets. -#' tinyplot(~ cyl, data = mtcars, type = "barplot", xord = "total") -#' tinyplot(~ cyl | vs, data = mtcars, type = "barplot", xord = "total") +#' tinyplot(extra ~ ID, data = sleep2, type = "barplot") +#' tinyplot(extra ~ ID, data = sleep2, type = "barplot", xord = "desc") +#' tinyplot(extra ~ ID | drug, data = sleep2, type = "barplot", beside = TRUE) #' -#' # The ranking statistic is always sorted ascending, so passing a function is -#' # how you get the reverse: `sum` undoes what `"total"` does. -#' tinyplot(~ cyl, data = mtcars, type = "barplot", xord = function(y) sum(y)) -#' -#' # The two arguments compose, `xlevels` first: here we fix an explicit order -#' # and then flip it. +#' # Change the aggregation (non-grouped case) from the `FUN = mean` default to +#' # ask a more interesting question: which subject benefitted most from the +#' # switch to drug 2? #' tinyplot( -#' ~ cyl, data = mtcars, type = "barplot", -#' xlevels = c("8", "6", "4"), xord = "rev" +#' extra ~ ID, data = sleep2, type = "barplot", +#' FUN = diff, xord = "desc", +#' main = "Sleep gain (drug 2 vs drug 1)" #' ) #' -#' # Note: Above we used automatic argument passing for `beside`. But this -#' # wouldn't work for `width`, since it would conflict with the top-level +#' # 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. #' 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 -#' tinyplot( -#' extra ~ ID | group, facet = "by", data = sleep, -#' type = "barplot", -#' theme = "clean2" -#' ) +#' # +#' ## matrix method (no formula required) +#' +#' tinyplot(VADeaths, type = "barplot") +#' tinyplot(VADeaths, type = "barplot", beside = TRUE) +#' # etc. see ?tinyplot.matrix #' -#' # Fancy frequency table: +#' # +#' ## Frequency tables +#' +#' # No y variable (frequency calculated on the fly) +#' tinyplot(~ cyl, data = mtcars, type = "barplot") +#' tinyplot(~ cyl | vs, data = mtcars, type = "barplot") +#' +#' +#' # 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. @@ -136,7 +146,8 @@ #' theme = list("clean2", palette.qualitative = hcols) #' ) #' -#' # Use cases for the `offset` argument +#' # +#' ## Offset #' #' # 1. Waterfall plot #' d = data.frame(item = c("Sales", "Services", "Costs", "Returns", "TOTAL"), diff --git a/R/type_errorbar.R b/R/type_errorbar.R index 27d246a0..c5e4ee95 100644 --- a/R/type_errorbar.R +++ b/R/type_errorbar.R @@ -15,7 +15,8 @@ #' - `xord` instead accepts a keyword or custom function, which then _derives_ #' the order from the data. Options are: #' -#' - `"total"` ranks the categories by their `y` values, largest first. +#' - `"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. @@ -24,8 +25,7 @@ #' 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) sum(y)` reverses `"total"`, and `function(y) -median(y)` -#' ranks by median rather than by sum. +#' `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 diff --git a/R/type_lines.R b/R/type_lines.R index af80a3e3..46a90f20 100644 --- a/R/type_lines.R +++ b/R/type_lines.R @@ -76,7 +76,8 @@ data_lines = function(dodge = 0, fixed.dodge = FALSE, xlevels = NULL, xord = NUL if (!is.null(xord) && is.null(xlevels)) { datapoints[["x"]] = sanitize_ord( datapoints[["x"]], datapoints[["y"]], NULL, - xord, arg = "xord", keywords = ord_keywords_distribution + xord, arg = "xord", keywords = ord_keywords_distribution, + stat = "mean" ) } if (is.factor(datapoints[["x"]])) { diff --git a/R/type_pointrange.R b/R/type_pointrange.R index 8d6c6887..2e64df95 100644 --- a/R/type_pointrange.R +++ b/R/type_pointrange.R @@ -64,7 +64,8 @@ data_pointrange = function(dodge, fixed.dodge, xlevels = NULL, xord = "asis", or if (!is.null(xord) && is.null(xlevels)) { datapoints$x = sanitize_ord( datapoints$x, datapoints[["y"]], NULL, - xord, arg = "xord", keywords = ord_keywords_distribution + xord, arg = "xord", keywords = ord_keywords_distribution, + stat = "mean" ) } if (is.factor(datapoints$x)) { diff --git a/R/type_points.R b/R/type_points.R index e1cf7e93..31c2fc55 100644 --- a/R/type_points.R +++ b/R/type_points.R @@ -15,16 +15,15 @@ #' - `xord` instead accepts a keyword or custom function, which then _derives_ #' the order from the data. Options are: #' -#' - `"total"` ranks the categories by the `y` values observed at each one, -#' largest first. +#' - `"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) sum(y)` reverses `"total"`, and `function(y) -median(y)` -#' ranks by median rather than by sum. +#' `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 @@ -85,7 +84,8 @@ data_points = function(clim = c(0.5, 2.5), dodge = 0, fixed.dodge = FALSE, xleve if (!is.null(xord) && is.null(xlevels)) { datapoints$x = sanitize_ord( datapoints$x, datapoints[["y"]], NULL, - xord, arg = "xord", keywords = ord_keywords_distribution + xord, arg = "xord", keywords = ord_keywords_distribution, + stat = "mean" ) } if (is.factor(datapoints$x)) { diff --git a/R/type_ribbon.R b/R/type_ribbon.R index 23b4fda7..78c7e206 100644 --- a/R/type_ribbon.R +++ b/R/type_ribbon.R @@ -12,9 +12,11 @@ #' the `by` group layers, thus controlling the order in which they stack. #' Options are: #' -#' - `"start"`, `"end"` and `"total"` are positional keywords that rank the -#' groups by their `y` values along the `x` axis. In each case, the group with -#' the largest value is stacked first, as the bottom layer. +#' - `"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"` or `"rev"` permute the existing levels without consulting the @@ -60,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 @@ -147,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 37fcf6e9..ef33b613 100644 --- a/R/type_ridge.R +++ b/R/type_ridge.R @@ -38,9 +38,9 @@ #' - `yord` instead accepts a keyword or custom function, which then _derives_ #' the order from the data. Options are: #' -#' - `"total"` ranks the ridges by summed `x`, largest first. (Ridge plots -#' have no separate response, so the ranking runs on the continuous `x` -#' variable.) +#' - `"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 @@ -48,8 +48,7 @@ #' 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) sum(y)` reverses `"total"`, and `function(y) -median(y)` -#' ranks by median rather than by sum. +#' `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. @@ -330,12 +329,13 @@ data_ridge = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, 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 "total" - ## orders by summed x, "minvar" by the spread of each distribution. + ## 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 + yord, arg = "yord", keywords = ord_keywords_distribution, + stat = "mean" ) if (y_by) datapoints$by = datapoints$y } diff --git a/R/type_spineplot.R b/R/type_spineplot.R index 48256bfb..6056bf25 100644 --- a/R/type_spineplot.R +++ b/R/type_spineplot.R @@ -15,14 +15,14 @@ #' - `xord` instead accepts a keyword or custom function, which then _derives_ #' the order from the data. Options are: #' -#' - `"total"` ranks the categories by (weighted) frequency, i.e. most -#' common first. +#' - `"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) sum(y)` reverses `"total"`. +#' `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 @@ -209,7 +209,7 @@ data_spineplot = function(off = NULL, breaks = NULL, xlevels = xlevels, xord = N } ## Both axes here are categorical, so there is no response to rank on: ## the size keywords count observations instead (weighted, if given), - ## i.e. "total" orders the categories by frequency. + ## 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( diff --git a/man/type_barplot.Rd b/man/type_barplot.Rd index dcd1af77..f470493a 100644 --- a/man/type_barplot.Rd +++ b/man/type_barplot.Rd @@ -62,17 +62,23 @@ 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{"total"} ranks the categories by value, largest first. In practice -this is the keyword most reach for, since it sorts the bars by height. -Note that it ranks the \emph{aggregated} bars, i.e. whatever \code{FUN} produced, -rather than the underlying rows. +\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) sum(y)} reverses \code{"total"}, and \code{function(y) -median(y)} -ranks by median rather than by sum. +\code{function(y) -median(y)} ranks by median, largest first. } } @@ -103,55 +109,59 @@ 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 -# 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) +sleep2 = transform(sleep, drug = group) # less misleading name (same people) -# Or let the data decide the order, rather than naming it. `xord = "total"` -# sorts the bars by height; the ordering is shared across groups and facets. -tinyplot(~ cyl, data = mtcars, type = "barplot", xord = "total") -tinyplot(~ cyl | vs, data = mtcars, type = "barplot", xord = "total") +tinyplot(extra ~ ID, data = sleep2, type = "barplot") +tinyplot(extra ~ ID, data = sleep2, type = "barplot", xord = "desc") +tinyplot(extra ~ ID | drug, data = sleep2, type = "barplot", beside = TRUE) -# The ranking statistic is always sorted ascending, so passing a function is -# how you get the reverse: `sum` undoes what `"total"` does. -tinyplot(~ cyl, data = mtcars, type = "barplot", xord = function(y) sum(y)) - -# The two arguments compose, `xlevels` first: here we fix an explicit order -# and then flip it. +# Change the aggregation (non-grouped case) from the `FUN = mean` default to +# ask a more interesting question: which subject benefitted most from the +# switch to drug 2? tinyplot( - ~ cyl, data = mtcars, type = "barplot", - xlevels = c("8", "6", "4"), xord = "rev" + extra ~ ID, data = sleep2, type = "barplot", + FUN = diff, xord = "desc", + main = "Sleep gain (drug 2 vs drug 1)" ) -# Note: Above we used automatic argument passing for `beside`. But this -# wouldn't work for `width`, since it would conflict with the top-level +# 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. 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 -tinyplot( - extra ~ ID | group, facet = "by", data = sleep, - type = "barplot", - theme = "clean2" -) +# +## matrix method (no formula required) + +tinyplot(VADeaths, type = "barplot") +tinyplot(VADeaths, type = "barplot", beside = TRUE) +# etc. see ?tinyplot.matrix -# Fancy frequency table: +# +## Frequency tables + +# No y variable (frequency calculated on the fly) +tinyplot(~ cyl, data = mtcars, type = "barplot") +tinyplot(~ cyl | vs, data = mtcars, type = "barplot") + + +# 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. # Aside: use `lighten = FALSE` to avoid lightening the bar fill colors. @@ -165,7 +175,8 @@ tinyplot( theme = list("clean2", palette.qualitative = hcols) ) -# Use cases for the `offset` argument +# +## Offset # 1. Waterfall plot d = data.frame(item = c("Sales", "Services", "Costs", "Returns", "TOTAL"), diff --git a/man/type_errorbar.Rd b/man/type_errorbar.Rd index b35a6725..b1e4bfe9 100644 --- a/man/type_errorbar.Rd +++ b/man/type_errorbar.Rd @@ -50,7 +50,8 @@ 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{"total"} ranks the categories by their \code{y} values, largest first. +\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. @@ -59,8 +60,7 @@ 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) sum(y)} reverses \code{"total"}, and \code{function(y) -median(y)} -ranks by median rather than by sum. +\code{function(y) -median(y)} ranks by median, largest first. } } diff --git a/man/type_lines.Rd b/man/type_lines.Rd index c62addb9..f9be54a1 100644 --- a/man/type_lines.Rd +++ b/man/type_lines.Rd @@ -57,16 +57,15 @@ 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{"total"} ranks the categories by the \code{y} values observed at each one, -largest first. +\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) sum(y)} reverses \code{"total"}, and \code{function(y) -median(y)} -ranks by median rather than by sum. +\code{function(y) -median(y)} ranks by median, largest first. } } diff --git a/man/type_points.Rd b/man/type_points.Rd index 3d238069..4ee9342e 100644 --- a/man/type_points.Rd +++ b/man/type_points.Rd @@ -49,16 +49,15 @@ 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{"total"} ranks the categories by the \code{y} values observed at each one, -largest first. +\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) sum(y)} reverses \code{"total"}, and \code{function(y) -median(y)} -ranks by median rather than by sum. +\code{function(y) -median(y)} ranks by median, largest first. } } diff --git a/man/type_ribbon.Rd b/man/type_ribbon.Rd index 590ec153..282b063a 100644 --- a/man/type_ribbon.Rd +++ b/man/type_ribbon.Rd @@ -24,9 +24,11 @@ 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 the -groups by 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{"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"} or \code{"rev"} permute the existing levels without consulting the @@ -99,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 @@ -185,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 ce60fab1..0ca4dd4a 100644 --- a/man/type_ridge.Rd +++ b/man/type_ridge.Rd @@ -58,9 +58,9 @@ 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{"total"} ranks the ridges by summed \code{x}, largest first. (Ridge plots -have no separate response, so the ranking runs on the continuous \code{x} -variable.) +\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 @@ -68,8 +68,7 @@ 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) sum(y)} reverses \code{"total"}, and \code{function(y) -median(y)} -ranks by median rather than by sum. +\code{function(y) -median(y)} ranks by median, largest first. } } diff --git a/man/type_spineplot.Rd b/man/type_spineplot.Rd index d30fa45a..7fb8c48b 100644 --- a/man/type_spineplot.Rd +++ b/man/type_spineplot.Rd @@ -41,14 +41,14 @@ 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{"total"} ranks the categories by (weighted) frequency, i.e. most -common first. +\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) sum(y)} reverses \code{"total"}. +\code{function(y) -median(y)} ranks by median, largest first. } } From f7bdb022cb3c33ea89463de4aaeecfccf151d434 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Thu, 27 Aug 2026 21:15:29 -0700 Subject: [PATCH 07/23] more examples --- R/tinyplot.matrix.R | 11 +++++++++-- man/tinyplot.matrix.Rd | 11 +++++++++-- man/type_barplot.Rd | 3 +-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/R/tinyplot.matrix.R b/R/tinyplot.matrix.R index 03260bcd..149451ba 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", beside = TRUE, 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/man/tinyplot.matrix.Rd b/man/tinyplot.matrix.Rd index e39f60a4..da266206 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", beside = TRUE, facet = "by", legend = FALSE) + } \seealso{ \code{\link[graphics]{matplot}} diff --git a/man/type_barplot.Rd b/man/type_barplot.Rd index f470493a..087fe612 100644 --- a/man/type_barplot.Rd +++ b/man/type_barplot.Rd @@ -150,7 +150,6 @@ tinyplot(VADeaths, type = "barplot", beside = TRUE) tinyplot(~ cyl, data = mtcars, type = "barplot") tinyplot(~ cyl | vs, data = mtcars, type = "barplot") - # Fancy frequency table (y = frequency aleady computed) tinyplot( Freq ~ Sex | Survived, data = as.data.frame(Titanic), @@ -176,7 +175,7 @@ tinyplot( ) # -## Offset +## Offset examples # 1. Waterfall plot d = data.frame(item = c("Sales", "Services", "Costs", "Returns", "TOTAL"), From 60305dbe89a2d7050a181275cbac513b4690bcc9 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Thu, 27 Aug 2026 21:20:27 -0700 Subject: [PATCH 08/23] again --- R/tinyplot.matrix.R | 2 +- R/type_barplot.R | 3 +-- man/tinyplot.matrix.Rd | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/R/tinyplot.matrix.R b/R/tinyplot.matrix.R index 149451ba..f031ce2c 100644 --- a/R/tinyplot.matrix.R +++ b/R/tinyplot.matrix.R @@ -63,7 +63,7 @@ #' # barplot(s) #' tinyplot(VADeaths, type = "barplot", beside = TRUE) #' tinyplot(t(VADeaths), type = "barplot", beside = TRUE) -#' tinyplot(VADeaths, type = "barplot", beside = TRUE, facet = "by", legend = FALSE) +#' tinyplot(VADeaths, type = "barplot", facet = "by", legend = FALSE) #' #' @export tinyplot.matrix = function(x, type = NULL, legend = NULL, facet = NULL, xlab = NULL, ylab = NULL, ...) { diff --git a/R/type_barplot.R b/R/type_barplot.R index 3bf21dd7..ecf6192b 100644 --- a/R/type_barplot.R +++ b/R/type_barplot.R @@ -121,7 +121,6 @@ #' tinyplot(~ cyl, data = mtcars, type = "barplot") #' tinyplot(~ cyl | vs, data = mtcars, type = "barplot") #' -#' #' # Fancy frequency table (y = frequency aleady computed) #' tinyplot( #' Freq ~ Sex | Survived, data = as.data.frame(Titanic), @@ -147,7 +146,7 @@ #' ) #' #' # -#' ## Offset +#' ## Offset examples #' #' # 1. Waterfall plot #' d = data.frame(item = c("Sales", "Services", "Costs", "Returns", "TOTAL"), diff --git a/man/tinyplot.matrix.Rd b/man/tinyplot.matrix.Rd index da266206..ad908448 100644 --- a/man/tinyplot.matrix.Rd +++ b/man/tinyplot.matrix.Rd @@ -84,7 +84,7 @@ 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", beside = TRUE, facet = "by", legend = FALSE) +tinyplot(VADeaths, type = "barplot", facet = "by", legend = FALSE) } \seealso{ From 6760ea87666bb62e54148cf4fc746bf8136c1fff Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Fri, 28 Aug 2026 09:43:00 -0700 Subject: [PATCH 09/23] tinylabel + abs precision (#689) --- R/tinylabel.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/R/tinylabel.R b/R/tinylabel.R index 9e3afec3..5ca08b65 100644 --- a/R/tinylabel.R +++ b/R/tinylabel.R @@ -178,7 +178,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) From ba50f158d9e2c544873507b5f0c01a8fcdc0a761 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Fri, 28 Aug 2026 10:17:40 -0700 Subject: [PATCH 10/23] tinylabel dictionary docs (late follow-up to #690) --- R/legend.R | 17 +++++++++-------- R/tinylabel.R | 13 +++++++------ R/tinyplot.R | 19 +++++++++++-------- R/tpar.R | 2 +- R/type_text.R | 10 +++++----- man/build_legend_args.Rd | 5 +++-- man/build_legend_env.Rd | 5 +++-- man/draw_legend.Rd | 7 +++---- man/tinyAxis.Rd | 13 +++++++------ man/tinylabel.Rd | 13 +++++++------ man/tinyplot.Rd | 19 +++++++++++-------- man/tpar.Rd | 2 +- man/type_text.Rd | 10 +++++----- 13 files changed, 73 insertions(+), 62 deletions(-) diff --git a/R/legend.R b/R/legend.R index eebb0ede..0dff41d1 100644 --- a/R/legend.R +++ b/R/legend.R @@ -542,8 +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 Function, character keyword, or named vector for formatting -#' or relabelling the labels. See [`tinylabel`] for the accepted forms. +#' @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) @@ -771,8 +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 Function, character keyword, or named vector for formatting -#' or relabelling the labels. See [`tinylabel`] for the accepted forms. +#' @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) @@ -881,10 +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 Function, character keyword, or named vector for formatting -#' or relabelling the labels (`lgnd_labs`). See [`tinylabel`] for the -#' accepted forms. -#' 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/tinylabel.R b/R/tinylabel.R index 5ca08b65..ce694ee2 100644 --- a/R/tinylabel.R +++ b/R/tinylabel.R @@ -13,15 +13,16 @@ #' for which common formatting transformations are provided: `"percent"` #' (`"%"`), `"comma"` (`","`), `"log"` (`"l"`), `"dollar"` (`"$"`), `"euro"` #' (`"€"`), or `"sterling"` (`"£"`). -#' - a *named* character vector or list, acting as a dictionary, e.g. -#' `c(setosa = "SET")`. 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. +#' - 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 -#' could not be told apart from a formatting keyword when `x` is of length +#' 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)]`. diff --git a/R/tinyplot.R b/R/tinyplot.R index 2e279ad1..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,12 +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, character keyword, or named vector 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. +#' @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. #' @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. 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_text.R b/R/type_text.R index 1e990859..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, convenience string, or named vector -#' 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 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/man/build_legend_args.Rd b/man/build_legend_args.Rd index 4f093e7a..28492eee 100644 --- a/man/build_legend_args.Rd +++ b/man/build_legend_args.Rd @@ -32,8 +32,9 @@ build_legend_args( \item{lgnd_labs}{The legend labels} -\item{labeller}{Function, character keyword, or named vector for formatting -or relabelling the labels. See \code{\link{tinylabel}} for the accepted forms.} +\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 69d9b0cf..62a162b3 100644 --- a/man/build_legend_env.Rd +++ b/man/build_legend_env.Rd @@ -36,8 +36,9 @@ build_legend_env( \item{lgnd_labs}{The legend labels} -\item{labeller}{Function, character keyword, or named vector for formatting -or relabelling the labels. See \code{\link{tinylabel}} for the accepted forms.} +\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 145b5ed9..412ba7b5 100644 --- a/man/draw_legend.Rd +++ b/man/draw_legend.Rd @@ -39,10 +39,9 @@ draw_legend( \item{lgnd_labs}{The labels passed to \code{legend(legend = ...)}.} -\item{labeller}{Function, character keyword, or named vector for formatting -or relabelling the labels (\code{lgnd_labs}). See \code{\link{tinylabel}} for the -accepted forms. -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 f5182168..baacbad0 100644 --- a/man/tinyAxis.Rd +++ b/man/tinyAxis.Rd @@ -29,16 +29,17 @@ package). 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 \emph{named} character vector or list, acting as a dictionary, e.g. -\code{c(setosa = "SET")}. 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. +\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 -could not be told apart from a formatting keyword when \code{x} is of length +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)]}.} diff --git a/man/tinylabel.Rd b/man/tinylabel.Rd index 2997d05c..4ebbe088 100644 --- a/man/tinylabel.Rd +++ b/man/tinylabel.Rd @@ -18,16 +18,17 @@ package). 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 \emph{named} character vector or list, acting as a dictionary, e.g. -\code{c(setosa = "SET")}. 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. +\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 -could not be told apart from a formatting keyword when \code{x} is of length +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)]}.} diff --git a/man/tinyplot.Rd b/man/tinyplot.Rd index ba643546..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,12 +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, character keyword, or named vector 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.} +\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.} \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/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_text.Rd b/man/type_text.Rd index b8705c81..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, convenience string, or named vector -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{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 From 9514972fa9cd87d4404230d526886318f2148ab5 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Fri, 28 Aug 2026 11:48:28 -0700 Subject: [PATCH 11/23] tests --- .../tinylabel_center_percent.svg | 75 +++++++++++++++++++ inst/tinytest/test-tinylabel.R | 69 +++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 inst/tinytest/_tinysnapshot/tinylabel_center_percent.svg 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/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") From f888ac1e1fc78cfdb4dcc8cdb47dec768792d899 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Fri, 28 Aug 2026 16:42:34 -0700 Subject: [PATCH 12/23] fix numeric->factor layering (#691) --- R/align_layer.R | 19 ++- .../tinyplot_add_numeric_x_barplot.svg | 71 ++++++++++ .../tinyplot_add_numeric_x_violin.svg | 122 ++++++++++++++++++ 3 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 inst/tinytest/_tinysnapshot/tinyplot_add_numeric_x_barplot.svg create mode 100644 inst/tinytest/_tinysnapshot/tinyplot_add_numeric_x_violin.svg 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/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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 0e4a7a22b4c5a7a74cfa00385b5c51b13c5a03db Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Fri, 28 Aug 2026 17:46:00 -0700 Subject: [PATCH 13/23] docs: better examples --- R/type_barplot.R | 52 ++++++++++++++++++++++++++++++++------------- man/type_barplot.Rd | 51 +++++++++++++++++++++++++++++++------------- 2 files changed, 73 insertions(+), 30 deletions(-) diff --git a/R/type_barplot.R b/R/type_barplot.R index ecf6192b..9c3db69c 100644 --- a/R/type_barplot.R +++ b/R/type_barplot.R @@ -75,33 +75,52 @@ #' `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, which now -#' accepts a named vector mapping old labels to new ones, and applies +#' accepts a dictionary mapping old labels to new ones, and applies #' consistently across plot types. This argument will be removed in a future #' release. #' #' @examples #' # -#' ## Basic use +#' ## Basic use (raw values) #' -#' sleep2 = transform(sleep, drug = group) # less misleading name (same people) +#' tinyplot(GNP ~ Year, data = longley, type = "barplot") #' -#' tinyplot(extra ~ ID, data = sleep2, type = "barplot") -#' tinyplot(extra ~ ID, data = sleep2, type = "barplot", xord = "desc") -#' tinyplot(extra ~ ID | drug, data = sleep2, type = "barplot", beside = TRUE) +#' tinyplot(demand ~ Time, data = BOD, type = "bar") # "bar" is a shorthand +#' tinyplot_add(type = "text", pos = 3, xpd = NA) # add y values as text +#' +#' # +#' ## Aggregated vs grouped values (multiple ys per x) #' -#' # Change the aggregation (non-grouped case) from the `FUN = mean` default to -#' # ask a more interesting question: which subject benefitted most from the -#' # switch to drug 2? +#' # 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 soporiphic drugs" +#' ) +#' # switch to diff (answers a more relevant q: who benefits most from drug 2?) #' tinyplot( -#' extra ~ ID, data = sleep2, type = "barplot", -#' FUN = diff, xord = "desc", -#' main = "Sleep gain (drug 2 vs drug 1)" +#' 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" #' ) #' +#' # 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( #' extra ~ ID | drug, data = sleep2, #' type = type_barplot(beside = TRUE, xord = "desc", width = 0.5) @@ -120,6 +139,7 @@ #' # 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( @@ -132,8 +152,8 @@ #' # #' ## 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") @@ -144,6 +164,7 @@ #' flip = TRUE, yaxl = "percent", #' theme = list("clean2", palette.qualitative = hcols) #' ) +#' tinyplot_add(type = "vline", col = "white") #' #' # #' ## Offset examples @@ -188,6 +209,7 @@ #' 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, xord = NULL, drop.zeros = FALSE, lighten = TRUE, xaxlabels = NULL) { diff --git a/man/type_barplot.Rd b/man/type_barplot.Rd index 087fe612..8ee1a957 100644 --- a/man/type_barplot.Rd +++ b/man/type_barplot.Rd @@ -97,7 +97,7 @@ displays consistent and lets the fill read cleanly over grid lines. Set to \item{xaxlabels}{[Deprecated] a character vector with the axis labels for the \code{x} variable. Use the top-level \code{xaxl} argument instead, which now -accepts a named vector mapping old labels to new ones, and applies +accepts a dictionary mapping old labels to new ones, and applies consistently across plot types. This argument will be removed in a future release.} } @@ -110,27 +110,45 @@ using some function (default: mean). } \examples{ # -## Basic use +## Basic use (raw values) -sleep2 = transform(sleep, drug = group) # less misleading name (same people) +tinyplot(GNP ~ Year, data = longley, type = "barplot") -tinyplot(extra ~ ID, data = sleep2, type = "barplot") -tinyplot(extra ~ ID, data = sleep2, type = "barplot", xord = "desc") -tinyplot(extra ~ ID | drug, data = sleep2, type = "barplot", beside = TRUE) +tinyplot(demand ~ Time, data = BOD, type = "bar") # "bar" is a shorthand +tinyplot_add(type = "text", pos = 3, xpd = NA) # add y values as text + +# +## Aggregated vs grouped values (multiple ys per x) -# Change the aggregation (non-grouped case) from the `FUN = mean` default to -# ask a more interesting question: which subject benefitted most from the -# switch to drug 2? +# 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 soporiphic 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)" + extra ~ ID, data = sleep2, + type = "barplot", FUN = diff, xord = "desc", + main = "Sleep gain (drug 2 vs drug 1), ordered" ) +# 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( extra ~ ID | drug, data = sleep2, type = type_barplot(beside = TRUE, xord = "desc", width = 0.5) @@ -149,6 +167,7 @@ tinyplot(VADeaths, type = "barplot", beside = TRUE) # 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( @@ -161,8 +180,8 @@ tinyplot( # ## 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") @@ -173,6 +192,7 @@ tinyplot( flip = TRUE, yaxl = "percent", theme = list("clean2", palette.qualitative = hcols) ) +tinyplot_add(type = "vline", col = "white") # ## Offset examples @@ -217,5 +237,6 @@ tinyplot( main = "Hypothetical Likert example with category offset" ) tinyplot_add(type = "vline") +tinyplot_add(type = "vline", v = 1, lty = 2) } From 862b462a4a07cfb8b0294063c8dfa0f1e21bb945 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Fri, 28 Aug 2026 19:22:25 -0700 Subject: [PATCH 14/23] forgot the actual tests --- inst/tinytest/test-tinyplot_add.R | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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") From cba3a0421ce053ce174fdeb623f6a0bae3be9d51 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Fri, 28 Aug 2026 21:02:46 -0700 Subject: [PATCH 15/23] barplot tests --- .../_tinysnapshot/barplot_xaxl_dict.svg | 63 ++++++++++ .../barplot_xord_aggregated_mean.svg | 59 ++++++++++ .../barplot_xord_aggregated_sum.svg | 55 +++++++++ .../_tinysnapshot/barplot_xord_ascending.svg | 63 ++++++++++ .../_tinysnapshot/barplot_xord_asis.svg | 59 ++++++++++ .../_tinysnapshot/barplot_xord_desc.svg | 63 ++++++++++ .../_tinysnapshot/barplot_xord_rev.svg | 63 ++++++++++ inst/tinytest/test-type_barplot.R | 111 ++++++++++++++++++ 8 files changed, 536 insertions(+) create mode 100644 inst/tinytest/_tinysnapshot/barplot_xaxl_dict.svg create mode 100644 inst/tinytest/_tinysnapshot/barplot_xord_aggregated_mean.svg create mode 100644 inst/tinytest/_tinysnapshot/barplot_xord_aggregated_sum.svg create mode 100644 inst/tinytest/_tinysnapshot/barplot_xord_ascending.svg create mode 100644 inst/tinytest/_tinysnapshot/barplot_xord_asis.svg create mode 100644 inst/tinytest/_tinysnapshot/barplot_xord_desc.svg create mode 100644 inst/tinytest/_tinysnapshot/barplot_xord_rev.svg 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/test-type_barplot.R b/inst/tinytest/test-type_barplot.R index a994a37a..f3355cac 100644 --- a/inst/tinytest/test-type_barplot.R +++ b/inst/tinytest/test-type_barplot.R @@ -192,3 +192,114 @@ 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 +# TODO: sanitize_xlevels() warns about the partial match and *then* aborts on +# the complete miss. expect_error() does not muffle the warning, so it escapes +# to R's deferred list and surfaces at the end of a suite run. Restore this and +# the `xlevels = "rev"` case below once the no-match stop() is ordered ahead of +# the partial-match warning. +# expect_warning( +# tinyplot(~ cyl, data = mtcars, type = type_barplot(xlevels = "asis")), +# pattern = "correspond to levels" +# ) + +# "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" +) +# and a complete miss is fatal, rather than surfacing later as an unrelated +# error about zero-length ranges. (Commented out; see the TODO above.) +# expect_error( +# tinyplot(~ cyl, data = mtcars, type = type_barplot(xlevels = "rev")), +# pattern = "matches none of the 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") + From 24b48bd051689d0d5a6409d9bdeb92a8d7eb99ad Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Fri, 28 Aug 2026 21:41:34 -0700 Subject: [PATCH 16/23] some extra examples --- R/type_barplot.R | 20 ++++++++++++++++---- man/type_barplot.Rd | 20 ++++++++++++++++---- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/R/type_barplot.R b/R/type_barplot.R index 9c3db69c..90221255 100644 --- a/R/type_barplot.R +++ b/R/type_barplot.R @@ -74,10 +74,8 @@ #' 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, which now -#' accepts a dictionary mapping old labels to new ones, and applies -#' consistently across plot types. This argument will be removed in a future -#' release. +#' the `x` variable. Use the top-level `xaxl` argument instead (see +#' `[tinylabel]`). This argument will be removed in a future release. #' #' @examples #' # @@ -88,6 +86,11 @@ #' 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) #' @@ -126,6 +129,15 @@ #' type = type_barplot(beside = TRUE, xord = "desc", width = 0.5) #' ) #' +#' # speaking of top-level args, use xaxl to format the x labels, e.g. with a +#' # dictionary, keyword, or (here:) function +#' +#' tinyplot( +#' extra ~ ID | drug, data = sleep2, +#' type = type_barplot(beside = TRUE, xord = "desc"), +#' xaxl = as.roman +#' ) +#' #' # #' ## matrix method (no formula required) #' diff --git a/man/type_barplot.Rd b/man/type_barplot.Rd index 8ee1a957..aa722763 100644 --- a/man/type_barplot.Rd +++ b/man/type_barplot.Rd @@ -96,10 +96,8 @@ 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, which now -accepts a dictionary mapping old labels to new ones, and applies -consistently across plot types. This argument will be removed in a future -release.} +the \code{x} variable. Use the top-level \code{xaxl} argument instead (see +\verb{[tinylabel]}). This argument will be removed in a future release.} } \description{ Type function for producing barplots. For formulas of type @@ -117,6 +115,11 @@ 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) @@ -154,6 +157,15 @@ tinyplot( type = type_barplot(beside = TRUE, xord = "desc", width = 0.5) ) +# speaking of top-level args, use xaxl to format the x labels, e.g. with a +# dictionary, keyword, or (here:) function + +tinyplot( + extra ~ ID | drug, data = sleep2, + type = type_barplot(beside = TRUE, xord = "desc"), + xaxl = as.roman +) + # ## matrix method (no formula required) From 628d5c1c355a881b75a78fe8744a708fd51c2488 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Fri, 28 Aug 2026 21:55:48 -0700 Subject: [PATCH 17/23] x/yord and x/yaxl tests --- .../_tinysnapshot/area_stack_byord_asc.svg | 87 +++++++++++++ .../_tinysnapshot/area_stack_byord_desc.svg | 87 +++++++++++++ .../_tinysnapshot/area_stack_byord_start.svg | 89 +++++++++++++ .../_tinysnapshot/area_stack_flip.svg | 8 +- .../tinytest/_tinysnapshot/lines_xord_rev.svg | 64 ++++++++++ .../_tinysnapshot/points_xord_desc.svg | 91 ++++++++++++++ .../_tinysnapshot/points_xord_rev.svg | 91 ++++++++++++++ .../_tinysnapshot/ridge_yaxl_dict.svg | 71 +++++++++++ .../_tinysnapshot/ridge_yaxl_toupper.svg | 71 +++++++++++ .../_tinysnapshot/ridge_yord_minvar.svg | 67 ++++++++++ .../tinytest/_tinysnapshot/ridge_yord_rev.svg | 67 ++++++++++ .../_tinysnapshot/spineplot_xaxl_toupper.svg | 63 ++++++++++ .../_tinysnapshot/spineplot_xord_desc.svg | 70 +++++++++++ .../_tinysnapshot/spineplot_yaxl_dict.svg | 118 ++++++++++++++++++ .../_tinysnapshot/spineplot_yaxl_toupper.svg | 118 ++++++++++++++++++ .../_tinysnapshot/spineplot_yord_rev.svg | 70 +++++++++++ inst/tinytest/test-type_area.R | 40 ++++-- inst/tinytest/test-type_lines.R | 6 +- inst/tinytest/test-type_pointrange.R | 16 ++- inst/tinytest/test-type_points.R | 37 ++++++ inst/tinytest/test-type_ridge.R | 38 ++++++ inst/tinytest/test-type_spineplot.R | 48 +++++++ 22 files changed, 1400 insertions(+), 17 deletions(-) create mode 100644 inst/tinytest/_tinysnapshot/area_stack_byord_asc.svg create mode 100644 inst/tinytest/_tinysnapshot/area_stack_byord_desc.svg create mode 100644 inst/tinytest/_tinysnapshot/area_stack_byord_start.svg create mode 100644 inst/tinytest/_tinysnapshot/lines_xord_rev.svg create mode 100644 inst/tinytest/_tinysnapshot/points_xord_desc.svg create mode 100644 inst/tinytest/_tinysnapshot/points_xord_rev.svg create mode 100644 inst/tinytest/_tinysnapshot/ridge_yaxl_dict.svg create mode 100644 inst/tinytest/_tinysnapshot/ridge_yaxl_toupper.svg create mode 100644 inst/tinytest/_tinysnapshot/ridge_yord_minvar.svg create mode 100644 inst/tinytest/_tinysnapshot/ridge_yord_rev.svg create mode 100644 inst/tinytest/_tinysnapshot/spineplot_xaxl_toupper.svg create mode 100644 inst/tinytest/_tinysnapshot/spineplot_xord_desc.svg create mode 100644 inst/tinytest/_tinysnapshot/spineplot_yaxl_dict.svg create mode 100644 inst/tinytest/_tinysnapshot/spineplot_yaxl_toupper.svg create mode 100644 inst/tinytest/_tinysnapshot/spineplot_yord_rev.svg create mode 100644 inst/tinytest/test-type_points.R 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/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/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_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..c8013554 100644 --- a/inst/tinytest/test-type_ridge.R +++ b/inst/tinytest/test-type_ridge.R @@ -190,3 +190,41 @@ 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" +) + +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") + From cca384cf9492c59790095a680990c952114b00e9 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Fri, 28 Aug 2026 22:45:41 -0700 Subject: [PATCH 18/23] news --- NEWS.md | 81 +++++++++++++++++++++++++++++++++++++++------- altdoc/pkgdown.yml | 4 +-- 2 files changed, 72 insertions(+), 13 deletions(-) diff --git a/NEWS.md b/NEWS.md index ea6b04ca..49ebf47c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -8,6 +8,13 @@ where the formatting is also better._ ### Breaking changes +- The `xaxlabels` argument of `type_barplot()`, and the `xaxlabels` / + `yaxlabels` arguments of `type_spineplot()`, are deprecated in favour of the + top-level `xaxl` / `yaxl` arguments, which accept a dictionary mapping old + labels to new ones and apply consistently across plot types. Supplying them + still works but warns. Note that `type_barplot()`'s `xaxlabels` renamed the + underlying factor levels, so repeated names merged categories; `xaxl` + relabels the ticks only and cannot do this. (#688 @grantmcdermott) - `type_lines()` and its shortcut equivalents like `"l"` and `"b"` now order categorical `x` data by (coerced) factor levels, rather than simple order of appearance. This resolves a longstanding tension between line types and @@ -15,7 +22,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 @@ -66,6 +73,40 @@ 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")`. +#### Improved facilities for ordering and labelling categorical variables + +Types with a categorical `x`, `y`, or `by` variable gain several new arguments +that enable finer control over the order of their levels, as well as the labels +used to display them: + +- Expanded `xlevels` support for reordering a categorical `x` variable on the + fly. `type_points()`, `type_lines()`, `type_errorbar()`, and + `type_pointrange()` all gain this (type-level) argument, matching existing + functionality for `type_barplot()` and several other types. Values can be a + character vector of level names or a numeric vector of level indexes, e.g. + `3:1`. (#683, #694 @grantmcdermott) +- New `xord` / `yord` arguments for _deriving_ order from the data, rather + than stating it literally (which is what `x/ylevels` do). Specifically, every + type that takes an `xlevels` or `ylevels` argument now also takes its `x/yord` + sibling. The two are complementary: `x/ylevels` name the levels explicitly, + while `x/yord` computes them via type-appropriate keywords or a custom ranking + function. For example, `"desc(ending)"`/`"asc(ending)"` order by value, while + `"asis"` ignores factor levels and just takes the order of categories as they + appear in the data. Among other things this makes it possible to sort bars 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. _Note: users should only supply one or the other. If both are given, + `x/ylevels` takes precedence and `x/yord` is ignored, since the former is + more explcit. (#683 #688, #694 @grantmcdermott) +- `tinylabel()` gains a dictionary form, i.e, a *named* character vector or + list, mapping existing labels to new ones, e.g. `c(old = "new")`. Labels that + are not named are left alone, so a partial mapping is fine, and the lookup is + by value rather than by position, which means it is unaffected by any + reordering of the underlying categories. Because everything that formats + labels routes through `tinylabel()`, this is available anywhere a `labeller` + is accepted: the top-level `xaxl` / `yaxl` arguments, the legend's + `labeller`, `type_text()`, and facet titles. (#688 @grantmcdermott) + #### Other new features - `type_area()` gains a `stack` argument for stacked area plots. A sister @@ -78,16 +119,6 @@ a (custom) theme, e.g. `tinytheme("clean", facet.axes = "outer")`. 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) - 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 @@ -117,6 +148,34 @@ 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%`. (#688 @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. (#688 @grantmcdermott) +- Fixed the formatting keywords being unusable on a centered `type_barplot()`. + Centering prepends `"abs_"` to `yaxl`, but the prefix was applied before a + symbol was resolved to its full name, so documented values such as + `yaxl = ","` failed with an unrelated `match.arg()` error. + (#688 @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 @grantmcdermott) +- The legend key for a stacked `type_area()` is no longer reversed when + `flip = TRUE`. Flipping lays the bands out left-to-right, so the bottom-up + reading that the reversal exists to match no longer applies. + (#688 @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/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 From 9555635b67d81931869131996e9e784d3102a67f Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sat, 29 Aug 2026 17:57:45 -0700 Subject: [PATCH 19/23] new touch-ups --- NEWS.md | 129 +++++++++++++++++++++++++++----------------------------- 1 file changed, 63 insertions(+), 66 deletions(-) diff --git a/NEWS.md b/NEWS.md index 49ebf47c..86002ead 100644 --- a/NEWS.md +++ b/NEWS.md @@ -8,13 +8,6 @@ where the formatting is also better._ ### Breaking changes -- The `xaxlabels` argument of `type_barplot()`, and the `xaxlabels` / - `yaxlabels` arguments of `type_spineplot()`, are deprecated in favour of the - top-level `xaxl` / `yaxl` arguments, which accept a dictionary mapping old - labels to new ones and apply consistently across plot types. Supplying them - still works but warns. Note that `type_barplot()`'s `xaxlabels` renamed the - underlying factor levels, so repeated names merged categories; `xaxl` - relabels the ticks only and cannot do this. (#688 @grantmcdermott) - `type_lines()` and its shortcut equivalents like `"l"` and `"b"` now order categorical `x` data by (coerced) factor levels, rather than simple order of appearance. This resolves a longstanding tension between line types and @@ -39,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 @@ -73,52 +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")`. -#### Improved facilities for ordering and labelling categorical variables - -Types with a categorical `x`, `y`, or `by` variable gain several new arguments -that enable finer control over the order of their levels, as well as the labels -used to display them: - -- Expanded `xlevels` support for reordering a categorical `x` variable on the - fly. `type_points()`, `type_lines()`, `type_errorbar()`, and - `type_pointrange()` all gain this (type-level) argument, matching existing - functionality for `type_barplot()` and several other types. Values can be a - character vector of level names or a numeric vector of level indexes, e.g. - `3:1`. (#683, #694 @grantmcdermott) -- New `xord` / `yord` arguments for _deriving_ order from the data, rather - than stating it literally (which is what `x/ylevels` do). Specifically, every - type that takes an `xlevels` or `ylevels` argument now also takes its `x/yord` - sibling. The two are complementary: `x/ylevels` name the levels explicitly, - while `x/yord` computes them via type-appropriate keywords or a custom ranking - function. For example, `"desc(ending)"`/`"asc(ending)"` order by value, while - `"asis"` ignores factor levels and just takes the order of categories as they - appear in the data. Among other things this makes it possible to sort bars by - height (e.g., `type_barplot(xord = "desc")`), or ridges by their spread (e.g., +#### Ordering and labelling categorical variables + +This release bring 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) argument 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 others 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. _Note: users should only supply one or the other. If both are given, - `x/ylevels` takes precedence and `x/yord` is ignored, since the former is - more explcit. (#683 #688, #694 @grantmcdermott) + hand. (#683, #694 @grantmcdermott) +- (Note: users should only supply one of 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, mapping existing labels to new ones, e.g. `c(old = "new")`. Labels that - are not named are left alone, so a partial mapping is fine, and the lookup is - by value rather than by position, which means it is unaffected by any - reordering of the underlying categories. Because everything that formats - labels routes through `tinylabel()`, this is available anywhere a `labeller` - is accepted: the top-level `xaxl` / `yaxl` arguments, the legend's - `labeller`, `type_text()`, and facet titles. (#688 @grantmcdermott) + 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)labeling 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 axes tick. (#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_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 @@ -128,12 +140,6 @@ used to display them: 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. @@ -157,25 +163,16 @@ used to display them: - 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%`. (#688 @grantmcdermott) + `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. (#688 @grantmcdermott) -- Fixed the formatting keywords being unusable on a centered `type_barplot()`. - Centering prepends `"abs_"` to `yaxl`, but the prefix was applied before a - symbol was resolved to its full name, so documented values such as - `yaxl = ","` failed with an unrelated `match.arg()` error. - (#688 @grantmcdermott) + 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 @grantmcdermott) -- The legend key for a stacked `type_area()` is no longer reversed when - `flip = TRUE`. Flipping lays the bands out left-to-right, so the bottom-up - reading that the reversal exists to match no longer applies. - (#688 @grantmcdermott) + 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) From 2b37084f0bf3c0efcbf20723d82f4da56dff80f9 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sat, 29 Aug 2026 20:44:19 -0700 Subject: [PATCH 20/23] typos etc. --- NEWS.md | 22 +++++++++++----------- R/type_barplot.R | 4 ++-- man/type_barplot.Rd | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/NEWS.md b/NEWS.md index 86002ead..53e3b94c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -76,18 +76,18 @@ a (custom) theme, e.g. `tinytheme("clean", facet.axes = "outer")`. #### Ordering and labelling categorical variables -This release bring several enhancements for working with _categorical_ +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) argument permit on-the-fly +- `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 others types for a while---we now - extend `xlevels` support to `type_points()`, `type_lines()`, + 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 @@ -96,18 +96,18 @@ and convenient label formatting. 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., + (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 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 +- (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)labeling machinery---including `x/yaxl`, `type_text()`, and any function + (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 @@ -117,7 +117,7 @@ and convenient label formatting. 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 axes tick. (#692 @grantmcdermott) + 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, diff --git a/R/type_barplot.R b/R/type_barplot.R index 90221255..f396c54b 100644 --- a/R/type_barplot.R +++ b/R/type_barplot.R @@ -75,7 +75,7 @@ #' `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. +#' [`tinylabel`]). This argument will be removed in a future release. #' #' @examples #' # @@ -101,7 +101,7 @@ #' tinyplot( #' extra ~ ID, data = sleep2, #' type = "barplot", -#' main = "Mean extra sleep from 2 soporiphic drugs" +#' main = "Mean extra sleep from 2 soporific drugs" #' ) #' # switch to diff (answers a more relevant q: who benefits most from drug 2?) #' tinyplot( diff --git a/man/type_barplot.Rd b/man/type_barplot.Rd index aa722763..d47299bd 100644 --- a/man/type_barplot.Rd +++ b/man/type_barplot.Rd @@ -97,7 +97,7 @@ displays consistent and lets the fill read cleanly over grid lines. Set to \item{xaxlabels}{[Deprecated] a character vector with the axis labels for the \code{x} variable. Use the top-level \code{xaxl} argument instead (see -\verb{[tinylabel]}). This argument will be removed in a future release.} +\code{\link{tinylabel}}). This argument will be removed in a future release.} } \description{ Type function for producing barplots. For formulas of type @@ -130,7 +130,7 @@ sleep2 = transform(sleep, drug = group) # less misleading name tinyplot( extra ~ ID, data = sleep2, type = "barplot", - main = "Mean extra sleep from 2 soporiphic drugs" + main = "Mean extra sleep from 2 soporific drugs" ) # switch to diff (answers a more relevant q: who benefits most from drug 2?) tinyplot( From 5685c3240ba2643a65c52ee7b2c728eb412a2e64 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sat, 29 Aug 2026 20:58:59 -0700 Subject: [PATCH 21/23] reorder to catch degenerate case --- R/sanitize_xlevels.R | 18 ++++++++++++------ inst/tinytest/test-type_barplot.R | 22 ++++++---------------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/R/sanitize_xlevels.R b/R/sanitize_xlevels.R index bb96125b..a9431588 100644 --- a/R/sanitize_xlevels.R +++ b/R/sanitize_xlevels.R @@ -28,18 +28,18 @@ sanitize_xlevels = function(x, xlevels, arg = "xlevels") { xlevels = levels(x)[xlevels] } v = substr(arg, 1, 1) - if (anyNA(xlevels) || !all(xlevels %in% levels(x))) { - warning(sprintf( - "not all '%s' correspond to levels of '%s'", - arg, v - )) - } # 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( @@ -47,6 +47,12 @@ sanitize_xlevels = function(x, xlevels, arg = "xlevels") { 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, v + )) + } dropped = setdiff(levels(x), xlevels) if (length(dropped) > 0L) { warning(sprintf( diff --git a/inst/tinytest/test-type_barplot.R b/inst/tinytest/test-type_barplot.R index f3355cac..08923ce7 100644 --- a/inst/tinytest/test-type_barplot.R +++ b/inst/tinytest/test-type_barplot.R @@ -224,16 +224,12 @@ 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 -# TODO: sanitize_xlevels() warns about the partial match and *then* aborts on -# the complete miss. expect_error() does not muffle the warning, so it escapes -# to R's deferred list and surfaces at the end of a suite run. Restore this and -# the `xlevels = "rev"` case below once the no-match stop() is ordered ahead of -# the partial-match warning. -# expect_warning( -# tinyplot(~ cyl, data = mtcars, type = type_barplot(xlevels = "asis")), -# pattern = "correspond to levels" -# ) +# 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" +) # "start"/"end" name a position along a secondary axis, which x-categories do # not have; offering them here would silently alias "desc" (ungrouped) or @@ -272,12 +268,6 @@ expect_warning( tinyplot(~ cyl, data = mtcars, type = type_barplot(xlevels = c("8", "4"))), pattern = "omits 1 of the 3 levels" ) -# and a complete miss is fatal, rather than surfacing later as an unrelated -# error about zero-length ranges. (Commented out; see the TODO above.) -# expect_error( -# tinyplot(~ cyl, data = mtcars, type = type_barplot(xlevels = "rev")), -# pattern = "matches none of the levels" -# ) asis_dat = data.frame( g = factor(c("z", "z", "a", "m", "m", "m")), # appearance z,a,m; levels a,m,z From 52242909a52551d56d487aa015ccdfc8d427d6c1 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sat, 29 Aug 2026 21:08:47 -0700 Subject: [PATCH 22/23] fix(sanitize_ord): correct bad-input diagnostics --- R/sanitize_ord.R | 29 ++++++++++++++++++++++++----- inst/tinytest/test-type_barplot.R | 8 ++++++++ inst/tinytest/test-type_ridge.R | 7 +++++++ 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/R/sanitize_ord.R b/R/sanitize_ord.R index 179f982a..b74bdbeb 100644 --- a/R/sanitize_ord.R +++ b/R/sanitize_ord.R @@ -94,9 +94,12 @@ ord_aliases = c( ) sanitize_ord = function(v, y, x, ord, arg = "ord", keywords = ord_keywords, stat = c("sum", "mean")) { - # nlevels < 2 has exactly one ordering, so skip the work (and the degeneracy - # check below, which a single level would otherwise trip). - if (is.null(ord) || !is.factor(v) || nlevels(v) < 2L) { + # 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) @@ -124,6 +127,14 @@ sanitize_ord = function(v, y, x, ord, arg = "ord", keywords = ord_keywords, stat ) } + # 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. @@ -139,10 +150,18 @@ sanitize_ord = function(v, y, x, ord, arg = "ord", keywords = ord_keywords, stat # 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 = \"%s\"` ranks on a numeric variable, but was given %s.\n Only \"asis\" and \"rev\" work without one.", - arg, ord, class(y)[1L] + "%s ranks on a numeric variable, but was given %s.\n Only \"asis\" and \"rev\" work without one.", + culprit, class(y)[1L] ), call. = FALSE ) diff --git a/inst/tinytest/test-type_barplot.R b/inst/tinytest/test-type_barplot.R index 08923ce7..08230193 100644 --- a/inst/tinytest/test-type_barplot.R +++ b/inst/tinytest/test-type_barplot.R @@ -231,6 +231,14 @@ expect_error( 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) diff --git a/inst/tinytest/test-type_ridge.R b/inst/tinytest/test-type_ridge.R index c8013554..0ed6fb47 100644 --- a/inst/tinytest/test-type_ridge.R +++ b/inst/tinytest/test-type_ridge.R @@ -207,6 +207,13 @@ 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")), From f5aa8cc65de9e293d6f1187a67de3ac5afe007d8 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sat, 29 Aug 2026 21:30:01 -0700 Subject: [PATCH 23/23] facet labeller dictionary gotcha --- R/assertions.R | 29 +- .../_tinysnapshot/facet_labeller_dict.svg | 328 ++++++++++++++++++ inst/tinytest/test-facet.R | 12 + 3 files changed, 364 insertions(+), 5 deletions(-) create mode 100644 inst/tinytest/_tinysnapshot/facet_labeller_dict.svg 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/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/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