0

I am trying to download a already plotted ggplot image without replotting it again. All the solutions I found (e.g. Save plots made in a shiny app) calling the function to create the figure again when they download the image. Is there a workaround? My images are very complex and take some time to create.

Community
  • 1
  • 1
drmariod
  • 11,106
  • 16
  • 64
  • 110

2 Answers2

2

This is an old thread. But I also needed to figure out how to avoid replotting in Shiny. My application generates many plots and calling last_plot() in ggsave() does not help much.

I figured it out. Indeed, since we already have an image in our application - we can just save that image. Right-click and select "Save Image" in browser. Although, this is not great - a download button and automation for downloading many plots would be nice to have.

To do that:

  1. I used javascript to get image URI (use library(shinyjs) , call useShinyjs() in the ui section of the app and call runjs() in the server section);
  2. Javascript for some reason will not work inside downloadHandler. So, I had to make actionButton that runs the javascript and also simulates a .click() on downloadButton. I learned this trick here: link
  3. Use library(magick) for image decoding from URI and saving. This is the link how to do that: link

Here is the example code:

library(shiny)
library(magick)
library(shinyjs)

ui <- fluidPage(
  useShinyjs(),
  #visibale action button - this button simulates click on hidden download button 
  actionButton("save_myPlot", "Download", icon = icon("download")),
  #hidden download button
  downloadButton("save_myPlot_hidden", style = "visibility: hidden;"),
  # plot
  plotOutput("myPlot")
)

server <- function(input, output, session) {
  
  #get the plot image URI and simulate click on download button
  observeEvent(input$save_myPlot, {
    shinyjs::runjs(
      "
       var p_src = document.getElementById('myPlot').childNodes[0].src; 
       Shiny.setInputValue('plot_src', p_src);
       document.getElementById('save_myPlot_hidden').click();
      "
    )
  })
  
  # downaload handler - save the image
  output$save_myPlot_hidden <- downloadHandler(
    filename = function() { 
      paste0("plot_", Sys.Date(), ".png") },
    content = function(file) {
      # get image code from URI
      plot_src <- gsub("^data.*base64,", "", input$plot_src)
      # decode the image code into the image
      plot_image <- image_read(base64_decode(plot_src))
      # save the image
      image_write(plot_image, file)
    })
  
  # plot
  output$myPlot <- renderPlot(
    plot(rnorm(5), rnorm(5))
  )
}

shinyApp(ui = ui, server = server)
Pavel Khokhlov
  • 160
  • 1
  • 8
1

Use the ggplot2::last_plot function:

library(shiny)
library(ggplot2)
k <- 0

runApp(list(
  ui = fluidPage(
    plotOutput("fooplot"),
    textOutput("fook"),
    downloadButton('foo')
    ),
  server = function(input, output) {
    plotInput = function() {
      k <<- k + 1

      qplot(speed, dist, data = cars)
    }
    output$fooplot <- renderPlot({
      plotInput()
    })

    output$fook <- renderPrint({
      k
    })

    output$foo = downloadHandler(
      filename = 'test.png',
      content = function(file) {
        device <- function(..., width, height) {
          grDevices::png(..., width = width, height = height,
                         res = 300, units = "in")
        }
        ggsave("myplot.png", plot = last_plot(), device = device)
      })
  }
))

Forgive the use of the global assignment, just including to show that the plotInput is not called twice.

mlegge
  • 6,763
  • 3
  • 40
  • 67