Effective at SESYNC's closure in December 2022, this page is no longer maintained. The information may be out of date or inaccurate.

Interactive Web Applications with Shiny

Handouts for this lesson need to be saved on your computer. Download and unzip this material into the directory (a.k.a. folder) where you plan to work.


Lesson Goals

This lesson presents an introduction to creating interactive web applications using the R Shiny package. It covers:

  • basic building blocks of a Shiny app
  • how to display interactive elements & plots
  • how to customize and arrange elements on a page

Data

This lesson makes use of several publicly available datasets that have been customized for teaching purposes, including the Population estimates from the U.S. Census Bureau.

What is Shiny?

Shiny is a web application framework for R that allows you to create interactive web apps without requiring knowledge of HTML, CSS, or JavaScript. These web apps can be used for exploratory data analysis and visualization, to facilitate remote collaboration, share results, and much more. The example below, and many additional examples, will open in a new browser window (you may need to prevent your broswer from blocking pop-out windows in order to view the app).

The shiny package includes some built-in examples to demonstrate some of its basic features. When applications are running, they are displayed in a separate browser window or the RStudio Viewer pane.

> library(shiny)
> runExample('01_hello')

Listening on http://127.0.0.1:4321

Notice back in RStudio that a stop sign appears in the Console window while your app is running. This is because the current R session is busy running your application.

Closing the app window may not stop the app from using your R session. Force the app to close when necessary by clicking the stop sign in the header of the Console window. The Console window prompt > should return.

Top of Section


Shiny Components

Depending on the purpose and computing requirements of any Shiny app, you may set it up to run R code on your computer, a remote server, or in the cloud. However all Shiny apps consists of the same two main components:

  • The user interface (UI) defines what users will see in the app and its design.
  • The server defines the instructions for how to assemble components of the app like plots and input widgets.

Who is “Listening” on Where?

The terms “client” and “server” show up a lot in discussing Shiny. The client is the web browser. The server is a running program that is waiting to process requests sent by the client. For development, one computer runs both the client and the server.

  • 127.0.0.1 is the IP address your computer uses for itself (it’s the same as ‘localhost’).
  • Your computer is hosting a web page (the UI) whose content is controlled by a running R session.
  • When you run an app through RStudio, that R session is also running the server on your computer.
  • The server responds when you interact with the web page, processing R commands and updating UI objects accordingly.

For big projects, the UI and server components may be defined in separate files called ui.R and server.R and saved in a folder representing the app.

my_app
├── ui.R
├── server.R
├── www
└── data

For ease of demonstration, we’ll use the alternative approach of defining UI and server objects in a R script representing the app.

# User Interface
ui <- ... 

# Server
server <- ...

# Create the Shiny App
shinyApp(ui = ui, server = server)

Hello, World!

Open worksheet-1.R in your handouts repository. In this file, define objects ui and server with the assignment operator <- and then pass them to the function shinyApp(). These are the essential components of a Shiny app.

# User Interface
ui <- navbarPage(title = 'Hello, Shiny World!')

# Server
server <- function(input, output) {}

# Create the Shiny App
shinyApp(ui = ui, server = server)

The Run App button in the Editor allows you to control whether the app runs in a browser window, in the RStudio Viewer pane, or in an external RStudio window.

The shiny package must be installed for RStudio to identify files associated with a Shiny App and provide a Run App button. Note that the file names must be ui.R and server.R if these components are scripted in separate files.

Top of Section


Accessing Data

Because the Shiny app is going to be using your local R session to run, it will be able to recognize anything that is loaded into your working environment. It won’t however find variables in your current environment! Every dependency must be in the script run by the server.

To begin building your own Shiny app, first read in a CSV file with data that we will explore in the app. These data are yearly population estimates for U.S. cities between 2010-2019 from the U.S. Census Bureau. Once the data is in your R environment, it will be available to use in both the ui and server objects.

# Data
popdata <- read.csv('data/citypopdata.csv')

Shiny apps can also be designed to interact with remote data or database servers.

Top of Section


Input/Output Objects

The user interface and the server interact with each other through input and output objects. The user’s interaction with input objects alters parameters in the server’s instructions – instructions for creating output objects shown in the UI.

Writing an app requires careful attention to how input and output objects relate to each other, i.e. knowing what actions will initiate what sections of code to run when.

