R Shiny
This lab will cover the basic steps in creating an
R Shiny
web app, highlighting features that many past
students have found useful.
# Please install and load the following packages
# install.packages("shiny")
library(shiny)
library(ggplot2)
Directions (Please read before starting)
\(~\)
Shiny apps allow users to interact with data and R objects in a flexible reactive environment. They can be built by an R script that houses two components:
Below is an example of a very simple shiny app:
## Set up the UI object
ui <- fluidPage(
numericInput(inputId = 'n', label = 'Number of obs:', value = 100),
sliderInput(inputId = 'bins', label = "Number of bins:", min = 2, max = 20, value = 10),
plotOutput('plot')
)
## Set up the server function
server <- function(input, output){
output$plot <- renderPlot({
ggplot() + geom_histogram(aes(x = rnorm(input$n)), bins = input$bins)
})
}
## Build and run the app
shinyApp(ui, server)
This app’s UI is “fluid page” created using the
fluidPage()
function. This page contains three
components:
numericInput
- a text box where the user can enter a
numeric valuesliderInput
- a slider that the user can adjustplotOutput
- a plot that appears on the pageBy default, these design elements will appear as rows in the UI in the order they were given.
Notice that two of these components are inputs, meaning they
will pass information from the UI into the server
function.
While the third is an output, which will receive output from
server
function.
Next, the app’s server function is created by the code:
function(input, output){ .. }
. This function translates the
user’s inputs into the ‘plot’ output that appears in the UI.
Server output must be passed back to the UI using a render function. There are many different render functions, each corresponding to specific type of output object:
renderPlot()
- renders plot outputrenderTable()
- renders tabular outputrenderPrint()
- renders printed character strings or
textrenderTextOutput()
- renders text output exactly as it
appears in the R Console’renderPlotly()
- renders graphics generated by
plotly
renderLeaflet()
- renders maps generated by
leaflet
A common error is to use a render function that is incompatible with
the type of output your UI is expecting, so make sure that they match
(ie: renderPlot()
and plotOutput()
,
renderLeafet()
and leafletOutput()
, etc.)
\(~\)
Unless otherwise specified, UI design elements are vertically arranged (as rows) in the order they are given using default widths for each component. This layout can be unappealing, and one of the following alternatives should be preferred:
sidebarLayout
- a divided layout that splits the
app’s UI into a sidebar panel containing inputs and a main
panel containing outputsfluidRow
approach - customized positions within the
12-unit wide grid system used by ShinyThe code below demonstrates how to apply a sidebar layout to our first app. Notice how it neatly places the inputs in a panel to the right of the main graph.
## Set up the UI object
ui <- fluidPage(
sidebarLayout(position = "right",
sidebarPanel(
numericInput(inputId = 'n', label = 'Number of obs:', value = 100),
sliderInput(inputId = 'bins', label = "Number of bins:", min = 2, max = 20, value = 10)),
mainPanel(
plotOutput('plot')
)
)
)
## Set up the server function
server <- function(input, output){
output$plot <- renderPlot({
ggplot() + geom_histogram(aes(x = rnorm(input$n)), bins = input$bins)
})
}
## Build and run the
shinyApp(ui, server)
The code below demonstrates the fluidRow
approach:
## Set up the UI object
ui <- fluidPage(
fluidRow(
column(2, wellPanel(numericInput(inputId = 'n', label = 'Number of obs:', value = 100))),
column(8, sliderInput(inputId = 'bins', label = "Number of bins:", min = 2, max = 20, value = 10))
),
fluidRow(
column(8,plotOutput('plot'))
)
)
## Set up the server function
server <- function(input, output){
output$plot <- renderPlot({
ggplot() + geom_histogram(aes(x = rnorm(input$n)), bins = input$bins)
})
}
## Build and run the
shinyApp(ui, server)
This approach is more flexible, but also more complicated to explain.
fluidRow()
sets up a row containing two columns
column()
creates a 2-unit wide
numericInput
using the wellPanel
style (grey
background) at the start of the first row.sliderInput
,
notice this doesn’t extend across the entire 8-unit length (but that
space was allocated for it).fluidRow()
is used to set up a second row
column()
creates an 8-unit wide
plotOutput
within this second row.Question #1: Using the example app as a template,
remove the second use of fluidRow()
and place the
plotOutput
column into the first row after the two input
features. Briefly describe how this changes the appearance of the app’s
UI (relative to the example code that uses fluidRow
twice).
You should see a subtle difference in how components are positioned.
\(~\)
Sometimes you’ll want an app user to be able to toggle between
different types of output in a single panel within the UI. The
tabsetPanel()
function allows for this:
## Set up the UI object
ui <- fluidPage(
sidebarLayout(position = "left",
sidebarPanel(
numericInput(inputId = 'n', label = 'Number of obs:', value = 100),
sliderInput(inputId = 'bins', label = "Number of bins:", min = 2, max = 20, value = 10)),
mainPanel(
tabsetPanel(
tabPanel("Histogram", plotOutput('plot')),
tabPanel("Summary", verbatimTextOutput('summary'))
)
)
)
)
## Set up the server function
server <- function(input, output){
output$plot <- renderPlot({
ggplot() + geom_histogram(aes(x = rnorm(input$n)), bins = input$bins)
})
output$summary <- renderPrint({
summary(rnorm(input$n))
})
}
## Build and run the
shinyApp(ui, server)
In this example, we use the tabsetPanel()
function to
split the main panel into two different tab panels. The first
displays the histogram output, while the second displays text.
Something you might notice is that manipulating the bins
slider does not cause the numeric summary to be recalculated,
but it does cause the histogram to be recreated. This due to
something known as “reactivity” (discussed in a future section of this
lab).
\(~\)
In our example app, we previously saw that manipulating the
bins
slider did not produce a new numeric summary, but did
result in a new histogram being drawn. This new histogram was made using
entirely new data, even though new data isn’t necessary or desired when
manipulating bins
.
This simple example can be used to understand the idea of a reactive program, where the basic premise is that one or more reactive endpoints will “listen” for changes in the inputs used to create them. A reactive piece of code will re-run whenever the inputs it depends upon change.
In our example, an entirely new data set was created each time we
manipulated the ‘bins’ slider because the bins
input is
contained inside of the renderPlot()
function that also
generates the data. However, the bins
input is not present
inside the renderPrint()
function that generates the text
summary, so that function will not be re-run when the value of the
bins
input changes.
render_
functions is desirable since it causes the app to
respond in real time to any inputs that are changed.The isolate()
function and an “action button” provide a
simple way to manage reactivity:
## Set up the UI object
ui <- fluidPage(
sidebarLayout(position = "left",
sidebarPanel(
numericInput(inputId = 'n', label = 'Number of obs:', value = 100),
sliderInput(inputId = 'bins', label = "Number of bins:", min = 2, max = 20, value = 10),
actionButton("my_button", "Show Changes", class = "btn-success")), ## Go Button on the UI side
mainPanel(
tabsetPanel(
tabPanel("Histogram", plotOutput('plot')),
tabPanel("Summary", verbatimTextOutput('summary'))
)
)
)
)
## Set up the server function
server <- function(input, output){
data <- reactive(rnorm(input$n)) ## create a single data object
output$plot <- renderPlot({
input$my_button ## Go Button on the server side
b <- isolate(input$bins) ## prevent reactivity
n <- isolate(data())
ggplot() + geom_histogram(aes(x = n), bins = b)
})
output$summary <- renderPrint({
input$my_button ## Go Button on the server side
n <- isolate(data()) ## prevent reactivity
summary(n)
})
}
## Build and run
shinyApp(ui, server)
In this example, we first create an object to store the data used in
both the plot and text output using the reactive()
function. We make this object reactive because we want the data to
change whenever the input “n” changes.
Next, we isolate each reactive input (or object), except
for the action button, that appears inside our app’s
render_
functions. Inputs that are isolated will not
provoke reactive behavior, so the command
isolate(input$bins)
does not prompt the code inside of
renderPlot()
to be re-run when the “bins” input is
manipulated.
Thus, the action button is the only input capable of prompting the
code contained inside of our render_
functions to re-run,
so pressing it is the only way to generate the new histogram and summary
when the “bins” and “n” inputs have been changed.
\(~\)
Our examples so far have used randomly generated data that was created within the app. The example below demonstrates a few important details that arise when working with real data in the shiny environment:
## Notice we can load the data outside the server and UI
colleges <- read.csv("https://remiller1450.github.io/data/Colleges2019.csv")
## Set up the UI object
ui <- fluidPage(
sidebarLayout(position = "left",
sidebarPanel(
selectInput(inputId = "variable", label = "Choose your variable:",
choices = c("Enrolled Students" = "Enrollment",
"Median ACT score" = "ACT_median",
"Average Faculty Salary" = "Avg_Fac_Salary"))),
mainPanel(
plotOutput('plot')
)
)
)
## Set up the server function
server <- function(input, output){
output$plot <- renderPlot({
ggplot(data = colleges, aes_string(x = input$variable)) + geom_histogram() ## notice the use of aes_string
})
}
## Build and run
shinyApp(ui, server)
The selectInput()
function sets up an input that is
internally named “variable” and appears in the UI with the label “Choose
your variable:”. The choices
argument creates UI labels
(left) and maps them to variable names (right).
choices = names(colleges)
to set
up an input that contains all of the named columns in “colleges”.
Additionally, you should note that inputs are passed to the server as
character strings. So, when a user selects the “Enrolled Students”
option on the drop down menu the string "Enrollment"
is
stored in input$variable
. Because ggplot()
expects object names rather than strings in its aesthetic mappings, we
must use the aes_string
argument to reference them within
ggplot()
.
\(~\)
Question #3: Create an app that uses the “colleges”
data set, where the main panel display is a scatter plot with
“Salary10yr_median” as the “y” variable. The user should be able to
select their own “x” variable from at least 3 of the other numeric
variables in the data. Additionally, the user should be able to filter
the data by “State” using a selectizeInput
with the
argument multiple = TRUE
(which allows for the selection of
multiple states as part of the filtering criteria). Finally, an action
button should be the only way for the app to redraw the scatter
plot.
Hints: The filtering step should use the
reactive()
function and should also use %in%
.
You might also consider isolating the reactive data set produced by the
filtering step.
\(~\)
At this point you should have a good grasp on the basics of R Shiny, and the possibilities for where to go next are vast. I encourage you to begin by browsing the resources below:
The first and second links provide ideas (and sometimes code) for what is possible within the R Shiny environment, while the third should provide a broad reference guide to the different components of your app.
Question #4: Find an example app from either the R Shiny Gallery or the Grinnell College page and identify one UI feature or function that you’d like to use. As your response to this question, include a link to the app and a brief description of the function(s) used (or that you think are used) to create the feature/function you identified.
\(~\)
The following a few examples of functions that you may find useful when working with R Shiny:
switch()
- will switch a specific input value into
another.In this example, the input string "shape"
is switched to
the string "square"
. If our input was
"length"
, the output would have been 5
:
my_input = "shape"
switch(my_input,
"color" = "red",
"shape" = "square",
"length" = 5)
\(~\)
get()
- takes string input and attempts to “get” the R
object whose name corresponds to that stringFor example, suppose your input is a string containing the name of a variable and you want to find the mean of the corresponding variable:
my_var = 1:10
my_input = "my_var"
mean(my_input) ## not what you want
mean(get(my_input)) ## What you want
\(~\)
with()
- will setup a local environment within a data
frame where all object references are made within the data.For example, suppose your input is a string containing the name of a variable in the Colleges data set and you want to find it’s mean:
my_input = "Enrollment"
# mean(colleges$my_input) ## this doesn't work
with(colleges, mean(get(my_input))) ## this works!