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)

  1. Please work together with your partner. Make sure you both fully understand each concept before you move on.
  2. Please record your answers and any related code for all embedded lab questions. I encourage you to try out the embedded examples, but you shouldn’t turn them in.
  3. Please ask for help, clarification, or even just a check-in if anything seems unclear.

\(~\)

Preamble

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:

  1. UI - an object that contains the app’s user interface
  2. Server - a function with two arguments, “input” and “output”, that receives information from the UI and return output/results

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 value
  • sliderInput - a slider that the user can adjust
  • plotOutput - a plot that appears on the page

By 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 output
  • renderTable() - renders tabular output
  • renderPrint() - renders printed character strings or text
  • renderTextOutput() - 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.)

\(~\)

Lab

Setting up the UI

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:

  1. A sidebarLayout - a divided layout that splits the app’s UI into a sidebar panel containing inputs and a main panel containing outputs
  2. The fluidRow approach - customized positions within the 12-unit wide grid system used by Shiny

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

  • First, fluidRow() sets up a row containing two columns
    • The first instance of column() creates a 2-unit wide numericInput using the wellPanel style (grey background) at the start of the first row.
    • The second instance creates an 8-unit wide sliderInput, notice this doesn’t extend across the entire 8-unit length (but that space was allocated for it).
  • Next, fluidRow() is used to set up a second row
    • Then the third use of 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.

\(~\)

Tabsets

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

\(~\)

Reactivity

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.

  • In some circumstances, the default reactive behavior of render_ functions is desirable since it causes the app to respond in real time to any inputs that are changed.
  • In other circumstances, particularly those involving computationally expensive code or visualization, this behavior is undesirable and will make the app feel laggy and slow.

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.

\(~\)

Working with data

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

  • It’s not necessary to provide a UI label for every choice, and you could use a command like choices = names(colleges) to set up an input that contains all of the named columns in “colleges”.
    • However, shortcuts like this can be problematic if some choices aren’t compatible with your output (such as character variables in this example, which cannot be used to create a histogram)

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().

\(~\)

Practice

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.

\(~\)

Next Steps

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.

\(~\)

Additonal functions to know

The following a few examples of functions that you may find useful when working with R Shiny:

  1. 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)

\(~\)

  1. get() - takes string input and attempts to “get” the R object whose name corresponds to that string

For 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

\(~\)

  1. 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!