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
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ Steps:
4. Run `make document` to update NAMESPACE
5. Add tests in `inst/tinytest/test-type_<name>.R`
6. Add snapshot SVGs by running tests on Linux (devcontainer)
7. Add the new page to the website navigation in `altdoc/quarto_website.yml`

**Important:** Step 7 applies to any new exported function or page, not just plot types. Whenever you add or rename an exported function that gets its own `.Rd` page, add it to `altdoc/quarto_website.yml` under the appropriate section.

### Modifying Legend Behaviour
Type-specific legend customizations should go in the type's `data` function by modifying `settings$legend_args`:
Expand Down
3 changes: 3 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export(tinylabel)
export(tinyplot)
export(tinyplot_add)
export(tinytheme)
export(tinytheme_list)
export(tinytheme_register)
export(tinytheme_unregister)
export(tpar)
export(type_abline)
export(type_area)
Expand Down
6 changes: 6 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ New theme features:
- `"nber"` (NBER working paper style)
- `"socviz"` (based on Kieran Healy's [book](https://socviz.co/))
- `"web"` (web publication, e.g. FiveThirtyEight)
- New `tinytheme_register()` function for registering custom user themes.
Registered themes inherit from any built-in (or previously registered) theme,
apply user-specified overrides, and can then be used by name with
`tinytheme(<theme>)` or `tinyplot(..., theme = <theme>)`. Companion functions
`tinytheme_list()` and `tinytheme_unregister()` further support this
functionality. (#608 @grantmcdermott)

Theme fixes:

Expand Down
3 changes: 2 additions & 1 deletion R/environment.R
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@ set_environment_variable(
.saved_par_after = NULL,
.saved_par_first = NULL,
.last_call = NULL,
.tpar_hooks = NULL
.tpar_hooks = NULL,
.registered_themes = NULL
)
5 changes: 2 additions & 3 deletions R/tinyplot.R
Original file line number Diff line number Diff line change
Expand Up @@ -1028,9 +1028,8 @@ tinyplot.default = function(
# definition so dynmar_side uses theme mgp/tcl/las (which aren't in
# par() yet since the before.plot.new hook hasn't fired).
.tinytheme = get_tpar("tinytheme", default = "default")
.theme_def = if (!is.null(.tinytheme) && .tinytheme != "default") {
get(paste0("theme_", .tinytheme), envir = asNamespace("tinyplot"))
} else NULL
.theme_def = get_theme_def(.tinytheme)
if (identical(.theme_def, theme_default)) .theme_def = NULL
.theme_mar = if (!is.null(.theme_def[["mar"]])) .theme_def[["mar"]] else par("mar")
.tpars = if (!is.null(.theme_def)) modifyList(.theme_def, tpar()) else tpar()
# Merge pending before.plot.new hook values into .tpars so user
Expand Down
163 changes: 149 additions & 14 deletions R/tinytheme.R
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
#' @param ... Named arguments to override specific theme settings. These
#' arguments are passed to `tpar()` and take precedence over the predefined
#' settings in the selected theme.
#' @param register Optional character string. If provided, the theme (with any
#' `...` overrides) is registered under this name via [`tinytheme_register()`]
#' and simultaneously activated. This is a shortcut for calling
#' [`tinytheme_register()`] and `tinytheme()` separately.
#'
#' @details
#' Sets a list of graphical parameters using `tpar()`
Expand Down Expand Up @@ -111,7 +115,8 @@
#'
#' @return The function returns nothing. It is called for its side effects.
#'
#' @seealso [`tpar`] which does the heavy lifting under the hood.
#' @seealso [`tpar`] which does the heavy lifting under the hood;
#' [tinytheme_register()] for registering custom named themes.
#'
#' @examples
#' # Reusable plot function
Expand Down Expand Up @@ -192,25 +197,19 @@ tinytheme = function(
"ridge", "ridge2",
"tufte", "float", "void"
),
...
...,
register = NULL
) {

theme = match.arg(theme)
if (length(theme) > 1) theme = theme[1]

registered = names(get_environment_variable(".registered_themes"))
assert_choice(theme, c(builtin_themes, registered))

# in notebooks, we don't want to close the device because no image.
# init_tpar() tries to be smart, but may fail.
init_tpar(rm_hook = TRUE)

assert_choice(
theme,
c(
"default",
sort(c("basic", "broadsheet", "bw", "classic", "clean", "clean2", "dark",
"dynamic", "float", "ipsum", "ipsum2", "linedraw", "minimal",
"nber", "ridge", "ridge2", "socviz", "tufte", "void", "web"))
)
)

settings = switch(theme,
"default" = theme_default,
"basic" = theme_basic,
Expand All @@ -233,6 +232,7 @@ tinytheme = function(
"float" = theme_float,
"void" = theme_void,
"web" = theme_web,
get_environment_variable(".registered_themes")[[theme]]
)

dots = list(...)
Expand Down Expand Up @@ -262,10 +262,15 @@ tinytheme = function(
settings[["mgp"]] = c(.mgp1, .mgp2, 0)
}

if (!is.null(register)) {
tinytheme_register(register, theme = theme, ...)
settings[["tinytheme"]] = register
}

if (length(settings) > 0) {
if (theme == "default") {
# for default theme, we want to revert the original pars and turn off the
# before.new.plot hook (otherwise manual par(x = y) changes won't work)
# before.new.plot hook (otherwise manual par(x = y) changes won't work)
tpar(settings, hook = FALSE)
old_hooks = get_environment_variable(".tpar_hooks")
remove_hooks(old_hooks)
Expand All @@ -282,6 +287,15 @@ tinytheme = function(
#
## Themes (these are read and set at initial load time)

builtin_themes = c(
"default", "basic", "dynamic",
"clean", "clean2", "bw", "linedraw", "classic",
"minimal", "ipsum", "ipsum2", "dark",
"socviz", "broadsheet", "nber", "web",
"ridge", "ridge2",
"tufte", "float", "void"
)

# theme_default = list()

theme_default = list(
Expand Down Expand Up @@ -645,3 +659,124 @@ theme_void = modifyList(theme_dynamic, list(
xaxt = "none",
yaxt = "none"
))


#
## Theme registry helpers
#

# Internal: unified theme lookup (registered first, then built-in)
get_theme_def = function(name) {
if (is.null(name) || name == "default") return(theme_default)
registry = get_environment_variable(".registered_themes")
if (!is.null(registry[[name]])) return(registry[[name]])
obj_name = paste0("theme_", name)
if (exists(obj_name, envir = asNamespace("tinyplot"), inherits = FALSE)) {
return(get(obj_name, envir = asNamespace("tinyplot")))
}
NULL
}


#' Register, List, and Unregister Custom Themes
#'
#' @md
#' @description
#' `tinytheme_register()` registers a custom theme so it can be used by name
#' with `tinytheme()` or `tinyplot(..., theme = )`. Custom themes inherit from
#' a base theme and apply user-specified overrides. Registered themes are
#' session-scoped: they persist across plots but not across R sessions. To make
#' a custom theme permanently available, register it in your `.Rprofile`.
#'
#' `tinytheme_list()` returns the names of all available themes (built-in and
#' registered).
#'
#' `tinytheme_unregister()` removes a previously registered theme from the
#' registry. Does not reset an active theme.
#'
#' @param name Character string. The name for your custom theme. Cannot clash
#' with or overwrite a built-in theme name (`"default"`, `"clean"`, etc.)
#' @param theme Character string or list. The base theme to inherit from. If a
#' string, it must reference a built-in or previously-registered theme. If a
#' list, it is used directly as the base definition. Default is `"default"`.
#' @param ... Named arguments to override specific theme settings. These are
#' the same parameters accepted by `tpar()`.
#'
#' @return `tinytheme_register()` returns the theme definition list (invisibly).
#' `tinytheme_list()` returns a named list with character vectors `builtin`
#' and `registered`. `tinytheme_unregister()` returns `NULL` (invisibly).
#'
#' @seealso [tinytheme()]
#'
#' @examples
#' # Register a custom theme based on "float" but with a grid
#' tinytheme_register("float2", theme = "float", grid = TRUE)
#'
#' # Use it
#' tinyplot(1:5, theme = "float2")
#'
#' # List all themes
#' tinytheme_list()
#'
#' # Unregister
#' tinytheme_unregister("float2")
#'
#' @export
tinytheme_register = function(name, theme = "default", ...) {
if (!is.character(name) || length(name) != 1 || nchar(name) == 0) {
stop("`name` must be a single non-empty character string.", call. = FALSE)
}
builtins = builtin_themes
if (name %in% builtins) {
stop(
sprintf("'%s' is a built-in theme and cannot be overridden.", name),
call. = FALSE
)
}

if (is.character(theme) && length(theme) == 1) {
base_theme = get_theme_def(theme)
if (is.null(base_theme)) {
stop(sprintf("Base theme '%s' not found.", theme), call. = FALSE)
}
} else if (is.list(theme)) {
base_theme = theme
} else {
stop("`theme` must be a theme name (string) or a list.", call. = FALSE)
}

dots = list(...)
new_theme = if (length(dots) > 0) modifyList(base_theme, dots) else base_theme
new_theme[["tinytheme"]] = name

registry = get_environment_variable(".registered_themes") %||% list()
registry[[name]] = new_theme
set_environment_variable(.registered_themes = registry)
invisible(new_theme)
}


#' @rdname tinytheme_register
#' @export
tinytheme_list = function() {
builtins = builtin_themes
registered = names(get_environment_variable(".registered_themes"))
list(builtin = builtins, registered = registered)
}


#' @rdname tinytheme_register
#' @export
tinytheme_unregister = function(name) {
registry = get_environment_variable(".registered_themes")
if (!name %in% names(registry)) {
warning(
sprintf("Theme '%s' is not registered. Nothing to remove.", name),
call. = FALSE
)
return(invisible(NULL))
}
registry[[name]] = NULL
set_environment_variable(.registered_themes = registry)
invisible(NULL)
}
2 changes: 1 addition & 1 deletion altdoc/pkgdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ altdoc: 0.7.2
pandoc: 3.9.0.2
pkgdown: 2.1.3
pkgdown_sha: ~
last_built: 2026-06-02T22:46:25+0000
last_built: 2026-06-04T16:46:48+0000
urls:
reference: https://grantmcdermott.com/tinyplot/man
article: https://grantmcdermott.com/tinyplot/vignettes
4 changes: 4 additions & 0 deletions altdoc/quarto_website.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ website:
file: man/tinyplot_add.qmd
- text: tinytheme
file: man/tinytheme.qmd
- text: tinytheme_register
file: man/tinytheme_register.qmd
- section: "Plot types"
contents:
- section: Shapes
Expand Down Expand Up @@ -96,6 +98,8 @@ website:
file: man/type_barplot.qmd
- text: type_boxplot
file: man/type_boxplot.qmd
- text: type_chull
file: man/type_chull.qmd
- text: type_density
file: man/type_density.qmd
- text: type_histogram
Expand Down
79 changes: 79 additions & 0 deletions inst/tinytest/_tinysnapshot/tinytheme_register_float2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading