Search code examples
rr-markdownknitr

How to save all ggplots of a `.Rmd` as `.rds`


I use markdown documents for my analyses. I create lots of plots and use knitr::opts_chunk$set(dev= c("png", "svg", "pdf") and rmarkdown::render(... , clean = FALSE) to obtain png ( for use with google slides) , svg (for use with powerpoint) and pdf (to be included in manual latex reports). However, I would now like to combine plots from different analyses into figure panels while being able to change plot sizes and aspect ratios without rerunning all the analyses each time.

One way to achieve this is to save the ggplots in .rds files using saveRDS(ggplot2::last_plot(), "figure_1.rds") in the analysis notebooks and library(patchwork); readRDS("figure_1.rds") / readRDS("figure_2.rds") in a separate script generating the figure panels. This could partially be automated using a hook:

example_analysis.Rmd

```{r setup}
knitr::opts_chunk$set(dev= c("png", "svg", "pdf")
knitr::knit_hooks$set(hook_save_plot_as_rds = function(before, options, envir, name) {
  if(before) return() # run only after chunk
  if(length(knitr:::get_plot_files())==0) return() # only run if 
  saveRDS(ggplot2::last_plot(), knitr::fig_chunk(knitr::opts_chunk$get("label"), ext = "rds"))
})
```
Here we do some heavy analysis
```{r sepal-plot}
ggplot(iris, aes(Sepal.Width, Sepal.Length)) + geom_point()
```

Here we do some more heavy analysis
```{r petal-plot}
ggplot(iris, aes(Petal.Width, Petal.Length)) + geom_point()
```

Somewhere else:

library(patchwork)
get_figure <- function(name) readRDS(paste0("example_analysis_files/figure-html/", name, "-1.rds"))
get_figure("petal-plot") / get_figure("sepal-plot") + plot_annotation(tag_levels="A")

But that only works for the last ggplot of each chunk. Is there a way that works for all plots of chunk? Is there maybe a hidden device="rds"?


Solution

  • The solution is actually quite simple: the right place to do this is not an extra device or some hooks, but the print function. knitr provides the knit_print S3 generic that gives control over how R objects are printed by knitr. One can simply add a knit_print.ggplot that also saves the plot.

    I knew yihui must have made this possible.

    Full example:

    example_analysis.Rmd

    ---
    title: "example_analysis"
    output: 
      html_document:
        keep_md: yes
    ---
    
    ```{r setup}
    local({ # to keep global environment uncluttered
      counter <- NA
      previous_label <- NA
      print_and_save.ggplot <- function(x, ...) {
        ret <-  print(x, ...)
        current_label <- knitr::opts_current$get("label")
        if(isTRUE(previous_label==current_label)) {
          counter <<- counter+1 # keep track of plot number
        } else { # reset plot number for each new chunk
          previous_label <<- current_label
          counter <<- 1
        }
        dir.create(knitr::opts_current$get("fig.path"), recursive = TRUE, showWarnings = FALSE)
        saveRDS(ret, knitr::fig_path(suffix = "rds", number = counter))
        invisible(ret)
      }
      library(knitr)
      registerS3method("knit_print", "ggplot", print_and_save.ggplot)
    })
    library(ggplot2)
    ```
    
    Here we do some heavy analysis
    ```{r sepal-plot}
    ggplot(iris, aes(Sepal.Width, Sepal.Length, fill= Species)) + geom_point()
    ggplot(iris, aes(Sepal.Length, fill= Species)) + geom_histogram() + coord_flip()
    ```
    
    Here we do some more heavy analysis
    ```{r petal-plot}
    ggplot(iris, aes(Petal.Width, Petal.Length, fill= Species)) + geom_point()
    ggplot(iris, aes(Petal.Length, fill= Species)) + geom_histogram() + coord_flip()
    ```
    
    

    somewhere else:

    library(patchwork)
    get_figure <- function(name, idx = 1) readRDS(paste0("example_analysis_files/figure-html/", name, "-", idx,".rds"))
    (get_figure("petal-plot") + get_figure("petal-plot", 2))  / (get_figure("sepal-plot") + get_figure("sepal-plot", 2)) + plot_annotation(tag_levels="A")
    #> `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
    #> `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.