Chapter 9 Shiny: Interactive Web Apps in R

Shiny is a framework that turns R code and figures into interactive web applications. Let’s start out by looking at a built-in example Shiny app.

library(shiny)
runExample("01_hello")

In the bottom panel of the resulting Shiny app (Figure 9.1), we can see the R script that is essential to running any Shiny app: app.R. Take a minute to explore how the app works and how the script code is structured. The first part of the script (ui <-) defines the app’s user interface (UI) using directives that partition the resulting web page and placement of input and output elements. The second part of the script defines a function called server with arguments input and output. This function provides the logic required to run the app (in this case, to draw the histogram of the Old Faithful Geyser Data). Third, ui and server are passed to a function called shinyApp which creates the application.54

Hello Shiny example app

FIGURE 9.1: Hello Shiny example app

9.1 Running a Simple Shiny App

Shiny apps allow users to interact with a data set. Let’s say we have access to a dummy data set name_list.csv. Begin by taking a quick look at what this CSV file contains.

namesDF <- read.csv("https://www.finley-lab.com/files/data/name_list.csv", 
                    stringsAsFactors = FALSE)
str(namesDF)
## 'data.frame':    200 obs. of  4 variables:
##  $ First: chr  "Sarah" "Boris" "Jessica" "Diane" ...
##  $ Last : chr  "Poole" "Dowd" "Wilkins" "Murray" ...
##  $ Age  : int  15 52 12 58 56 4 14 0 28 27 ...
##  $ Sex  : chr  "F" "M" "F" "F" ...

Given the namesDF data, our goal over the next few sections is to develop a Shiny application that allows users to explore age distributions for males and females. We begin by defining a very basic Shiny application script upon which we can build the desired functionality. The script file that defines this basic application is called app.R and is provided in the code block below called “version 1”. Follow the subsequent steps to create and run this Shiny app:

  1. Create a new directory called “ShinyPractice”.
  2. In the “ShinyPractice” directory, create a blank R script called app.R.
  3. Copy the code in “app.R version 1” into app.R.
  4. Run the Shiny app from RStudio. There are two ways to do this: 1) use the RStudio button in the top right of the script window or; 2) type the function runApp('file location') in the RStudio console, where file location is replaced with the relative path to the directory where app.R is saved.
# app.R version 1

library(shiny)

names.df <- read.csv("https://www.finley-lab.com/files/data/name_list.csv")

# Define UI
ui <- fluidPage(
  
  titlePanel("Random Names Analysis"),
  
  sidebarLayout(
    
    sidebarPanel("our inputs will be here"),
    
    mainPanel("our output will appear here")
  )
)

# Define server logic
server <- function(input, output) {}

# Create Shiny app
shinyApp(ui = ui, server = server)

If everything compiles correctly, you should get a window that looks like Figure 9.2. There are a few things to notice. First, we read in the CSV file at the beginning of the app.R script so that these data can be accessed in subsquent development steps (although we don’t use it yet in this initial application). Second, our Shiny app is not interactive yet because there is no way for a user to input any data—the page is view-only. Third, the function function(input,output) {} in app.R does not include any code, reflecting that we are not yet using any user inputs or creating any outputs.

Your first Shiny app

FIGURE 9.2: Your first Shiny app

9.2 Adding User Input

Shiny package widgets are used to collect user provided inputs (visit the Shiny Widget Gallery to get a sense of the different kinds of input dialogues). In the subsequent development we’ll use slider bars, dropdown menus, and check boxes. All widget functions, at minimum, include an inputId and label argument. The character string assigned to the inputId argument will be the variable name used to access the value(s) selected or provided by the user. This variable name is used in the application development and not seen by the user. The character string assigned to the label argument appears in the UI above the given input widget and serves to provide a user-friendly description of the desired input.

Building on the “version 1” code, we add a dropdown menu to the script’s UI portion using the selectInput function to the “version 2” code below. Update app.R and rerun the application. What is our inputId for the dropdown menu? What is our label? Can you see where the label is displayed in the app when you rerun it?

Look at the structure of the choices argument in selectInput. It’s a vector of options in the form "name" = "value" where the name is what the user sees, and the value is what is returned to the server logic function of the app.R file.

# app.R version 2

library(shiny)

names.df <- read.csv("https://www.finley-lab.com/files/data/name_list.csv")

# Define UI
ui <- fluidPage(
  titlePanel("Random Names Age Analysis"),
  sidebarLayout(
    sidebarPanel(
      # Dropdown selection for Male/Female
      selectInput(inputId = "sexInput", label = "Sex:",
                  choices = c("Female" = "F", 
                              "Male" = "M", 
                              "Both" = "B"))
    ),
    mainPanel("our output will appear here")
  )
)

