Blog


Telling a data story can be fun, challenging, or often both. The shiny package from RStudio provides the framework for bringing your data story to life via interactive web applications. Shiny apps look great, are engaging for users and are constrained only by the imagination. This post aims to introduce the basic structure of a shiny app so you can begin developing these fun and effective tools.

The Main Sections

While highly complex apps can be broken into separate .R files, the most basic app can be created in a single R file named app.R. This file consists of four main sections:

  • Global - load packages, data and other “setup” items
  • Server - a function we create to build app objects
  • User Interface (UI) - the UI definition of the app (the visual layout)
  • Run - a shiny function to launch the app
# Global

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

# UI
ui <- fluidPage()

# Run the app 
shinyApp(ui = ui, server = server)


The code above is what the structure of app.R looks like prior to adding any content. In the following sections, we will dive into each section to show what is needed to launch your first app.

Global

The global section of a shiny app can be thought of as the app’s staging area. As we will see later on, data in the app that powers tables and visuals can be changed by users, but the global section is off-limits to user changes. This section executes once when the app initiates, and any data loaded in this section is available to objects created in ther server section of the app.

Since the global section executes once upon initiation, it is a good place to load packages and the app’s base data. In the example below, we will begin to create an app that allows users to explore the starwars data set from the dplyr package.

# Global
library(tidyverse)
library(shiny)
data.main <- starwars


With these three lines, we have set the stage for our new app. When the app is initiated, the tidyverse package will load (dplyr is included), the shiny package will load, and the starwars data will be assigned to a global object named data.main. This object will be available for use in other parts of the app and is not subject to modification by users.

Server and UI

The server section of an app is where the majority of work is done. It’s where the majority of your code will live. This section is where the objects of your app (i.e. tables, graphs, widgets, etc.) are created and where any necessary intermediate coding will take place. Closely linked to the server section is the ui section of the app, which clearly lays out how the objects you create are ultimately rendered on-screen. Therefore, before diving into the server and building objects, it is good practice to think about what you want to show in the ui and how you want to show it.

The server and ui sections of an app are linked together. On-screen objects laid out in the ui section can be touched and adjusted by users, and these adjustments ultimately enact changes to objects in the server section. One way to think about it is that inputs are collected in the ui and cause changes in the outputs of the server. From a coding perspective, this means objects appearing in the ui need to be referenced in the server code in order to capture user adjustments. These adjustments will feed into your code to usually create a new object (i.e. table, graph) and ultimatley render on-screen. Establishing links between these two sections may sound tricky, but the shiny package contains functions for generating the various inputs and outputs of an app.

Continuing with our app example, suppose users want to explore the starwars data set by selecting a film and seeing the average height breakdown by eye color as either a table of values or as a graphical display. Thinking through how this interactivity could work, we likely need four shiny objects:

  • One dropdown filter for film
  • One set of radio buttons to select table or graph
  • One table of average height by eye color
  • One graph of average height by eye color

With the path forward laid out, it’s time to build!

Shiny Widgets

Creating dropdown boxes, radio buttons and other objects for users may sound difficult to create, but shiny makes it very easy with ready-made widgets. Check out the shiny widget gallery to explore all possible widgets at our disposal (and if you want even more options, check out the shinyWidgets package).

The dropdown filter and radio buttons needed for our example are basic widgets from the shiny widget gallery. These will be two diferent functions in the server section of our app that will allow users to interact with our data. The dropdown filter will be created via the selectInput() function, and the radio buttons will be created via the radioButtons() function. The functions and arguments are shown below.

  • selectInput(inputId, label, choices, selected, multiple, width)
  • radioButtons(inputId, label, choices, selected, inline, width)

In both functions, an inputId is required. This name is important because it is going to serve as the link between the ui and server in regards to changing data behind the scenes. The label is what will be displayed next to the widget for users to see. Since each of these widgets allows users to select something from a list of options, the choices argument is needed to provide the list of somethings to choose from. The selected argument allows us to set the initial value of the widget (defaults to the first in the list), and the width argument lets us set the pixel width of each widget.

In selectInput(), there is an argument for multiple. This is a logical value indicating whether or not the user should be allowed to select more than one option from the list. In radioButtons(), the inline argument is a logical value indicating whether the options should be side-by-side (inline) or stacked when rendered.

