Progress indicators

If your Shiny app contains computations that take a long time to complete, a progress bar can improve the user experience by communicating how far along the computation is, and how much is left. Progress bars were added in Shiny 0.10.2. In Shiny 0.14, they were changed to use the notifications system, although the previous styling can be used (see the Old style progress bars section below).

To see progress bars in action, see this app in the gallery.

Adding a progress indicator

The simplest way to add a progress indicator is to put withProgress() inside of the reactive(), observer(), or renderXx() function that contains the long-running computation. In this example, we’ll simulate a long computation by creating an empty data frame and then adding one row to it every 0.1 seconds. (Note that this example is written as a single-file app). To run this, you can copy and paste the code into the R console.)

server <- function(input, output) {
  output$plot <- renderPlot({
    input$goPlot # Re-run when button is clicked

    # Create 0-row data frame which will be used to store data
    dat <- data.frame(x = numeric(0), y = numeric(0))

    withProgress(message = 'Making plot', value = 0, {
      # Number of times we'll go through the loop
      n <- 10

      for (i in 1:n) {
        # Each time through the loop, add another row of data. This is
        # a stand-in for a long-running computation.
        dat <- rbind(dat, data.frame(x = rnorm(1), y = rnorm(1)))

        # Increment the progress bar, and update the detail text.
        incProgress(1/n, detail = paste("Doing part", i))

        # Pause for 0.1 seconds to simulate a long computation.
        Sys.sleep(0.1)
      }
    })

    plot(dat$x, dat$y)
  })
}

ui <- shinyUI(basicPage(
  plotOutput('plot', width = "300px", height = "300px"),
  actionButton('goPlot', 'Go plot')
))

shinyApp(ui = ui, server = server)

This is what will happen:

The withProgress() function is used to start a progress bar, and then the value is incremented with incProgress(). By default, the range of values for the bar goes from 0 to 1, although this can be changed with the min and max arguments.

There are two levels of messages: message, and detail. The message is presented in bold, and the detail is presented in normal-weight text.

In the example above, withProgress() is used inside of renderPlot(), but it could also be used inside of any other render function, like renderTable(), or inside of a reactive().

It’s possible to nest calls to withProgress; if you do this, the second-level progress bar will appear directly under the top-level progress bar, and the second-level text will appear under the top-level text. Further levels of nesting will have a similar pattern.

Using a Progress object

The withProgress() function is a convenient interface around a Progress object. In most cases, it’s simpler and easier to use withProgress, but in some cases, you may need the greater level of control provided by the Progress object. Before we delve into a more complex example, we’ll simply convert the example above from using withProgress to using a Progress object.

server <- function(input, output) {
  output$plot <- renderPlot({
    input$goPlot # Re-run when button is clicked

    # Create 0-row data frame which will be used to store data
    dat <- data.frame(x = numeric(0), y = numeric(0))

    # Create a Progress object
    progress <- shiny::Progress$new()
    # Make sure it closes when we exit this reactive, even if there's an error
    on.exit(progress$close())

    progress$set(message = "Making plot", value = 0)

    # Number of times we'll go through the loop
    n <- 10

    for (i in 1:n) {
      # Each time through the loop, add another row of data. This is
      # a stand-in for a long-running computation.
      dat <- rbind(dat, data.frame(x = rnorm(1), y = rnorm(1)))

      # Increment the progress bar, and update the detail text.
      progress$inc(1/n, detail = paste("Doing part", i))

      # Pause for 0.1 seconds to simulate a long computation.
      Sys.sleep(0.1)
    }

    plot(dat$x, dat$y)
  })
}

ui <- shinyUI(basicPage(
  plotOutput('plot', width = "300px", height = "300px"),
  actionButton('goPlot', 'Go plot')
))

shinyApp(ui = ui, server = server)

Notice that we need to explicitly create the progress object and make sure that it closes properly, using on.exit().

A more complex Progress example

In the example below, the renderTable() calls out to another function, compute_data(), to do the long-running computation. If we were to just update the progress indicator before and after compute_data() were called, then it would only be updated at the beginning, when nothing has been done yet, and at the end, when the computation is completed. In some cases, the best we can do may be to set it to a starting value of, say, 0.3, and then move it to 1 at completion. This may be true if, for example, the function is in an external package.

