How to save plots that are made in a shiny app

53,413

Solution 1

Not sure if this question is still active but it's the first one that came up when searching for "saving plots in shiny app" so I wanted to quickly add how to get ggsave to work with downloadHandler along the lines of the original question.

The alternative strategies suggested by juba using direct output instead of ggsave and alternative strategy suggested by alexwhan himself both work great, this is just for those who absolutely want to use ggsave in the downloadHandler).

The problem reported by alexwhan is caused by ggsave trying to match the file extension to the correct graphics device. The temporary file, however, doesn't have an extension so the matching fails. This can be remedied by specifically setting the device in the ggsave function call, like so in the original code example (for a png):

output$downloadPlot <- downloadHandler(
    filename = function() { paste(input$dataset, '.png', sep='') },
    content = function(file) {
        device <- function(..., width, height) grDevices::png(..., width = width, height = height, res = 300, units = "in")
        ggsave(file, plot = plotInput(), device = device)
    }
)

This call basically takes the device function for a png that ggsave assigns internally (you can look at the ggsave function code to see the syntax for jpg, pdf, etc). Perhaps, ideally, one could specify the file extension (if different from the file name - as is the case here for the temporary file) as a ggsave parameter but this option is currently not available in ggsave.


A minimal self-contained working example:

library(shiny)
library(ggplot2)
runApp(list(
  ui = fluidPage(downloadButton('foo')),
  server = function(input, output) {
    plotInput = function() {
      qplot(speed, dist, data = cars)
    }
    output$foo = downloadHandler(
      filename = 'test.png',
      content = function(file) {
        device <- function(..., width, height) {
          grDevices::png(..., width = width, height = height,
                         res = 300, units = "in")
        }
        ggsave(file, plot = plotInput(), device = device)
      })
  }
))

sessionInfo()
# R version 3.1.1 (2014-07-10)
# Platform: x86_64-pc-linux-gnu (64-bit)
# 
# locale:
#  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
#  [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
#  [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
#  [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
#  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
# [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       
# 
# attached base packages:
# [1] stats     graphics  grDevices utils     datasets  methods   base     
# 
# other attached packages:
# [1] ggplot2_1.0.0 shiny_0.10.1 
# 
# loaded via a namespace (and not attached):
#  [1] bitops_1.0-6     caTools_1.17     colorspace_1.2-4 digest_0.6.4    
#  [5] formatR_1.0      grid_3.1.1       gtable_0.1.2     htmltools_0.2.6 
#  [9] httpuv_1.3.0     labeling_0.2     MASS_7.3-34      munsell_0.4.2   
# [13] plyr_1.8.1       proto_0.3-10     Rcpp_0.11.2      reshape2_1.4    
# [17] RJSONIO_1.3-0    scales_0.2.4     stringr_0.6.2    tools_3.1.1     
# [21] xtable_1.7-3    

Update

As of ggplot2 version 2.0.0, the ggsave function supports character input for the device parameter, that means the temporary file created by the downloadHandler can now be saved with a direct call to ggsave by specifying that the extension to be used should be e.g. "pdf" (rather than passing in a device function). This simplifies the above example to the following

output$downloadPlot <- downloadHandler(
    filename = function() { paste(input$dataset, '.png', sep='') },
    content = function(file) {
        ggsave(file, plot = plotInput(), device = "png")
    }
)

Solution 2

I didn't manage to make it work with ggsave, but with a standard call to png() it seems to be okay.

I only changed the output$downloadPlot part of your server.R file :

 output$downloadPlot <- downloadHandler(
    filename = function() { paste(input$dataset, '.png', sep='') },
    content = function(file) {
      png(file)
      print(plotInput())
      dev.off()
    })

Note that I had some problems with the 0.3 version of shiny, but it works with the latest from Github :

library(devtools)
install_github("shiny","rstudio")

Solution 3

Here's a solution that allows using ggsave for saving shiny plots. It uses a logical checkbox and text input to call ggsave(). Add this to the ui.R file inside sidebarPanel:

textInput('filename', "Filename"),
checkboxInput('savePlot', "Check to save")

Then add this to the server.R file instead of the current output$plot reactivePlot function:

output$plot <- reactivePlot(function() {
    name <- paste0(input$filename, ".png")
    if(input$savePlot) {
      ggsave(name, plotInput(), type="cairo-png")
    }
    else print(plotInput())
  })

A user can then type the desired filename in the textbox (without extension) and tick the checkbox to save in the app directory. Unchecking the box prints the plot again. I'm sure there are neater ways of doing this, but at least I can now use ggsave and cairo in windows for much nicer png graphics.

Please add any suggestions you may have.

Solution 4

This is old, but still the top hit when someone googles "R shiny save ggplot", so I will contribute another workaround. Very simple... call ggsave in the same function that displays your graph, which will save the graph as a file on the server.

output$plot <- renderPlot({
    ggsave("plot.pdf", plotInput())
    plotInput()
})