Create our Widgets

With the shiny widget functions identified, we can now add them within the server function in our script. The beauty of creating widgets in the server is that, if you recall, the data we loaded in the global section is available for us to use. Why is this helpful? If we consider filling in the choices argument for the selectInput() function, where users can select a film of interest, we would have to list (i.e. type) all seven films available in the data set. If the data is ever updated and a new film is added, we would then have to revisit this widget and update the available choices. However, since the data.main object is available to us, we can simply reference this object to ensure we always list the current available films.

# Server
server <- function(input, output, session) {
  
  selectInput(inputId = 'film', 
              label = 'Select film',
              choices = sort(unique(starwars$films %>% unlist())), 
              selected = 'Return of the Jedi', 
              multiple = F, 
              width = '250px')
  
  radioButtons(inputId = 'visual', 
               label = 'Output', 
               choices = c('Table', 'Graph'), 
               selected = 'Table', 
               inline = T, 
               width = '250px')
  
}


With the widgets now defined, the last thing we need to do is wrap each widget in a renderUI() function. In shiny, in order to make objects created in the server available for addition to the ui, they must be contained within a render function (which makes our object reactive - more to discuss later) and assigned to an output object. The naming of the output object is what will ultimately be referenced in the ui in order to make our widgets available for users to interact with. Thus, let’s wrap each of our widgets in a render function with the specific naming structure output$myname.

# Server
server <- function(input, output, session) {
  # Input widgets
  output$select.film <- renderUI({
    selectInput(inputId = 'film', 
              label = 'Select film',
              choices = sort(unique(starwars$films %>% unlist())), 
              selected = 'Return of the Jedi', 
              multiple = F, 
              width = '250px')
  })
  
  output$select.visual <- renderUI({
    radioButtons(inputId = 'visual', 
               label = 'Output', 
               choices = c('Table', 'Graph'), 
               selected = 'Table', 
               inline = T, 
               width = '250px')
  })
}


At this stage, we have successfully created two widgets for our users to interact with. When we eventually reference them in the ui, we will refer to them as select.film and select.visual. Now let’s look at creating the two visual outputs.

Create our Visuals

For our visual outputs, we will need to create two different objects - a table and a graph using ggplot2. Both will be built off of the same data. The data driving these graphics will be our data.main object with one slight adjustment. If a user wants to see Return of the Jedi, we will need to filter our data to only include this film In order to do this we will need to reference the value in the select.film widget.

It cannot be stated enough, but linking between the server and ui in an app is very powerful. As briefly mentioned, what creates these links are references to the various names of our objects. I purposely referenced the wrong object name just a moment ago to draw attention to the importance of name referencing. The name select.film should only be used in the ui when arranging our user interface. Since we are working in the server and need to identify which film is selected, we actually want to work with the object named film. More specifically (and similar to what we did with our output), we want to work with input$film. This input object will help us properly identify which film to filter to when modifying data.main to fit our user’s selection.

In the case of creating a table for users to view, we simply need to create a data frame that will ulimately be rendered in the app. To do so, we use dplyr verbs just as we would in any standard analysis. The only differnce now is that the value we use to filter will be coming from an input object. Therefore, we simply pass that object in the dplyr chain and end up with our desired data frame.

# Server
server <- function(input, output, session) {
  df <- data.main %>%
    rowwise() %>%
    mutate(films = paste(unlist(films), collapse = ', ')) %>%
    ungroup() %>%
    filter(str_detect(films, input$film)) %>%
    group_by(eye_color) %>%
    summarise(Height = round(mean(height, na.rm = T), 2)) %>%
    ungroup() %>%
    arrange(Height)
}


With the data frame now created, the last thing we need to do is wrap this chain in a render function, similar to before. The shiny package has several render functions for various outputs. Since we want to generate a table for this object, we will use the renderTable() function and give it a meaningful name in the output$myname format.

# Server
server <- function(input, output, session) {
  # Visuals
  output$height.table <- renderTable({
    df <- data.main %>%
    rowwise() %>%
    mutate(films = paste(unlist(films), collapse = ', ')) %>%
    ungroup() %>%
    filter(str_detect(films, input$film)) %>%
    group_by(eye_color) %>%
    summarise(Height = round(mean(height, na.rm = T), 2)) %>%
    ungroup() %>%
    arrange(Height)
    
    df
  })
}