This diagrams input and output relationships within the UI and server objects:

  • Input objects are created and named in the UI with *Input() functions like selectInput() or radioButtons().
  • Data from input objects affects render*() functions, like renderPlot() or renderText(), in the server that create output objects.
  • Output objects are placed in the UI using *Output() functions like plotOutput() or textOutput().

Input Objects

Input objects collect information from the user, and are passed to the server as a list. Input values change when a user changes the input, and the server is immediately notified. Inputs can be many different things: single values, text, vectors, dates, or even files uploaded by the user.

The best way to find the object you want is through Shiny’s gallery of input objects with sample code.

The first two arguments for all input objects are:

  • inputId (notice the capitalization here!), for giving the input object a name to refer to in the server
  • label or the text to display for the user

Create an input object to let users select a city name from the population data.

# User Interface
in1 <- selectInput(
  inputId = 'selected_city',
  label = 'Select a city',
  choices = unique(popdata[['NAME']]))

Add the input object to the tabPanel() and place the tab object in the ui page. There’s more to come for that panel!

...
tab1 <- tabPanel(
  title = 'City Population',
  in1, ...)
ui <- navbarPage(
  title = 'Census Population Explorer',
  tab1)

Use the selectInput() function to create an input object called selected_city. Use the choices = argument to define a vector with the unique values in the NAME column. Make the input object an argument to the function tabPanel(), preceded by a title argument. We will learn about design and layout in a subsequent section.

Input Object Tips

  • “choices”” for inputs can be named using a list matching a display name to its value, such as list(Male = 'M', Female = 'F').
  • Selectize inputs are a useful option for long drop down lists.
  • Always be aware of what the default value is for input objects you create.

Output Objects

Output objects are created by a render*() function in the server and displayed by a the paired *Output() function in the UI. The server function adds the result of each render*() function to a list of output objects.

Desired UI Object render*() *Output()
plot renderPlot() plotOutput()
text renderPrint() verbatimTextOutput()
text renderText() textOutput()
static table renderTable() tableOutput()
interactive table renderDataTable() dataTableOutput()

Text Output

Render the city name as text using renderText() in the server function, identifying the output as city_label.

# Server
server <- function(input, output) {
  output[['city_label']] <- renderText({
    input[['selected_city']]
  })
}

Display the city name as text in the user interface’s tabPanel as a textOutput object.

out1 <- textOutput('city_label')
tab1 <- tabPanel(
  title = 'City Population',
  in1, out1)

Now the worksheet-2.R file is a complete app, so go ahead and runApp!

Render functions tell Shiny how to build an output object to display in the user interface. Output objects can be data frames, plots, images, text, or most anything you can create with R code to be visualized.

Use outputId names in quotes to refer to output objects within *Output() functions. Other arguments to *Output() functions can control their size in the UI as well as add advanced interactivity such as selecting observations to view data by clicking on a plot.

Note that it is also possible to render reactive input objects using the renderUI() and uiOutput() functions for situations where you want the type or parameters of an input object to change based on another input. For an exmaple, see “Creating controls on the fly” here.

Graphical Output

The app in worksheet-3.R, will use the popdata table to plot population over time of the selected city, rather than just printing its name.

First, the server must filter the data based on the selected city, and then create a plot within the renderPlot() function. Don’t forget to import the necessary libraries.

library(ggplot2)
library(dplyr)
# Server
server <- function(input, output) {
  output[['city_label']] <- renderText({
    input[['selected_city']]
  })
  output[['city_plot']] <- renderPlot({
    df <- popdata %>% 
      dplyr::filter(NAME == input[['selected_city']])
    ggplot(df, aes(x = year, y = population)) +
      geom_line()
  })
}

Second, use the corresponding plotOutput() function in the UI to display the plot in the app.

out1 <- textOutput('city_label')
out2 <- plotOutput('city_plot')
tab1 <- tabPanel(
  title = 'City Population',
  in1, out1, out2)
ui <- navbarPage(
  title = 'Census Population Explorer',
  tab1)

Now the worksheet-3.R file is again a complete app, so go ahead and runApp!

Top of Section


Design and Layout

A suite of *Layout() functions make for a nicer user interface. You can organize elements with a page using pre-defined high level layouts such as

  • sidebarLayout()
  • splitLayout()
  • verticalLayout()