# Define server logic
server <- function(input, output) {}

# Create Shiny app
shinyApp(ui = ui, server = server)

9.3 Adding Output

Now that we’ve included the user input dialog, let’s make the application truly interactive by changing the output depending on user input. This is accomplished by modifying the server logic portion of the script. Our goal is to plot an age distribution histogram in the main panel given the sex selected by the user.

9.3.1 Interactive Server Logic

Server logic is defined by two arguments: input and output. These objects are both list-like, so they can contain multiple other objects. We already know from the user input part of the app.R script that we have an input component called sexInput, which can be accessed in the reactive portion of the server logic by calling input$sexInput (notice the use of the $ to access the input value associated with sexInput). In “version 3” of the application, we use the information held in input$sexInput to subset names.df then create a histogram of names.df$Age. This histogram graphic is included as an element in the output object and ultimately made available in the UI portion of the script by referencing its name histogram.

Reactive portions of our app.R script’s server logic are inside the server function. We create reactive outputs using Shiny’s functions like renderPlot.55 Obviously, renderPlot renders the contents of the function into a plot object; this is then stored in the output list wherever we assign it (in this case, output$histogram). Notice the contents of the renderPlot function are contained not only by regular parentheses, but also by curly brackets (just one more piece of syntax to keep track of).

# app.R version 3

library(shiny)

names.df <- read.csv("https://www.finley-lab.com/files/data/name_list.csv")

# Define UI
ui <- fluidPage(
  titlePanel("Random Names Age Analysis"),
  sidebarLayout(
    sidebarPanel(
      # Dropdown selection for Male/Female
      selectInput(inputId = "sexInput", label = "Sex:",
                  choices = c("Female" = "F", 
                              "Male" = "M", 
                              "Both" = "B"))
    ),
    mainPanel("our output will appear here")
  )
)

# Define server logic
server <- function(input, output) {

  output$histogram <- renderPlot({
    
    if(input$sexInput != "B"){
      subset.names.df <- subset(names.df, Sex == input$sexInput)
    } else {
      subset.names.df <- names.df
    }
    
    ages <- subset.names.df$Age
    
    # draw the histogram with the specified 20 bins
    hist(ages, col = 'darkgray', border = 'white')
  })

}

# Create Shiny app
shinyApp(ui = ui, server = server)

Update your app.R server logic function to match the code above. Rerun the application. Note the appearance of the app doesn’t change because we have not updated the UI portion with the resulting histogram.

9.3.2 Interactive User Interface

Now we update the UI part of app.R to make the app interactive. In the “version 4” code below, the plotOutput("histogram") function in ui accesses the histogram component of the output list and plots it in the main panel. Copy the code below and rerun the application. You have now created your first Shiny app!

# app.R version 4

library(shiny)

names.df <- read.csv("https://www.finley-lab.com/files/data/name_list.csv")

# Define UI
ui <- fluidPage(
  titlePanel("Random Names Age Analysis"),
  sidebarLayout(
    sidebarPanel(
      # Dropdown selection for Male/Female
      selectInput(inputId = "sexInput", label = "Sex:",
                  choices = c("Female" = "F", 
                              "Male" = "M", 
                              "Both" = "B"))
    ),
    mainPanel(plotOutput("histogram"))
  )
)

# Define server logic
server <- function(input, output) {

  output$histogram <- renderPlot({

    if(input$sexInput != "B"){
      subset.names.df <- subset(names.df, Sex == input$sexInput)
    } else {
      subset.names.df <- names.df
    }
    
    ages <- subset.names.df$Age
    
    # draw the histogram with the specified 20 bins
    hist(ages, col = 'darkgray', border = 'white')
  })

}

# Create Shiny app
shinyApp(ui = ui, server = server)

9.4 More Advanced Shiny App: Michigan Campgrounds

The Michigan Department of Natural Resources (DNR) has made substantial investments in open-sourcing its data via the Michigan DNR’s open data initiative. We’ll use some of these data to motivate our next Shiny application. Specifically we’ll work with the DNR State Park Campgrounds data available here as a CSV file.

