# install.packages(c("shiny", "DT", "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.
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 the code in this lab will be the presentation of examples that can (and should) be run elsewhere. I recommend opening a new R script and copying the examples there. Once you have the code copied, you can run the app by either:
Ctrl+Shift+Enter (possibly Cmd for
Mac)Ctrl+EnterAll 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 associated 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. The following code will fail to run
correctly:
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, R is also running in the background. 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)
Verify that you see the warning message printed in your console.
While this example is a bit contrived, it does represent a common enough occurrence. Whenever ambiguity about an object arises, printing diagnostic information to the console is always a good first step.
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 “Type” or
“Region” (or both). Examples for these other types of inputs are
included in the thinks above
## Note that we can read in data and manipulate variables outside of ui() or server()
colleges <- read.csv("https://collinn.github.io/data/college2019.csv")
# Character vector of numeric college variables
numcols <- names(colleges)[sapply(colleges, is.numeric)]
ui <- fluidPage(
## Add stuff here
)
## Set up the server function
server <- function(input, output) {
## Add stuff here
}
## 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://collinn.github.io/data/college2019.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) {
## Here I need to manipulate `colleges` based on input$stateSub
## Then I need to generate a table of that data
}
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 reiterate, any reactive object (created with
render<Type>()) will change or update in response to
any changes in input contained within it. As the plot
associated with output$myhist contains both
input$n and input$bins, it will automatically
update any time one of these values changes.
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 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 may seem redundant now, but as the complexity of our Shiny app
grows it will be an essential tool for helping to manage the web of
relationships between reactive objects and the outputs associated with
them. 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. How does this save on computation time?
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)