Interactivity with Shiny

Overview

Teaching: 60 min
Exercises: 60 min
Questions
  • How can I create interactive visualisations?

Objectives
  • To explain the idea of reactivity in Shiny

  • To trace a reactive path through a basic app

  • To create an interactive app using multiple inputs and outputs

Creating interactive web applications provides an alternative way of presenting your data. Rather than providing a single, predetermined summary of your data, you can instead provide the tools for your end users to investigate your data in their own way.

The basic tools for producing these apps can be found in the Shiny package. These apps can be extended with a little bit of HTML, CSS or JavaScript, but you can create a fully functional app using only R code.

Reactivity in Shiny

Creating these interactive applications will involve a pretty large conceptual shift however, because it ‘breaks’ the way we have learned R behaves.

Standard behaviour

Consider the following lines of code:

a <- 10
b <- a + 5

b

a <- 20

b

What is the value of b each time it is printed?

Solution

b is 15 both times.

In a traditional environment, when R creates a variable from an expression it determines the value of that variable at the time of assignment and stores it. Once stored, the variable loses any connection to the values were used to create it. So in the example above, once the value of b has been set the value of a can be changed without any effect on b.

The Shiny framework provides a reactive environment, in which inputs and outputs can be linked together, allowing them to all update in response to changes. Conceptually, the above example running in a reactive environment would result in:

a <- 10
b <- a + 5
 
b
[1] 15
a <- 20

b
[1] 25

The variable b now updates its value in response to changes in a. Shiny handles the process of creating this interactivity in the background, and you can read more about it here. From our perspective however, changes to one value in our code will appear to flow on to other parts of code that depend on them.

The one limitation that Shiny imposes on our code is that any elements you create within a reactive environment cannot be accessed by elements in the traditional environment. Since the value of a reactive element is not fixed, it is impossible for traditional evaluation to occur. The opposite is not true however, reactive elements can access other reactive elements as well as those in the traditional environment.

An app in two halves

When writing a Shiny app, your code will be broken up into two separate parts.

User Interface

The first is the user interface (UI) of the app. This contains all of the inputs that your end user will be able to control, and the outputs that they will see. The final result of your UI code is a single HTML page that can be opened in a web browser.

Server

The second is the server logic of the app. This is what will run your code in response to user input, and create the outputs to provide to the user. This part of the code requires an active R session to function properly, which means you need to find a way to host your app if you want to make it accessible to other people.

Creating an app

Shiny comes with a number of inbuilt example applications that can be run from the R console. We’ll explore one of these before we start diving in to creating your own

The end product

The first example app can be run with:

library(shiny)
runExample("01_hello")

Explore this basic example and list the reactive, and non-reactive elements you can find. Can you further classify the reactive elements as inputs or outputs?

You have been interacting with the UI half of the example application. What do you think is happening in the server half?

Creating a Shiny app of your own can be done within RStudio by creating a new Project in a fresh directory. At the Project Type window, select Shiny Web Application.

This creates a skeleton application with the code from the example we just looked at. To run this app again, click the Run App button in the top corner of the code editor pane .

Exploring an app

Run the default app created when you set up the project and confirm that it has the same function as the example app run previously.

Look through the the code of the app to identify:

  • The UI and the server code
  • The UI inputs and outputs you identified in the previous exercise
  • How those inputs and outputs are referred to in the server

Hint: Searching the file (Ctrl+F) for ‘input’ and ‘output’ will help with the last two parts if you are stuck.

Solution

  • UI: lines 13–33, Server: lines 36–46
  • Number of bins input slider: lines 21–25, Plot output: line 30
  • Input slider: line 41 (input$bins), Output plot: lines 38–45 (output$distPlot)

The functions used in the UI part of the code, just produce the HTML code that makes up the web application. You can see this for yourself by running some of these functions at the R console. For example:

titlePanel("Old Faithful Geyser Data")
<h2>Old Faithful Geyser Data</h2>

or

plotOutput("distPlot")
<div id="distPlot" class="shiny-plot-output" style="width: 100% ; height: 400px"></div>

Within the server code, you will mostly see R expressions like you are used to. Two variables are created (x and bins) and then a histogram is produced with hist().

Changing things around (optional)

There are some small differences between the example app you ran initially and your current code. To properly recreate the example edit your code to:

  • Change the title of the page to “Hello Shiny!”
  • Change the colour of the histogram from "darkgrey" to "#75AADB"
  • Add a title to the histogram reading “Histogram of waiting times” (main argument of hist() function)
  • Add a label to the x axis of the histogram reading “Waiting time to next eruption (in mins)” (xlab argument of hist() function)

