Software Tools for Data Analysis
STA 9750
Michael Weylandt
Week 9 – Tuesday 2026-11-03, Thursday 2026-10-29
Last Updated: 2026-10-29

STA 9750 Week 9

Today: Lecture #07: Advanced ggplot2 – maps, interactivity, animation

These slides can be found online at:

https://michael-weylandt.com/STA9750/slides/slides09.html

In-class activities can be found at:

https://michael-weylandt.com/STA9750/labs/lab07.html

Upcoming TODO

Upcoming student responsibilities - Tuesday Section:

Date Time Student Responsibility
2026-11-03 11:59pm ET Mid-Semester Teammate Peer Evaluations Due
2026-11-06 11:59pm ET Mini-Project #02 Due
2026-11-10 6:00pm ET Pre-Assignment #08 Due
2026-11-16 11:59pm ET Mini-Project Peer Feedback #02 Due
2026-11-17 6:00pm ET Pre-Assignment #09 Due
2026-11-24 6:00pm ET Pre-Assignment #10 Due
2026-11-27 11:59pm ET Mini-Project #03 Due
2026-12-01 6:00pm ET Final Project Presentation Slides Due

Upcoming TODO

Upcoming student responsibilities - Thursday Section:

Date Time Student Responsibility
2026-10-29 11:59pm ET Mid-Semester Teammate Peer Evaluations Due
2026-10-31 11:59pm ET Mini-Project Peer Feedback #01 Due
2026-11-05 6:00pm ET Pre-Assignment #08 Due
2026-11-06 11:59pm ET Mini-Project #02 Due
2026-11-12 6:00pm ET Pre-Assignment #09 Due
2026-11-16 11:59pm ET Mini-Project Peer Feedback #02 Due
2026-11-19 6:00pm ET Pre-Assignment #10 Due
2026-11-26
Thanksgiving

Other Important Dates

Other Important Dates:

Date Instructor Action
2026-11-04 Estimated Return of Peer Comments #01
2026-11-05 Mini-Project Released #03
2026-11-08 Estimated Return of Meta-Review Comments #01
2026-11-09 Mini-Project Peer Feedback Assigned #02
2026-11-19 Mini-Project Released #04
2026-11-20 Estimated Return of Peer Comments #02
2026-11-24 Estimated Return of Meta-Review Comments #02
2026-11-26 Thanksgiving #01

STA 9750 Week 9

Today: Lecture #07: Advanced ggplot2 – maps, interactivity, animation

  • Communicating Results (quarto) ✅
  • R Basics ✅
  • Data Manipulation in R ✅
  • Data Visualization in R ⬅️
    • Static Plots ✅
    • Interactivity, Maps, Animated Plots ⬅️
  • Getting Data into R
  • Statistical Modeling in R

Today

Today

  • Course Administration
  • Review Exercise
  • Advanced ggplot2
    • Spatial Data (Maps)
    • Animation
    • Interactivity
  • Wrap-Up
    • Life Tip of the Day

Administrative Business

STA 9750 Mini-Projects

Upcoming:

  • Mini-Project #01 (2026-10-16 at 11:59pm ET)
    • Submission ✅
    • Peer Feedback ⬅️
  • Mini-Project #02 (2026-11-06 at 11:59pm ET)
    • Submission ⬅️
    • Peer Feedback
  • Mini-Project #03 (2026-11-27 at 11:59pm ET)
  • Mini-Project #04 (2026-12-11 at 11:59pm ET)

Course Support

  • Synchronous - MW Office Hours 2x / week:
    • Tuesdays 5pm: Zoom
    • Thursdays 5pm: Zoom
  • Asynchronous: Teams

See Week 02 Slides for advice on asking good questions - Good questions get faster answers

Ask early for help with MPs - fixing problems is slow and iterative

Course Project

Course Project should be your main focus for rest of course

  • But you still need to do mini-projects and pre-assignments(!)

Course Project

Final submissions:

Notes added on integration of SQs and strategies for estimating causal effects. Ask questions! I’m happy to fill these out further.

