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
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.
read.csv("https://www.finley-lab.com/files/data/name_list.csv",
namesDF <-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:
- Create a new directory called “ShinyPractice”.
- In the “ShinyPractice” directory, create a blank R script called
app.R
. - Copy the code in “app.R version 1” into
app.R
. - 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, wherefile location
is replaced with the relative path to the directory whereapp.R
is saved.
# app.R version 1
library(shiny)
read.csv("https://www.finley-lab.com/files/data/name_list.csv")
names.df <-
# Define UI
fluidPage(
ui <-
titlePanel("Random Names Analysis"),
sidebarLayout(
sidebarPanel("our inputs will be here"),
mainPanel("our output will appear here")
)
)
# Define server logic
function(input, output) {}
server <-
# 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.
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)
read.csv("https://www.finley-lab.com/files/data/name_list.csv")
names.df <-
# Define UI
fluidPage(
ui <-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
function(input, output) {}
server <-
# 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)
read.csv("https://www.finley-lab.com/files/data/name_list.csv")
names.df <-
# Define UI
fluidPage(
ui <-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
function(input, output) {
server <-
$histogram <- renderPlot({
output
if(input$sexInput != "B"){
subset(names.df, Sex == input$sexInput)
subset.names.df <-else {
} names.df
subset.names.df <-
}
subset.names.df$Age
ages <-
# 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)
read.csv("https://www.finley-lab.com/files/data/name_list.csv")
names.df <-
# Define UI
fluidPage(
ui <-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
function(input, output) {
server <-
$histogram <- renderPlot({
output
if(input$sexInput != "B"){
subset(names.df, Sex == input$sexInput)
subset.names.df <-else {
} names.df
subset.names.df <-
}
subset.names.df$Age
ages <-
# 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.
"https://www.finley-lab.com/files/data/Michigan_State_Park_Campgrounds.csv"
u <- read.csv(u, stringsAsFactors = TRUE)
sites <-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)
read.csv(
sites <-"https://www.finley-lab.com/files/data/Michigan_State_Park_Campgrounds.csv",
stringsAsFactors = TRUE)
fluidPage(
ui <-
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")
)
)
)
function(input, output) {
server <-
$text1 <- renderText({
output
subset(sites,
sites1 <->= input$rangeNum[1] &
TOTAL_SITE TOTAL_SITE <= input$rangeNum[2] &
Camp_type == input$type &
if(input$ada){ ADA_SITES > 0 } else {ADA_SITES >= 0})
if(nrow(sites1) > 0){
"<ul>"
outStr <-for(site in sites1$FACILITY){
paste0(outStr,"<li>",site,"</li>")
outStr <-
} paste0(outStr,"</ul>")
outStr <-else {
} ""
outStr <-
}
paste("<p>There are",
nrow(sites1),
"campgrounds that match your search:</p>",
outStr)
})
$plot1 <- renderPlot({
output subset(sites,
sites1 <->= input$rangeNum[1] &
TOTAL_SITE TOTAL_SITE <= input$rangeNum[2] &
Camp_type == input$type &
if(input$ada){ ADA_SITES > 0 } else {ADA_SITES >= 0})
map_data("state", region = "michigan")
miMap <- ggplot() +
plt <- geom_polygon(data=miMap, aes(x=long,y=lat,group=group),
colour="black", fill="gray") +
coord_fixed(ratio = 1)
if(nrow(sites1) > 0){
plt + geom_point(data = sites1,aes(x=Long,y=Lat), colour="red")
plt <-
}
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.
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 aggplot2
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.
subset(sites,
sites1 <->= input$rangeNum[1] &
TOTAL_SITE 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)
read.csv(
sites <-"https://www.finley-lab.com/files/data/Michigan_State_Park_Campgrounds.csv",
stringsAsFactors = TRUE)
# Define UI for application that draws a histogram
fluidPage(
ui <-
# 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")
)
)
)
function(input, output) {
server <- read.csv("Michigan_State_Park_Campgrounds.csv")
sites <-
$text1 <- renderText({
output# create a subset of campsites based on inputs
subset(sites,
sites1 <->= input$rangeNum[1] &
TOTAL_SITE 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
"<ul>"
outStr <-for(site in sites1$FACILITY){
paste0(outStr,"<li>",site,"</li>")
outStr <-
} paste0(outStr,"</ul>")
outStr <-
#
paste("<p>There are",
nrow(sites1),
"campgrounds that match your search:</p>",
outStr)
})
$plot1 <- renderLeaflet({
output# create a subset of campsites based on inputs
subset(sites,
sites1 <->= input$rangeNum[1] &
TOTAL_SITE 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)))},
$FACILITY,sites1$TOTAL_SITE, SIMPLIFY = F)
sites1
)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
$plot1 <- renderLeaflet({
output# create a subset of campsites based on inputs
subset(sites,
sites1 <->= input$rangeNum[1] &
TOTAL_SITE 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)))},
$FACILITY, sites1$TOTAL_SITE
sites1
)
)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.
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
andui.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 singleapp.R
file. See https://shiny.rstudio.com/articles/app-formats.html for more information.↩︎Every reactive output function’s name in Shiny is of the form
render*
.↩︎