Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -80,6 +80,39 @@ New theme features:
`tinytheme(<theme>)` or `tinyplot(..., theme = <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.
(#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:

Expand Down
91 changes: 85 additions & 6 deletions R/by_aesthetics.R
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -296,16 +334,32 @@ by_col = function(col, palette, alpha, by_ordered, by_continuous, ngrps, adjustc

if (is_by_keyword(col)) col = NULL

pal_theme = if (ordered || gradient) {
get_tpar("palette.sequential", default = NULL)
} else {
get_tpar("palette.qualitative", default = NULL)
}

# Resolve a (possibly relative) col.default against the qualitative palette.
# A relative (negative) index also drops the chosen colour from the grouped
# palette, so e.g. `col.default = -1` uses the palette's leading colour for
# single-group displays and the palette-minus-that-colour for grouped ones
# (see resolve_col_default). col.default only applies to qualitative displays.
if (!ordered && !gradient) {
cd = resolve_col_default(get_tpar("col.default", default = NULL), pal_theme)
pal_theme = cd[["palette"]]
# Single-group default colour (theme-settable via tpar). When NULL, defer to
# the usual palette resolution below (first qualitative colour, or palette()[1]).
if (is.null(col) && ngrps == 1L) {
col = cd[["col_default"]]
}
}

cols = resolve_manual_colors(col, ngrps, gradient, ordered, alpha, adjustcolor)
if (!is.null(cols)) {
return(cols)
}

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,
Expand All @@ -330,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,
Expand All @@ -352,6 +411,26 @@ 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) && type %in% c("boxplot", "violin", "barplot", "histogram")) {
# Single-group fill tracks the theme's *default* colour (col.default ->
# palette[1] -> black) so that themed box/violin/bar/histogram plots match
# their own multi-group counterparts. We resolve this default independently
# of `col` so that a user-supplied outline colour (e.g. `col = "white"`)
# doesn't bleed into the fill.
fill_base = by_col(
col = NULL, palette = palette, alpha = 1, by_ordered = by_ordered,
by_continuous = by_continuous, ngrps = 1L, adjustcolor = adjustcolor
)
# For a *chromatic* default the fill is a lighter-but-opaque tint of that
# colour (via seq_palette, the same HCL ramp used for ridge fills and legend
# 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 = is_achromatic(fill_base)
bg = if (achromatic) "lightgray" else seq_palette(fill_base, n = 3)[3]
}

bg
Expand Down
19 changes: 16 additions & 3 deletions R/tinytheme.R
Original file line number Diff line number Diff line change
Expand Up @@ -418,11 +418,12 @@ theme_classic = modifyList(theme_dynamic, list(
bty = "l",
cex.axis = 0.8,
cex.cap = 0.8,
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 = "Okabe-Ito"
palette.qualitative = "ggplot2"
))

# derivatives of "clean"
Expand All @@ -439,6 +440,7 @@ theme_clean2 = modifyList(theme_clean, list(
theme_bw = modifyList(theme_clean, list(
tinytheme = "bw",
cex.axis = 0.8,
col.default = -1L, # black single-group; drop it from the grouped palette
font.main = 1,
gap.axis = 0.1,
gap.lab = 0.4,
Expand All @@ -447,7 +449,7 @@ theme_bw = modifyList(theme_clean, list(
grid.lwd = 0.5,
lwd = 0.5,
lwd.axis = 0.5,
palette.qualitative = "Okabe-Ito"
palette.qualitative = "ggplot2"
))

theme_linedraw = modifyList(theme_bw, list(
Expand Down Expand Up @@ -477,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,
Expand All @@ -493,12 +496,15 @@ theme_ipsum2 = modifyList(theme_minimal, list(
bty = "n",
font.sub = 3,
adj.ylab = 1,
adj.xlab = 1
adj.xlab = 1,
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(
tinytheme = "dark",
bg = "#1A1A1A",
col.default = NULL, # no fixed default: single-group uses palette[1] (Set 2)
fg = "#BBBBBB",
# col = "white",
col.xaxs = "#BBBBBB",
Expand All @@ -519,11 +525,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
))
Expand All @@ -539,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,
Expand All @@ -565,6 +574,7 @@ theme_broadsheet = modifyList(theme_dynamic, list(
bty = "n",
cex.cap = 0.8,
col.cap = "gray40",
col.default = -1L, # black single-group; drop it from the grouped palette
col.sub = "gray40",
font.main = 2,
gap.axis = 0.1,
Expand All @@ -584,6 +594,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,
Expand Down Expand Up @@ -641,6 +652,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),
Expand All @@ -650,6 +662,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,
Expand Down
14 changes: 14 additions & 0 deletions R/tpar.R
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<filename>.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`: 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.
Expand Down Expand Up @@ -234,6 +235,7 @@ known_tpar = c(
"cex.xlab",
"cex.ylab",
"col.cap",
"col.default",
"col.xaxs",
"col.yaxs",
"cairo",
Expand Down Expand Up @@ -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) && !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")
}

facet.col = .tpar[["facet.col"]]
if (!is.null(facet.col)) {
if (!is.null(facet.col) && !is.numeric(facet.col) && !is.character(facet.col)) {
Expand Down Expand Up @@ -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")

Expand Down
7 changes: 5 additions & 2 deletions R/type_barplot.R
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,11 @@ 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 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"
}
Expand Down
5 changes: 3 additions & 2 deletions R/type_boxplot.R
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
17 changes: 11 additions & 6 deletions R/type_histogram.R
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,20 @@ 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", "null_by", "null_palette"))

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
# 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
Expand Down
Loading
Loading