Interactive reporting + visualization with Shiny

Lecture 18

Dr. Mine Çetinkaya-Rundel

Duke University
STA 313 - Spring 2024

Warm up

Announcements

  • HW 5 is posted, due next Tuesday, work on it in lab tomorrow

Setup

# load packages
library(tidyverse)

# set figure parameters for knitr
knitr::opts_chunk$set(
  fig.width = 7, # 7" width
  fig.asp = 0.618, # the golden ratio
  fig.retina = 3, # dpi multiplier for displaying HTML output on retina
  fig.align = "center", # center align figures
  dpi = 300 # higher dpi, sharper image
)

Shiny: High level view

Shiny

Every Shiny app has a webpage that the user visits,
and behind this webpage there is a computer that serves this webpage by running R.

Shiny

When running your app locally, the computer serving your app is your computer.

Shiny

When your app is deployed, the computer serving your app is a web server.

Shiny

Demo

Anatomy of a Shiny app

What’s in an app?

library(shiny)
ui <- fluidPage()


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


shinyApp(
  ui = ui, 
  server = server
)
  • User interface controls the layout and appearance of app

  • Server function contains instructions needed to build app

Data: Ask a manager

Source: Ask a Manager Survey via TidyTuesday

This data does not reflect the general population; it reflects Ask a Manager readers who self-selected to respond, which is a very different group (as you can see just from the demographic breakdown below, which is very white and very female).

Some findings here.

Data: manager

manager <- read_csv("data/survey.csv")
manager
# A tibble: 26,232 × 18
   timestamp          how_old_are_you industry  job_title additional_context_o…¹
   <chr>              <chr>           <chr>     <chr>     <chr>                 
 1 4/27/2021 11:02:10 25-34           Educatio… Research… <NA>                  
 2 4/27/2021 11:02:22 25-34           Computin… Change &… <NA>                  
 3 4/27/2021 11:02:38 25-34           Accounti… Marketin… <NA>                  
 4 4/27/2021 11:02:41 25-34           Nonprofi… Program … <NA>                  
 5 4/27/2021 11:02:42 25-34           Accounti… Accounti… <NA>                  
 6 4/27/2021 11:02:46 25-34           Educatio… Scholarl… <NA>                  
 7 4/27/2021 11:02:51 25-34           Publishi… Publishi… <NA>                  
 8 4/27/2021 11:03:00 25-34           Educatio… Librarian High school, FT       
 9 4/27/2021 11:03:01 45-54           Computin… Systems … Data developer/ETL De…
10 4/27/2021 11:03:02 35-44           Accounti… Senior A… <NA>                  
# ℹ 26,222 more rows
# ℹ abbreviated name: ¹​additional_context_on_job_title
# ℹ 13 more variables: annual_salary <dbl>, other_monetary_comp <dbl>,
#   currency <chr>, currency_other <chr>, additional_context_on_income <chr>,
#   country <chr>, state <chr>, city <chr>,
#   overall_years_of_professional_experience <chr>,
#   years_of_experience_in_field <chr>, …

Ultimate goal

Interactive reporting with Shiny

Livecoding

Go to the ae-13 project and code along in app-1.R.


Highlights:

  • Data pre-processing
  • Basic reactivity

Livecoding

Go to the ae-21 project and code along in app-2.R.


Highlights:

  • Data pre-processing outside of the app
  • Tabsets
  • Validation

Interactive visualizations with Shiny

Livecoding

Go to the ae-13 project and code along in app-3.R.


Highlights:

  • Conditional panels
  • Linked brushing

Reference

Reference

The code for the app can be found here.

# Load packages ----------------------------------------------------------------

library(shiny)
library(tidyverse)
library(ggthemes)
library(scales)
library(countrycode)

# Load data --------------------------------------------------------------------

manager_survey <- read_rds("data/manager-survey.rds")

# Find all industries ----------------------------------------------------------

industry_choices <- manager_survey |>
  distinct(industry_other) |>
  arrange(industry_other) |>
  pull(industry_other)

# Randomly select 3 industries to start with -----------------------------------

selected_industry_choices <- sample(industry_choices, 3)

# Define UI --------------------------------------------------------------------

