Interactive Web Applications with RShiny
Lesson 6 with Kelly Hondula
Lesson Goals
This lesson presents an introduction to creating interactive web applications using the R Shiny package. It covers:
- The basic building blocks of a Shiny app
- How to create interactive elements, including plots
- How to customize and arrange elements on a page
Data
This lesson makes use of several publicly available datasets that have been customized for teaching purposes, including the Portal teaching database.
What is Shiny?
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.
library(shiny)
runExample("01_hello")
Notice back in RStudio that a stop sign appears in the Console window while your app is running. This is because the current R session is busy running your application.
Shiny components
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:
-
The user interface (UI) which defines what users will see in the app and its design.
-
The server which defines the instructions for how to assemble components of the app like plots and input widgets.
For big projects, these components may be defined in separate files called ui.R
and server.R
and saved in a folder representing the app.
dir("my_app")
[1] "ui.R" "server.R" "data" "www"
For ease of demonstration, we’ll use the alternative approach of defining UI and server objects in a R script representing the app.
# User Interface
ui <- ...
# Server
server <- ...
# Create the Shiny App
shinyApp(ui = ui, server = server)
But, what’s going on?
-
The UI is a web page whose content is controlled by a running R session.
-
When you run an app through RStudio, that session is running on your laptop or desktop.
-
Your browser gets directed to a local port, say it’s port 1234, on your personal computer acting like a web server, which will be at http://127.0.0.1:1234
-
The R session responds when you interact with the web page, processing R commands and updating UI objects according to their result.
Hello, Shiny World!
Open lesson-6-1.R
in your handouts repository.
In this file, define objects ui
and server
with the assignment operator <-
and then pass them to the function shinyApp()
.
These are the basic components of a shiny app.
# User Interface
ui <- navbarPage(title = "Hello, Shiny World!")
# Server
server <- function(input, output){}
# Create the Shiny App
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.
Accessing data
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.
Here, we read in CSV files from the Portal dataset, so it is available to both the ui and server definitions.
The app script is in the same folder as data
, and you only need to specify the relative file path.
# Data
species <- read.csv("data/species.csv", stringsAsFactors = FALSE)
surveys <- read.csv("data/surveys.csv", na.strings = "", stringsAsFactors = FALSE)
# User Interface
ui <- ...
# Server
server <- ...
# Run app
shinyApp(ui = ui, server = server)
Shiny apps can also be designed to interact with remote data or shared databases.
Input and Output Objects
The user interface and the server interact with each other through input and output objects. The user’s interaction with input objects alters parameters in the server’s instructions – instructions for creating output objects shown in the UI.
Writing an app 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.
This diagrams input and output relationships within the UI and server objects:
- Input objects are created and named in the UI with functions like
selectInput()
orradioButtons()
. - Data from input objects affects render functions in the server object which create output objects.
- Output objects are placed in the UI using output functions like
plotOutput()
ortextOutput()
.
Input Objects
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 objects are:
inputId
(notice the capitalization here!) 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.
Define the UI by adding an input object that lets users select a species ID from the species table.
# User Interface
in1 <- selectInput("pick_species",
label = "Pick a species",
choices = unique(species[["species_id"]]))
...
tab <- tabPanel("Species", in1, ...)
ui <- navbarPage(title = "Portal Project", tab)
Output objects
Output objects are created through the combination of pairs of render*()
and *Output()
functions, in the UI and server respectively.
Desired UI Object | render*() |
*Output() |
---|---|---|
plot | renderPlot() | plotOutput() |
text | renderPrint() | verbatimTextOutput() |
text | renderText() | textOutput() |
static table | renderTable() | tableOutput() |
interactive table | renderDataTable() | dataTableOutput() |
The server function adds the output of the render*()
functions to a list of output objects.
Textual Output
Display the species id as text under the input object using textOutput
in the UI and renderText
in the server object.
# User Interface
in1 <- selectInput("pick_species",
label = "Pick a species",
choices = unique(species[["species_id"]]))
out1 <- textOutput("species_id")
tab <- tabPanel("Species", in1, out1)
ui <- navbarPage(title = "Portal Project", tab)
# Server
server <- function(input, output) {
output[["species_id"]] <- renderText(input[["pick_species"]])
}
Go ahead and run the app!
Graphical Output
In lesson-6-3.R
, we use the surveys table to plot abundance of the selected species, rather than just printing its id.
First, the server must filter the survey data based on the selected species, and then create a bar plot within the renderPlot()
function.
Don’t forget to import the necessary libraries.
server <- function(input, output) {
output[["species_plot"]] <- renderPlot(
surveys %>%
filter(species_id == input[["pick_species"]]) %>%
ggplot(aes(year)) +
geom_bar()
)
}
Second, use the corresponding plotOutput()
function in the UI to display the plot in the app.
# User Interface
in1 <- selectInput("pick_species",
label = "Pick a species",
choices = unique(species["species_id"]))
out2 <- plotOutput("species_plot")
tab <- tabPanel("Species", in1, out2)
ui <- navbarPage(title = "Portal Project", tab)
Exercise 1
Use textOutput()
to add a title above the plot giving the full species name. The function paste()
with argument collapse = ' '
will convert a data frame row to a text string. Hint: Multiple render*()
functions are allowed in the server function.
Design and Layout
A suite of *Layout()
functions make for a nicer user interface. You can organize elements using pre-defined high level layouts such as
sidebarLayout()
splitLayout()
verticalLayout()
The more general fluidRow()
allows any organization of elements within a grid.
The folowing UI elements, and more, can be layered on top of each other in either a fluid page or pre-defined layouts.
tabsetPanel()
navlistPanel()
navbarPage()
Here is a schematic of nested UI elements inside the sidebarLayout()
. Red boxes represent input objects and blue boxes represent output objects.
To re-organize the elements of the “Species” tab using a sidebar layout, we modify the UI to specify the sidebar and main elements.
# User Interface
in1 <- selectInput("pick_species",
label = "Pick a species",
choices = unique(species[["species_id"]]))
out2 <- plotOutput("species_plot")
side <- sidebarPanel("Options", in1)
main <- mainPanel(out2)
tab <- tabPanel("Species",
sidebarLayout(side, main))
ui <- navbarPage(title = "Portal Project", tab)
Exercise 2
Include a tabset within the main panel. Call the first element of the tabset “Plot” and show the current plot. Call the second element of the tabset “Data” and show an interactive table with the surveys data used in the plot.
General layouts
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")
))
Customization
Along with input 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.
h5("This is a level 5 header")
a(href="www.sesync.org", "This syntax renders a link")
Exercise 3
Turn the title of the sidebar into a formatted header. Use the ability of the shiny::builder functions to specify HTML attributes that center the heading.
Some useful features and resources
- In addition to titles for tabs, you can also use icons.
- Use the argument
position = "right"
in thesidebarLayout()
function if you prefer to have the side panel appear on the right. - See here for additional html tags you can use.
- For large blocks of text consider saving the text in a separate markdown, html, or text file and use an
include*
function (example). - Add images by saving those files in a folder called www. Link to it with
img(src="<file name>")
- Use a shiny theme with the shinythemes package
Reactivity
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.
The outputs of render functions are called observers because they observe all “upstream” reactive values for changes.
Reactive objects
The code inside the body of render*()
functions will re-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.
- Question
- Which element is an observer in the app within
lesson-6-3.R
. - Answer
- The object created by
renderPlot()
and stored with outputId “species_plot”.
In lesson-6-4.R
we’re going to create a new input object in the sidebar panel that constrains the plotted data to a user defined range of months.
in2 <- sliderInput("slider_months",
label = "Month Range",
min = 1,
max = 12,
value = c(1, 12))
side <- sidebarPanel(h3("Options", align="center"), in1, in2)
To limit surveys to the user specified months, an additional filter is needed that is something like
filter(month %in% %reactive_range%)
The way we will define the reactive_range within the server makes it a function, so we must call it when used in the observer definition.
# Server
server <- function(input, output) {
reactive_range <- ...
output[["species_plot"]] <- renderPlot(
surveys %>%
filter(species_id == input[["pick_species"]]) %>%
filter(month %in% reactive_range()) %>%
ggplot(aes(year)) +
geom_bar()
)
}
The function reactive()
creates generic objects for use by observers.
# Server
server <- function(input, output) {
reactive_range <- reactive(
seq(input[["slider_months"]][1],
input[["slider_months"]][2])
)
output[["species_plot"]] <- renderPlot(
surveys %>%
filter(species_id == input[["pick_species"]]) %>%
filter(month %in% reactive_range()) %>%
ggplot(aes(year)) +
geom_bar()
)
}
User submitted data
A particularly useful type of user input is a file, made possible with the fileInput()
function.
- relates a data frame with column
datapath
giving the path to each uploaded file - allows explicity control over permissible data (by specification of MIME type, an internet standard)
Data download
It is also possible to let users download files from a Shiny app, such as a csv file of the currently visible data.
The downloadHandler()
function, analagous to the render*()
functions that create output objects, requires two arguments:
- filename, a string or a function that returns a string ending with a file extention
- content, a function to generate the content of the file and write it to a temporary file
# Server
...
output[["download_data"]] <- downloadHandler(
filename = "species.csv",
content = function(file) {
surveys %>%
filter(species_id == input[["pick_species"]]) %>%
filter(month %in% reactive_range()) %>%
write.csv(file)
}
)
}
The UI now gets a download button!
# User Interface
...
dl <- downloadButton("download_data", label = "Download")
side <- sidebarPanel(h3("Options", align="center"), in1, in2, dl)
...
Exercise 4
Notice the exact same code exists twice within the server function … what a waste of CPU time! Extract the data processing to its own reactive object, which updates when and only when the input objects it references are updated.
Share your app
Once you have made an app, there are several ways to share it with others. It is important to make sure that everything the app needs to run (data and packages) will be loaded into the R session.
There is a series of articles on the RStudio website here about deploying apps.
Share as files
- email or copy app.R, or ui.R and server.R, and all required data files
- host the same files and data as a GitHub repository and advertise its accessbility through calls to
shiny::runGitHub("%repo%", "%username%")
Share as a website
To share just the UI (i.e. the web page) it will need to be hosted by a computer able to run the R code that powers the app while acting as a public web server. There is limited free hosting available through RStudio with shinapps.io. SESYNC maintains a Shiny Apps server for our working group participants, and many other research centers are doing the same.
Solution 1
# Libraries
library(ggplot2)
library(dplyr)
# Data
species <- read.csv("data/species.csv", stringsAsFactors = FALSE)
surveys <- read.csv("data/surveys.csv", na.strings = "", stringsAsFactors = FALSE)
# User Interface
in1 <- selectInput("pick_species",
label = "Pick a species",
choices = unique(species["species_id"]))
out1 <- textOutput("species_name")
out2 <- plotOutput("species_plot")
tab <- tabPanel("Species", in1, out1, out2)
ui <- navbarPage(title = "Portal Project", tab)
server <- function(input, output) {
output[["species_name"]] <- renderText(
species %>%
filter(species_id == input[["pick_species"]]) %>%
select(genus, species) %>%
paste(collapse = ' ')
)
output[["species_plot"]] <- renderPlot(
surveys %>%
filter(species_id == input[["pick_species"]]) %>%
ggplot(aes(year)) +
geom_bar()
)
}
# Create the Shiny App
shinyApp(ui = ui, server = server)
Solution 2
# Libraries
library(ggplot2)
library(dplyr)
# Data
species <- read.csv("data/species.csv", stringsAsFactors = FALSE)
surveys <- read.csv("data/surveys.csv", na.strings = "", stringsAsFactors = FALSE)
# User Interface
in1 <- selectInput("pick_species",
label = "Pick a species",
choices = unique(species["species_id"]))
side <- sidebarPanel("Options", in1)
out1 <- textOutput("species_name")
tab1 <- tabPanel("Plot",
plotOutput("species_plot"))
tab2 <- tabPanel("Data",
dataTableOutput("species_table"))
out2 <- tabsetPanel(tab1, tab2)
main <- mainPanel(out1, out2)
tab <- tabPanel("Species",
sidebarLayout(side, main))
ui <- navbarPage(title = "Portal Project", tab)
server <- function(input, output) {
output[["species_name"]] <- renderText(
species %>%
filter(species_id == input[["pick_species"]]) %>%
select(genus, species) %>%
paste(collapse = ' ')
)
output[["species_plot"]] <- renderPlot(
surveys %>%
filter(species_id == input[["pick_species"]]) %>%
ggplot(aes(year)) +
geom_bar()
)
output[["species_table"]] <- renderDataTable(
surveys %>%
filter(species_id == input[["pick_species"]])
)
}
# Create the Shiny App
shinyApp(ui = ui, server = server)
Solution 3
# Libraries
library(ggplot2)
library(dplyr)
# Data
species <- read.csv("data/species.csv", stringsAsFactors = FALSE)
surveys <- read.csv("data/surveys.csv", na.strings = "", stringsAsFactors = FALSE)
# User Interface
in1 <- selectInput("pick_species",
label = "Pick a species",
choices = unique(species["species_id"]))
side <- sidebarPanel(h2("Options", align="center"), in1)
out1 <- textOutput("species_name")
tab1 <- tabPanel("Plot",
plotOutput("species_plot"))
tab2 <- tabPanel("Data",
dataTableOutput("species_table"))
out2 <- tabsetPanel(tab1, tab2)
main <- mainPanel(out1, out2)
tab <- tabPanel("Species",
sidebarLayout(side, main))
ui <- navbarPage(title = "Portal Project", tab)
server <- function(input, output) {
output[["species_name"]] <- renderText(
species %>%
filter(species_id == input[["pick_species"]]) %>%
select(genus, species) %>%
paste(collapse = ' ')
)
output[["species_plot"]] <- renderPlot(
surveys %>%
filter(species_id == input[["pick_species"]]) %>%
ggplot(aes(year)) +
geom_bar()
)
output[["species_table"]] <- renderDataTable(
surveys %>%
filter(species_id == input[["pick_species"]])
)
}
# Create the Shiny App
shinyApp(ui = ui, server = server)
Solution 4
# Libraries
library(ggplot2)
library(dplyr)
# Data
species <- read.csv("data/species.csv", stringsAsFactors = FALSE)
surveys <- read.csv("data/surveys.csv", na.strings = "", stringsAsFactors = FALSE)
# User Interface
in1 <- selectInput("pick_species",
label = "Pick a Species",
choices = unique(species[["species_id"]]))
in2 <- sliderInput("slider_months",
label = "Month Range",
min = 1,
max = 12,
value = c(1, 12))
dl <- downloadButton("download_data", label = "Download")
side <- sidebarPanel(h3("Options", align="center"), in1, in2, dl)
out2 <- plotOutput("species_plot")
main <- mainPanel(out2)
tab <- tabPanel("Species",
sidebarLayout(side, main))
ui <- navbarPage("Portal Project", tab)
# Server
server <- function(input, output) {
reactive_range <- reactive(
seq(input[["slider_months"]][1],
input[["slider_months"]][2])
)
reactive_data <- reactive(
surveys %>%
filter(species_id == input[["pick_species"]]) %>%
filter(month %in% reactive_range())
)
output[["species_plot"]] <- renderPlot(
reactive_data() %>%
ggplot(aes(year)) +
geom_bar()
)
output[["download_data"]] <- downloadHandler(
filename = "species.csv",
content = function(file) {
reactive_data() %>%
write.csv(file)
})
}
# Create the Shiny App
shinyApp(ui = ui, server = server)