u <- "https://www.finley-lab.com/files/data/Michigan_State_Park_Campgrounds.csv"
sites <- read.csv(u, stringsAsFactors = TRUE)
str(sites)
## 'data.frame':    151 obs. of  19 variables:
##  $ X          : num  -84.4 -84.4 -83.8 -83.8 -83.8 ...
##  $ Y          : num  42.9 42.9 42.5 42.5 42.5 ...
##  $ OBJECTID   : int  10330 10331 5986 5987 5988 5989 5990 5991 5992 5993 ...
##  $ Type       : Factor w/ 1 level "Campground": 1 1 1 1 1 1 1 1 1 1 ...
##  $ Detail_Type: Factor w/ 2 levels "","State Park": 2 2 2 2 2 2 2 2 2 2 ...
##  $ DISTRICT   : Factor w/ 8 levels "Baraga","Bay City",..: 8 8 8 8 8 8 6 6 6 4 ...
##  $ COUNTY     : Factor w/ 50 levels "Alcona","Baraga",..: 10 10 27 27 27 27 47 47 47 7 ...
##  $ FACILITY   : Factor w/ 76 levels "Algonac State Park",..: 58 58 7 7 6 7 1 1 1 2 ...
##  $ Camp_type  : Factor w/ 7 levels "Equestrian","Group",..: 3 5 3 5 6 6 3 3 5 3 ...
##  $ TOTAL_SITE : int  203 1 144 10 25 25 219 75 2 297 ...
##  $ ADA_SITES  : int  0 0 0 0 0 0 0 3 0 13 ...
##  $ name       : Factor w/ 114 levels "Algonac State Park  ",..: 88 88 11 9 8 10 2 3 1 4 ...
##  $ Ownership  : Factor w/ 1 level "State": 1 1 1 1 1 1 1 1 1 1 ...
##  $ Management : Factor w/ 1 level "DNR": 1 1 1 1 1 1 1 1 1 1 ...
##  $ Surface    : Factor w/ 1 level "Dirt": 1 1 1 1 1 1 1 1 1 1 ...
##  $ Use_       : Factor w/ 2 levels "","Recreation": 2 2 2 2 2 2 2 2 2 2 ...
##  $ Condition  : Factor w/ 2 levels "Good","Unknown": 2 2 2 2 2 2 2 2 2 2 ...
##  $ Lat        : num  42.9 42.9 42.5 42.5 42.5 ...
##  $ Long       : num  -84.4 -84.4 -83.8 -83.8 -83.8 ...

We see that Michigan has 151 state park campgrounds, and our data frame contains 19 variables. Let’s create a Shiny app UI in which the user selects desired campground specifications, and the app displays the list of resulting campgrounds and their location on a map. The complete Shiny app.R is shown below. Create a new directory called CampsitesMI, and copy and paste the following code into a script file called app.R. Start out by running the application to see how it works. Examine the example code and how it relates to the application that you are running. The following sections detail each of the app’s components.

# app.R
# CampsitesMI

library(shiny)
library(maps)
library(ggplot2)

sites <- read.csv(
 "https://www.finley-lab.com/files/data/Michigan_State_Park_Campgrounds.csv", 
  stringsAsFactors = TRUE)

ui <- fluidPage(

  titlePanel("Michigan Campsite Search"),

  sidebarLayout(
    sidebarPanel(
      sliderInput("rangeNum",
                  label = "Number of campsites:",
                  min = 0,
                  max = 420,
                  value = c(0,420),
                  step=20
      ),

      selectInput("type",
                  label = "Type of campsites:",
                  levels(sites$Camp_type)
      ),

      checkboxInput("ada",
                    label = "ADA Sites Available:",
                    FALSE)
    ),

    mainPanel(
      plotOutput("plot1"),
      br(),
      htmlOutput("text1")
    )
  )
)

server <- function(input, output) {

  output$text1 <- renderText({

    sites1 <- subset(sites,
                     TOTAL_SITE >= input$rangeNum[1] &
                       TOTAL_SITE <= input$rangeNum[2] &
                       Camp_type == input$type &
                       if(input$ada){ ADA_SITES > 0 } else {ADA_SITES >= 0})

    if(nrow(sites1) > 0){
      outStr <- "<ul>"
      for(site in sites1$FACILITY){
        outStr <- paste0(outStr,"<li>",site,"</li>")
      }
      outStr <- paste0(outStr,"</ul>")
    } else {
      outStr <- ""
    }

    paste("<p>There are",
          nrow(sites1),
          "campgrounds that match your search:</p>",
          outStr)

  })

  output$plot1 <- renderPlot({
    sites1 <- subset(sites,
                     TOTAL_SITE >= input$rangeNum[1] &
                       TOTAL_SITE <= input$rangeNum[2] &
                       Camp_type == input$type &
                       if(input$ada){ ADA_SITES > 0 } else {ADA_SITES >= 0})

    miMap <- map_data("state", region = "michigan")
    plt <- ggplot() +
      geom_polygon(data=miMap, aes(x=long,y=lat,group=group), 
           colour="black", fill="gray") +
      coord_fixed(ratio = 1)

    if(nrow(sites1) > 0){
      plt <- plt + geom_point(data = sites1,aes(x=Long,y=Lat), colour="red")
    }

    plot(plt)

  })

}

