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.)
<- function(input, output) {
server $plot <- renderPlot({
output$goPlot # Re-run when button is clicked
input
# Create 0-row data frame which will be used to store data
<- data.frame(x = numeric(0), y = numeric(0))
dat
withProgress(message = 'Making plot', value = 0, {
# Number of times we'll go through the loop
<- 10
n
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.
<- rbind(dat, data.frame(x = rnorm(1), y = rnorm(1)))
dat
# 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)
})
}
<- shinyUI(basicPage(
ui 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.
<- function(input, output) {
server $plot <- renderPlot({
output$goPlot # Re-run when button is clicked
input
# Create 0-row data frame which will be used to store data
<- data.frame(x = numeric(0), y = numeric(0))
dat
# Create a Progress object
<- shiny::Progress$new()
progress # Make sure it closes when we exit this reactive, even if there's an error
on.exit(progress$close())
$set(message = "Making plot", value = 0)
progress
# Number of times we'll go through the loop
<- 10
n
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.
<- rbind(dat, data.frame(x = rnorm(1), y = rnorm(1)))
dat
# Increment the progress bar, and update the detail text.
$inc(1/n, detail = paste("Doing part", i))
progress
# Pause for 0.1 seconds to simulate a long computation.
Sys.sleep(0.1)
}
plot(dat$x, dat$y)
})
}
<- shinyUI(basicPage(
ui 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.
<- function(updateProgress = NULL) {
compute_data # Create 0-row data frame which will be used to store data
<- data.frame(x = numeric(0), y = numeric(0))
dat
for (i in 1:10) {
Sys.sleep(0.25)
# Compute new row of data
<- data.frame(x = rnorm(1), y = rnorm(1))
new_row
# If we were passed a progress update function, call it
if (is.function(updateProgress)) {
<- paste0("x:", round(new_row$x, 2), " y:", round(new_row$y, 2))
text updateProgress(detail = text)
}
# Add the new row of data
<- rbind(dat, new_row)
dat
}
dat
}
<- function(input, output) {
server $table <- renderTable({
output$goTable
input
# Create a Progress object
<- shiny::Progress$new()
progress $set(message = "Computing data", value = 0)
progress# 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.
<- function(value = NULL, detail = NULL) {
updateProgress if (is.null(value)) {
<- progress$getValue()
value <- value + (progress$getMax() - value) / 5
value
}$set(value = value, detail = detail)
progress
}
# Compute the new data, and pass in the updateProgress function so
# that it can update the progress indicator.
compute_data(updateProgress)
})
}
<- shinyUI(basicPage(
ui 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:
<- 20
n <- function(detail = NULL) {
updateProgress $inc(amount = 1/n, detail = detail)
progress }
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.