However, if you do have control over the function doing the computation, you may want to modify it to accept either a Progress object which it will update directly, or to accept a function which it calls each time it does some part of the computation.

In the example below, we’ll take the latter approach. The compute_data() function accepts an optional updateProgress function, which it calls periodically as it does the computation. The updateProgress function is a closure that captures the Progress object; each time it’s called, it updates the progress indicator.

Again, you can copy and paste this code in your R console to see it in action:

# This function computes a new data set. It can optionally take a function,
# updateProgress, which will be called as each row of data is added.
compute_data <- function(updateProgress = NULL) {
  # Create 0-row data frame which will be used to store data
  dat <- data.frame(x = numeric(0), y = numeric(0))

  for (i in 1:10) {
    Sys.sleep(0.25)
  
    # Compute new row of data
    new_row <- data.frame(x = rnorm(1), y = rnorm(1))

    # If we were passed a progress update function, call it
    if (is.function(updateProgress)) {
      text <- paste0("x:", round(new_row$x, 2), " y:", round(new_row$y, 2))
      updateProgress(detail = text)
    }

    # Add the new row of data
    dat <- rbind(dat, new_row)
  }

  dat
}


server <- function(input, output) {
  output$table <- renderTable({
    input$goTable

    # Create a Progress object
    progress <- shiny::Progress$new()
    progress$set(message = "Computing data", value = 0)
    # Close the progress when this reactive exits (even if there's an error)
    on.exit(progress$close())
    
    # Create a callback function to update progress.
    # Each time this is called:
    # - If `value` is NULL, it will move the progress bar 1/5 of the remaining
    #   distance. If non-NULL, it will set the progress to that value.
    # - It also accepts optional detail text.
    updateProgress <- function(value = NULL, detail = NULL) {
      if (is.null(value)) {
        value <- progress$getValue()
        value <- value + (progress$getMax() - value) / 5
      }
      progress$set(value = value, detail = detail)
    }

    # Compute the new data, and pass in the updateProgress function so
    # that it can update the progress indicator.
    compute_data(updateProgress)
  })
}

ui <- shinyUI(basicPage(
  tableOutput('table'),
  actionButton('goTable', 'Go table')
))

shinyApp(ui = ui, server = server)

It’s possible to use other constructions for the updateProgress function that have different behavior. In the example above, each time updateProgress() is called, the progress bar moves 1/5 of the remaining distance. This tells the user that something is happening, and it’s simple because you don’t need to know ahead of time how many times it’s goingto run. However, it’s not the most accurate representation of progress, since it approaches the end asymptotically, whereas a linear approach would be more accurate.

One alternative is to have the external function call updateProgress() with a specific value. If, for example, the external function knows that it will iterate over the loop 100 times, it could call updateProgress() with value=0.01, then value=0.02, and so on.

Another alternative is to construct a different updateProgress callback, one which increments by a fixed amount each time. To do this, before you call compute_data(), you must know how many times it will call updateProgress() in the loop. Let’s assume that it will be called 20 times. Then updateProgress could be defined like so:

    n <- 20
    updateProgress <- function(detail = NULL) {
      progress$inc(amount = 1/n, detail = detail)
    }

Each time this version of updateProgress() is called, it moves the bar 1/20th of the total distance.

Old style progress bars

In Shiny 0.14, the progress bars switched to Shiny’s notification API. However, if you created application that used the old progress bars and had custom styling with CSS, you will need to use the old style output to keep the custom styling. This can be done with by calling withProgress() or Progress$new() with the argument style="old". For an example, see the example app.

Recap

You can add progress indicators to your app, using the simpler withProgress() interface, or the Progress object if you need more control. These progress indicators can provide feedback to the user that will make their experience more satisfying.



We love it when R users help each other, but RStudio does not monitor or answer the comments in this thread. If you'd like to get specific help, we recommend the RStudio Community as well as the Shiny Discussion Forum for in depth discussion of Shiny related questions and How to get help article for a list of the best ways to get help with R code.

comments powered by Disqus

Start
Build
Improve