From 426a054d5b0f3756607f33ad59ab23c464c55b55 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 28 Jun 2026 21:57:39 -0700 Subject: [PATCH 01/18] lighten argument --- R/by_aesthetics.R | 98 +++++++++++++++++++++++++++++++++++------------ R/tinyplot.R | 1 + R/type_barplot.R | 13 +++++-- R/type_boxplot.R | 12 ++++-- R/type_violin.R | 14 +++++-- 5 files changed, 104 insertions(+), 34 deletions(-) diff --git a/R/by_aesthetics.R b/R/by_aesthetics.R index 2595b63f..2b4ca23d 100755 --- a/R/by_aesthetics.R +++ b/R/by_aesthetics.R @@ -8,7 +8,7 @@ by_aesthetics = function(settings) { environment(), c( "datapoints", "by", "type", "null_by", "pch", "bg", "lty", "lwd", - "bubble", "cex", "alpha", "col", "fill", "ribbon.alpha" + "bubble", "cex", "alpha", "col", "fill", "ribbon.alpha", "lighten" ) ) @@ -59,6 +59,7 @@ by_aesthetics = function(settings) { type = type, by = by, ribbon.alpha = ribbon.alpha, + lighten = lighten, adjustcolor = adjustcolor ) @@ -83,6 +84,17 @@ apply_alpha = function(cols, alpha, adjustcolor) { adjustcolor(cols, alpha.f = alpha) } +# Apply the lighter-but-opaque tint (PR #614) to each colour in a vector. +lighten_fill = function(cols) { + if (is.null(cols)) return(cols) + vapply( + cols, + function(cc) if (is_achromatic(cc)) "lightgray" else seq_palette(cc, n = 3)[3], + character(1), + USE.NAMES = FALSE + ) +} + is_by_keyword = function(x) { is.character(x) && length(x) == 1 && !is.na(x) && identical(x, "by") } @@ -374,15 +386,31 @@ by_col = function(col, palette, alpha, by_ordered, by_continuous, ngrps, adjustc } -by_bg = function(bg, fill, col, palette, alpha, by_ordered, by_continuous, ngrps, type, by, ribbon.alpha, adjustcolor) { +by_bg = function(bg, fill, col, palette, alpha, by_ordered, by_continuous, ngrps, type, by, ribbon.alpha, lighten, adjustcolor) { + lighten = if (is.null(lighten)) TRUE else isTRUE(lighten) if (is.null(bg) && !is.null(fill)) bg = fill + + # A numeric bg/fill in [0,1] is an alpha *request*, not a colour: remember it + # as the transparency to layer on top of the resolved (and possibly lightened) + # base fill, and defer to the "by" palette for the base colour itself. Applying + # alpha last -- after lightening -- means a fill request blends a light tint + # toward the background rather than darkening a saturated base (issue #646). if (!is.null(bg) && length(bg) == 1 && is.numeric(bg) && bg >= 0 && bg <= 1) { alpha = bg bg = "by" } - if (!is.null(bg) && length(bg) == 1 && is_by_keyword(bg)) { - ordered = if (is.null(by_ordered)) FALSE else by_ordered - gradient = if (is.null(by_continuous)) FALSE else by_continuous + + ordered = if (is.null(by_ordered)) FALSE else by_ordered + gradient = if (is.null(by_continuous)) FALSE else by_continuous + + # Whether the base fill comes from palette resolution (the "by" keyword, incl. + # a numeric fill request) or the single-group default, vs. an explicit colour + # such as `fill = "bisque"`. Only the former are eligible for lightening; an + # explicit colour is always honoured verbatim. + bg_from_palette = !is.null(bg) && length(bg) == 1 && is_by_keyword(bg) + palette_fill = bg_from_palette + + if (bg_from_palette) { pal_theme = if (ordered || gradient) { get_tpar("palette.sequential", default = NULL) } else { @@ -393,44 +421,64 @@ by_bg = function(bg, fill, col, palette, alpha, by_ordered, by_continuous, ngrps if (!ordered && !gradient) { pal_theme = resolve_col_default(get_tpar("col.default", default = NULL), pal_theme)[["palette"]] } + # Resolve opaque (alpha = 1); any user alpha is layered on below. bg = resolve_palette_colors( palette = palette, theme_palette = pal_theme, ngrps = ngrps, - ordered = if (is.null(by_ordered)) FALSE else by_ordered, - gradient = if (is.null(by_continuous)) FALSE else by_continuous, - alpha = if (is.null(alpha)) 1 else alpha, + ordered = ordered, + gradient = gradient, + alpha = 1, adjustcolor = adjustcolor ) - } else if (length(bg) != ngrps) { - bg = rep(bg, ngrps) - } - if (type == "ribbon" || (type == "boxplot" && !is.null(by))) { - if (!is.null(bg)) { - bg = adjustcolor(bg, ribbon.alpha) - } else if (!is.null(col)) { - bg = adjustcolor(col, ribbon.alpha) - } } else if (ngrps == 1L && is.null(bg) && type %in% c("boxplot", "violin", "barplot", "histogram")) { # Single-group fill tracks the theme's *default* colour (col.default -> # palette[1] -> black) so that themed box/violin/bar/histogram plots match # their own multi-group counterparts. We resolve this default independently # of `col` so that a user-supplied outline colour (e.g. `col = "white"`) # doesn't bleed into the fill. - fill_base = by_col( + bg = by_col( col = NULL, palette = palette, alpha = 1, by_ordered = by_ordered, by_continuous = by_continuous, ngrps = 1L, adjustcolor = adjustcolor ) - # For a *chromatic* default the fill is a lighter-but-opaque tint of that - # colour (via seq_palette, the same HCL ramp used for ridge fills and legend + palette_fill = TRUE + } else if (length(bg) != ngrps) { + bg = rep(bg, ngrps) + } + + # The solid categorical area types take the lighter-but-opaque tint, but only + # for qualitative groupings: lightening a sequential/continuous ramp would + # crush the contrast between adjacent levels. + area_light_types = c("barplot", "boxplot", "violin") + light_ok = lighten && type %in% area_light_types && !ordered && !gradient + + if (palette_fill) { + # For a *chromatic* base the fill is a lighter-but-opaque tint of that colour + # (via seq_palette, the same HCL ramp used for ridge fills and legend # swatches), so it reads cleanly over grid lines unlike alpha blending. For - # an *achromatic* default (typically black, e.g. the "bw"/"classic"/"ipsum" + # an *achromatic* base (typically black, e.g. the "bw"/"classic"/"ipsum" # themes and the plain default) seq_palette's light endpoint can't reach the # neutral "lightgray" used by the no-theme path and base R's hist()/boxplot() - # -- so we use that literal directly, keeping all black-default single-group - # fills consistent regardless of whether a theme palette happens to be set. - achromatic = is_achromatic(fill_base) - bg = if (achromatic) "lightgray" else seq_palette(fill_base, n = 3)[3] + # -- so we fall back to that literal. Histograms only lighten in the + # single-group case; grouped histograms keep their semi-transparent fill so + # that overlapping distributions remain legible (issue #646). + if (light_ok) { + bg = lighten_fill(bg) + } else if (type == "histogram" && ngrps == 1L) { + bg = lighten_fill(bg) + } + # Layer any user-supplied transparency on top of the (lightened) base fill. + if (!is.null(alpha)) bg = apply_alpha(bg, alpha, adjustcolor) + } + + # Legacy semi-transparent fill: ribbon-type plots, and the opt-out path for + # grouped boxplots (`lighten = FALSE`), which keeps the historical look. + if (type == "ribbon" || (type == "boxplot" && !is.null(by) && !light_ok)) { + if (!is.null(bg)) { + bg = adjustcolor(bg, ribbon.alpha) + } else if (!is.null(col)) { + bg = adjustcolor(col, ribbon.alpha) + } } bg diff --git a/R/tinyplot.R b/R/tinyplot.R index 80376aa4..46dbe47e 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -908,6 +908,7 @@ tinyplot.default = function( dodge = NULL, dots = dots, flip = flip, + lighten = TRUE, # area types may override via their type_data() fn group_offsets = NULL, offsets_axis = NULL, sub = sub, diff --git a/R/type_barplot.R b/R/type_barplot.R index 15ac0b5c..5e4a231d 100644 --- a/R/type_barplot.R +++ b/R/type_barplot.R @@ -41,6 +41,10 @@ #' @param drop.zeros logical. Should bars with zero height be dropped? If set #' to `FALSE` (default) a zero height bar is still drawn for which the border #' lines will still be visible. +#' @param lighten logical. Should the fills use a lighter, opaque tint of the +#' 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. #' #' @examples #' # Basic examples of frequency tables (without y variable) @@ -131,9 +135,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) { +type_barplot = function(width = 5/6, beside = FALSE, center = FALSE, offset = NULL, FUN = NULL, xlevels = 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), + data = data_barplot(width = width, beside = beside, center = center, offset = offset, FUN = FUN, xlevels = xlevels, xaxlabels = xaxlabels, drop.zeros = drop.zeros, lighten = lighten), draw = draw_rect(), name = "barplot" ) @@ -142,7 +146,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) { +data_barplot = function(width = 5/6, beside = FALSE, center = FALSE, offset = NULL, FUN = NULL, xlevels = NULL, xaxlabels = NULL, drop.zeros = FALSE, lighten = TRUE) { fun = function(settings, ...) { env2env( settings, @@ -277,6 +281,9 @@ data_barplot = function(width = 5/6, beside = FALSE, center = FALSE, offset = NU range(c(stack_range, off_range), na.rm = TRUE) * 1.02 } + ## fill lightening (see by_bg) + settings[["lighten"]] = lighten + ## default color palette ngrps = length(unique(datapoints$by)) if (ngrps == 1L && null_palette) { diff --git a/R/type_boxplot.R b/R/type_boxplot.R index 51504fe6..52f2f604 100644 --- a/R/type_boxplot.R +++ b/R/type_boxplot.R @@ -7,6 +7,10 @@ #' numeric. #' #' @inheritParams graphics::boxplot +#' @param lighten logical. Should the fills use a lighter, opaque tint of the +#' 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. #' @examples #' # "boxplot" type convenience string #' tinyplot(count ~ spray, data = InsectSprays, type = "boxplot") @@ -32,7 +36,8 @@ type_boxplot = function( outline = TRUE, boxwex = 0.8, staplewex = 0.5, - outwex = 0.5) { + outwex = 0.5, + lighten = TRUE) { out = list( draw = draw_boxplot( range = range, @@ -43,7 +48,7 @@ type_boxplot = function( boxwex = boxwex, staplewex = staplewex, outwex = outwex), - data = data_boxplot(boxwex = boxwex), + data = data_boxplot(boxwex = boxwex, lighten = lighten), name = "boxplot" ) class(out) = "tinyplot_type" @@ -89,9 +94,10 @@ draw_boxplot = function(range, width, varwidth, notch, outline, boxwex, staplewe -data_boxplot = function(boxwex = 0.8) { +data_boxplot = function(boxwex = 0.8, lighten = TRUE) { fun = function(settings, ...) { env2env(settings, environment(), c("datapoints", "by", "facet", "null_facet", "null_palette", "x", "col", "bg", "null_by")) + settings[["lighten"]] = lighten # Convert x to factor if it's not already datapoints$x = as.factor(datapoints$x) diff --git a/R/type_violin.R b/R/type_violin.R index 1c7847e7..08112154 100644 --- a/R/type_violin.R +++ b/R/type_violin.R @@ -9,6 +9,10 @@ #' range of the data. Default is `FALSE`. #' @param width numeric (ideally in the range `[0, 1]`, although this isn't #' enforced) giving the normalized width of the individual violins. +#' @param lighten logical. Should the fills use a lighter, opaque tint of the +#' 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. #' @inherit stats::density details #' @details See [`type_density`] for more details and considerations related to #' bandwidth selection and kernel types. @@ -58,7 +62,8 @@ type_violin = function( n = 512, # more args from density here? trim = FALSE, - width = 0.9 + width = 0.9, + lighten = TRUE ) { kernel = match.arg(kernel, c("gaussian", "epanechnikov", "rectangular", "triangular", "biweight", "cosine", "optcosine")) if (is.logical(joint.bw)) { @@ -67,7 +72,8 @@ type_violin = function( joint.bw = match.arg(joint.bw, c("mean", "full", "none")) out = list( data = data_violin(bw = bw, adjust = adjust, kernel = kernel, n = n, - joint.bw = joint.bw, trim = trim, width = width), + joint.bw = joint.bw, trim = trim, width = width, + lighten = lighten), # draw = NULL, # name = "polygon" draw = draw_polygon(density = NULL), @@ -78,9 +84,11 @@ type_violin = function( } data_violin = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, - joint.bw = "none", trim = FALSE, width = 0.9) { + joint.bw = "none", trim = FALSE, width = 0.9, + lighten = TRUE) { fun = function(settings, ...) { env2env(settings, environment(), c("datapoints", "by", "null_palette", "facet", "ylab", "col", "bg", "log", "null_by", "null_facet")) + settings[["lighten"]] = lighten # Handle ordering based on by and facet variables From 916c9707b6b6ec1145e7317dd5af056d23e901be Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 28 Jun 2026 21:59:27 -0700 Subject: [PATCH 02/18] tidy code order --- R/by_aesthetics.R | 766 +++++++++++++++++++++++----------------------- 1 file changed, 383 insertions(+), 383 deletions(-) diff --git a/R/by_aesthetics.R b/R/by_aesthetics.R index 2b4ca23d..e266c19f 100755 --- a/R/by_aesthetics.R +++ b/R/by_aesthetics.R @@ -1,5 +1,5 @@ # -## orchestration function ----- +## main orchestration function ----- # by_aesthetics = function(settings) { @@ -73,134 +73,417 @@ by_aesthetics = function(settings) { # -## helper functions ----- +## subsidiary functions ----- # +by_col = function(col, palette, alpha, by_ordered, by_continuous, ngrps, adjustcolor) { + ordered = if (is.null(by_ordered)) FALSE else by_ordered + gradient = if (is.null(by_continuous)) FALSE else by_continuous + assert_logical(ordered) + assert_logical(gradient) -apply_alpha = function(cols, alpha, adjustcolor) { - if (is.null(cols) || is.null(alpha) || identical(alpha, 0)) { + if (is.null(alpha)) alpha = 1 + if (gradient) ngrps = 100L + + if (is_by_keyword(col)) col = NULL + + pal_theme = if (ordered || gradient) { + get_tpar("palette.sequential", default = NULL) + } else { + get_tpar("palette.qualitative", default = NULL) + } + + # Resolve a (possibly relative) col.default against the qualitative palette. + # A relative (negative) index also drops the chosen colour from the grouped + # palette, so e.g. `col.default = -1` uses the palette's leading colour for + # single-group displays and the palette-minus-that-colour for grouped ones + # (see resolve_col_default). col.default only applies to qualitative displays. + if (!ordered && !gradient) { + cd = resolve_col_default(get_tpar("col.default", default = NULL), pal_theme) + pal_theme = cd[["palette"]] + # Single-group default colour (theme-settable via tpar). When NULL, defer to + # the usual palette resolution below (first qualitative colour, or palette()[1]). + if (is.null(col) && ngrps == 1L) { + col = cd[["col_default"]] + } + } + + cols = resolve_manual_colors(col, ngrps, gradient, ordered, alpha, adjustcolor) + if (!is.null(cols)) { return(cols) } - adjustcolor(cols, alpha.f = alpha) -} -# Apply the lighter-but-opaque tint (PR #614) to each colour in a vector. -lighten_fill = function(cols) { - if (is.null(cols)) return(cols) - vapply( - cols, - function(cc) if (is_achromatic(cc)) "lightgray" else seq_palette(cc, n = 3)[3], - character(1), - USE.NAMES = FALSE + cols = resolve_palette_colors( + palette = palette, + theme_palette = pal_theme, + ngrps = ngrps, + ordered = ordered, + gradient = gradient, + alpha = alpha, + adjustcolor = adjustcolor ) -} -is_by_keyword = function(x) { - is.character(x) && length(x) == 1 && !is.na(x) && identical(x, "by") + cols } -warn_recycle_colors = function(ncols, ngrps) { - warning( - "\nFewer colours (", ncols, ") provided than there are groups (", - ngrps, "). Recycling to make up the shortfall." - ) -} -expand_colors_to_ngrps = function(values, ngrps, gradient) { - if (length(values) == 1) { - return(rep(values, ngrps)) - } - if (length(values) >= ngrps) { - return(values[seq_len(ngrps)]) - } - if (gradient) { - return(colorRampPalette(colors = values, alpha = TRUE)(ngrps)) - } - warn_recycle_colors(length(values), ngrps) - rep_len(values, ngrps) -} +by_bg = function(bg, fill, col, palette, alpha, by_ordered, by_continuous, ngrps, type, by, ribbon.alpha, lighten, adjustcolor) { + lighten = if (is.null(lighten)) TRUE else isTRUE(lighten) + if (is.null(bg) && !is.null(fill)) bg = fill -assert_len_1_or_ngrps = function(x, ngrps, name, allow_character = FALSE) { - types = if (allow_character) "numeric or character" else "numeric" - valid_type = is.numeric(x) || (allow_character && is.character(x)) - valid = is.atomic(x) && is.vector(x) && valid_type && (length(x) == 1 || length(x) == ngrps) - if (!valid) { - stop(sprintf("`%s` must be `NULL`, or a %s vector of length 1 or %s.", name, types, ngrps), call. = FALSE) + # A numeric bg/fill in [0,1] is an alpha *request*, not a colour: remember it + # as the transparency to layer on top of the resolved (and possibly lightened) + # base fill, and defer to the "by" palette for the base colour itself. Applying + # alpha last -- after lightening -- means a fill request blends a light tint + # toward the background rather than darkening a saturated base (issue #646). + if (!is.null(bg) && length(bg) == 1 && is.numeric(bg) && bg >= 0 && bg <= 1) { + alpha = bg + bg = "by" } -} -match_palette_name = function(name, candidates) { - normalize = function(x) tolower(gsub("[-, _, \\,, (, ), \\ , \\.]", "", x)) - charmatch(normalize(name), normalize(candidates)) -} + ordered = if (is.null(by_ordered)) FALSE else by_ordered + gradient = if (is.null(by_continuous)) FALSE else by_continuous -## Resolve a palette spec to its full concrete colour vector (or NULL if it -## can't be resolved to a discrete set, e.g. a continuous hcl palette). Used to -## support relative `col.default` indices. -resolve_palette_to_colors = function(palette) { - if (is.null(palette)) return(NULL) - if (is.character(palette) && length(palette) > 1) return(unname(palette)) - if (is.character(palette) && length(palette) == 1) { - discrete_pals = palette.pals() - idx = match_palette_name(palette, discrete_pals) - if (!is.na(idx) && idx >= 1L) { - return(unname(palette.colors(palette = discrete_pals[idx]))) + # Whether the base fill comes from palette resolution (the "by" keyword, incl. + # a numeric fill request) or the single-group default, vs. an explicit colour + # such as `fill = "bisque"`. Only the former are eligible for lightening; an + # explicit colour is always honoured verbatim. + bg_from_palette = !is.null(bg) && length(bg) == 1 && is_by_keyword(bg) + palette_fill = bg_from_palette + + if (bg_from_palette) { + pal_theme = if (ordered || gradient) { + get_tpar("palette.sequential", default = NULL) + } else { + get_tpar("palette.qualitative", default = NULL) + } + # Mirror by_col(): a relative col.default trims the grouped palette, so + # grouped fills stay in step with grouped line colours. + if (!ordered && !gradient) { + pal_theme = resolve_col_default(get_tpar("col.default", default = NULL), pal_theme)[["palette"]] } + # Resolve opaque (alpha = 1); any user alpha is layered on below. + bg = resolve_palette_colors( + palette = palette, + theme_palette = pal_theme, + ngrps = ngrps, + ordered = ordered, + gradient = gradient, + alpha = 1, + adjustcolor = adjustcolor + ) + } else if (ngrps == 1L && is.null(bg) && type %in% c("boxplot", "violin", "barplot", "histogram")) { + # Single-group fill tracks the theme's *default* colour (col.default -> + # palette[1] -> black) so that themed box/violin/bar/histogram plots match + # their own multi-group counterparts. We resolve this default independently + # of `col` so that a user-supplied outline colour (e.g. `col = "white"`) + # doesn't bleed into the fill. + bg = by_col( + col = NULL, palette = palette, alpha = 1, by_ordered = by_ordered, + by_continuous = by_continuous, ngrps = 1L, adjustcolor = adjustcolor + ) + palette_fill = TRUE + } else if (length(bg) != ngrps) { + bg = rep(bg, ngrps) } - NULL -} -## Resolve a (possibly relative) `col.default` against the qualitative palette. -## `col_default` may be NULL, a character colour, or a numeric index into the -## palette. A negative index additionally *drops* that colour from the palette -## returned for grouped displays. So `col.default = -1` with an "Okabe-Ito" -## palette uses black (the leading colour) for single-group plots and an -## Okabe-Ito-minus-black palette for grouped plots, avoiding the need to keep a -## separate "no-black" palette copy around (#598, #614). Returns a list with the -## resolved single-group `col_default` and the (possibly trimmed) `palette`. -resolve_col_default = function(col_default, palette_spec) { - if (!is.numeric(col_default)) { - return(list(col_default = col_default, palette = palette_spec)) + # The solid categorical area types take the lighter-but-opaque tint, but only + # for qualitative groupings: lightening a sequential/continuous ramp would + # crush the contrast between adjacent levels. + area_light_types = c("barplot", "boxplot", "violin") + light_ok = lighten && type %in% area_light_types && !ordered && !gradient + + if (palette_fill) { + # For a *chromatic* base the fill is a lighter-but-opaque tint of that colour + # (via seq_palette, the same HCL ramp used for ridge fills and legend + # swatches), so it reads cleanly over grid lines unlike alpha blending. For + # an *achromatic* base (typically black, e.g. the "bw"/"classic"/"ipsum" + # themes and the plain default) seq_palette's light endpoint can't reach the + # neutral "lightgray" used by the no-theme path and base R's hist()/boxplot() + # -- so we fall back to that literal. Histograms only lighten in the + # single-group case; grouped histograms keep their semi-transparent fill so + # that overlapping distributions remain legible (issue #646). + if (light_ok) { + bg = lighten_fill(bg) + } else if (type == "histogram" && ngrps == 1L) { + bg = lighten_fill(bg) + } + # Layer any user-supplied transparency on top of the (lightened) base fill. + if (!is.null(alpha)) bg = apply_alpha(bg, alpha, adjustcolor) } - full = resolve_palette_to_colors(palette_spec) - if (is.null(full)) { - return(list(col_default = NULL, palette = palette_spec)) + + # Legacy semi-transparent fill: ribbon-type plots, and the opt-out path for + # grouped boxplots (`lighten = FALSE`), which keeps the historical look. + if (type == "ribbon" || (type == "boxplot" && !is.null(by) && !light_ok)) { + if (!is.null(bg)) { + bg = adjustcolor(bg, ribbon.alpha) + } else if (!is.null(col)) { + bg = adjustcolor(col, ribbon.alpha) + } } - i = as.integer(col_default) - out_col = full[abs(i)] - if (i < 0L) palette_spec = full[-abs(i)] - list(col_default = out_col, palette = palette_spec) + + bg } -## Handle direct color input via `col` arg. Returns colors or NULL if not applicable. -resolve_manual_colors = function(col, ngrps, gradient, ordered, alpha, adjustcolor) { - if (is.null(col) || !is.atomic(col) || !is.vector(col)) { - return(NULL) - } - cols = col - if (length(cols) == 1) { - cols = rep(cols, ngrps) - } else if (length(cols) < ngrps) { - cols = expand_colors_to_ngrps(cols, ngrps, gradient) - } +by_pch = function(ngrps, type, pch = NULL) { + no_pch = FALSE + if (identical(type, "text")) { + pch = rep(15, ngrps) + } else if (!type %in% c("p", "b", "o", "pointrange", "errorbar", "boxplot", "qq")) { + no_pch = TRUE + pch = NULL - # Map numeric indices to palette colors (unless ordered) - if (!ordered && is.numeric(cols)) { - base_pal = palette() - cols = if (ngrps <= length(base_pal)) { - base_pal[cols] - } else { - hcl.colors(max(cols))[cols] + # special "by" convenience keyword + } else if (is_by_keyword(pch)) { + no_pch = TRUE # skip checks below + pch = 1:ngrps + par("pch") - 1 + # correctly recycle if over max pch type + if (max(pch) > 25L) { + pch_below = pch[pch <= 25L] + pch_above = pch[pch > 25L] + pch_above = rep_len(0:25, length(pch_above)) + pch = c(pch_below, pch_above) } + + # return NULL if not a valid point type + } else if (is.null(pch)) { + pch = par("pch") } - if (gradient) cols = rev(cols) - apply_alpha(cols, alpha, adjustcolor) + if (!no_pch) { + assert_len_1_or_ngrps(pch, ngrps, "pch", allow_character = TRUE) + if (length(pch) == 1) pch = rep(pch, ngrps) + } + + return(pch) } -## High-level palette resolution: theme fallback, defaults, then delegate to resolve_palette_spec. -resolve_palette_colors = function(palette, theme_palette, ngrps, ordered, gradient, alpha, adjustcolor) { + +by_lty = function(ngrps, type, lty = NULL) { + # We only care about line types, otherwise return NULL + if (!type %in% c("l", "b", "o", "c", "h", "s", "S", "ribbon", "barplot", "boxplot", "rect", "segments", "qq", "abline", "hline", "vline")) { + lty = NULL + + # special "by" convenience keyword + } else if (is_by_keyword(lty)) { + lty_dict = c("solid", "dashed", "dotted", "dotdash", "longdash", "twodash") + par_lty = par("lty") + + if (!par_lty %in% lty_dict) { + warning( + "\nBespoke lty specifications (i.e., using string combinations) are not ", + "currently supported alongside the lty='by' keyword argument. ", + "Defaulting to 1 and looping from there.\n" + ) + par_lty = 1 + } else { + par_lty = which(par_lty == lty_dict) + } + lty = 1:ngrps + par_lty - 1 + # correctly recycle if over max lty type + if (max(lty) > 6L) { + lty_below = lty[lty <= 6L] + lty_above = lty[lty > 6L] + lty_above = rep_len(1:6, length(lty_above)) + lty = c(lty_below, lty_above) + } + + # NULL -> solid (or default) line + } else if (is.null(lty)) { + if (!identical(type, "boxplot")) { + lty = rep(par("lty"), ngrps) + } + + # atomic vector: sanity check length + } else if (is.atomic(lty) && is.vector(lty)) { + assert_len_1_or_ngrps(lty, ngrps, "lty") + if (length(lty) == 1) lty = rep(lty, ngrps) + } + + lty +} + + +by_lwd = function(ngrps, type, lwd = NULL) { + lwd_base = par("lwd") + lwd_floor = lwd_base / min(5, max((ngrps - 1), 1)) + lwd_ceiling = lwd_base * min(5, ngrps) + + no_lwd = FALSE + # special "by" convenience keyword + if (is_by_keyword(lwd)) { + no_lwd = TRUE # skip checks below + lwd = seq(lwd_floor, lwd_ceiling, length.out = ngrps) + } else if (is.null(lwd)) { + no_lwd = TRUE + lwd = NULL + } + + if (!no_lwd) { + assert_len_1_or_ngrps(lwd, ngrps, "lwd") + if (length(lwd) == 1) lwd = rep(lwd, ngrps) + } + + return(lwd) +} + + +by_cex = function(ngrps, type, bubble = FALSE, cex = NULL) { + no_cex = FALSE + # special "by" convenience keyword + if (is_by_keyword(cex)) { + no_cex = TRUE # skip checks below + cex = rescale_num(c(1:ngrps), to = c(1, 2.5)) + } else if (is.null(cex)) { + no_cex = TRUE + # cex = NULL + # can't leave cex as NULL otherwise JIT cex_fct_adj adjustment in + # draw_legend() won't work later + cex = 1 + cex = rep(cex, ngrps) + } + + # placehodler + if (bubble) no_cex = TRUE + + if (!no_cex) { + assert_len_1_or_ngrps(cex, ngrps, "cex") + if (length(cex) == 1) cex = rep(cex, ngrps) + } + + return(cex) +} + + +# +## helper functions ----- +# + +apply_alpha = function(cols, alpha, adjustcolor) { + if (is.null(cols) || is.null(alpha) || identical(alpha, 0)) { + return(cols) + } + adjustcolor(cols, alpha.f = alpha) +} + +# Apply the lighter-but-opaque tint (PR #614) to each colour in a vector. +lighten_fill = function(cols) { + if (is.null(cols)) return(cols) + vapply( + cols, + function(cc) if (is_achromatic(cc)) "lightgray" else seq_palette(cc, n = 3)[3], + character(1), + USE.NAMES = FALSE + ) +} + +is_by_keyword = function(x) { + is.character(x) && length(x) == 1 && !is.na(x) && identical(x, "by") +} + +warn_recycle_colors = function(ncols, ngrps) { + warning( + "\nFewer colours (", ncols, ") provided than there are groups (", + ngrps, "). Recycling to make up the shortfall." + ) +} + +expand_colors_to_ngrps = function(values, ngrps, gradient) { + if (length(values) == 1) { + return(rep(values, ngrps)) + } + if (length(values) >= ngrps) { + return(values[seq_len(ngrps)]) + } + if (gradient) { + return(colorRampPalette(colors = values, alpha = TRUE)(ngrps)) + } + warn_recycle_colors(length(values), ngrps) + rep_len(values, ngrps) +} + +assert_len_1_or_ngrps = function(x, ngrps, name, allow_character = FALSE) { + types = if (allow_character) "numeric or character" else "numeric" + valid_type = is.numeric(x) || (allow_character && is.character(x)) + valid = is.atomic(x) && is.vector(x) && valid_type && (length(x) == 1 || length(x) == ngrps) + if (!valid) { + stop(sprintf("`%s` must be `NULL`, or a %s vector of length 1 or %s.", name, types, ngrps), call. = FALSE) + } +} + +match_palette_name = function(name, candidates) { + normalize = function(x) tolower(gsub("[-, _, \\,, (, ), \\ , \\.]", "", x)) + charmatch(normalize(name), normalize(candidates)) +} + +## Resolve a palette spec to its full concrete colour vector (or NULL if it +## can't be resolved to a discrete set, e.g. a continuous hcl palette). Used to +## support relative `col.default` indices. +resolve_palette_to_colors = function(palette) { + if (is.null(palette)) return(NULL) + if (is.character(palette) && length(palette) > 1) return(unname(palette)) + if (is.character(palette) && length(palette) == 1) { + discrete_pals = palette.pals() + idx = match_palette_name(palette, discrete_pals) + if (!is.na(idx) && idx >= 1L) { + return(unname(palette.colors(palette = discrete_pals[idx]))) + } + } + NULL +} + +## Resolve a (possibly relative) `col.default` against the qualitative palette. +## `col_default` may be NULL, a character colour, or a numeric index into the +## palette. A negative index additionally *drops* that colour from the palette +## returned for grouped displays. So `col.default = -1` with an "Okabe-Ito" +## palette uses black (the leading colour) for single-group plots and an +## Okabe-Ito-minus-black palette for grouped plots, avoiding the need to keep a +## separate "no-black" palette copy around (#598, #614). Returns a list with the +## resolved single-group `col_default` and the (possibly trimmed) `palette`. +resolve_col_default = function(col_default, palette_spec) { + if (!is.numeric(col_default)) { + return(list(col_default = col_default, palette = palette_spec)) + } + full = resolve_palette_to_colors(palette_spec) + if (is.null(full)) { + return(list(col_default = NULL, palette = palette_spec)) + } + i = as.integer(col_default) + out_col = full[abs(i)] + if (i < 0L) palette_spec = full[-abs(i)] + list(col_default = out_col, palette = palette_spec) +} + +## Handle direct color input via `col` arg. Returns colors or NULL if not applicable. +resolve_manual_colors = function(col, ngrps, gradient, ordered, alpha, adjustcolor) { + if (is.null(col) || !is.atomic(col) || !is.vector(col)) { + return(NULL) + } + + cols = col + if (length(cols) == 1) { + cols = rep(cols, ngrps) + } else if (length(cols) < ngrps) { + cols = expand_colors_to_ngrps(cols, ngrps, gradient) + } + + # Map numeric indices to palette colors (unless ordered) + if (!ordered && is.numeric(cols)) { + base_pal = palette() + cols = if (ngrps <= length(base_pal)) { + base_pal[cols] + } else { + hcl.colors(max(cols))[cols] + } + } + + if (gradient) cols = rev(cols) + apply_alpha(cols, alpha, adjustcolor) +} + +## High-level palette resolution: theme fallback, defaults, then delegate to resolve_palette_spec. +resolve_palette_colors = function(palette, theme_palette, ngrps, ordered, gradient, alpha, adjustcolor) { palette_choice = palette # Pick theme palette if no explicit palette provided @@ -330,286 +613,3 @@ resolve_palette_spec = function(palette, ngrps, gradient, ordered, alpha, adjust apply_alpha(cols, alpha, adjustcolor) } - -# -## subsidiary functions ----- -# - -by_col = function(col, palette, alpha, by_ordered, by_continuous, ngrps, adjustcolor) { - ordered = if (is.null(by_ordered)) FALSE else by_ordered - gradient = if (is.null(by_continuous)) FALSE else by_continuous - assert_logical(ordered) - assert_logical(gradient) - - if (is.null(alpha)) alpha = 1 - if (gradient) ngrps = 100L - - if (is_by_keyword(col)) col = NULL - - pal_theme = if (ordered || gradient) { - get_tpar("palette.sequential", default = NULL) - } else { - get_tpar("palette.qualitative", default = NULL) - } - - # Resolve a (possibly relative) col.default against the qualitative palette. - # A relative (negative) index also drops the chosen colour from the grouped - # palette, so e.g. `col.default = -1` uses the palette's leading colour for - # single-group displays and the palette-minus-that-colour for grouped ones - # (see resolve_col_default). col.default only applies to qualitative displays. - if (!ordered && !gradient) { - cd = resolve_col_default(get_tpar("col.default", default = NULL), pal_theme) - pal_theme = cd[["palette"]] - # Single-group default colour (theme-settable via tpar). When NULL, defer to - # the usual palette resolution below (first qualitative colour, or palette()[1]). - if (is.null(col) && ngrps == 1L) { - col = cd[["col_default"]] - } - } - - cols = resolve_manual_colors(col, ngrps, gradient, ordered, alpha, adjustcolor) - if (!is.null(cols)) { - return(cols) - } - - cols = resolve_palette_colors( - palette = palette, - theme_palette = pal_theme, - ngrps = ngrps, - ordered = ordered, - gradient = gradient, - alpha = alpha, - adjustcolor = adjustcolor - ) - - cols -} - - -by_bg = function(bg, fill, col, palette, alpha, by_ordered, by_continuous, ngrps, type, by, ribbon.alpha, lighten, adjustcolor) { - lighten = if (is.null(lighten)) TRUE else isTRUE(lighten) - if (is.null(bg) && !is.null(fill)) bg = fill - - # A numeric bg/fill in [0,1] is an alpha *request*, not a colour: remember it - # as the transparency to layer on top of the resolved (and possibly lightened) - # base fill, and defer to the "by" palette for the base colour itself. Applying - # alpha last -- after lightening -- means a fill request blends a light tint - # toward the background rather than darkening a saturated base (issue #646). - if (!is.null(bg) && length(bg) == 1 && is.numeric(bg) && bg >= 0 && bg <= 1) { - alpha = bg - bg = "by" - } - - ordered = if (is.null(by_ordered)) FALSE else by_ordered - gradient = if (is.null(by_continuous)) FALSE else by_continuous - - # Whether the base fill comes from palette resolution (the "by" keyword, incl. - # a numeric fill request) or the single-group default, vs. an explicit colour - # such as `fill = "bisque"`. Only the former are eligible for lightening; an - # explicit colour is always honoured verbatim. - bg_from_palette = !is.null(bg) && length(bg) == 1 && is_by_keyword(bg) - palette_fill = bg_from_palette - - if (bg_from_palette) { - pal_theme = if (ordered || gradient) { - get_tpar("palette.sequential", default = NULL) - } else { - get_tpar("palette.qualitative", default = NULL) - } - # Mirror by_col(): a relative col.default trims the grouped palette, so - # grouped fills stay in step with grouped line colours. - if (!ordered && !gradient) { - pal_theme = resolve_col_default(get_tpar("col.default", default = NULL), pal_theme)[["palette"]] - } - # Resolve opaque (alpha = 1); any user alpha is layered on below. - bg = resolve_palette_colors( - palette = palette, - theme_palette = pal_theme, - ngrps = ngrps, - ordered = ordered, - gradient = gradient, - alpha = 1, - adjustcolor = adjustcolor - ) - } else if (ngrps == 1L && is.null(bg) && type %in% c("boxplot", "violin", "barplot", "histogram")) { - # Single-group fill tracks the theme's *default* colour (col.default -> - # palette[1] -> black) so that themed box/violin/bar/histogram plots match - # their own multi-group counterparts. We resolve this default independently - # of `col` so that a user-supplied outline colour (e.g. `col = "white"`) - # doesn't bleed into the fill. - bg = by_col( - col = NULL, palette = palette, alpha = 1, by_ordered = by_ordered, - by_continuous = by_continuous, ngrps = 1L, adjustcolor = adjustcolor - ) - palette_fill = TRUE - } else if (length(bg) != ngrps) { - bg = rep(bg, ngrps) - } - - # The solid categorical area types take the lighter-but-opaque tint, but only - # for qualitative groupings: lightening a sequential/continuous ramp would - # crush the contrast between adjacent levels. - area_light_types = c("barplot", "boxplot", "violin") - light_ok = lighten && type %in% area_light_types && !ordered && !gradient - - if (palette_fill) { - # For a *chromatic* base the fill is a lighter-but-opaque tint of that colour - # (via seq_palette, the same HCL ramp used for ridge fills and legend - # swatches), so it reads cleanly over grid lines unlike alpha blending. For - # an *achromatic* base (typically black, e.g. the "bw"/"classic"/"ipsum" - # themes and the plain default) seq_palette's light endpoint can't reach the - # neutral "lightgray" used by the no-theme path and base R's hist()/boxplot() - # -- so we fall back to that literal. Histograms only lighten in the - # single-group case; grouped histograms keep their semi-transparent fill so - # that overlapping distributions remain legible (issue #646). - if (light_ok) { - bg = lighten_fill(bg) - } else if (type == "histogram" && ngrps == 1L) { - bg = lighten_fill(bg) - } - # Layer any user-supplied transparency on top of the (lightened) base fill. - if (!is.null(alpha)) bg = apply_alpha(bg, alpha, adjustcolor) - } - - # Legacy semi-transparent fill: ribbon-type plots, and the opt-out path for - # grouped boxplots (`lighten = FALSE`), which keeps the historical look. - if (type == "ribbon" || (type == "boxplot" && !is.null(by) && !light_ok)) { - if (!is.null(bg)) { - bg = adjustcolor(bg, ribbon.alpha) - } else if (!is.null(col)) { - bg = adjustcolor(col, ribbon.alpha) - } - } - - bg -} - - -by_pch = function(ngrps, type, pch = NULL) { - no_pch = FALSE - if (identical(type, "text")) { - pch = rep(15, ngrps) - } else if (!type %in% c("p", "b", "o", "pointrange", "errorbar", "boxplot", "qq")) { - no_pch = TRUE - pch = NULL - - # special "by" convenience keyword - } else if (is_by_keyword(pch)) { - no_pch = TRUE # skip checks below - pch = 1:ngrps + par("pch") - 1 - # correctly recycle if over max pch type - if (max(pch) > 25L) { - pch_below = pch[pch <= 25L] - pch_above = pch[pch > 25L] - pch_above = rep_len(0:25, length(pch_above)) - pch = c(pch_below, pch_above) - } - - # return NULL if not a valid point type - } else if (is.null(pch)) { - pch = par("pch") - } - - if (!no_pch) { - assert_len_1_or_ngrps(pch, ngrps, "pch", allow_character = TRUE) - if (length(pch) == 1) pch = rep(pch, ngrps) - } - - return(pch) -} - - -by_lty = function(ngrps, type, lty = NULL) { - # We only care about line types, otherwise return NULL - if (!type %in% c("l", "b", "o", "c", "h", "s", "S", "ribbon", "barplot", "boxplot", "rect", "segments", "qq", "abline", "hline", "vline")) { - lty = NULL - - # special "by" convenience keyword - } else if (is_by_keyword(lty)) { - lty_dict = c("solid", "dashed", "dotted", "dotdash", "longdash", "twodash") - par_lty = par("lty") - - if (!par_lty %in% lty_dict) { - warning( - "\nBespoke lty specifications (i.e., using string combinations) are not ", - "currently supported alongside the lty='by' keyword argument. ", - "Defaulting to 1 and looping from there.\n" - ) - par_lty = 1 - } else { - par_lty = which(par_lty == lty_dict) - } - lty = 1:ngrps + par_lty - 1 - # correctly recycle if over max lty type - if (max(lty) > 6L) { - lty_below = lty[lty <= 6L] - lty_above = lty[lty > 6L] - lty_above = rep_len(1:6, length(lty_above)) - lty = c(lty_below, lty_above) - } - - # NULL -> solid (or default) line - } else if (is.null(lty)) { - if (!identical(type, "boxplot")) { - lty = rep(par("lty"), ngrps) - } - - # atomic vector: sanity check length - } else if (is.atomic(lty) && is.vector(lty)) { - assert_len_1_or_ngrps(lty, ngrps, "lty") - if (length(lty) == 1) lty = rep(lty, ngrps) - } - - lty -} - - -by_lwd = function(ngrps, type, lwd = NULL) { - lwd_base = par("lwd") - lwd_floor = lwd_base / min(5, max((ngrps - 1), 1)) - lwd_ceiling = lwd_base * min(5, ngrps) - - no_lwd = FALSE - # special "by" convenience keyword - if (is_by_keyword(lwd)) { - no_lwd = TRUE # skip checks below - lwd = seq(lwd_floor, lwd_ceiling, length.out = ngrps) - } else if (is.null(lwd)) { - no_lwd = TRUE - lwd = NULL - } - - if (!no_lwd) { - assert_len_1_or_ngrps(lwd, ngrps, "lwd") - if (length(lwd) == 1) lwd = rep(lwd, ngrps) - } - - return(lwd) -} - - -by_cex = function(ngrps, type, bubble = FALSE, cex = NULL) { - no_cex = FALSE - # special "by" convenience keyword - if (is_by_keyword(cex)) { - no_cex = TRUE # skip checks below - cex = rescale_num(c(1:ngrps), to = c(1, 2.5)) - } else if (is.null(cex)) { - no_cex = TRUE - # cex = NULL - # can't leave cex as NULL otherwise JIT cex_fct_adj adjustment in - # draw_legend() won't work later - cex = 1 - cex = rep(cex, ngrps) - } - - # placehodler - if (bubble) no_cex = TRUE - - if (!no_cex) { - assert_len_1_or_ngrps(cex, ngrps, "cex") - if (length(cex) == 1) cex = rep(cex, ngrps) - } - - return(cex) -} From cb757fa1a21b33c7417d1fa5110b8b412e6a670e Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 28 Jun 2026 22:00:21 -0700 Subject: [PATCH 03/18] docs --- man/type_barplot.Rd | 8 +++++++- man/type_boxplot.Rd | 8 +++++++- man/type_violin.Rd | 8 +++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/man/type_barplot.Rd b/man/type_barplot.Rd index c6944d6d..c8ac6915 100644 --- a/man/type_barplot.Rd +++ b/man/type_barplot.Rd @@ -12,7 +12,8 @@ type_barplot( FUN = NULL, xlevels = NULL, xaxlabels = NULL, - drop.zeros = FALSE + drop.zeros = FALSE, + lighten = TRUE ) } \arguments{ @@ -59,6 +60,11 @@ defaulting to the levels of \code{x}.} \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 lines will still be visible.} + +\item{lighten}{logical. Should the fills use a lighter, opaque tint of the +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.} } \description{ Type function for producing barplots. For formulas of type diff --git a/man/type_boxplot.Rd b/man/type_boxplot.Rd index a23a6855..50140bac 100644 --- a/man/type_boxplot.Rd +++ b/man/type_boxplot.Rd @@ -12,7 +12,8 @@ type_boxplot( outline = TRUE, boxwex = 0.8, staplewex = 0.5, - outwex = 0.5 + outwex = 0.5, + lighten = TRUE ) } \arguments{ @@ -47,6 +48,11 @@ type_boxplot( \item{outwex}{outlier line width expansion, proportional to box width.} + +\item{lighten}{logical. Should the fills use a lighter, opaque tint of the +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.} } \description{ Type function for producing box-and-whisker plots. diff --git a/man/type_violin.Rd b/man/type_violin.Rd index 3251f5e8..cbc45d76 100644 --- a/man/type_violin.Rd +++ b/man/type_violin.Rd @@ -12,7 +12,8 @@ type_violin( "cosine", "optcosine"), n = 512, trim = FALSE, - width = 0.9 + width = 0.9, + lighten = TRUE ) } \arguments{ @@ -51,6 +52,11 @@ range of the data. Default is \code{FALSE}.} \item{width}{numeric (ideally in the range \verb{[0, 1]}, although this isn't enforced) giving the normalized width of the individual violins.} + +\item{lighten}{logical. Should the fills use a lighter, opaque tint of the +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.} } \description{ Type function for violin plots, which are an alternative to box From 375adf484ec312cb25adf7291a19081be1c230b0 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 28 Jun 2026 22:01:39 -0700 Subject: [PATCH 04/18] global var --- R/zzz.R | 1 + 1 file changed, 1 insertion(+) diff --git a/R/zzz.R b/R/zzz.R index 6b893367..ef16d3a2 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -50,6 +50,7 @@ "iby", "ifacet", "labels", + "lighten", "lty", "lwd", "nfacet_cols", From 4da4cba8a420041abe770721fed166090f2498c7 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 28 Jun 2026 22:31:45 -0700 Subject: [PATCH 05/18] update tests and snapshots --- .../_tinysnapshot/barplot_aggregation.svg | 84 ++--- .../_tinysnapshot/barplot_custom_xtitle.svg | 44 +-- .../_tinysnapshot/barplot_custom_ytitle.svg | 44 +-- inst/tinytest/_tinysnapshot/barplot_facet.svg | 28 +- .../_tinysnapshot/barplot_facet_free.svg | 28 +- .../_tinysnapshot/barplot_flip_fancy.svg | 36 +-- inst/tinytest/_tinysnapshot/barplot_group.svg | 16 +- .../_tinysnapshot/barplot_group_beside.svg | 16 +- .../barplot_group_lighten_false.svg | 86 +++++ .../barplot_offset_beside_group.svg | 44 +-- .../_tinysnapshot/barplot_offset_stacked.svg | 12 +- .../_tinysnapshot/boxplot_facet_by.svg | 104 +++--- .../_tinysnapshot/boxplot_facet_by_x_same.svg | 12 +- .../tinytest/_tinysnapshot/boxplot_groups.svg | 16 +- .../_tinysnapshot/boxplot_groups_argpass.svg | 16 +- .../boxplot_groups_facets_with_missings.svg | 26 +- .../boxplot_groups_lighten_false.svg | 121 +++++++ .../boxplot_groups_x_equivalent.svg | 12 +- ...oxplot_groups_x_equivalent_legendFALSE.svg | 8 +- .../_tinysnapshot/boxplot_groups_x_same.svg | 12 +- .../boxplot_groups_x_same_legendFALSE.svg | 6 +- ...tinyplot_add_jitter_on_grouped_boxplot.svg | 136 ++++---- .../tinyplot_add_jitter_on_grouped_violin.svg | 136 ++++---- .../tinyplot_add_jitter_on_violin.svg | 300 +++++++++--------- .../_tinysnapshot/violin_by_x_equivalent.svg | 8 +- .../_tinysnapshot/violin_facet_by.svg | 104 +++--- .../_tinysnapshot/violin_facet_x_by_same.svg | 12 +- inst/tinytest/_tinysnapshot/violin_groups.svg | 16 +- .../_tinysnapshot/violin_groups_argpass.svg | 16 +- .../violin_groups_lighten_false.svg | 80 +++++ inst/tinytest/_tinysnapshot/violin_x_by.svg | 6 +- inst/tinytest/test-tinyplot_add.R | 8 +- inst/tinytest/test-type_barplot.R | 26 +- inst/tinytest/test-type_boxplot.R | 10 + inst/tinytest/test-type_violin.R | 13 +- 35 files changed, 979 insertions(+), 663 deletions(-) create mode 100644 inst/tinytest/_tinysnapshot/barplot_group_lighten_false.svg create mode 100644 inst/tinytest/_tinysnapshot/boxplot_groups_lighten_false.svg create mode 100644 inst/tinytest/_tinysnapshot/violin_groups_lighten_false.svg diff --git a/inst/tinytest/_tinysnapshot/barplot_aggregation.svg b/inst/tinytest/_tinysnapshot/barplot_aggregation.svg index 11103164..4945c9e9 100644 --- a/inst/tinytest/_tinysnapshot/barplot_aggregation.svg +++ b/inst/tinytest/_tinysnapshot/barplot_aggregation.svg @@ -26,8 +26,8 @@ - - + + group 1 2 @@ -97,48 +97,48 @@ 2 - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/barplot_custom_xtitle.svg b/inst/tinytest/_tinysnapshot/barplot_custom_xtitle.svg index 527459bb..421ca64f 100644 --- a/inst/tinytest/_tinysnapshot/barplot_custom_xtitle.svg +++ b/inst/tinytest/_tinysnapshot/barplot_custom_xtitle.svg @@ -26,8 +26,8 @@ - - + + grp 0 1 @@ -76,26 +76,26 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/barplot_custom_ytitle.svg b/inst/tinytest/_tinysnapshot/barplot_custom_ytitle.svg index 260dc9a9..61e1ff46 100644 --- a/inst/tinytest/_tinysnapshot/barplot_custom_ytitle.svg +++ b/inst/tinytest/_tinysnapshot/barplot_custom_ytitle.svg @@ -26,8 +26,8 @@ - - + + grp 0 1 @@ -76,26 +76,26 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/barplot_facet.svg b/inst/tinytest/_tinysnapshot/barplot_facet.svg index ee5ee91a..5f098036 100644 --- a/inst/tinytest/_tinysnapshot/barplot_facet.svg +++ b/inst/tinytest/_tinysnapshot/barplot_facet.svg @@ -26,8 +26,8 @@ - - + + vs 0 1 @@ -87,20 +87,20 @@ 1 - - - - - - + + + + + + - - - - - - + + + + + + diff --git a/inst/tinytest/_tinysnapshot/barplot_facet_free.svg b/inst/tinytest/_tinysnapshot/barplot_facet_free.svg index 7477260b..4d5d9365 100644 --- a/inst/tinytest/_tinysnapshot/barplot_facet_free.svg +++ b/inst/tinytest/_tinysnapshot/barplot_facet_free.svg @@ -26,8 +26,8 @@ - - + + vs 0 1 @@ -100,20 +100,20 @@ 1 - - - - - - + + + + + + - - - - - - + + + + + + diff --git a/inst/tinytest/_tinysnapshot/barplot_flip_fancy.svg b/inst/tinytest/_tinysnapshot/barplot_flip_fancy.svg index 94cadd07..333bf6ad 100644 --- a/inst/tinytest/_tinysnapshot/barplot_flip_fancy.svg +++ b/inst/tinytest/_tinysnapshot/barplot_flip_fancy.svg @@ -26,8 +26,8 @@ - - + + Survived No Yes @@ -120,28 +120,28 @@ Crew - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + diff --git a/inst/tinytest/_tinysnapshot/barplot_group.svg b/inst/tinytest/_tinysnapshot/barplot_group.svg index bcea289e..c519a1b8 100644 --- a/inst/tinytest/_tinysnapshot/barplot_group.svg +++ b/inst/tinytest/_tinysnapshot/barplot_group.svg @@ -26,8 +26,8 @@ - - + + vs 0 1 @@ -69,12 +69,12 @@ - - - - - - + + + + + + diff --git a/inst/tinytest/_tinysnapshot/barplot_group_beside.svg b/inst/tinytest/_tinysnapshot/barplot_group_beside.svg index 5d3dc336..6b7f55fc 100644 --- a/inst/tinytest/_tinysnapshot/barplot_group_beside.svg +++ b/inst/tinytest/_tinysnapshot/barplot_group_beside.svg @@ -26,8 +26,8 @@ - - + + vs 0 1 @@ -69,12 +69,12 @@ - - - - - - + + + + + + diff --git a/inst/tinytest/_tinysnapshot/barplot_group_lighten_false.svg b/inst/tinytest/_tinysnapshot/barplot_group_lighten_false.svg new file mode 100644 index 00000000..05db0c70 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/barplot_group_lighten_false.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + +vs +0 +1 + + + + + + + +cyl +Count + + +4 +6 +8 +0 +2 +4 +6 +8 +10 +12 +14 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/barplot_offset_beside_group.svg b/inst/tinytest/_tinysnapshot/barplot_offset_beside_group.svg index 1a7c6108..2a4ba693 100644 --- a/inst/tinytest/_tinysnapshot/barplot_offset_beside_group.svg +++ b/inst/tinytest/_tinysnapshot/barplot_offset_beside_group.svg @@ -26,8 +26,8 @@ - - + + group 1 2 @@ -74,26 +74,26 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/barplot_offset_stacked.svg b/inst/tinytest/_tinysnapshot/barplot_offset_stacked.svg index 2883bb5d..f770424c 100644 --- a/inst/tinytest/_tinysnapshot/barplot_offset_stacked.svg +++ b/inst/tinytest/_tinysnapshot/barplot_offset_stacked.svg @@ -26,8 +26,8 @@ - - + + Survived No Yes @@ -62,10 +62,10 @@ - - - - + + + + diff --git a/inst/tinytest/_tinysnapshot/boxplot_facet_by.svg b/inst/tinytest/_tinysnapshot/boxplot_facet_by.svg index d7ab71de..bedda878 100644 --- a/inst/tinytest/_tinysnapshot/boxplot_facet_by.svg +++ b/inst/tinytest/_tinysnapshot/boxplot_facet_by.svg @@ -26,10 +26,10 @@ - - - - + + + + Diet 1 2 @@ -226,7 +226,7 @@ - + @@ -234,7 +234,7 @@ - + @@ -243,21 +243,21 @@ - + - + - + @@ -265,49 +265,49 @@ - + - + - + - + - + - + - + @@ -316,28 +316,28 @@ - + - + - + - + @@ -345,7 +345,7 @@ - + @@ -354,7 +354,7 @@ - + @@ -363,7 +363,7 @@ - + @@ -371,7 +371,7 @@ - + @@ -379,7 +379,7 @@ - + @@ -388,21 +388,21 @@ - + - + - + @@ -411,7 +411,7 @@ - + @@ -422,14 +422,14 @@ - + - + @@ -437,63 +437,63 @@ - + - + - + - + - + - + - + - + - + @@ -502,28 +502,28 @@ - + - + - + - + @@ -531,7 +531,7 @@ - + @@ -539,7 +539,7 @@ - + @@ -547,7 +547,7 @@ - + @@ -556,35 +556,35 @@ - + - + - + - + - + diff --git a/inst/tinytest/_tinysnapshot/boxplot_facet_by_x_same.svg b/inst/tinytest/_tinysnapshot/boxplot_facet_by_x_same.svg index b1254752..9ab329b4 100644 --- a/inst/tinytest/_tinysnapshot/boxplot_facet_by_x_same.svg +++ b/inst/tinytest/_tinysnapshot/boxplot_facet_by_x_same.svg @@ -26,9 +26,9 @@ - - - + + + gear 3 4 @@ -134,7 +134,7 @@ - + @@ -143,7 +143,7 @@ - + @@ -152,7 +152,7 @@ - + diff --git a/inst/tinytest/_tinysnapshot/boxplot_groups.svg b/inst/tinytest/_tinysnapshot/boxplot_groups.svg index 93510859..275194de 100644 --- a/inst/tinytest/_tinysnapshot/boxplot_groups.svg +++ b/inst/tinytest/_tinysnapshot/boxplot_groups.svg @@ -26,8 +26,8 @@ - - + + supp OJ VC @@ -72,35 +72,35 @@ - + - + - + - + - + @@ -108,7 +108,7 @@ - + diff --git a/inst/tinytest/_tinysnapshot/boxplot_groups_argpass.svg b/inst/tinytest/_tinysnapshot/boxplot_groups_argpass.svg index 4f770701..09cac22e 100644 --- a/inst/tinytest/_tinysnapshot/boxplot_groups_argpass.svg +++ b/inst/tinytest/_tinysnapshot/boxplot_groups_argpass.svg @@ -28,8 +28,8 @@ - - + + Ascorbic acid Orange juice @@ -76,35 +76,35 @@ - + - + - + - + - + @@ -112,7 +112,7 @@ - + diff --git a/inst/tinytest/_tinysnapshot/boxplot_groups_facets_with_missings.svg b/inst/tinytest/_tinysnapshot/boxplot_groups_facets_with_missings.svg index ee62bf0a..d6050182 100644 --- a/inst/tinytest/_tinysnapshot/boxplot_groups_facets_with_missings.svg +++ b/inst/tinytest/_tinysnapshot/boxplot_groups_facets_with_missings.svg @@ -26,9 +26,9 @@ - - - + + + factor(cyl) 4 6 @@ -104,35 +104,35 @@ - + - + - + - + - + @@ -141,35 +141,35 @@ - + - + - + - + - + diff --git a/inst/tinytest/_tinysnapshot/boxplot_groups_lighten_false.svg b/inst/tinytest/_tinysnapshot/boxplot_groups_lighten_false.svg new file mode 100644 index 00000000..044b88a6 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/boxplot_groups_lighten_false.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + +supp +OJ +VC + + + + + + + +dose +len + + +0.5 +1 +2 +5 +10 +15 +20 +25 +30 +35 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/boxplot_groups_x_equivalent.svg b/inst/tinytest/_tinysnapshot/boxplot_groups_x_equivalent.svg index 097e131a..c46f1dac 100644 --- a/inst/tinytest/_tinysnapshot/boxplot_groups_x_equivalent.svg +++ b/inst/tinytest/_tinysnapshot/boxplot_groups_x_equivalent.svg @@ -26,8 +26,8 @@ - - + + player2 female male @@ -66,28 +66,28 @@ - + - + - + - + diff --git a/inst/tinytest/_tinysnapshot/boxplot_groups_x_equivalent_legendFALSE.svg b/inst/tinytest/_tinysnapshot/boxplot_groups_x_equivalent_legendFALSE.svg index 01eb9005..9c1ef9e6 100644 --- a/inst/tinytest/_tinysnapshot/boxplot_groups_x_equivalent_legendFALSE.svg +++ b/inst/tinytest/_tinysnapshot/boxplot_groups_x_equivalent_legendFALSE.svg @@ -52,28 +52,28 @@ - + - + - + - + diff --git a/inst/tinytest/_tinysnapshot/boxplot_groups_x_same.svg b/inst/tinytest/_tinysnapshot/boxplot_groups_x_same.svg index 71732137..b27d4b68 100644 --- a/inst/tinytest/_tinysnapshot/boxplot_groups_x_same.svg +++ b/inst/tinytest/_tinysnapshot/boxplot_groups_x_same.svg @@ -26,9 +26,9 @@ - - - + + + Species setosa versicolor @@ -76,21 +76,21 @@ - + - + - + diff --git a/inst/tinytest/_tinysnapshot/boxplot_groups_x_same_legendFALSE.svg b/inst/tinytest/_tinysnapshot/boxplot_groups_x_same_legendFALSE.svg index d5658723..8d54cfa7 100644 --- a/inst/tinytest/_tinysnapshot/boxplot_groups_x_same_legendFALSE.svg +++ b/inst/tinytest/_tinysnapshot/boxplot_groups_x_same_legendFALSE.svg @@ -60,21 +60,21 @@ - + - + - + diff --git a/inst/tinytest/_tinysnapshot/tinyplot_add_jitter_on_grouped_boxplot.svg b/inst/tinytest/_tinysnapshot/tinyplot_add_jitter_on_grouped_boxplot.svg index 3f8b4aea..0fe33915 100644 --- a/inst/tinytest/_tinysnapshot/tinyplot_add_jitter_on_grouped_boxplot.svg +++ b/inst/tinytest/_tinysnapshot/tinyplot_add_jitter_on_grouped_boxplot.svg @@ -26,8 +26,8 @@ - - + + supp OJ VC @@ -72,35 +72,35 @@ - + - + - + - + - + @@ -108,73 +108,73 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinyplot_add_jitter_on_grouped_violin.svg b/inst/tinytest/_tinysnapshot/tinyplot_add_jitter_on_grouped_violin.svg index d464d24e..f83bef53 100644 --- a/inst/tinytest/_tinysnapshot/tinyplot_add_jitter_on_grouped_violin.svg +++ b/inst/tinytest/_tinysnapshot/tinyplot_add_jitter_on_grouped_violin.svg @@ -26,8 +26,8 @@ - - + + supp OJ VC @@ -68,72 +68,72 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinyplot_add_jitter_on_violin.svg b/inst/tinytest/_tinysnapshot/tinyplot_add_jitter_on_violin.svg index 7cd8fb86..4e5f97f6 100644 --- a/inst/tinytest/_tinysnapshot/tinyplot_add_jitter_on_violin.svg +++ b/inst/tinytest/_tinysnapshot/tinyplot_add_jitter_on_violin.svg @@ -57,156 +57,156 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/violin_by_x_equivalent.svg b/inst/tinytest/_tinysnapshot/violin_by_x_equivalent.svg index 445297fb..4c5243b7 100644 --- a/inst/tinytest/_tinysnapshot/violin_by_x_equivalent.svg +++ b/inst/tinytest/_tinysnapshot/violin_by_x_equivalent.svg @@ -52,10 +52,10 @@ - - - - + + + + diff --git a/inst/tinytest/_tinysnapshot/violin_facet_by.svg b/inst/tinytest/_tinysnapshot/violin_facet_by.svg index 4e1b42f0..3bc3000c 100644 --- a/inst/tinytest/_tinysnapshot/violin_facet_by.svg +++ b/inst/tinytest/_tinysnapshot/violin_facet_by.svg @@ -26,10 +26,10 @@ - - - - + + + + Diet 1 2 @@ -222,60 +222,60 @@ - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/violin_facet_x_by_same.svg b/inst/tinytest/_tinysnapshot/violin_facet_x_by_same.svg index 0c218eb4..d81c399d 100644 --- a/inst/tinytest/_tinysnapshot/violin_facet_x_by_same.svg +++ b/inst/tinytest/_tinysnapshot/violin_facet_x_by_same.svg @@ -26,9 +26,9 @@ - - - + + + gear 3 4 @@ -128,13 +128,13 @@ - + - + - + diff --git a/inst/tinytest/_tinysnapshot/violin_groups.svg b/inst/tinytest/_tinysnapshot/violin_groups.svg index 03f02910..f3125fc9 100644 --- a/inst/tinytest/_tinysnapshot/violin_groups.svg +++ b/inst/tinytest/_tinysnapshot/violin_groups.svg @@ -26,8 +26,8 @@ - - + + supp OJ VC @@ -68,12 +68,12 @@ - - - - - - + + + + + + diff --git a/inst/tinytest/_tinysnapshot/violin_groups_argpass.svg b/inst/tinytest/_tinysnapshot/violin_groups_argpass.svg index 72848341..3586836a 100644 --- a/inst/tinytest/_tinysnapshot/violin_groups_argpass.svg +++ b/inst/tinytest/_tinysnapshot/violin_groups_argpass.svg @@ -26,8 +26,8 @@ - - + + Ascorbic acid Orange juice @@ -72,12 +72,12 @@ - - - - - - + + + + + + diff --git a/inst/tinytest/_tinysnapshot/violin_groups_lighten_false.svg b/inst/tinytest/_tinysnapshot/violin_groups_lighten_false.svg new file mode 100644 index 00000000..acfc38f7 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/violin_groups_lighten_false.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + +supp +OJ +VC + + + + + + + +dose +len + + +0.5 +1 +2 +0 +10 +20 +30 +40 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/violin_x_by.svg b/inst/tinytest/_tinysnapshot/violin_x_by.svg index b06d7032..30686459 100644 --- a/inst/tinytest/_tinysnapshot/violin_x_by.svg +++ b/inst/tinytest/_tinysnapshot/violin_x_by.svg @@ -54,9 +54,9 @@ - - - + + + diff --git a/inst/tinytest/test-tinyplot_add.R b/inst/tinytest/test-tinyplot_add.R index d5a57836..e2921243 100644 --- a/inst/tinytest/test-tinyplot_add.R +++ b/inst/tinytest/test-tinyplot_add.R @@ -99,7 +99,7 @@ expect_snapshot_plot(f, label = "tinyplot_add_no_recursive_margins") f = function() { set.seed(42) tinyplot(Sepal.Length ~ Species, data = iris, type = "violin") - tinyplot_add(type = "jitter", cex = 0.5, alpha = 0.3) + tinyplot_add(type = "jitter") } expect_snapshot_plot(f, label = "tinyplot_add_jitter_on_violin") @@ -107,15 +107,15 @@ expect_snapshot_plot(f, label = "tinyplot_add_jitter_on_violin") f = function() { set.seed(42) tinyplot(len ~ dose | supp, data = ToothGrowth, type = "boxplot") - tinyplot_add(type = "jitter", cex = 0.5, alpha = 0.3) + tinyplot_add(type = "jitter") } expect_snapshot_plot(f, label = "tinyplot_add_jitter_on_grouped_boxplot") # jitter layer on top of grouped violin (#493) f = function() { set.seed(42) - tinyplot(len ~ dose | supp, data = ToothGrowth, type = "violin", bg = 0.2) - tinyplot_add(type = "jitter", cex = 0.5, alpha = 0.3) + tinyplot(len ~ dose | supp, data = ToothGrowth, type = "violin") + tinyplot_add(type = "jitter") } expect_snapshot_plot(f, label = "tinyplot_add_jitter_on_grouped_violin") diff --git a/inst/tinytest/test-type_barplot.R b/inst/tinytest/test-type_barplot.R index 275930f9..a994a37a 100644 --- a/inst/tinytest/test-type_barplot.R +++ b/inst/tinytest/test-type_barplot.R @@ -13,31 +13,30 @@ expect_snapshot_plot(f, label = "barplot_group") f = function() { - tinyplot(~ cyl | vs, data = mtcars, type = "barplot", beside = TRUE, fill = 0.2) + tinyplot(~ cyl | vs, data = mtcars, type = "barplot", beside = TRUE) } expect_snapshot_plot(f, label = "barplot_group_beside") f = function() { - tinyplot(~ cyl | vs, data = mtcars, type = "barplot", fill = 0.2, - facet = "by") + tinyplot(~ cyl | vs, data = mtcars, type = "barplot", facet = "by") } expect_snapshot_plot(f, label = "barplot_facet") f = function() { - tinyplot(~ cyl | vs, data = mtcars, type = "barplot", fill = 0.2, + tinyplot(~ cyl | vs, data = mtcars, type = "barplot", facet = "by", facet.args = list(free = TRUE)) } expect_snapshot_plot(f, label = "barplot_facet_free") f = function() { tinyplot(extra ~ ID | group, facet = "by", data = sleep, - type = "barplot", beside = TRUE, fill = 0.6) + type = "barplot", beside = TRUE) } expect_snapshot_plot(f, label = "barplot_aggregation") f = function() { tinyplot(Freq ~ Sex | Survived, facet = ~ Class, data = as.data.frame(Titanic), - type = "barplot", flip = TRUE, fill = 0.6, beside = TRUE) + type = "barplot", flip = TRUE, beside = TRUE) } expect_snapshot_plot(f, label = "barplot_flip_fancy") @@ -150,14 +149,16 @@ hec = as.data.frame(proportions(HairEyeColor, 2:3)) # Character offset auto-places a set-aside category (the #420 use case) f = function() { tinyplot(Freq ~ Eye | Hair, facet = Sex ~ 1, data = hec, type = "barplot", - center = TRUE, flip = TRUE, yaxl = "percent", offset = "Red") + center = TRUE, flip = TRUE, lighten = FALSE, offset = "Red", + yaxl = "percent") } expect_snapshot_plot(f, label = "barplot_offset_aside") # Named numeric offset places a set-aside category at an explicit baseline f = function() { tinyplot(Freq ~ Eye | Hair, facet = Sex ~ 1, data = hec, type = "barplot", - center = TRUE, flip = TRUE, yaxl = "percent", offset = c(Red = 1.1)) + center = TRUE, flip = TRUE, lighten = FALSE, offset = c(Red = 1.1), + yaxl = "percent") } expect_snapshot_plot(f, label = "barplot_offset_aside_explicit") @@ -182,3 +183,12 @@ f = function() { tinyplot(~ Species, data = iris, type = "barplot", xlab = NA) } expect_snapshot_plot(f, label = "barplot_xlab_na_issue635") + +# Lighter opaque grouped fills (#646). The grouped bar fill should default to a +# lighter, opaque tint of the palette colour, and `lighten = FALSE` should fall +# back to the fully-saturated palette colour. +f = function() { + tinyplot(~ cyl | vs, data = mtcars, + type = type_barplot(lighten = FALSE), theme = "clean2") +} +expect_snapshot_plot(f, label = "barplot_group_lighten_false") diff --git a/inst/tinytest/test-type_boxplot.R b/inst/tinytest/test-type_boxplot.R index 6ae7e924..2a7ae573 100644 --- a/inst/tinytest/test-type_boxplot.R +++ b/inst/tinytest/test-type_boxplot.R @@ -107,3 +107,13 @@ f = function() { suppressWarnings(plt(mpg ~ factor(am), data = mtcars)) } expect_snapshot_plot(f, label = "boxplot_auto_factor") + +# +## lighter opaque grouped fills (#646); `lighten = FALSE` keeps the legacy +## semi-transparent fill + +f = function() { + plt(len ~ dose | supp, data = ToothGrowth, + type = type_boxplot(lighten = FALSE), theme = "clean2") +} +expect_snapshot_plot(f, label = "boxplot_groups_lighten_false") diff --git a/inst/tinytest/test-type_violin.R b/inst/tinytest/test-type_violin.R index e4dc2b91..3bfb6676 100644 --- a/inst/tinytest/test-type_violin.R +++ b/inst/tinytest/test-type_violin.R @@ -35,7 +35,6 @@ expect_snapshot_plot(f, label = "violin_groups") f = function() { plt(len ~ dose | factor(supp, labels = c("Ascorbic acid", "Orange juice")), data = ToothGrowth, - fill = 0.2, main = "Guinea Pigs' Tooth Growth", xlab = "Vitamin C dose mg", ylab = "tooth length", type = type_violin(trim = TRUE, joint.bw = FALSE), @@ -67,7 +66,7 @@ expect_snapshot_plot(f, label = "violin_by_x_equivalent") ## facets f = function() { - plt(weight ~ Time | Diet, ChickWeight, type = "violin", facet = "by", fill = 0.2) + plt(weight ~ Time | Diet, ChickWeight, type = "violin", facet = "by") } expect_snapshot_plot(f, label = "violin_facet_by") @@ -80,3 +79,13 @@ expect_snapshot_plot(f, label = "violin_facet_x_by_same") # plt(mpg ~ gear | factor(cyl), data = mtcars, type = "violin", facet = ~am) # } # expect_snapshot_plot(f, label = "violin_groups_facets_with_missings") + +# +## lighter opaque grouped fills (#646); `lighten = FALSE` uses the saturated +## palette colour(s) + +f = function() { + plt(len ~ dose | supp, data = ToothGrowth, + type = type_violin(lighten = FALSE), theme = "clean2") +} +expect_snapshot_plot(f, label = "violin_groups_lighten_false") From e95050e593728c7327fa127a2a076b5100a914de Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Tue, 30 Jun 2026 17:20:45 -0700 Subject: [PATCH 06/18] wip: issue #646 lighten (parked) --- NEWS.md | 13 ++ R/by_aesthetics.R | 2 +- R/legend.R | 20 +- R/type_spineplot.R | 40 ++-- inst/tinytest/_tinysnapshot/spineplot_yby.svg | 36 ++-- .../spineplot_yby_lighten_false.svg | 166 ++++++++++++++++ inst/tinytest/test-fill_lighten.R | 177 ++++++++++++++++++ inst/tinytest/test-type_spineplot.R | 13 ++ man/type_spineplot.Rd | 8 +- 9 files changed, 441 insertions(+), 34 deletions(-) create mode 100644 inst/tinytest/_tinysnapshot/spineplot_yby_lighten_false.svg create mode 100644 inst/tinytest/test-fill_lighten.R diff --git a/NEWS.md b/NEWS.md index 72ae7efe..56f32977 100644 --- a/NEWS.md +++ b/NEWS.md @@ -113,6 +113,19 @@ New theme features: Note that this does imply a change for `"barplot"` types, which previously used a slightly darker `"grey"` (to match base R's `barplot()`), but we decided internal consistency was the more important feature to prioritize. +- Building on the above, _multi-group_ `"boxplot"`, `"violin"`, `"barplot"`, and + `"spineplot"` (grouped `y` variable) displays now also use the + lighter-but-opaque fill, so that grouped plots match their single-group + counterparts (previously grouped barplots used dark saturated fills and grouped + boxplots/violins used semi-transparent fills). Each type gains a `lighten` + argument (default `TRUE`); set `type_barplot(lighten = FALSE)` (etc.) to recover + the fully-saturated palette colours. `"histogram"` is deliberately excluded, + since semi-transparency remains preferable for overlapping grouped + distributions. (#646 @grantmcdermott @zeileis) + - Relatedly, transparency requested via `fill` or `alpha` is now layered _on + top of_ the (lightened) fill rather than the saturated base colour. This + fixes a counterintuitive case where adding an alpha fill (e.g. `fill = 0.7`) + could _darken_ the interior of an area-based plot. Theme fixes: diff --git a/R/by_aesthetics.R b/R/by_aesthetics.R index e266c19f..fe650733 100755 --- a/R/by_aesthetics.R +++ b/R/by_aesthetics.R @@ -262,7 +262,7 @@ by_pch = function(ngrps, type, pch = NULL) { by_lty = function(ngrps, type, lty = NULL) { # We only care about line types, otherwise return NULL - if (!type %in% c("l", "b", "o", "c", "h", "s", "S", "ribbon", "barplot", "boxplot", "rect", "segments", "qq", "abline", "hline", "vline")) { + if (!type %in% c("l", "b", "o", "c", "h", "s", "S", "ribbon", "barplot", "boxplot", "rect", "spineplot", "segments", "qq", "abline", "hline", "vline")) { lty = NULL # special "by" convenience keyword diff --git a/R/legend.R b/R/legend.R index a13a51db..14b21ad4 100644 --- a/R/legend.R +++ b/R/legend.R @@ -496,6 +496,15 @@ prepare_legend = function(settings) { } } + # Grouped (`y_by`) spineplots draw their fills inside draw_spineplot() rather + # than via the shared `bg` channel, so `settings$bg` is NULL and the legend has + # nothing to mirror. Resolve the swatch fill here (where `col` is the resolved + # group palette) into `bg`, so the rest of the legend machinery treats it like + # any other area type. The fill tracks `lighten`, matching the plotted tiles. + if (identical(settings$type, "spineplot") && isTRUE(settings$type_info[["y_by"]])) { + settings$bg = if (isTRUE(settings$lighten)) lighten_fill(col) else col + } + env2env( environment(), settings, @@ -603,7 +612,16 @@ build_legend_args = function( # Special pt.bg handling for types that need color-based fills if (identical(type, "spineplot")) { - legend_args[["pt.bg"]] = legend_args[["pt.bg"]] %||% legend_args[["col"]] + # The swatch fill comes via `bg` (resolved in prepare_legend for the grouped + # `y_by` case; NULL otherwise, falling back to the group colour). + legend_args[["pt.bg"]] = legend_args[["pt.bg"]] %||% bg %||% legend_args[["col"]] + # Grouped spineplot tiles always carry black separating borders, so the + # bordered swatch (pt.lwd turned on for `y_by` in data_spineplot) matches + # with a black border too. A group-coloured border would vanish against a + # pale fill (e.g. a pastel palette). + if (!is.null(legend_args[["pt.lwd"]]) && any(legend_args[["pt.lwd"]] > 0)) { + legend_args[["col"]] = par("fg") + } } else if (identical(type, "ridge") && isFALSE(gradient)) { legend_args[["pt.bg"]] = legend_args[["pt.bg"]] %||% sapply(legend_args[["col"]], function(ccol) seq_palette(ccol, n = 2)[2]) } else { diff --git a/R/type_spineplot.R b/R/type_spineplot.R index 133dea47..fb855500 100644 --- a/R/type_spineplot.R +++ b/R/type_spineplot.R @@ -8,6 +8,7 @@ #' levels of the `x` and `y` variables (if character) or the corresponding indexes #' (if numeric) for the plot. #' @inheritParams graphics::spineplot +#' @inheritParams type_barplot #' @examples #' # "spineplot" type convenience string #' tinyplot(Species ~ Sepal.Width, data = iris, type = "spineplot") @@ -73,11 +74,11 @@ #' ) #' #' @export -type_spineplot = function(breaks = NULL, tol.ylab = 0.05, off = NULL, xlevels = NULL, ylevels = NULL, col = NULL, xaxlabels = NULL, yaxlabels = NULL, weights = NULL) { +type_spineplot = function(breaks = NULL, tol.ylab = 0.05, off = NULL, xlevels = NULL, ylevels = NULL, col = NULL, xaxlabels = NULL, yaxlabels = NULL, weights = NULL, lighten = TRUE) { col = col out = list( - data = data_spineplot(off = off, breaks = breaks, xlevels = xlevels, ylevels = ylevels, xaxlabels = xaxlabels, yaxlabels = yaxlabels, weights = weights), - draw = draw_spineplot(tol.ylab = tol.ylab, off = off, col = col, xaxlabels = xaxlabels, yaxlabels = yaxlabels), + data = data_spineplot(off = off, breaks = breaks, xlevels = xlevels, ylevels = ylevels, 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" ) class(out) = "tinyplot_type" @@ -85,9 +86,10 @@ 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) { +data_spineplot = function(off = NULL, breaks = NULL, xlevels = xlevels, ylevels = ylevels, xaxlabels = NULL, yaxlabels = NULL, weights = NULL, lighten = TRUE) { fun = function(settings, ...) { - env2env(settings, environment(), c("datapoints", "xlim", "ylim", "facet", "facet.args", "by", "xaxb", "yaxb", "null_by", "null_facet", "col", "bg", "axes", "xaxt", "yaxt")) + env2env(settings, environment(), c("datapoints", "xlim", "ylim", "facet", "facet.args", "by", "xaxb", "yaxb", "null_by", "null_facet", "col", "bg", "axes", "xaxt", "yaxt", "lwd")) + settings[["lighten"]] = lighten ## process weights: a top-level `weights` column (carried on datapoints ## via NSE) takes precedence over the constructor-level `weights` arg. @@ -289,7 +291,15 @@ data_spineplot = function(off = NULL, breaks = NULL, xlevels = xlevels, ylevels # legend customizations settings$legend_args[["pch"]] = settings$legend_args[["pch"]] %||% 22 settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% 3.5 - settings$legend_args[["pt.lwd"]] = settings$legend_args[["pt.lwd"]] %||% 0 + # Spineplot tiles carry black separating borders, so the swatches get a + # matching (black) border too (the colour override happens in + # build_legend_args when the swatch border is drawn). The swatch border + # width tracks the tile border width `lwd` -- the `pch = 22` swatch reads + # its border width from `pt.lwd` -- so e.g. `lwd = 0` drops both. (Base + # `legend()` can't dash a filled-square border, so `lty` only affects the + # tiles, not the swatch -- as with the other area types.) + spine_pt_lwd = if (is.null(lwd)) 1 else lwd + settings$legend_args[["pt.lwd"]] = settings$legend_args[["pt.lwd"]] %||% spine_pt_lwd settings$legend_args[["y.intersp"]] = settings$legend_args[["y.intersp"]] %||% 1.25 settings$legend_args[["seg.len"]] = settings$legend_args[["seg.len"]] %||% 1.25 @@ -303,7 +313,7 @@ data_spineplot = function(off = NULL, breaks = NULL, xlevels = xlevels, ylevels return(fun) } -draw_spineplot = function(tol.ylab = 0.05, off = NULL, col = NULL, xaxlabels = NULL, yaxlabels = NULL) { +draw_spineplot = function(tol.ylab = 0.05, off = NULL, col = NULL, xaxlabels = NULL, yaxlabels = NULL, lighten = TRUE) { fun = function(ixmin, iymin, ixmax, iymax, ilty, ilwd, icol, ibg, flip, facet_window_args, @@ -325,7 +335,6 @@ draw_spineplot = function(tol.ylab = 0.05, off = NULL, col = NULL, xaxlabels = N ## graphical parameters if (is.null(col)) { - if (is.null(ibg)) ibg = icol if (isFALSE(y_by)) { # For single-group displays, use a neutral grey ramp (gray.colors) # whenever the resolved seed colour is achromatic (e.g. the black @@ -335,18 +344,23 @@ draw_spineplot = function(tol.ylab = 0.05, off = NULL, col = NULL, xaxlabels = N # displays we never switch to grayscale: each group (including one # whose palette colour is black) follows the same seq_palette ramp so # the fills stay in sync with the legend swatches. + if (is.null(ibg)) ibg = icol gs = isTRUE(null_by) && is_achromatic(ibg) ibg = seq_palette(ibg, ny, grayscale = gs) + } else { + # When the y variable is itself the grouping (`y_by`), each band is a + # group's palette colour. The fill is resolved once in prepare_legend() + # and arrives via `ibg` -- lightened to match the other area types + # (barplot/boxplot/violin) unless `lighten` is off (issue #646). Only + # fall back to lightening the group colour `icol` here if no fill was + # supplied (e.g. a standalone draw outside the legend pipeline). + if (is.null(ibg)) ibg = if (isTRUE(lighten)) lighten_fill(icol) else icol } ibg = rep_len(ibg, ny) } else { ibg = col } - - if (type_info[["xaxt"]] %in% c("l", "t", "n") && - type_info[["yaxt"]] %in% c("l", "t", "n") && - !all(c(type_info[["xaxt"]], type_info[["yaxt"]]) == "n")) ilwd = 0 - + rect( xleft = ixmin, ybottom = iymin, xright = ixmax, ytop = iymax, lty = ilty, diff --git a/inst/tinytest/_tinysnapshot/spineplot_yby.svg b/inst/tinytest/_tinysnapshot/spineplot_yby.svg index 3cb490b6..4ec2f2c1 100644 --- a/inst/tinytest/_tinysnapshot/spineplot_yby.svg +++ b/inst/tinytest/_tinysnapshot/spineplot_yby.svg @@ -26,9 +26,9 @@ - - - + + + Species setosa versicolor @@ -49,11 +49,11 @@ - - - - - + + + + + @@ -87,11 +87,11 @@ - - - - - + + + + + @@ -125,11 +125,11 @@ - - - - - + + + + + diff --git a/inst/tinytest/_tinysnapshot/spineplot_yby_lighten_false.svg b/inst/tinytest/_tinysnapshot/spineplot_yby_lighten_false.svg new file mode 100644 index 00000000..f58d3432 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/spineplot_yby_lighten_false.svg @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + +Species +setosa +versicolor +virginica + + + + + + + +Sepal.Width +Species + + + + + + + + + + + + + + + + + + + + + +2 +2.5 +3 +3.5 +4 +virginica +versicolor +setosa + + + + + + + +0.0 +0.2 +0.4 +0.6 +0.8 +1.0 + + + + + + + + + + + + + + + + + +2 +2.5 +3 +3.5 +4 +virginica +versicolor +setosa + + + + + + + +0.0 +0.2 +0.4 +0.6 +0.8 +1.0 + + + + + + + + + + + + + + + + + +2 +2.5 +3 +3.5 +4 +virginica +versicolor +setosa + + + + + + + +0.0 +0.2 +0.4 +0.6 +0.8 +1.0 + + + + diff --git a/inst/tinytest/test-fill_lighten.R b/inst/tinytest/test-fill_lighten.R new file mode 100644 index 00000000..97ca1cbf --- /dev/null +++ b/inst/tinytest/test-fill_lighten.R @@ -0,0 +1,177 @@ +source("helpers.R") + +# Platform-independent checks for the lighter-opaque fill logic (#646, #614). +# These assert on the colours resolved by the internal by_bg() helper, so they +# run everywhere (no snapshot rendering required). + +ac = grDevices::adjustcolor + +bb = function(...) { + tinyplot:::by_bg( + palette = NULL, ribbon.alpha = 0.2, adjustcolor = ac, ... + ) +} + +# Helper predicates on hex colours ------------------------------------------- + +# Fully opaque (alpha == FF), i.e. no transparency applied. +is_opaque = function(x) all(grepl("FF$", toupper(x)) | nchar(x) %in% c(7L) | !grepl("^#", x)) +# Semi-transparent (alpha < FF), e.g. the legacy "33" ribbon.alpha fill. +is_semitransparent = function(x) all(grepl("^#.{6}(?!FF)..$", x, perl = TRUE)) +# A "light" tint: high luminance in all channels. +is_light = function(x) { + rgb = grDevices::col2rgb(x) + all(colMeans(rgb) > 180) +} + +# Activate a colour-forward theme so the qualitative palette starts with a +# chromatic colour (Tableau 10 -> blue), making the tint visible. +tinytheme("clean2") + +# Grouped barplot: default lighten = TRUE -> light, opaque tints --------------- +bar_light = bb( + bg = "by", fill = NULL, col = NULL, alpha = NULL, + by_ordered = FALSE, by_continuous = FALSE, ngrps = 3, + type = "barplot", by = factor(1:3), lighten = TRUE +) +expect_equal(length(bar_light), 3L) +expect_true(is_opaque(bar_light)) +expect_true(is_light(bar_light)) + +# Grouped barplot: lighten = FALSE -> saturated, opaque palette colours -------- +bar_dark = bb( + bg = "by", fill = NULL, col = NULL, alpha = NULL, + by_ordered = FALSE, by_continuous = FALSE, ngrps = 3, + type = "barplot", by = factor(1:3), lighten = FALSE +) +expect_true(is_opaque(bar_dark)) +expect_false(is_light(bar_dark)) +# The lightened fill should differ from the saturated one. +expect_false(isTRUE(all.equal(bar_light, bar_dark))) + +# Grouped boxplot: default lighten = TRUE -> light opaque (NOT semi-transparent) +box_light = bb( + bg = "by", fill = NULL, col = NULL, alpha = NULL, + by_ordered = FALSE, by_continuous = FALSE, ngrps = 3, + type = "boxplot", by = factor(1:3), lighten = TRUE +) +expect_true(is_opaque(box_light)) +expect_true(is_light(box_light)) + +# Grouped boxplot: lighten = FALSE -> legacy semi-transparent fill ------------- +box_legacy = bb( + bg = "by", fill = NULL, col = NULL, alpha = NULL, + by_ordered = FALSE, by_continuous = FALSE, ngrps = 3, + type = "boxplot", by = factor(1:3), lighten = FALSE +) +expect_true(is_semitransparent(box_legacy)) + +# Grouped violin mirrors barplot/boxplot --------------------------------------- +vio_light = bb( + bg = "by", fill = NULL, col = NULL, alpha = NULL, + by_ordered = FALSE, by_continuous = FALSE, ngrps = 3, + type = "violin", by = factor(1:3), lighten = TRUE +) +expect_true(is_opaque(vio_light)) +expect_true(is_light(vio_light)) + +# Single-group fill is lightened by default and matches grouped colour[1] ------ +bar_single = bb( + bg = NULL, fill = NULL, col = NULL, alpha = NULL, + by_ordered = FALSE, by_continuous = FALSE, ngrps = 1, + type = "barplot", by = NULL, lighten = TRUE +) +expect_equal(bar_single, bar_light[1]) + +# A numeric `fill` request layers transparency on top of the *lightened* base, +# so adding alpha lightens (never darkens) the interior (#646 follow-up). The +# base RGB should match the opaque light tint; only the alpha channel changes. +bar_fill07 = bb( + bg = 0.7, fill = NULL, col = NULL, alpha = NULL, + by_ordered = FALSE, by_continuous = FALSE, ngrps = 1, + type = "barplot", by = NULL, lighten = TRUE +) +expect_equal(substr(bar_fill07, 1, 7), substr(bar_single, 1, 7)) +expect_true(is_semitransparent(bar_fill07)) + +# An explicit fill colour is always honoured verbatim (never lightened) -------- +box_bisque = bb( + bg = "bisque", fill = NULL, col = NULL, alpha = NULL, + by_ordered = FALSE, by_continuous = FALSE, ngrps = 3, + type = "boxplot", by = factor(1:3), lighten = TRUE +) +expect_equal(box_bisque, rep("bisque", 3)) + +# Ordered groupings use a sequential palette and must NOT be lightened --------- +box_ordered = bb( + bg = "by", fill = NULL, col = NULL, alpha = NULL, + by_ordered = TRUE, by_continuous = FALSE, ngrps = 3, + type = "boxplot", by = ordered(1:3), lighten = TRUE +) +expect_false(is_light(box_ordered)) + +tinytheme() + +# No theme active: single-group area fills fall back to neutral "lightgray" ---- +bar_none = bb( + bg = NULL, fill = NULL, col = NULL, alpha = NULL, + by_ordered = FALSE, by_continuous = FALSE, ngrps = 1, + type = "barplot", by = NULL, lighten = TRUE +) +expect_equal(bar_none, "lightgray") + +# Grouped spineplot (`y_by`) fills follow the same lighter-opaque tint as the +# other area types (#646). draw_spineplot() lightens the per-group bands; the +# legend swatch fill is resolved in prepare_legend() into `bg`. Here we check the +# lighten_fill() helper directly against the resolved group colours. +tinytheme("clean2") +spine_seed = tinyplot:::by_col( + col = NULL, palette = NULL, alpha = NULL, by_ordered = FALSE, + by_continuous = FALSE, ngrps = 3, adjustcolor = ac +) +spine_light = tinyplot:::lighten_fill(spine_seed) +expect_true(is_opaque(spine_light)) +expect_true(is_light(spine_light)) +expect_false(isTRUE(all.equal(spine_light, spine_seed))) + +# Spineplot legend swatch: data_spineplot sets the swatch border width from the +# tile border width `lwd` (defaulting to 1), and build_legend_args() forces that +# border *black* whenever it is drawn (pt.lwd > 0), matching the tiles -- a +# group-coloured border would vanish against a pale fill. The fill arrives via +# `bg` (lightened in prepare_legend for the grouped `y_by` case, saturated +# otherwise); only the fill colour differs. +build_swatch = function(bg, pt.lwd = 1) { + le = new.env(parent = emptyenv()) + tinyplot:::build_legend_args( + legend_env = le, legend = NULL, + legend_args = list(pch = 22, pt.lwd = pt.lwd), + by_dep = "g", lgnd_labs = c("a", "b", "c"), + type = "spineplot", pch = 22, lty = 1, lwd = 1, + col = spine_seed, bg = bg, cex = NULL, + gradient = FALSE + ) + le$args +} + +sw_light = build_swatch(bg = spine_light) +expect_equal(sw_light[["col"]], par("fg")) # black swatch border (matches tiles) +expect_true(is_light(sw_light[["pt.bg"]])) # lightened fill (from bg) +expect_equal(sw_light[["pt.lwd"]], 1) # border drawn + +sw_dark = build_swatch(bg = spine_seed) +expect_equal(sw_dark[["col"]], par("fg")) # black swatch border (matches tiles) +expect_equal(sw_dark[["pt.bg"]], spine_seed) # saturated fill (from bg) +expect_equal(sw_dark[["pt.lwd"]], 1) # border drawn + +# A thicker tile border (lwd) carries through to the swatch border width. +sw_thick = build_swatch(bg = spine_seed, pt.lwd = 3) +expect_equal(sw_thick[["col"]], par("fg")) # still black +expect_equal(sw_thick[["pt.lwd"]], 3) # thick border + +# `lwd = 0` (pt.lwd = 0) draws no swatch border, matching borderless tiles. The +# black-colour override is skipped, since no border is drawn. +sw_none = build_swatch(bg = spine_seed, pt.lwd = 0) +expect_equal(sw_none[["pt.lwd"]], 0) # no border +expect_false(identical(sw_none[["col"]], par("fg"))) # colour not overridden to black + +tinytheme() diff --git a/inst/tinytest/test-type_spineplot.R b/inst/tinytest/test-type_spineplot.R index 38ba8081..fc321045 100644 --- a/inst/tinytest/test-type_spineplot.R +++ b/inst/tinytest/test-type_spineplot.R @@ -112,3 +112,16 @@ f = function() { theme = "dynamic", type = "spineplot", xlab = NA) } expect_snapshot_plot(f, label = "spineplot_xlab_na_issue635") + +# +## lighter opaque grouped (`y_by`) fills (#646); `lighten = FALSE` keeps the +## fully-saturated palette colour(s) + +f = function() { + tinyplot( + Species ~ Sepal.Width | Species, data = iris, + type = type_spineplot(breaks = 4, lighten = FALSE), + palette = "Pastel 1" + ) +} +expect_snapshot_plot(f, label = "spineplot_yby_lighten_false") diff --git a/man/type_spineplot.Rd b/man/type_spineplot.Rd index f318c90f..654a6736 100644 --- a/man/type_spineplot.Rd +++ b/man/type_spineplot.Rd @@ -13,7 +13,8 @@ type_spineplot( col = NULL, xaxlabels = NULL, yaxlabels = NULL, - weights = NULL + weights = NULL, + lighten = TRUE ) } \arguments{ @@ -44,6 +45,11 @@ levels of the \code{x} and \code{y} variables (if character) or the correspondin 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 are ignored.} + +\item{lighten}{logical. Should the fills use a lighter, opaque tint of the +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.} } \description{ Type function(s) for producing spineplots and spinograms, which From 52e153a42bb85538d3346383ae87a18a3cdad4c4 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Wed, 1 Jul 2026 17:45:32 -0700 Subject: [PATCH 07/18] spineplot default to lighten = FALSE --- R/type_spineplot.R | 21 +++++++++++++++++---- man/type_spineplot.Rd | 16 +++++++++++----- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/R/type_spineplot.R b/R/type_spineplot.R index fb855500..86b228c5 100644 --- a/R/type_spineplot.R +++ b/R/type_spineplot.R @@ -8,7 +8,16 @@ #' levels of the `x` and `y` variables (if character) or the corresponding indexes #' (if numeric) for the plot. #' @inheritParams graphics::spineplot -#' @inheritParams type_barplot +#' @param lighten logical. For grouped spineplots where the `y` variable is +#' itself the grouping variable (i.e. `y == by`), should the fills use a +#' lighter, opaque tint of the series colour(s)? Default is `FALSE`, i.e. the +#' fills use the fully-saturated palette colour(s). (Unlike the other area +#' types such as [`type_barplot`], where lightening is the default, spineplot +#' tiles abut one another with no gap, so the darker saturated fills read +#' better against their matching border colours.) Set to `TRUE` to opt in to +#' 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. #' @examples #' # "spineplot" type convenience string #' tinyplot(Species ~ Sepal.Width, data = iris, type = "spineplot") @@ -74,7 +83,7 @@ #' ) #' #' @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 = TRUE) { +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) { col = col out = list( data = data_spineplot(off = off, breaks = breaks, xlevels = xlevels, ylevels = ylevels, xaxlabels = xaxlabels, yaxlabels = yaxlabels, weights = weights, lighten = lighten), @@ -86,7 +95,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 = TRUE) { +data_spineplot = function(off = NULL, breaks = NULL, xlevels = xlevels, ylevels = ylevels, 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", "xaxt", "yaxt", "lwd")) settings[["lighten"]] = lighten @@ -289,6 +298,10 @@ data_spineplot = function(off = NULL, breaks = NULL, xlevels = xlevels, ylevels ) # legend customizations + # lty = 0 so the filled-square swatch has no line component; otherwise + # legend() reserves `seg.len` horizontal space for a line segment beside + # the square, leaving an odd gap before the label (matches type_barplot). + settings$legend_args[["lty"]] = settings$legend_args[["lty"]] %||% 0 settings$legend_args[["pch"]] = settings$legend_args[["pch"]] %||% 22 settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% 3.5 # Spineplot tiles carry black separating borders, so the swatches get a @@ -313,7 +326,7 @@ data_spineplot = function(off = NULL, breaks = NULL, xlevels = xlevels, ylevels return(fun) } -draw_spineplot = function(tol.ylab = 0.05, off = NULL, col = NULL, xaxlabels = NULL, yaxlabels = NULL, lighten = TRUE) { +draw_spineplot = function(tol.ylab = 0.05, off = NULL, col = NULL, xaxlabels = NULL, yaxlabels = NULL, lighten = FALSE) { fun = function(ixmin, iymin, ixmax, iymax, ilty, ilwd, icol, ibg, flip, facet_window_args, diff --git a/man/type_spineplot.Rd b/man/type_spineplot.Rd index 654a6736..9d5080e0 100644 --- a/man/type_spineplot.Rd +++ b/man/type_spineplot.Rd @@ -14,7 +14,7 @@ type_spineplot( xaxlabels = NULL, yaxlabels = NULL, weights = NULL, - lighten = TRUE + lighten = FALSE ) } \arguments{ @@ -46,10 +46,16 @@ levels of the \code{x} and \code{y} variables (if character) or the correspondin assumed to be 1. If \code{x} is already a 2-way table, the weights are ignored.} -\item{lighten}{logical. Should the fills use a lighter, opaque tint of the -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{lighten}{logical. For grouped spineplots where the \code{y} variable is +itself the grouping variable (i.e. \code{y == by}), should the fills use a +lighter, opaque tint of the series colour(s)? Default is \code{FALSE}, i.e. the +fills use the fully-saturated palette colour(s). (Unlike the other area +types such as \code{\link{type_barplot}}, where lightening is the default, spineplot +tiles abut one another with no gap, so the darker saturated fills read +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.} } \description{ Type function(s) for producing spineplots and spinograms, which From e3f0ffb7410107acc587500d13959d2362bd6f89 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Wed, 1 Jul 2026 17:45:42 -0700 Subject: [PATCH 08/18] udpate tests --- inst/tinytest/_tinysnapshot/df_pairs.svg | 274 +++++++-------- inst/tinytest/_tinysnapshot/df_pairs_by.svg | 324 +++++++++--------- .../_tinysnapshot/df_pairs_labs_frames.svg | 324 +++++++++--------- .../_tinysnapshot/spineplot_facet_by.svg | 8 +- inst/tinytest/_tinysnapshot/spineplot_xby.svg | 4 +- inst/tinytest/_tinysnapshot/spineplot_yby.svg | 36 +- .../spineplot_yby_lighten_true.svg | 166 +++++++++ inst/tinytest/test-fill_lighten.R | 5 +- inst/tinytest/test-type_spineplot.R | 12 +- 9 files changed, 675 insertions(+), 478 deletions(-) create mode 100644 inst/tinytest/_tinysnapshot/spineplot_yby_lighten_true.svg diff --git a/inst/tinytest/_tinysnapshot/df_pairs.svg b/inst/tinytest/_tinysnapshot/df_pairs.svg index 6e689cde..9395140d 100644 --- a/inst/tinytest/_tinysnapshot/df_pairs.svg +++ b/inst/tinytest/_tinysnapshot/df_pairs.svg @@ -2768,30 +2768,30 @@ - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + 4 @@ -2806,6 +2806,7 @@ 0.6 0.8 1.0 + @@ -2825,42 +2826,42 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 @@ -2875,6 +2876,7 @@ 0.6 0.8 1.0 + @@ -2894,42 +2896,42 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 @@ -2944,6 +2946,7 @@ 0.6 0.8 1.0 + @@ -2963,45 +2966,45 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 @@ -3016,6 +3019,7 @@ 0.6 0.8 1.0 + diff --git a/inst/tinytest/_tinysnapshot/df_pairs_by.svg b/inst/tinytest/_tinysnapshot/df_pairs_by.svg index f085e2be..ca4ac7bd 100644 --- a/inst/tinytest/_tinysnapshot/df_pairs_by.svg +++ b/inst/tinytest/_tinysnapshot/df_pairs_by.svg @@ -692,21 +692,21 @@ - + - + - + @@ -1372,7 +1372,7 @@ - + @@ -1380,14 +1380,14 @@ - + - + @@ -2058,7 +2058,7 @@ - + @@ -2066,7 +2066,7 @@ - + @@ -2074,7 +2074,7 @@ - + @@ -2739,7 +2739,7 @@ - + @@ -2748,14 +2748,14 @@ - + - + @@ -2781,14 +2781,14 @@ - - - - - - - - + + + + + + + + 4 @@ -2803,6 +2803,7 @@ 0.6 0.8 1.0 + @@ -2810,14 +2811,14 @@ - - - - - - - - + + + + + + + + 4 @@ -2832,16 +2833,17 @@ 0.6 0.8 1.0 + - - - - - - - - + + + + + + + + 4 @@ -2856,6 +2858,7 @@ 0.6 0.8 1.0 + @@ -2875,18 +2878,18 @@ - - - - - - - - - - - - + + + + + + + + + + + + 2 @@ -2901,6 +2904,7 @@ 0.6 0.8 1.0 + @@ -2908,18 +2912,18 @@ - - - - - - - - - - - - + + + + + + + + + + + + 2 @@ -2934,20 +2938,21 @@ 0.6 0.8 1.0 + - - - - - - - - - - - - + + + + + + + + + + + + 2 @@ -2962,6 +2967,7 @@ 0.6 0.8 1.0 + @@ -2981,18 +2987,18 @@ - - - - - - - - - - - - + + + + + + + + + + + + 1 @@ -3007,6 +3013,7 @@ 0.6 0.8 1.0 + @@ -3014,18 +3021,18 @@ - - - - - - - - - - - - + + + + + + + + + + + + 1 @@ -3040,20 +3047,21 @@ 0.6 0.8 1.0 + - - - - - - - - - - - - + + + + + + + + + + + + 1 @@ -3068,6 +3076,7 @@ 0.6 0.8 1.0 + @@ -3087,19 +3096,19 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + 0 @@ -3114,6 +3123,7 @@ 0.6 0.8 1.0 + @@ -3121,19 +3131,19 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + 0 @@ -3148,21 +3158,22 @@ 0.6 0.8 1.0 + - - - - - - - - - - - - - + + + + + + + + + + + + + 0 @@ -3177,6 +3188,7 @@ 0.6 0.8 1.0 + @@ -3222,15 +3234,15 @@ - - - - - - - - - + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/df_pairs_labs_frames.svg b/inst/tinytest/_tinysnapshot/df_pairs_labs_frames.svg index 49b7a7d2..02a010ef 100644 --- a/inst/tinytest/_tinysnapshot/df_pairs_labs_frames.svg +++ b/inst/tinytest/_tinysnapshot/df_pairs_labs_frames.svg @@ -742,21 +742,21 @@ - + - + - + @@ -1474,7 +1474,7 @@ - + @@ -1482,14 +1482,14 @@ - + - + @@ -2212,7 +2212,7 @@ - + @@ -2220,7 +2220,7 @@ - + @@ -2228,7 +2228,7 @@ - + @@ -2945,7 +2945,7 @@ - + @@ -2954,14 +2954,14 @@ - + - + @@ -3006,14 +3006,14 @@ - - - - - - - - + + + + + + + + 4 @@ -3027,16 +3027,17 @@ 0.6 0.8 1.0 + - - - - - - - - + + + + + + + + 4 @@ -3050,16 +3051,17 @@ 0.6 0.8 1.0 + - - - - - - - - + + + + + + + + 4 @@ -3073,6 +3075,7 @@ 0.6 0.8 1.0 + @@ -3109,18 +3112,18 @@ - - - - - - - - - - - - + + + + + + + + + + + + 2 @@ -3134,20 +3137,21 @@ 0.6 0.8 1.0 + - - - - - - - - - - - - + + + + + + + + + + + + 2 @@ -3161,20 +3165,21 @@ 0.6 0.8 1.0 + - - - - - - - - - - - - + + + + + + + + + + + + 2 @@ -3188,6 +3193,7 @@ 0.6 0.8 1.0 + @@ -3224,18 +3230,18 @@ - - - - - - - - - - - - + + + + + + + + + + + + 1 @@ -3249,20 +3255,21 @@ 0.6 0.8 1.0 + - - - - - - - - - - - - + + + + + + + + + + + + 1 @@ -3276,20 +3283,21 @@ 0.6 0.8 1.0 + - - - - - - - - - - - - + + + + + + + + + + + + 1 @@ -3303,6 +3311,7 @@ 0.6 0.8 1.0 + @@ -3339,19 +3348,19 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + 0 @@ -3365,21 +3374,22 @@ 0.6 0.8 1.0 + - - - - - - - - - - - - - + + + + + + + + + + + + + 0 @@ -3393,21 +3403,22 @@ 0.6 0.8 1.0 + - - - - - - - - - - - - - + + + + + + + + + + + + + 0 @@ -3421,6 +3432,7 @@ 0.6 0.8 1.0 + @@ -3469,15 +3481,15 @@ - - - - - - - - - + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/spineplot_facet_by.svg b/inst/tinytest/_tinysnapshot/spineplot_facet_by.svg index a1c75a3c..74429f28 100644 --- a/inst/tinytest/_tinysnapshot/spineplot_facet_by.svg +++ b/inst/tinytest/_tinysnapshot/spineplot_facet_by.svg @@ -26,10 +26,10 @@ - - - - + + + + Class 1st 2nd diff --git a/inst/tinytest/_tinysnapshot/spineplot_xby.svg b/inst/tinytest/_tinysnapshot/spineplot_xby.svg index f3c80f03..004b0b4f 100644 --- a/inst/tinytest/_tinysnapshot/spineplot_xby.svg +++ b/inst/tinytest/_tinysnapshot/spineplot_xby.svg @@ -26,8 +26,8 @@ - - + + Sex Female Male diff --git a/inst/tinytest/_tinysnapshot/spineplot_yby.svg b/inst/tinytest/_tinysnapshot/spineplot_yby.svg index 4ec2f2c1..f58d3432 100644 --- a/inst/tinytest/_tinysnapshot/spineplot_yby.svg +++ b/inst/tinytest/_tinysnapshot/spineplot_yby.svg @@ -26,9 +26,9 @@ - - - + + + Species setosa versicolor @@ -49,11 +49,11 @@ - - - - - + + + + + @@ -87,11 +87,11 @@ - - - - - + + + + + @@ -125,11 +125,11 @@ - - - - - + + + + + diff --git a/inst/tinytest/_tinysnapshot/spineplot_yby_lighten_true.svg b/inst/tinytest/_tinysnapshot/spineplot_yby_lighten_true.svg new file mode 100644 index 00000000..4ec2f2c1 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/spineplot_yby_lighten_true.svg @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + +Species +setosa +versicolor +virginica + + + + + + + +Sepal.Width +Species + + + + + + + + + + + + + + + + + + + + + +2 +2.5 +3 +3.5 +4 +virginica +versicolor +setosa + + + + + + + +0.0 +0.2 +0.4 +0.6 +0.8 +1.0 + + + + + + + + + + + + + + + + + +2 +2.5 +3 +3.5 +4 +virginica +versicolor +setosa + + + + + + + +0.0 +0.2 +0.4 +0.6 +0.8 +1.0 + + + + + + + + + + + + + + + + + +2 +2.5 +3 +3.5 +4 +virginica +versicolor +setosa + + + + + + + +0.0 +0.2 +0.4 +0.6 +0.8 +1.0 + + + + diff --git a/inst/tinytest/test-fill_lighten.R b/inst/tinytest/test-fill_lighten.R index 97ca1cbf..bdf6c44c 100644 --- a/inst/tinytest/test-fill_lighten.R +++ b/inst/tinytest/test-fill_lighten.R @@ -138,8 +138,9 @@ expect_false(isTRUE(all.equal(spine_light, spine_seed))) # tile border width `lwd` (defaulting to 1), and build_legend_args() forces that # border *black* whenever it is drawn (pt.lwd > 0), matching the tiles -- a # group-coloured border would vanish against a pale fill. The fill arrives via -# `bg` (lightened in prepare_legend for the grouped `y_by` case, saturated -# otherwise); only the fill colour differs. +# `bg` (lightened in prepare_legend only when `lighten = TRUE`, which is opt-in +# for spineplots; saturated otherwise). Both fills are exercised below by +# passing `bg` explicitly. build_swatch = function(bg, pt.lwd = 1) { le = new.env(parent = emptyenv()) tinyplot:::build_legend_args( diff --git a/inst/tinytest/test-type_spineplot.R b/inst/tinytest/test-type_spineplot.R index fc321045..f47466a6 100644 --- a/inst/tinytest/test-type_spineplot.R +++ b/inst/tinytest/test-type_spineplot.R @@ -79,7 +79,7 @@ f = function() { tinyplot( Survived ~ Sex | Class, facet = "by", data = ttnc, type = type_spineplot(weights = ttnc$Freq), - palette = "Dark 2", axes = "t", legend = FALSE + palette = "Dark 2", axes = "t", legend = FALSE, lwd = 0 ) } expect_snapshot_plot(f, label = "spineplot_facet_by_fancy") @@ -114,14 +114,16 @@ f = function() { expect_snapshot_plot(f, label = "spineplot_xlab_na_issue635") # -## lighter opaque grouped (`y_by`) fills (#646); `lighten = FALSE` keeps the -## fully-saturated palette colour(s) +## spineplots default to saturated fills (lighten = FALSE); `lighten = TRUE` +## opts in to the lighter-but-opaque tint used by the other area types (#646). +## (The default saturated look is already covered by the `spineplot_yby` case +## above.) f = function() { tinyplot( Species ~ Sepal.Width | Species, data = iris, - type = type_spineplot(breaks = 4, lighten = FALSE), + type = type_spineplot(breaks = 4, lighten = TRUE), palette = "Pastel 1" ) } -expect_snapshot_plot(f, label = "spineplot_yby_lighten_false") +expect_snapshot_plot(f, label = "spineplot_yby_lighten_true") From 9a8c51876caf4628076cecd534d5a5aef5c7bf24 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Wed, 1 Jul 2026 17:48:44 -0700 Subject: [PATCH 09/18] news --- NEWS.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/NEWS.md b/NEWS.md index 56f32977..23061573 100644 --- a/NEWS.md +++ b/NEWS.md @@ -113,15 +113,18 @@ New theme features: Note that this does imply a change for `"barplot"` types, which previously used a slightly darker `"grey"` (to match base R's `barplot()`), but we decided internal consistency was the more important feature to prioritize. -- Building on the above, _multi-group_ `"boxplot"`, `"violin"`, `"barplot"`, and - `"spineplot"` (grouped `y` variable) displays now also use the - lighter-but-opaque fill, so that grouped plots match their single-group - counterparts (previously grouped barplots used dark saturated fills and grouped - boxplots/violins used semi-transparent fills). Each type gains a `lighten` - argument (default `TRUE`); set `type_barplot(lighten = FALSE)` (etc.) to recover - the fully-saturated palette colours. `"histogram"` is deliberately excluded, - since semi-transparency remains preferable for overlapping grouped - distributions. (#646 @grantmcdermott @zeileis) +- Building on the above, _multi-group_ `"boxplot"`, `"violin"`, and `"barplot"` + displays now also use the lighter-but-opaque fill, so that grouped plots match + their single-group counterparts (previously grouped barplots used dark + saturated fills and grouped boxplots/violins used semi-transparent fills). Each + type gains a `lighten` argument (default `TRUE`); set + `type_barplot(lighten = FALSE)` (etc.) to recover the fully-saturated palette + colours. `"histogram"` is deliberately excluded, since semi-transparency + remains preferable for overlapping grouped distributions. `"spineplot"` also + gains a `lighten` argument (only relevant for the `y == by` case), but defaults + to `FALSE` (saturated fills): its tiles abut one another with no gap, so the + darker fills read better against their matching border colours. + (#646 @grantmcdermott @zeileis) - Relatedly, transparency requested via `fill` or `alpha` is now layered _on top of_ the (lightened) fill rather than the saturated base colour. This fixes a counterintuitive case where adding an alpha fill (e.g. `fill = 0.7`) From d1d3922ae9e8940a2ba7f62f19f22f114e0d1475 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Wed, 1 Jul 2026 17:52:24 -0700 Subject: [PATCH 10/18] vignettes --- vignettes/gallery_figs/barplot-meat.R | 2 +- vignettes/gallery_figs/likert.R | 2 +- vignettes/gallery_figs/spineplot-titanic.R | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/vignettes/gallery_figs/barplot-meat.R b/vignettes/gallery_figs/barplot-meat.R index bbdc1762..75209082 100644 --- a/vignettes/gallery_figs/barplot-meat.R +++ b/vignettes/gallery_figs/barplot-meat.R @@ -12,7 +12,7 @@ tinyplot( consumption ~ decade | type, facet = country ~ 1, data = meat, - type = "barplot", + type = "barplot", lighten = FALSE, grid = "X", flip = TRUE, legend = list("bottom!", title = NULL), diff --git a/vignettes/gallery_figs/likert.R b/vignettes/gallery_figs/likert.R index f371b884..1348b20b 100644 --- a/vignettes/gallery_figs/likert.R +++ b/vignettes/gallery_figs/likert.R @@ -19,7 +19,7 @@ pal = c("#b2182b", "#ef8a62", "#67a9cf", "#2166ac", "grey") plt( share ~ question | response, data = lik, - type = "barplot", center = TRUE, offset = "Unsure", + type = type_barplot(lighten = FALSE, center = TRUE, offset = "Unsure"), flip = TRUE, xlab = NA, ylab = NA, yaxl = "percent", legend = list("top!", title = FALSE), theme = list("clean2", palette.qualitative = pal), diff --git a/vignettes/gallery_figs/spineplot-titanic.R b/vignettes/gallery_figs/spineplot-titanic.R index 26d56e52..e4fd6c60 100644 --- a/vignettes/gallery_figs/spineplot-titanic.R +++ b/vignettes/gallery_figs/spineplot-titanic.R @@ -3,7 +3,7 @@ ttnc = as.data.frame(Titanic) tinyplot( Survived ~ Sex | Class, facet = "by", facet.args = list(nrow = 1), - legend = FALSE, + legend = FALSE, lwd = 0, data = ttnc, type = "spineplot", weights = Freq, theme = "void", axes = "t", From 75ec277afb4bf0c87167023ff575f5a8f83ba85e Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Thu, 2 Jul 2026 10:38:58 -0700 Subject: [PATCH 11/18] update barplot examples --- R/type_barplot.R | 22 ++++++++++++---------- man/type_barplot.Rd | 22 ++++++++++++---------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/R/type_barplot.R b/R/type_barplot.R index 5e4a231d..d75d8372 100644 --- a/R/type_barplot.R +++ b/R/type_barplot.R @@ -51,7 +51,6 @@ #' tinyplot(~ cyl, data = mtcars, type = "barplot") #' tinyplot(~ cyl | vs, data = mtcars, type = "barplot") #' tinyplot(~ cyl | vs, data = mtcars, type = "barplot", beside = TRUE) -#' tinyplot(~ cyl | vs, data = mtcars, type = "barplot", beside = TRUE, fill = 0.2) #' #' # Reorder x variable categories either by their character levels or numeric indexes #' tinyplot(~ cyl, data = mtcars, type = "barplot", xlevels = c("8", "6", "4")) @@ -62,14 +61,14 @@ #' # `tinyplot(..., width = )` argument. It's safer to pass these args #' # through the `type_barplot()` functional equivalent. #' tinyplot( -#' ~ cyl | vs, data = mtcars, fill = 0.2, +#' ~ cyl | vs, data = mtcars, #' type = type_barplot(beside = TRUE, drop.zeros = TRUE, width = 0.65) #' ) #' #' # Example for numeric y aggregated by x (default: FUN = mean) + facets #' tinyplot( #' extra ~ ID | group, facet = "by", data = sleep, -#' type = "barplot", fill = 0.6, +#' type = "barplot", #' theme = "clean2" #' ) #' @@ -77,18 +76,19 @@ #' tinyplot( #' Freq ~ Sex | Survived, data = as.data.frame(Titanic), #' facet = ~ Class, facet.args = list(nrow = 1), -#' type = "barplot", flip = TRUE, fill = 0.6, +#' type = "barplot", flip = TRUE, #' theme = "clean2" #' ) #' -#' # Centered barplot for conditional proportions of hair color (black/brown vs. -#' # red/blond) given eye color and sex +#' # 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. #' hec = as.data.frame(proportions(HairEyeColor, 2:3)) #' hcols = c("black", "sienna", "indianred", "goldenrod") #' tinyplot( #' Freq ~ Eye | Hair, data = hec, #' facet = ~ Sex, facet.args = list(ncol = 1), -#' type = "barplot", center = TRUE, +#' type = type_barplot(center = TRUE, lighten = FALSE), #' flip = TRUE, yaxl = "percent", #' theme = list("clean2", palette.qualitative = hcols) #' ) @@ -102,7 +102,9 @@ #' d$offset = c(0, cumsum(d$value[1:3]), 0) #' tinyplot( #' value ~ item | I(value < 0), data = d, -#' type = type_barplot(offset = d$offset), legend = FALSE +#' type = type_barplot(offset = d$offset, lighten = FALSE), +#' col = NA, # (optional: turn off border) +#' legend = FALSE #' ) #' tinyplot_add(type = type_vline(4.5), lty = 2) #' @@ -126,9 +128,9 @@ #' pal = c("#b2182b", "#ef8a62", "#67a9cf", "#2166ac", "grey") #' tinyplot( #' share ~ question | response, data = lik, -#' type = "barplot", center = TRUE, offset = "Unsure", +#' type = type_barplot(center = TRUE, offset = "Unsure", lighten = FALSE), #' flip = TRUE, xlab = NA, ylab = NA, yaxl = "percent", -#' legend = list("top!", title = NULL), +#' legend = list("top!", title = FALSE), #' theme = list("clean2", palette.qualitative = pal), #' main = "Hypothetical Likert example with category offset" #' ) diff --git a/man/type_barplot.Rd b/man/type_barplot.Rd index c8ac6915..9867fbe6 100644 --- a/man/type_barplot.Rd +++ b/man/type_barplot.Rd @@ -78,7 +78,6 @@ using some function (default: mean). tinyplot(~ cyl, data = mtcars, type = "barplot") tinyplot(~ cyl | vs, data = mtcars, type = "barplot") tinyplot(~ cyl | vs, data = mtcars, type = "barplot", beside = TRUE) -tinyplot(~ cyl | vs, data = mtcars, type = "barplot", beside = TRUE, fill = 0.2) # Reorder x variable categories either by their character levels or numeric indexes tinyplot(~ cyl, data = mtcars, type = "barplot", xlevels = c("8", "6", "4")) @@ -89,14 +88,14 @@ tinyplot(~ cyl, data = mtcars, type = "barplot", xlevels = 3:1) # `tinyplot(..., width = )` argument. It's safer to pass these args # through the `type_barplot()` functional equivalent. tinyplot( - ~ cyl | vs, data = mtcars, fill = 0.2, + ~ cyl | vs, data = mtcars, type = type_barplot(beside = TRUE, drop.zeros = TRUE, width = 0.65) ) # Example for numeric y aggregated by x (default: FUN = mean) + facets tinyplot( extra ~ ID | group, facet = "by", data = sleep, - type = "barplot", fill = 0.6, + type = "barplot", theme = "clean2" ) @@ -104,18 +103,19 @@ tinyplot( tinyplot( Freq ~ Sex | Survived, data = as.data.frame(Titanic), facet = ~ Class, facet.args = list(nrow = 1), - type = "barplot", flip = TRUE, fill = 0.6, + type = "barplot", flip = TRUE, theme = "clean2" ) -# Centered barplot for conditional proportions of hair color (black/brown vs. -# red/blond) given eye color and sex +# 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. hec = as.data.frame(proportions(HairEyeColor, 2:3)) hcols = c("black", "sienna", "indianred", "goldenrod") tinyplot( Freq ~ Eye | Hair, data = hec, facet = ~ Sex, facet.args = list(ncol = 1), - type = "barplot", center = TRUE, + type = type_barplot(center = TRUE, lighten = FALSE), flip = TRUE, yaxl = "percent", theme = list("clean2", palette.qualitative = hcols) ) @@ -129,7 +129,9 @@ d$item = factor(d$item, levels = d$item) d$offset = c(0, cumsum(d$value[1:3]), 0) tinyplot( value ~ item | I(value < 0), data = d, - type = type_barplot(offset = d$offset), legend = FALSE + type = type_barplot(offset = d$offset, lighten = FALSE), + col = NA, # (optional: turn off border) + legend = FALSE ) tinyplot_add(type = type_vline(4.5), lty = 2) @@ -153,9 +155,9 @@ lik$share = c( # proportions summing to 1 within each question pal = c("#b2182b", "#ef8a62", "#67a9cf", "#2166ac", "grey") tinyplot( share ~ question | response, data = lik, - type = "barplot", center = TRUE, offset = "Unsure", + type = type_barplot(center = TRUE, offset = "Unsure", lighten = FALSE), flip = TRUE, xlab = NA, ylab = NA, yaxl = "percent", - legend = list("top!", title = NULL), + legend = list("top!", title = FALSE), theme = list("clean2", palette.qualitative = pal), main = "Hypothetical Likert example with category offset" ) From 9733e25d15e1483112d3c705abe614ba780ec80c Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Thu, 2 Jul 2026 10:56:37 -0700 Subject: [PATCH 12/18] update boxplot examples --- R/type_boxplot.R | 25 +++++++++++++++++++------ man/type_boxplot.Rd | 25 +++++++++++++++++++------ 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/R/type_boxplot.R b/R/type_boxplot.R index 52f2f604..781bd8a7 100644 --- a/R/type_boxplot.R +++ b/R/type_boxplot.R @@ -13,19 +13,32 @@ #' `FALSE` to use the fully-saturated palette colour(s) instead. #' @examples #' # "boxplot" type convenience string -#' tinyplot(count ~ spray, data = InsectSprays, type = "boxplot") +#' tinyplot(weight ~ feed, data = chickwts, type = "boxplot") #' #' # Note: Specifying the type here is redundant. Like base plot, tinyplot #' # automatically produces a boxplot if x is a factor and y is numeric -#' tinyplot(count ~ spray, data = InsectSprays) +#' tinyplot(weight ~ feed, data = chickwts) #' -#' # Grouped boxplot example -#' tinyplot(len ~ dose | supp, data = ToothGrowth, type = "boxplot") +#' # For flipped boxplots, it's usually better to use a dynamic theme to +#' # accomodate (horizontal) y-axis labels +#' tinyplot(weight ~ feed, data = chickwts, flip = TRUE, theme = "dynamic") +#' +#' # Grouped boxplot example using a different dataset +#' # (C.f., the final example in `?boxplot`) +#' tinyplot( +#' len ~ dose | supp, data = ToothGrowth, type = "boxplot", +#' main = "Guinea Pigs' Tooth Growth", +#' legend = list(title = "Supplement"), +#' xlab = "Vitamin C dose mg", ylab = "tooth length" +#' ) #' #' # Use `type_boxplot()` to pass extra arguments for customization #' tinyplot( -#' len ~ dose | supp, data = ToothGrowth, lty = 1, -#' type = type_boxplot(boxwex = 0.3, staplewex = 0, outline = FALSE) +#' len ~ dose | supp, data = ToothGrowth, +#' type = type_boxplot(boxwex = 0.3, staplewex = 0, outline = FALSE), lty = 1, +#' legend = list(title = "Supplement"), +#' main = "Guinea Pigs' Tooth Growth", +#' xlab = "Vitamin C dose mg", ylab = "tooth length" #' ) #' @export type_boxplot = function( diff --git a/man/type_boxplot.Rd b/man/type_boxplot.Rd index 50140bac..318b7ef8 100644 --- a/man/type_boxplot.Rd +++ b/man/type_boxplot.Rd @@ -63,18 +63,31 @@ numeric. } \examples{ # "boxplot" type convenience string -tinyplot(count ~ spray, data = InsectSprays, type = "boxplot") +tinyplot(weight ~ feed, data = chickwts, type = "boxplot") # Note: Specifying the type here is redundant. Like base plot, tinyplot # automatically produces a boxplot if x is a factor and y is numeric -tinyplot(count ~ spray, data = InsectSprays) +tinyplot(weight ~ feed, data = chickwts) -# Grouped boxplot example -tinyplot(len ~ dose | supp, data = ToothGrowth, type = "boxplot") +# For flipped boxplots, it's usually better to use a dynamic theme to +# accomodate (horizontal) y-axis labels +tinyplot(weight ~ feed, data = chickwts, flip = TRUE, theme = "dynamic") + +# Grouped boxplot example using a different dataset +# (C.f., the final example in `?boxplot`) +tinyplot( + len ~ dose | supp, data = ToothGrowth, type = "boxplot", + main = "Guinea Pigs' Tooth Growth", + legend = list(title = "Supplement"), + xlab = "Vitamin C dose mg", ylab = "tooth length" +) # Use `type_boxplot()` to pass extra arguments for customization tinyplot( - len ~ dose | supp, data = ToothGrowth, lty = 1, - type = type_boxplot(boxwex = 0.3, staplewex = 0, outline = FALSE) + len ~ dose | supp, data = ToothGrowth, + type = type_boxplot(boxwex = 0.3, staplewex = 0, outline = FALSE), lty = 1, + legend = list(title = "Supplement"), + main = "Guinea Pigs' Tooth Growth", + xlab = "Vitamin C dose mg", ylab = "tooth length" ) } From e9015a2634e37838be831e968c1c9179707ea117 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Thu, 2 Jul 2026 11:08:50 -0700 Subject: [PATCH 13/18] update violin examples --- R/type_violin.R | 39 ++++++++++++++++++++------------------- man/type_violin.Rd | 41 +++++++++++++++++++++-------------------- 2 files changed, 41 insertions(+), 39 deletions(-) diff --git a/R/type_violin.R b/R/type_violin.R index 08112154..daafe96c 100644 --- a/R/type_violin.R +++ b/R/type_violin.R @@ -20,36 +20,37 @@ #' #' @examples #' # "violin" type convenience string -#' tinyplot(count ~ spray, data = InsectSprays, type = "violin") +#' tinyplot(weight ~ feed, data = chickwts, type = "violin") #' -#' # aside: to match the defaults of `ggplot2::geom_violin()`, use `trim = TRUE` -#' # and `joint.bw = FALSE` -#' tinyplot(count ~ spray, data = InsectSprays, type = "violin", -#' trim = TRUE, joint.bw = FALSE) +#' # to match the defaults of `ggplot2::geom_violin()`, use `trim = TRUE` and +#' # `joint.bw = FALSE` +#' tinyplot( +#' weight ~ feed, data = chickwts, +#' # type = type_violin(trim = TRUE, joint.bw = FALSE) # same but see final ex. +#' type = "violin", trim = TRUE, joint.bw = FALSE +#' ) #' -#' # use flip = TRUE to reorient the axes -#' tinyplot(count ~ spray, data = InsectSprays, type = "violin", flip = TRUE) -#' -#' # for flipped plots with long group labels, it's better to use a theme for -#' # dynamic plot resizing -#' tinytheme("clean") -#' tinyplot(weight ~ feed, data = chickwts, type = "violin", flip = TRUE) +#' # For flipped violin plots, it's usually better to use a dynamic theme to +#' # accomodate (horizontal) y-axis labels +#' tinyplot( +#' weight ~ feed, data = chickwts, type = "violin", flip = TRUE, +#' theme = "dynamic" # or "clean(2)", "classic", "minimal", etc. +#' ) #' #' # you can group by the x var to add colour (here with the original orientation) #' tinyplot(weight ~ feed | feed, data = chickwts, type = "violin", legend = FALSE) #' #' # dodged grouped violin plot example (different dataset) -#' tinyplot(len ~ dose | supp, data = ToothGrowth, type = "violin", fill = 0.2) +#' tinyplot(len ~ dose | supp, data = ToothGrowth, type = "violin") #' #' # note: above we relied on `...` argument passing alongside the "violin" #' # type convenience string. But this won't work for `width`, since it will #' # clash with the top-level `tinyplot(..., width = )` arg. To ensure -#' # correct arg passing, it's safer to use the formal `type_violin()` option. -#' tinyplot(len ~ dose | supp, data = ToothGrowth, fill = 0.2, -#' type = type_violin(width = 0.8)) -#' -#' # reset theme -#' tinytheme() +#' # correct arg passing, it's safer to use the functional `type_violin()` type. +#' tinyplot( +#' len ~ dose | supp, data = ToothGrowth, +#' type = type_violin(width = 0.75) +#' ) #' #' @importFrom stats density weighted.mean #' @importFrom stats bw.SJ bw.bcv bw.nrd bw.nrd0 bw.ucv diff --git a/man/type_violin.Rd b/man/type_violin.Rd index cbc45d76..2469c3bd 100644 --- a/man/type_violin.Rd +++ b/man/type_violin.Rd @@ -69,35 +69,36 @@ bandwidth selection and kernel types. } \examples{ # "violin" type convenience string -tinyplot(count ~ spray, data = InsectSprays, type = "violin") - -# aside: to match the defaults of `ggplot2::geom_violin()`, use `trim = TRUE` -# and `joint.bw = FALSE` -tinyplot(count ~ spray, data = InsectSprays, type = "violin", - trim = TRUE, joint.bw = FALSE) - -# use flip = TRUE to reorient the axes -tinyplot(count ~ spray, data = InsectSprays, type = "violin", flip = TRUE) +tinyplot(weight ~ feed, data = chickwts, type = "violin") + +# to match the defaults of `ggplot2::geom_violin()`, use `trim = TRUE` and +# `joint.bw = FALSE` +tinyplot( + weight ~ feed, data = chickwts, + # type = type_violin(trim = TRUE, joint.bw = FALSE) # same but see final ex. + type = "violin", trim = TRUE, joint.bw = FALSE +) -# for flipped plots with long group labels, it's better to use a theme for -# dynamic plot resizing -tinytheme("clean") -tinyplot(weight ~ feed, data = chickwts, type = "violin", flip = TRUE) +# For flipped violin plots, it's usually better to use a dynamic theme to +# accomodate (horizontal) y-axis labels +tinyplot( + weight ~ feed, data = chickwts, type = "violin", flip = TRUE, + theme = "dynamic" # or "clean(2)", "classic", "minimal", etc. +) # you can group by the x var to add colour (here with the original orientation) tinyplot(weight ~ feed | feed, data = chickwts, type = "violin", legend = FALSE) # dodged grouped violin plot example (different dataset) -tinyplot(len ~ dose | supp, data = ToothGrowth, type = "violin", fill = 0.2) +tinyplot(len ~ dose | supp, data = ToothGrowth, type = "violin") # note: above we relied on `...` argument passing alongside the "violin" # type convenience string. But this won't work for `width`, since it will # clash with the top-level `tinyplot(..., width = )` arg. To ensure -# correct arg passing, it's safer to use the formal `type_violin()` option. -tinyplot(len ~ dose | supp, data = ToothGrowth, fill = 0.2, - type = type_violin(width = 0.8)) - -# reset theme -tinytheme() +# correct arg passing, it's safer to use the functional `type_violin()` type. +tinyplot( + len ~ dose | supp, data = ToothGrowth, + type = type_violin(width = 0.75) +) } From c9ade2ed6ec0fa991a76845bd76c9aae98e3ac18 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Thu, 2 Jul 2026 12:26:40 -0700 Subject: [PATCH 14/18] update spineplot examples --- R/type_spineplot.R | 61 +++++++++++++++++++++++++------------------ man/type_spineplot.Rd | 57 +++++++++++++++++++++++----------------- 2 files changed, 68 insertions(+), 50 deletions(-) diff --git a/R/type_spineplot.R b/R/type_spineplot.R index 86b228c5..2b4f00d0 100644 --- a/R/type_spineplot.R +++ b/R/type_spineplot.R @@ -27,55 +27,64 @@ #' tinyplot(Species ~ Sepal.Width, data = iris) #' #' # Use `type_spineplot()` to pass extra arguments for customization -#' tinyplot(Species ~ Sepal.Width, data = iris, type = type_spineplot(breaks = 4)) +#' tinyplot( +#' Species ~ Sepal.Width, data = iris, +#' type = type_spineplot(breaks = 4) +#' ) #' -#' p = palette.colors(3, "Pastel 1") -#' tinyplot(Species ~ Sepal.Width, data = iris, type = type_spineplot(breaks = 4, col = p)) -#' rm(p) +#' # Passing custom colors to the y-axis categories +#' tinyplot( +#' Species ~ Sepal.Width, data = iris, +#' type = type_spineplot(breaks = 4, col = palette.colors(3, "Pastel 1")) +#' ) #' #' # More idiomatic tinyplot way of drawing the previous plot: use y == by #' tinyplot( -#' Species ~ Sepal.Width | Species, data = iris, type = type_spineplot(breaks = 4), +#' Species ~ Sepal.Width | Species, data = iris, +#' type = type_spineplot(breaks = 4), #' palette = "Pastel 1", legend = FALSE #' ) #' -#' # Grouped and faceted spineplots. The Titanic dataset is pre-tabulated, so we -#' # pass its frequency counts via the top-level `weights` argument (which -#' # supports non-standard evaluation in the formula method). +#' ## Grouped and faceted spineplots #' #' ttnc = as.data.frame(Titanic) -#' +#' +#' # Note:The Titanic (ttnc) dataset is pre-tabulated, so we pass its frequency +#' # counts via the top-level `weights` argument (accepted via non-standard +#' # evaluation in the formula method). #' tinyplot( #' Survived ~ Sex, facet = ~ Class, data = ttnc, #' # type_spineplot(weights = ttnc$Freq), ## same thing but not NSE -#' type = "spineplot", -#' weights = Freq +#' type = "spineplot", weights = Freq #' ) -#' -#' # For grouped "by" spineplots, it's better visually to facet as well +#' +#' # Reorder x and y variable categories either by their character levels or +#' # numeric indexes. (Here we combine a top-level `weights` with constructor- +#' # level arguments passed through `type_spineplot()`.) #' tinyplot( -#' Survived ~ Sex | Class, facet = "by", data = ttnc, -#' type = "spineplot", +#' Survived ~ Sex, facet = ~ Class, data = ttnc, +#' type = type_spineplot(xlevels = c("Female", "Male"), ylevels = 2:1), #' weights = Freq #' ) #' -#' # Fancier version. Note the smart inheritance of spacing etc. +#' # For (colour) grouped "by" spineplots, it's visually better to facet too #' tinyplot( -#' Survived ~ Sex | Class, facet = "by", data = ttnc, -#' type = "spineplot", -#' weights = Freq, -#' palette = "Dark 2", facet.args = list(nrow = 1), axes = "t" +#' Survived ~ Sex | Class, data = ttnc, +#' facet = "by", +#' type = "spineplot", weights = Freq #' ) #' -#' # Reorder x and y variable categories either by their character levels or -#' # numeric indexes. (Here we combine a top-level `weights` with constructor- -#' # level arguments passed through `type_spineplot()`.) +#' # Fancier version. Note the smart inheritance of spacing etc. #' tinyplot( -#' Survived ~ Sex, facet = ~ Class, data = ttnc, weights = Freq, -#' type = type_spineplot(xlevels = c("Female", "Male"), ylevels = 2:1) +#' Survived ~ Sex | Class, data = ttnc, +#' facet = "by", facet.args = list(nrow = 1), +#' type = "spineplot", weights = Freq, +#' theme = "void", axes = "t", lty = 0, legend = FALSE, +#' main = "Who survived the Titanic disaster?", +#' sub = "Frequencies by boarding class and sex" #' ) #' -#' # Note: It's possible to use "by" on its own (without faceting), but the +#' # Aside: It's possible to use "by" on its own (without faceting), but the #' # overlaid result isn't great. We will likely overhaul this behaviour in a #' # future version of tinyplot... #' tinyplot(Survived ~ Sex | Class, data = ttnc, diff --git a/man/type_spineplot.Rd b/man/type_spineplot.Rd index 9d5080e0..aaeb70f2 100644 --- a/man/type_spineplot.Rd +++ b/man/type_spineplot.Rd @@ -72,55 +72,64 @@ tinyplot(Species ~ Sepal.Width, data = iris, type = "spineplot") tinyplot(Species ~ Sepal.Width, data = iris) # Use `type_spineplot()` to pass extra arguments for customization -tinyplot(Species ~ Sepal.Width, data = iris, type = type_spineplot(breaks = 4)) +tinyplot( + Species ~ Sepal.Width, data = iris, + type = type_spineplot(breaks = 4) +) -p = palette.colors(3, "Pastel 1") -tinyplot(Species ~ Sepal.Width, data = iris, type = type_spineplot(breaks = 4, col = p)) -rm(p) +# Passing custom colors to the y-axis categories +tinyplot( + Species ~ Sepal.Width, data = iris, + type = type_spineplot(breaks = 4, col = palette.colors(3, "Pastel 1")) +) # More idiomatic tinyplot way of drawing the previous plot: use y == by tinyplot( - Species ~ Sepal.Width | Species, data = iris, type = type_spineplot(breaks = 4), + Species ~ Sepal.Width | Species, data = iris, + type = type_spineplot(breaks = 4), palette = "Pastel 1", legend = FALSE ) -# Grouped and faceted spineplots. The Titanic dataset is pre-tabulated, so we -# pass its frequency counts via the top-level `weights` argument (which -# supports non-standard evaluation in the formula method). +## Grouped and faceted spineplots ttnc = as.data.frame(Titanic) +# Note:The Titanic (ttnc) dataset is pre-tabulated, so we pass its frequency +# counts via the top-level `weights` argument (accepted via non-standard +# evaluation in the formula method). tinyplot( Survived ~ Sex, facet = ~ Class, data = ttnc, # type_spineplot(weights = ttnc$Freq), ## same thing but not NSE - type = "spineplot", - weights = Freq + type = "spineplot", weights = Freq ) -# For grouped "by" spineplots, it's better visually to facet as well +# Reorder x and y variable categories either by their character levels or +# numeric indexes. (Here we combine a top-level `weights` with constructor- +# level arguments passed through `type_spineplot()`.) tinyplot( - Survived ~ Sex | Class, facet = "by", data = ttnc, - type = "spineplot", + Survived ~ Sex, facet = ~ Class, data = ttnc, + type = type_spineplot(xlevels = c("Female", "Male"), ylevels = 2:1), weights = Freq ) -# Fancier version. Note the smart inheritance of spacing etc. +# For (colour) grouped "by" spineplots, it's visually better to facet too tinyplot( - Survived ~ Sex | Class, facet = "by", data = ttnc, - type = "spineplot", - weights = Freq, - palette = "Dark 2", facet.args = list(nrow = 1), axes = "t" + Survived ~ Sex | Class, data = ttnc, + facet = "by", + type = "spineplot", weights = Freq ) -# Reorder x and y variable categories either by their character levels or -# numeric indexes. (Here we combine a top-level `weights` with constructor- -# level arguments passed through `type_spineplot()`.) +# Fancier version. Note the smart inheritance of spacing etc. tinyplot( - Survived ~ Sex, facet = ~ Class, data = ttnc, weights = Freq, - type = type_spineplot(xlevels = c("Female", "Male"), ylevels = 2:1) + Survived ~ Sex | Class, data = ttnc, + facet = "by", facet.args = list(nrow = 1), + type = "spineplot", weights = Freq, + theme = "void", axes = "t", lty = 0, legend = FALSE, + main = "Who survived the Titanic disaster?", + sub = "Frequencies by boarding class and sex" ) -# Note: It's possible to use "by" on its own (without faceting), but the +# Aside: It's possible to use "by" on its own (without faceting), but the # overlaid result isn't great. We will likely overhaul this behaviour in a # future version of tinyplot... tinyplot(Survived ~ Sex | Class, data = ttnc, From 1c743d6a8412060dbbc446090e020f5a866d42cd Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Thu, 2 Jul 2026 13:57:15 -0700 Subject: [PATCH 15/18] spineplot touchups and test updates --- R/legend.R | 9 +- R/type_spineplot.R | 26 ++- inst/tinytest/_tinysnapshot/df_pairs.svg | 4 - inst/tinytest/_tinysnapshot/df_pairs_by.svg | 12 -- .../_tinysnapshot/df_pairs_labs_frames.svg | 12 -- inst/tinytest/test-fill_lighten.R | 178 ------------------ 6 files changed, 14 insertions(+), 227 deletions(-) delete mode 100644 inst/tinytest/test-fill_lighten.R diff --git a/R/legend.R b/R/legend.R index 14b21ad4..45c0e7bc 100644 --- a/R/legend.R +++ b/R/legend.R @@ -615,13 +615,8 @@ build_legend_args = function( # The swatch fill comes via `bg` (resolved in prepare_legend for the grouped # `y_by` case; NULL otherwise, falling back to the group colour). legend_args[["pt.bg"]] = legend_args[["pt.bg"]] %||% bg %||% legend_args[["col"]] - # Grouped spineplot tiles always carry black separating borders, so the - # bordered swatch (pt.lwd turned on for `y_by` in data_spineplot) matches - # with a black border too. A group-coloured border would vanish against a - # pale fill (e.g. a pastel palette). - if (!is.null(legend_args[["pt.lwd"]]) && any(legend_args[["pt.lwd"]] > 0)) { - legend_args[["col"]] = par("fg") - } + # Conversely, the border colour is always black + legend_args[["col"]] = par("fg") } else if (identical(type, "ridge") && isFALSE(gradient)) { legend_args[["pt.bg"]] = legend_args[["pt.bg"]] %||% sapply(legend_args[["col"]], function(ccol) seq_palette(ccol, n = 2)[2]) } else { diff --git a/R/type_spineplot.R b/R/type_spineplot.R index 2b4f00d0..e3ec4dde 100644 --- a/R/type_spineplot.R +++ b/R/type_spineplot.R @@ -106,7 +106,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) { fun = function(settings, ...) { - env2env(settings, environment(), c("datapoints", "xlim", "ylim", "facet", "facet.args", "by", "xaxb", "yaxb", "null_by", "null_facet", "col", "bg", "axes", "xaxt", "yaxt", "lwd")) + 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 ## process weights: a top-level `weights` column (carried on datapoints @@ -280,6 +280,11 @@ data_spineplot = function(off = NULL, breaks = NULL, xlevels = xlevels, ylevels axes_orig = axes xaxt_orig = xaxt yaxt_orig = yaxt + # `frame.plot` defaults to TRUE for numeric-x spinograms (the outer box + # is drawn by draw_spineplot below, after the tiles); NULL/unset counts + # as TRUE. Preserve the user's choice before overwriting it, so the box + # honours the top-level `frame.plot` rather than the tile-border `lwd`. + frameplot_orig = !isFALSE(frame.plot) axes = FALSE frame.plot = FALSE @@ -299,6 +304,7 @@ data_spineplot = function(off = NULL, breaks = NULL, xlevels = xlevels, ylevels yaxlabels = yaxlabels, breaks = breaks, axes = axes_orig, + frame.plot = frameplot_orig, xaxt = xaxt_orig, yaxt = yaxt_orig, null_by = null_by, @@ -307,21 +313,10 @@ data_spineplot = function(off = NULL, breaks = NULL, xlevels = xlevels, ylevels ) # legend customizations - # lty = 0 so the filled-square swatch has no line component; otherwise - # legend() reserves `seg.len` horizontal space for a line segment beside - # the square, leaving an odd gap before the label (matches type_barplot). + # Mirror type_barplot() settings$legend_args[["lty"]] = settings$legend_args[["lty"]] %||% 0 settings$legend_args[["pch"]] = settings$legend_args[["pch"]] %||% 22 settings$legend_args[["pt.cex"]] = settings$legend_args[["pt.cex"]] %||% 3.5 - # Spineplot tiles carry black separating borders, so the swatches get a - # matching (black) border too (the colour override happens in - # build_legend_args when the swatch border is drawn). The swatch border - # width tracks the tile border width `lwd` -- the `pch = 22` swatch reads - # its border width from `pt.lwd` -- so e.g. `lwd = 0` drops both. (Base - # `legend()` can't dash a filled-square border, so `lty` only affects the - # tiles, not the swatch -- as with the other area types.) - spine_pt_lwd = if (is.null(lwd)) 1 else lwd - settings$legend_args[["pt.lwd"]] = settings$legend_args[["pt.lwd"]] %||% spine_pt_lwd settings$legend_args[["y.intersp"]] = settings$legend_args[["y.intersp"]] %||% 1.25 settings$legend_args[["seg.len"]] = settings$legend_args[["seg.len"]] %||% 1.25 @@ -410,7 +405,10 @@ draw_spineplot = function(tol.ylab = 0.05, off = NULL, col = NULL, xaxlabels = N if (is_facet_position(if(flip) "bottom" else "right", ifacet, facet_window_args)) spine_axis(if (flip) 1 else 4, type = type_info[["yaxt"]], categorical = FALSE) } - if(!x.categorical && (is.null(ilwd) || ilwd > 0)) box() + # Outer box for numeric-x spinograms. This is a structural frame, so it + # follows the top-level `frame.plot` (via type_info) rather than the + # tile-border `lwd` -- otherwise `lwd = 0` would wrongly drop the box too. + if (!x.categorical && isTRUE(type_info[["frame.plot"]])) box() } return(fun) diff --git a/inst/tinytest/_tinysnapshot/df_pairs.svg b/inst/tinytest/_tinysnapshot/df_pairs.svg index 9395140d..bbbc3b71 100644 --- a/inst/tinytest/_tinysnapshot/df_pairs.svg +++ b/inst/tinytest/_tinysnapshot/df_pairs.svg @@ -2806,7 +2806,6 @@ 0.6 0.8 1.0 - @@ -2876,7 +2875,6 @@ 0.6 0.8 1.0 - @@ -2946,7 +2944,6 @@ 0.6 0.8 1.0 - @@ -3019,7 +3016,6 @@ 0.6 0.8 1.0 - diff --git a/inst/tinytest/_tinysnapshot/df_pairs_by.svg b/inst/tinytest/_tinysnapshot/df_pairs_by.svg index ca4ac7bd..194d1687 100644 --- a/inst/tinytest/_tinysnapshot/df_pairs_by.svg +++ b/inst/tinytest/_tinysnapshot/df_pairs_by.svg @@ -2803,7 +2803,6 @@ 0.6 0.8 1.0 - @@ -2833,7 +2832,6 @@ 0.6 0.8 1.0 - @@ -2858,7 +2856,6 @@ 0.6 0.8 1.0 - @@ -2904,7 +2901,6 @@ 0.6 0.8 1.0 - @@ -2938,7 +2934,6 @@ 0.6 0.8 1.0 - @@ -2967,7 +2962,6 @@ 0.6 0.8 1.0 - @@ -3013,7 +3007,6 @@ 0.6 0.8 1.0 - @@ -3047,7 +3040,6 @@ 0.6 0.8 1.0 - @@ -3076,7 +3068,6 @@ 0.6 0.8 1.0 - @@ -3123,7 +3114,6 @@ 0.6 0.8 1.0 - @@ -3158,7 +3148,6 @@ 0.6 0.8 1.0 - @@ -3188,7 +3177,6 @@ 0.6 0.8 1.0 - diff --git a/inst/tinytest/_tinysnapshot/df_pairs_labs_frames.svg b/inst/tinytest/_tinysnapshot/df_pairs_labs_frames.svg index 02a010ef..0ea7870b 100644 --- a/inst/tinytest/_tinysnapshot/df_pairs_labs_frames.svg +++ b/inst/tinytest/_tinysnapshot/df_pairs_labs_frames.svg @@ -3027,7 +3027,6 @@ 0.6 0.8 1.0 - @@ -3051,7 +3050,6 @@ 0.6 0.8 1.0 - @@ -3075,7 +3073,6 @@ 0.6 0.8 1.0 - @@ -3137,7 +3134,6 @@ 0.6 0.8 1.0 - @@ -3165,7 +3161,6 @@ 0.6 0.8 1.0 - @@ -3193,7 +3188,6 @@ 0.6 0.8 1.0 - @@ -3255,7 +3249,6 @@ 0.6 0.8 1.0 - @@ -3283,7 +3276,6 @@ 0.6 0.8 1.0 - @@ -3311,7 +3303,6 @@ 0.6 0.8 1.0 - @@ -3374,7 +3365,6 @@ 0.6 0.8 1.0 - @@ -3403,7 +3393,6 @@ 0.6 0.8 1.0 - @@ -3432,7 +3421,6 @@ 0.6 0.8 1.0 - diff --git a/inst/tinytest/test-fill_lighten.R b/inst/tinytest/test-fill_lighten.R deleted file mode 100644 index bdf6c44c..00000000 --- a/inst/tinytest/test-fill_lighten.R +++ /dev/null @@ -1,178 +0,0 @@ -source("helpers.R") - -# Platform-independent checks for the lighter-opaque fill logic (#646, #614). -# These assert on the colours resolved by the internal by_bg() helper, so they -# run everywhere (no snapshot rendering required). - -ac = grDevices::adjustcolor - -bb = function(...) { - tinyplot:::by_bg( - palette = NULL, ribbon.alpha = 0.2, adjustcolor = ac, ... - ) -} - -# Helper predicates on hex colours ------------------------------------------- - -# Fully opaque (alpha == FF), i.e. no transparency applied. -is_opaque = function(x) all(grepl("FF$", toupper(x)) | nchar(x) %in% c(7L) | !grepl("^#", x)) -# Semi-transparent (alpha < FF), e.g. the legacy "33" ribbon.alpha fill. -is_semitransparent = function(x) all(grepl("^#.{6}(?!FF)..$", x, perl = TRUE)) -# A "light" tint: high luminance in all channels. -is_light = function(x) { - rgb = grDevices::col2rgb(x) - all(colMeans(rgb) > 180) -} - -# Activate a colour-forward theme so the qualitative palette starts with a -# chromatic colour (Tableau 10 -> blue), making the tint visible. -tinytheme("clean2") - -# Grouped barplot: default lighten = TRUE -> light, opaque tints --------------- -bar_light = bb( - bg = "by", fill = NULL, col = NULL, alpha = NULL, - by_ordered = FALSE, by_continuous = FALSE, ngrps = 3, - type = "barplot", by = factor(1:3), lighten = TRUE -) -expect_equal(length(bar_light), 3L) -expect_true(is_opaque(bar_light)) -expect_true(is_light(bar_light)) - -# Grouped barplot: lighten = FALSE -> saturated, opaque palette colours -------- -bar_dark = bb( - bg = "by", fill = NULL, col = NULL, alpha = NULL, - by_ordered = FALSE, by_continuous = FALSE, ngrps = 3, - type = "barplot", by = factor(1:3), lighten = FALSE -) -expect_true(is_opaque(bar_dark)) -expect_false(is_light(bar_dark)) -# The lightened fill should differ from the saturated one. -expect_false(isTRUE(all.equal(bar_light, bar_dark))) - -# Grouped boxplot: default lighten = TRUE -> light opaque (NOT semi-transparent) -box_light = bb( - bg = "by", fill = NULL, col = NULL, alpha = NULL, - by_ordered = FALSE, by_continuous = FALSE, ngrps = 3, - type = "boxplot", by = factor(1:3), lighten = TRUE -) -expect_true(is_opaque(box_light)) -expect_true(is_light(box_light)) - -# Grouped boxplot: lighten = FALSE -> legacy semi-transparent fill ------------- -box_legacy = bb( - bg = "by", fill = NULL, col = NULL, alpha = NULL, - by_ordered = FALSE, by_continuous = FALSE, ngrps = 3, - type = "boxplot", by = factor(1:3), lighten = FALSE -) -expect_true(is_semitransparent(box_legacy)) - -# Grouped violin mirrors barplot/boxplot --------------------------------------- -vio_light = bb( - bg = "by", fill = NULL, col = NULL, alpha = NULL, - by_ordered = FALSE, by_continuous = FALSE, ngrps = 3, - type = "violin", by = factor(1:3), lighten = TRUE -) -expect_true(is_opaque(vio_light)) -expect_true(is_light(vio_light)) - -# Single-group fill is lightened by default and matches grouped colour[1] ------ -bar_single = bb( - bg = NULL, fill = NULL, col = NULL, alpha = NULL, - by_ordered = FALSE, by_continuous = FALSE, ngrps = 1, - type = "barplot", by = NULL, lighten = TRUE -) -expect_equal(bar_single, bar_light[1]) - -# A numeric `fill` request layers transparency on top of the *lightened* base, -# so adding alpha lightens (never darkens) the interior (#646 follow-up). The -# base RGB should match the opaque light tint; only the alpha channel changes. -bar_fill07 = bb( - bg = 0.7, fill = NULL, col = NULL, alpha = NULL, - by_ordered = FALSE, by_continuous = FALSE, ngrps = 1, - type = "barplot", by = NULL, lighten = TRUE -) -expect_equal(substr(bar_fill07, 1, 7), substr(bar_single, 1, 7)) -expect_true(is_semitransparent(bar_fill07)) - -# An explicit fill colour is always honoured verbatim (never lightened) -------- -box_bisque = bb( - bg = "bisque", fill = NULL, col = NULL, alpha = NULL, - by_ordered = FALSE, by_continuous = FALSE, ngrps = 3, - type = "boxplot", by = factor(1:3), lighten = TRUE -) -expect_equal(box_bisque, rep("bisque", 3)) - -# Ordered groupings use a sequential palette and must NOT be lightened --------- -box_ordered = bb( - bg = "by", fill = NULL, col = NULL, alpha = NULL, - by_ordered = TRUE, by_continuous = FALSE, ngrps = 3, - type = "boxplot", by = ordered(1:3), lighten = TRUE -) -expect_false(is_light(box_ordered)) - -tinytheme() - -# No theme active: single-group area fills fall back to neutral "lightgray" ---- -bar_none = bb( - bg = NULL, fill = NULL, col = NULL, alpha = NULL, - by_ordered = FALSE, by_continuous = FALSE, ngrps = 1, - type = "barplot", by = NULL, lighten = TRUE -) -expect_equal(bar_none, "lightgray") - -# Grouped spineplot (`y_by`) fills follow the same lighter-opaque tint as the -# other area types (#646). draw_spineplot() lightens the per-group bands; the -# legend swatch fill is resolved in prepare_legend() into `bg`. Here we check the -# lighten_fill() helper directly against the resolved group colours. -tinytheme("clean2") -spine_seed = tinyplot:::by_col( - col = NULL, palette = NULL, alpha = NULL, by_ordered = FALSE, - by_continuous = FALSE, ngrps = 3, adjustcolor = ac -) -spine_light = tinyplot:::lighten_fill(spine_seed) -expect_true(is_opaque(spine_light)) -expect_true(is_light(spine_light)) -expect_false(isTRUE(all.equal(spine_light, spine_seed))) - -# Spineplot legend swatch: data_spineplot sets the swatch border width from the -# tile border width `lwd` (defaulting to 1), and build_legend_args() forces that -# border *black* whenever it is drawn (pt.lwd > 0), matching the tiles -- a -# group-coloured border would vanish against a pale fill. The fill arrives via -# `bg` (lightened in prepare_legend only when `lighten = TRUE`, which is opt-in -# for spineplots; saturated otherwise). Both fills are exercised below by -# passing `bg` explicitly. -build_swatch = function(bg, pt.lwd = 1) { - le = new.env(parent = emptyenv()) - tinyplot:::build_legend_args( - legend_env = le, legend = NULL, - legend_args = list(pch = 22, pt.lwd = pt.lwd), - by_dep = "g", lgnd_labs = c("a", "b", "c"), - type = "spineplot", pch = 22, lty = 1, lwd = 1, - col = spine_seed, bg = bg, cex = NULL, - gradient = FALSE - ) - le$args -} - -sw_light = build_swatch(bg = spine_light) -expect_equal(sw_light[["col"]], par("fg")) # black swatch border (matches tiles) -expect_true(is_light(sw_light[["pt.bg"]])) # lightened fill (from bg) -expect_equal(sw_light[["pt.lwd"]], 1) # border drawn - -sw_dark = build_swatch(bg = spine_seed) -expect_equal(sw_dark[["col"]], par("fg")) # black swatch border (matches tiles) -expect_equal(sw_dark[["pt.bg"]], spine_seed) # saturated fill (from bg) -expect_equal(sw_dark[["pt.lwd"]], 1) # border drawn - -# A thicker tile border (lwd) carries through to the swatch border width. -sw_thick = build_swatch(bg = spine_seed, pt.lwd = 3) -expect_equal(sw_thick[["col"]], par("fg")) # still black -expect_equal(sw_thick[["pt.lwd"]], 3) # thick border - -# `lwd = 0` (pt.lwd = 0) draws no swatch border, matching borderless tiles. The -# black-colour override is skipped, since no border is drawn. -sw_none = build_swatch(bg = spine_seed, pt.lwd = 0) -expect_equal(sw_none[["pt.lwd"]], 0) # no border -expect_false(identical(sw_none[["col"]], par("fg"))) # colour not overridden to black - -tinytheme() From 4e5f44580f026ce10ccda6344fed6fbe2a56e409 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Thu, 2 Jul 2026 13:58:45 -0700 Subject: [PATCH 16/18] gallery match --- altdoc/pkgdown.yml | 2 +- vignettes/gallery_figs/spineplot-titanic.R | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/altdoc/pkgdown.yml b/altdoc/pkgdown.yml index 5c18c76d..bec33e38 100644 --- a/altdoc/pkgdown.yml +++ b/altdoc/pkgdown.yml @@ -2,7 +2,7 @@ altdoc: 0.7.2 pandoc: '3.10' pkgdown: 2.1.3 pkgdown_sha: ~ -last_built: 2026-06-27T04:15:54+0000 +last_built: 2026-07-02T18:34:34+0000 urls: reference: https://grantmcdermott.com/tinyplot/man article: https://grantmcdermott.com/tinyplot/vignettes diff --git a/vignettes/gallery_figs/spineplot-titanic.R b/vignettes/gallery_figs/spineplot-titanic.R index e4fd6c60..ef178e12 100644 --- a/vignettes/gallery_figs/spineplot-titanic.R +++ b/vignettes/gallery_figs/spineplot-titanic.R @@ -3,7 +3,7 @@ ttnc = as.data.frame(Titanic) tinyplot( Survived ~ Sex | Class, facet = "by", facet.args = list(nrow = 1), - legend = FALSE, lwd = 0, + legend = FALSE, lty = 0, data = ttnc, type = "spineplot", weights = Freq, theme = "void", axes = "t", From bc44d2eae08ea2f43aa90ca2b531eead6d970573 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Thu, 2 Jul 2026 15:49:55 -0700 Subject: [PATCH 17/18] typos --- R/type_boxplot.R | 2 +- R/type_spineplot.R | 2 +- R/type_violin.R | 2 +- man/type_boxplot.Rd | 2 +- man/type_spineplot.Rd | 2 +- man/type_violin.Rd | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/R/type_boxplot.R b/R/type_boxplot.R index 781bd8a7..0e501b63 100644 --- a/R/type_boxplot.R +++ b/R/type_boxplot.R @@ -20,7 +20,7 @@ #' tinyplot(weight ~ feed, data = chickwts) #' #' # For flipped boxplots, it's usually better to use a dynamic theme to -#' # accomodate (horizontal) y-axis labels +#' # accommodate (horizontal) y-axis labels #' tinyplot(weight ~ feed, data = chickwts, flip = TRUE, theme = "dynamic") #' #' # Grouped boxplot example using a different dataset diff --git a/R/type_spineplot.R b/R/type_spineplot.R index e3ec4dde..d6496528 100644 --- a/R/type_spineplot.R +++ b/R/type_spineplot.R @@ -49,7 +49,7 @@ #' #' ttnc = as.data.frame(Titanic) #' -#' # Note:The Titanic (ttnc) dataset is pre-tabulated, so we pass its frequency +#' # Note: The Titanic (ttnc) dataset is pre-tabulated, so we pass its frequency #' # counts via the top-level `weights` argument (accepted via non-standard #' # evaluation in the formula method). #' tinyplot( diff --git a/R/type_violin.R b/R/type_violin.R index daafe96c..04cf2593 100644 --- a/R/type_violin.R +++ b/R/type_violin.R @@ -31,7 +31,7 @@ #' ) #' #' # For flipped violin plots, it's usually better to use a dynamic theme to -#' # accomodate (horizontal) y-axis labels +#' # accommodate (horizontal) y-axis labels #' tinyplot( #' weight ~ feed, data = chickwts, type = "violin", flip = TRUE, #' theme = "dynamic" # or "clean(2)", "classic", "minimal", etc. diff --git a/man/type_boxplot.Rd b/man/type_boxplot.Rd index 318b7ef8..0aeee668 100644 --- a/man/type_boxplot.Rd +++ b/man/type_boxplot.Rd @@ -70,7 +70,7 @@ tinyplot(weight ~ feed, data = chickwts, type = "boxplot") tinyplot(weight ~ feed, data = chickwts) # For flipped boxplots, it's usually better to use a dynamic theme to -# accomodate (horizontal) y-axis labels +# accommodate (horizontal) y-axis labels tinyplot(weight ~ feed, data = chickwts, flip = TRUE, theme = "dynamic") # Grouped boxplot example using a different dataset diff --git a/man/type_spineplot.Rd b/man/type_spineplot.Rd index aaeb70f2..041b56d6 100644 --- a/man/type_spineplot.Rd +++ b/man/type_spineplot.Rd @@ -94,7 +94,7 @@ tinyplot( ttnc = as.data.frame(Titanic) -# Note:The Titanic (ttnc) dataset is pre-tabulated, so we pass its frequency +# Note: The Titanic (ttnc) dataset is pre-tabulated, so we pass its frequency # counts via the top-level `weights` argument (accepted via non-standard # evaluation in the formula method). tinyplot( diff --git a/man/type_violin.Rd b/man/type_violin.Rd index 2469c3bd..b63c578b 100644 --- a/man/type_violin.Rd +++ b/man/type_violin.Rd @@ -80,7 +80,7 @@ tinyplot( ) # For flipped violin plots, it's usually better to use a dynamic theme to -# accomodate (horizontal) y-axis labels +# accommodate (horizontal) y-axis labels tinyplot( weight ~ feed, data = chickwts, type = "violin", flip = TRUE, theme = "dynamic" # or "clean(2)", "classic", "minimal", etc. From 4191ec55441578debfb93ece1b6bb998e8bf7b20 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Thu, 2 Jul 2026 16:00:48 -0700 Subject: [PATCH 18/18] don't revert to old boxplot transparency unless user makes explicit --- R/by_aesthetics.R | 7 ++++--- .../boxplot_groups_lighten_false.svg | 16 ++++++++-------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/R/by_aesthetics.R b/R/by_aesthetics.R index fe650733..babe936f 100755 --- a/R/by_aesthetics.R +++ b/R/by_aesthetics.R @@ -212,9 +212,10 @@ by_bg = function(bg, fill, col, palette, alpha, by_ordered, by_continuous, ngrps if (!is.null(alpha)) bg = apply_alpha(bg, alpha, adjustcolor) } - # Legacy semi-transparent fill: ribbon-type plots, and the opt-out path for - # grouped boxplots (`lighten = FALSE`), which keeps the historical look. - if (type == "ribbon" || (type == "boxplot" && !is.null(by) && !light_ok)) { + # Semi-transparent fill for ribbon-type plots (the default look for ribbons). + # Other area types only become transparent if the user explicitly asks (e.g. + # `fill = 0.3`), which is handled via the `alpha` layering above. + if (type == "ribbon") { if (!is.null(bg)) { bg = adjustcolor(bg, ribbon.alpha) } else if (!is.null(col)) { diff --git a/inst/tinytest/_tinysnapshot/boxplot_groups_lighten_false.svg b/inst/tinytest/_tinysnapshot/boxplot_groups_lighten_false.svg index 044b88a6..c763ada3 100644 --- a/inst/tinytest/_tinysnapshot/boxplot_groups_lighten_false.svg +++ b/inst/tinytest/_tinysnapshot/boxplot_groups_lighten_false.svg @@ -26,8 +26,8 @@ - - + + supp OJ VC @@ -73,35 +73,35 @@ - + - + - + - + - + @@ -109,7 +109,7 @@ - +