
11 Heatmaps
theme_freshprince()
Graphs in this chapter use fmggp2::theme_freshprince(), modeled on The Fresh Prince of Bel-Air logo: a pink glow background, a graffiti green frame and title, green, teal, lime, and olive marks, and an Arial Black title. Continuous fills run from teal to lime.
11.1 Description
Heatmaps are color graphs that show data as a matrix with categories on the x and y axes. Each cell’s color corresponds to its value. They are useful for showing magnitude in two dimensions and often include a color scale. The intersecting cells contain variations of color saturation (i.e., the grade of purity or vividness) to represent the numerical values between groups.
Heatmap legends should be positioned on top or bottom and justified horizontally to preserve shape and improve readability.
11.2 Set up
PACKAGES:
Install packages.
show/hide
library(ggplot2)
library(dplyr) # for tibble() and data manipulation
library(tidyr) # for expand_grid()DATA:
The Fresh Prince of Bel-Air ran for six seasons (1990 to 1996) and leaned on a handful of running gags. We’ll simulate whether each gag shows up in each of the 24 episodes per season. Every gag gets a starting chance of appearing (base) and a trend that makes it more or less common as the show goes on.
plogis() turns those numbers into a probability between 0 and 1, and rbinom() flips a weighted coin for every gag in every episode (present is 1 when the gag appears). Then we count the episodes with each gag in each season (occurrences). Setting the seed with set.seed() means you’ll get the same 144 episodes every time you run the code below.
show/hide
set.seed(1990) # same episodes every time the code runs
fp_gags <- tibble(
gag = c("Will flirts", "Phil yells at Will", "Geoffrey zinger", # six running gags
"Jazz thrown out", "Hilary goes shopping", "Carlton dance"),
base = c(1.5, 1.2, 0.8, -0.4, -0.2, -2.2), # how common each gag is (higher is more common)
trend = c(-0.3, -0.1, 0, 0.2, 0, 0.5) # change per season (positive gags grow)
)
fp_episodes <- fp_gags |>
tidyr::expand_grid(season = 1:6, episode = 1:24) |> # every gag in every episode
mutate(
p = plogis(base + trend * (season - 3.5)), # chance the gag shows up in that episode
present = rbinom(n(), size = 1, prob = p) # 1 = the gag happened, 0 = it didn't
)
fp_heatmap <- fp_episodes |>
group_by(season, gag) |> # one group per gag per season
summarise(occurrences = sum(present), .groups = "drop") # episodes with the gag
glimpse(fp_heatmap)
#> Rows: 36
#> Columns: 3
#> $ season <int> 1, 1, 1, 1, 1, 1, 2, 2, 2, 2…
#> $ gag <chr> "Carlton dance", "Geoffrey z…
#> $ occurrences <int> 1, 18, 7, 8, 20, 22, 0, 17, …11.3 Distribution
Every tile in a heatmap is a count, and every count here is built from 24 yes or no outcomes. A gag that appears in most episodes produces bright tiles in every season, and a gag whose chances grow over time produces tiles that brighten from left to right. Below we’ll move from the episodes, to a picture, to the formula.
COUNT IT:
Start with the first four episodes of season 1.
show/hide
fp_episodes |>
filter(season == 1, episode <= 4) |>
select(gag, episode, p, present) |>
arrange(episode)Each row is one coin flip, and p is the chance the coin comes up 1. Will flirts in almost every episode (p is above 0.9), while the Carlton dance has only about a 3% chance in season 1. Now compare the counts for those two gags across all six seasons with the counts we’d expect (24 episodes times p).
show/hide
fp_episodes |>
filter(gag %in% c("Will flirts", "Carlton dance")) |>
group_by(gag, season) |>
summarise(occurrences = sum(present),
expected = round(24 * first(p), 1),
.groups = "drop")The Carlton dance climbs from almost never to a few times a season, while Will flirts in nearly every episode no matter the season.
DRAW IT:
Below are two heatmaps. The first shows the expected counts (24 times p), and the second shows what our simulation actually produced. The patterns match: Will’s flirting stays bright in every season, and the Carlton dance slowly lights up from left to right. The simulated heatmap is a little patchier because each tile is only 24 coin flips.
show/hide
fp_episodes |>
distinct(gag, season, p) |>
ggplot(aes(x = season, y = gag, fill = 24 * p)) +
geom_tile() +
scale_fill_continuous(limits = c(0, 24)) +
labs(title = "Expected", x = "Season", y = NULL, fill = "Episodes") +
theme(legend.position = "bottom")
ggplot(fp_heatmap, aes(x = season, y = gag, fill = occurrences)) +
geom_tile() +
scale_fill_continuous(limits = c(0, 24)) +
labs(title = "Simulated", x = "Season", y = NULL, fill = "Episodes") +
theme(legend.position = "bottom")

NAME IT:
Each gag in each episode is a Bernoulli trial (a single yes or no outcome) with probability \(p\). Adding up 24 independent trials gives a binomial distribution:
\[ \text{occurrences} \sim \text{Binomial}(n = 24, \; p_{\text{gag, season}}) \]
The expected count is \(24p\), and the standard deviation is \(\sqrt{24p(1 - p)}\). That spread is largest when \(p = 0.5\) (about 2.4 episodes) and shrinks toward zero when a gag almost always or almost never appears. That’s why the Will flirts row looks so steady while the middle rows flicker from season to season.
The probability itself comes from plogis(), which turns base + trend * (season - 3.5) into a number between 0 and 1. A positive trend (like the Carlton dance’s 0.5) makes a gag more likely every season. Try giving “Jazz thrown out” a negative trend and watch his row fade to teal from left to right.
11.4 Grammar
CODE:
Create labels with
labs()Initialize the graph with
ggplot()and providedataAssign
seasontox,gagtoy, andoccurrencestofillAdd the
geom_tile()Move the legend to the bottom with
theme(legend.position = "bottom")
show/hide
labs_heatmap_tile <- labs(
title = "The Fresh Prince of Bel-Air",
caption = "Simulated data",
subtitle = "Running gags per season (24 episodes each)",
x = "Season",
y = "Running gag",
fill = "Episodes")
ggp2_heatmap_tile <- ggplot(data = fp_heatmap,
aes(x = season,
y = gag,
fill = occurrences)) +
geom_tile() +
theme(legend.position = "bottom")
ggp2_heatmap_tile +
labs_heatmap_tileGRAPH:

11.5 More info
In addition to geom_tile(), heatmaps can also be created with the geom_raster() function.
11.5.1 geom_raster()
Create labels with
labs()Initialize the graph with
ggplot()and providedataAssign
seasontox,gagtoy, andoccurrencesto fillAdd the
geom_raster()Move the legend to the bottom with
theme(legend.position = "bottom")
show/hide
labs_heatmap_raster <- labs(
title = "The Fresh Prince of Bel-Air",
caption = "Simulated data",
subtitle = "Running gags per season (24 episodes each)",
x = "Season",
y = "Running gag",
fill = "Episodes")
ggp2_heatmap_raster <- ggplot(data = fp_heatmap,
aes(x = season,
y = gag,
fill = occurrences)) +
geom_raster() +
theme(legend.position = "bottom")
ggp2_heatmap_raster +
labs_heatmap_raster