From 58f6acce2eb3c5893a223c39e0bec7b7cf9e41c9 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sat, 19 Sep 2026 20:01:09 -0700 Subject: [PATCH 01/12] las arg --- R/sanitize_axes.R | 13 +++++++++++-- R/tinyplot.R | 33 +++++++++++++++++++++++++++++++++ man/tinyplot.Rd | 10 ++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/R/sanitize_axes.R b/R/sanitize_axes.R index 88c2855e..668e1a95 100644 --- a/R/sanitize_axes.R +++ b/R/sanitize_axes.R @@ -1,6 +1,7 @@ sanitize_axes = function(settings) { env2env(settings, environment(), - c("axes", "xaxt", "yaxt", "frame.plot", "xaxr", "yaxr", "xpad", "ypad")) + c("axes", "xaxt", "yaxt", "frame.plot", "xaxr", "yaxr", "xpad", "ypad", + "las")) ## handle defaults of axes, xaxt, yaxt, frame.plot ## - convert axes to character if necessary ## - set defaults of xaxt/yaxt (if these are NULL) based on axes @@ -35,6 +36,14 @@ sanitize_axes = function(settings) { if (!is.null(xaxr) && (!is.finite(xaxr) || xaxr %% 360 == 0)) xaxr = NULL if (!is.null(yaxr) && (!is.finite(yaxr) || yaxr %% 360 == 0)) yaxr = NULL + ## tick label orientation. Unlike x/yaxr, not resolved against get_tpar(): + ## that reads the global .tpar, which lacks the active theme's own las (queued + ## in a hook), so folding it in would overwrite a theme's las with 0. + assert_numeric(las, len = 1, lower = 0, upper = 3, null.ok = TRUE, name = "las") + if (!is.null(las) && las %% 1 != 0) { + stop("`las` must be a whole number in 0:3", call. = FALSE) + } + ## axis padding: an explicit x/ypad wins over the theme's tpar setting. This ## has to resolve here, before flip_datapoints() swaps the pair -- resolving ## it later would leave a tpar default attached to the axis rather than to @@ -55,6 +64,6 @@ sanitize_axes = function(settings) { environment(), settings, c("axes", "xaxt", "yaxt", "frame.plot", "xaxr", "yaxr", "xpad", "ypad", - "xpad_user", "ypad_user") + "xpad_user", "ypad_user", "las") ) } diff --git a/R/tinyplot.R b/R/tinyplot.R index 24921f51..54846b81 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -303,6 +303,14 @@ #' documentation and examples. Note that this is a post-processing step that #' affects the _appearance_ of the tick labels only; use in conjunction with #' `x/yaxb` if you would like to adjust the position of the tick marks too. +#' @param las numeric in `0:3` giving the orientation of the axis tick labels, +#' following the base \code{\link[graphics]{par}} convention: `0` (parallel +#' to the axis, the default), `1` (always horizontal), `2` (perpendicular to +#' the axis), or `3` (always vertical). `NULL` (the default) defers to the +#' active theme, then to `par("las")`. Passing it here overrides both, but +#' only for this plot; unlike `tpar(las=)` or `tinytheme(las=)` it does not +#' persist. For rotations other than the four right angles, see `xaxr`/`yaxr` +#' below. #' @param xaxr,yaxr numeric giving the rotation of the x- or y-axis tick labels, #' in degrees counter-clockwise; `NULL` (the default) leaves them unrotated. #' Setting one overrides `las` for that axis alone, leaving the other axis @@ -815,6 +823,7 @@ tinyplot.default = function( yaxl = NULL, xaxr = NULL, yaxr = NULL, + las = NULL, log = "", flip = FALSE, frame.plot = NULL, @@ -1009,6 +1018,7 @@ tinyplot.default = function( yaxr = yaxr, yaxs = yaxs, ypad = ypad, + las = las, frame.plot = frame.plot, xlim = xlim, ylim = ylim, @@ -1235,6 +1245,27 @@ tinyplot.default = function( env2env(settings, environment()) + # + ## per-call tick label orientation ----- + # + # A theme sets its own las at plot.new(), so ours is appended after its hook + # (last appended runs last). The direct par() covers add = TRUE, which fires + # no plot.new(). Undone on exit, so the override stays per-call. (#353) + # + if (!is.null(las)) { + .las_old = par("las") + .las_hook = function() par(las = las) + setHook("before.plot.new", .las_hook, action = "append") + on.exit({ + .hks = getHook("before.plot.new") + .keep = !vapply(.hks, identical, logical(1), .las_hook) + setHook("before.plot.new", .hks[.keep], action = "replace") + # par() would *open* a device if none is left (e.g. after `file=`) + if (!is.null(dev.list())) par(las = .las_old) + }, add = TRUE) + par(las = las) + } + # ## dynmar: compute margins up front ----- # @@ -1267,6 +1298,8 @@ tinyplot.default = function( .bp = environment(.h)[["base_par"]] if (is.list(.bp)) .tpars = modifyList(.tpars, .bp) } + # runs before plot.new(), so the las hook hasn't fired and par() is stale + if (!is.null(las)) .tpars[["las"]] = las if (!is.null(.tpars[["mar"]])) .theme_mar = .tpars[["mar"]] # Tick-label cex is per-side (cex.xaxs/cex.yaxs), each falling back to the diff --git a/man/tinyplot.Rd b/man/tinyplot.Rd index be5fb20e..6df1b783 100644 --- a/man/tinyplot.Rd +++ b/man/tinyplot.Rd @@ -46,6 +46,7 @@ tinyplot(x, ...) yaxl = NULL, xaxr = NULL, yaxr = NULL, + las = NULL, log = "", flip = FALSE, frame.plot = NULL, @@ -480,6 +481,15 @@ default theme a long rotated label will be clipped unless you widen \code{mar} yourself. Defaults to the value of \code{tpar("xaxr")} / \code{tpar("yaxr")}, whose documentation covers this and the label-spacing caveat in more detail.} +\item{las}{numeric in \code{0:3} giving the orientation of the axis tick labels, +following the base \code{\link[graphics]{par}} convention: \code{0} (parallel +to the axis, the default), \code{1} (always horizontal), \code{2} (perpendicular to +the axis), or \code{3} (always vertical). \code{NULL} (the default) defers to the +active theme, then to \code{par("las")}. Passing it here overrides both, but +only for this plot; unlike \code{tpar(las=)} or \code{tinytheme(las=)} it does not +persist. For rotations other than the four right angles, see \code{xaxr}/\code{yaxr} +below.} + \item{log}{a character string which contains \code{"x"} if the x axis is to be logarithmic, \code{"y"} if the y axis is to be logarithmic and \code{"xy"} or \code{"yx"} if both axes are to be logarithmic.} From a1412add16b3a18b49401a5371424f7e6b02ba4c Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sat, 19 Sep 2026 20:07:31 -0700 Subject: [PATCH 02/12] tests --- inst/tinytest/_tinysnapshot/axis_las1.svg | 73 ++++++++++++++++ .../_tinysnapshot/axis_las2_override.svg | 83 +++++++++++++++++++ inst/tinytest/test-tinyAxis.R | 16 ++++ 3 files changed, 172 insertions(+) create mode 100644 inst/tinytest/_tinysnapshot/axis_las1.svg create mode 100644 inst/tinytest/_tinysnapshot/axis_las2_override.svg diff --git a/inst/tinytest/_tinysnapshot/axis_las1.svg b/inst/tinytest/_tinysnapshot/axis_las1.svg new file mode 100644 index 00000000..a4822a07 --- /dev/null +++ b/inst/tinytest/_tinysnapshot/axis_las1.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + +Index +1:10 + + + + + + +2 +4 +6 +8 +10 + + + + + + +2 +4 +6 +8 +10 + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/_tinysnapshot/axis_las2_override.svg b/inst/tinytest/_tinysnapshot/axis_las2_override.svg new file mode 100644 index 00000000..f9e9aa6b --- /dev/null +++ b/inst/tinytest/_tinysnapshot/axis_las2_override.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + +species +bill_len + + + + +Adelie +Chinstrap +Gentoo + + + + + + + +35 +40 +45 +50 +55 +60 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inst/tinytest/test-tinyAxis.R b/inst/tinytest/test-tinyAxis.R index 5d708b82..e428fad9 100644 --- a/inst/tinytest/test-tinyAxis.R +++ b/inst/tinytest/test-tinyAxis.R @@ -46,3 +46,19 @@ f = function() { plt(disp ~ mpg, data = mtcars, log = "y") } expect_silent(f()) + + +# +## las as a per-call argument (#353) +# + +# `las` was accepted by tpar()/tinytheme() but ignored as an argument. +f = function() tinyplot(1:10, las = 1) +expect_snapshot_plot(f, label = "axis_las1") + +# An explicit `las` must beat a theme's default, but dynmar still has to reserve +# room for the rotated x labels. +f = function() { + tinyplot(bill_len ~ species, data = penguins, las = 2, theme = "dynamic") +} +expect_snapshot_plot(f, label = "axis_las2_override") From 4a492ac2b8dfadfa745ceffe601ba923300259da Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sat, 19 Sep 2026 20:14:30 -0700 Subject: [PATCH 03/12] news --- NEWS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/NEWS.md b/NEWS.md index 17be8be0..848520a2 100644 --- a/NEWS.md +++ b/NEWS.md @@ -163,6 +163,12 @@ related to plot layering. See "Bug fixes" below. theme, since the plot margins are resized to fit the rotated labels. Also settable globally via `tpar(xaxr = , yaxr = )` and thus as part of a `tinytheme` too. (#717 @grantmcdermott) + - `las` does the same thing, but limited to the four right angles of base R's + `par(las=)` convention, having previously been settable only via `tpar()` or + a theme. Note that this top-level argument applies to a single plot only and + takes precedence over the active theme, so it can also be used to opt _out_ + of a theme's `las`, e.g. `tinyplot(..., theme = "clean", las = 0)`. + (#353 @grantmcdermott) - (Experimental) `record` enables recording plots as replayable objects, closing another long-standing feature request (#121). Specifically, setting `record = TRUE` returns a `"recordedtinyplot"` object (see From e42f6c494e22b198c654953ccf79459f85ef0415 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sat, 19 Sep 2026 20:33:30 -0700 Subject: [PATCH 04/12] copilot comments --- R/sanitize_axes.R | 8 +++++--- R/tinyplot.R | 6 +++--- man/tinyplot.Rd | 6 +++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/R/sanitize_axes.R b/R/sanitize_axes.R index 668e1a95..435975e8 100644 --- a/R/sanitize_axes.R +++ b/R/sanitize_axes.R @@ -39,9 +39,11 @@ sanitize_axes = function(settings) { ## tick label orientation. Unlike x/yaxr, not resolved against get_tpar(): ## that reads the global .tpar, which lacks the active theme's own las (queued ## in a hook), so folding it in would overwrite a theme's las with 0. - assert_numeric(las, len = 1, lower = 0, upper = 3, null.ok = TRUE, name = "las") - if (!is.null(las) && las %% 1 != 0) { - stop("`las` must be a whole number in 0:3", call. = FALSE) + ## one membership test, so NA/NaN/Inf and fractions all land on the same + ## message rather than tripping over a comparison that returns NA + assert_numeric(las, len = 1, null.ok = TRUE, name = "las") + if (!is.null(las) && !(las %in% 0:3)) { + stop("`las` must be one of 0, 1, 2, or 3", call. = FALSE) } ## axis padding: an explicit x/ypad wins over the theme's tpar setting. This diff --git a/R/tinyplot.R b/R/tinyplot.R index 54846b81..c500c711 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -305,9 +305,9 @@ #' `x/yaxb` if you would like to adjust the position of the tick marks too. #' @param las numeric in `0:3` giving the orientation of the axis tick labels, #' following the base \code{\link[graphics]{par}} convention: `0` (parallel -#' to the axis, the default), `1` (always horizontal), `2` (perpendicular to -#' the axis), or `3` (always vertical). `NULL` (the default) defers to the -#' active theme, then to `par("las")`. Passing it here overrides both, but +#' to the axis), `1` (always horizontal), `2` (perpendicular to the axis), or +#' `3` (always vertical). `NULL` (the default) defers to the active theme, +#' then to `par("las")`. Passing it here overrides both, but #' only for this plot; unlike `tpar(las=)` or `tinytheme(las=)` it does not #' persist. For rotations other than the four right angles, see `xaxr`/`yaxr` #' below. diff --git a/man/tinyplot.Rd b/man/tinyplot.Rd index 6df1b783..55f0a96a 100644 --- a/man/tinyplot.Rd +++ b/man/tinyplot.Rd @@ -483,9 +483,9 @@ documentation covers this and the label-spacing caveat in more detail.} \item{las}{numeric in \code{0:3} giving the orientation of the axis tick labels, following the base \code{\link[graphics]{par}} convention: \code{0} (parallel -to the axis, the default), \code{1} (always horizontal), \code{2} (perpendicular to -the axis), or \code{3} (always vertical). \code{NULL} (the default) defers to the -active theme, then to \code{par("las")}. Passing it here overrides both, but +to the axis), \code{1} (always horizontal), \code{2} (perpendicular to the axis), or +\code{3} (always vertical). \code{NULL} (the default) defers to the active theme, +then to \code{par("las")}. Passing it here overrides both, but only for this plot; unlike \code{tpar(las=)} or \code{tinytheme(las=)} it does not persist. For rotations other than the four right angles, see \code{xaxr}/\code{yaxr} below.} From 7c5a613eaf7c8c3f736c3d0d599dc8045d6fee00 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 20 Sep 2026 14:57:57 -0700 Subject: [PATCH 05/12] fix(theme): Restore plot region on theme rollback - An ephemeral theme's margins live only in the `before.plot.new` hook, so rolling it back left `par(plt)` describing a different region than was drawn into. - Base R recomputes the device clip rect from `par()` whenever `xpd` changes, silently truncating anything added afterwards. --- R/tinyplot.R | 5 ++++- R/utils.R | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/R/tinyplot.R b/R/tinyplot.R index c500c711..4fcbf156 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -938,7 +938,10 @@ tinyplot.default = function( } else { dtheme = theme_default otheme = opar[names(dtheme)] - on.exit(do.call(tinytheme, otheme), add = TRUE) + on.exit({ + do.call(tinytheme, otheme) + restore_plot_region(opar[["mar"]]) # See #629 + }, add = TRUE) } } diff --git a/R/utils.R b/R/utils.R index 983502db..d962d550 100644 --- a/R/utils.R +++ b/R/utils.R @@ -229,6 +229,63 @@ restore_margin_inner = function(ooma, topmar_epsilon = 0.1) { } +#' Restore the original plot region +#' +#' @description A theme applies its margins through the `before.plot.new` hook, +#' so they live in the current graphics state but never reach `par()`. +#' Rolling an ephemeral theme back therefore leaves `par(plt)` describing a +#' different region than the one we just drew into. Base R recomputes the +#' device clipping rectangle from `par()`, but only when `xpd` changes, so +#' anything added afterwards gets silently truncated to the wrong rectangle +#' as soon as some intervening call touches `xpd` (e.g. `box()`, `mtext()`, +#' or `type_text(xpd = NA)`). See #629. +#' +#' Fix: put the original (drawn-into) region back, then hand `mar_before` +#' back at the next `plot.new()` so the theme's margins don't leak into the +#' following plot. +#' +#' @param mar_before Pre-theme inner margins (from par("mar")) +#' +#' @returns NULL (called for side effect of resetting par("plt")) +#' +#' @keywords internal +restore_plot_region = function(mar_before) { + if (is.null(dev.list())) return(invisible(NULL)) + # read .saved_par_after directly: get_saved_par()'s match.arg() costs more + # than everything else here put together + dplt = .tinyplot_env[[".saved_par_after"]][["plt"]] + if (is.null(dplt) || all(par("plt") == dplt)) return(invisible(NULL)) + par(plt = dplt) + # Arm the reset for the next plot.new(). `dplt` doubles as the sentinel: mar + # and plt are derived from each other, so plt still matching on the way out + # means nothing else has claimed the margins since. + .tinyplot_env[[".mar_pending"]] = list(mar = mar_before, plt = dplt) + hks = getHook("before.plot.new") + if (!any(vapply(hks, function(h) isTRUE(attr(h, "tinyplot_mar")), logical(1)))) { + # first in line, so a theme's own margin hook still has the last word + setHook("before.plot.new", mar_reset_hook, action = "prepend") + } + invisible(NULL) +} + + +# Installed once by restore_plot_region() and then left registered, so that +# re-arming is a bare assignment. Every tinyplot_add() layer re-enters +# restore_plot_region() -- the theme rollback resets `plt` just beforehand -- +# and setHook()/getHook() churn on each of them measurably outweighs leaving +# an inert closure in place. A no-op unless some plot has armed it. +mar_reset_hook = structure( + function() { + pending = .tinyplot_env[[".mar_pending"]] + if (is.null(pending)) return(invisible(NULL)) + .tinyplot_env[[".mar_pending"]] = NULL + if (all(par("plt") == pending[["plt"]])) par(mar = pending[["mar"]]) + invisible(NULL) + }, + tinyplot_mar = TRUE +) + + # 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 From 340119cf782cfc737c5446d83d811a5beb9e1a84 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 20 Sep 2026 16:52:00 -0700 Subject: [PATCH 06/12] get_tinytheme --- R/tinyplot.R | 2 +- R/tinyplot.data.frame.R | 2 +- R/tinytheme.R | 45 ++++++++++++++++++++++++++++++++++++- R/tpar.R | 1 + man/restore_plot_region.Rd | 29 ++++++++++++++++++++++++ man/tinytheme.Rd | 2 ++ man/tinytheme_get.Rd | 46 ++++++++++++++++++++++++++++++++++++++ man/tinytheme_register.Rd | 2 +- man/tpar.Rd | 1 + 9 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 man/restore_plot_region.Rd create mode 100644 man/tinytheme_get.Rd diff --git a/R/tinyplot.R b/R/tinyplot.R index 4fcbf156..e627a72c 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -1286,7 +1286,7 @@ tinyplot.default = function( # Read the theme's intended mar. Also build a tpars list from the theme # 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") + .tinytheme = tinytheme_get() .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") diff --git a/R/tinyplot.data.frame.R b/R/tinyplot.data.frame.R index 1a26d0ad..963771bd 100644 --- a/R/tinyplot.data.frame.R +++ b/R/tinyplot.data.frame.R @@ -170,7 +170,7 @@ tinypairs = function (x, by = NULL, labs = FALSE, frames = FALSE, ...) { ## language object because `cl` is a matched call, so `cl[["theme"]]` is ## unevaluated (e.g. the call `list("dark")`, not a list). cex_fct_adj = ifelse(n > 2, 0.66, 0.83) # use same scaling as with faceted plots. - active_theme = get_tpar("tinytheme", default = "default") + active_theme = tinytheme_get() theme_arg = cl[["theme"]] if (is.null(theme_arg)) { theme_ij = bquote(list(.(active_theme), cex = .(cex_fct_adj))) diff --git a/R/tinytheme.R b/R/tinytheme.R index 5e8a3a41..efdc8791 100644 --- a/R/tinytheme.R +++ b/R/tinytheme.R @@ -117,6 +117,8 @@ #' @return The function returns nothing. It is called for its side effects. #' #' @seealso [`tpar`] which does the heavy lifting under the hood; +#' [tinytheme_get()] for retrieving the name of the active theme; +#' [tinytheme_list()] for listing the names of all available themes; #' [tinytheme_register()] for registering custom named themes. #' #' @examples @@ -297,6 +299,47 @@ tinytheme = function( } +#' @title Get the currently active theme +#' +#' @description Returns the name of the [`tinytheme`] that is currently in +#' effect. Handy for saving a theme and restoring it later, or for querying +#' the active theme programmatically. +#' +#' @details A thin convenience wrapper around the `"tinytheme"` entry of +#' [`tpar`]. The difference is that it always returns a plain character +#' string, including in a session where no theme has been set yet (where +#' `tpar("tinytheme")` returns `NULL` rather than `"default"`). Use [`tpar`] +#' if you want the full set of underlying theme settings, rather than just +#' the name. +#' +#' Note that a theme passed to the `tinyplot(..., theme =)` argument is +#' ephemeral: it is reset on exit, so it is only visible to +#' `tinytheme_get()` from inside that same call (e.g. via `draw`). +#' +#' @returns A character string naming the active theme. +#' +#' @seealso [tinytheme], [tinytheme_register], [tpar] +#' +#' @examples +#' # no theme set yet +#' tinytheme_get() +#' +#' # save the current theme, switch, then restore it afterwards +#' otheme = tinytheme_get() +#' tinytheme("classic") +#' tinytheme_get() +#' +#' tinyplot(mpg ~ wt, data = mtcars) +#' +#' tinytheme(otheme) # back to where we started +#' tinytheme_get() +#' +#' @export +tinytheme_get = function() { + get_tpar("tinytheme", default = "default") +} + + # ## Themes (these are read and set at initial load time) @@ -771,7 +814,7 @@ get_theme_def = function(name) { #' `tinytheme_list()` returns a named list with character vectors `builtin` #' and `registered`. `tinytheme_unregister()` returns `NULL` (invisibly). #' -#' @seealso [tinytheme()] +#' @seealso [tinytheme()], [tinytheme_get()] #' #' @examples #' # Register a custom theme based on "float" but with a grid diff --git a/R/tpar.R b/R/tpar.R index b6efa10a..745e128d 100644 --- a/R/tpar.R +++ b/R/tpar.R @@ -100,6 +100,7 @@ #' stages of a `tinyplot` call (and used for internal accounting purposes). #' [`tinytheme`] allows users to easily set a group of graphics parameters #' in a single function call, according to a variety of predefined themes. +#' [`tinytheme_get`] returns the name of the currently active theme. #' #' @examples #' # Return a list of existing base and tinyplot graphic params diff --git a/man/restore_plot_region.Rd b/man/restore_plot_region.Rd new file mode 100644 index 00000000..8cb6e166 --- /dev/null +++ b/man/restore_plot_region.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{restore_plot_region} +\alias{restore_plot_region} +\title{Restore the original plot region} +\usage{ +restore_plot_region(mar_before) +} +\arguments{ +\item{mar_before}{Pre-theme inner margins (from par("mar"))} +} +\value{ +NULL (called for side effect of resetting par("plt")) +} +\description{ +A theme applies its margins through the \code{before.plot.new} hook, +so they live in the current graphics state but never reach \code{par()}. +Rolling an ephemeral theme back therefore leaves \code{par(plt)} describing a +different region than the one we just drew into. Base R recomputes the +device clipping rectangle from \code{par()}, but only when \code{xpd} changes, so +anything added afterwards gets silently truncated to the wrong rectangle +as soon as some intervening call touches \code{xpd} (e.g. \code{box()}, \code{mtext()}, +or \code{type_text(xpd = NA)}). See #629. + +Fix: put the original (drawn-into) region back, then hand \code{mar_before} +back at the next \code{plot.new()} so the theme's margins don't leak into the +following plot. +} +\keyword{internal} diff --git a/man/tinytheme.Rd b/man/tinytheme.Rd index cc5261db..d268e2ac 100644 --- a/man/tinytheme.Rd +++ b/man/tinytheme.Rd @@ -216,5 +216,7 @@ tinytheme() } \seealso{ \code{\link{tpar}} which does the heavy lifting under the hood; +\code{\link[=tinytheme_get]{tinytheme_get()}} for retrieving the name of the active theme; +\code{\link[=tinytheme_list]{tinytheme_list()}} for listing the names of all available themes; \code{\link[=tinytheme_register]{tinytheme_register()}} for registering custom named themes. } diff --git a/man/tinytheme_get.Rd b/man/tinytheme_get.Rd new file mode 100644 index 00000000..caacd994 --- /dev/null +++ b/man/tinytheme_get.Rd @@ -0,0 +1,46 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tinytheme.R +\name{tinytheme_get} +\alias{tinytheme_get} +\title{Get the currently active theme} +\usage{ +tinytheme_get() +} +\value{ +A character string naming the active theme. +} +\description{ +Returns the name of the \code{\link{tinytheme}} that is currently in +effect. Handy for saving a theme and restoring it later, or for querying +the active theme programmatically. +} +\details{ +A thin convenience wrapper around the \code{"tinytheme"} entry of +\code{\link{tpar}}. The difference is that it always returns a plain character +string, including in a session where no theme has been set yet (where +\code{tpar("tinytheme")} returns \code{NULL} rather than \code{"default"}). Use \code{\link{tpar}} +if you want the full set of underlying theme settings, rather than just +the name. + +Note that a theme passed to the \code{tinyplot(..., theme =)} argument is +ephemeral: it is reset on exit, so it is only visible to +\code{tinytheme_get()} from inside that same call (e.g. via \code{draw}). +} +\examples{ +# no theme set yet +tinytheme_get() + +# save the current theme, switch, then restore it afterwards +otheme = tinytheme_get() +tinytheme("classic") +tinytheme_get() + +tinyplot(mpg ~ wt, data = mtcars) + +tinytheme(otheme) # back to where we started +tinytheme_get() + +} +\seealso{ +\link{tinytheme}, \link{tinytheme_register}, \link{tpar} +} diff --git a/man/tinytheme_register.Rd b/man/tinytheme_register.Rd index b89b44a3..bd1b2e2a 100644 --- a/man/tinytheme_register.Rd +++ b/man/tinytheme_register.Rd @@ -56,5 +56,5 @@ tinytheme_unregister("float2") } \seealso{ -\code{\link[=tinytheme]{tinytheme()}} +\code{\link[=tinytheme]{tinytheme()}}, \code{\link[=tinytheme_get]{tinytheme_get()}} } diff --git a/man/tpar.Rd b/man/tpar.Rd index 1bfb5de4..d33155e2 100644 --- a/man/tpar.Rd +++ b/man/tpar.Rd @@ -145,4 +145,5 @@ is a convenience function for retrieving graphical parameters at different stages of a \code{tinyplot} call (and used for internal accounting purposes). \code{\link{tinytheme}} allows users to easily set a group of graphics parameters in a single function call, according to a variety of predefined themes. +\code{\link{tinytheme_get}} returns the name of the currently active theme. } From b2e8dedd4a33971afcd66ae94a460a863629a22f Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 20 Sep 2026 16:52:32 -0700 Subject: [PATCH 07/12] website --- altdoc/quarto_website.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/altdoc/quarto_website.yml b/altdoc/quarto_website.yml index 24e36155..2cfe173f 100644 --- a/altdoc/quarto_website.yml +++ b/altdoc/quarto_website.yml @@ -74,6 +74,10 @@ website: contents: - text: tinytheme file: man/tinytheme.qmd + - text: tinytheme_get + file: man/tinytheme_get.qmd + - text: tinytheme_list + file: man/tinytheme_register.qmd - text: tinytheme_register file: man/tinytheme_register.qmd - section: "Types" From a1647c89fe1f12753165bab1502c1620dea0b99c Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 20 Sep 2026 16:53:02 -0700 Subject: [PATCH 08/12] namespace --- NAMESPACE | 1 + 1 file changed, 1 insertion(+) diff --git a/NAMESPACE b/NAMESPACE index 5e7f1164..f492017a 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -17,6 +17,7 @@ export(tinypairs) export(tinyplot) export(tinyplot_add) export(tinytheme) +export(tinytheme_get) export(tinytheme_list) export(tinytheme_register) export(tinytheme_unregister) From 681b2a861062e7585038f400d6517de77c847556 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 20 Sep 2026 16:58:19 -0700 Subject: [PATCH 09/12] tests --- inst/tinytest/test-tinytheme.R | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/inst/tinytest/test-tinytheme.R b/inst/tinytest/test-tinytheme.R index 26d87bb7..c615ed40 100644 --- a/inst/tinytest/test-tinytheme.R +++ b/inst/tinytest/test-tinytheme.R @@ -249,3 +249,32 @@ f = function() { ) } expect_silent(f()) + + +## tinytheme_get() (#629) + +# a bare reset reports the default theme +tinytheme() +expect_equal(tinytheme_get(), "default") + +tinytheme("classic") +expect_equal(tinytheme_get(), "classic") + +# the save/restore idiom the accessor exists for +otheme = tinytheme_get() +tinytheme("bw") +expect_equal(tinytheme_get(), "bw") +tinytheme(otheme) +expect_equal(tinytheme_get(), "classic") + +# extra tpar overrides don't change the reported theme name +tinytheme("ipsum", las = 2) +expect_equal(tinytheme_get(), "ipsum") + +# registered themes report their own name +tinytheme_register("float3", theme = "float", grid = TRUE) +tinytheme("float3") +expect_equal(tinytheme_get(), "float3") +tinytheme_unregister("float3") + +tinytheme() From fa02f30837ee687214d303e40b0c7c3a68ef62a9 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 20 Sep 2026 17:00:12 -0700 Subject: [PATCH 10/12] news --- NEWS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS.md b/NEWS.md index 848520a2..328aa798 100644 --- a/NEWS.md +++ b/NEWS.md @@ -216,6 +216,8 @@ related to plot layering. See "Bug fixes" below. so that tiles meet the panel edge, and also rotates the tick labels against their respective axes. Colour fills default to the "tealgrn" sequential palette. (#677 @grantmcdermott) + - New `tinytheme_get()` function returns the name of the currently active + theme. (#629 @grantmcdermott) - Custom plot types have more control over the surrounding plot machinery, via a new `type_hints` mechanism. A type can declare properties about itself---that it draws its own axes, needs a secondary right-hand axis, uses proportional From 8f91173a85e1ed262ed822d99656813cbadc7155 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 20 Sep 2026 17:15:22 -0700 Subject: [PATCH 11/12] news --- NEWS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/NEWS.md b/NEWS.md index 328aa798..f185f1d2 100644 --- a/NEWS.md +++ b/NEWS.md @@ -235,6 +235,12 @@ related to plot layering. See "Bug fixes" below. ### Bug fixes +- Annotations and layers added after a plot that used an ephemeral `theme` + argument are no longer clipped to the wrong region. Only triggered once an + intervening annotation changed `xpd` (e.g. `box()`, `mtext()`, or + `type_text(xpd = NA)`), since that is what makes base R recompute the + clipping rectangle. Thanks to @bastistician for the report. + (#629 @grantmcdermott) - `type_text()` no longer converts a categorical axis to a numeric one. (#730 @grantmcdermott) - `type_hline()`, `type_vline()`, and `type_abline()` now respect From d4872c36e4b523014519999df3d11c482dd1dc2a Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 20 Sep 2026 17:31:59 -0700 Subject: [PATCH 12/12] test --- .../tinytheme_ephemeral_clip_xpd.svg | 70 +++++++++++++++++++ inst/tinytest/test-tinytheme.R | 9 +++ 2 files changed, 79 insertions(+) create mode 100644 inst/tinytest/_tinysnapshot/tinytheme_ephemeral_clip_xpd.svg diff --git a/inst/tinytest/_tinysnapshot/tinytheme_ephemeral_clip_xpd.svg b/inst/tinytest/_tinysnapshot/tinytheme_ephemeral_clip_xpd.svg new file mode 100644 index 00000000..bcc6eacc --- /dev/null +++ b/inst/tinytest/_tinysnapshot/tinytheme_ephemeral_clip_xpd.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + +0 +0 + + + + + + +-1.0 +-0.5 +0.0 +0.5 +1.0 + + + + + + +-1.0 +-0.5 +0.0 +0.5 +1.0 + + + + + + + + + + + +foo + + + + + + diff --git a/inst/tinytest/test-tinytheme.R b/inst/tinytest/test-tinytheme.R index c615ed40..14043e9f 100644 --- a/inst/tinytest/test-tinytheme.R +++ b/inst/tinytest/test-tinytheme.R @@ -215,6 +215,15 @@ f = function() { } expect_snapshot_plot(f, label = "ephemeral_default_theme_add") +# Layers added after an ephemeral theme should not be clipped to the wrong +# region once an intervening annotation has toggled `xpd` (#629) +f = function() { + plt(0, 0, theme = "classic") + plt_add(par("usr")[1], y = 0.3, type = type_text(labels = "foo", xpd = NA, pos = 2)) + plt_add(type = type_hline(0.3)) +} +expect_snapshot_plot(f, label = "tinytheme_ephemeral_clip_xpd") + # User mar override respected under dynmar (#587) f = function() { tinytheme("dynamic", mar = c(5, 5, 5, 5))