The more general fluidRow() allows any organization of elements within a grid. The folowing UI elements, and more, can be layered on top of each other in either a fluid page or pre-defined layouts.

  • tabsetPanel()
  • navlistPanel()
  • navbarPage()

Here is a schematic of nested UI elements inside the sidebarLayout(). Red boxes represent input objects and blue boxes represent output objects. Each object is located within one or more nested panels, which are nested within a layout. Objects and panels that are at the same level of hierarchy need to be separated by commas in calls to parent functions. Mistakes in usage of commas and parentheses between UI elements is one of the first things to look for when debugging a shiny app!

To re-organize the elements of the “City Population” tab using a sidebar layout, we modify the UI to specify the sidebar and main elements. Create objects side and main then redefine the layout of tab1.

You may need to resize your app viewer window wider for the sidebar to appear on the left.

side <- sidebarPanel('Options', in1)
main <- mainPanel(out1, out2)
tab1 <- tabPanel(
  title = 'City Population',
  sidebarLayout(side, main))

Customization

Along with input and output objects, you can add headers, text, images, links, and other html objects to the user interface using “builder” functions. There are shiny function equivalents for many common html tags such as h1() through h6() for headers. You can use the console to see that the return from these functions produce HTML code.

> h5('This is a level 5 header.')
<h5>This is a level 5 header.</h5>
> a(href = 'https://www.sesync.org',
+   'This renders a link')
<a href="https://www.sesync.org">This renders a link</a>

More Layout Tips and Options

  • In addition to titles for tabs, you can also use icons.
  • Use the argument position = 'right' in the sidebarLayout() function if you prefer to have the side panel appear on the right.
  • See here for additional html tags you can use.
  • For large blocks of text consider saving the text in a separate markdown, html, or text file and use an include* function (example).
  • Add images by saving those files in a folder called www. Embed it with img(src='<FILENAME>')
  • Use shinythemes!

Top of Section


Reactivity

Input objects are reactive which means that an update to this value by a user will notify objects in the server that its value has been changed.

The outputs of render functions are called observers because they observe all “upstream” reactive values for relevant changes.

Reactive Objects

The code inside the body of render*() functions will re-run whenever a reactive value (e.g. an input objet) inside the code is changed by the user. When any observer is re-rendered, the UI is notified that it has to update.

Question
Which element is an observer in the app within worksheet-3.R?
Answer
The object created by renderPlot() and stored with outputId “city_plot”.

The app in worksheet-4.R will have a new input object in the sidebar panel: a slider that constrains the plotted data to a user defined range of years

in2 <- sliderInput(
  inputId = "my_xlims", 
  label = "Set X axis limits",
  min = 2010, 
  max = 2018,
  value = c(2010, 2018))

side <- sidebarPanel('Options', in1, in2)

The goal is to limit records to the user’s input by adding an additional filter within the renderPlot() function.

filter(year %in% ...)

In order for filter() to dynamically respond to the slider, whatever replaces ... must react to the slider.

Shiny provides a function factory called reactive(). It returns a function that behaves like elements in the input list–they are reactive. We’ll use it to create the function slider_years() to dynamically update and pass to the filter.

slider_years <- reactive({
    ...
    ...
  })

The %in% test within filter() needs a sequence, so we wrap seq in reactive to generate a function that creates a sequence based on the user selected values in the slider bar.

slider_years <- reactive({
  seq(input[['my_xlims']][1],
    input[['my_xlims']][2])
})

Make sure that slider_years() is “called”, i.e. has parentheses, within the renderPlot() function.

output[['city_plot']] <- renderPlot({
    df <- popdata %>% 
      filter(NAME == input[['selected_city']]) %>%
      filter(year %in% slider_years())
    ggplot(df, aes(x = year, y = population)) + 
      geom_line() 
})

The new reactive can be used in multiple observers, which is easier than repeating the seq definition again, both for you to code and for the server to process.

output[['city_table']] <- renderDataTable({
  df <- popdata %>% 
      filter(NAME == input[['selected_city']]) %>%
      filter(year %in% slider_years())
  df
})

Too see the data table output, add a corresponding dataTableOutput() to the user interface and place it in the main panel.

out3 <- dataTableOutput('city_table')
main <- mainPanel(out1, out2, out3)

Now the worksheet-4.R file is again a complete app, so go ahead and runApp!

