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 R objects in a flexible reactive environment. They can be built by an R script containing two components:
Below is 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 was created using the fluidPage()
function, which sets up three design elements:
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.
Next, the app’s server function is created using
function(input, output){}
. Inside this function are the
instructions needed to create the ‘plot’ output that appears in the UI.
Any output must be passed back to the UI using a render
function. There are many different 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 created using default widths for each element. This layout is tends to be unappealing, and one of the following alternatives should be preferred:
sidebarLayout
- a divided layout that splits the app
into a sidebar panel that contains inputs and a main panel that contains
outputsfluidRow
approach - custom specification of the
12-unit width of the grid system used by ShinyThe code below demonstrates how to apply a sidebar layout to our first app, neatly placing all inputs to the right of the graph within a single panel:
## 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)
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 is allocated for it).plotOutput
,
notice how this was created within a new row.Question #1: Using the example app as a template,
try removing the second use of fluidRow
and placing the
plotOutput
into a single use of fluidRow
that
contains 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).
\(~\)
Sometimes you might want the user of an app to be able to toggle
between different output types within a single section of 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)
The example above allows the user to tab back and forth between a histogram and a numeric summary of the data.
Notice how manipulating the bins
slider doe 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).
\(~\)
In an earlier example we saw that manipulating the bins
slider would not result in a new numeric summary, but it would result in
a new histogram (displaying new data).
This example is useful in understanding 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, and they will re-execute whenever any of those inputs
change. In our example, an entirely new data set was created each time
we manipulated the ‘bins’ slider because the bins
input is
part of the render_
function that also generates the
data.
A simple way to manage reactivity is to use the
isolate()
function and an “action button”:
## 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("goButton", "Implement 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$goButton ## 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$goButton ## 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 a data object using the
reactive()
function. This is because we only want the data
to change in response to the input “n”, we do not want changes in any
other inputs to impact the data displayed in our outputs.
Next, we isolate every reactive input, except for the
action button, found within our render_
functions.
This leads to the action button being the only input capable of causing
the code within the render_
functions to re-run; thus, the
histogram/summary table are only re-created when the action is
pressed.
\(~\)
The examples so far have used simulated data that was generated within the app. The example below demonstrates a few important nuances involved in working with real data within the shiny environment:
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” with the UI label “Choose your variable:”.
The choices
argument creates UI labels (left) and maps them
to variable names (right).
Because these variable names are stored in the input
object as character strings, 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.