Search code examples
rrlanggganimate

How to temporarily change the code in a function in an R package?


I have found a bug in the gganimate package that breaks one of my graphs (https://github.com/thomasp85/gganimate/issues/502). I know how to fix it if I step through the relevant function in debug mode (https://github.com/thomasp85/gganimate/issues/502#issuecomment-2395009593). I need to run this line of code:

data <- lapply(data, function(el){
      el[order(el$x, decreasing = TRUE),]
    })

just after line 76 of the gganimate:::ggplot_build.gganim function (https://github.com/thomasp85/gganimate/blob/b46d1ff9bfee89c33ca8d6c4fd3fa1074c649027/R/plot-build.R#L76). Of course, using debug only works if I'm running the code interactively. What I'd like to do is patch this function temporarily to get my graph to come out correctly when I'm running the code non-interactively (e.g., with knitr).

I'd like to get a working graph while I wait for a bug fix in the package.

I was thinking something like this:

bdy = deparse(body(gganimate:::ggplot_build.gganim))
c(
  bdy[1:41],
  "data <- lapply(data, function(el){
      el[order(el$x, decreasing = TRUE),]
    })",
  bdy[42:length(bdy)]
  ) -> bdy 
f <- function(plot){}
body(f) <- as.expression(parse(text=paste0(bdy, collapse="\n")))

rlang::env_unlock(env = asNamespace('gganimate'))
rlang::env_binding_unlock(env = asNamespace('gganimate'))
assignInNamespace("ggplot_build.gganim", f, "gganimate")
rlang::env_binding_lock(env = asNamespace('gganimate'))
rlang::env_lock(asNamespace('gganimate'))

(from cobbling together answers https://stackoverflow.com/a/38732761/1129889 and https://stackoverflow.com/a/8502385/1129889) but it seems not to work, because it then doesn't see the gganimate package functions:

library(ggplot2)
library(gganimate)
library(ragg)
library(gifski)

dplyr::tibble(
  x = seq(0,.5,length.out=256),
  y = 1-pnorm(qnorm(x)/5)
) -> df

res = 300
w = 7
h = 4
# If this is run in knitr, w and h are in inches, otherwise
# they are in pixels
anim_w = ifelse(interactive(),w*res,w)
anim_h = ifelse(interactive(),h*res,h)


ggplot(df,aes(x=x,y=y)) +
  geom_line() +
  geom_point() +
  coord_cartesian(
    xlim=c(0,.5),ylim=c(.5,1),expand = FALSE
  ) -> gg

anim = gg + transition_reveal(x, range = c(.5,0)) 

## Try to hack the function in place
bdy = deparse(body(gganimate:::ggplot_build.gganim))
c(
  bdy[1:41],
  "data <- lapply(data, function(el){
      el[order(el$x, decreasing = TRUE),]
    })",
  bdy[42:length(bdy)]
  ) -> bdy 
f <- function(plot){}
body(f) <- as.expression(parse(text=paste0(bdy, collapse="\n")))

rlang::env_unlock(env = asNamespace('gganimate'))
rlang::env_binding_unlock(env = asNamespace('gganimate'))
assignInNamespace("ggplot_build.gganim", f, "gganimate")
rlang::env_binding_lock(env = asNamespace('gganimate'))
rlang::env_lock(asNamespace('gganimate'))

anim

produces the error:

Error in plot_clone(plot) : could not find function "plot_clone"

(I suppose because plot_clone is an internal function to gganimate)

Is there any way to do something like this (or to accomplish the same thing in another way)?

Note: I do not want to fork the package and insert this line then install my version of it (because I only need this temporary fix for one graph, and it will break other calls to the function).


Solution

  • Use trace to insert the code, run plot and untrace it. trace is based on statements rather than source lines so we used 31 rather than 76.

    The print statement can be removed after you are sure it is inserting the code at the correct point. (Note that in the print statement the second argument must be FALSE or else it will display the original source rather than the traced source.)

    e <- quote(data <- lapply(data, function(el){
          el[order(el$x, decreasing = TRUE),]
        })
    )
    trace(gganimate:::ggplot_build.gganim, e, at = 31)
    print(gganimate:::ggplot_build.gganim, FALSE) # check insertion point
    
    # ... run plot ...
    
    untrace(gganimate:::ggplot_build.gganim)