Talking together

The UI and server in an app communicate using the input and output objects seen in the server code of the example app. The slider in the UI code is set up with the ID bins (sliderInput("bins", ...)) and then the value of the slider is able to be accessed in the server by using input$bins.

Similarly, the histogram that is created is saved as output$distPlot. The ID this is saved under (distPlot) is then referenced in the UI when creating a place to display the histogram (plotOutput("distPlot")).

You might notice that there is no explicit connection made between the input and the output in this code. Instead, there is an implicit connection because the output plot uses the input$bins value in creating the histogram. Shiny keeps track of these ‘dependencies’ (the histogram plot is dependent on the input$bins value), and reruns code if the dependencies have changed.

So when the slider is moved, the value of input$bins changes. Shiny knows that the output$distPlot is dependent on the input$bins value and asks the code that produces the histogram to run again. While running again the new input$bins value is used, creating a different histogram.

Inputs

So far, we have looked at a single type of input - a slider that changes a single numerical value. In the UI code, this slider is created with the sliderInput() function:

sliderInput("bins",
  "Number of bins:",
  min = 1,
  max = 50,
  value = 30)

The first argument ("bins") is the ID that will be associated with the slider. This ID is how we can access the value of this slider in our code (through input$bins) and so must be unique within your app.

The other arguments determine the label of the slider (the text that is displayed in the UI describing what the slider controls) as well as the minimum and maximum values it can take. The value argument determines what the initial value of the slider will be when the app is started.

This same basic structure is used for all the input elements you can create. There will be an input creation function used in the UI of the form _____Input("ID", ...) and the value of that input is accessible in the server code with input$ID. Along with sliders you can create:

and other possibilities that you can see in the Shiny input gallery. Some of these input options are just different ways of getting the same result. Try substituting the sliderInput in your code for numericInput for example. The app should still run the same, but the way that the number of histogram bins are selected will have changed.

Taking input

Starting with a minimal Shiny application:

library(shiny)
ui <- fluidPage(

)
server <- function(input, output) {

}
shinyApp(ui = ui, server = server)

Create four different input types in your UI; a slider (sliderInput()), a selection box (selectInput) containing at least three options, a numeric field (numericInput), and a free text entry field (textInput).

Run your app to check that they are all displaying properly. How would you refer to the values of these inputs in your server code?

Outputs

To create outputs we will need three components. Like the inputs, there will need to be a UI element created to display the output. These output elements use the form _____Output("ID"), such as in the example where the UI element to display the histogram is created with plotOutput("distPlot"). The "ID" tells Shiny where the server code will be creating the output (output$ID), and so again it must be unique among output IDs in your app.

The final part of creating an output is to ensure that the output is being produced in a reactive environment. Remember that only reactive elements can access other reactive elements. The input and output objects Shiny creates are reactive, so without this step our plot would not be able to read the input$bins value from the slider.

Making the output reactive is done by wrapping the histogram plotting code in the renderPlot() function. Each render____() function works with a corresponding output type in the UI. So the renderPlot() function creates an outut that can be used in a plotOutput(). The renderPlot() function takes the code to produce a plot as it’s first argument and it runs that code in a reactive environment. If multiple lines of code are needed to produce the plot, they can be wrapped in {} to run together.

In the example, this overall structure looked like:

output$distPlot <- renderPlot({
  # Plotting code goes here
})

There are different output types you may be interested in creating. The most common ones you are likely to need are:

Other possibilities can be found by searching the Shiny help files with ??shiny::render

Giving feedback

To get comfortable with the idea of creating outputs that respond to user input, we will begin by repeating back the values provided by the inputs in the previous exercise.

Pick one input to work with initially and:

  • Determine what output type is most appropriate and create a place to display the output in the UI with ____Output("inputA").
  • Create the output in the server with output$inputA
  • Save the value of the input into output$inputA. Don’t forget to wrap it in the appropriate render____() function.

Repeat this process for the other three inputs so that your app will now display the value of each of them.

Solution

A minimal example for a slider input:

library(shiny)
ui <- fluidPage(
    sliderInput("slider_number", "A slider value:", min = 1, max = 10, value = 5),
    textOutput("slider_value")
)
server <- function(input, output) {
    output$slider_value <- renderText({ input$slider_number })
}
shinyApp(ui = ui, server = server)

