From 52e0e59ab8c14bb1d4c4aa212317f1427a9a928f Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 20 Sep 2026 20:42:50 -0700 Subject: [PATCH 1/6] stop clobber --- R/tinyplot.R | 27 +++++++++++++++++++++++++-- R/tpar.R | 12 ++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/R/tinyplot.R b/R/tinyplot.R index e627a72c..57cc0773 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -922,6 +922,13 @@ tinyplot.default = function( # Ephemeral theme if (!is.null(theme)) { + # Capture the outgoing theme before the ephemeral one replaces it. `opar` + # carries base par values only, so the theme's *name* is not among them. + ptheme = tinytheme_get() + # tinytheme() calls init_tpar(), which wipes .tpar wholesale -- taking any + # user tpar() settings with it. An ephemeral theme should leave no trace, + # so snapshot the settings here and put them back on exit. (#739) + ptpar = as.list(.tpar) if (is.character(theme) && length(theme) == 1) { tinytheme(theme) } else if (is.list(theme)) { @@ -934,12 +941,28 @@ tinyplot.default = function( # clobbered. Only needed for "default" theme which uses hook = FALSE # and thus sets par(mar) immediately. (#557) par(mar = opar$mar) - on.exit(init_tpar(rm_hook = TRUE), add = TRUE) + on.exit({ + init_tpar(rm_hook = TRUE) + # init_tpar() wipes .tpar wholesale, so a persistent theme has to be + # reset explicitly. (#739) + if (!identical(ptheme, "default")) tinytheme(ptheme) + reset_tpar(ptpar) + }, add = TRUE) } else { dtheme = theme_default otheme = opar[names(dtheme)] on.exit({ - do.call(tinytheme, otheme) + if (identical(ptheme, "default")) { + # No persistent theme was active; still restore user's par settings. + do.call(tinytheme, otheme) + } else { + # A persistent theme *was* active, so restore it by name. We must + # not splat `opar` on top: those are the theme's pre-hook par values, + # so passing them back as overrides would clobber the very theme we + # are restoring. (#739) + tinytheme(ptheme) + } + reset_tpar(ptpar) restore_plot_region(opar[["mar"]]) # See #629 }, add = TRUE) } diff --git a/R/tpar.R b/R/tpar.R index 745e128d..0fd564f1 100644 --- a/R/tpar.R +++ b/R/tpar.R @@ -474,3 +474,15 @@ init_tpar = function(rm_hook = FALSE) { ## initialize internal environment for tpar variables .tpar = new.env() init_tpar() + + +# Restore a snapshot of .tpar taken with as.list(). Used to roll back an +# ephemeral theme, whose tinytheme() calls wipe .tpar via init_tpar() and would +# otherwise discard the user's own tpar() settings along with it. (#739) +reset_tpar = function(snapshot) { + nms = names(.tpar) + extra = nms[!nms %in% names(snapshot)] + if (length(extra) > 0) rm(list = extra, envir = .tpar) + list2env(snapshot, envir = .tpar) + invisible(NULL) +} From bea0431e6eeb1e2bb734d0e5b270a4af29f5085d Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 20 Sep 2026 20:44:23 -0700 Subject: [PATCH 2/6] tests --- inst/tinytest/test-tinytheme.R | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/inst/tinytest/test-tinytheme.R b/inst/tinytest/test-tinytheme.R index 14043e9f..7a45204e 100644 --- a/inst/tinytest/test-tinytheme.R +++ b/inst/tinytest/test-tinytheme.R @@ -224,6 +224,24 @@ f = function() { } expect_snapshot_plot(f, label = "tinytheme_ephemeral_clip_xpd") +## an ephemeral theme should leave no trace (#739) + +pdf(NULL) + +# a persistent theme survives an ephemeral one +tinytheme("classic") +plt(1, theme = "dark") +expect_equal(tinytheme_get(), "classic") + +# as do the user's own tpar() settings +tpar(grid = TRUE) +plt(1, theme = "dark") +expect_true(isTRUE(get_tpar("grid"))) + +tinytheme() +invisible(dev.off()) + + # User mar override respected under dynmar (#587) f = function() { tinytheme("dynamic", mar = c(5, 5, 5, 5)) From 394c9b5f1307fe06ae8d1d7253a322380730750d Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 20 Sep 2026 20:52:25 -0700 Subject: [PATCH 3/6] faster --- R/tinyplot.R | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/R/tinyplot.R b/R/tinyplot.R index 57cc0773..a1073bf1 100644 --- a/R/tinyplot.R +++ b/R/tinyplot.R @@ -953,8 +953,12 @@ tinyplot.default = function( otheme = opar[names(dtheme)] on.exit({ if (identical(ptheme, "default")) { - # No persistent theme was active; still restore user's par settings. - do.call(tinytheme, otheme) + # No persistent theme was active. reset_tpar() below restores .tpar + # wholesale, so all that is left here is to drop the ephemeral + # theme's hooks and hand the user's own par settings back directly. + init_tpar(rm_hook = TRUE) + upar = otheme[!is.na(names(otheme))] + if (length(upar) > 0) par(upar) } else { # A persistent theme *was* active, so restore it by name. We must # not splat `opar` on top: those are the theme's pre-hook par values, From 8f59fd18c35918055ab53358d3ef845327c433de Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 20 Sep 2026 20:56:16 -0700 Subject: [PATCH 4/6] news --- NEWS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NEWS.md b/NEWS.md index f185f1d2..e554fdbc 100644 --- a/NEWS.md +++ b/NEWS.md @@ -235,6 +235,9 @@ related to plot layering. See "Bug fixes" below. ### Bug fixes +- An ephemeral `theme` argument no longer clobbers a persistent `tinytheme()` + i.e., beyond the intended single plot override. Similarly for a user's own + `tpar()` settings. (#739 @grantmcdermott) - 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 From 7b8cdffb47e2042f4b93893a6140bdf6f4cf7a30 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 20 Sep 2026 21:30:04 -0700 Subject: [PATCH 5/6] drop vignette workaround --- altdoc/pkgdown.yml | 2 +- vignettes/introduction.qmd | 11 ----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/altdoc/pkgdown.yml b/altdoc/pkgdown.yml index b99c18aa..cac7f3c9 100644 --- a/altdoc/pkgdown.yml +++ b/altdoc/pkgdown.yml @@ -2,7 +2,7 @@ altdoc: 0.7.3 pandoc: 3.10.2 pkgdown: 2.1.3 pkgdown_sha: ~ -last_built: 2026-09-19T23:23:30+0000 +last_built: 2026-09-21T04:24:20+0000 urls: reference: https://grantmcdermott.com/tinyplot/man article: https://grantmcdermott.com/tinyplot/vignettes diff --git a/vignettes/introduction.qmd b/vignettes/introduction.qmd index 5cc42123..3333a96f 100644 --- a/vignettes/introduction.qmd +++ b/vignettes/introduction.qmd @@ -341,11 +341,6 @@ tinyplot( ) ``` -```{r} -#| include: false -tinytheme("clean2") -``` - Similarly, **tinyplot** also supports special types to fit models and display their predictions, along with confidence intervals. Here is a somewhat silly example where we fit a linear model to predict temperature by day of month.^[The @@ -555,12 +550,6 @@ tinyplot( ) ``` - -```{r theme_again} -#| include: false -tinytheme("clean2") -``` - ## Save and replay plots A final point to note is that **tinyplot** offers convenience features for From d6427ece4c458bd4292b0e62cf2be7b9b9a26df3 Mon Sep 17 00:00:00 2001 From: Grant McDermott Date: Sun, 20 Sep 2026 21:30:25 -0700 Subject: [PATCH 6/6] r cmd check --- R/zzz.R | 5 ++++- inst/tinytest/test-tinytheme.R | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/R/zzz.R b/R/zzz.R index 53bcf30e..da8cfdba 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -55,6 +55,7 @@ "iby", "ifacet", "labels", + "las", "legend", "legend_args", "legend_draw_flag", @@ -107,6 +108,7 @@ "xmin", "xmin_dep", "xpad", + "xpad_user", "y", "y_dep", "yaxb", @@ -122,6 +124,7 @@ "ymax_dep", "ymin", "ymin_dep", - "ypad" + "ypad", + "ypad_user" )) } diff --git a/inst/tinytest/test-tinytheme.R b/inst/tinytest/test-tinytheme.R index 7a45204e..e0e16200 100644 --- a/inst/tinytest/test-tinytheme.R +++ b/inst/tinytest/test-tinytheme.R @@ -236,7 +236,7 @@ expect_equal(tinytheme_get(), "classic") # as do the user's own tpar() settings tpar(grid = TRUE) plt(1, theme = "dark") -expect_true(isTRUE(get_tpar("grid"))) +expect_true(isTRUE(tpar("grid"))) tinytheme() invisible(dev.off())