The second visual we need to create is our graph. The graph is going to use the same data, yet it’s going to need to be wrapped in a different render function and given a specific output name. Since the film will be changing based on the user selection, one nice touch we will add to the graph is a custom title. Referencing the user selection input$film, we can dynamically update the title with each selection made. All of these steps are taken care of below.

server <- function(input, output, session) {
  # Visuals
  output$height.table <- renderTable({
    df <- data.main %>%
    rowwise() %>%
    mutate(films = paste(unlist(films), collapse = ', ')) %>%
    ungroup() %>%
    filter(str_detect(films, input$film)) %>%
    group_by(eye_color) %>%
    summarise(Height = round(mean(height, na.rm = T), 2)) %>%
    ungroup() %>%
    arrange(Height)
    
    df
  })
  
  output$height.graph <- renderPlot({
    
    df <- data.main %>%
    rowwise() %>%
    mutate(films = paste(unlist(films), collapse = ', ')) %>%
    ungroup() %>%
    filter(str_detect(films, input$film)) %>%
    group_by(eye_color) %>%
    summarise(Height = round(mean(height, na.rm = T), 2)) %>%
    ungroup() %>%
    arrange(Height)
    
    # Dynamically set title by referencing cohort selected
    Title <- paste0('Average Height by Eye Color (', input$film, ')')
    
    ggplot(df, aes(x = eye_color, y = Height)) +
      geom_bar(stat = 'identity', fill = 'grey10') +
      theme_minimal() +
      labs(x = 'Eye Color',
           y = 'Height (cm)',
           title = Title) +
      theme(plot.title = element_text(size = 20, face = 'bold'),
            axis.title = element_text(size = 16),
            axis.text = element_text(size = 14))
  })
}

Reactivity

The concept of reactivity in shiny is a somewhat complext topic that takes time to get used to, but once understood, it can really pay dividends. In short, reactivity allows a shiny app to update instantly upon user interaction. Dependencies between objects in the server update only when they are activated, creating a domino effect of changes happening behind the scenes. To learn more about shiny reactivity, check out the overview from RStudio.

How is this relevant to our example? If we look at the two visual objects we created, they both use the same data. In fact, the data frame generated (df) in each object is exactly the same, meaning any future changes to our dplyr chain will need to be updated in multiple locations (which can be hard to keep track of!). In a shiny app, this sort of duplication is unnecessary, thanks to reactivity.

To streamline our code, we are going to pull out the duplicated lines and put them into a new reactive object named data.rx. This requires use of the reactive() function from shiny, which basically converts a normal expression into a reactive expression (one that can change). In server, this looks like:

server <- function(input, output, session) {
  
  data.rx <- reactive({
    df <- data.main %>%
    rowwise() %>%
    mutate(films = paste(unlist(films), collapse = ', ')) %>%
    ungroup() %>%
    filter(str_detect(films, input$film)) %>%
    group_by(eye_color) %>%
    summarise(Height = round(mean(height, na.rm = T), 2)) %>%
    ungroup() %>%
    arrange(Height)
    
    df
  })
}


What we have just created in our app is a new reactive data object that will update whenever a user selects a new film. The way this helps us is that wherever we had the dplyr chain to create our data frame, we can now replace it with a simple reference to our reactive object. To refer to a reactive object, the name of the object must be followed by a set of empty parentheses. Thus, we need to go back to our visual objects and update them with references to data.rx().

server <- function(input, output, session) {
  # Reactive object
  data.rx <- reactive({
    df <- data.main %>%
      filter(`Cohort Year` == input$cohort) %>%
      group_by(`Matric Program`, Gender) %>%
      summarise(N = n()) %>%
      ungroup() %>%
      spread(key = Gender, value = N) %>%
      mutate(MalePct = round(Male/(Female + Male), 2)) %>%
      mutate(MalePct = ifelse(is.na(MalePct), 0, MalePct)) %>%
      select(`Matric Program`, MalePct)
    
    df
  })
  
  # Visuals
  output$height.table <- renderTable({
    data.rx()
  })
  
  output$height.graph <- renderPlot({
    # Dynamically set title by referencing cohort selected
    Title <- paste0('Average Height by Eye Color (', input$film, ')')
    
    ggplot(data.rx(), aes(x = eye_color, y = Height)) +
      geom_bar(stat = 'identity', fill = 'grey10') +
      theme_minimal() +
      labs(x = 'Eye Color',
           y = 'Height (cm)',
           title = Title) +
      theme(plot.title = element_text(size = 20, face = 'bold'),
            axis.title = element_text(size = 16),
            axis.text = element_text(size = 14))
  })
}