It might seem odd to just be repeating the input values back as outputs, but printing these values out to the screen is a technique you may find useful when solving errors in your app.

More useful outputs

Let’s start to produce outputs that are more interesting than telling us the input values. We will create a plot of GDP per capita and life expectancy from the gapminder data where the user can select the year that is plotted.

The steps you will need to add to your code are:

  • Read in the gapminder data
  • Create an input in the UI that can select the year to plot (try a selectInput())
  • Create an output in the UI that will display the plot
  • Create the output object in the server that will contain the plot
  • Filter the gapminder data by year based on the input value and create a plot
  • Wrap the filtering and plotting code in a renderPlot() function and assign it to the output object

Generic reactives

Sometimes, you will need an intermediate step that is neither an input or output, but is still reactive. This might be because you want to reduce duplication in code, or because you have a time consuming step that you need to run and don’t want to slow your app down by needing to run it multiple times.

A reactive expression can be created by wrapping lines of code in a call to reactive(). To see this in action the following lines in the example app:

# Directly link inputs and outputs
output$distPlot <- renderPlot({
    x    <- faithful[, 2]
    bins <- seq(min(x), max(x), length.out = input$bins + 1)

    hist(x, breaks = bins, col = 'darkgray', border = 'white')
})

could have been rewritten as:

# Create a reactive intermediate
reactive_bins <- reactive({
    bins <- seq(min(x), max(x), length.out = input$bins + 1)
    
    bins
})

# Use the reactive intermediate
output$distPlot <- renderPlot({
    x    <- faithful[, 2]
    
    hist(x, breaks = reactive_bins(), col = 'darkgray', border = 'white')
})

Three points to note in the above example

The dependency structure of this new code is that input$bins is a dependency of reactive bins, which is a dependency of output$distPlot. So when the input slider is moved, the value of input$bins changes, which changes the value of reactive_bins(), which changes the histogram.

When to use intermediates

One common reason you might need to use a reactive intermediate is if you have some data processing steps and are then creating multiple plots from the processed data. Rather than running the processing steps separately for each plot, you might create a reactive intermediate containing the processed data, then produce plots from that intermediate.

Visually, we are trying to turn a situation like this: into something like this:

Because the code inside the call to reactive() is in a reactive environment, it can access input values from the input object and can be called within render____() functions (or other reactive()) functions to have multiple linked intermediate values.

Two things at once

Create a second plot in your app above that shows population by lifeExp for the selected year.

Extract out the shared filtering step from each plot into a single reactive expression using reactive()

Use the intermediate filtered dataset in your plotting code instead. Don’t forget to add () to the reactive variable you have created when you want to access it’s value.

Organising an app

You might be starting to feel like your current app.R file is getting a bit large, even though we have only created a small app. Rather than keeping everything in a single file it is possible to separate the UI and the server code into two separate files. These files must be in the same folder as each other and be named ui.R and server.R.

Single file:

library(shiny)

ui <- fluidPage(
    titlePanel("Old Faithful Geyser Data"),

    sidebarLayout(
        sidebarPanel(
            sliderInput("bins",
                        "Number of bins:",
                        min = 1,
                        max = 50,
                        value = 30)
        ),

        mainPanel(
           plotOutput("distPlot")
        )
    )
)

server <- function(input, output) {

    output$distPlot <- renderPlot({
        x    <- faithful[, 2]
        bins <- seq(min(x), max(x), length.out = input$bins + 1)

        hist(x, breaks = bins, col = 'darkgray', border = 'white')
    })
}

shinyApp(ui = ui, server = server)

Two files

ui.R

library(shiny)

ui <- fluidPage(
    titlePanel("Old Faithful Geyser Data"),

    sidebarLayout(
        sidebarPanel(
            sliderInput("bins",
                        "Number of bins:",
                        min = 1,
                        max = 50,
                        value = 30)
        ),

        mainPanel(
           plotOutput("distPlot")
        )
    )
)

server.R

library(shiny)

server <- function(input, output) {

    output$distPlot <- renderPlot({
        x    <- faithful[, 2]
        bins <- seq(min(x), max(x), length.out = input$bins + 1)

        hist(x, breaks = bins, col = 'darkgray', border = 'white')
    })
}

The ui and server objects must be the final things created in these files, so if you have any additional code (eg. loading datasets) it must be placed at the top of the file.

Key Points

  • Reactive elements update when their inputs change

  • Reactive elements can interact with non-reactive elements, but not the other way around

  • Interactive applications can be broken up into ‘UI’ and ‘server’ components