# install.packages("shiny")
# install.packages("DT")
# install.packages("shinyWidgets")
library(shiny)
library(dplyr)
library(ggplot2)
For the next component of our class, we are going to begin our introduction to R Shiny, a package that provides a framework for building interactive web applications in R without requiring any knowledge of HTML, CSS, or JavaScript.
Additionally, much of this and the following labs “borrow heavily” from the Mastering Shiny book, in particular Chapters 1-6. I have tried to contain the bulk of what we need to get running in the labs that follow, but if you’re interested in learning more or if you’re not able to find guidance on what you are trying to do through an internet search, this would be a decent place to look.
Because R Shiny requires an active R session to run, it can cause issues when embedded in a static HTML document such as those created with knitting in R Markdown. To handle this, we are going to slightly modify our workflow for the next few labs. At the top of a new R Markdown folder we typically see this line for our setup:
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = TRUE)
```
We are going to add an argument here (eval = FALSE
) so
that, by default, R Markdown doesn’t try to run any of the code we
include in code blocks. This will allow you to submit an R Markdown
document with R code but without it trying to run Shiny apps in each
block:
```{r setup, include=FALSE}
# Adding `warning` and `message` will hide package output, which is nice
knitr::opts_chunk$set(echo = TRUE, eval = FALSE, warning = FALSE, message = FALSE)
```
Much of this lab will be the presentation of examples that need to be ran elsewhere (this will make more sense Shiny code has been copied into a blank R Script, you can run the app by:
Ctrl+Shift+Enter
(possibly Cmd
for
Mac)Ctrl+Enter
All of the code we have will be safe to overwrite, so don’t fret on deleting everything and starting over. You are also highly encouraged to modify examples on your own to see how your apps respond to changes. Any code that you are asked to keep can be placed in a code chunk in your R Markdown file.
An R Shiny application consists of three parts that must be included in each app:
ui
(user interface) objectserver
functionshinyApp(ui, server)
.The first of these objects, ui
, is responsible for
organizing all of the user interface; this primarily means collecting
input from the user and organizing output. The second object, the
server
function, is responsible for performing “back-end”
operations such as creating plots or subsetting data frames. It acts in
response to changes in user input. And finally,
shinyApp(ui, server)
, takes these two components and builds
the R Shiny app. A completely minimal Shiny app looks like this:
library(shiny)
## Set up the UI object
ui <- fluidPage(
"Hello world"
)
## Set up the server function
server <- function(input, output) {
}
## Build and run the app
shinyApp(ui, server)
Question 0: Copy and paste the above code into a blank R script. Verify that you are able to run the Shiny app and successfully close it when you are finished.
Note: I won’t make a point of saying it explicitly each
time, but when you see a code block like the one above containing a
ui
, server
, and shinyApp
, it
should be understood that you are to copy and paste it into your R
script, run it, play with it, and make sure you understand what it is
doing before moving on.
This lab will be primarily focused on the UI aspects of a Shiny App. Following this, we will have a lab on server-side code, and finally, we will wrap up with a few tips on adding a little bit of style.
Broadly speaking, the UI interface of a shiny app can be broken into operations:
The input and output that you specify in the ui
object
will be passed to the server
function as indicated by the
arguments it takes, input
and output
.
Both input and output are created with functions. Conveniently, most
(but not all) of the input functions are named according to the
convention <type>Input()
. The first argument of all
input functions will be the identifier which allows you to
manipulate the input in the server function, while the rest tend to be
function specific. In this next example, we see two inputs with the ID
"n"
and "bins"
along with their use in the
server function.
ui <- fluidPage(
# Input functions allow us to solicit input from user
numericInput(inputId = "n", label = "Number of obs:", value = 100), # comma after each object in ui
sliderInput(inputId = "bins", label = "Number of bins:", min = 2, max = 20, value = 10),
# Output functions reserve space for output
plotOutput(outputId = "myhist")
)
server <- function(input, output) {
# Note that output$myhist aligns with outputId "myhist" in the ui
# And that input$n aligns with inputId "n" in the ui (same with input$bins)
output$myhist <- renderPlot({
# Draw n values from a random normal distribution
X <- rnorm(input$n)
ggplot() + geom_histogram(aes(x = X), bins = input$bins)
})
}
shinyApp(ui, server)
Here, there are a few things to take note of. First and perhaps most
importantly, we see that we are able to access each of the inputs in the
server function with the use of input$varname
; that is,
input$n
was used in rnorm
to compute the
quantity of random numbers we wished to draw and input$bin
was used to determine how many bins we wanted in our histogram. Second,
take note that each function in ui
is separated by a comma.
Shiny is pretty vocal if you ever forget this.
The class of the input object is determined by the input function.
Both numericInput()
and sliderInput()
return
numeric values by default so there was no issue with them being used as
number for sample size and bins. textInput()
, on the other
hand, returns a character vector by default which is not compatible with
(most) numeric operations.
ui <- fluidPage(
textInput(inputId = "n", label = "Number of obs:", value = 100),
numericInput(inputId = "bins", label = "Number of bins:", min = 2, max = 20, value = 10),
plotOutput(outputId = "myhist")
)
server <- function(input, output) {
output$myhist <- renderPlot({
# Draw n values from a random normal distribution
N <- 1 * input$n # rnorm("10") will coerce without warning, but 1 * "10" will not
X <- rnorm(N)
ggplot() + geom_histogram(aes(x = X), bins = input$bins)
})
}
shinyApp(ui, server)
Debugging applications in Shiny can be bewildering at first because
of the number of moving parts. However, we can utilize the fact that
when a Shiny app is running, so is R. We can use this to our advantage
to print things to the console for investigation. In this case, I’m not
sure what class input$n
is, so I will include a
print()
function which won’t impact the app but will send
output to my console.
ui <- fluidPage(
textInput(inputId = "n", label = "Number of obs:", value = 100),
sliderInput(inputId = "bins", label = "Number of bins:", min = 2, max = 20, value = 10),
plotOutput(outputId = "myhist")
)
server <- function(input, output) {
output$myhist <- renderPlot({
## Look for this output in the console
print(paste0("Class of input$n: ", class(input$n)))
N <- 1 * input$n
X <- rnorm(N)
ggplot() + geom_histogram(aes(x = X), bins = input$bins)
})
}
shinyApp(ui, server)
To solve this, we would either need to change the type of input
function we use to solicit the data (textInput()
to
numericInput()
or we would need to coerce the variable with
as.numeric(input$n)
). We will introduce additional
debugging tips as they arise.
By far the most common classes for inputs to take are numeric or
character. A consequence of how these values are stored arises when we
try to use functions like ggplot
or mutate
which take unquoted variable names rather than character strings for
their arguments. Addressing this issue (known as non-standard
evaluation) lies comfortably beyond the scope of this course.
Regarding dplyr
functions, we will simply try and perform
most of our data manipulations prior to running Shiny, though we will
see in the next lab a few ways around this when data manipulation is a
critical aspect of the application. As for ggplot
, we will
introduce two idioms: the first is using aes_string()
to
pass character strings to aesthetics instead of names, while the second
uses as.formula()
in conjunction with faceting. I will
include examples of both for reference. Here, we are asking for two
inputs: one to choose a numeric variable to plot with displacement, the
other with which to facet wrap.
ui <- fluidPage(
# Select plotting variable (we can used a named vector to change name of radio buttons)
selectInput(inputId = "var", label = "Select Y Axis:", choices = c("City" = "cty",
"Highway" = "hwy")),
# Select facet variable (in addition to named list, we can use choiceNames/choiceValues)
radioButtons(inputId = "facet", label = "Select Facet Column:",
choiceNames = c("Cylinder", "Drive", "Year"),
choiceValues = c("cyl", "drv", "year")),
plotOutput(outputId = "myhist")
)
server <- function(input, output) {
output$myhist <- renderPlot({
## Use aes_string, note that "displ" must now be in quotes
ggplot(mpg, aes_string("displ", input$var)) + geom_point() +
# replace input$facet with any other character string in your own app
facet_wrap(as.formula(paste("~", input$facet)))
})
}
shinyApp(ui, server)
\(~\)
There are a number of additional input functions here that cater to a variety of different inputs and formats that you may be interested, see the list here:
Question 1: Using the outline of the code below,
write a shiny app that creates a scatter plot using any two numeric
variables from the dataset (already computed below in
numcols
). In addition to this, either add an additional
input from numcols
to add a color aesthetic to your plot OR
add radio buttons to facet between either the variable “Private” or
“Region” (or both).
colleges <- read.csv("https://remiller1450.github.io/data/Colleges2019.csv")
# Character vector of numeric college variables
numcols <- names(colleges)[sapply(colleges, is.numeric)]
ui <- fluidPage(
)
## Set up the server function
server <- function(input, output) {
}
## Build and run the
shinyApp(ui, server)
From our previous examples, you may have already inferred a bit about
how outputs work in R Shiny. Similar to the inputs, we specify output
using a function that is typically named according to the convention
<type>Output
, along with an associated ID. Unlike
input objects, however, we don’t manipulate outputs directly; rather, we
pair an output function with an associated render function
within server()
.
Let’s return to the first example we ran, and consider again that on
the UI side we allocate space for a plot with
plotOutput(outputId = "myhist")
. On the server side, we
assign an object to output$myhist
that directly
corresponds to the associated ID from the UI with the function
renderPlot()
.
ui <- fluidPage(
numericInput(inputId = "n", label = "Number of obs:", value = 100),
# Output functions reserve space for output with ID "myhist"
plotOutput(outputId = "myhist")
)
server <- function(input, output) {
# A render function then assigns output to output$myhist
output$myhist <- renderPlot({
ggplot() + geom_histogram(aes(x = rnorm(input$n)), bins = 20)
})
}
shinyApp(ui, server)
For every UI function associated with output, there is a
corresponding render<type>()
function in
server()
. Without this render function, the app will not
work as intended (or at all, actually):
ui <- fluidPage(
numericInput(inputId = "n", label = "Number of obs:", value = 100),
plotOutput(outputId = "myhist")
)
server <- function(input, output) {
# Remove renderPlot() results in error
output$myhist <- ggplot() + geom_histogram(aes(x = rnorm(input$n)), bins = 20)
}
shinyApp(ui, server)
This render function does two things:
In addition to the output/render functions for plots, the following table will prove useful.
Input | Output | Description |
---|---|---|
plotOutput() |
renderPlot() |
Produces basic plot or ggplot objects |
textOutput() |
renderText() |
Prints character strings |
verbatimTextOutput() |
renderPrint() |
Prints text as raw R output |
tableOutput() or DT::DTOutput() |
renderTable() or DT::renderDT() |
Creates nice tables from data frames |
plotlyOutput() |
renderPlotly() |
Produce plotly graphs in Shiny |
Question 2: Use the appropriate output functions in
order to render a table or data table showing the colleges
dataset subset by selected states. Hint: use subset
and
%in%
on the server side to create the necessary data
frame.
library(DT)
colleges <- read.csv("https://remiller1450.github.io/data/Colleges2019.csv")
ui <- fluidPage(
# I can use values from colleges to populate choices
# multiple = TRUE allows me to select multiple states at once
selectInput("stateSub", label = "Choose states to subset:",
choices = unique(colleges$State), multiple = TRUE)
)
server <- function(input, output, session) {
}
shinyApp(ui, server)
As we mentioned previously, the use of the
render<Type>()
functions creates a reactive
environment necessary for Shiny to run. Put simply, a reactive
environment is one that is able to respond to user inputs. In a typical
R session, a particular value is computed once when the code is
initially run, whereas with Shiny, we want reactive code to change each
time we modify the input. It’s this relationship between reactive
objects and the UI that allows the the number of observations or bins in
a histogram to change after we modify the input.
To be clear, reactive objects change in response to changes in the
input
object. So when we see code like this (you don’t have
to run this):
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(outputId = "myhist")
)
server <- function(input, output) {
output$myhist <- renderPlot({
X <- rnorm(input$n)
ggplot() + geom_histogram(aes(x = X), bins = input$bins)
})
}
shinyApp(ui, server)
we understand that the renderPlot()
is responding to
changes in input$n
and input$bins
.
We can explicitly create our own reactive objects with the
recative()
function. Unlike functions we have seen so far
in R, this doesn’t simply return a static object, but rather another
function. This means that to use reactive objects elsewhere, we need to
append ()
to the end of it:
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(outputId = "myhist")
)
server <- function(input, output) {
## Create reactive objects with reactive()
n <- reactive(input$n)
bins <- reactive(input$bins)
output$myhist <- renderPlot({
# Use reactive objects n() and bins()
X <- rnorm(n())
ggplot() + geom_histogram(aes(x = X), bins = bins())
})
}
shinyApp(ui, server)
These reactive objects will respond just as their associated inputs
would: changes to input$n
trigger n()
to
update which then triggers the reactivity in
renderPlot()
.
This will be a critical tool for us to use as the complexity of our Shiny app grows. In particular, it will help us manage webs of reactive relationships between objects as well as reduce redundant code and computation. We will spend the next lab exploring reactivity in a number of different contexts.
Question 3: Modify the following Shiny app so that duplicated expressions are contained within a reactive context.
library(shiny)
ui <- fluidPage(
sliderInput("x", "If x is", min = 1, max = 50, value = 30),
sliderInput("y", "and y is", min = 1, max = 50, value = 5),
textOutput("product"),
textOutput("product_plus5"),
textOutput("product_plus10")
)
server <- function(input, output, session) {
output$product <- renderText({
product <- input$x * input$y
paste0("Then (x * y) is: ", product)
})
output$product_plus5 <- renderText({
product <- input$x * input$y
paste0("and (x * y) + 5 is: ", product + 5)
})
output$product_plus10 <- renderText({
product <- input$x * input$y
paste0("and (x * y) + 10 is: ", product + 10)
})
}
shinyApp(ui, server)