Preamble

# 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.

Lab Workflow

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:

  1. Pushing Ctrl+Shift+Enter (possibly Cmd for Mac)
  2. Clicking the little Run button at the top of the window pane
  3. Highlighting all of the code in the R Script file related to your app and hitting 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.

Example Shiny

An R Shiny application consists of three parts that must be included in each app:

  1. A ui (user interface) object
  2. A server function
  3. A call to the function shinyApp(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.

Lab

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.

UI

Broadly speaking, the UI interface of a shiny app can be broken into operations:

  1. Receiving user input
  2. Organizing output

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.

Input

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.

Using ggplot2 and dplyr

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)

\(~\)

Additional Input Functions

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:

  1. Additional Shiny input functions
  2. Fancy Shiny input functions with package shinyWidgets

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)

Output

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:

  1. First, it generates the necessary HTML for the object to be correctly presented in the Shiny app
  2. Second, it creates a reactive context that responds to changes in the user input, allowing the output to have the appropriate values. We will explore this briefly in the next section and comprehensively in the next lab.

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)

Reactivity

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)