12

Just wondering if there are tricks/ways in which I could cache the plots being generated through our shiny app.

Background:

We are doing somewhat compute intensive computations which finally result in a plot. I am already caching(using memoise) the computations done, globally in shiny but it still takes about .75 seconds to render a plot. I was just wondering if we can decrease that time by removing the time it takes to render an image and if there are slick ways of already doing it.

More details:

I am using grid to create the plot(heatmap in this case. Ideally would like the caching to be disk based as storing plots in memory wont scale up.

Thanks! -Abhi

Abhi
  • 6,075
  • 10
  • 41
  • 55
  • See the example at `?renderImage`, it might give you some ideas. Basically you want a memoized plot function that returns a PNG file, I think; and use renderImage to call that memoized function. – Joe Cheng Jun 12 '14 at 23:36
  • Thanks Joe. Any ideas on how we could get shiny to auto scale static images we render through renderImage. – Abhi Jun 13 '14 at 23:32

3 Answers3

7

Edit

Caching of images created with renderPlot()/plotOutput() is supported since shiny 1.2.0.

The solution below behaves similar to the following usage of renderCachedPlot().

output$plot <- renderCachedPlot(
  expr = {
    histfaithful(bins = input$bins, col = input$col) 
  },
  cache = diskCache()
)

renderCachedPlot() allows caching in memory and on disk with sensible defaults. The rules for generating hash keys can be customized and by default digest::digest() is used for all reactive expressions that appear in expr.

The solution below demonstrates how a subset of these features (caching on disk) can be implemented with a shiny module. The basic strategy is to use

  • digest::digest() to create cache keys based on arguments sent to a plot function
  • do.call() to pass the arguments to the plot function unless the key created from digest() signifies that the image is already cached
  • grDevices::png() to capture an image from the call to do.call() and add it to the cache
  • shiny::renderImage() to serve images from the cache

Original answer

Although both answers to this question are very good, I wanted do add another one using shiny modules. The following module takes a plotfunction and a reactive version of it's arguments as inputs. In the end do.call(plotfun, args()) is used to create the plot.

library(shiny)

cachePlot <- function(input, output, session, plotfun, args, width = 480, height = 480,
                      dir = tempdir(), prefix = "cachedPlot", deleteonexit = TRUE){
  hash <- function(args) digest::digest(args)

  output$plot <- renderImage({
    args <- args()
    if (!is.list(args)) args <- list(args)
    imgpath <- file.path(dir, paste0(prefix, "-", hash(args), ".png"))

    if(!file.exists(imgpath)){
      png(imgpath, width = width, height = height)
      do.call(plotfun, args)
      dev.off()
    }
    list(src = imgpath)
  }, deleteFile = FALSE)

  if (deleteonexit) session$onSessionEnded(function(){
    imgfiles <- list.files(dir, pattern = prefix, full.names = TRUE)
    file.remove(imgfiles)
  })
}

cachePlotUI <- function(id){
  ns <- NS(id)
  imageOutput(ns("plot"))
}

As we can see, the module deletes the image files created if needed and gives the option to use a custom caching-directory in case persistent caching is needed (as it is in my actual usecase).

For a usage example, I'll use the hist(faithful[, 2]) example just like Stedy.

histfaithful <- function(bins, col){
  message("calling histfaithful with args ", bins, " and ", col) 
  x  <- faithful[, 2]
  bins <- seq(min(x), max(x), length.out = bins + 1)
  hist(x, breaks = bins, col = col, border = 'white')
}

shinyApp(
  ui = fluidPage(
    inputPanel(
      sliderInput("bins", "bins", 5, 30, 10, 1),
      selectInput("col", "color", c("blue", "red"))
    ),
    cachePlotUI("cachedPlot")
  ),
  server = function(input, output, session){
    callModule(
      cachePlot, "cachedPlot", histfaithful, 
      args = reactive(list(bins = input$bins, col = input$col))
    )
  }
)
Gregor de Cillia
  • 7,397
  • 1
  • 26
  • 43
6

Assuming you are using ggplot (which with Shiny, I would bet is a fair assumption).

  1. Create an empty list to store your grob, say Plist.
  2. When a user request a graph, create a string hash based on the shiny inputs
  3. Check if the graph is already saved, eg hash %in% names(Plist)
  4. If yes, serve up that graph
  5. If no, generate the graph, save the grob to the list, name the element by the hash, eg, Plist[hash] <- new_graph
Ricardo Saporta
  • 54,400
  • 17
  • 144
  • 178
  • 1
    thanks for quick reply. I am using grid for this..just wondering what do you mean by grob and also storing multiple plots in the memory could be expensive and also I would like the caching to be persistent across server restarts. Any packages which caches the plots to disk and render them from there ? – Abhi Jun 12 '14 at 20:47
  • also wondering do you know of any package which could create a string hash based on input to a function..this would be handy for my case. – Abhi Jun 12 '14 at 20:54
  • `grob` is just a `graphical object`. You can save them to disk much like any other R object. (see `?saveRDS`). However, loading them from disk might take just as long as calculating them from scratch. – Ricardo Saporta Jun 13 '14 at 18:08
3

The answer from Ricardo Saporta is very good and what I used to solve a similar problem, but I wanted to add a code solution as well.

For caching I used digest::digest() where I just fed a list of the parameters for that particular graph to that function to create a hash string. I initially thought that I would have to extract the hash string from observe() and then use an if/else statment to determine if I should send it to renderImage() or renderPlot() based on if the image had previously been created. I flailed with this for a while and then stumbled upon just using renderImage(). Its not a perfect image substitution but more than close enough for the purposes of this demo.

ui.R

library(shiny)

fluidPage(
  sidebarLayout(
    sidebarPanel(
       sliderInput("bins",
                   "Number of bins:",
                   min = 1,
                   max = 50,
                   value = 25),
      selectInput("plot_color", "Barplot color",
                   c("green"="green",
                      "blue"="blue"))
    ),
    mainPanel(
       plotOutput("distPlot", width='100%', height='480px')
    )
  )
)

and server.R

library(shiny)

function(input, output) {

base <- reactive({
  fn <- digest::digest(c(input$bins, input$plot_color))
  fn})

output$distPlot <- renderImage({
    filename <- paste0(base(), ".png")
    if(filename %in% list.files()){
      list(src=filename)
    } else {
    x  <- faithful[, 2]
    bins <- seq(min(x), max(x), length.out = input$bins + 1)
    png(filename)
    hist(x, breaks = bins, col = input$plot_color, border = 'white')
    dev.off()
list(src=filename)
    }

  }, deleteFile = FALSE)
}
Stedy
  • 7,359
  • 14
  • 57
  • 77