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 assigned 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 R objects in a flexible reactive environment. They can be built by an R script containing two components:

  1. ui - an object that contains the app’s user interface
  2. server - a function with two arguments, “input” and “output”

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

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

UI layouts

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:

  1. A sidebarLayout - a divided layout that splits the app into a sidebar panel that contains inputs and a main panel that contains outputs
  2. The fluidRow approach - custom specification of the 12-unit width of the grid system used by Shiny

The 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)
  • 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 is allocated for it).
  • The third instance creates an 8-unit wide 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).

\(~\)

Tabsets

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

\(~\)

Reactivity

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.

\(~\)

Working with data

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

\(~\)

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.