By adding a reactive element, we have made our life easier by decreasing the overall lines of code in the app and eliminating redundancy. If we ever need to update the logic underlying this data frame, we only have to do it in one place (yet it will still update both visuals!).

With this step complete, and thinking back to the creation of our input widgets, the total server code is as follows:

server <- function(input, output, session) {
  # Input widgets
  output$select.film <- renderUI({
    selectInput(inputId = 'film', 
              label = 'Select film',
              choices = sort(unique(starwars$films %>% unlist())), 
              selected = 'Return of the Jedi', 
              multiple = F, 
              width = '250px')
  })
  
  output$select.visual <- renderUI({
    radioButtons(inputId = 'visual', 
               label = 'Output', 
               choices = c('Table', 'Graph'), 
               selected = 'Table', 
               inline = T, 
               width = '250px')
  })
  
  # Reactive object
  data.rx <- reactive({
    df <- data.main %>%
    rowwise() %>%
    mutate(films = paste(unlist(films), collapse = ', ')) %>%
    ungroup() %>%
    filter(str_detect(films, input$film)) %>%
    group_by(eye_color) %>%
    summarise(Height = round(mean(height, na.rm = T), 2)) %>%
    ungroup() %>%
    arrange(Height)
    
    df
  })
  
  # Visuals
  output$height.table <- renderTable({
    data.rx()
  })
  
  output$height.graph <- renderPlot({
    # Dynamically set title by referencing cohort selected
    Title <- paste0('Average Height by Eye Color (', input$film, ')')
    
    ggplot(data.rx(), aes(x = eye_color, y = Height)) +
      geom_bar(stat = 'identity', fill = 'grey10') +
      theme_minimal() +
      labs(x = 'Eye Color',
           y = 'Height (cm)',
           title = Title) +
      theme(plot.title = element_text(size = 20, face = 'bold'),
            axis.title = element_text(size = 16),
            axis.text = element_text(size = 14))
  })
}

UI

The ui is where we finally start laying out the objects we have created on-screen. In our case, we have to think about the two input widgets and two visuals (the reactive data frame is kept behind the scenes). While shiny offers some pre-defined layouts, we will look at the most flexible option - the fluid page.

A fluid page layout is enacted by calling the fluidPage() function. This responsive (fits the dimensions of the screen) layout is made up of rows and columns. Each row in this layout is broken into 12 columns. Therefore, if we place an object in a fluid row, it will look to fill the entire 12 columns in that row (unless we specify otherwise). Each fluid row is created using the fluidRow() function. Working with fluid rows and columns takes some getting used to, but once understood, objects can be placed on screen in the precise location you desire.

Getting back to our example, we will need to ultimately place three items on-screen: two widgets and the selected visual. Having total control, we can do this however we please. One layout that is often enjoyed is having all filters down the left-hand side of the screen and the visual rendering to the right of these.

To do this, we will need to call a fluid row, and within this row break our data into two columns. One column for the input widgets, and one for the output. Below is how we would accomplish this task. Remember, each fluidRow() consists of 12 columns, so the number of columns most often sums to 12 (or less to leave empty space to the right for aesthetic purposes). Also, notice the shiny functions used to output each object - they have “Output” in the name. For our widgets, which were created using renderUI(), they require the uiOutput() function. The table visual, which was rendered using the renderTable() function, requires the tableOutput() function.

# UI ----
ui <- fluidPage(
  fluidRow(column(1, 
                  uiOutput('select.film'),
                  uiOutput('select.visual')),
           column(9,
                  tableOutput('height.table'))
           )
)


Using this layout produces the following:

While this fits the idea we were trying to achieve, a couple spacing modifications could help. First, the objects are flush with the top of the page. We can add a space here by using the br() command before our fluid row (add as many as you desire). Also, the film selection widget looks crammed for space. More space can be provided by increasing the columns from 1 to 2 (be sure to readjust the total column sum if it exceeds 12).

