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 NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 29 additions & 2 deletions R/tinyplot.R
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -934,12 +941,32 @@ 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. 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,
# 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)
}
Expand Down
12 changes: 12 additions & 0 deletions R/tpar.R
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
5 changes: 4 additions & 1 deletion R/zzz.R
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"iby",
"ifacet",
"labels",
"las",
"legend",
"legend_args",
"legend_draw_flag",
Expand Down Expand Up @@ -107,6 +108,7 @@
"xmin",
"xmin_dep",
"xpad",
"xpad_user",
"y",
"y_dep",
"yaxb",
Expand All @@ -122,6 +124,7 @@
"ymax_dep",
"ymin",
"ymin_dep",
"ypad"
"ypad",
"ypad_user"
))
}
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.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
18 changes: 18 additions & 0 deletions inst/tinytest/test-tinytheme.R
Original file line number Diff line number Diff line change
Expand Up @@ -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(tpar("grid")))

tinytheme()
invisible(dev.off())


# User mar override respected under dynmar (#587)
f = function() {
tinytheme("dynamic", mar = c(5, 5, 5, 5))
Expand Down
11 changes: 0 additions & 11 deletions vignettes/introduction.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading