From feb69390e7fd18d6f39feac7e10bfb59d3a97fd5 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 7 Jun 2026 16:04:36 -0700 Subject: [PATCH 01/16] Single-group colors via `col.default` --- R/by_aesthetics.R | 13 +++++++++++++ R/tinytheme.R | 21 ++++++++++++++++++--- R/tpar.R | 14 ++++++++++++++ R/type_barplot.R | 5 +++-- R/type_boxplot.R | 5 +++-- R/type_histogram.R | 1 - R/type_ridge.R | 43 ++++++++++++++++++++++++++++++++++++++----- R/type_violin.R | 5 +++-- man/tpar.Rd | 1 + man/type_ridge.Rd | 7 ++++++- 10 files changed, 99 insertions(+), 16 deletions(-) diff --git a/R/by_aesthetics.R b/R/by_aesthetics.R index b53b80350..bf7a158f6 100755 --- a/R/by_aesthetics.R +++ b/R/by_aesthetics.R @@ -296,6 +296,12 @@ by_col = function(col, palette, alpha, by_ordered, by_continuous, ngrps, adjustc if (is_by_keyword(col)) col = NULL + # 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 && !ordered && !gradient) { + col = get_tpar("col.default", default = NULL) + } + cols = resolve_manual_colors(col, ngrps, gradient, ordered, alpha, adjustcolor) if (!is.null(cols)) { return(cols) @@ -352,6 +358,13 @@ by_bg = function(bg, fill, col, palette, alpha, by_ordered, by_continuous, ngrps } else if (!is.null(col)) { bg = adjustcolor(col, ribbon.alpha) } + } else if (ngrps == 1L && is.null(bg) && !is.null(col) && type %in% c("boxplot", "violin", "barplot")) { + # Single-group fill tracks the resolved border colour, so that themed + # box/violin/bar plots match their own multi-group counterparts. Boxplots + # use a translucent fill (ribbon.alpha); violins and bars use a solid fill. + # (The data functions leave `bg = NULL` here only when a theme palette is + # active; the no-theme default keeps a neutral grey fill.) + bg = if (type == "boxplot") adjustcolor(col, ribbon.alpha) else col } bg diff --git a/R/tinytheme.R b/R/tinytheme.R index 982394033..1bcaa7873 100644 --- a/R/tinytheme.R +++ b/R/tinytheme.R @@ -298,6 +298,14 @@ builtin_themes = c( # theme_default = list() +# Okabe-Ito qualitative palette with the leading black dropped. Themes that use +# this for multi-group plots pair it with `col.default = "black"` so single-group +# displays still use black (see #598). +okabe_ito_noblack = c( + "#E69F00", "#56B4E9", "#009E73", "#F0E442", + "#0072B2", "#D55E00", "#CC79A7", "#999999" +) + theme_default = list( tinytheme = "default", adj = par("adj"), # 0.5, @@ -418,11 +426,12 @@ theme_classic = modifyList(theme_dynamic, list( bty = "l", cex.axis = 0.8, cex.cap = 0.8, + col.default = "black", facet.bg = NULL, font.main = 1, gap.axis = 0.1, gap.lab = 0.4, - palette.qualitative = "Okabe-Ito" + palette.qualitative = okabe_ito_noblack )) # derivatives of "clean" @@ -439,6 +448,7 @@ theme_clean2 = modifyList(theme_clean, list( theme_bw = modifyList(theme_clean, list( tinytheme = "bw", cex.axis = 0.8, + col.default = "black", font.main = 1, gap.axis = 0.1, gap.lab = 0.4, @@ -447,7 +457,7 @@ theme_bw = modifyList(theme_clean, list( grid.lwd = 0.5, lwd = 0.5, lwd.axis = 0.5, - palette.qualitative = "Okabe-Ito" + palette.qualitative = okabe_ito_noblack )) theme_linedraw = modifyList(theme_bw, list( @@ -499,6 +509,7 @@ theme_ipsum2 = modifyList(theme_minimal, list( theme_dark = modifyList(theme_minimal, list( tinytheme = "dark", bg = "#1A1A1A", + col.default = "#BBBBBB", fg = "#BBBBBB", # col = "white", col.xaxs = "#BBBBBB", @@ -519,11 +530,13 @@ theme_dark = modifyList(theme_minimal, list( theme_ridge = modifyList(theme_clean, list( tinytheme = "ridge", + col.default = "black", # keep black ridgelines; Zissou is for gradient fills palette.qualitative = "Zissou 1", grid = FALSE )) theme_ridge2 = modifyList(theme_clean2, list( tinytheme = "ridge2", + col.default = "black", # keep black ridgelines; Zissou is for gradient fills palette.qualitative = "Zissou 1", grid = FALSE )) @@ -565,6 +578,7 @@ theme_broadsheet = modifyList(theme_dynamic, list( bty = "n", cex.cap = 0.8, col.cap = "gray40", + col.default = "black", col.sub = "gray40", font.main = 2, gap.axis = 0.1, @@ -573,7 +587,7 @@ theme_broadsheet = modifyList(theme_dynamic, list( grid.col = "gray85", grid.lty = 1, grid.lwd = 0.5, - palette.qualitative = "Okabe-Ito", + palette.qualitative = okabe_ito_noblack, tcl = -0.2, yaxt = "labels" )) @@ -584,6 +598,7 @@ theme_broadsheet = modifyList(theme_dynamic, list( theme_nber = modifyList(theme_broadsheet, list( tinytheme = "nber", bg = "#F2F7FB", + col.default = NULL, # override broadsheet's black: nber palette leads with blue cex.cap = 1, cex.main = 1.4, cex.sub = 1, diff --git a/R/tpar.R b/R/tpar.R index 3b437c1c0..5934b5316 100644 --- a/R/tpar.R +++ b/R/tpar.R @@ -55,6 +55,7 @@ #' * `cairo`: Logical indicating whether \code{\link[grDevices]{cairo_pdf}} should be used when writing plots to PDF. If `FALSE`, then \code{\link[grDevices]{pdf}} will be used instead, with implications for embedding (non-standard) fonts. Only used if `tinyplot(..., file = ".pdf")` is called. Defaults to the value of `capabilities("cairo")`. #' * `cex.cap`: Numeric expansion factor for the plot caption text. Defaults to `1` for the default, basic, and dynamic themes, and `0.8` for clean/classic and their descendants. #' * `col.cap`: Character specifying the colour of the plot caption. Defaults to `"black"`. +#' * `col.default`: Character specifying the default colour for single-group displays (i.e. plots without a `by` grouping). Defaults to `NULL`, in which case the first colour of the active qualitative palette is used (or base `palette()[1]`, typically black, if no theme is set). Themes may set this to override the single-group colour independently of the multi-group palette; for example, a theme can drop the leading colour from `palette.qualitative` for grouped plots while still using it (e.g. black) for single-group plots. #' * `font.cap`: Integer specifying the font face for the plot caption (`1` = plain, `2` = bold, `3` = italic, `4` = bold italic). Defaults to `1`. #' * `line.cap`: Numeric specifying the margin line on which to draw the caption. If `NULL` (default), computed automatically based on the available bottom margin. #' * `dynmar`: Logical indicating whether `tinyplot` should attempt dynamic adjustment of margins to reduce whitespace and/or account for spacing of text elements (e.g., long horizontal y-axis labels). Note that this parameter is tightly coupled to internal `tinythemes()` logic and should _not_ be adjusted manually unless you really know what you are doing or don't mind risking unintended consequences to your plot. @@ -234,6 +235,7 @@ known_tpar = c( "cex.xlab", "cex.ylab", "col.cap", + "col.default", "col.xaxs", "col.yaxs", "cairo", @@ -305,6 +307,14 @@ assert_tpar = function(.tpar) { assert_string(.tpar[["grid.bg"]], null.ok = TRUE, name = "grid.bg") assert_numeric(.tpar[["fmar"]], len = 4, null.ok = TRUE, name = "fmar") + col.default = .tpar[["col.default"]] + if (!is.null(col.default)) { + if (!is.character(col.default)) { + stop("col.default needs to be NULL or a (length-1) character colour", call. = FALSE) + } + assert_true(length(col.default) == 1, name = "length(col.default)==1") + } + facet.col = .tpar[["facet.col"]] if (!is.null(facet.col)) { if (!is.null(facet.col) && !is.numeric(facet.col) && !is.character(facet.col)) { @@ -367,6 +377,10 @@ init_tpar = function(rm_hook = FALSE) { .tpar$grid.lty = if (is.null(getOption("tinyplot_grid.lty"))) "dotted" else getOption("tinyplot_grid.lty") .tpar$grid.lwd = if (is.null(getOption("tinyplot_grid.lwd"))) 1 else as.numeric(getOption("tinyplot_grid.lwd")) + # Default colour for single-group displays (NULL defers to the first + # qualitative palette colour, or base palette()[1] if no theme is active) + .tpar$col.default = if (is.null(getOption("tinyplot_col.default"))) NULL else getOption("tinyplot_col.default") + # Legend justification .tpar$ljust = if (is.null(getOption("tinyplot_ljust"))) "left" else getOption("tinyplot_ljust") diff --git a/R/type_barplot.R b/R/type_barplot.R index af36f2def..c0b8b4e09 100644 --- a/R/type_barplot.R +++ b/R/type_barplot.R @@ -170,8 +170,9 @@ data_barplot = function(width = 5/6, beside = FALSE, center = FALSE, offset = NU ## default color palette ngrps = length(unique(datapoints$by)) if (ngrps == 1L && null_palette) { - if (is.null(col)) col = par("fg") - if (is.null(bg)) bg = "grey" + # With a theme palette active, leave bg = NULL so the fill tracks + # the resolved border colour (see by_bg). Otherwise use neutral grey. + if (is.null(bg) && is.null(get_tpar("palette.qualitative", default = NULL))) bg = "grey" } else { if (is.null(bg)) bg = "by" } diff --git a/R/type_boxplot.R b/R/type_boxplot.R index 6610846cf..51504fe68 100644 --- a/R/type_boxplot.R +++ b/R/type_boxplot.R @@ -113,8 +113,9 @@ data_boxplot = function(boxwex = 0.8) { # Check if user provided palette before substitute) if (length(unique(datapoints[["by"]])) == 1 && null_palette) { - if (is.null(col)) col = par("fg") - if (is.null(bg)) bg = "lightgray" + # With a theme palette active, leave bg = NULL so the fill tracks + # the resolved border colour (see by_bg). Otherwise use neutral grey. + if (is.null(bg) && is.null(get_tpar("palette.qualitative", default = NULL))) bg = "lightgray" } else { if (is.null(bg)) bg = "by" } diff --git a/R/type_histogram.R b/R/type_histogram.R index 5d504d222..f50a449d2 100644 --- a/R/type_histogram.R +++ b/R/type_histogram.R @@ -106,7 +106,6 @@ data_histogram = function(breaks = "Sturges", hbreaks = ifelse(!sapply(.breaks, is.null), .breaks, "Sturges") if (is.null(by) && is.null(palette)) { - if (is.null(col)) col = par("fg") if (is.null(bg)) bg = "lightgray" } else { if (is.null(bg)) bg = ribbon.alpha diff --git a/R/type_ridge.R b/R/type_ridge.R index cd79cc031..67f534af5 100644 --- a/R/type_ridge.R +++ b/R/type_ridge.R @@ -64,7 +64,12 @@ #' of the ridge densities. Note that a singular value is expected; if multiple #' colors are provided then only the first will be used. This argument is mostly #' useful for the aesthetic effect of drawing a common outline color in -#' combination with gradient fills. See Examples. +#' combination with gradient fills. See Examples. If left unset here, the +#' outline color can also be supplied via the top-level `tinyplot(..., col =)` +#' call (the `type_ridge(col =)` argument takes precedence if both are given). +#' When neither is supplied, the outline defaults to the active theme's +#' `col.default` (see [tpar]), falling back to the first color of the theme's +#' qualitative palette, or black if no theme is set. #' @param alpha Numeric in the range `[0,1]` for adjusting the alpha #' transparency of the density fills. In most cases, will default to a value of #' 1, i.e. fully opaque. But for some `by` grouped plots (excepting the special @@ -257,6 +262,12 @@ data_ridge = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, fun = function(settings, ...) { env2env(settings, environment(), c("datapoints", "yaxt", "xaxt", "null_by")) + # `col` may arrive either via the top-level `tinyplot(..., col =)` call + # (stored in settings) or via the `type_ridge(col =)` constructor arg. The + # constructor arg takes precedence; otherwise fall back to the settings + # value so that a user-supplied `col` is respected. (#598) + if (is.null(col)) col = settings[["col"]] + # catch for special cases anyby = !null_by x_by = anyby && identical(datapoints$x, datapoints$by) @@ -396,7 +407,24 @@ data_ridge = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, breaks[length(breaks)] = pmax(breaks[length(breaks)], xlim[2L]) } - if (is.null(col) && (!anyby || x_by)) col = "black" + # Single-group (or x_by) ridges: default the outline colour consistently + # with the other plot types. An explicit `col.default` wins; otherwise fall + # back to the first colour of the active qualitative palette (e.g. blue under + # "clean"), or base palette()[1] (black) when no theme palette is set. (#598) + if (is.null(col) && (!anyby || x_by)) { + col = get_tpar("col.default", default = NULL) + if (is.null(col)) { + pal_q = .tpar[["palette.qualitative"]] + col = if (!is.null(pal_q)) { + resolve_palette_spec( + pal_q, ngrps = 1L, gradient = FALSE, ordered = FALSE, + alpha = 1, adjustcolor = adjustcolor + ) + } else { + palette()[1L] + } + } + } # For ridge themes without groups, a numeric bg (e.g. bg = 0.2) should # produce transparent gray, not a transparent palette colour. (#547) @@ -458,10 +486,15 @@ draw_ridge = function() { if (is.null(ibg)) { pal_q = .tpar[["palette.qualitative"]] # For non-ridge themes with a palette, derive fill from the first palette - # colour. We need palette.colors() here because the palette is still just - # a string name at this point (not yet resolved to colours). (#547) + # colour. The theme palette may be a name (e.g. "Tableau 10") or a vector + # of colours (e.g. the ipsum/socviz themes), so resolve it to an actual + # colour rather than assuming a name. (#547, #598) default_bg = if (!ridge_theme && !is.null(pal_q)) { - seq_palette(palette.colors(1, palette = pal_q), n = 2)[2] + pal_q1 = resolve_palette_spec( + pal_q, ngrps = 1L, gradient = FALSE, ordered = FALSE, + alpha = 1, adjustcolor = adjustcolor + ) + seq_palette(pal_q1, n = 2)[2] } else { "gray" } diff --git a/R/type_violin.R b/R/type_violin.R index 7f7e6795a..1c7847e71 100644 --- a/R/type_violin.R +++ b/R/type_violin.R @@ -126,8 +126,9 @@ data_violin = function(bw = "nrd0", adjust = 1, kernel = "gaussian", n = 512, } if (length(unique(datapoints[["by"]])) == 1 && null_palette) { - if (is.null(col)) col = par("fg") - if (is.null(bg)) bg = "lightgray" + # With a theme palette active, leave bg = NULL so the fill tracks + # the resolved border colour (see by_bg). Otherwise use neutral grey. + if (is.null(bg) && is.null(get_tpar("palette.qualitative", default = NULL))) bg = "lightgray" } else if (is.null(bg)) { bg = "by" } diff --git a/man/tpar.Rd b/man/tpar.Rd index ccfe92a9d..3f52a2303 100644 --- a/man/tpar.Rd +++ b/man/tpar.Rd @@ -66,6 +66,7 @@ you should rather use \code{par()} instead. \item \code{cairo}: Logical indicating whether \code{\link[grDevices]{cairo_pdf}} should be used when writing plots to PDF. If \code{FALSE}, then \code{\link[grDevices]{pdf}} will be used instead, with implications for embedding (non-standard) fonts. Only used if \code{tinyplot(..., file = ".pdf")} is called. Defaults to the value of \code{capabilities("cairo")}. \item \code{cex.cap}: Numeric expansion factor for the plot caption text. Defaults to \code{1} for the default, basic, and dynamic themes, and \code{0.8} for clean/classic and their descendants. \item \code{col.cap}: Character specifying the colour of the plot caption. Defaults to \code{"black"}. +\item \code{col.default}: Character specifying the default colour for single-group displays (i.e. plots without a \code{by} grouping). Defaults to \code{NULL}, in which case the first colour of the active qualitative palette is used (or base \code{palette()[1]}, typically black, if no theme is set). Themes may set this to override the single-group colour independently of the multi-group palette; for example, a theme can drop the leading colour from \code{palette.qualitative} for grouped plots while still using it (e.g. black) for single-group plots. \item \code{font.cap}: Integer specifying the font face for the plot caption (\code{1} = plain, \code{2} = bold, \code{3} = italic, \code{4} = bold italic). Defaults to \code{1}. \item \code{line.cap}: Numeric specifying the margin line on which to draw the caption. If \code{NULL} (default), computed automatically based on the available bottom margin. \item \code{dynmar}: Logical indicating whether \code{tinyplot} should attempt dynamic adjustment of margins to reduce whitespace and/or account for spacing of text elements (e.g., long horizontal y-axis labels). Note that this parameter is tightly coupled to internal \code{tinythemes()} logic and should \emph{not} be adjusted manually unless you really know what you are doing or don't mind risking unintended consequences to your plot. diff --git a/man/type_ridge.Rd b/man/type_ridge.Rd index b8e2ec6ac..4ceb9de48 100644 --- a/man/type_ridge.Rd +++ b/man/type_ridge.Rd @@ -99,7 +99,12 @@ section below.} of the ridge densities. Note that a singular value is expected; if multiple colors are provided then only the first will be used. This argument is mostly useful for the aesthetic effect of drawing a common outline color in -combination with gradient fills. See Examples.} +combination with gradient fills. See Examples. If left unset here, the +outline color can also be supplied via the top-level \code{tinyplot(..., col =)} +call (the \code{type_ridge(col =)} argument takes precedence if both are given). +When neither is supplied, the outline defaults to the active theme's +\code{col.default} (see \link{tpar}), falling back to the first color of the theme's +qualitative palette, or black if no theme is set.} \item{alpha}{Numeric in the range \verb{[0,1]} for adjusting the alpha transparency of the density fills. In most cases, will default to a value of From 6b27debc24270d43914c9f26dfb85da64039ef3e Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 7 Jun 2026 16:18:36 -0700 Subject: [PATCH 02/16] update tests --- ...ridge_gradient_white_facet_theme_ridge.svg | 2172 +++++++++++++++++ .../_tinysnapshot/ridge_theme_palette.svg | 6 +- .../_tinysnapshot/tinytheme_broadsheet.svg | 68 +- inst/tinytest/_tinysnapshot/tinytheme_bw.svg | 68 +- .../_tinysnapshot/tinytheme_classic.svg | 68 +- .../tinytheme_dynamic_boxplot_flip.svg | 90 +- .../tinytheme_dynamic_x_boxplot.svg | 90 +- .../_tinysnapshot/tinytheme_dynamic_yaxl.svg | 118 +- .../_tinysnapshot/tinytheme_ipsum2.svg | 68 +- .../_tinysnapshot/tinytheme_linedraw.svg | 68 +- .../_tinysnapshot/tinytheme_minimal.svg | 68 +- inst/tinytest/test-type_ridge.R | 16 +- 12 files changed, 2536 insertions(+), 364 deletions(-) create mode 100644 inst/tinytest/_tinysnapshot/ridge_gradient_white_facet_theme_ridge.svg diff --git a/inst/tinytest/_tinysnapshot/ridge_gradient_white_facet_theme_ridge.svg b/inst/tinytest/_tinysnapshot/ridge_gradient_white_facet_theme_ridge.svg new file mode 100644 index 000000000..7a3291e3b --- /dev/null +++ b/inst/tinytest/_tinysnapshot/ridge_gradient_white_facet_theme_ridge.svg @@ -0,0 +1,2172 @@ + + + + + + + + + + + + + +mpg +am + + + + + + + + + + + + + + + + + + +5 +10 +15 +20 +25 +30 +35 +40 + +0 + + + + + + + + + + + + + + + + + + + +5 +10 +15 +20 +25 +30 +35 +40 + +1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 +1 + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/ridge_theme_palette.svg b/inst/tinytest/_tinysnapshot/ridge_theme_palette.svg index e029639a1..3d71d7027 100644 --- a/inst/tinytest/_tinysnapshot/ridge_theme_palette.svg +++ b/inst/tinytest/_tinysnapshot/ridge_theme_palette.svg @@ -58,11 +58,11 @@ - + - + - + setosa diff --git a/inst/tinytest/_tinysnapshot/tinytheme_broadsheet.svg b/inst/tinytest/_tinysnapshot/tinytheme_broadsheet.svg index ce08192e2..9d94a938f 100644 --- a/inst/tinytest/_tinysnapshot/tinytheme_broadsheet.svg +++ b/inst/tinytest/_tinysnapshot/tinytheme_broadsheet.svg @@ -26,8 +26,8 @@ - - + + factor(am) 0 1 @@ -74,38 +74,38 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_bw.svg b/inst/tinytest/_tinysnapshot/tinytheme_bw.svg index 6f793ac86..004b8a4ba 100644 --- a/inst/tinytest/_tinysnapshot/tinytheme_bw.svg +++ b/inst/tinytest/_tinysnapshot/tinytheme_bw.svg @@ -26,8 +26,8 @@ - - + + factor(am) 0 1 @@ -100,38 +100,38 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_classic.svg b/inst/tinytest/_tinysnapshot/tinytheme_classic.svg index d3c2a1288..b59687a9e 100644 --- a/inst/tinytest/_tinysnapshot/tinytheme_classic.svg +++ b/inst/tinytest/_tinysnapshot/tinytheme_classic.svg @@ -26,8 +26,8 @@ - - + + factor(am) 0 1 @@ -76,38 +76,38 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_dynamic_boxplot_flip.svg b/inst/tinytest/_tinysnapshot/tinytheme_dynamic_boxplot_flip.svg index 5714b2876..c4083a332 100644 --- a/inst/tinytest/_tinysnapshot/tinytheme_dynamic_boxplot_flip.svg +++ b/inst/tinytest/_tinysnapshot/tinytheme_dynamic_boxplot_flip.svg @@ -79,51 +79,51 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_dynamic_x_boxplot.svg b/inst/tinytest/_tinysnapshot/tinytheme_dynamic_x_boxplot.svg index b02c2e410..dd2f0cea6 100644 --- a/inst/tinytest/_tinysnapshot/tinytheme_dynamic_x_boxplot.svg +++ b/inst/tinytest/_tinysnapshot/tinytheme_dynamic_x_boxplot.svg @@ -79,51 +79,51 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_dynamic_yaxl.svg b/inst/tinytest/_tinysnapshot/tinytheme_dynamic_yaxl.svg index e8ca35f84..1a134ecb3 100644 --- a/inst/tinytest/_tinysnapshot/tinytheme_dynamic_yaxl.svg +++ b/inst/tinytest/_tinysnapshot/tinytheme_dynamic_yaxl.svg @@ -79,65 +79,65 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_ipsum2.svg b/inst/tinytest/_tinysnapshot/tinytheme_ipsum2.svg index 7e5f97c02..860d47b81 100644 --- a/inst/tinytest/_tinysnapshot/tinytheme_ipsum2.svg +++ b/inst/tinytest/_tinysnapshot/tinytheme_ipsum2.svg @@ -26,8 +26,8 @@ - - + + factor(am) 0 1 @@ -86,38 +86,38 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_linedraw.svg b/inst/tinytest/_tinysnapshot/tinytheme_linedraw.svg index ac1547ce8..686675e41 100644 --- a/inst/tinytest/_tinysnapshot/tinytheme_linedraw.svg +++ b/inst/tinytest/_tinysnapshot/tinytheme_linedraw.svg @@ -26,8 +26,8 @@ - - + + factor(am) 0 1 @@ -100,38 +100,38 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_minimal.svg b/inst/tinytest/_tinysnapshot/tinytheme_minimal.svg index ecc39d3d3..24ef0a1d3 100644 --- a/inst/tinytest/_tinysnapshot/tinytheme_minimal.svg +++ b/inst/tinytest/_tinysnapshot/tinytheme_minimal.svg @@ -26,8 +26,8 @@ - - + + factor(am) 0 1 @@ -86,38 +86,38 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/test-type_ridge.R b/inst/tinytest/test-type_ridge.R index eef900fc6..ca8d8c77c 100644 --- a/inst/tinytest/test-type_ridge.R +++ b/inst/tinytest/test-type_ridge.R @@ -118,8 +118,7 @@ expect_snapshot_plot(f, label = "ridge_gradient_probs") f = function() { tinyplot( am ~ mpg, facet = ~vs, data = mtcars, - type = type_ridge(gradient = "agsunset"), - col = "white" + type = type_ridge(gradient = "agsunset") ) } expect_snapshot_plot(f, label = "ridge_gradient_facet") @@ -129,22 +128,23 @@ tinytheme("ridge2") expect_snapshot_plot(f, label = "ridge_gradient_facet_theme_ridge2") tinytheme() +# Dedicated test for a white boundary color between gradient ridges, which is a +# common aesthetic for separating overlapping densities. (#598) f = function() { tinyplot( am ~ mpg, facet = ~vs, data = mtcars, - type = type_ridge(gradient = "agsunset", raster = TRUE, alpha = 0.5), - col = "white" + type = type_ridge(gradient = TRUE), + col = "white", + theme = "ridge" ) } -expect_snapshot_plot(f, label = "ridge_gradient_facet_raster_alpha") - +expect_snapshot_plot(f, label = "ridge_gradient_white_facet_theme_ridge") f = function() { tinyplot( am ~ mpg, facet = ~vs, data = mtcars, - type = type_ridge(gradient = "agsunset", raster = TRUE, alpha = 0.5), - col = "white" + type = type_ridge(gradient = "agsunset", raster = TRUE, alpha = 0.5) ) } expect_snapshot_plot(f, label = "ridge_gradient_facet_raster_alpha") From 37e89b12a68b8fe6a073fb1c353f29da3e5b3d4c Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 7 Jun 2026 16:33:00 -0700 Subject: [PATCH 03/16] use "ggplot" palette for (built-in) ggplot2-inspired themes --- R/tinytheme.R | 14 +++- inst/tinytest/_tinysnapshot/tinytheme_bw.svg | 68 +++++++++---------- .../_tinysnapshot/tinytheme_classic.svg | 68 +++++++++---------- .../_tinysnapshot/tinytheme_linedraw.svg | 68 +++++++++---------- .../_tinysnapshot/tinytheme_minimal.svg | 68 +++++++++---------- 5 files changed, 147 insertions(+), 139 deletions(-) diff --git a/R/tinytheme.R b/R/tinytheme.R index 1bcaa7873..414c32d5f 100644 --- a/R/tinytheme.R +++ b/R/tinytheme.R @@ -306,6 +306,13 @@ okabe_ito_noblack = c( "#0072B2", "#D55E00", "#CC79A7", "#999999" ) +# ggplot2 default qualitative palette with the leading black dropped, paired +# with `col.default = "black"` (see #598). Used by the ggplot2-inspired themes. +ggplot2_noblack = c( + "#F8766D", "#00BA38", "#619CFF", "#00BFC4", + "#F564E3", "#B79F00", "#9E9E9E" +) + theme_default = list( tinytheme = "default", adj = par("adj"), # 0.5, @@ -431,7 +438,7 @@ theme_classic = modifyList(theme_dynamic, list( font.main = 1, gap.axis = 0.1, gap.lab = 0.4, - palette.qualitative = okabe_ito_noblack + palette.qualitative = ggplot2_noblack )) # derivatives of "clean" @@ -457,7 +464,7 @@ theme_bw = modifyList(theme_clean, list( grid.lwd = 0.5, lwd = 0.5, lwd.axis = 0.5, - palette.qualitative = okabe_ito_noblack + palette.qualitative = ggplot2_noblack )) theme_linedraw = modifyList(theme_bw, list( @@ -503,7 +510,8 @@ theme_ipsum2 = modifyList(theme_minimal, list( bty = "n", font.sub = 3, adj.ylab = 1, - adj.xlab = 1 + adj.xlab = 1, + palette.qualitative = okabe_ito_noblack # keep Okabe-Ito, not the ggplot2 palette )) theme_dark = modifyList(theme_minimal, list( diff --git a/inst/tinytest/_tinysnapshot/tinytheme_bw.svg b/inst/tinytest/_tinysnapshot/tinytheme_bw.svg index 004b8a4ba..d061d6401 100644 --- a/inst/tinytest/_tinysnapshot/tinytheme_bw.svg +++ b/inst/tinytest/_tinysnapshot/tinytheme_bw.svg @@ -26,8 +26,8 @@ - - + + factor(am) 0 1 @@ -100,38 +100,38 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_classic.svg b/inst/tinytest/_tinysnapshot/tinytheme_classic.svg index b59687a9e..f813f2323 100644 --- a/inst/tinytest/_tinysnapshot/tinytheme_classic.svg +++ b/inst/tinytest/_tinysnapshot/tinytheme_classic.svg @@ -26,8 +26,8 @@ - - + + factor(am) 0 1 @@ -76,38 +76,38 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_linedraw.svg b/inst/tinytest/_tinysnapshot/tinytheme_linedraw.svg index 686675e41..04112b7fa 100644 --- a/inst/tinytest/_tinysnapshot/tinytheme_linedraw.svg +++ b/inst/tinytest/_tinysnapshot/tinytheme_linedraw.svg @@ -26,8 +26,8 @@ - - + + factor(am) 0 1 @@ -100,38 +100,38 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_minimal.svg b/inst/tinytest/_tinysnapshot/tinytheme_minimal.svg index 24ef0a1d3..f4df4e755 100644 --- a/inst/tinytest/_tinysnapshot/tinytheme_minimal.svg +++ b/inst/tinytest/_tinysnapshot/tinytheme_minimal.svg @@ -26,8 +26,8 @@ - - + + factor(am) 0 1 @@ -86,38 +86,38 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 740036b0f93739cb6bfc140bbca207c319fed4b9 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 7 Jun 2026 16:38:05 -0700 Subject: [PATCH 04/16] updates --- R/tinytheme.R | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/R/tinytheme.R b/R/tinytheme.R index 414c32d5f..c10901628 100644 --- a/R/tinytheme.R +++ b/R/tinytheme.R @@ -517,7 +517,7 @@ theme_ipsum2 = modifyList(theme_minimal, list( theme_dark = modifyList(theme_minimal, list( tinytheme = "dark", bg = "#1A1A1A", - col.default = "#BBBBBB", + col.default = NULL, # no fixed default: single-group uses palette[1] (Set 2) fg = "#BBBBBB", # col = "white", col.xaxs = "#BBBBBB", @@ -664,6 +664,7 @@ theme_tufte = modifyList(theme_dynamic, list( theme_float = modifyList(theme_tufte, list( tinytheme = "float", + col.default = "black", gap.axis = 0, gap.lab = 0.7, lab = c(5, 5, 7), @@ -673,6 +674,7 @@ theme_float = modifyList(theme_tufte, list( theme_void = modifyList(theme_dynamic, list( tinytheme = "void", + col.default = "black", facet.bg = NULL, facet.border = NA, font.main = 1, From bde37a17d0359789e3b0ed2e28186a0960a952ed Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 7 Jun 2026 16:47:32 -0700 Subject: [PATCH 05/16] news --- NEWS.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/NEWS.md b/NEWS.md index 2a3917d76..567b35b56 100644 --- a/NEWS.md +++ b/NEWS.md @@ -80,6 +80,35 @@ New theme features: `tinytheme()` or `tinyplot(..., theme = )`. Companion functions `tinytheme_list()` and `tinytheme_unregister()` further support this functionality. (#608 @grantmcdermott) +- We adopt a new logic for selecting the default colour for single-group + (i.e. plots without a `by` grouping) versus multi-group displays. The upshot + is that, for many themes, single-group displays will simply default to + "black", whereas multi-group displays will drop "black" and only present + colour palettes. This behaviour is operationalized through the new + `col.default` parameter, settable via `tpar()` or within a theme: when unset + (`NULL`) the first colour of the theme's palette is used; otherwise + `col.default` overrides it. Single-group displays are also now resolved + consistently across _all_ plot types, whereas types like `"boxplot"`, + `"barplot"`, `"violin"`, and `"histogram"` previously hard-coded a black + colour regardless of theme. (#598 @grantmcdermott @zeileis) + - As a result, the ggplot2-inspired themes now drop the leading black from + their _grouped_ palettes while continuing to use black for single-group + displays (via `col.default = "black"`). The `"bw"`, `"classic"`, + `"linedraw"`, and `"minimal"` themes now use the ggplot2 default palette + (less its leading black), so grouped plots start at the familiar salmon + hue. The `"ipsum2"` and `"broadsheet"` themes similarly use the Okabe-Ito + palette less its leading black, so grouped plots start at orange. The + `"float"` and `"void"` themes also default to black for single-group + displays. Themes whose palette already leads with a non-black colour (e.g. + `"clean"`, `"dark"`, `"nber"`, `"web"`) now use that colour for single-group + displays too (e.g. a single-group boxplot under `"clean"` gains a blue + border, matching its points and lines). + - Relatedly, when a theme palette is active, the _fill_ of single-group + `"boxplot"`, `"violin"`, and `"barplot"` displays now tracks the resolved + border colour rather than a fixed neutral grey, so that single-group plots + match their own multi-group counterparts. Boxplots use a translucent fill + (`ribbon.alpha`), while violins and bars use a solid fill. The plain + (theme-less) default is unchanged and retains its neutral grey fill. Theme fixes: From 8850767cbaa45e670e25e12ababea7460ec1680b Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 7 Jun 2026 17:36:29 -0700 Subject: [PATCH 06/16] single-group tests --- .../_tinysnapshot/tinytheme_single_basic.svg | 110 +++++++++++++++ .../tinytheme_single_broadsheet.svg | 97 ++++++++++++++ .../_tinysnapshot/tinytheme_single_bw.svg | 123 +++++++++++++++++ .../tinytheme_single_classic.svg | 99 ++++++++++++++ .../_tinysnapshot/tinytheme_single_clean.svg | 110 +++++++++++++++ .../_tinysnapshot/tinytheme_single_clean2.svg | 96 +++++++++++++ .../_tinysnapshot/tinytheme_single_dark.svg | 109 +++++++++++++++ .../tinytheme_single_default.svg | 99 ++++++++++++++ .../tinytheme_single_dynamic.svg | 99 ++++++++++++++ .../_tinysnapshot/tinytheme_single_float.svg | 98 ++++++++++++++ .../_tinysnapshot/tinytheme_single_ipsum.svg | 109 +++++++++++++++ .../_tinysnapshot/tinytheme_single_ipsum2.svg | 109 +++++++++++++++ .../tinytheme_single_linedraw.svg | 123 +++++++++++++++++ .../tinytheme_single_minimal.svg | 109 +++++++++++++++ .../_tinysnapshot/tinytheme_single_nber.svg | 97 ++++++++++++++ .../_tinysnapshot/tinytheme_single_ridge.svg | 99 ++++++++++++++ .../_tinysnapshot/tinytheme_single_ridge2.svg | 85 ++++++++++++ .../_tinysnapshot/tinytheme_single_socviz.svg | 123 +++++++++++++++++ .../_tinysnapshot/tinytheme_single_tufte.svg | 126 ++++++++++++++++++ .../_tinysnapshot/tinytheme_single_void.svg | 74 ++++++++++ .../_tinysnapshot/tinytheme_single_web.svg | 96 +++++++++++++ inst/tinytest/test-tinytheme.R | 13 ++ 22 files changed, 2203 insertions(+) create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_single_basic.svg create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_single_broadsheet.svg create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_single_bw.svg create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_single_classic.svg create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_single_clean.svg create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_single_clean2.svg create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_single_dark.svg create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_single_default.svg create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_single_dynamic.svg create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_single_float.svg create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_single_ipsum.svg create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_single_ipsum2.svg create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_single_linedraw.svg create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_single_minimal.svg create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_single_nber.svg create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_single_ridge.svg create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_single_ridge2.svg create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_single_socviz.svg create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_single_tufte.svg create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_single_void.svg create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_single_web.svg diff --git a/inst/tinytest/_tinysnapshot/tinytheme_single_basic.svg b/inst/tinytest/_tinysnapshot/tinytheme_single_basic.svg new file mode 100644 index 000000000..5772d2bb9 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_single_basic.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + +tinytheme("basic") +Title of the plot +hp +mpg + + + + + + + +50 +100 +150 +200 +250 +300 + + + + + + +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_single_broadsheet.svg b/inst/tinytest/_tinysnapshot/tinytheme_single_broadsheet.svg new file mode 100644 index 000000000..19cea57e8 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_single_broadsheet.svg @@ -0,0 +1,97 @@ + + + + + + + + + + + + + +tinytheme("broadsheet") +Title of the plot +hp +mpg + + + + + + + +50 +100 +150 +200 +250 +300 +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_single_bw.svg b/inst/tinytest/_tinysnapshot/tinytheme_single_bw.svg new file mode 100644 index 000000000..241e3e480 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_single_bw.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + + +tinytheme("bw") +Title of the plot +hp +mpg + + + + + + + +50 +100 +150 +200 +250 +300 + + + + + + +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_single_classic.svg b/inst/tinytest/_tinysnapshot/tinytheme_single_classic.svg new file mode 100644 index 000000000..5996f169b --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_single_classic.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + +tinytheme("classic") +Title of the plot +hp +mpg + + + + + + + +50 +100 +150 +200 +250 +300 + + + + + + +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_single_clean.svg b/inst/tinytest/_tinysnapshot/tinytheme_single_clean.svg new file mode 100644 index 000000000..89748a45e --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_single_clean.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + +tinytheme("clean") +Title of the plot +hp +mpg + + + + + + + +50 +100 +150 +200 +250 +300 + + + + + + +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_single_clean2.svg b/inst/tinytest/_tinysnapshot/tinytheme_single_clean2.svg new file mode 100644 index 000000000..1a5198713 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_single_clean2.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + +tinytheme("clean2") +Title of the plot +hp +mpg +50 +100 +150 +200 +250 +300 +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_single_dark.svg b/inst/tinytest/_tinysnapshot/tinytheme_single_dark.svg new file mode 100644 index 000000000..8a435403d --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_single_dark.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + +tinytheme("dark") +Title of the plot +hp +mpg +50 +100 +150 +200 +250 +300 +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_single_default.svg b/inst/tinytest/_tinysnapshot/tinytheme_single_default.svg new file mode 100644 index 000000000..6ae66bf6c --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_single_default.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + +tinytheme("default") +Title of the plot +hp +mpg + + + + + + + +50 +100 +150 +200 +250 +300 + + + + + + +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_single_dynamic.svg b/inst/tinytest/_tinysnapshot/tinytheme_single_dynamic.svg new file mode 100644 index 000000000..57cb37820 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_single_dynamic.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + +tinytheme("dynamic") +Title of the plot +hp +mpg + + + + + + + +50 +100 +150 +200 +250 +300 + + + + + + +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_single_float.svg b/inst/tinytest/_tinysnapshot/tinytheme_single_float.svg new file mode 100644 index 000000000..20ef6c83f --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_single_float.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + +tinytheme("float") +Title of the plot +hp +mpg + + + + + + + +50 +100 +150 +200 +250 +300 + + + + + + +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_single_ipsum.svg b/inst/tinytest/_tinysnapshot/tinytheme_single_ipsum.svg new file mode 100644 index 000000000..286e4397e --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_single_ipsum.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + +tinytheme("ipsum") +Title of the plot +hp +mpg +50 +100 +150 +200 +250 +300 +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_single_ipsum2.svg b/inst/tinytest/_tinysnapshot/tinytheme_single_ipsum2.svg new file mode 100644 index 000000000..b0019f70c --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_single_ipsum2.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + +tinytheme("ipsum2") +Title of the plot +hp +mpg +50 +100 +150 +200 +250 +300 +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_single_linedraw.svg b/inst/tinytest/_tinysnapshot/tinytheme_single_linedraw.svg new file mode 100644 index 000000000..1aeb1a89e --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_single_linedraw.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + + +tinytheme("linedraw") +Title of the plot +hp +mpg + + + + + + + +50 +100 +150 +200 +250 +300 + + + + + + +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_single_minimal.svg b/inst/tinytest/_tinysnapshot/tinytheme_single_minimal.svg new file mode 100644 index 000000000..32deb0ed9 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_single_minimal.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + +tinytheme("minimal") +Title of the plot +hp +mpg +50 +100 +150 +200 +250 +300 +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_single_nber.svg b/inst/tinytest/_tinysnapshot/tinytheme_single_nber.svg new file mode 100644 index 000000000..f7ab70885 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_single_nber.svg @@ -0,0 +1,97 @@ + + + + + + + + + + + + + +tinytheme("nber") +Title of the plot +hp +mpg + + + + + + + +50 +100 +150 +200 +250 +300 +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_single_ridge.svg b/inst/tinytest/_tinysnapshot/tinytheme_single_ridge.svg new file mode 100644 index 000000000..4509e2aba --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_single_ridge.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + +tinytheme("ridge") +Title of the plot +hp +mpg + + + + + + + +50 +100 +150 +200 +250 +300 + + + + + + +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_single_ridge2.svg b/inst/tinytest/_tinysnapshot/tinytheme_single_ridge2.svg new file mode 100644 index 000000000..860190da2 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_single_ridge2.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + +tinytheme("ridge2") +Title of the plot +hp +mpg +50 +100 +150 +200 +250 +300 +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_single_socviz.svg b/inst/tinytest/_tinysnapshot/tinytheme_single_socviz.svg new file mode 100644 index 000000000..ff3623b7f --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_single_socviz.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + + +tinytheme("socviz") +Title of the plot +hp +mpg + + + + + + + +50 +100 +150 +200 +250 +300 + + + + + + +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_single_tufte.svg b/inst/tinytest/_tinysnapshot/tinytheme_single_tufte.svg new file mode 100644 index 000000000..c979719e8 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_single_tufte.svg @@ -0,0 +1,126 @@ + + + + + + + + + + + + + +tinytheme("tufte") +Title of the plot +hp +mpg + + + + + + + + + + + + + + + + +60 +80 +100 +140 +180 +220 +260 +300 +340 + + + + + + + + + + + + + + +10 +12 +14 +16 +18 +20 +22 +24 +26 +28 +30 +32 +34 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_single_void.svg b/inst/tinytest/_tinysnapshot/tinytheme_single_void.svg new file mode 100644 index 000000000..39d583f5d --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_single_void.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + +tinytheme("void") +Title of the plot +hp +mpg + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_single_web.svg b/inst/tinytest/_tinysnapshot/tinytheme_single_web.svg new file mode 100644 index 000000000..db17b54ec --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_single_web.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + +tinytheme("web") +Title of the plot +hp +mpg +50 +100 +150 +200 +250 +300 +10 +15 +20 +25 +30 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/test-tinytheme.R b/inst/tinytest/test-tinytheme.R index e63f63e61..7c6491e07 100644 --- a/inst/tinytest/test-tinytheme.R +++ b/inst/tinytest/test-tinytheme.R @@ -16,6 +16,19 @@ for (thm in thms) { } rm(thm) +# Single-group (no `by`) displays: safeguard the per-theme default colour +# logic, i.e. col.default and the leading-black palette drop. (#598) +for (thm in thms) { + tinytheme(thm) + f = function() tinyplot( + mpg ~ hp, data = mtcars, + main = "Title of the plot", + sub = paste0('tinytheme("', thm, '")') + ) + expect_snapshot_plot(f, label = paste0("tinytheme_single_", thm)) +} +rm(thm) + # legend placement f = function() { From 90979f1a0ec509e4f02d242338d689cfb3fc609c Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 7 Jun 2026 18:34:44 -0700 Subject: [PATCH 07/16] fix old register test --- .../_tinysnapshot/tinytheme_register_float2.svg | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/inst/tinytest/_tinysnapshot/tinytheme_register_float2.svg b/inst/tinytest/_tinysnapshot/tinytheme_register_float2.svg index 0f7523e66..c7c3d758e 100644 --- a/inst/tinytest/_tinysnapshot/tinytheme_register_float2.svg +++ b/inst/tinytest/_tinysnapshot/tinytheme_register_float2.svg @@ -69,11 +69,11 @@ - - - - - + + + + + From c0aa66cc4042e28aa91ccb50ef69ae250a2e5320 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 7 Jun 2026 18:37:18 -0700 Subject: [PATCH 08/16] pr link in news --- NEWS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 567b35b56..d87e59442 100644 --- a/NEWS.md +++ b/NEWS.md @@ -46,7 +46,7 @@ margin spacing and related plot elements to reduce whitespace and improve the overall plot aesthetic. The `?tinytheme` help file and online [Themes](https://grantmcdermott.com/tinyplot/vignettes/themes.html) vignette cover the new features in detail. But see below for the highlights. -(#549, #591, #595, #606 @grantmcdermott, @vincentarelbundock) +(#549, #591, #595, #606, #614 @grantmcdermott, @vincentarelbundock, @zeileis) New theme features: @@ -90,7 +90,7 @@ New theme features: `col.default` overrides it. Single-group displays are also now resolved consistently across _all_ plot types, whereas types like `"boxplot"`, `"barplot"`, `"violin"`, and `"histogram"` previously hard-coded a black - colour regardless of theme. (#598 @grantmcdermott @zeileis) + colour regardless of theme. (#614 @grantmcdermott @zeileis) - As a result, the ggplot2-inspired themes now drop the leading black from their _grouped_ palettes while continuing to use black for single-group displays (via `col.default = "black"`). The `"bw"`, `"classic"`, From 8924b6965ad0041e01734df6dabfbf62ea7d6d46 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 7 Jun 2026 19:26:03 -0700 Subject: [PATCH 09/16] fix legacy 'bug' for single group displays without theme --- R/type_histogram.R | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/R/type_histogram.R b/R/type_histogram.R index f50a449d2..fa14abc07 100644 --- a/R/type_histogram.R +++ b/R/type_histogram.R @@ -101,15 +101,15 @@ data_histogram = function(breaks = "Sturges", hright = right fun = function(settings, .breaks = hbreaks, .freebreaks = hfree.breaks, .freq = hfreq, .right = hright, .drop.zeros = hdrop.zeros, ...) { - env2env(settings, environment(), c("palette", "bg", "col", "plot", "datapoints", "ymin", "ymax", "xmin", "xmax", "freq", "ylab", "xlab", "facet", "ribbon.alpha")) + env2env(settings, environment(), c("palette", "bg", "col", "plot", "datapoints", "ymin", "ymax", "xmin", "xmax", "freq", "ylab", "xlab", "facet", "ribbon.alpha", "by")) hbreaks = ifelse(!sapply(.breaks, is.null), .breaks, "Sturges") - if (is.null(by) && is.null(palette)) { - if (is.null(bg)) bg = "lightgray" - } else { - if (is.null(bg)) bg = ribbon.alpha - } + # Histogram fills are drawn at `ribbon.alpha` transparency regardless of + # group count or theme. The fill colour itself is resolved downstream + # (see by_bg): a single-group display picks up the active palette's first + # colour, or base palette()[1] (black) when no palette is set. + if (is.null(bg)) bg = ribbon.alpha if (!.freebreaks) xbreaks = hist(datapoints$x, breaks = hbreaks, right = .right, plot = FALSE)$breaks datapoints = split(datapoints, list(datapoints$by, datapoints$facet)) From 9a3a6c6bb3c1b228c58bf35943b72ac5a2fea23e Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 7 Jun 2026 19:37:51 -0700 Subject: [PATCH 10/16] histogram gotcha --- R/by_aesthetics.R | 25 ++++++++++++++++++------- R/type_histogram.R | 13 +++++++------ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/R/by_aesthetics.R b/R/by_aesthetics.R index bf7a158f6..ba18d476e 100755 --- a/R/by_aesthetics.R +++ b/R/by_aesthetics.R @@ -358,13 +358,24 @@ by_bg = function(bg, fill, col, palette, alpha, by_ordered, by_continuous, ngrps } else if (!is.null(col)) { bg = adjustcolor(col, ribbon.alpha) } - } else if (ngrps == 1L && is.null(bg) && !is.null(col) && type %in% c("boxplot", "violin", "barplot")) { - # Single-group fill tracks the resolved border colour, so that themed - # box/violin/bar plots match their own multi-group counterparts. Boxplots - # use a translucent fill (ribbon.alpha); violins and bars use a solid fill. - # (The data functions leave `bg = NULL` here only when a theme palette is - # active; the no-theme default keeps a neutral grey fill.) - bg = if (type == "boxplot") adjustcolor(col, ribbon.alpha) else col + } 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. Boxplots and histograms use a translucent + # fill (ribbon.alpha); violins and bars use a solid fill. (The data functions + # leave `bg = NULL` here only when a theme palette is active; the no-theme + # default keeps a neutral grey fill.) + fill_base = by_col( + col = NULL, palette = palette, alpha = 1, by_ordered = by_ordered, + by_continuous = by_continuous, ngrps = 1L, adjustcolor = adjustcolor + ) + bg = if (type %in% c("boxplot", "histogram")) { + adjustcolor(fill_base, ribbon.alpha) + } else { + fill_base + } } bg diff --git a/R/type_histogram.R b/R/type_histogram.R index fa14abc07..46d4c14d7 100644 --- a/R/type_histogram.R +++ b/R/type_histogram.R @@ -101,15 +101,16 @@ data_histogram = function(breaks = "Sturges", hright = right fun = function(settings, .breaks = hbreaks, .freebreaks = hfree.breaks, .freq = hfreq, .right = hright, .drop.zeros = hdrop.zeros, ...) { - env2env(settings, environment(), c("palette", "bg", "col", "plot", "datapoints", "ymin", "ymax", "xmin", "xmax", "freq", "ylab", "xlab", "facet", "ribbon.alpha", "by")) + env2env(settings, environment(), c("palette", "bg", "col", "plot", "datapoints", "ymin", "ymax", "xmin", "xmax", "freq", "ylab", "xlab", "facet", "ribbon.alpha", "by", "null_by")) hbreaks = ifelse(!sapply(.breaks, is.null), .breaks, "Sturges") - # Histogram fills are drawn at `ribbon.alpha` transparency regardless of - # group count or theme. The fill colour itself is resolved downstream - # (see by_bg): a single-group display picks up the active palette's first - # colour, or base palette()[1] (black) when no palette is set. - if (is.null(bg)) bg = ribbon.alpha + # Histogram fills are drawn at `ribbon.alpha` transparency. For + # single-group displays we leave `bg = NULL` so the fill tracks the + # resolved border colour (see by_bg), which honours `col.default` (e.g. + # black under themes like "classic"). Multi-group displays fill from the + # palette via the `ribbon.alpha` keyword. + if (is.null(bg) && !null_by) bg = ribbon.alpha if (!.freebreaks) xbreaks = hist(datapoints$x, breaks = hbreaks, right = .right, plot = FALSE)$breaks datapoints = split(datapoints, list(datapoints$by, datapoints$facet)) From 27a66c3d9ed487da92fa54e6a16a8df59728cec8 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Mon, 15 Jun 2026 14:49:46 -0700 Subject: [PATCH 11/16] Implement @zeileis's suggestions --- R/by_aesthetics.R | 93 ++++++++++++++++++++++++++++++++++++---------- R/tinytheme.R | 32 +++++----------- R/tpar.R | 6 +-- R/type_barplot.R | 6 ++- R/type_histogram.R | 19 ++++++---- R/type_spineplot.R | 27 +------------- R/utils.R | 35 +++++++++++++++++ man/tpar.Rd | 2 +- 8 files changed, 140 insertions(+), 80 deletions(-) diff --git a/R/by_aesthetics.R b/R/by_aesthetics.R index ba18d476e..b2fcace0b 100755 --- a/R/by_aesthetics.R +++ b/R/by_aesthetics.R @@ -122,6 +122,44 @@ match_palette_name = function(name, candidates) { 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)) { @@ -296,10 +334,25 @@ by_col = function(col, palette, alpha, by_ordered, by_continuous, ngrps, adjustc if (is_by_keyword(col)) col = NULL - # 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 && !ordered && !gradient) { - col = get_tpar("col.default", default = 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) @@ -307,11 +360,6 @@ by_col = function(col, palette, alpha, by_ordered, by_continuous, ngrps, adjustc return(cols) } - pal_theme = if (ordered || gradient) { - get_tpar("palette.sequential", default = NULL) - } else { - get_tpar("palette.qualitative", default = NULL) - } cols = resolve_palette_colors( palette = palette, theme_palette = pal_theme, @@ -336,10 +384,15 @@ by_bg = function(bg, fill, col, palette, alpha, by_ordered, by_continuous, ngrps ordered = if (is.null(by_ordered)) FALSE else by_ordered gradient = if (is.null(by_continuous)) FALSE else by_continuous pal_theme = if (ordered || gradient) { - get_tpar("palette.sequential", default = NULL) + 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"]] + } bg = resolve_palette_colors( palette = palette, theme_palette = pal_theme, @@ -363,19 +416,21 @@ by_bg = function(bg, fill, col, palette, alpha, by_ordered, by_continuous, ngrps # 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. Boxplots and histograms use a translucent - # fill (ribbon.alpha); violins and bars use a solid fill. (The data functions - # leave `bg = NULL` here only when a theme palette is active; the no-theme - # default keeps a neutral grey fill.) + # doesn't bleed into the fill. fill_base = by_col( col = NULL, palette = palette, alpha = 1, by_ordered = by_ordered, by_continuous = by_continuous, ngrps = 1L, adjustcolor = adjustcolor ) - bg = if (type %in% c("boxplot", "histogram")) { - adjustcolor(fill_base, ribbon.alpha) - } else { - fill_base - } + # 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 + # swatches), so it reads cleanly over grid lines unlike alpha blending. For + # an *achromatic* default (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 = drop(to_hcl(fill_base))[2L] < 1 + bg = if (achromatic) "lightgray" else seq_palette(fill_base, n = 3)[3] } bg diff --git a/R/tinytheme.R b/R/tinytheme.R index c10901628..556a82303 100644 --- a/R/tinytheme.R +++ b/R/tinytheme.R @@ -298,21 +298,6 @@ builtin_themes = c( # theme_default = list() -# Okabe-Ito qualitative palette with the leading black dropped. Themes that use -# this for multi-group plots pair it with `col.default = "black"` so single-group -# displays still use black (see #598). -okabe_ito_noblack = c( - "#E69F00", "#56B4E9", "#009E73", "#F0E442", - "#0072B2", "#D55E00", "#CC79A7", "#999999" -) - -# ggplot2 default qualitative palette with the leading black dropped, paired -# with `col.default = "black"` (see #598). Used by the ggplot2-inspired themes. -ggplot2_noblack = c( - "#F8766D", "#00BA38", "#619CFF", "#00BFC4", - "#F564E3", "#B79F00", "#9E9E9E" -) - theme_default = list( tinytheme = "default", adj = par("adj"), # 0.5, @@ -433,12 +418,12 @@ theme_classic = modifyList(theme_dynamic, list( bty = "l", cex.axis = 0.8, cex.cap = 0.8, - col.default = "black", + col.default = -1L, # black single-group; drop it from the grouped palette facet.bg = NULL, font.main = 1, gap.axis = 0.1, gap.lab = 0.4, - palette.qualitative = ggplot2_noblack + palette.qualitative = "ggplot2" )) # derivatives of "clean" @@ -455,7 +440,7 @@ theme_clean2 = modifyList(theme_clean, list( theme_bw = modifyList(theme_clean, list( tinytheme = "bw", cex.axis = 0.8, - col.default = "black", + col.default = -1L, # black single-group; drop it from the grouped palette font.main = 1, gap.axis = 0.1, gap.lab = 0.4, @@ -464,7 +449,7 @@ theme_bw = modifyList(theme_clean, list( grid.lwd = 0.5, lwd = 0.5, lwd.axis = 0.5, - palette.qualitative = ggplot2_noblack + palette.qualitative = "ggplot2" )) theme_linedraw = modifyList(theme_bw, list( @@ -494,6 +479,7 @@ theme_ipsum = modifyList(theme_minimal, list( adj.xlab = 1, adj.ylab = 1, cex.lab = 0.8, + col.default = "black", # bespoke palette doesn't lead with black; pin it font.main = 2, font.sub = 1, font.cap = 3, @@ -511,7 +497,8 @@ theme_ipsum2 = modifyList(theme_minimal, list( font.sub = 3, adj.ylab = 1, adj.xlab = 1, - palette.qualitative = okabe_ito_noblack # keep Okabe-Ito, not the ggplot2 palette + col.default = -1L, # black single-group; drop it from the grouped palette + palette.qualitative = "Okabe-Ito" # keep Okabe-Ito, not the ggplot2 palette )) theme_dark = modifyList(theme_minimal, list( @@ -560,6 +547,7 @@ theme_socviz = modifyList(theme_minimal, list( cex.lab = 1, cex.main = 1.4, cex.sub = 1.05, + col.default = "black", # bespoke palette doesn't lead with black; pin it col.xaxs = "gray10", col.yaxs = "gray10", facet.bg = NULL, @@ -586,7 +574,7 @@ theme_broadsheet = modifyList(theme_dynamic, list( bty = "n", cex.cap = 0.8, col.cap = "gray40", - col.default = "black", + col.default = -1L, # black single-group; drop it from the grouped palette col.sub = "gray40", font.main = 2, gap.axis = 0.1, @@ -595,7 +583,7 @@ theme_broadsheet = modifyList(theme_dynamic, list( grid.col = "gray85", grid.lty = 1, grid.lwd = 0.5, - palette.qualitative = okabe_ito_noblack, + palette.qualitative = "Okabe-Ito", tcl = -0.2, yaxt = "labels" )) diff --git a/R/tpar.R b/R/tpar.R index 5934b5316..23d89de76 100644 --- a/R/tpar.R +++ b/R/tpar.R @@ -55,7 +55,7 @@ #' * `cairo`: Logical indicating whether \code{\link[grDevices]{cairo_pdf}} should be used when writing plots to PDF. If `FALSE`, then \code{\link[grDevices]{pdf}} will be used instead, with implications for embedding (non-standard) fonts. Only used if `tinyplot(..., file = ".pdf")` is called. Defaults to the value of `capabilities("cairo")`. #' * `cex.cap`: Numeric expansion factor for the plot caption text. Defaults to `1` for the default, basic, and dynamic themes, and `0.8` for clean/classic and their descendants. #' * `col.cap`: Character specifying the colour of the plot caption. Defaults to `"black"`. -#' * `col.default`: Character specifying the default colour for single-group displays (i.e. plots without a `by` grouping). Defaults to `NULL`, in which case the first colour of the active qualitative palette is used (or base `palette()[1]`, typically black, if no theme is set). Themes may set this to override the single-group colour independently of the multi-group palette; for example, a theme can drop the leading colour from `palette.qualitative` for grouped plots while still using it (e.g. black) for single-group plots. +#' * `col.default`: Default colour for single-group displays (i.e. plots without a `by` grouping). Can be `NULL`, a length-1 character colour, or a length-1 numeric index into `palette.qualitative`. Defaults to `NULL`, in which case the first colour of the active qualitative palette is used (or base `palette()[1]`, typically black, if no theme is set). A character value sets the single-group colour independently of the multi-group palette. A numeric value `i` selects the `i`th colour of `palette.qualitative` as the single-group default; a *negative* index additionally drops that colour from the palette used for grouped plots. For example, `col.default = -1` paired with `palette.qualitative = "Okabe-Ito"` uses black (the leading palette colour) for single-group plots and an Okabe-Ito-minus-black palette for grouped plots, avoiding the need to maintain a separate "no-black" palette. #' * `font.cap`: Integer specifying the font face for the plot caption (`1` = plain, `2` = bold, `3` = italic, `4` = bold italic). Defaults to `1`. #' * `line.cap`: Numeric specifying the margin line on which to draw the caption. If `NULL` (default), computed automatically based on the available bottom margin. #' * `dynmar`: Logical indicating whether `tinyplot` should attempt dynamic adjustment of margins to reduce whitespace and/or account for spacing of text elements (e.g., long horizontal y-axis labels). Note that this parameter is tightly coupled to internal `tinythemes()` logic and should _not_ be adjusted manually unless you really know what you are doing or don't mind risking unintended consequences to your plot. @@ -309,8 +309,8 @@ assert_tpar = function(.tpar) { col.default = .tpar[["col.default"]] if (!is.null(col.default)) { - if (!is.character(col.default)) { - stop("col.default needs to be NULL or a (length-1) character colour", call. = FALSE) + if (!is.character(col.default) && !is.numeric(col.default)) { + stop("col.default needs to be NULL, a (length-1) character colour, or a (length-1) numeric palette index", call. = FALSE) } assert_true(length(col.default) == 1, name = "length(col.default)==1") } diff --git a/R/type_barplot.R b/R/type_barplot.R index c0b8b4e09..744f7aec2 100644 --- a/R/type_barplot.R +++ b/R/type_barplot.R @@ -171,8 +171,10 @@ data_barplot = function(width = 5/6, beside = FALSE, center = FALSE, offset = NU ngrps = length(unique(datapoints$by)) if (ngrps == 1L && null_palette) { # With a theme palette active, leave bg = NULL so the fill tracks - # the resolved border colour (see by_bg). Otherwise use neutral grey. - if (is.null(bg) && is.null(get_tpar("palette.qualitative", default = NULL))) bg = "grey" + # the resolved border colour (see by_bg). Otherwise use the neutral + # "lightgray" shared by all single-group area fills (matches base R + # hist()/boxplot()). + if (is.null(bg) && is.null(get_tpar("palette.qualitative", default = NULL))) bg = "lightgray" } else { if (is.null(bg)) bg = "by" } diff --git a/R/type_histogram.R b/R/type_histogram.R index 46d4c14d7..4f461b7ea 100644 --- a/R/type_histogram.R +++ b/R/type_histogram.R @@ -101,16 +101,21 @@ data_histogram = function(breaks = "Sturges", hright = right fun = function(settings, .breaks = hbreaks, .freebreaks = hfree.breaks, .freq = hfreq, .right = hright, .drop.zeros = hdrop.zeros, ...) { - env2env(settings, environment(), c("palette", "bg", "col", "plot", "datapoints", "ymin", "ymax", "xmin", "xmax", "freq", "ylab", "xlab", "facet", "ribbon.alpha", "by", "null_by")) + env2env(settings, environment(), c("palette", "bg", "col", "plot", "datapoints", "ymin", "ymax", "xmin", "xmax", "freq", "ylab", "xlab", "facet", "ribbon.alpha", "by", "null_by", "null_palette")) hbreaks = ifelse(!sapply(.breaks, is.null), .breaks, "Sturges") - # Histogram fills are drawn at `ribbon.alpha` transparency. For - # single-group displays we leave `bg = NULL` so the fill tracks the - # resolved border colour (see by_bg), which honours `col.default` (e.g. - # black under themes like "classic"). Multi-group displays fill from the - # palette via the `ribbon.alpha` keyword. - if (is.null(bg) && !null_by) bg = ribbon.alpha + # Multi-group displays fill from the palette at `ribbon.alpha` + # transparency via the `ribbon.alpha` keyword. For single-group displays + # with a theme palette active we leave `bg = NULL` so the fill tracks the + # resolved border colour (see by_bg), which honours `col.default`. With + # no theme palette, single-group uses the neutral "lightgray" shared by + # all single-group area fills (matches base R hist()). + if (is.null(bg) && !null_by) { + bg = ribbon.alpha + } else if (is.null(bg) && null_by && null_palette && is.null(get_tpar("palette.qualitative", default = NULL))) { + bg = "lightgray" + } if (!.freebreaks) xbreaks = hist(datapoints$x, breaks = hbreaks, right = .right, plot = FALSE)$breaks datapoints = split(datapoints, list(datapoints$by, datapoints$facet)) diff --git a/R/type_spineplot.R b/R/type_spineplot.R index 36ea2d782..c89fdc571 100644 --- a/R/type_spineplot.R +++ b/R/type_spineplot.R @@ -297,7 +297,6 @@ data_spineplot = function(off = NULL, breaks = NULL, xlevels = xlevels, ylevels return(fun) } -#' @importFrom grDevices gray.colors draw_spineplot = function(tol.ylab = 0.05, off = NULL, col = NULL, xaxlabels = NULL, yaxlabels = NULL) { fun = function(ixmin, iymin, ixmax, iymax, ilty, ilwd, icol, ibg, flip, @@ -322,7 +321,7 @@ draw_spineplot = function(tol.ylab = 0.05, off = NULL, col = NULL, xaxlabels = N if (is.null(col)) { if (is.null(ibg)) ibg = icol if (isFALSE(y_by)) { - ibg = if (isTRUE(grayscale)) gray.colors(ny) else seq_palette(ibg, ny) + ibg = seq_palette(ibg, ny, grayscale = grayscale) } ibg = rep_len(ibg, ny) } else { @@ -398,27 +397,3 @@ spine_axis = function(side, ..., type = "standard", categorical = TRUE) { do.call("axis", args) } } - -#' @importFrom grDevices col2rgb convertColor hcl -to_hcl = function(x) { - x = t(col2rgb(x, alpha = TRUE)/255) - alpha = x[, 4] - x = x[, 1:3] - x = convertColor(x, from = "sRGB", to = "Luv") - x = cbind(H = atan2(x[, 3L], x[, 2L]) * 180/pi, C = sqrt(x[, 2L]^2 + x[, 3L]^2), L = x[, 1L]) - x[is.na(x[, 1L]), 1L] = 0 - x[x[, 1L] < 0, 1L] = x[x[, 1L] < 0, 1L] + 360 - attr(x, "alpha") = alpha - return(x) -} - -seq_palette = function(x, n, power = 1.5) { - x = drop(to_hcl(x[1L])) - alpha = attr(x, "alpha") - hcl( - h = x[1L], - c = seq.int(from = x[2L]^(1/power), to = 0, length.out = n + 1)[1L:n]^power, - l = 100 - seq.int(from = (100 - x[3L])^(1/power), to = pmin(8, (100 - x[3L])/2)^(1/power), length.out = n)^power, - alpha = alpha - )[1L:n] -} diff --git a/R/utils.R b/R/utils.R index 6afe6b068..bb48c2fc6 100644 --- a/R/utils.R +++ b/R/utils.R @@ -217,3 +217,38 @@ restore_margin_inner = function(ooma, topmar_epsilon = 0.1) { par(omd = c(0, 1, 0, 1)) } } + + +# Convert colour(s) to HCL-like (Luv) coordinates, preserving alpha. Helper for +# seq_palette(). (Originally lived in type_spineplot.R.) +#' @importFrom grDevices col2rgb convertColor hcl +to_hcl = function(x) { + x = t(col2rgb(x, alpha = TRUE)/255) + alpha = x[, 4] + x = x[, 1:3] + x = convertColor(x, from = "sRGB", to = "Luv") + x = cbind(H = atan2(x[, 3L], x[, 2L]) * 180/pi, C = sqrt(x[, 2L]^2 + x[, 3L]^2), L = x[, 1L]) + x[is.na(x[, 1L]), 1L] = 0 + x[x[, 1L] < 0, 1L] = x[x[, 1L] < 0, 1L] + 360 + attr(x, "alpha") = alpha + return(x) +} + +# Build an n-step sequential ramp from colour `x` toward near-white, in HCL +# space (reduces chroma, increases lightness). Used for single-group fills +# (boxplot/violin/barplot/histogram), ridge fills, legend swatches, and +# spineplot shading. `seq_palette(col, 3)[3]` is thus a lighter-but-*opaque* +# tint of `col`. When `grayscale = TRUE`, returns a neutral grey ramp via +# gray.colors() instead (used by spineplot when no colour grouping is active). +#' @importFrom grDevices gray.colors +seq_palette = function(x, n, power = 1.5, grayscale = FALSE) { + if (isTRUE(grayscale)) return(gray.colors(n)) + x = drop(to_hcl(x[1L])) + alpha = attr(x, "alpha") + hcl( + h = x[1L], + c = seq.int(from = x[2L]^(1/power), to = 0, length.out = n + 1)[1L:n]^power, + l = 100 - seq.int(from = (100 - x[3L])^(1/power), to = pmin(8, (100 - x[3L])/2)^(1/power), length.out = n)^power, + alpha = alpha + )[1L:n] +} diff --git a/man/tpar.Rd b/man/tpar.Rd index 3f52a2303..eb2b966ca 100644 --- a/man/tpar.Rd +++ b/man/tpar.Rd @@ -66,7 +66,7 @@ you should rather use \code{par()} instead. \item \code{cairo}: Logical indicating whether \code{\link[grDevices]{cairo_pdf}} should be used when writing plots to PDF. If \code{FALSE}, then \code{\link[grDevices]{pdf}} will be used instead, with implications for embedding (non-standard) fonts. Only used if \code{tinyplot(..., file = ".pdf")} is called. Defaults to the value of \code{capabilities("cairo")}. \item \code{cex.cap}: Numeric expansion factor for the plot caption text. Defaults to \code{1} for the default, basic, and dynamic themes, and \code{0.8} for clean/classic and their descendants. \item \code{col.cap}: Character specifying the colour of the plot caption. Defaults to \code{"black"}. -\item \code{col.default}: Character specifying the default colour for single-group displays (i.e. plots without a \code{by} grouping). Defaults to \code{NULL}, in which case the first colour of the active qualitative palette is used (or base \code{palette()[1]}, typically black, if no theme is set). Themes may set this to override the single-group colour independently of the multi-group palette; for example, a theme can drop the leading colour from \code{palette.qualitative} for grouped plots while still using it (e.g. black) for single-group plots. +\item \code{col.default}: Default colour for single-group displays (i.e. plots without a \code{by} grouping). Can be \code{NULL}, a length-1 character colour, or a length-1 numeric index into \code{palette.qualitative}. Defaults to \code{NULL}, in which case the first colour of the active qualitative palette is used (or base \code{palette()[1]}, typically black, if no theme is set). A character value sets the single-group colour independently of the multi-group palette. A numeric value \code{i} selects the \code{i}th colour of \code{palette.qualitative} as the single-group default; a \emph{negative} index additionally drops that colour from the palette used for grouped plots. For example, \code{col.default = -1} paired with \code{palette.qualitative = "Okabe-Ito"} uses black (the leading palette colour) for single-group plots and an Okabe-Ito-minus-black palette for grouped plots, avoiding the need to maintain a separate "no-black" palette. \item \code{font.cap}: Integer specifying the font face for the plot caption (\code{1} = plain, \code{2} = bold, \code{3} = italic, \code{4} = bold italic). Defaults to \code{1}. \item \code{line.cap}: Numeric specifying the margin line on which to draw the caption. If \code{NULL} (default), computed automatically based on the available bottom margin. \item \code{dynmar}: Logical indicating whether \code{tinyplot} should attempt dynamic adjustment of margins to reduce whitespace and/or account for spacing of text elements (e.g., long horizontal y-axis labels). Note that this parameter is tightly coupled to internal \code{tinythemes()} logic and should \emph{not} be adjusted manually unless you really know what you are doing or don't mind risking unintended consequences to your plot. From 660999707c72c329ceede7d5ec031cc181c03bd0 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Mon, 15 Jun 2026 16:28:19 -0700 Subject: [PATCH 12/16] regenerate snapshots --- .../barplot_formula_univariate.svg | 6 +-- .../_tinysnapshot/barplot_formula_y1.svg | 6 +-- .../_tinysnapshot/barplot_offset_flip.svg | 6 +-- .../_tinysnapshot/barplot_offset_scalar.svg | 20 +++++----- .../barplot_offset_waterfall.svg | 8 ++-- .../tinytest/_tinysnapshot/barplot_simple.svg | 6 +-- .../_tinysnapshot/barplot_text_issue469.svg | 6 +-- .../barplot_xlevels_issue430.svg | 6 +-- .../tinytest/_tinysnapshot/facet_hist_3x2.svg | 26 ++++++------- .../_tinysnapshot/formula_y1_cust_lab.svg | 16 ++++---- .../_tinysnapshot/hist_facet_free.svg | 30 +++++++-------- .../hist_facet_free_breaks_free.svg | 38 +++++++++---------- inst/tinytest/_tinysnapshot/hist_faceted.svg | 28 +++++++------- .../_tinysnapshot/hist_formula_y1.svg | 16 ++++---- inst/tinytest/_tinysnapshot/hist_simple.svg | 18 ++++----- .../_tinysnapshot/hist_simple_white.svg | 18 ++++----- .../issue_545_xaxs_yaxs_restoration.svg | 6 +-- .../tinytheme_dynamic_boxplot_flip.svg | 12 +++--- .../tinytheme_dynamic_x_boxplot.svg | 12 +++--- .../_tinysnapshot/tinytheme_dynamic_yaxl.svg | 16 ++++---- 20 files changed, 150 insertions(+), 150 deletions(-) diff --git a/inst/tinytest/_tinysnapshot/barplot_formula_univariate.svg b/inst/tinytest/_tinysnapshot/barplot_formula_univariate.svg index ecd38adc3..4e096cb56 100644 --- a/inst/tinytest/_tinysnapshot/barplot_formula_univariate.svg +++ b/inst/tinytest/_tinysnapshot/barplot_formula_univariate.svg @@ -51,9 +51,9 @@ - - - + + + diff --git a/inst/tinytest/_tinysnapshot/barplot_formula_y1.svg b/inst/tinytest/_tinysnapshot/barplot_formula_y1.svg index ecd38adc3..4e096cb56 100644 --- a/inst/tinytest/_tinysnapshot/barplot_formula_y1.svg +++ b/inst/tinytest/_tinysnapshot/barplot_formula_y1.svg @@ -51,9 +51,9 @@ - - - + + + diff --git a/inst/tinytest/_tinysnapshot/barplot_offset_flip.svg b/inst/tinytest/_tinysnapshot/barplot_offset_flip.svg index 627db2767..ef4ad340b 100644 --- a/inst/tinytest/_tinysnapshot/barplot_offset_flip.svg +++ b/inst/tinytest/_tinysnapshot/barplot_offset_flip.svg @@ -49,9 +49,9 @@ - - - + + + diff --git a/inst/tinytest/_tinysnapshot/barplot_offset_scalar.svg b/inst/tinytest/_tinysnapshot/barplot_offset_scalar.svg index 17d6d75b3..34174ca75 100644 --- a/inst/tinytest/_tinysnapshot/barplot_offset_scalar.svg +++ b/inst/tinytest/_tinysnapshot/barplot_offset_scalar.svg @@ -60,16 +60,16 @@ - - - - - - - - - - + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/barplot_offset_waterfall.svg b/inst/tinytest/_tinysnapshot/barplot_offset_waterfall.svg index a73e64c95..208bcde10 100644 --- a/inst/tinytest/_tinysnapshot/barplot_offset_waterfall.svg +++ b/inst/tinytest/_tinysnapshot/barplot_offset_waterfall.svg @@ -50,10 +50,10 @@ - - - - + + + + diff --git a/inst/tinytest/_tinysnapshot/barplot_simple.svg b/inst/tinytest/_tinysnapshot/barplot_simple.svg index 224799fd4..d3383b2a9 100644 --- a/inst/tinytest/_tinysnapshot/barplot_simple.svg +++ b/inst/tinytest/_tinysnapshot/barplot_simple.svg @@ -55,9 +55,9 @@ - - - + + + diff --git a/inst/tinytest/_tinysnapshot/barplot_text_issue469.svg b/inst/tinytest/_tinysnapshot/barplot_text_issue469.svg index e8131eea1..fd88909bd 100644 --- a/inst/tinytest/_tinysnapshot/barplot_text_issue469.svg +++ b/inst/tinytest/_tinysnapshot/barplot_text_issue469.svg @@ -55,9 +55,9 @@ - - - + + + 11 diff --git a/inst/tinytest/_tinysnapshot/barplot_xlevels_issue430.svg b/inst/tinytest/_tinysnapshot/barplot_xlevels_issue430.svg index 26076ab12..bd58dafcc 100644 --- a/inst/tinytest/_tinysnapshot/barplot_xlevels_issue430.svg +++ b/inst/tinytest/_tinysnapshot/barplot_xlevels_issue430.svg @@ -55,9 +55,9 @@ - - - + + + diff --git a/inst/tinytest/_tinysnapshot/facet_hist_3x2.svg b/inst/tinytest/_tinysnapshot/facet_hist_3x2.svg index 9c8098d4c..8e474f72f 100644 --- a/inst/tinytest/_tinysnapshot/facet_hist_3x2.svg +++ b/inst/tinytest/_tinysnapshot/facet_hist_3x2.svg @@ -244,29 +244,29 @@ - - + + - - - + + + - + - + - - - - + + + + - - + + diff --git a/inst/tinytest/_tinysnapshot/formula_y1_cust_lab.svg b/inst/tinytest/_tinysnapshot/formula_y1_cust_lab.svg index 27aaa9bb8..b2a09ac42 100644 --- a/inst/tinytest/_tinysnapshot/formula_y1_cust_lab.svg +++ b/inst/tinytest/_tinysnapshot/formula_y1_cust_lab.svg @@ -62,14 +62,14 @@ - - - - - - - - + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/hist_facet_free.svg b/inst/tinytest/_tinysnapshot/hist_facet_free.svg index 75556111e..5f2eb9d75 100644 --- a/inst/tinytest/_tinysnapshot/hist_facet_free.svg +++ b/inst/tinytest/_tinysnapshot/hist_facet_free.svg @@ -150,25 +150,25 @@ - - - + + + - - - - - + + + + + - - - - - - - + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/hist_facet_free_breaks_free.svg b/inst/tinytest/_tinysnapshot/hist_facet_free_breaks_free.svg index 6ca60a1d6..16bdb9639 100644 --- a/inst/tinytest/_tinysnapshot/hist_facet_free_breaks_free.svg +++ b/inst/tinytest/_tinysnapshot/hist_facet_free_breaks_free.svg @@ -152,29 +152,29 @@ - - - - - + + + + + - - - - - - - - + + + + + + + + - - - - - - + + + + + + diff --git a/inst/tinytest/_tinysnapshot/hist_faceted.svg b/inst/tinytest/_tinysnapshot/hist_faceted.svg index 622673976..f16d3afc0 100644 --- a/inst/tinytest/_tinysnapshot/hist_faceted.svg +++ b/inst/tinytest/_tinysnapshot/hist_faceted.svg @@ -138,24 +138,24 @@ - - + + - - - - - - + + + + + + - - - - - - + + + + + + diff --git a/inst/tinytest/_tinysnapshot/hist_formula_y1.svg b/inst/tinytest/_tinysnapshot/hist_formula_y1.svg index 599d843dd..25eb1045d 100644 --- a/inst/tinytest/_tinysnapshot/hist_formula_y1.svg +++ b/inst/tinytest/_tinysnapshot/hist_formula_y1.svg @@ -62,14 +62,14 @@ - - - - - - - - + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/hist_simple.svg b/inst/tinytest/_tinysnapshot/hist_simple.svg index 2b256a7b3..3726ce377 100644 --- a/inst/tinytest/_tinysnapshot/hist_simple.svg +++ b/inst/tinytest/_tinysnapshot/hist_simple.svg @@ -62,15 +62,15 @@ - - - - - - - - - + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/hist_simple_white.svg b/inst/tinytest/_tinysnapshot/hist_simple_white.svg index 46a9dacf0..35b95245d 100644 --- a/inst/tinytest/_tinysnapshot/hist_simple_white.svg +++ b/inst/tinytest/_tinysnapshot/hist_simple_white.svg @@ -62,15 +62,15 @@ - - - - - - - - - + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/issue_545_xaxs_yaxs_restoration.svg b/inst/tinytest/_tinysnapshot/issue_545_xaxs_yaxs_restoration.svg index 18f127161..437f14517 100644 --- a/inst/tinytest/_tinysnapshot/issue_545_xaxs_yaxs_restoration.svg +++ b/inst/tinytest/_tinysnapshot/issue_545_xaxs_yaxs_restoration.svg @@ -63,9 +63,9 @@ - - - + + + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_dynamic_boxplot_flip.svg b/inst/tinytest/_tinysnapshot/tinytheme_dynamic_boxplot_flip.svg index c4083a332..2b02605e7 100644 --- a/inst/tinytest/_tinysnapshot/tinytheme_dynamic_boxplot_flip.svg +++ b/inst/tinytest/_tinysnapshot/tinytheme_dynamic_boxplot_flip.svg @@ -79,42 +79,42 @@ - + - + - + - + - + - + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_dynamic_x_boxplot.svg b/inst/tinytest/_tinysnapshot/tinytheme_dynamic_x_boxplot.svg index dd2f0cea6..9a33ee38d 100644 --- a/inst/tinytest/_tinysnapshot/tinytheme_dynamic_x_boxplot.svg +++ b/inst/tinytest/_tinysnapshot/tinytheme_dynamic_x_boxplot.svg @@ -79,42 +79,42 @@ - + - + - + - + - + - + diff --git a/inst/tinytest/_tinysnapshot/tinytheme_dynamic_yaxl.svg b/inst/tinytest/_tinysnapshot/tinytheme_dynamic_yaxl.svg index 1a134ecb3..f2aab9040 100644 --- a/inst/tinytest/_tinysnapshot/tinytheme_dynamic_yaxl.svg +++ b/inst/tinytest/_tinysnapshot/tinytheme_dynamic_yaxl.svg @@ -79,7 +79,7 @@ - + @@ -87,14 +87,14 @@ - + - + @@ -102,28 +102,28 @@ - + - + - + - + @@ -131,7 +131,7 @@ - + From 798ba3fd5b7ad2a2f27a0bbe1bafb2110d0bf40b Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Mon, 15 Jun 2026 16:55:18 -0700 Subject: [PATCH 13/16] news --- NEWS.md | 52 ++++++++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/NEWS.md b/NEWS.md index d87e59442..fc8f59192 100644 --- a/NEWS.md +++ b/NEWS.md @@ -85,30 +85,34 @@ New theme features: is that, for many themes, single-group displays will simply default to "black", whereas multi-group displays will drop "black" and only present colour palettes. This behaviour is operationalized through the new - `col.default` parameter, settable via `tpar()` or within a theme: when unset - (`NULL`) the first colour of the theme's palette is used; otherwise - `col.default` overrides it. Single-group displays are also now resolved - consistently across _all_ plot types, whereas types like `"boxplot"`, - `"barplot"`, `"violin"`, and `"histogram"` previously hard-coded a black - colour regardless of theme. (#614 @grantmcdermott @zeileis) - - As a result, the ggplot2-inspired themes now drop the leading black from - their _grouped_ palettes while continuing to use black for single-group - displays (via `col.default = "black"`). The `"bw"`, `"classic"`, - `"linedraw"`, and `"minimal"` themes now use the ggplot2 default palette - (less its leading black), so grouped plots start at the familiar salmon - hue. The `"ipsum2"` and `"broadsheet"` themes similarly use the Okabe-Ito - palette less its leading black, so grouped plots start at orange. The - `"float"` and `"void"` themes also default to black for single-group - displays. Themes whose palette already leads with a non-black colour (e.g. - `"clean"`, `"dark"`, `"nber"`, `"web"`) now use that colour for single-group - displays too (e.g. a single-group boxplot under `"clean"` gains a blue - border, matching its points and lines). - - Relatedly, when a theme palette is active, the _fill_ of single-group - `"boxplot"`, `"violin"`, and `"barplot"` displays now tracks the resolved - border colour rather than a fixed neutral grey, so that single-group plots - match their own multi-group counterparts. Boxplots use a translucent fill - (`ribbon.alpha`), while violins and bars use a solid fill. The plain - (theme-less) default is unchanged and retains its neutral grey fill. + `col.default` parameter, settable via `tpar()` or within a theme. + (#614 @grantmcdermott @zeileis) + - Themes whose palette already leads with a non-black colour (e.g. + `"clean(2)"`, `"dark"`, `"nber"`, `"web"`) consistently use that colour for + single-group displays too. + - The ggplot2-inspired themes (`"bw"`, `"classic"`, `"linedraw"`, and + `"minimal"`) continue to use black for single-group display. But they now + drop the leading black from their _grouped_ palettes. (Note that these + themes also use the default `"ggplot2"` palette now, so grouped plots start + at the familiar salmon hue.) + - Similarly, the `"ipsum2"` and `"broadsheet"` themes use the + Okabe-Ito palette less its leading black, so grouped plots start at orange. + The `"float"` and `"void"` themes also default to black for single-group + displays. +- Relatedly, the _fill_ of single-group `"boxplot"`, `"violin"`, `"barplot"`, + and `"histogram"` displays is now unified across these four types, so that a + single-group plot looks the same regardless of which one you reach for. + (#614 @grantmcdermott @zeileis) + - When the resolved default colour is _chromatic_ (e.g. the blue of `"clean"` + or the teal of `"dark"`), the fill is a lighter-but-opaque tint of it; + following the same sequential HCL ramp that `"ridge"` type plots have been + using for a while. + - When the default is _achromatic_ (black, as in the plain default and the + `"bw"`/`"classic"`/`"ipsum"` themes), all four types share a neutral + `"lightgray"` fill, matching base R's `hist()` and `boxplot()` defaults. + 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. Theme fixes: From 987df15169726c86c89029085d55bd5c13e9a3bd Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Mon, 15 Jun 2026 17:07:44 -0700 Subject: [PATCH 14/16] fix old snapshot --- inst/tinytest/_tinysnapshot/text_labeller_percent.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inst/tinytest/_tinysnapshot/text_labeller_percent.svg b/inst/tinytest/_tinysnapshot/text_labeller_percent.svg index c88224287..ed0de01aa 100644 --- a/inst/tinytest/_tinysnapshot/text_labeller_percent.svg +++ b/inst/tinytest/_tinysnapshot/text_labeller_percent.svg @@ -50,8 +50,8 @@ - - + + 50% 80% From 318b8b6d4ad17693bd00a49ed22c222633290b8b Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Mon, 15 Jun 2026 19:11:24 -0700 Subject: [PATCH 15/16] spineplot gotcha --- R/type_spineplot.R | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/R/type_spineplot.R b/R/type_spineplot.R index c89fdc571..d272f8c34 100644 --- a/R/type_spineplot.R +++ b/R/type_spineplot.R @@ -276,6 +276,7 @@ data_spineplot = function(off = NULL, breaks = NULL, xlevels = xlevels, ylevels xaxt = xaxt_orig, yaxt = yaxt_orig, grayscale = grayscale, + null_by = null_by, x_by = x_by, y_by = y_by ) @@ -314,6 +315,7 @@ draw_spineplot = function(tol.ylab = 0.05, off = NULL, col = NULL, xaxlabels = N ny = type_info[["ny"]] x.categorical = type_info[["x.categorical"]] grayscale = type_info[["grayscale"]] + null_by = type_info[["null_by"]] x_by = type_info[["x_by"]] y_by = type_info[["y_by"]] @@ -321,7 +323,16 @@ draw_spineplot = function(tol.ylab = 0.05, off = NULL, col = NULL, xaxlabels = N if (is.null(col)) { if (is.null(ibg)) ibg = icol if (isFALSE(y_by)) { - ibg = seq_palette(ibg, ny, grayscale = grayscale) + # For single-group displays, use a neutral grey ramp (gray.colors) + # whenever the resolved seed colour is achromatic -- not only when no + # palette is set. This keeps black-default themes consistent with each + # other regardless of whether they happen to declare a palette (e.g. + # "ipsum" matches "dynamic"), mirroring the single-group fill logic in + # by_bg(). For grouped 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. + gs = grayscale || (isTRUE(null_by) && drop(to_hcl(ibg))[2L] < 1) + ibg = seq_palette(ibg, ny, grayscale = gs) } ibg = rep_len(ibg, ny) } else { From f61c4d7bc6527ffddba7e1d9168e7f83e820cbb0 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Mon, 15 Jun 2026 19:26:37 -0700 Subject: [PATCH 16/16] consolidate code --- R/by_aesthetics.R | 2 +- R/type_spineplot.R | 23 +++++++++-------------- R/utils.R | 8 ++++++++ 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/R/by_aesthetics.R b/R/by_aesthetics.R index b2fcace0b..2595b63fc 100755 --- a/R/by_aesthetics.R +++ b/R/by_aesthetics.R @@ -429,7 +429,7 @@ by_bg = function(bg, fill, col, palette, alpha, by_ordered, by_continuous, ngrps # 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 = drop(to_hcl(fill_base))[2L] < 1 + achromatic = is_achromatic(fill_base) bg = if (achromatic) "lightgray" else seq_palette(fill_base, n = 3)[3] } diff --git a/R/type_spineplot.R b/R/type_spineplot.R index d272f8c34..bca7aebff 100644 --- a/R/type_spineplot.R +++ b/R/type_spineplot.R @@ -79,7 +79,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) { fun = function(settings, ...) { - env2env(settings, environment(), c("datapoints", "xlim", "ylim", "facet", "facet.args", "by", "xaxb", "yaxb", "null_by", "null_facet", "null_palette", "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")) ## process weights if (!is.null(weights)) { @@ -237,9 +237,6 @@ data_spineplot = function(off = NULL, breaks = NULL, xlevels = xlevels, ylevels # catch for x_by / y/by if (isTRUE(x_by)) datapoints$by = factor(rep(xaxlabels, each = ny)) # each x label extends over ny rows if (isTRUE(y_by)) datapoints$by = factor(rep_len(yaxlabels, nrow(datapoints))) - - ## grayscale flag - grayscale = null_by && null_palette && is.null(.tpar[["palette.qualitative"]]) x = c(datapoints$xmin, datapoints$xmax) y = c(datapoints$ymin, datapoints$ymax) @@ -275,7 +272,6 @@ data_spineplot = function(off = NULL, breaks = NULL, xlevels = xlevels, ylevels axes = axes_orig, xaxt = xaxt_orig, yaxt = yaxt_orig, - grayscale = grayscale, null_by = null_by, x_by = x_by, y_by = y_by @@ -314,7 +310,6 @@ draw_spineplot = function(tol.ylab = 0.05, off = NULL, col = NULL, xaxlabels = N nx = type_info[["nx"]] ny = type_info[["ny"]] x.categorical = type_info[["x.categorical"]] - grayscale = type_info[["grayscale"]] null_by = type_info[["null_by"]] x_by = type_info[["x_by"]] y_by = type_info[["y_by"]] @@ -324,14 +319,14 @@ draw_spineplot = function(tol.ylab = 0.05, off = NULL, col = NULL, xaxlabels = N 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 -- not only when no - # palette is set. This keeps black-default themes consistent with each - # other regardless of whether they happen to declare a palette (e.g. - # "ipsum" matches "dynamic"), mirroring the single-group fill logic in - # by_bg(). For grouped 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. - gs = grayscale || (isTRUE(null_by) && drop(to_hcl(ibg))[2L] < 1) + # whenever the resolved seed colour is achromatic (e.g. the black + # default of the plain default or the "bw"/"ipsum" themes), so these + # are consistent regardless of whether a palette is declared -- the + # same principle as the single-group fill logic in by_bg(). For grouped + # 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. + gs = isTRUE(null_by) && is_achromatic(ibg) ibg = seq_palette(ibg, ny, grayscale = gs) } ibg = rep_len(ibg, ny) diff --git a/R/utils.R b/R/utils.R index bb48c2fc6..43a283d8b 100644 --- a/R/utils.R +++ b/R/utils.R @@ -234,6 +234,14 @@ to_hcl = function(x) { return(x) } +# Is a colour (effectively) neutral grey, i.e. has near-zero chroma? Used to +# decide when a single-group fill should fall back to a plain grey rather than a +# coloured tint (see by_bg() and draw_spineplot()). Black, white, and greys are +# all achromatic; any palette hue is not. +is_achromatic = function(x, tol = 1) { + drop(to_hcl(x))[2L] < tol +} + # Build an n-step sequential ramp from colour `x` toward near-white, in HCL # space (reduces chroma, increases lightness). Used for single-group fills # (boxplot/violin/barplot/histogram), ridge fills, legend swatches, and