diff --git a/DESCRIPTION b/DESCRIPTION index 5b4c546f..33d49ba8 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: tinyplot Type: Package Title: Lightweight Extension of the Base R Graphics System -Version: 0.4.1 +Version: 0.4.1.99 Date: 2025-06-02 Authors@R: c( diff --git a/NEWS.md b/NEWS.md index 89667a7d..9c1c01ed 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,6 +4,15 @@ _If you are viewing this file on CRAN, please check the [latest NEWS](https://grantmcdermott.com/tinyplot/NEWS.html) on our website where the formatting is also better._ +## Development + +### Bug fixes + +- Safer handling of pre-plot hooks. Resolves an issue affecting how `tinyplot` + behaves inside loops, particularly for themed plots where only the final plot + was being drawn in Quarto/RMarkdown contexts. Special thanks to @hadley and @cderv + for helping us to debug. (@vincentarelbundock #425) + ## 0.4.1 ### Bug fixes diff --git a/R/environment.R b/R/environment.R new file mode 100644 index 00000000..6febbb7a --- /dev/null +++ b/R/environment.R @@ -0,0 +1,14 @@ +init_environment = function() { + tnypltptns = parent.env(environment()) + assign(".tinyplot_env", new.env(), envir = tnypltptns) + .tpar = new.env() + assign(".tpar", .tpar, envir = tnypltptns) +} + +get_environment_variable = function(name) { + get(name, envir = get(".tinyplot_env", envir = parent.env(environment()))) +} + +set_environment_variable = function(name, value) { + assign(name, value, envir = get(".tinyplot_env", envir = parent.env(environment()))) +} diff --git a/R/hooks.R b/R/hooks.R new file mode 100644 index 00000000..1c4af278 --- /dev/null +++ b/R/hooks.R @@ -0,0 +1,36 @@ +# Copied from https://raw.githubusercontent.com/r-lib/evaluate/refs/heads/main/R/hooks.R +# license: MIT + file LICENSE + + + +#' Set and remove hooks +#' +#' This interface wraps the base [setHook()] function to provide a return +#' value that makes it easy to undo. +#' +#' @param hooks a named list of hooks - each hook can either be a function or +#' a list of functions. +#' @param action `"replace"`, `"append"` or `"prepend"` +#' @keywords internal +set_hooks <- function(hooks, action = "append") { + old <- list() + for (hook_name in names(hooks)) { + old[[hook_name]] <- getHook(hook_name) + setHook(hook_name, hooks[[hook_name]], action = action) + } + invisible(old) +} + +#' @rdname set_hooks +#' @keywords internal +remove_hooks <- function(hooks) { + for (hook_name in names(hooks)) { + hook <- getHook(hook_name) + if (length(hook) > 0) { + for (fun in unlist(hooks[hook_name])) { + hook[sapply(hook, identical, fun)] <- NULL + } + } + setHook(hook_name, hook, "replace") + } +} diff --git a/R/tinytheme.R b/R/tinytheme.R index 23d2f90d..ab1e9f12 100644 --- a/R/tinytheme.R +++ b/R/tinytheme.R @@ -157,7 +157,8 @@ tinytheme = function( # 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) tpar(settings, hook = FALSE) - setHook("before.new.plot", NULL, "replace") + old_hooks = get_environment_variable(".tpar_hooks") + remove_hooks(old_hooks) } else { tpar(settings, hook = TRUE) } diff --git a/R/tpar.R b/R/tpar.R index d1e662df..ab587d36 100644 --- a/R/tpar.R +++ b/R/tpar.R @@ -139,7 +139,12 @@ tpar = function(..., hook = FALSE) { base_par = opts[base_par] if (length(base_par) > 0) { if (isTRUE(hook)) { - setHook("before.plot.new", function() par(base_par), action = "replace") + # append new hook to existing ones + new_hooks = list("before.plot.new" = function() par(base_par)) + set_hooks(new_hooks, action = "append") + # save new hook to tinyplot environment for later removal + old_hooks = get_environment_variable(".tpar_hooks") + set_environment_variable(".tpar_hooks", c(old_hooks, new_hooks)) } else { par_names = names(par(no.readonly = TRUE)) base_par = base_par[names(base_par) %in% par_names] @@ -310,11 +315,10 @@ init_tpar = function(rm_hook = FALSE) { rm(list = names(.tpar), envir = .tpar) if (isTRUE(rm_hook)) { - hook = getHook("before.plot.new") - if (length(hook) > 0) { - # need weird function because of Quarto evaluate::evaluate failure - # setHook("before.plot.new", NULL, action = "replace") - setHook("before.plot.new", function() NULL, action = "replace") + old_hooks = get_environment_variable(".tpar_hooks") + if (length(old_hooks) > 0) { + remove_hooks(old_hooks) + set_environment_variable(".tpar_hooks", NULL) } } diff --git a/R/zzz.R b/R/zzz.R index f75af8fa..08f3f4ee 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -7,18 +7,13 @@ .onLoad = function(libname, pkgname) { # https://stackoverflow.com/questions/12598242/global-variables-in-packages-in-r # https://stackoverflow.com/questions/49056642/r-how-to-make-variable-available-to-namespace-at-loading-time?noredirect=1&lq=1 - tnypltptns = parent.env(environment()) - assign(".tinyplot_env", new.env(), envir = tnypltptns) - .tpar = new.env() - - assign(".tpar", .tpar, envir = tnypltptns) - + init_environment() init_tpar() - - assign(".saved_par_before", NULL, envir = get(".tinyplot_env", envir = parent.env(environment()))) - assign(".saved_par_after", NULL, envir = get(".tinyplot_env", envir = parent.env(environment()))) - assign(".saved_par_first", NULL, envir = get(".tinyplot_env", envir = parent.env(environment()))) - assign(".last_call", NULL, envir = get(".tinyplot_env", envir = parent.env(environment()))) + set_environment_variable(".saved_par_before", NULL) + set_environment_variable(".saved_par_after", NULL) + set_environment_variable(".saved_par_first", NULL) + set_environment_variable(".last_call", NULL) + set_environment_variable(".tpar_hooks", NULL) globalVariables(c( "add", diff --git a/man/set_hooks.Rd b/man/set_hooks.Rd new file mode 100644 index 00000000..b1821b41 --- /dev/null +++ b/man/set_hooks.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/hooks.R +\name{set_hooks} +\alias{set_hooks} +\alias{remove_hooks} +\title{Set and remove hooks} +\usage{ +set_hooks(hooks, action = "append") + +remove_hooks(hooks) +} +\arguments{ +\item{hooks}{a named list of hooks - each hook can either be a function or +a list of functions.} + +\item{action}{\code{"replace"}, \code{"append"} or \code{"prepend"}} +} +\description{ +This interface wraps the base \code{\link[=setHook]{setHook()}} function to provide a return +value that makes it easy to undo. +} +\keyword{internal}