This lesson presents an introduction to creating interactive web applications using the R Shiny package. It covers:
This lesson makes use of several publicly available datasets that have been customized for teaching purposes, including the Portals teaching database.
Shiny is a web application framework for R that allows you to create interactive web apps without requiring knowledge of HTML, CSS, or JavaScript. These web apps can be used for exploratory data analysis and visualization, to facilitate remote collaboration, share results, and much more.
The shiny
package includes some built-in examples to demonstrate some of its basic features. When applications are running, they are displayed in a separate browser window or the RStudio Viewer pane.
After installing the shiny package, load the shiny package and run one of the built-in examples:
# install.packages("shiny")
library(shiny)
runExample("01_hello")
The example will open in a new browser window (you may need to prevent your broswer from blocking pop-out windows in order to view the app). Notice back in RStudio that a stop sign that appears in the Console window while your app is running. This is because the current R session is busy running your application. Closing the app window does not stop the app from using your R session. Make sure to end the app when you are finished by clicking the stop sign in the header of the Console window. The Console window prompt >
should return.
Depending on the purpose and computing requirements of any Shiny app, you may set it up to run R code on your computer, a remote server, or in the cloud. However all Shiny apps consists of the same two main components:
These components can be defined either in two separate files called ui.R
and server.R
saved in the same folder, or they can be defined as objects called ui
and server
within one file called app.R
.
The appearance of the web page (the UI) is controlled by the computer running an R session. When you are using standalone RStudio, that session is running on your laptop or desktop. Users can manipulate elements within the UI, which triggers R code to run, in turn updating UI objects.
When the shiny
package is installed and loaded, RStudio will identify these file structures and put a green arrow with a Run App button when you open a file in the app. Note that the file names must be exactly as specified.
Create a file in your %sandbox% directory (the same location where you have your data folder) called
app.R
. In this file, define objectsui
andserver
with the assignment operator<-
and then pass them to the functionshinyApp()
. These are the basic components of a shiny app.
ui <- navbarPage(title = "Hello, World!")
server <- function(input, output){}
shinyApp(ui = ui, server = server)
Notice the green Run App button appear when the file is saved. This button also allows you to control whether the app runs in a browser window, in the RStudio Viewer pane, or in an external RStudio window.
Because the Shiny app is going to be using your local R session to run, it will be able to recognize anything that is loaded into your working environment. Shiny apps can also be designed to interact with remote data or shared databases.
Modify the
app.R
file to read in 3 csvs from the Portals database at the beginning of the file. Because the app is saved in your %sandbox% directory, you only need to specify the relative path location.
# Read in data
plots <- read.csv("data/plots.csv", stringsAsFactors = FALSE)
species <- read.csv("data/species.csv", stringsAsFactors = FALSE)
surveys <- read.csv("data/surveys.csv", na.strings = "", stringsAsFactors = FALSE)
# User interface
ui <- navbarPage(title = "Hello, World!")
# Server
server <- function(input, output){}
# Run app
shinyApp(ui = ui, server = server)
The user interface and the server interact with each other through input and output objects. The information in the server is the recipe for how to construct output objects to display in the UI, and the user’s interaction with input objects alters output objects based on the code in the server instructions. The instructions for creating input objects are in the UI.
Having your app function as you intend requires careful attention to how your input and output objects relate to each other, i.e. knowing what actions will initiate what sections of code to run at what time.
The diagram above depicts how input and output objects are referred to within the UI and server objects:
selectInput()
or radioButtons()
.plotOutput()
or textOutput()
.Input objects collect information from the user and save it into a list. Input values can change when a user changes the input. These inputs can be many different things: single values, text, vectors, dates, or even files uploaded by the user.
The first two arguments for all input widgets are:
inputId =
which is for giving the input object a name to refer to in the server, andlabel =
which is for text to display to the user.Other arguments depend on the type of input widget. Input objects are stored in a list and are referred to in the server with the syntax input$inputID
.
See a gallery of input widgets with sample code here
We will now customize the UI by adding an input object to allow users to select one of the species ID in the portals data set. Use the
selectInput()
function to create an input object calledpick_species
. Use thechoices =
argument to define a vector with the unique values in the species id column. Separate functions in the UI with commas. Put this function in atabPanel
called Portals. We will learn about design and layout in a subsequent section.
# User interface
ui <- navbarPage(title = "Hello Shiny!",
tabPanel("Portals",
selectInput("pick_species", label = "Pick a species",
choices = unique(species$species_id))))
Input objects are reactive which means that an update to this value by a user will notify objects in the server that its value has been changed.
Other notes about input objects
list("Male" = "M", "Female" = "F")
.Output objects are created through the combination of pairs of render*()
and *Output()
functions. The server object defines a list of output objects using render functions with syntax such as:
output$myplot <- renderPlot({})
output$mydata <- renderTable({})
output$mymessage <- renderText({})
Display the species id as text under the input widget using
textOutput
in the UI andrenderText
in the server object.
# User interface
ui <- navbarPage(title = "Hello Shiny!",
tabPanel("Portals",
selectInput("pick_species", label = "Pick a species",
choices = unique(species$species_id)),
textOutput(outputId = "species_id")))
# Server
server <- function(input, output){
output$species_id <- renderText({input$pick_species})
}
Render functions tell Shiny how to build an output object to display in the user interface. Output objects can be data frames, plots, images, text, or most anything you can create with R code to be visualized.
The outputs of render functions are called observers because they observe all upstream reactive values for changes. The code inside the body of the render function will run whenever a reactive value inside the code changes, such as when an input object’s value is changed in the UI. The input object notifies its observers that it has changed, which causes the output objects to re-render and update the display.
Use outputId names in quotes to refer to output objects within *Output()
functions. Other arguments to *Output()
functions can control their size in the UI as well as add advanced interactivity such as selecting observations to view data by clicking on a plot.
Here are some common pairs of render and output functions:
render function | output function | displays |
---|---|---|
renderPlot() | plotOutput() | plots |
renderPrint() | verbatimTextOutput() | text |
renderText() | textOutput() | text |
renderTable() | tableOutput() | static table |
renderDataTable() | dataTableOutput() | interactive table |
Note that it is also possible to render reactive input objects using the renderUI()
and uiOutput()
functions for situations where you want the type or parameters of an input widget to change based on another input. For an exmaple, see “Creating controls on the fly” here.
We will now use the input object pick_species
within a renderPlot()
function.
Filter the survey data based on the selected species. Use the
renderPlot()
function to define a barplot that shows how many surveys recorded that selected species by year. Use the correspondingplotOutput()
function in the UI to display the plot in the app. Make sure to separate UI elements with commas.
# User interface
ui <- navbarPage(title = "Hello Shiny!",
tabPanel("Portals",
selectInput("pick_species", label = "Pick a species",
choices = unique(species$species_id)),
textOutput(outputId = "species_id"),
plotOutput("species_plot")
))
# Server
server <- function(input, output){
output$species_id <- renderText({input$pick_species})
output$species_plot <- renderPlot({
surveys_subset <- subset(surveys, surveys$species_id == input$pick_species)
barplot(table(surveys_subset$year))
})
}
Challenge: Add a title to the plot that includes the full species name.
Output objects can react to multiple input objects chosen by the user.
Exercise: Add an additional input widget that allows the user to filter the survey observations based on a range of consecutive months.
# add to ui
sliderInput("slider_months", label = "Month range",
min = 1, max = 12, value = c(1,12))
# update renderPlot function
output$species_plot <- renderPlot({
surveys_subset <- subset(surveys, surveys$species_id == input$pick_species &
surveys$month %in%
input$slider_months[1]:input$slider_months[2])
barplot(table(surveys_subset$year))
})
Challenge: Use the “date range” input widget to specify a range of specific dates instead of just months.
Within the user interface, arrange where elements appear by using a page layout. You can organize elements using pre-defined high level layouts such as sidebarLayout()
, splitLayout()
, or verticalLayout()
, or using the more general fluidRow()
(described in depth here)to organize rows of elements within a grid. Elements can be layered on top of each other using tabsetPanel()
, navlistPanel()
, or navbarPage()
.
The diagram above depicts the nestedness of UI elements in the sidebar layout. The red boxes represent input objects and the blue boxes represent output objects. Each object is located within one or more nested panels, which are nested within a layout. Notice that tab panels with the sidebar layout’s main panel are nested within the tabset panel. Objects and panels that are at the same level of hierarchy need to be separated by commas.
Mistakes in usage of commas and parentheses between UI elements is one of the first things to look for when debugging a shiny app!
The fluidPage()
layout design consists of rows which contain columns of elements. To use it, first define the width of an element relative to a 12-unit grid within each column using the function fluidRow()
and listing columns in units of 12. The argument offset =
can be used to add extra spacing. For example:
fluidPage(
fluidRow(
column(4, "4"),
column(4, offset = 4, "4 offset 4")
),
fluidRow(
column(3, offset = 3, "3 offset 3"),
column(3, offset = 3, "3 offset 3")
))
Organize the elements of the Portals tab using a sidebar layout with the input widgets in the sidebar and the plot in the main panel.
# User interface
ui <- navbarPage(title = "Hello Shiny!",
tabPanel("Portals",
sidebarLayout(
sidebarPanel(
selectInput("pick_species", label = "Pick a species",
choices = unique(species$species_id)),
sliderInput("slider_months", label = "Month range",
min = 1, max = 12, value = c(1,12)),
textOutput(outputId = "species_id")
),
mainPanel(
plotOutput("species_plot")
)
)
)
)
Exercise: Make a second tab panel called “Data” to show a data frame with the surveys data used in the plot that shows in the first panel.
# add inside navbarPage function
tabPanel(title = "Data", dataTableOutput("surveys_subset"))
# within server function
output$surveys_subset <- renderDataTable({
surveys_subset <- subset(surveys, surveys$species_id == input$pick_species &
surveys$month %in%
input$slider_months[1]:input$slider_months[2])
surveys_subset
})
Notice the many features of the data table output. There are many options that can be controlled within the render function such as pagination and default length. See here for examples and how to extend this functionality using JavaScript.
Along with widgets and output objects, you can add headers, text, images, links, and other html objects to the user interface. There are shiny function equivalents for many common html tags such as h1()
through h6()
for headers. You can use the console to see that the return from these functions produce HTML code.
shiny::h5("This is a level 5 header")
shiny::a(href="www.sesync.org", "This syntax renders <aside></aside> a link")
This syntax renders <aside></aside> a link
position = "right"
in the sidebarLayout()
function if you prefer to have the side panel appear on the right.include*
function (example).img(src="<file name>")
Input objects that are used in multiple render functions to create different output objects can be created independently as reactive objects. This value is then cached to reduce computation required, since only the code to create this object is re-run when input values are updated. For example, in order to display both the plot and the data used in the plot, we had to duplicate portions of code in the renderPlot()
and renderDataTable()
functions.
The diagram above shows the relationship between input and output objects with (B) and without (A) the use of an intermediary reactive object. The surveys_subset reactive object in (B) becomes cached in the app’s memory so it does not need to be computed independently in both the plot and data output objects.
Use the function reactive()
to create reactive objects and use them with function syntax, i.e. with ()
. Reactive objects are not output objects so do not use output$
in front of their name either.
The diagram above depicts the new relationship between input objects and reactive functions to produce reactive objects, which are then used in render functions.
Make the filtered data set a reactive object called
surveys_subset
to use to render both the plot and the data table, instead of repeating the code to create the filtered data set.
# Server
server <- function(input, output){
output$species_id <- renderText({input$pick_species})
surveys_subset <- reactive({
surveys_subset <- subset(surveys, surveys$species_id == input$pick_species)
return(surveys_subset)
})
output$species_plot <- renderPlot({
species_name <- paste(species[species$species_id==input$pick_species,"genus"],
species[species$species_id==input$pick_species,"species"])
barplot(table(surveys_subset()$year), main = paste("Observations of", species_name, "per year"))
})
output$surveys_subset <- renderDataTable({
surveys_subset()
})
}
It is possible to allow users to upload and download files from a Shiny app, such as a csv file of the currently visible data. Objects to download are output objects created in the server using the function downloadHandler()
which is analogous to the render functions. That object is made available using a downloadButton()
or downloadLink()
function in the ui. The downloadHandler()
function requires two arguments:
Uploading files is possible with the input function fileInput()
to create an input object. This object is a data frame that contains a column datapath
which can be used to locate the user’s upload file locally within the app. See the documentation and example for more information.
Add a button to the sidebar in the Plot panel which allows users to download a csv of the data used to generate the plot and data table.
# in UI after "selectInput"
downloadButton("download_data", label = "Download")
# in server function
output$download_data <- downloadHandler(
filename = "portals_subset.csv",
content = function(file) {
write.csv(surveys_subset(), file)
}
)
You can enhance and extend the functionality and sophistication of Shiny apps using existing tools and platforms. Javascript visualizations can be used in RShiny with a framework called htmlwidgets, which lets you access powerful features of tools like Leaflet, plot.ly,and d3 within R. Since these frameworks are bridges to, or wrappers, for the original libraries and packages that may have been written in another programming language, deploying them requires becoming familiar with the logic and structure of the output objects being created. You can also incorporate Javascript code in apps. Read more about how to create bindings to Javascript libraries here.
The Leaflet package for R which we will use is well-integrated with other R packages like Shiny and sp however it is also useful to refer to the more extensive documentation of its JavaScript library.
Some shiny extensions
Install and load the leaflet
package and make a simple interactive map to view within RStudio’s Viewer Pane. Learn about how “slippy” or zoomable web maps work on the Mapbox website here.
install.packages("leaflet")
library(leaflet)
leaflet() %>% addTiles()
install.packages("leaflet")
library(leaflet)
leaflet() %>% addTiles() %>%
setView(lng = -76.505206, lat = 38.9767231, zoom = 5) %>%
addWMSTiles(
"http://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi",
layers = "nexrad-n0r-900913", group = "base_reflect",
options = WMSTileOptions(format = "image/png", transparent = TRUE),
attribution = "Weather data © 2012 IEM Nexrad"
)
Just like plots, text, and data frames, UI elements created with htmlwidgets are based on the combination of a render function and an output function. For Leaflet, these functions are renderLeaflet()
and leafletOutput()
. Leaflet map output objects are defined in the render function and can incorporate input objects.
The code inside the render function describes how to create the leaflet map object based on functions in the leaflet package. It starts with the function leaflet()
which returns a map object, and then adds to and modifies elements of that object using the pipe operator %>%
. Elements include background map tiles, markers, polygons, lines, and other geographical features.
Add a new panel to the navbar called “Map” that contains a Leaflet map object with a marker at SESYNC’s location. Note that it may take some time for the map to appear.
# in server
output$sesync_map <- renderLeaflet({
leaflet() %>%
addTiles() %>%
addMarkers(lng = -76.505206, lat = 38.9767231, popup = "SESYNC")
})
# in UI
tabPanel("Map", leafletOutput("sesync_map"))
)
Using addTiles()
displays the default background map tiles. However there are many more options to pick from. There is a list of the free background tiles available and what they look like here. To use a different background specify which ProviderTiles
to display. (Or if you are ambitious, you can create your own tiles for example using free data from OpenStreetMap.
Change the background image from the default to Esri’s free World Imagery.
output$sesync_map <- renderLeaflet({
leaflet() %>%
addProviderTiles("Esri.WorldImagery") %>%
addMarkers(lng = -76.505206, lat = 38.9767231, popup = "SESYNC")
})
tabPanel("Map", leafletOutput("sesync_map"))
Leaflet uses the Web Mercator projection. The use of a (pseudo)-conformal projection is useful for “slippy” web map features of panning and zooming since it preserves a north-up orientation. However because of some mathematical intricacies of Web Mercator, Leaflet will only convert objects to EPSG:3857 if it can. The Leaflet package in R does not yet have the capability to handle objects that are not in common projections like WGS84.
In order to plot the huc_md
object created in the geospatial lesson, we will need to use non-projected coordinates instead of the Alber’s equal area projection.
Read in the counties, watershed boundaries, and NLCD datasets as in the geospatial lesson. Subset the counties to those in MD.
library(sp)
library(rgdal)
library(rgeos)
library(raster)
# make sure you are in your %sandbox% directory!
counties <- readOGR(dsn = "data/cb_2014_us_county_500k/cb_2014_us_county_500k.shp",
layer = "cb_2014_us_county_500k",
stringsAsFactors = FALSE)
huc <- readOGR(dsn = "data/huc250k/huc250k.shp",
layer = "huc250k",
stringsAsFactors = FALSE)
nlcd <- raster("data/nlcd_agg.grd")
counties_md <- counties[counties$STATEFP == "24", ]
In order to perform the union and intersection operations but preserve compatability with leaflet, transform the watershed boundaries and Maryland counties to unprojected coordinate systems.
huc <- spTransform(huc, CRS("+proj=longlat +datum=WGS84"))
counties_md <- spTransform(counties_md, CRS("+proj=longlat +datum=WGS84"))
state_md <- gUnaryUnion(counties_md)
huc_md <- gIntersection(huc, state_md, byid = TRUE,
id = paste(1:length(huc), huc$HUC_NAME))
Add the watershed boundaries in Maryland layer to the map using addPolygons()
. Overlay the NLCD data using addRasterImage()
.
# in server
output$sesync_map <- renderLeaflet({
leaflet(huc_md) %>%
setView(lng = -76.505206, lat = 38.9767231, zoom = 7) %>%
addProviderTiles("Esri.WorldImagery") %>%
addMarkers(lng = -76.505206, lat = 38.9767231, popup = "SESYNC") %>%
addPolygons(fill = FALSE) %>%
addRasterImage(nlcd, opacity = 0.5, maxBytes = 10*1024*1024)
})
The values of the zoom
argument in setView()
are based on zoom levels in tile management schemes. Get a sense for zoom levels of tiles here
Since drawing maps can be computationally intensive, interactivity within the map is typically handed outside of the main render function using a function in the server called leafletProxy()
, and the static map elements are handled within the first render function. See an example of how to implement this here and here.
We can add some simple interactivity by assigning groups to layers and using the addLayersControl()
function. See how this works by adding 2 different masks of the nlcd data with an additional argument group =
in the addRasterImage()
function. For grouped layers, add a feature to toggle between them with layers control. See documentation on this feature here.
output$sesync_map <- renderLeaflet({
leaflet(huc_md) %>%
setView(lng = -76.505206, lat = 38.9767231, zoom = 7) %>%
addProviderTiles("Esri.WorldImagery") %>%
addMarkers(lng = -76.505206, lat = 38.9767231, popup = "SESYNC") %>%
addPolygons(fill = FALSE, group = "MD watersheds") %>%
addRasterImage(mask(nlcd, nlcd == 41, maskvalue = FALSE), opacity = 0.5,
group = "Deciduous Forest", colors = "green") %>%
addRasterImage(mask(nlcd, nlcd == 81, maskvalue = FALSE), opacity = 0.5,
group = "Pasture", colors = "yellow") %>%
addLayersControl(baseGroups=c("Deciduous Forest", "Pasture"),
overlayGroups = c("MD watersheds"))
})