library(tidyverse)
theme_set(theme_light())ggplot2
36-315: Statistical Graphics and Visualization, Summer 2026
Pick graphs that are most appropriate for particular data types
Requires a working knowledge of different graph types and need to appropriately distinguish different data types
For any graph, need to know what information is visible vs hidden
Characterize distributions (visually and quantitatively)
Notice key attributes (e.g., center, shape, spread for 1D quantitative data) and choose graphs that showcase distributional characteristics (e.g., boxplots suck)
Choose graph specifications that showcase distribution quantities (e.g., binwidth, bandwidth)
Perform statistical inference to complement graphs
Validate what a graph shows to determine if that trend/difference is significant
Requires a working knowledge of statistical inference (e.g., tests, confidence intervals)
Interpret inferential results in context
MAKE HIGH-QUALITY GRAPHICS
Plot composition
Colors
Themes
Annotations
Animations
Interactive figures
cowplotExample: shot attempts by the Sabrina Ionescu in the 2023 WNBA season
Plot density curve and ECDF for shot distance together
ionescu_shots <- read_csv("https://raw.githubusercontent.com/qntkhvn/36-315-summer26/refs/heads/master/data/ionescu-shots.csv")
ionescu_shot_density <- ionescu_shots |>
ggplot(aes(x = shot_distance)) +
geom_density() +
geom_rug(alpha = 0.3) +
labs(x = "Shot distance (in feet)",
y = "Number of shot attempts")
ionescu_shot_ecdf <- ionescu_shots |>
ggplot(aes(x = shot_distance)) +
stat_ecdf() +
geom_rug(alpha = 0.3) +
labs(x = "Shot distance (in feet)",
y = "Proportion of Ionescu shot attempts")
library(cowplot)
plot_grid(ionescu_shot_density, ionescu_shot_ecdf, labels = c("A", "B"), label_size = 15)cowplotpatchworkionescu_shots <- read_csv("https://raw.githubusercontent.com/qntkhvn/36-315-summer26/refs/heads/master/data/ionescu-shots.csv")
ionescu_scoring_density <- ionescu_shots |>
ggplot(aes(x = shot_distance, color = scoring_play)) +
geom_density() +
geom_rug(alpha = 0.3) +
labs(x = "Shot distance (in feet)",
y = "Number of shot attempts")
ionescu_scoring_ecdf <- ionescu_shots |>
ggplot(aes(x = shot_distance, color = scoring_play)) +
stat_ecdf() +
geom_rug(alpha = 0.3) +
labs(x = "Shot distance (in feet)",
y = "Proportion of Ionescu shot attempts")
library(patchwork)
ionescu_scoring_density + ionescu_scoring_ecdf + plot_layout(guides = "collect")patchworkDiscrete: used to represent distinct categories with no ordering (nominal categorical). Colors should be distinct clearly with no single category emphasized unless intentionally highlighting a specific group
Sequential: used when values are mapped to a single color gradient (e.g. choropleth maps), or for an ordered categorical variable or low to high continuous variable
Divergent: used when data are centered around a meaningful midpoint and diverges in two directions from it (e.g., 0 for positive/negative values or 50% for percentages)
RSpecify a color in R using
color name (see all with colors())
hex code (see, e.g., here)
rgb(): build a color using a quantity of red, green, and blue (+ transparency)
ggplot2 colorscale_color_brewer()ggplot2 colorggplot2 themesink affects all foreground elements and parts of layers
paper affects all background elements and parts of layers
accent has niche application in geom_smooth() or geom_contour()
penguins |>
ggplot(aes(x = body_mass, y = bill_len, color = island)) +
geom_point(alpha = 0.5) +
facet_wrap(~ island) +
labs(
title = "Penguin Body Mass vs Bill Length",
x = "Body Mass (g)",
y = "Bill Length (mm)",
color = "Island"
) +
theme_bw(base_size = 18) +
theme(
# plot components
plot.title = element_text(face = "bold.italic", size = 20, hjust = 0.5),
axis.title = element_text(face = "bold"),
axis.text = element_text(color = "navy"),
axis.ticks = element_blank(),
# panel/grid components
panel.grid.major = element_line(color = "lightgray"),
panel.grid.minor = element_blank(),
panel.border = element_rect(color = "darkgray"),
# facet components
strip.background = element_rect(fill = "lightblue", color = NA),
strip.text = element_text(face = "bold"),
# legend components
legend.position = "bottom",
legend.title = element_text(face = "italic")
)And many more… (see here)
Default choices tend to treat each element with equal weight
Design plot with the visual hierarchy in mind
Make elements of that are more important look more important
Match visual weight to the focus of the graphic for communication
annotate() to manually add geoms (text, lines, rectangles, etc.)mtcars |>
ggplot(aes(x = wt, y = mpg)) +
geom_point() +
annotate(geom = "text", x = 4, y = 23, label = "Some text") +
annotate(geom = "rect", xmin = 3, xmax = 4, ymin = 15, ymax = 20, alpha = 0.2) +
annotate(geom = "curve", linewidth = 0.5, x = 4, y = 22, xend = 3.5, yend = 20, curvature = -0.3,
arrow = arrow(type = "closed", length = unit(10, "pt")))f1_constructors <- read_csv("https://raw.githubusercontent.com/qntkhvn/36-315-summer26/refs/heads/master/data/f1-constructors.csv") |>
filter(name %in% c("McLaren", "Renault", "Racing Point"), year == 2020)
f1_constructors |>
ggplot(aes(x = round, y = points, color = name)) +
geom_line(linewidth = 2) +
scale_x_continuous(breaks = seq(1, 17, 1)) +
labs(x = NULL,
y = "Accumulated points",
color = "Team",
title = "The race for third place in the 2020 F1 season")gganimatelibrary(gganimate)
f1_constructors |>
ggplot(aes(x = round, y = points, color = name)) +
geom_line(linewidth = 2) +
scale_x_continuous(breaks = seq(1, 17, 1)) +
labs(x = NULL,
y = "Accumulated points",
color = "Team",
title = "The race for third place in the 2020 F1 season") +
transition_reveal(round)gganimate conceptstransition_*(): defines how data should be spread out and how it relates to itself across time
view_*(): defines how positional scales should change along the animation
shadow_*(): defines how data from other points in time should be presented in the given point in time
enter_*()/exit_*(): defines how new data should appear and how old data should disappear
ease_aes(): defines how different aesthetics should be eased during transitions
Think about pace: speed of animation (too fast: hard to follow; too slow: boring and tedious)
Think about perplexity: amount of information; avoid information overload
Think about purpose: usefulness of animation (needed at all? providing additional value?)
Always make and describe the static version first
Never add animation just because it looks cool
When presenting, make sure to explain what is being displayed in the animation and
which elements should be emphasized
plotlyggplotly() to a static plotFigure 1: Corruption and human development. The most developed countries experience the least corruption. Data sources: Transparency International & UN Human Development Report.
Mastering Data Visualization with ggplot2 workshop at posit::conf(2025)
(featuring latest ggplot2 updates)