Then, use downloadHandler and use file.copy() to write data from the existing file to the "file" parameter.

output$dndPlot <- downloadHandler(
    filename = function() {
        "plot.pdf"
    },
    content = function(file) {
        file.copy("plot.pdf", file, overwrite=TRUE)
    }
)

Works for me.

Share:
53,413
alexwhan
Author by

alexwhan

Postdoc at CSIRO in Canberra Australia. Using R for analysis of biological experimental data.

Updated on January 06, 2022

Comments

  • alexwhan
    alexwhan over 2 years

    I'm trying to figure out how to use downloadButton to save a plot with shiny. The example in the package demonstrates downloadButton/downloadHandler to save a .csv. I'm going to make a reproducible example based on that.

    For ui.R

    shinyUI(pageWithSidebar(
      headerPanel('Downloading Data'),
      sidebarPanel(
    selectInput("dataset", "Choose a dataset:", 
                choices = c("rock", "pressure", "cars")),
        downloadButton('downloadData', 'Download Data'),
        downloadButton('downloadPlot', 'Download Plot')
      ),
      mainPanel(
        plotOutput('plot')
      )
    ))
    

    For server.R

    library(ggplot2)
    shinyServer(function(input, output) {
      datasetInput <- reactive({
        switch(input$dataset,
               "rock" = rock,
               "pressure" = pressure,
               "cars" = cars)
      })
      
      plotInput <- reactive({
        df <- datasetInput()
        p <-ggplot(df, aes_string(x=names(df)[1], y=names(df)[2])) +
          geom_point()
      })
      
      output$plot <- renderPlot({
        print(plotInput())
      })
      
      output$downloadData <- downloadHandler(
        filename = function() { paste(input$dataset, '.csv', sep='') },
        content = function(file) {
          write.csv(datatasetInput(), file)
        }
      )
      output$downloadPlot <- downloadHandler(
        filename = function() { paste(input$dataset, '.png', sep='') },
        content = function(file) {
          ggsave(file,plotInput())
        }
      )
    })
    

    If you're answering this question, you are probably familiar with this, but to get this working, save the above into separate scripts (ui.R and server.R into a folder (foo) within the working directory. To run the shiny app, run runApp("foo").

    Using ggsave, I get an error message indicating that ggsave can't use the filename function (I think). If I use the standard graphics device (like below), the Download Plot works without an error, but it doesn't write the graphic.

    Any tips to get downloadHandler working for writing plots would be appreciated.

  • alexwhan
    alexwhan about 11 years
    OK, I'm going to accept that ggsave isn't going to work at this stage of proceedings with downloadHandler. shiny 0.3 falls apart with downloadHandler, you're right. I'll post an alternative solution I figured out avoiding downloadHandler which will allow ggsave to work.
  • jpd527
    jpd527 over 10 years
    Without an isolate block around the input$filename, any change to the filename text box will also prompt a file save if the box is checked.
  • Yihui Xie
    Yihui Xie over 9 years
    I believe your answer is actually the correct one here. You can also just use ggsave(file, plotInput(), device = png) instead of creating a device (wrapper) function.
  • alexwhan
    alexwhan over 9 years
    @sebkopf I missed your answer in the intervening year and a bit!
  • zx8754
    zx8754 over 9 years
    @Yihui This solution doesn't work for me: R version 3.1.0, ggplot2_1.0.0 shiny_0.10.1. Save box pops up, click save, but no file is saved. Can anyone confirm?
  • Yihui Xie
    Yihui Xie over 9 years
    @zx8754 I just added a full example to the answer. Note you should run it in your web browser instead of viewing it in RStudio, since the RStudio viewer has a known bug of being unable to download files.
  • zx8754
    zx8754 over 9 years
    @Yihui Thanks, yes I was running it on RStudio viewer... Ah!
  • sebkopf
    sebkopf over 9 years
    @Yihui Thanks for adding the full example. You probably found this already but just for clarification: the device wrapper function is required because the parameters passed by ggsave are not always compatible with the device defaults (e.g. for the png function's units parameter the default is "px"). For a general solution to this question, there's a pull request for a feature that would allow ggsave(... device="png") as an easy way to have ggsave specifically pick its own png wrapper.
  • Yihui Xie
    Yihui Xie over 9 years
    @sebkopf Yes, I realized it after I tried a real example, so my first comment here was actually wrong. Thanks for the clarification!
  • geotheory
    geotheory over 9 years
    @juba any idea why this attempt to output to pdf with a similar (non-ggplot2) method doesn't work? I just get a broken pdf that doesn't open. Can plotInput not deliver a plot instead of a plot object?
  • agenis
    agenis over 6 years
    ggsave with argument lastPlot() really saves the day!
  • nexonvantec
    nexonvantec over 4 years
    Typo: In the update it should say "output$foo" instead of "output$downloadPlot", shouldn't it?
  • Steven Ouellette
    Steven Ouellette about 2 years
    This was the best solution for me - thanks!