# Create Shiny app ----
shinyApp(ui = ui, server = server)

9.4.1 Michigan Campgrounds UI

First, let’s look at the structure of the page. Similar to our first application, we again use a fluidPage layout with title panel and sidebar. The sidebar contains a sidebar panel and a main panel. Our sidebar panel has three user input widgets:

  • sliderInput: Allows user to specify a range of campsites desired in their campground. Since the maximum number of campsites in any Michigan state park campground is 411, 420 was chosen as the maximum.
  • selectInput: Allows user to select what type of campsites they want. To get the entire list of camp types, we used the data frame, sites, loaded at the beginning of the script.
  • checkboxInput: Allows the user to see only campgrounds with ADA sites available.
TABLE 9.1: Interactive elements
Element ID Description Function to Create
input\(rangeNum </td> <td style="text-align:left;"> desired range of campsite quantity </td> <td style="text-align:left;"> sliderInput </td> </tr> <tr> <td style="text-align:left;"> input\)type desired campsite type selectInput
input\(ada </td> <td style="text-align:left;"> desired ADA site availability </td> <td style="text-align:left;"> checkboxInput </td> </tr> <tr> <td style="text-align:left;"> output\)plot1 map with campground markers renderPlot
output$text1 HTML-formatted list of campgrounds renderText

Table 9.1 provides a list of the various input and output elements. Take your time and track how the app defines then uses each input and output.

9.4.2 Michigan Campgrounds Server Logic

In creating our server variable, we have two functions that fill in our output elements:

  • renderText: Creates a character string of HTML code to print the bulleted list of available sites.
  • renderPlot: Creates a ggplot2 map of Michigan with campground markers.

Note that both of these functions contain identical subsetting of the sites data frame into the smaller sites1 data frame (see below). As you can see from the code, we use the three inputs from the application to subset the data: rangeNum from the slider widget, type from the dropdown menu, and ada from the checkbox.

This repeated code can be avoided using Shiny’s reactive expressions. These expressions will update in value whenever the widget input values change. See here for more information. The use of reactive expressions is beyond the scope of this chapter, but it is an important concept to be familiar with if you plan to regularly create Shiny applications.

sites1 <- subset(sites, 
                  TOTAL_SITE >= input$rangeNum[1] &  
                  TOTAL_SITE <= input$rangeNum[2] &  
                  Camp_type == input$type &   
                  if(input$ada){ ADA_SITES > 0 } else {ADA_SITES >= 0}
                )

9.5 Adding Leaflet to Shiny

Leaflet can be easily incorporated in Shiny apps (See Chapter 8 of this text to learn more about spatial data). In the Michigan Campgrounds example code, we used plotOutput and renderPlot to put a plot widget in our Shiny app. Similarly, in this code, we will use leafletOutput and renderLeaflet to add Leaflet widgets to our app. Create a new directory called CampsitesMI_Leaflet and copy the following code into a new app.R script file.

# app.R
# CampsitesMI - Leaflet

library(shiny)
library(leaflet)
library(htmltools)

sites <- read.csv(
 "https://www.finley-lab.com/files/data/Michigan_State_Park_Campgrounds.csv", 
  stringsAsFactors = TRUE)

# Define UI for application that draws a histogram
ui <- fluidPage(

  # Application title
  titlePanel("Michigan Campsite Search"),

  # Sidebar
  sidebarLayout(
    sidebarPanel(
      # slider input for number of campsites
      sliderInput("rangeNum",
                  label = "Number of campsites:",
                  min = 0,
                  max = 420,
                  value = c(0,420),
                  step=20
      ),

      selectInput("type",
                  label = "Type of campsites:",
                  levels(sites$Camp_type)
      ),

      checkboxInput("ada",
                    label = "ADA Sites Available:",
                    FALSE)

    ),

    mainPanel(
      # Show the map of campgrounds
      leafletOutput("plot1"),
      br(),
      # Show the text list of campgrounds
      htmlOutput("text1")
    )
  )
)