ui <- fluidPage(
  titlePanel(title = "Ask a Manager"),
  sidebarLayout(
    
    # Sidebar panel
    sidebarPanel(
      checkboxGroupInput(
        inputId = "industry",
        label = "Select up to 8 industies:",
        choices = industry_choices,
        selected = selected_industry_choices
      ),
    ),
    
    # Main panel
    mainPanel(
      hr(),
      "Showing only results for those with salaries in USD who have provided information on their industry and highest level of education completed.",
      br(), br(),
      textOutput(outputId = "selected_industries"),
      hr(),
      br(),
      tabsetPanel(
        type = "tabs",
        tabPanel("Average salaries", plotOutput(outputId = "avg_salary_plot")),
        tabPanel(
          "Individual salaries",
          conditionalPanel(
            condition = "input.industry.length <= 8",
            sliderInput(
              inputId = "ylim",
              label = "Zoom in to salaries between",
              min = 0,
              value = c(0, 1000000),
              max = max(manager_survey$annual_salary),
              width = "100%"
            )
          ),
          plotOutput(outputId = "indiv_salary_plot", brush = "indiv_salary_brush"),
          tableOutput(outputId = "indiv_salary_table")
        ),
        tabPanel("Data", DT::dataTableOutput(outputId = "data"))
      )
    )
    
  )
)

# Define server function -------------------------------------------------------

server <- function(input, output, session) {
  
  # Print number of selected industries
  output$selected_industries <- reactive({
    paste("You've selected", length(input$industry), "industries.")
  })
  
  # Filter data for selected industries
  manager_survey_filtered <- reactive({
    manager_survey |>
      filter(industry_other %in% input$industry)
  })
  
  # Make a table of filtered data
  output$data <- DT::renderDataTable({
    manager_survey_filtered() |>
      select(
        industry,
        job_title,
        annual_salary,
        other_monetary_comp,
        country,
        overall_years_of_professional_experience,
        years_of_experience_in_field,
        highest_level_of_education_completed,
        gender,
        race
      )
  })
  
  # Futher filter for salary range
  observeEvent(input$industry, {
    updateSliderInput(
      inputId = "ylim",
      min = min(manager_survey_filtered()$annual_salary),
      max = max(manager_survey_filtered()$annual_salary),
      value = c(
        min(manager_survey_filtered()$annual_salary),
        max(manager_survey_filtered()$annual_salary)
      )
    )
  })
  
  # Plot of jittered salaries from filtered data
  output$indiv_salary_plot <- renderPlot({
    
    validate(
      need(length(input$industry) <= 8, "Please select a maxiumum of 8 industries.")
    )
    
    ggplot(
      manager_survey_filtered(),
      aes(
        x = highest_level_of_education_completed,
        y = annual_salary,
        color = industry
      )
    ) +
      geom_jitter(size = 2, alpha = 0.6) +
      theme_minimal(base_size = 16) +
      theme(legend.position = "top") +
      scale_color_colorblind() +
      scale_x_discrete(labels = label_wrap_gen(10)) +
      scale_y_continuous(
        limits = input$ylim,
        labels = label_dollar()
      ) +
      labs(
        x = "Highest level of education completed",
        y = "Annual salary",
        color = "Industry",
        title = "Individual salaries"
      )
  })
  
  # Linked brushing
  output$indiv_salary_table <- renderTable({
    brushedPoints(manager_survey_filtered(), input$indiv_salary_brush)
  })
  
  # Plot of average salaries from filtered data
  output$avg_salary_plot <- renderPlot({
    
    validate(
      need(length(input$industry) <= 8, "Please select a maxiumum of 8 industries.")
    )
    
    manager_survey_filtered() |>
      group_by(industry, highest_level_of_education_completed) |>
      summarise(
        mean_annual_salary = mean(annual_salary, na.rm = TRUE),
        .groups = "drop"
      ) |>
      ggplot(aes(
        x = highest_level_of_education_completed,
        y = mean_annual_salary,
        group = industry,
        color = industry
      )) +
      geom_line(linewidth = 1) +
      theme_minimal(base_size = 16) +
      theme(legend.position = "top") +
      scale_color_colorblind() +
      scale_x_discrete(labels = label_wrap_gen(10)) +
      scale_y_continuous(labels = label_dollar()) +
      labs(
        x = "Highest level of education completed",
        y = "Mean annual salary",
        color = "Industry",
        title = "Average salaries"
      )
  })
}


# Create the Shiny app object --------------------------------------------------

shinyApp(ui = ui, server = server)