# UI ----
ui <- fluidPage(
  br(),
  br(),
  fluidRow(column(2, 
                  uiOutput('select.film'),
                  uiOutput('select.visual')),
           column(8,
                  tableOutput('height.table'))
           )
)


This looks better!

We could continue making modifications to move our objects all over the screen, but let’s leave it as-is for now.

The final task we need to complete is to make the “Output” widget operable. Right now, if we were to select the Graph radio button, nothing would happen. In order to bring this option to life, we are going to use the conditionalPanel() function in the ui. This function allows us to check a condition prior to rendering an object on-screen. We will thus check the value of input$visual to ensure we show the proper visual for the user (since this is a JavaScript test, the “$” is replaced by a “.” for “input.visual”). Also, since we are going to output a plot, we use the plotOutput() function.

# UI ----
ui <- fluidPage(
  br(),
  br(),
  fluidRow(column(2, 
                  uiOutput('select.film'),
                  uiOutput('select.visual')),
           column(1),
           column(8,
                  conditionalPanel(condition = "input.visual == 'Table'",
                                  tableOutput('height.table')),
                  conditionalPanel(condition = "input.visual == 'Graph'",
                                   plotOutput('height.graph')))
  )
)


Run the App

The final section of the app is not really a section but a shiny function to launch the app. The arguments of the function point to the server and ui we just created.

# Run the app 
shinyApp(ui = ui, server = server)

Extra Resources

Shiny applications are an engaging, fun and effective tool for telling a data story. Users are provided the ability to explore data in novel ways while analysts and developers are encouraged to be creative in assembling and displaying data. The shiny package provides all the building blocks necessary to achieve full-scale web applications.

I hope this brief tutorial was helpful in understanding the components of a shiny app and will prove useful in creating your first. The material covered herein barely scratches the surface of all the possibilities that await. Below are a collection of references for your future shiny development.


Here’s the working app

To create the example app detailed in this post, simply copy and paste the code below into a new R script, save it and click Run App.

library(tidyverse)
library(shiny)
data.main <- starwars

server <- function(input, output, session) {
  # Input widgets
  output$select.film <- renderUI({
    selectInput(inputId = 'film', 
                label = 'Select film',
                choices = sort(unique(starwars$films %>% unlist())), 
                selected = 'Return of the Jedi', 
                multiple = F, 
                width = '250px')
  })
  
  output$select.visual <- renderUI({
    radioButtons(inputId = 'visual', 
                 label = 'Output', 
                 choices = c('Table', 'Graph'), 
                 selected = 'Table', 
                 inline = T, 
                 width = '250px')
  })
  
  # Reactive object
  data.rx <- reactive({
    df <- data.main %>%
      rowwise() %>%
      mutate(films = paste(unlist(films), collapse = ', ')) %>%
      ungroup() %>%
      filter(stringr::str_detect(films, input$film)) %>%
      group_by(eye_color) %>%
      summarise(Height = round(mean(height, na.rm = T), 2)) %>%
      ungroup() %>%
      arrange(Height)
    
    df
  })
  
  # Visuals
  output$height.table <- renderTable({
    data.rx()
  })
  
  output$height.graph <- renderPlot({
    # Dynamically set title by referencing cohort selected
    Title <- paste0('Average Height by Eye Color (', input$film, ')')
    
    ggplot(data.rx(), aes(x = eye_color, y = Height)) +
      geom_bar(stat = 'identity', fill = 'grey10') +
      theme_minimal() +
      labs(x = 'Eye Color',
           y = 'Height (cm)',
           title = Title) +
      theme(plot.title = element_text(size = 20, face = 'bold'),
            axis.title = element_text(size = 16),
            axis.text = element_text(size = 14))
  })
}

# UI ----
ui <- fluidPage(
  br(),
  br(),
  fluidRow(column(2, 
                  uiOutput('select.film'),
                  uiOutput('select.visual')),
           column(8,
                  conditionalPanel(condition = "input.visual == 'Table'",
                                   tableOutput('height.table')),
                  conditionalPanel(condition = "input.visual == 'Graph'",
                                   plotOutput('height.graph')))
  )
)

# Run the app 
shinyApp(ui = ui, server = server)