Render plotly plots asynchronously in shiny app

Viewed 1548

In shiny app I render couple plotly plots at once, but they render only after all of them are calculated. For example, if rendering 8 of 9 plots takes 8 seconds and rendering 9th takes 15 seconds, the first 8 plots will appear only after 9th is rendered (after 15 seconds instead of 8). See example below.

box_plot1 appears only when box_plot2 is rendered. I played a bit with shiny promises, but didn't find solution so far.

MWE:

library(shinydashboard)
library(plotly)

header <- dashboardHeader(
  title = ""
)

body <- dashboardBody(
  fluidRow(
    column(width = 6,
           box(width = NULL, solidHeader = TRUE,
               plotly::plotlyOutput("box_plot1")
           )
    ),
    column(width = 6,
           box(width = NULL, solidHeader = TRUE,
               plotly::plotlyOutput("box_plot2")
           )
    )
  )
)

ui <- dashboardPage(
  header,
  dashboardSidebar(disable = TRUE),
  body
)

server <- function(input, output, session) {

  output$box_plot1 <- plotly::renderPlotly({
    p <- plot_ly(ggplot2::diamonds, x = ~cut, y = ~price, color = ~clarity, type = "box") %>%
      layout(boxmode = "group")

    p
  })

  output$box_plot2 <- plotly::renderPlotly({

    for (i in 1:3) {
      print(i)
      Sys.sleep(1)
    }

    plot_ly(ggplot2::diamonds, y = ~price, color = ~cut, type = "box")
  })
}

shinyApp(ui = ui, server = server)
2 Answers

The answer by @DSGym works in showing one plot after another but this still does not function asynchronously. In fact if you have a plot which takes a long time to render or a data frame which take a long time to calculate, we need to perform these operations asynchronously. As an example consider this regular shiny app with no asynchronous support,

library(shinydashboard)
library(plotly)
library(future)
library(promises)

plan(multisession)

header <- dashboardHeader(
  title = ""
)

body <- dashboardBody(
  fluidRow(
    column(width = 6,
           box(width = NULL, solidHeader = TRUE,
               plotly::plotlyOutput("box_plot1")
           )
    ),
    column(width = 6,
           box(width = NULL, solidHeader = TRUE,
               plotly::plotlyOutput("box_plot2")
           )
    )
  )
)

ui <- dashboardPage(
  header,
  dashboardSidebar(disable = TRUE),
  body
)

server <- function(input, output, session) {

  output$box_plot1 <- plotly::renderPlotly({

      for (i in 1:10) {
        print(i)
        Sys.sleep(1)
      }

      plot_ly(ggplot2::diamonds, x = ~cut, y = ~price, color = ~clarity, type = "box") %>%
        layout(boxmode = "group")
  })

  output$box_plot2 <- plotly::renderPlotly({


      for (i in 11:20) {
        print(i)
        Sys.sleep(1)
      }

      plot_ly(ggplot2::diamonds, y = ~price, color = ~cut, type = "box")


  })
}

shinyApp(ui = ui, server = server)

Each plot counts to 10 and shows its output. The whole operation takes 20+ seconds to complete from when runApp() is executed.

To call both the plots asynchronously we use the futures and promises package.

library(shinydashboard)
library(plotly)
library(future)
library(promises)

plan(multisession)

header <- dashboardHeader(
  title = ""
)

body <- dashboardBody(
  fluidRow(
    column(width = 6,
           box(width = NULL, solidHeader = TRUE,
               plotly::plotlyOutput("box_plot1")
           )
    ),
    column(width = 6,
           box(width = NULL, solidHeader = TRUE,
               plotly::plotlyOutput("box_plot2")
           )
    )
  )
)

ui <- dashboardPage(
  header,
  dashboardSidebar(disable = TRUE),
  body
)

server <- function(input, output, session) {

  output$box_plot1 <- plotly::renderPlotly({
    future({
      for (i in 1:10) {
        print(i)
        Sys.sleep(1)
      }

      plot_ly(ggplot2::diamonds, x = ~cut, y = ~price, color = ~clarity, type = "box") %>%
        layout(boxmode = "group")
    })
  })

  output$box_plot2 <- plotly::renderPlotly({

    future({
      for (i in 11:20) {
        print(i)
        Sys.sleep(1)
      }

      plot_ly(ggplot2::diamonds, y = ~price, color = ~cut, type = "box")
    })

  })
}

shinyApp(ui = ui, server = server)

Now, even though both plots count up to 10, the plots execute asynchronously. The total time to load the plots reduced to below 20 seconds.

However, both plots still load together. This is because of the inherent flush cycle in shiny. Hence, even if we execute the plots asynchronously, all plots will always load at the same time.

You can read more about this here: https://rstudio.github.io/promises/articles/shiny.html

You can use renderUI in combination with reactiveValues which keep track of the order of the calculations.

library(shinydashboard)
library(plotly)

header <- dashboardHeader(
    title = ""
)

body <- dashboardBody(
    fluidRow(
        column(width = 6,
               uiOutput("plot1")
        ),
        column(width = 6,
               uiOutput("plot2")
        )
    )
)

ui <- dashboardPage(
    header,
    dashboardSidebar(disable = TRUE),
    body
)

server <- function(input, output, session) {

    rv <- reactiveValues(val = 0)


    output$plot1 <- renderUI({

        output$box_plot1 <- plotly::renderPlotly({

            for (i in 3:5) {
                print(i)
                Sys.sleep(1)
            }

            p <- plot_ly(ggplot2::diamonds, x = ~cut, y = ~price, color = ~clarity, type = "box") %>%
                layout(boxmode = "group")
            rv$val <- 1
            p
        })

        return(
            tagList(
                box(width = NULL, solidHeader = TRUE,
                    plotly::plotlyOutput("box_plot1")
                )
            )
        )

    })



    output$plot2 <- renderUI({

        if(rv$val == 0) {
            return(NULL)
        }

        output$box_plot2 <- plotly::renderPlotly({

            for (i in 1:3) {
                print(i)
                Sys.sleep(1)
            }

            plot_ly(ggplot2::diamonds, y = ~price, color = ~cut, type = "box")
        })

        return(
            tagList(
                box(width = NULL, solidHeader = TRUE,
                    plotly::plotlyOutput("box_plot2")
                )
            )
        )

    })



}

shinyApp(ui = ui, server = server)
Related