Top of Section


Share Your App

Once you have made an app, there are several ways to share it with others. It is important to make sure that everything the app needs to run (data and packages) will be loaded into the R session.

There is a series of articles on the RStudio website about deploying apps.

Sharing as Files

  • Directly share the source code (app.R, or ui.R and server.R) along with all required data files
  • Publish to a GitHub repository, and advertise that your app can be cloned and run with runGitHub('<USERNAME>/<REPO>')

Sharing as a Site

To share just the UI (i.e. the web page), your app will need to be hosted by a server able to run the R code that powers the app while also acting as a public web server. There is limited free hosting available through RStudio with shinyapps.io. SESYNC maintains a Shiny Apps server for our working group participants, and many other research centers are doing the same.

Top of Section


Exercises

Exercise 1

Starting from worksheet-2.R, modify the call to renderText() to create an output[['city_label']] with both the city name and its statistical area description. You can find those data in the “LSAD” column of popdata. Hint: The function paste() with argument collapse = ' ' will convert a data frame row to a text string.

View solution

Exercise 2

Modify the app completed in worksheet-3.R to include a tabsetPanel() nested within the main panel. Title the first tab in the tabset “Plot” and show the current plot. Title the second tab in the tabset “Data” and show a dataTableOutput(), which you can borrow from the app completed in worksheet-4.R.

View solution

Exercise 3

Use the img builder function to add a logo, photo, or other image to your app in . The help under ?img states that HTML attributes come from named arguments to img, and the “img” HTML tag requires two attributes, and you’ll probably also want to set a valide width. Hint: Use the www/images folder mentioned in the addResourcesPath() function at the bottom of .

View solution

Exercise 4

Notice the exact same code exists twice within the server function of the app you completed in worksheet-4.R: once for renderPlot() and once for renderDataTable. The server has no way to identify an intermediate result, the filtered data frame, which it could have reused. Replace slider_years() with a new selection_years() function that returns the entire data.frame needed by both outputs. Bask in the knowledge of the CPU time saved, and congratulate yourself for practicing DYR!

View solution

Solutions

Solution 1

# Packages
library(dplyr)
library(ggplot2)

# Data
popdata <- read.csv('data/citypopdata.csv')

# User Interface
in1 <- selectInput(
  inputId = 'selected_city',
  label = 'Select a city',
  choices = unique(popdata[['NAME']])
)
out1 <- textOutput('city_label')
out2 <- plotOutput('city_plot')
tab <- tabPanel(
  title = 'City Population',
  in1, out1, out2)
ui <- navbarPage(
  title = 'Census Population Explorer',
  tab)

# Server
server <- function(input, output) {
  output[['city_label']] <- renderText({
    popdata %>% filter(NAME == input[['selected_city']]) %>%
    slice(1) %>% 
    dplyr::select(NAME, LSAD) %>%
    paste(collapse = ' ')
  })
  output[['city_plot']] <- renderPlot({
    df <- popdata %>% 
      dplyr::filter(NAME == input[['selected_city']])
     ggplot(df, aes(x = year, y = population)) + 
      geom_line() 
  })
}

# Create the Shiny App
shinyApp(ui = ui, server = server)

Return

Solution 2

# Packages
library(dplyr)
library(ggplot2)

# Data
popdata <- read.csv('citypopdata.csv')

# User Interface
in1 <- selectInput(
  inputId = 'selected_city',
  label = 'Select a city',
  choices = unique(popdata[['NAME']]))
side <- sidebarPanel('Options', in1)
out1 <- textOutput('city_label')
out2 <- tabPanel(
  title = 'Plot',
  plotOutput('city_plot'))
out3 <- tabPanel(
  title = 'Data',
  dataTableOutput('city_table'))                 
main <- mainPanel(out1, tabsetPanel(out2, out3))
tab <- tabPanel(
  title = 'City Population',
  sidebarLayout(side, main))
ui <- navbarPage(
  title = 'Census Population Explorer',
  tab)

# Server
server <- function(input, output) {
  output[['city_label']] <- renderText({
    popdata %>% filter(NAME == input[['selected_city']]) %>%
    slice(1) %>% 
    dplyr::select(NAME, LSAD) %>%
    paste(collapse = ' ')
  })
  output[['city_plot']] <- renderPlot({
    df <- popdata %>% 
      dplyr::filter(NAME == input[['selected_city']])
     ggplot(df, aes(x = year, y = population)) + 
      geom_line() 
  })
  output[['city_table']] <- renderDataTable({
    popdata %>%
      dplyr::filter(NAME == input[['selected_city']])
  })
}