Review Exercise

SSA Registered Births

Time series data of babies born and registered with the Social Security Administration (previously in Lab #05)

Rows: 7,305
Columns: 7
$ year        <dbl> 1969, 1969, 1969, 1969, 1969, 1969, 1969, 1969, 1969, 1969…
$ month       <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
$ day         <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,…
$ births      <dbl> 8486, 9002, 9542, 8960, 8390, 9560, 9738, 9734, 9434, 1004…
$ day_of_year <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,…
$ day_of_week <dbl> 3, 4, 5, 6, 7, 1, 2, 3, 4, 5, 6, 7, 1, 2, 3, 4, 5, 6, 7, 1…
$ id          <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,…

SSA Registered Births

Practice visualization:

  1. Are there months with more births?
  2. Are there days of the week with more births?
  3. Is there a long-term trend in births?

See the Review section of this week’s lab for details.

Breakout Rooms

Breakout Rooms
Please join the Breakout Room for your Project Team
Breakout #01: Beyond the Scale (T) + TeamTZ (R) Breakout #02: Table for One (T) + Zipliners (R)
Breakout #03: Degrees of Freedom (T) + Team Nexus (R) Breakout #04: CitiThatNeverSleeps (T) + Collide-o-Scope (R)
Breakout #05: Crude Awakening (T) + Emergency Care Bears (R) Breakout #06: Breathe Easy (T) + King Philip (R)
Breakout #07: Beat The Odds (T) + xG Analytics (R) Breakout #08: EconoMates (T) + NYC Housing Pulse (R)
Breakout #09: Team Rocket (T) + Unemployment Hub (R) Breakout #10: CUNY-naughts (T) + Data Vinci (R)

Go to your team’s breakout room. I will adjust / merge rooms based to ensure every room has enough people.

Advanced ggplot2

Spatial Data

Maps are more interesting than you think!

  • The world isn’t flat!

This can get intense, but we will get by with simple features (sf)

Map Projections

WGS 84

WGS 84 is a robust and widely-used way of creating maps from 3D coordinates.

  • Good default
  • Convert existing sf using sf::st_transform(4326)

Not universal!

NYC also has a bespoke projection optimized for local accuracy

sf Package

The sf package provides tools for dealing with spatial data:

  • st_read for reading geojson or shp files
  • st_join for combining spatial data
  • Integrates with ggplot2::geom_sf() for plotting

sf Objects

sf object - data.frame with additional geometry information

library(sf); library(tidyverse)
nc <- st_read(system.file("shape/nc.shp", package="sf"))
Reading layer `nc' from data source 
  `/Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/library/sf/shape/nc.shp' 
  using driver `ESRI Shapefile'
Simple feature collection with 100 features and 14 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: -84.32385 ymin: 33.88199 xmax: -75.45698 ymax: 36.58965
Geodetic CRS:  NAD27

sf Objects

glimpse(nc)
Rows: 100
Columns: 15
$ AREA      <dbl> 0.114, 0.061, 0.143, 0.070, 0.153, 0.097, 0.062, 0.091, 0.11…
$ PERIMETER <dbl> 1.442, 1.231, 1.630, 2.968, 2.206, 1.670, 1.547, 1.284, 1.42…
$ CNTY_     <dbl> 1825, 1827, 1828, 1831, 1832, 1833, 1834, 1835, 1836, 1837, …
$ CNTY_ID   <dbl> 1825, 1827, 1828, 1831, 1832, 1833, 1834, 1835, 1836, 1837, …
$ NAME      <chr> "Ashe", "Alleghany", "Surry", "Currituck", "Northampton", "H…
$ FIPS      <chr> "37009", "37005", "37171", "37053", "37131", "37091", "37029…
$ FIPSNO    <dbl> 37009, 37005, 37171, 37053, 37131, 37091, 37029, 37073, 3718…
$ CRESS_ID  <int> 5, 3, 86, 27, 66, 46, 15, 37, 93, 85, 17, 79, 39, 73, 91, 42…
$ BIR74     <dbl> 1091, 487, 3188, 508, 1421, 1452, 286, 420, 968, 1612, 1035,…
$ SID74     <dbl> 1, 0, 5, 1, 9, 7, 0, 0, 4, 1, 2, 16, 4, 4, 4, 18, 3, 4, 1, 1…
$ NWBIR74   <dbl> 10, 10, 208, 123, 1066, 954, 115, 254, 748, 160, 550, 1243, …
$ BIR79     <dbl> 1364, 542, 3616, 830, 1606, 1838, 350, 594, 1190, 2038, 1253…
$ SID79     <dbl> 0, 3, 6, 2, 3, 5, 2, 2, 2, 5, 2, 5, 4, 4, 6, 17, 4, 7, 1, 0,…
$ NWBIR79   <dbl> 19, 12, 260, 145, 1197, 1237, 139, 371, 844, 176, 597, 1369,…
$ geometry  <MULTIPOLYGON [°]> MULTIPOLYGON (((-81.47276 3..., MULTIPOLYGON ((…

This last row-geometry-is where the magic happens.

Map Visualization

sf integrates well with ggplot2:

  • geom_sf knows how to handle geometries
  • other features all work as expected (fill, animation, legends, etc.)
ggplot(nc, 
       aes(geometry=geometry)) + 
  geom_sf()

Chloropleths

A map whose fill depends on the value of interest is called a chloropleth:

ggplot(nc, aes(fill=BIR79 / BIR74 - 1, geometry=geometry)) + 
  geom_sf() + 
  guides(fill="none") + 
  ggtitle("% Change in Number of Children Born Per Year 1974-1979")

From the Greek for ‘color-abundance’ (“pleth” as in “plethora”)

Breakout Exercises

Two sets of spatial exercises:

  • Chloropleths
  • Cartograms

Animation

Animated Graphics

gganimate is the de facto standard for animated ggplot2:

  • Make a bunch of png files
  • Combine into a gif

Most commonly: +transition_time(VARIABLE)

NB: Unlike facet_wrap, no ~ before VARIABLE

transition_*() functions

Animation is implemented via a new plot element: transition_*():

library(gganimate)
ggplot(births, aes(x=day_of_year, y=births)) +
  geom_line() + 
  transition_time(year)

transition_*() functions

Add a title to indicate time being shown:

library(gganimate)
ggplot(births, aes(x=day_of_year, y=births)) +
  geom_line() + 
  transition_time(year) + ggtitle("Year: {frame_time}")

transition_*() functions

gganimate “tweens” (interpolates) between frames for smoothness - works best for continuous time data.

It’s a bit weird here and we might prefer a more discrete interpolation:

library(gganimate)
ggplot(births, aes(x=day_of_year, y=births)) +
  geom_line() + 
  transition_states(year) + ggtitle("Year: {closest_state}")

Easing

Use the ease_ functions (or enter_()/exit_*() separately) for finer control:

ggplot(births, aes(x=day_of_year, y=births, group=year)) +
  geom_line() + transition_states(year, transition_length = 4, state_length=1) + 
  ggtitle("Year: {closest_state}") + enter_grow() + exit_fade()

Grouping

The group variable is used to determine whether something is permanent over time.

penguins |> drop_na() |>
  ggplot(aes(x = bill_dep, y = body_mass)) +
  geom_point() + ggtitle("{frame_time}") + 
  transition_time(year)

Bad! Doesn’t make sense to interpolate penguins

Grouping

penguins |> drop_na() |>
  ggplot(aes(x = bill_dep, y = body_mass, group=year)) +
  geom_point() + ggtitle("{floor(frame_time)}") + 
  transition_time(year)

See gganimate Getting Started Documentation for more

Breakout Exercises

Animation exercises:

  • gapminder data
  • How has the world gotten healthier and richer over the past century?

Animated Visualizations in this week’s Lab

Animated Graphics

When it works, gganimate is great

  • PITA when external software is busted

Alternatives:

  • Facet plots
  • Split facets over pages and scroll quickly: ggforce::facet_wrap_paginate()
  • Interactivity with autoplay

Caution: think about how transitions should be structured

Interactivity

Interactivity

The ggiraph package provides for interactive ggplot2

  • Principally focused on tooltips (mouseover) and simple click actions

Not full dashboards - those come next

Basic workflow:

  • Replace ‘standard’ geom with interactive alternatives (geom_point() to geom_point_interactive())
  • Add tooltip or data_id aesthetics
  • run through ggiraph::giraph() before rendering

ggiraph

library(tidyverse); library(ggiraph)
g <- ggplot(penguins, 
       aes(x=flipper_len, y=body_mass, color=species, tooltip=species)) + 
  geom_point_interactive()
girafe(g)

Here tooltip specifies mouseover message

ggiraph

Use standard ggplot2 customization and extensions:

g <- g + theme_bw() + scale_color_brewer(name="Species", palette=2, type="qual") + 
         xlab("Flipper Length (mm)") + ylab("Body Mass (g)") + theme(legend.position="bottom")
girafe(g)

ggiraph

To ‘correlate’ elements, use data_id:

g <- ggplot(penguins, 
       aes(x=flipper_len, y=body_mass, 
           color=species, tooltip=species, data_id=species)) + 
  geom_point_interactive()
girafe(g)

ggiraph

Can even link across facets:

girafe(g + facet_wrap(~island))

ggiraph

Or even more complex panels (in conjunction with patchwork)

library(patchwork) # Lets us 'add' plots together
g_basic <- ggplot(penguins, aes(color=species, tooltip=species, data_id=species))
g1 <- g_basic + geom_point_interactive(aes(x=body_mass, y=flipper_len))
g2 <- g_basic + geom_point_interactive(aes(x=bill_dep, y=bill_len))
girafe(g1 + g2)

ggiraph

With a bit of custom Javascript (beyond this class), can have custom click events:

Breakout Exercises

Interactivity exercises:

  • Adding interactivity to our MTA maps
  • Linking interactive elements across multiple plots with data_id and patchwork

Mouseover Visualizations in this week’s Lab

Alternatives

The plotly package provides a ggplotly() function which works similarly.

Advantages:

  • Has a more sophisticated “time change” interactivity

Disadvantages

  • Doesn’t support all of ggplot2
  • ggiraph works well with (essentially) all ggplot2 features

Alternatives

ggplotly - frame gives rise to a time slider:

library(plotly)
G <- ggplot(births, aes(x=day_of_year, y=births, frame=year)) +
  geom_point() + geom_line() + theme_bw()
ggplotly(G)

Shiny

Shiny

The shiny framework provides for reactive programming

  • The system ‘waits’ for user input and responds accordingly, updating (Think Excel)

Shiny

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| echo: false
#| viewerHeight: 420
library(shiny)
library(shinylive)
library(ggplot2)

ui <- fluidPage(
  titlePanel("Interactive T-Test Power Calculator"),
  
  sidebarLayout(
    sidebarPanel(
      numericInput("delta", "True Difference in Means (delta):", value = 0.5, step = 0.1),
      numericInput("sd", "Standard Deviation (sd):", value = 1.0, min = 0.1, step = 0.1),
      numericInput("n", "Sample Size per Group (n):", value = 30, min = 2, step = 1),
      numericInput("sig.level", "Significance Level (alpha):", value = 0.05, min = 0.001, max = 0.2, step = 0.01),
      selectInput("alternative", "Alternative Hypothesis:", 
                  choices = c("Two-Sided" = "two.sided", "One-Sided" = "one.sided")),
      selectInput("type", "Test Type:", 
                  choices = c("Two-Sample" = "two.sample", "One-Sample" = "one.sample", "Paired" = "paired"))
    ),
    
    mainPanel(
      h4(textOutput("powerText")),
      hr(),
      plotOutput("powerPlot")
    )
  )
)

server <- function(input, output) {
  
  power_res <- reactive({
    power.t.test(
      n = input$n,
      delta = input$delta,
      sd = input$sd,
      sig.level = input$sig.level,
      alternative = input$alternative,
      type = input$type
    )
  })
  
  output$powerText <- renderText({
    res <- power_res()
    paste0("Estimated Statistical Power (1 - \u03b2): ", round(res$power * 100, 2), "%")
  })
  
  output$powerPlot <- renderPlot({
    max_n <- max(100, input$n * 2)
    n_seq <- seq(max(2, input$n %/% 2), max_n, by = 1)
    
    power_vals <- sapply(n_seq, function(n_val) {
      power.t.test(
        n = n_val,
        delta = input$delta,
        sd = input$sd,
        sig.level = input$sig.level,
        alternative = input$alternative,
        type = input$type
      )$power
    })
    
    df <- data.frame(n = n_seq, power = power_vals)
    current_power <- power_res()$power
    
    ggplot(df, aes(x = n, y = power)) +
      geom_line(color = "steelblue", linewidth = 1.2) +
      geom_point(aes(x = input$n, y = current_power), 
                 color = "firebrick", 
                 size = 4) +
      geom_hline(yintercept = 0.80, 
                 linetype = "dashed", 
                 color = "gray50", linewidth = 0.8) +
      annotate("text", x = max(n_seq) * 0.75, y = 0.82, 
               label = "Target Power (80%)", color = "gray40",
               fontface = "italic") +
      labs(
        title = "Power Curve across Sample Sizes",
        subtitle = "Red dot highlights your current sample size selection",
        x = "Sample Size per Group (n)",
        y = "Statistical Power (1 - \u03b2)"
      ) +
      theme_minimal(base_size = 14) +
      ylim(0, 1)
  })
}

shinyApp(ui = ui, server = server)

Shiny

New Concepts:

  • ui: “front-end” specifies user-facing behavior
  • server: “back-end” performs calculations in response to ui input
  • *Input elements: takes user input and binds to a reactive R variable
  • reactive blocks: code that should be re-run in response to ui changes

Example - UI

UI from Previous Example:

ui <- fluidPage(
  titlePanel("Interactive T-Test Power Calculator"),
  
  sidebarLayout(
    sidebarPanel(
      numericInput("delta", "True Difference in Means (delta):", value = 0.5, step = 0.1),
      numericInput("sd", "Standard Deviation (sd):", value = 1.0, min = 0.1, step = 0.1),
      numericInput("n", "Sample Size per Group (n):", value = 30, min = 2, step = 1),
      numericInput("sig.level", "Significance Level (alpha):", value = 0.05, min = 0.001, max = 0.2, step = 0.01),
      selectInput("alternative", "Alternative Hypothesis:", 
                  choices = c("Two-Sided" = "two.sided", "One-Sided" = "one.sided")),
      selectInput("type", "Test Type:", 
                  choices = c("Two-Sample" = "two.sample", "One-Sample" = "one.sample", "Paired" = "paired"))
    ),
    
    mainPanel(
      h4(textOutput("powerText")),
      hr(),
      plotOutput("powerPlot")
    )
  )
)

Example - Server

server <- function(input, output) {
  
  power_res <- reactive({
    power.t.test(
      n = input$n,
      delta = input$delta,
      sd = input$sd,
      sig.level = input$sig.level,
      alternative = input$alternative,
      type = input$type
    )
  })
  
  output$powerText <- renderText({
    res <- power_res()
    paste0("Estimated Statistical Power (1 - \u03b2): ", round(res$power * 100, 2), "%")
  })
  
  output$powerPlot <- renderPlot({
    max_n <- max(100, input$n * 2)
    n_seq <- seq(max(2, input$n %/% 2), max_n, by = 1)
    
    power_vals <- sapply(n_seq, function(n_val) {
      power.t.test(
        n = n_val,
        delta = input$delta,
        sd = input$sd,
        sig.level = input$sig.level,
        alternative = input$alternative,
        type = input$type
      )$power
    })
    
    df <- data.frame(n = n_seq, power = power_vals)
    current_power <- power_res()$power
    
    ggplot(df, aes(x = n, y = power)) +
      geom_line(color = "steelblue", linewidth = 1.2) +
      geom_point(aes(x = input$n, y = current_power), color = "firebrick", size = 4) +
      geom_hline(yintercept = 0.80, linetype = "dashed", color = "gray50", linewidth = 0.8) +
      annotate("text", x = max(n_seq) * 0.75, y = 0.82, label = "Target Power (80%)", color = "gray40", fontface = "italic") +
      labs(
        title = "Power Curve across Sample Sizes",
        subtitle = "Red dot highlights your current sample size selection",
        x = "Sample Size per Group (n)",
        y = "Statistical Power (1 - \u03b2)"
      ) +
      theme_minimal(base_size = 14) +
      ylim(0, 1)
  })
}

Example - Combining

library(shiny)
library(tidyverse)

shinyApp(ui = ui, server = server)

Plotting FAQs

ggplot2 & Pie Charts

Why do Pie Charts have a bad reputation?

  • Use of area and angle over length: less accurate perception
  • Depends on fill to convey category - limited categories

But primarily - “insider smugness” and hate of Excel

ggplot2 Plot Type Choice

For me:

  • Exploratory mode:
    • Simple: line, scatter, bar, frequency
  • Publication mode:
    • Very context specific
    • “Simplicity” / “Elegance” depend on audience expectations

ggplot2 Font Sizing

Theme machinery!

ggplot(iris, aes(x=Sepal.Length, y=Sepal.Width)) + 
    geom_point() + theme(axis.text = element_text(size=24))

Overplotting / ScatterBlobs

Student asked about “scatterblobs” - typo(?) but I love it!

  • Density based plotting: hexbins, histograms, rugplots
  • Data reduction: summarization or sub-sampling

Optimizing ggplot2 Performance

Active project of ggplot2 team - not much you can do

Practical advice:

ggplot2 Beyond Scatter and Line

Some favorite semi-advanced plot types:

  • Violin plots: combination of boxplot and histogram
  • Ridgelines
  • Beeswarms

Deep rabbit hole

ggplot2 + High-Dimensional Data

High-dimensional data: measure many variables per observation (“wide”)

High-dimensional data is hard to visualize

Approaches:

  • Pair plots for “moderate” HDD
  • PCA (or similar dimension reduction. Take 9890!)

Custom ggplot2 Theme

my_theme <- theme_bw() + theme(panel.background = element_rect(fill = 'lightblue'))
ggplot(iris, aes(x=Sepal.Length, y = Sepal.Width)) + geom_point() + my_theme

Advanced:

  • theme_set() - change ggplot2 defaults for that session
  • .Rprofile - set code to run every time you start R

ggplot2 - When Not to Use

ggplot2 is designed to make good statistical graphics. Sub-par for:

  • Very advanced interactivity (unless combined with shiny or custom JS)
  • Really big data
  • Hardcore customization / “infographics”

git WTF

Reference: Happy Git with R

Wrap-Up

Review

Advanced ggplot2:

  • ggplot2 as a platform for powerful extensions
  • Spatial data: sf
  • Animation: gganimate
  • Interactivity: ggiraph, shiny

Wrap-Up

Orientation

  • Communicating Results (quarto) ✅
  • R Basics ✅
  • Data Manipulation in R ✅
  • Data Visualization in R ✅
    • Static Plots ✅
    • Interactivity, Maps, Animated Plots ✅
  • Getting Data into R
  • Statistical Modeling in R

Next Time

  • Communicating Results (quarto) ✅
  • R Basics ✅
  • Data Manipulation in R ✅
  • Data Visualization in R ✅
  • Getting Data into R ⬅️
    • Files and APIs ⬅️
    • Web Scraping
    • Cleaning and Processing Text
  • Statistical Modeling in R

Pre-Assignment: Structure of the File System and of HTTP

Next Time

Musical Treat