server <- function(input, output) {
  sites <- read.csv("Michigan_State_Park_Campgrounds.csv")

  output$text1 <- renderText({
    # create a subset of campsites based on inputs
    sites1 <- subset(sites,
                     TOTAL_SITE >= input$rangeNum[1] &
                       TOTAL_SITE <= input$rangeNum[2] &
                       Camp_type == input$type &
                       if(input$ada){ ADA_SITES > 0 } else {ADA_SITES >= 0})
    # create an HTML-formatted character string to be output
    outStr <- "<ul>"
    for(site in sites1$FACILITY){
      outStr <- paste0(outStr,"<li>",site,"</li>")
    }
    outStr <- paste0(outStr,"</ul>")

    #
    paste("<p>There are",
          nrow(sites1),
          "campgrounds that match your search:</p>",
          outStr)

  })

  output$plot1 <- renderLeaflet({
    # create a subset of campsites based on inputs
    sites1 <- subset(sites,
                     TOTAL_SITE >= input$rangeNum[1] &
                       TOTAL_SITE <= input$rangeNum[2] &
                       Camp_type == input$type &
                       if(input$ada){ ADA_SITES > 0 } else {ADA_SITES >= 0})

    if(nrow(sites1) > 0){
      leaflet(sites1) %>% addTiles()  %>%
        addCircleMarkers(lng = ~Long, lat = ~Lat,
                         radius = 5,
                         color = "red",
                         label = mapply(function(x,y) {
                           HTML(sprintf('<em>%s</em><br>%s site(s)',
                                        htmlEscape(x),
                                        htmlEscape(y)))},
                           sites1$FACILITY,sites1$TOTAL_SITE, SIMPLIFY = F)
        )
    } else {
      leaflet() %>% addTiles() %>%
        setView( -84.5555, 42.7325,   zoom = 7)
    }

  })

}

# Create Shiny app ----
shinyApp(ui = ui, server = server)

Run the Shiny app in the CampsitesMI_Leaflet directory. What are the differences between the plot widget in this app and the plot widget in the previous app (using ggplot2)?

The code inside renderLeaflet is displayed again below. As a reminder from our previous use of Leaflet, we can use the magrittr package’s pipe operator, %>%, to add properties to our Leaflet plot. In the addCircleMarkers function, you can see that we used the mapply function to apply HTML code to each of the markers. The HTML code simply prints the site name and number of sites within each marker label. The else statement serves the purpsose that if our subset, sites1, is empty, we render a map centered on Lansing, MI.

}

# renderLeaflet function from app.R

output$plot1 <- renderLeaflet({
    # create a subset of campsites based on inputs
    sites1 <- subset(sites, 
                    TOTAL_SITE >= input$rangeNum[1] & 
                      TOTAL_SITE <= input$rangeNum[2] & 
                      Camp_type == input$type & 
                      if(input$ada){ ADA_SITES > 0 } else {ADA_SITES >= 0})
    
    if(nrow(sites1) > 0){
      leaflet(sites1) %>% addTiles()  %>% 
        addCircleMarkers(lng = ~Long, lat = ~Lat, 
                         radius = 5,
                         color = "red",
                         label = mapply(function(x,y) {
                           HTML(sprintf('<em>%s</em><br>%s site(s)', 
                                        htmlEscape(x), 
                                        htmlEscape(y)))}, 
                           sites1$FACILITY, sites1$TOTAL_SITE
                           )
                         )
    } else {
        leaflet() %>% addTiles() %>%
        setView( -84.5555, 42.7325,   zoom = 7)
    }
    
  })

9.6 Why use Shiny?

In this chapter, we learned what a Shiny app is, what it’s components are, how to run the app, and how to incorporate Leaflet plots. We can also host these apps online on our own server or the shinyapps.io server, which can be accessed directly from RStudio. Hosting our apps on a server allows anyone with internet access to interact with our widgets.

The shinyapps.io server allows free hosting of apps within the monthly limits of 5 applications and 25 active hours. Paid plans are also available. A user guide to deploying your application on shinyapps.io is available here. Set up a free account and impress your friends by sending them links to your Shiny apps! In addition to hosting your applications, here are a few more things to discover with Shiny:

  • Dive deeper into development by working through the RStudio Shiny tutorial series.
  • Incorporate CSS stylesheets to make your apps fancier.
  • Explore the single-file Shiny app structure.

9.7 Exercises

Exercise Shiny Learning objectives: practice updating ggplot2 plot aesthetics; modify Shiny HTML output; add an interactive Shiny element.


  1. Since 2014, Shiny has supported single-file applications (one file called app.R that contains UI and server components), but in other resources, you may see two separate source files, server.R and ui.R, that correspond to those two components. We will use the updated one-file system here, but keep in mind that older resources you find on the internet using the Shiny package may employ the two-file approach. Ultimately, the code inside these files is almost identical to that within the single app.R file. See https://shiny.rstudio.com/articles/app-formats.html for more information.↩︎

  2. Every reactive output function’s name in Shiny is of the form render*.↩︎