# Create the Shiny App
addResourcePath('images', 'www/images')
shinyApp(ui = ui, server = server)

Notice the many features of the data table output. There are many options that can be controlled within the render function such as pagination and default length. See here for examples and how to extend this functionality using JavaScript.

Return

Solution 3

# Packages
library(dplyr)
library(ggplot2)

# Data
popdata <- read.csv('data/citypopdata.csv')

# User Interface
in1 <- selectInput(
  inputId = 'selected_city',
  label = 'Select a city',
  choices = unique(popdata[['NAME']]))
img <- img(
  src = 'images/outreach.jpg',
  alt = 'Census outreach flyer',
  width = '100%')
side <- sidebarPanel('Options', img, in1)
out1 <- textOutput('city_label')
out2 <- tabPanel(
  title = 'Plot',
  plotOutput('city_plot'))
out3 <- tabPanel(
  title = 'Data',
  dataTableOutput('city_table'))                 
main <- mainPanel(out1, tabsetPanel(out2, out3))
tab <- tabPanel(
  title = 'City Population',
  sidebarLayout(side, main))
ui <- navbarPage(
  title = 'Census Population Explorer',
  tab)

# Server
server <- function(input, output) {
  output[['city_label']] <- renderText({
    popdata %>% filter(NAME == input[['selected_city']]) %>%
      slice(1) %>% 
      dplyr::select(NAME, LSAD) %>%
      paste(collapse = ' ')
  })
  output[['city_plot']] <- renderPlot({
    df <- popdata %>% 
      dplyr::filter(NAME == input[['selected_city']])
    ggplot(df, aes(x = year, y = population)) + 
      geom_line() 
  })
  output[['city_table']] <- renderDataTable({
    popdata %>%
      dplyr::filter(NAME == input[['selected_city']])
  })
}

# Create the Shiny App
addResourcePath('images', 'www/images')
shinyApp(ui = ui, server = server)

Return

Solution 4

# Packages
library(dplyr)
library(ggplot2)

# Data
popdata <- read.csv('data/citypopdata.csv')

# User Interface
in1 <- selectInput(
  inputId = 'selected_city',
  label = 'Select a city',
  choices = unique(popdata[['NAME']]))

in2 <- sliderInput(
    inputId = "my_xlims", 
    label = "Set X axis limits",
    min = 2010, 
    max = 2018,
    value = c(2010, 2018))

side <- sidebarPanel('Options', in1, in2)									    
out1 <- textOutput('city_label')
out2 <- tabPanel(
    title = 'Plot',
    plotOutput('city_plot'))

out3 <- tabPanel(
    title = 'Data',
    dataTableOutput('city_table'))

main <- mainPanel(out1, tabsetPanel(out2, out3))

tab1 <- tabPanel(
    title = 'City Population',
    sidebarLayout(side, main))

ui <- navbarPage(
    title = 'Census Population Explorer',
    tab1)
  
  # Server
server <- function(input, output) {
    selected_years <- reactive({
      popdata %>%
        filter(NAME == input[['selected_city']]) %>%
        filter(
          year >= input[['my_xlims']][1],
          year <= input[['my_xlims']][2])
    })
    
    output[['city_label']] <- renderText({
      popdata %>% 
        filter(NAME == input[['selected_city']]) %>%
        slice(1) %>% 
        dplyr::select(NAME, LSAD) %>%
        paste(collapse = ' ')
    })
    
    output[['city_plot']] <- renderPlot({
      ggplot(selected_years(), aes(x = year, y = population)) + 
        geom_line() 
    })
    output[['city_table']] <- renderDataTable({
      selected_years()
    })
  }
  
  # Create the Shiny App
  shinyApp(ui = ui, server = server)

Return

Top of Section


If you need to catch-up before a section of code will work, just squish it's 🍅 to copy code above it into your clipboard. Then paste into your interpreter's console, run, and you'll be ready to start in on that section. Code copied by both 🍅 and 📋 will also appear below, where you can edit first, and then copy, paste, and run again.

# Nothing here yet!