From cdfcf612dee5d69134285b948f26de1a0255490a Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Thu, 20 Aug 2026 15:12:36 -0700 Subject: [PATCH 1/5] new facet.args: prefix, labeller, and sep --- R/assertions.R | 43 +++++++ R/facet.R | 250 ++++++++++++++++++++++++++++++++++++- R/sanitize_facet.R | 26 +++- R/tinyplot.R | 64 +++++++++- R/tpar.R | 12 ++ man/tinyplot.Rd | 44 ++++++- man/tpar.Rd | 3 + vignettes/introduction.qmd | 30 +++-- 8 files changed, 453 insertions(+), 19 deletions(-) diff --git a/R/assertions.R b/R/assertions.R index 2452da5bc..eefd0d330 100644 --- a/R/assertions.R +++ b/R/assertions.R @@ -80,6 +80,49 @@ assert_flag = function(x, null.ok = FALSE, name = as.character(substitute(x))) { } } +# facet titles prefix: TRUE/FALSE, or custom name(s) for the facet variable(s). +# The latter can be given as a character vector or as a list of strings (the +# vector is the canonical form, but `labeller` takes a list and there is no +# reason to reject the same container here). +assert_facet_prefix = function(x, name = as.character(substitute(x))) { + if (is.null(x)) return(invisible(TRUE)) + if (is.logical(x) && length(x) == 1L && !is.na(x)) return(invisible(TRUE)) + if ((is.character(x) || is.list(x)) && length(x) >= 1L) { + if (all(vapply(x, is_string1, logical(1L)))) return(invisible(TRUE)) + } + stop( + sprintf( + "`%s` must be a logical flag, or facet variable name(s) supplied as a character vector or list of strings.", + name + ), + call. = FALSE + ) +} + +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. +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)) + } + msg = if (isTRUE(list.ok)) { + "`%s` must be a function or a `tinylabel()` convenience string, or a list of them (one per facet variable)." + } else { + "`%s` must be a function, or a `tinylabel()` convenience string." + } + stop(sprintf(msg, name), call. = FALSE) +} + +is_labeller = function(x) { + is.function(x) || (is.character(x) && length(x) == 1L && !is.na(x)) +} + assert_length = function(x, len = 1, null.ok = FALSE, name = as.character(substitute(x))) { if (is.null(x) && isTRUE(null.ok)) { return(invisible(TRUE)) diff --git a/R/facet.R b/R/facet.R index 3f23a21df..c8e3a0261 100644 --- a/R/facet.R +++ b/R/facet.R @@ -234,6 +234,15 @@ draw_facet_window = function( } } + # Facet grids draw their row titles rotated into the RHS margin, so + # multi-line titles need extra width out there -- the counterpart to the + # fmar[3] bump that the top strips get above. Without it the rotated title + # (and its background rect) overflows the figure region. Regular facet + # titles all sit on top, so only grids need this. + if (isTRUE(attr(facet, "facet_grid")) && facet_newlines > 0) { + omar[4] = omar[4] + facet_newlines * facet_text + } + if (dl_overshoot > 0) { fmar[4] = fmar[4] + dl_overshoot } @@ -617,7 +626,10 @@ draw_facet_window = function( text( x = xpos, y = ypos, - labels = paste(facets[[ii]]), + # a labeller can return plotmath (e.g. tinylabel's "log"), in which + # case the element has to reach text() as a language object rather + # than being flattened by paste(); see facet_titles() + labels = if (is.expression(facets)) facets[[ii]] else paste(facets[[ii]]), adj = c(0.5, 0.5), cex = facet_text / cex_fct_adj, col = facet_col, @@ -737,6 +749,15 @@ facet_layout = function(settings) { nfacet_cols = 1 if (!is.null(facet)) { facets = if (is.factor(facet)) levels(facet) else sort(unique(facet)) + # optional labelling and/or "varname = value" prefixing; see facet_titles() + facets = facet_titles( + facets, + labeller = facet.args[["labeller"]] %||% .tpar[["facet.labeller"]], + prefix = facet.args[["prefix"]] %||% .tpar[["facet.prefix"]], + facet_names = settings$facet_names, + facet_grid = isTRUE(attr(facet, "facet_grid")), + sep = facet.args[["sep"]] %||% .tpar[["facet.sep"]] + ) ifacet = seq_along(facets) nfacets = length(facets) if (isTRUE(add)) { @@ -794,6 +815,228 @@ facet_layout = function(settings) { # +## Build the facet strip titles: optionally run the facet values through a +## `tinylabel()` labeller, then optionally prefix them with their variable +## name(s), e.g. "vs = 0" rather than a bare "0". See the `labeller`, `prefix` +## and `sep` entries of `facet.args` in ?tinyplot. +## +## `labels` are the raw facet titles as computed in facet_layout(), i.e. the +## levels (or sorted unique values) of the facet variable. `facet_names` is the +## list that sanitize_facet() resolves for the plot: an "x" element (names for +## the regular / top strip labels) and, for facet grids, a "y" element (names +## for the right strip labels). +## +## Composite titles are taken apart before either step is applied, so that a +## labeller sees the individual facet *values* rather than the glued-together +## string: grid titles split on "~" (matching the sub() patterns that +## draw_facet_window() uses to split them again at draw time), and each side +## then splits on the ":" that interaction() used for multi-variable facets. +## +## `labeller` and `prefix` both accept either one value for every facet +## variable, or one per variable -- positionally, in the order the variables +## appear in the `facet` specification, or named for the variables they apply +## to. See match_facet_vars(). +## +## `sep` separates the variables of a multi-variable title, whether or not they +## are prefixed, e.g. "\n" to stack them on separate lines. It defaults to the +## ":" that interaction() glued them with upstream, or to ", " once they carry +## their own names, which is easier to read (compare "vs = 0, am = 1" against +## "vs = 0:am = 1"). The name and its value are always joined by " = ". +## +## A labeller that returns plotmath (tinylabel's "log") survives as an +## expression for a single unprefixed facet variable, which draw_facet_window() +## hands to text() as-is; every other route glues strings together and so +## deparses it. +facet_titles = function( + labels, + labeller = NULL, + prefix = NULL, + facet_names = NULL, + facet_grid = FALSE, + sep = NULL) { + has_prefix = !(is.null(prefix) || isFALSE(prefix)) + if (is.null(labeller) && !has_prefix && is.null(sep)) return(labels) + + ## separator between the variables of a multi-variable title (see above) + if (is.null(sep)) sep = if (has_prefix) ", " else ":" + if (isTRUE(facet_grid) && grepl("~", sep, fixed = TRUE)) { + stop( + "`facet.args$sep` cannot contain a \"~\" for facet grids, since that is ", + "the separator between the top and right strip titles.", + call. = FALSE + ) + } + + xvars = facet_names[["x"]] + yvars = facet_names[["y"]] + nx = length(xvars) + ny = length(yvars) + ## Facet variables in *formula* order, i.e. the order the user wrote them: for + ## a grid the LHS (drawn as the right-hand strips) comes first, since + ## get_facet_fml() swaps the sides internally to plot rowwise. Splitting a + ## per-variable input back out therefore takes the y side off the front. + vars = c(yvars, xvars) + split_sides = function(x) { + if (is.null(x)) return(list(x = NULL, y = NULL)) + if (length(vars) == 0L) return(list(x = x, y = x)) + list(x = x[ny + seq_len(nx)], y = x[seq_len(ny)]) + } + + xnms = xvars + ynms = yvars + if (has_prefix) { + ## Also guards the internal entry point: anything that is neither a flag nor + ## name(s) used to fall through the branches below and be silently ignored, + ## leaving the public assert load-bearing for correctness rather than just + ## for error quality. + assert_facet_prefix(prefix, name = "facet.args$prefix") + if (is.character(prefix) || is.list(prefix)) { + pnms = match_facet_vars(prefix, vars, "facet.args$prefix") + ## a named input can name only some of the variables; the rest fall back + ## to their own (deparsed) name, exactly as `prefix = TRUE` would + if (length(vars) > 0L) { + pnms = lapply(seq_along(pnms), function(i) pnms[[i]] %||% vars[[i]]) + } + pnms = as.character(unlist(pnms)) + if (length(vars) == 0L) { + ## no known variable names (e.g. a facet variable that upstream methods + ## construct themselves): the string simply becomes the single prefix + xnms = pnms + ynms = NULL + nx = 1L + } else { + sides = split_sides(pnms) + xnms = sides[["x"]] + ynms = sides[["y"]] + } + } else if (nx + ny == 0L) { + ## prefix = TRUE, but we couldn't determine any variable names + has_prefix = FALSE + } + } + + ## per-variable labellers, split across the two sides the same way + labellers = if (is.null(labeller)) { + NULL + } else { + match_facet_vars(labeller, vars, "facet.args$labeller") + } + sides = split_sides(labellers) + + if (isTRUE(facet_grid)) { + labels = as.character(labels) + xlabs = facet_titles_side(sub("^(.*?)~.*", "\\1", labels), xnms, sides[["x"]], has_prefix, sep) + ylabs = facet_titles_side(sub("^.*?~(.*)", "\\1", labels), ynms, sides[["y"]], has_prefix, sep) + paste0(xlabs, "~", ylabs) + } else { + facet_titles_side(labels, xnms, sides[["x"]], has_prefix, sep) + } +} + + +## Resolve a per-variable `facet.args` input (`prefix` strings, `labeller` +## functions) into a list with one element per facet variable. +## +## Values can be supplied positionally, in the order the variables appear in the +## `facet` specification -- for a grid that is the formula LHS first, then the +## RHS, i.e. the order the user wrote them rather than the order tinyplot +## happens to store them in. A single value is recycled across every variable. +## +## Alternatively they can be *named* for the variables they apply to, in which +## case order is irrelevant and naming only some of the variables is fine: the +## rest come back as NULL, for the caller to fill in with its own default (the +## variable's own name for `prefix`, no labelling for `labeller`). +match_facet_vars = function(x, vars, arg) { + ## NB: as.list() on a function returns its formals, so single functions have + ## to be wrapped by hand rather than coerced + x = if (is.function(x)) list(x) else if (is.list(x)) x else as.list(x) + nms = names(x) + + if (!is.null(nms) && any(nzchar(nms))) { + if (!all(nzchar(nms))) { + stop("`", arg, "` should be either fully named or fully unnamed.", call. = FALSE) + } + if (length(vars) == 0L) { + stop( + "`", arg, "` cannot be named here, since the facet variable name(s) ", + "could not be determined.", + call. = FALSE + ) + } + unknown = setdiff(nms, vars) + if (length(unknown) > 0L) { + stop( + "`", arg, "` was named for unknown facet variable(s): ", + paste(unknown, collapse = ", "), + ". Available facet variable(s): ", paste(vars, collapse = ", "), ".", + call. = FALSE + ) + } + out = vector("list", length(vars)) + names(out) = vars + out[nms] = x + return(out) + } + + nvars = max(length(vars), 1L) + if (length(x) == 1L) x = rep(x, nvars) + if (length(x) != nvars) { + stop( + "`", arg, "` should be a single value, or one per facet variable (", + nvars, " here). Alternatively, name the values for the facet ", + "variable(s) they apply to.", + call. = FALSE + ) + } + return(x) +} + + +## Workhorse for facet_titles(): label and/or prefix one side of a facet title. +## +## The single-variable case (by far the most common) hands the values to +## tinylabel() untouched, i.e. still numeric, Date, etc., so that class-specific +## labellers work. Multi-variable sides have to be split into their components +## first, which means going through character; the labeller is then applied down +## each component in turn -- one call per component, not per label -- since +## labellers like "comma" and date formats derive a consistent format from the +## whole vector. Components are then rejoined with `sep`, which is why the split +## has to happen even when nothing is being labelled or prefixed. Any label that +## doesn't split into as many components as we have names for it (e.g. a level +## that itself contains a ":") is left alone. +## +## `labellers` is the per-variable list from normalize_labellers(), i.e. one +## element per component of this side (or NULL for no labelling at all). +facet_titles_side = function(labels, nms, labellers = NULL, has_prefix = FALSE, sep = ":") { + n = length(nms) + labeller_at = function(j) { + if (is.null(labellers) || length(labellers) < j) NULL else labellers[[j]] + } + + if (n <= 1L) { + labels = tinylabel(labels, labeller_at(1L)) + if (isTRUE(has_prefix) && n == 1L) labels = paste0(nms, " = ", labels) + return(labels) + } + + parts = strsplit(as.character(labels), ":", fixed = TRUE) + ok = lengths(parts) == n + out = as.character(labels) + if (any(ok)) { + mat = do.call(rbind, parts[ok]) + cols = lapply( + seq_len(n), + function(j) as.character(tinylabel(mat[, j], labeller_at(j))) + ) + if (isTRUE(has_prefix)) { + cols = Map(function(nm, vals) paste0(nm, " = ", vals), nms, cols) + } + out[ok] = do.call(paste, c(cols, list(sep = sep))) + } + return(out) +} + + # utility function for converting facet formulas into variables get_facet_fml = function(formula, data = NULL) { xfacet = yfacet = NULL @@ -827,6 +1070,10 @@ get_facet_fml = function(formula, data = NULL) { yfacet = if (no_yfacet) NULL else mf[, yfacet_loc] xfacet = mf[, xfacet_loc:NCOL(mf)] + ## variable names, for optional "varname = value" facet titles + xfacet_nms = names(mf)[xfacet_loc:NCOL(mf)] + yfacet_nms = if (no_yfacet) NULL else names(mf)[yfacet_loc] + ## return object xfacet = interaction(xfacet, sep = ":") if (no_yfacet) { @@ -838,6 +1085,7 @@ get_facet_fml = function(formula, data = NULL) { attr(ret, "facet_grid") = TRUE attr(ret, "facet_nrow") = length(unique(yfacet)) } + attr(ret, "facet_names") = list(x = xfacet_nms, y = yfacet_nms) return(ret) } diff --git a/R/sanitize_facet.R b/R/sanitize_facet.R index baddd8a5d..07cc782bb 100644 --- a/R/sanitize_facet.R +++ b/R/sanitize_facet.R @@ -2,21 +2,43 @@ sanitize_facet = function(settings) { env2env( settings, environment(), - c("facet", "by", "null_facet", "facet_attr", "facet_by") + c( + "facet", "by", "null_facet", "facet_attr", "facet_by", + "by_dep", "facet_dep", "legend_args" + ) ) # flag if facet=="by" (i.e., facet matches the grouping variable) facet_by = FALSE + # facet variable name(s), for optional "varname = value" facet titles + facet_names = NULL if (!is.null(facet) && length(facet) == 1 && facet == "by") { by = as.factor(by) ## if by==facet, then both need to be factors facet = by facet_by = TRUE + facet_names = list(x = legend_args[["title"]] %||% by_dep) # same as legend } else if (!is.null(facet) && inherits(facet, "formula")) { facet = get_facet_fml(facet, data = data) if (isTRUE(attr(facet, "facet_grid"))) { facet.args[["nrow"]] = attr(facet, "facet_nrow") } + facet_names = attr(facet, "facet_names") + } else if (!is.null(facet)) { + # names recorded by tinyplot.formula(), else fall back to the deparsed + # input of the default method, e.g. facet = dat$fvar. (When called via + # tinyplot.formula(), facet_dep is just the forwarded "facet" placeholder, + # but that method has already recorded the real name(s).) + facet_names = attr(facet, "facet_names") + if (is.null(facet_names) && !is.null(facet_dep) && !facet_dep %in% c("facet", "NULL")) { + facet_names = list(x = facet_dep) + } } + # The name(s) travel as an attribute so that they survive the handover from + # tinyplot.formula(), but they get stripped here: `facet` flows on into + # `datapoints`, where a stray attribute would break identity checks against + # `by` (e.g. type_violin()). + if (!is.null(facet)) attr(facet, "facet_names") = NULL + facet_attr = attributes(facet) # TODO: better way to restore facet attributes? null_facet = is.null(facet) @@ -24,6 +46,6 @@ sanitize_facet = function(settings) { env2env( environment(), settings, - c("facet", "null_facet", "facet_attr", "facet_by", "by") + c("facet", "null_facet", "facet_attr", "facet_by", "facet_names", "by") ) } diff --git a/R/tinyplot.R b/R/tinyplot.R index 6434c0e85..fca352250 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -80,6 +80,37 @@ #' adjustments are made for certain layouts, and depending on whether the plot #' is framed or not, to reduce excess whitespace. See #' \code{\link[tinyplot]{tpar}} for more details. +#' - `labeller` a formatting function or convenience string passed to +#' \code{\link[tinyplot]{tinylabel}} for adjusting facet titles. This is +#' applied to the underlying facet values, i.e. before any `prefix` (below) is +#' added, and component-wise if the facets are defined over several variables. +#' For the latter case, a (named) list or character vector can be used to +#' format each variable differently, e.g. +#' `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). +#' - `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, +#' pass a custom prefix name as a character value, e.g. `prefix = "Automatic"` +#' yields `"Automatic = 0"`. Like the `labeller` argument (above), a (named) +#' character vector or list can be used to prefix multiple facet variables, +#' e.g. `prefix = c(am = "Automatic", vs = "V-shaped")`, with any omitted +#' variable taking its own (deparsed) name. While not recommended, unnamed +#' values are matched positionally, according to the variable order in the +#' `facet` formula specification. Similarly, a single string is recycled +#' across all facet variables. (Regardless of how they are specified, note +#' that prefixed multi-variable facet titles are separated by commas, rather +#' than the usual colon.) Defaults to the value of `tpar("facet.prefix")`, +#' which is `NULL` (equivalent to `FALSE`, i.e. no prefix). +#' - `sep` a character string separating the individual variables of a +#' multi-variable facet title (ignored otherwise). For example, pass +#' `sep = "\n"` to stack each variable on a separate line. Defaults to the +#' value of `tpar("facet.sep")`, which is `NULL` and implies conditional +#' behaviour depending on whether the combined variables are prefixed (then: +#' `", "`) or the not (then: `":"`) for readability. #' - `cex`, `font`, `col`, `bg`, `border` for adjusting the facet title text #' and background. Default values for these arguments are inherited from #' \code{\link[tinyplot]{tpar}} (where they take a "facet." prefix, e.g. @@ -578,7 +609,8 @@ #' #' tinyplot( #' Temp ~ Day | Summer, -#' facet = ~Month, facet.args = list(nrow = 1), +#' facet = ~Month, +#' facet.args = list(nrow = 1), #' data = aq, #' type = "area", #' palette = "dark2", @@ -589,19 +621,25 @@ #' # Use a two-sided formula to arrange the facet windows in a fixed grid. #' # LHS -> facet rows; RHS -> facet columns #' -#' aq$hot = ifelse(aq$Temp >= 75, "hot", "cold") -#' aq$windy = ifelse(aq$Wind >= 15, "windy", "calm") +#' aq$hot = aq$Temp >= 75 +#' aq$windy = aq$Wind >= 15 #' tinyplot( #' Temp ~ Day, #' facet = windy ~ hot, +#' facet.args = list(prefix = TRUE), # optional #' data = aq #' ) +#' +#' # (Note: The optional `prefix = TRUE` argument prepends the facet variable +#' # names to the strip titles, making for a more informative display here.) +#' #' #' # To add common elements to each facet, use the `draw` argument #' #' tinyplot( #' Temp ~ Day, #' facet = windy ~ hot, +#' facet.args = list(prefix = TRUE), #' data = aq, #' draw = abline(h = 75, lty = 2, col = "hotpink") #' ) @@ -767,6 +805,9 @@ tinyplot.default = function( null.ok = TRUE, name = "facet.args$axes" ) + assert_labeller(facet.args[["labeller"]], name = "facet.args$labeller", list.ok = TRUE) + assert_facet_prefix(facet.args[["prefix"]], name = "facet.args$prefix") + assert_string(facet.args[["sep"]], null.ok = TRUE, name = "facet.args$sep") } # save for tinyplot_add() @@ -1778,6 +1819,11 @@ tinyplot.formula = function( ## placeholder for legend title legend_args = list(x = NULL) + ## deparsed facet input, for optional "varname = value" facet titles (only + ## used if `facet` is passed as data rather than as a formula, since the + ## latter carries its variable names through the model frame below) + facet_dep = if (is.null(substitute(facet))) NULL else deparse1(substitute(facet)) + ## turn facet into a formula if it does not evaluate successfully if (inherits(try(facet, silent = TRUE), "try-error")) { facet = as.formula(paste("~", deparse(substitute(facet)))) @@ -1839,6 +1885,10 @@ tinyplot.formula = function( xtype = if (is.null(xfacet)) "none" else if (ncol(xfacet) == 0L) "empty" else "data" ytype = if (is.null(yfacet)) "none" else if (ncol(yfacet) == 0L) "empty" else "data" + ## variable names, for optional "varname = value" facet titles + xfacet_nms = if (xtype == "data") names(xfacet) else NULL + yfacet_nms = if (ytype == "data") names(yfacet) else NULL + ## turn data frame (if specified) into a single factor if (xtype == "data") xfacet = if (ncol(xfacet) == 1L) xfacet[[1L]] else interaction(xfacet, sep = ":") if (ytype == "data") yfacet = if (ncol(yfacet) == 1L) yfacet[[1L]] else interaction(yfacet, sep = ":") @@ -1849,12 +1899,14 @@ tinyplot.formula = function( } else { if (xtype %in% c("none", "empty")) { facet = yfacet + facet_nms = list(x = yfacet_nms) if (xtype == "empty") { if (is.null(facet.args)) facet.args = list() if (is.null(facet.args[["nrow"]])) facet.args[["nrow"]] = length(unique(yfacet)) } } else if (ytype %in% c("none", "empty")) { facet = xfacet + facet_nms = list(x = xfacet_nms) if (ytype == "empty") { if (is.null(facet.args)) facet.args = list() if (is.null(facet.args[["nrow"]])) facet.args[["nrow"]] = 1L @@ -1863,8 +1915,14 @@ tinyplot.formula = function( facet = interaction(xfacet, yfacet, sep = "~") attr(facet, "facet_grid") = TRUE attr(facet, "facet_nrow") = length(unique(yfacet)) + facet_nms = list(x = xfacet_nms, y = yfacet_nms) } + attr(facet, "facet_names") = facet_nms } + } else if (!is.null(facet) && !inherits(facet, "formula") && + is.null(attr(facet, "facet_names")) && !identical(facet, "by")) { + ## facet passed as data (rather than a formula), e.g. facet = dat$fvar + attr(facet, "facet_names") = list(x = facet_dep) } ## nice axis and legend labels diff --git a/R/tpar.R b/R/tpar.R index 9bb1a4035..dc6884c2b 100644 --- a/R/tpar.R +++ b/R/tpar.R @@ -67,6 +67,9 @@ #' * `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.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`. #' * `file.res`: Numeric specifying the resolution (in dots per square inch) of any plot that is written to disk in bitmap format (i.e., PNG or JPEG) using the `tinyplot(..., file = X)` argument. Defaults to `300`. #' * `file.width`: Numeric specifying the width (in inches) of any plot that is written to disk using the `tinyplot(..., file = X)` argument. Defaults to `7`. @@ -258,6 +261,9 @@ known_tpar = c( "facet.cex", "facet.col", "facet.font", + "facet.labeller", + "facet.prefix", + "facet.sep", "file.height", "file.res", "file.width", @@ -314,6 +320,9 @@ assert_tpar = function(.tpar) { assert_numeric(.tpar[["facet.font"]], len = 1, null.ok = TRUE, name = "facet.font") assert_numeric(.tpar[["facet.cex"]], len = 1, null.ok = TRUE, name = "facet.cex") assert_choice(.tpar[["facet.axes"]], c("all", "outer", "none"), null.ok = TRUE, name = "facet.axes") + assert_labeller(.tpar[["facet.labeller"]], name = "facet.labeller", list.ok = TRUE) + assert_facet_prefix(.tpar[["facet.prefix"]], name = "facet.prefix") + assert_string(.tpar[["facet.sep"]], null.ok = TRUE, name = "facet.sep") assert_numeric(.tpar[["side.sub"]], len = 1, null.ok = TRUE, name = "side.sub") assert_string(.tpar[["grid.bg"]], null.ok = TRUE, name = "grid.bg") assert_numeric(.tpar[["fmar"]], len = 4, null.ok = TRUE, name = "fmar") @@ -381,6 +390,9 @@ init_tpar = function(rm_hook = FALSE) { .tpar$facet.col = if (is.null(getOption("tinyplot_facet.col"))) NULL else getOption("tinyplot_facet.col") .tpar$facet.bg = if (is.null(getOption("tinyplot_facet.bg"))) NULL else getOption("tinyplot_facet.bg") .tpar$facet.border = if (is.null(getOption("tinyplot_facet.border"))) NA else getOption("tinyplot_facet.border") + .tpar$facet.labeller = if (is.null(getOption("tinyplot_facet.labeller"))) NULL else getOption("tinyplot_facet.labeller") + .tpar$facet.prefix = if (is.null(getOption("tinyplot_facet.prefix"))) NULL else getOption("tinyplot_facet.prefix") + .tpar$facet.sep = if (is.null(getOption("tinyplot_facet.sep"))) NULL else getOption("tinyplot_facet.sep") # Plot grid .tpar$grid = if (is.null(getOption("tinyplot_grid"))) FALSE else as.logical(getOption("tinyplot_grid")) diff --git a/man/tinyplot.Rd b/man/tinyplot.Rd index 11a792a47..6e0473587 100644 --- a/man/tinyplot.Rd +++ b/man/tinyplot.Rd @@ -211,6 +211,37 @@ their global \code{\link[tinyplot]{tpar}} settings. Note some automatic adjustments are made for certain layouts, and depending on whether the plot is framed or not, to reduce excess whitespace. See \code{\link[tinyplot]{tpar}} for more details. +\item \code{labeller} a formatting function or convenience string passed to +\code{\link[tinyplot]{tinylabel}} for adjusting facet titles. This is +applied to the underlying facet values, i.e. before any \code{prefix} (below) is +added, and component-wise if the facets are defined over several variables. +For the latter case, a (named) list or character vector can be used to +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). +\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, +pass a custom prefix name as a character value, e.g. \code{prefix = "Automatic"} +yields \code{"Automatic = 0"}. Like the \code{labeller} argument (above), a (named) +character vector or list can be used to prefix multiple facet variables, +e.g. \code{prefix = c(am = "Automatic", vs = "V-shaped")}, with any omitted +variable taking its own (deparsed) name. While not recommended, unnamed +values are matched positionally, according to the variable order in the +\code{facet} formula specification. Similarly, a single string is recycled +across all facet variables. (Regardless of how they are specified, note +that prefixed multi-variable facet titles are separated by commas, rather +than the usual colon.) Defaults to the value of \code{tpar("facet.prefix")}, +which is \code{NULL} (equivalent to \code{FALSE}, i.e. no prefix). +\item \code{sep} a character string separating the individual variables of a +multi-variable facet title (ignored otherwise). For example, pass +\code{sep = "\\n"} to stack each variable on a separate line. Defaults to the +value of \code{tpar("facet.sep")}, which is \code{NULL} and implies conditional +behaviour depending on whether the combined variables are prefixed (then: +\code{", "}) or the not (then: \code{":"}) for readability. \item \code{cex}, \code{font}, \code{col}, \code{bg}, \code{border} for adjusting the facet title text and background. Default values for these arguments are inherited from \code{\link[tinyplot]{tpar}} (where they take a "facet." prefix, e.g. @@ -759,7 +790,8 @@ tinyplot( tinyplot( Temp ~ Day | Summer, - facet = ~Month, facet.args = list(nrow = 1), + facet = ~Month, + facet.args = list(nrow = 1), data = aq, type = "area", palette = "dark2", @@ -770,19 +802,25 @@ tinyplot( # Use a two-sided formula to arrange the facet windows in a fixed grid. # LHS -> facet rows; RHS -> facet columns -aq$hot = ifelse(aq$Temp >= 75, "hot", "cold") -aq$windy = ifelse(aq$Wind >= 15, "windy", "calm") +aq$hot = aq$Temp >= 75 +aq$windy = aq$Wind >= 15 tinyplot( Temp ~ Day, facet = windy ~ hot, + facet.args = list(prefix = TRUE), # optional data = aq ) +# (Note: The optional `prefix = TRUE` argument prepends the facet variable +# names to the strip titles, making for a more informative display here.) + + # To add common elements to each facet, use the `draw` argument tinyplot( Temp ~ Day, facet = windy ~ hot, + facet.args = list(prefix = TRUE), data = aq, draw = abline(h = 75, lty = 2, col = "hotpink") ) diff --git a/man/tpar.Rd b/man/tpar.Rd index 4139bf170..47ebd4a4a 100644 --- a/man/tpar.Rd +++ b/man/tpar.Rd @@ -78,6 +78,9 @@ 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.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}. \item \code{file.res}: Numeric specifying the resolution (in dots per square inch) of any plot that is written to disk in bitmap format (i.e., PNG or JPEG) using the \code{tinyplot(..., file = X)} argument. Defaults to \code{300}. \item \code{file.width}: Numeric specifying the width (in inches) of any plot that is written to disk using the \code{tinyplot(..., file = X)} argument. Defaults to \code{7}. diff --git a/vignettes/introduction.qmd b/vignettes/introduction.qmd index ac251a97e..ce0e7e903 100644 --- a/vignettes/introduction.qmd +++ b/vignettes/introduction.qmd @@ -35,8 +35,8 @@ library(tinyplot) aq = transform( airquality, Month = factor(month.abb[Month], levels = month.abb[5:9]), - hot = ifelse(Temp>=75, "hot", "cold"), - windy = ifelse(Wind>=15, "windy", "calm") + hot = Temp>=75, + windy = Wind>=15 ) ``` @@ -371,7 +371,7 @@ argument to fit a logistic regression. ```{r ribbon_pred_glm} tinyplot( - I(hot == "hot") ~ Wind, aq, + hot ~ Wind, aq, # type = "glm", ## default is gaussian type = type_glm(family = binomial), main = "Logit model: Temps above 75 °F", @@ -417,11 +417,15 @@ tinyplot( ) ``` -To customize facets, simply pass a list of named arguments through the companion -`facet.args` argument. Customization options include: override the default -"square" facet window arrangement; allow free-scaled axes so that the limits of -each individual facet are drawn independently; adjust the padding (margin) -between individual facets; change the facet title text and background; etc. +The companion `facet.args` argument accepts a list of (sub-)arguments for +customizing your facets. Options include: `nrow`/`ncol` to override the default +"square" facet window arrangement; `free` to allow free-scaled axes so that the +limits of each individual facet are drawn independently; `fmar` to adjust the +padding (margin) between individual facets; `labeller` to format the facet +titles (see `?tinylabel`); `prefix` to prepend the facet variable name to the +titles; as well as the regular text-based parameters like `cex`, `font`, `col`, +`bg`, and `border` to change the facet title text and background. + Here is a simple example where we (1) arrange the facets in a single row and (2) change the facet strip text and background fill. @@ -444,10 +448,16 @@ facets in a fixed grid layout. Here is a simple, if contrived, example. ```{r facet_grid} tinyplot( Temp ~ Day, data = aq, - facet = windy ~ hot + facet = windy ~ hot, + facet.args = list(prefix = TRUE) # optional ) ``` +Note that we used the `prefix = TRUE` argument to prepend the facet variable +names to the strip titles. This is entirely optional, but makes for a more +informative display since our two faceting variables are logical and would +othewise just display `TRUE`/`FALSE` without any context. + ## Layers In many contexts, it is convenient to build plots step-by-step, adding layers @@ -510,7 +520,7 @@ simplified version of our facet grid example from earlier. ```{r draw_simple} tinyplot( Temp ~ Day, data = aq, - facet = windy ~ hot, + facet = windy ~ hot, facet.args = list(prefix = TRUE), # draw a horizontal (threshold) line in each facet using the abline function draw = abline(h = 75, lty = 2) ) From 47ef03469056c82d16cb6544a9dd39cbb806d04e Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Thu, 20 Aug 2026 15:20:49 -0700 Subject: [PATCH 2/5] tests --- .../tinytest/_tinysnapshot/facet_labeller.svg | 140 +++++++++ .../_tinysnapshot/facet_labeller_list.svg | 276 ++++++++++++++++++ .../facet_multiline_titles_grid.svg | 212 ++++++++++++++ inst/tinytest/_tinysnapshot/facet_prefix.svg | 140 +++++++++ .../_tinysnapshot/facet_prefix_custom.svg | 140 +++++++++ .../_tinysnapshot/facet_prefix_grid.svg | 208 +++++++++++++ .../_tinysnapshot/facet_prefix_tpar.svg | 140 +++++++++ .../facet_prefix_tpar_override.svg | 140 +++++++++ .../_tinysnapshot/facet_sep_newline.svg | 212 ++++++++++++++ .../facet_sep_newline_prefix.svg | 212 ++++++++++++++ inst/tinytest/test-facet.R | 103 +++++++ 11 files changed, 1923 insertions(+) create mode 100644 inst/tinytest/_tinysnapshot/facet_labeller.svg create mode 100644 inst/tinytest/_tinysnapshot/facet_labeller_list.svg create mode 100644 inst/tinytest/_tinysnapshot/facet_multiline_titles_grid.svg create mode 100644 inst/tinytest/_tinysnapshot/facet_prefix.svg create mode 100644 inst/tinytest/_tinysnapshot/facet_prefix_custom.svg create mode 100644 inst/tinytest/_tinysnapshot/facet_prefix_grid.svg create mode 100644 inst/tinytest/_tinysnapshot/facet_prefix_tpar.svg create mode 100644 inst/tinytest/_tinysnapshot/facet_prefix_tpar_override.svg create mode 100644 inst/tinytest/_tinysnapshot/facet_sep_newline.svg create mode 100644 inst/tinytest/_tinysnapshot/facet_sep_newline_prefix.svg diff --git a/inst/tinytest/_tinysnapshot/facet_labeller.svg b/inst/tinytest/_tinysnapshot/facet_labeller.svg new file mode 100644 index 000000000..905c83c62 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/facet_labeller.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + +wt +mpg + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +Share = 0% + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +Share = 25% + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/facet_labeller_list.svg b/inst/tinytest/_tinysnapshot/facet_labeller_list.svg new file mode 100644 index 000000000..d712e04a5 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/facet_labeller_list.svg @@ -0,0 +1,276 @@ + + + + + + + + + + + + + +wt +mpg + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +vs = 0%, gear = G3 + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +vs = 25%, gear = G3 + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +vs = 0%, gear = G4 + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +vs = 25%, gear = G4 + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +vs = 0%, gear = G5 + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +vs = 25%, gear = G5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/facet_multiline_titles_grid.svg b/inst/tinytest/_tinysnapshot/facet_multiline_titles_grid.svg new file mode 100644 index 000000000..af6a13eb6 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/facet_multiline_titles_grid.svg @@ -0,0 +1,212 @@ + + + + + + + + + + + + + +wt +mpg + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +level +0 + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +level +1 + +level +0 + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +level +1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/facet_prefix.svg b/inst/tinytest/_tinysnapshot/facet_prefix.svg new file mode 100644 index 000000000..a1b5cc7c9 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/facet_prefix.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + +wt +mpg + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +vs = 0 + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +vs = 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/facet_prefix_custom.svg b/inst/tinytest/_tinysnapshot/facet_prefix_custom.svg new file mode 100644 index 000000000..8e889c59d --- /dev/null +++ b/inst/tinytest/_tinysnapshot/facet_prefix_custom.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + +wt +mpg + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +Engine = 0 + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +Engine = 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/facet_prefix_grid.svg b/inst/tinytest/_tinysnapshot/facet_prefix_grid.svg new file mode 100644 index 000000000..a1646a4c3 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/facet_prefix_grid.svg @@ -0,0 +1,208 @@ + + + + + + + + + + + + + +wt +mpg + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +Transmission = 0 + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +Transmission = 1 + +Engine = 0 + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +Engine = 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/facet_prefix_tpar.svg b/inst/tinytest/_tinysnapshot/facet_prefix_tpar.svg new file mode 100644 index 000000000..a1b5cc7c9 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/facet_prefix_tpar.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + +wt +mpg + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +vs = 0 + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +vs = 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/facet_prefix_tpar_override.svg b/inst/tinytest/_tinysnapshot/facet_prefix_tpar_override.svg new file mode 100644 index 000000000..27c6a182b --- /dev/null +++ b/inst/tinytest/_tinysnapshot/facet_prefix_tpar_override.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + +wt +mpg + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +0 + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/facet_sep_newline.svg b/inst/tinytest/_tinysnapshot/facet_sep_newline.svg new file mode 100644 index 000000000..da01cf540 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/facet_sep_newline.svg @@ -0,0 +1,212 @@ + + + + + + + + + + + + + +wt +mpg + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +0 +0 + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +1 +0 + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +0 +1 + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +1 +1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/facet_sep_newline_prefix.svg b/inst/tinytest/_tinysnapshot/facet_sep_newline_prefix.svg new file mode 100644 index 000000000..6b75b7e8c --- /dev/null +++ b/inst/tinytest/_tinysnapshot/facet_sep_newline_prefix.svg @@ -0,0 +1,212 @@ + + + + + + + + + + + + + +wt +mpg + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +am = 0 +vs = 0 + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +am = 1 +vs = 0 + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +am = 0 +vs = 1 + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +am = 1 +vs = 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/test-facet.R b/inst/tinytest/test-facet.R index e3c5523ff..89622ce69 100644 --- a/inst/tinytest/test-facet.R +++ b/inst/tinytest/test-facet.R @@ -663,6 +663,109 @@ f = function() { } expect_snapshot_plot(f, label = "facet_free_categorical_yaxis") +# +## facet title prefixes (#295) + +# `prefix = TRUE` prepends the (deparsed) facet variable name, e.g. "vs = 0" +# instead of a bare "0". +f = function() { + tinyplot( + mpg ~ wt, data = mtcars, + facet = ~vs, facet.args = list(prefix = TRUE) + ) +} +expect_snapshot_plot(f, label = "facet_prefix") + +# A character `prefix` supplies a custom name instead of the variable name. +f = function() { + tinyplot( + mpg ~ wt, data = mtcars, + facet = ~vs, facet.args = list(prefix = "Engine") + ) +} +expect_snapshot_plot(f, label = "facet_prefix_custom") + +# Facet grids prefix both strips, and a character vector names each variable in +# turn, ordered as they appear in the facet formula (LHS first). +f = function() { + tinyplot( + mpg ~ wt, data = mtcars, + facet = vs ~ am, facet.args = list(prefix = c("Engine", "Transmission")) + ) +} +expect_snapshot_plot(f, label = "facet_prefix_grid") + +# `sep` separates the individual variables of a multi-variable title. A newline +# stacks them, which also has to be reserved for in the facet strip margins. +f = function() { + tinyplot( + mpg ~ wt, data = mtcars, + facet = ~am + vs, facet.args = list(sep = "\n") + ) +} +expect_snapshot_plot(f, label = "facet_sep_newline") + +# ... and it applies to prefixed titles just the same +f = function() { + tinyplot( + mpg ~ wt, data = mtcars, + facet = ~am + vs, facet.args = list(prefix = TRUE, sep = "\n") + ) +} +expect_snapshot_plot(f, label = "facet_sep_newline_prefix") + +# Multi-line titles on a facet grid: the rotated RHS titles need the extra +# margin width, else they (and their background rects) overflow the figure +# region. (Sourced from a labeller here, since a grid with one variable per +# side has no `sep` to apply.) +f = function() { + tinyplot( + mpg ~ wt, data = mtcars, + facet = vs ~ am, + facet.args = list( + labeller = function(x) paste0("level\n", x), + bg = "grey90", border = "black" + ) + ) +} +expect_snapshot_plot(f, label = "facet_multiline_titles_grid") + +# `labeller` formats the facet values themselves, ahead of any prefix. +f = function() { + tinyplot( + mpg ~ wt, data = transform(mtcars, vs = vs / 4), + facet = ~vs, facet.args = list(labeller = "percent", prefix = "Share") + ) +} +expect_snapshot_plot(f, label = "facet_labeller") + +# A list of labellers formats each facet variable separately. +f = function() { + d = transform(mtcars, vs = vs / 4, gear = paste0("g", gear)) + tinyplot( + mpg ~ wt, data = d, facet = ~vs + gear, + facet.args = list(labeller = list("percent", toupper), prefix = TRUE) + ) +} +expect_snapshot_plot(f, label = "facet_labeller_list") + +# Global fallback via tpar (also makes it available to themes) +f = function() { + tpar(facet.prefix = TRUE) + on.exit(tpar(facet.prefix = NULL)) + tinyplot(mpg ~ wt, data = mtcars, facet = ~vs) +} +expect_snapshot_plot(f, label = "facet_prefix_tpar") + +# ... but a per-call `facet.args$prefix` wins over the global default +f = function() { + tpar(facet.prefix = TRUE) + on.exit(tpar(facet.prefix = NULL)) + tinyplot(mpg ~ wt, data = mtcars, facet = ~vs, facet.args = list(prefix = FALSE)) +} +expect_snapshot_plot(f, label = "facet_prefix_tpar_override") + + # # restore original par settings # From c7a712dea316220e21800205ea8911b07d027a6f Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Thu, 20 Aug 2026 16:09:56 -0700 Subject: [PATCH 3/5] news --- NEWS.md | 47 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/NEWS.md b/NEWS.md index 33c0724df..ff6a4a6f7 100644 --- a/NEWS.md +++ b/NEWS.md @@ -33,6 +33,39 @@ where the formatting is also better._ that the first row sits at the top (again matching `heatmap()`); pass an explicit `ylim` to override. (#677 @grantmcdermott) +#### Facet improvements + +The top-level `facet.args` list argument gains several new (sub-)arguments that +enable finer control and customization of faceted plots: + +- `axes`: gives explicit control over which facets draw their own axes: `"all"`, + `"outer"` (drop redundant interior axes), or `"none"`. Previously this was + only achievable as a side effect of `frame.plot = FALSE`, so `axes = "outer"` + now allows redundant axes to be dropped while _keeping_ the facet frames. Left + unspecified, the behaviour is still inferred from whether the plot is framed. + (#661, #673 @grantmcdermott) + - Themes with L-shaped axes (`"classic"`, `"socviz"`, `"tufte"`, and + `"float"`) now default to `facet.axes = "outer"`, so that they drop the + redundant interior axes of faceted plots. +- `labeller`: for formatting facet titles via `tinylabel()`. Accepts the usual + mix of convenience keywords (symbols) known to `tinylabel()`, or formatting + functions. A (named) vector or list can be used to separately format + multi-variable facets, e.g. `labeller = c(country = toupper, size = ",")`. + (#295 @grantmcdermott) +- `prefix`: for adding an informative prefix to facet titles. In its simplest + form, `prefix = TRUE` prepends the facet variable name, e.g. `"vs = 0"` and + `"vs = 1"` (rather than just `"0"` and `"1"`). Like `labeller` above, + multi-variable facets can be prefixed separately via a (named) vector or list, + e.g. `prefix = c(am = "Automatic", vs = "V-shaped")`. (#295 @grantmcdermott) +- `sep`: controls how the individual variables of a multi-variable facet title + are separated, e.g. use `sep = "\n"` to stack on separate lines rather than + concatenating via the default `":"`. (#295 @grantmcdermott) + +Note that each of these `facet.args` arguments is paired with an equivalent +`tpar(facet.)` parameter. For example, call `tpar(facet.axes = "outer")` +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")`. + #### Other new features - `type_points()`, `type_lines()`, `type_errorbar()`, and `type_pointrange()` @@ -54,20 +87,6 @@ where the formatting is also better._ 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) -- New `axes` argument for `facet.args`, giving explicit control over which - facets draw their own axes: `"all"`, `"outer"` (drop redundant interior - axes), or `"none"`. Previously this was only achievable as a side effect of - `frame.plot = FALSE`, so `facet.args = list(axes = "outer")` now allows - redundant axes to be dropped while _keeping_ the facet frames. - (#661, #673 @grantmcdermott) - - The same behaviour can be set globally via the new `facet.axes` parameter - (note the reverse order), e.g. `tpar(facet.axes = "outer")`, which also - makes it available to themes. A per-call `facet.args = list(axes = ...)` - takes precedence over the global setting, which in turn takes precedence - over the old frame-based inference. - - Accordingly, themes with L-shaped axes (`"classic"`, `"socviz"`, `"tufte"`, - and `"float"`) now default to `facet.axes = "outer"` so that they drop the - redundant interior axes of faceted plots. - `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 From 0f6f6e6bf20f3e962ea091adcc527eeff220837d Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Thu, 20 Aug 2026 16:11:19 -0700 Subject: [PATCH 4/5] preemptive pr no. --- NEWS.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/NEWS.md b/NEWS.md index ff6a4a6f7..a3852e96d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -51,15 +51,15 @@ enable finer control and customization of faceted plots: mix of convenience keywords (symbols) known to `tinylabel()`, or formatting functions. A (named) vector or list can be used to separately format multi-variable facets, e.g. `labeller = c(country = toupper, size = ",")`. - (#295 @grantmcdermott) + (#684 @grantmcdermott) - `prefix`: for adding an informative prefix to facet titles. In its simplest form, `prefix = TRUE` prepends the facet variable name, e.g. `"vs = 0"` and `"vs = 1"` (rather than just `"0"` and `"1"`). Like `labeller` above, multi-variable facets can be prefixed separately via a (named) vector or list, - e.g. `prefix = c(am = "Automatic", vs = "V-shaped")`. (#295 @grantmcdermott) + e.g. `prefix = c(am = "Automatic", vs = "V-shaped")`. (#684 @grantmcdermott) - `sep`: controls how the individual variables of a multi-variable facet title are separated, e.g. use `sep = "\n"` to stack on separate lines rather than - concatenating via the default `":"`. (#295 @grantmcdermott) + concatenating via the default `":"`. (#684 @grantmcdermott) Note that each of these `facet.args` arguments is paired with an equivalent `tpar(facet.)` parameter. For example, call `tpar(facet.axes = "outer")` From 2806bed57861bd2f2fefb012d713e76e8de5d28b Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Thu, 20 Aug 2026 16:46:32 -0700 Subject: [PATCH 5/5] fix grid labeller gotcha --- R/facet.R | 117 +++++--- R/sanitize_facet.R | 32 ++- R/tinyplot.R | 18 +- .../_tinysnapshot/facet_titles_combined.svg | 256 ++++++++++++++++++ inst/tinytest/test-facet.R | 24 ++ 5 files changed, 392 insertions(+), 55 deletions(-) create mode 100644 inst/tinytest/_tinysnapshot/facet_titles_combined.svg diff --git a/R/facet.R b/R/facet.R index c8e3a0261..2d09f0651 100644 --- a/R/facet.R +++ b/R/facet.R @@ -754,7 +754,7 @@ facet_layout = function(settings) { facets, labeller = facet.args[["labeller"]] %||% .tpar[["facet.labeller"]], prefix = facet.args[["prefix"]] %||% .tpar[["facet.prefix"]], - facet_names = settings$facet_names, + facet_vars = settings$facet_vars, facet_grid = isTRUE(attr(facet, "facet_grid")), sep = facet.args[["sep"]] %||% .tpar[["facet.sep"]] ) @@ -821,10 +821,10 @@ facet_layout = function(settings) { ## and `sep` entries of `facet.args` in ?tinyplot. ## ## `labels` are the raw facet titles as computed in facet_layout(), i.e. the -## levels (or sorted unique values) of the facet variable. `facet_names` is the -## list that sanitize_facet() resolves for the plot: an "x" element (names for -## the regular / top strip labels) and, for facet grids, a "y" element (names -## for the right strip labels). +## levels (or sorted unique values) of the facet variable. `facet_vars` is the +## list that sanitize_facet() resolves for the plot: an "x" element (the +## variables behind the regular / top strip labels) and, for facet grids, a "y" +## element (those behind the right strip labels). ## ## Composite titles are taken apart before either step is applied, so that a ## labeller sees the individual facet *values* rather than the glued-together @@ -851,7 +851,7 @@ facet_titles = function( labels, labeller = NULL, prefix = NULL, - facet_names = NULL, + facet_vars = NULL, facet_grid = FALSE, sep = NULL) { has_prefix = !(is.null(prefix) || isFALSE(prefix)) @@ -867,23 +867,24 @@ facet_titles = function( ) } - xvars = facet_names[["x"]] - yvars = facet_names[["y"]] - nx = length(xvars) - ny = length(yvars) - ## Facet variables in *formula* order, i.e. the order the user wrote them: for - ## a grid the LHS (drawn as the right-hand strips) comes first, since - ## get_facet_fml() swaps the sides internally to plot rowwise. Splitting a - ## per-variable input back out therefore takes the y side off the front. - vars = c(yvars, xvars) + ## `facet_vars` holds one entry per side: each variable's levels, keyed by the + ## variable's name. Flattened here into *formula* order, i.e. the order the + ## user wrote them -- for a grid the LHS (drawn as the right-hand strips) + ## comes first, since get_facet_fml() swaps the sides internally to plot + ## rowwise. Splitting a per-variable input back out takes the y side off the + ## front to match. + nx = length(facet_vars[["x"]]) + ny = length(facet_vars[["y"]]) + lvls = c(facet_vars[["y"]], facet_vars[["x"]]) + vars = names(lvls) split_sides = function(x) { if (is.null(x)) return(list(x = NULL, y = NULL)) if (length(vars) == 0L) return(list(x = x, y = x)) list(x = x[ny + seq_len(nx)], y = x[seq_len(ny)]) } - xnms = xvars - ynms = yvars + xnms = names(facet_vars[["x"]]) + ynms = names(facet_vars[["y"]]) if (has_prefix) { ## Also guards the internal entry point: anything that is neither a flag nor ## name(s) used to fall through the branches below and be silently ignored, @@ -922,18 +923,60 @@ facet_titles = function( match_facet_vars(labeller, vars, "facet.args$labeller") } sides = split_sides(labellers) + lvl_sides = split_sides(lvls) if (isTRUE(facet_grid)) { labels = as.character(labels) - xlabs = facet_titles_side(sub("^(.*?)~.*", "\\1", labels), xnms, sides[["x"]], has_prefix, sep) - ylabs = facet_titles_side(sub("^.*?~(.*)", "\\1", labels), ynms, sides[["y"]], has_prefix, sep) + xlabs = facet_titles_side( + sub("^(.*?)~.*", "\\1", labels), xnms, sides[["x"]], has_prefix, sep, + lvl_sides[["x"]] + ) + ylabs = facet_titles_side( + sub("^.*?~(.*)", "\\1", labels), ynms, sides[["y"]], has_prefix, sep, + lvl_sides[["y"]] + ) paste0(xlabs, "~", ylabs) } else { - facet_titles_side(labels, xnms, sides[["x"]], has_prefix, sep) + facet_titles_side(labels, xnms, sides[["x"]], has_prefix, sep, lvl_sides[["x"]]) } } +## Map the components of a composite facet title back onto the values they came +## from. Splitting a title necessarily works on strings, since interaction() +## flattened the variables into level labels upstream -- which would otherwise +## hand a labeller "0" where an unsplit, single-variable facet hands it 0, so +## that e.g. `labeller = as.logical` yields NA on the one and FALSE on the other +## (#295). Restoring from the variable's own values keeps the two consistent. +## +## Only character input is restored (an unsplit facet still holds its original +## values), and only when every component maps cleanly, so a level that defies +## the round trip is left as the string it already was. +restore_facet_values = function(x, lvls) { + if (is.null(lvls) || !is.character(x)) return(x) + idx = match(x, as.character(lvls)) + if (anyNA(idx)) return(x) + return(lvls[idx]) +} + + +## A facet variable's distinct values, in the order that interaction() lays its +## levels out. Kept alongside the variable names so that facet_titles_side() can +## map a component of a composite title back to the value it came from; see +## restore_facet_values() there. +facet_var_levels = function(v) { + if (is.factor(v)) levels(v) else sort(unique(v)) +} + + +## The same, as the single-variable list that `facet_vars` expects. +facet_var_list = function(v, name) { + out = list(facet_var_levels(v)) + names(out) = name + return(out) +} + + ## Resolve a per-variable `facet.args` input (`prefix` strings, `labeller` ## functions) into a list with one element per facet variable. ## @@ -1005,16 +1048,23 @@ match_facet_vars = function(x, vars, arg) { ## doesn't split into as many components as we have names for it (e.g. a level ## that itself contains a ":") is left alone. ## -## `labellers` is the per-variable list from normalize_labellers(), i.e. one -## element per component of this side (or NULL for no labelling at all). -facet_titles_side = function(labels, nms, labellers = NULL, has_prefix = FALSE, sep = ":") { +## `labellers` is the per-variable list from match_facet_vars(), i.e. one element +## per component of this side (or NULL for no labelling at all). `levels` is the +## matching list of each variable's own values, used to undo the stringification +## that splitting a composite title imposes; see restore_facet_values(). +facet_titles_side = function( + labels, + nms, + labellers = NULL, + has_prefix = FALSE, + sep = ":", + levels = NULL) { n = length(nms) - labeller_at = function(j) { - if (is.null(labellers) || length(labellers) < j) NULL else labellers[[j]] - } + at = function(x, j) if (is.null(x) || length(x) < j) NULL else x[[j]] if (n <= 1L) { - labels = tinylabel(labels, labeller_at(1L)) + labels = restore_facet_values(labels, at(levels, 1L)) + labels = tinylabel(labels, at(labellers, 1L)) if (isTRUE(has_prefix) && n == 1L) labels = paste0(nms, " = ", labels) return(labels) } @@ -1026,7 +1076,10 @@ facet_titles_side = function(labels, nms, labellers = NULL, has_prefix = FALSE, mat = do.call(rbind, parts[ok]) cols = lapply( seq_len(n), - function(j) as.character(tinylabel(mat[, j], labeller_at(j))) + function(j) { + vals = restore_facet_values(mat[, j], at(levels, j)) + as.character(tinylabel(vals, at(labellers, j))) + } ) if (isTRUE(has_prefix)) { cols = Map(function(nm, vals) paste0(nm, " = ", vals), nms, cols) @@ -1070,9 +1123,9 @@ get_facet_fml = function(formula, data = NULL) { yfacet = if (no_yfacet) NULL else mf[, yfacet_loc] xfacet = mf[, xfacet_loc:NCOL(mf)] - ## variable names, for optional "varname = value" facet titles - xfacet_nms = names(mf)[xfacet_loc:NCOL(mf)] - yfacet_nms = if (no_yfacet) NULL else names(mf)[yfacet_loc] + ## each facet variable's levels, keyed by its name; see facet_titles() + xfacet_vars = lapply(mf[xfacet_loc:NCOL(mf)], facet_var_levels) + yfacet_vars = if (no_yfacet) NULL else lapply(mf[yfacet_loc], facet_var_levels) ## return object xfacet = interaction(xfacet, sep = ":") @@ -1085,7 +1138,7 @@ get_facet_fml = function(formula, data = NULL) { attr(ret, "facet_grid") = TRUE attr(ret, "facet_nrow") = length(unique(yfacet)) } - attr(ret, "facet_names") = list(x = xfacet_nms, y = yfacet_nms) + attr(ret, "facet_vars") = list(x = xfacet_vars, y = yfacet_vars) return(ret) } diff --git a/R/sanitize_facet.R b/R/sanitize_facet.R index 07cc782bb..d540efbcd 100644 --- a/R/sanitize_facet.R +++ b/R/sanitize_facet.R @@ -10,42 +10,46 @@ sanitize_facet = function(settings) { # flag if facet=="by" (i.e., facet matches the grouping variable) facet_by = FALSE - # facet variable name(s), for optional "varname = value" facet titles - facet_names = NULL + # the facet variable(s) behind each side of the titles: a list of levels, + # keyed by variable name. The names drive the optional "varname = value" + # prefixes, and the levels let facet_titles() restore a value's type after + # splitting a composite title apart. See facet_var_list(). + facet_vars = NULL if (!is.null(facet) && length(facet) == 1 && facet == "by") { by = as.factor(by) ## if by==facet, then both need to be factors facet = by facet_by = TRUE - facet_names = list(x = legend_args[["title"]] %||% by_dep) # same as legend + # facet titles inherit the "by" variable name (same as the legend title) + facet_vars = list(x = facet_var_list(by, legend_args[["title"]] %||% by_dep)) } else if (!is.null(facet) && inherits(facet, "formula")) { facet = get_facet_fml(facet, data = data) if (isTRUE(attr(facet, "facet_grid"))) { facet.args[["nrow"]] = attr(facet, "facet_nrow") } - facet_names = attr(facet, "facet_names") + facet_vars = attr(facet, "facet_vars") } else if (!is.null(facet)) { - # names recorded by tinyplot.formula(), else fall back to the deparsed - # input of the default method, e.g. facet = dat$fvar. (When called via + # recorded by tinyplot.formula(), else fall back to the deparsed input of + # the default method, e.g. facet = dat$fvar. (When called via # tinyplot.formula(), facet_dep is just the forwarded "facet" placeholder, - # but that method has already recorded the real name(s).) - facet_names = attr(facet, "facet_names") - if (is.null(facet_names) && !is.null(facet_dep) && !facet_dep %in% c("facet", "NULL")) { - facet_names = list(x = facet_dep) + # but that method has already recorded the real name.) + facet_vars = attr(facet, "facet_vars") + if (is.null(facet_vars) && !is.null(facet_dep) && !facet_dep %in% c("facet", "NULL")) { + facet_vars = list(x = facet_var_list(facet, facet_dep)) } } - # The name(s) travel as an attribute so that they survive the handover from + # The variables travel as an attribute so that they survive the handover from # tinyplot.formula(), but they get stripped here: `facet` flows on into # `datapoints`, where a stray attribute would break identity checks against # `by` (e.g. type_violin()). - if (!is.null(facet)) attr(facet, "facet_names") = NULL + if (!is.null(facet)) attr(facet, "facet_vars") = NULL facet_attr = attributes(facet) # TODO: better way to restore facet attributes? null_facet = is.null(facet) - + # update settings env2env( environment(), settings, - c("facet", "null_facet", "facet_attr", "facet_by", "facet_names", "by") + c("facet", "null_facet", "facet_attr", "facet_by", "facet_vars", "by") ) } diff --git a/R/tinyplot.R b/R/tinyplot.R index fca352250..36c26c435 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -1885,9 +1885,9 @@ tinyplot.formula = function( xtype = if (is.null(xfacet)) "none" else if (ncol(xfacet) == 0L) "empty" else "data" ytype = if (is.null(yfacet)) "none" else if (ncol(yfacet) == 0L) "empty" else "data" - ## variable names, for optional "varname = value" facet titles - xfacet_nms = if (xtype == "data") names(xfacet) else NULL - yfacet_nms = if (ytype == "data") names(yfacet) else NULL + ## each facet variable's levels, keyed by its name; see facet_titles() + xfacet_vars = if (xtype == "data") lapply(xfacet, facet_var_levels) else NULL + yfacet_vars = if (ytype == "data") lapply(yfacet, facet_var_levels) else NULL ## turn data frame (if specified) into a single factor if (xtype == "data") xfacet = if (ncol(xfacet) == 1L) xfacet[[1L]] else interaction(xfacet, sep = ":") @@ -1899,14 +1899,14 @@ tinyplot.formula = function( } else { if (xtype %in% c("none", "empty")) { facet = yfacet - facet_nms = list(x = yfacet_nms) + fvars = list(x = yfacet_vars) if (xtype == "empty") { if (is.null(facet.args)) facet.args = list() if (is.null(facet.args[["nrow"]])) facet.args[["nrow"]] = length(unique(yfacet)) } } else if (ytype %in% c("none", "empty")) { facet = xfacet - facet_nms = list(x = xfacet_nms) + fvars = list(x = xfacet_vars) if (ytype == "empty") { if (is.null(facet.args)) facet.args = list() if (is.null(facet.args[["nrow"]])) facet.args[["nrow"]] = 1L @@ -1915,14 +1915,14 @@ tinyplot.formula = function( facet = interaction(xfacet, yfacet, sep = "~") attr(facet, "facet_grid") = TRUE attr(facet, "facet_nrow") = length(unique(yfacet)) - facet_nms = list(x = xfacet_nms, y = yfacet_nms) + fvars = list(x = xfacet_vars, y = yfacet_vars) } - attr(facet, "facet_names") = facet_nms + attr(facet, "facet_vars") = fvars } } else if (!is.null(facet) && !inherits(facet, "formula") && - is.null(attr(facet, "facet_names")) && !identical(facet, "by")) { + is.null(attr(facet, "facet_vars")) && !identical(facet, "by")) { ## facet passed as data (rather than a formula), e.g. facet = dat$fvar - attr(facet, "facet_names") = list(x = facet_dep) + attr(facet, "facet_vars") = list(x = facet_var_list(facet, facet_dep)) } ## nice axis and legend labels diff --git a/inst/tinytest/_tinysnapshot/facet_titles_combined.svg b/inst/tinytest/_tinysnapshot/facet_titles_combined.svg new file mode 100644 index 000000000..aaf90d0df --- /dev/null +++ b/inst/tinytest/_tinysnapshot/facet_titles_combined.svg @@ -0,0 +1,256 @@ + + + + + + + + + + + + + +wt +mpg + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +Automatic = FALSE +V-shape = FALSE + + + + + + + + + + + + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +Automatic = TRUE +V-shape = FALSE + + + + + + + + + + + + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +Automatic = FALSE +V-shape = TRUE + + + + + + + + + + + + + + + + + + + + + + + + + + +2 +3 +4 +5 + + + + + + +10 +15 +20 +25 +30 + +Automatic = TRUE +V-shape = TRUE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/test-facet.R b/inst/tinytest/test-facet.R index 89622ce69..ce6ef1159 100644 --- a/inst/tinytest/test-facet.R +++ b/inst/tinytest/test-facet.R @@ -749,6 +749,30 @@ f = function() { } expect_snapshot_plot(f, label = "facet_labeller_list") +# 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 +# the strings that the composite title has to be split on -- `as.logical(0)` is +# FALSE, whereas `as.logical("0")` is NA -- which is what keeps multi-variable +# and grid facets agreeing with single-variable ones. +f = function() { + # NB: this file coerces `mtcars$am` to a factor up top, and a factor's values + # *are* its level strings, so use the numeric original here -- otherwise the + # labeller has no type to preserve and NAs are the correct answer. + d = transform(mtcars, am = as.numeric(as.character(am))) + tinyplot( + mpg ~ wt, data = d, + facet = ~am:vs, + facet.args = list( + prefix = list("vs" = "V-shape", "am" = "Automatic"), + labeller = as.logical, + sep = "\n" + ), + theme = "clean" + ) +} +expect_snapshot_plot(f, label = "facet_titles_combined") + # Global fallback via tpar (also makes it available to themes) f = function() { tpar(facet.prefix = TRUE)