
37 Bubble charts
theme_seinfeld()
Graphs in this chapter use fmggp2::theme_seinfeld(), built from Seinfeld: a comedy club brick background, a yellow frame and title, the yellow, red, blue, and green logo colors for marks, cream text, and an American Typewriter title.
37.1 Description
Bubble graphs display relationships between three numeric (continuous) variables.
The position on the x and y axis illustrates the relationship between the first two variables, and the size of the point (i.e., the bubble) conveys the numeric value of the third.
37.2 Set up
PACKAGES:
Install packages.
show/hide
library(ggplot2)
library(dplyr) # for tibble() and data manipulationDATA:
Seinfeld was famously a show about nothing, and every episode got a bump whenever Kramer slid through Jerry’s door. We’ll simulate 180 episodes across nine seasons (20 per season) with three numeric variables:
kramer_minutes: Kramer’s screen time, drawn from a right skewed gamma distribution withrgamma()laughs_per_min: the studio audience’s laughs per minute, which go up 0.3 for every minute of Kramer (plus noise fromrnorm())viewers: the audience in millions, which grows by about 3 million each season. We draw these on the log scale withrnorm()and convert them back withexp()
Setting the seed with set.seed() means you’ll get the same 180 episodes every time you run the code below.
show/hide
set.seed(1989) # same 180 episodes every time the code runs
seinfeld_eps <- tibble(
episode = 1:180, # 180 episodes, one row each
season = rep(1:9, each = 20) # nine seasons with 20 episodes each
) |>
mutate(
kramer_minutes = round(
rgamma(n(), shape = 6, rate = 1.5), # Kramer's screen time (centered near 4 minutes)
1 # one decimal place
),
laughs_per_min = round(
2 + 0.3 * kramer_minutes + # more Kramer means more laughs
rnorm(n(), mean = 0, sd = 0.5), # random episode to episode noise
1 # one decimal place
),
viewers = round(
exp(rnorm(n(), mean = log(10 + 3 * (season - 1)), # audience (millions) grows each season
sd = 0.15)), # random episode to episode noise
1 # one decimal place
)
)
glimpse(seinfeld_eps)
#> Rows: 180
#> Columns: 5
#> $ episode <int> 1, 2, 3, 4, 5, 6, 7, 8, 9…
#> $ season <int> 1, 1, 1, 1, 1, 1, 1, 1, 1…
#> $ kramer_minutes <dbl> 5.6, 5.6, 1.4, 2.7, 3.1, …
#> $ laughs_per_min <dbl> 3.8, 2.0, 2.4, 2.7, 3.0, …
#> $ viewers <dbl> 13.8, 10.6, 10.8, 8.2, 10…37.3 Distribution
A bubble chart adds a third variable to a scatter plot with the size of each point. Our eyes read the area of a bubble, not its width, so how a value gets turned into a size matters as much as the value itself. Below we’ll move from the viewers, to a picture, to the formula.
COUNT IT:
Start with the first five episodes.
show/hide
head(seinfeld_eps, 5)Below we summarise the viewers for each season. The mean grows each season, and so does the standard deviation, but their ratio (cv) stays close to 0.15.
show/hide
seinfeld_seasons <- seinfeld_eps |>
group_by(season) |>
summarise(mean_viewers = mean(viewers),
sd_viewers = sd(viewers),
cv = sd_viewers / mean_viewers)
seinfeld_seasonsDRAW IT:
The graph below draws three bubbles for 10, 20, and 40 million viewers, two ways. On the left, scale_size() makes the area of each bubble proportional to the value (so 40 million looks four times bigger than 10 million). On the right, scale_radius() makes the width proportional to the value, and 40 million looks sixteen times bigger than 10 million.
show/hide
three_bubbles <- tibble(x = 1:3, y = 1, viewers = c(10, 20, 40))
ggplot(three_bubbles, aes(x = x, y = y, size = viewers)) +
geom_point(color = "#F7C33C") +
scale_size(range = c(0, 20), limits = c(0, 40)) +
scale_x_continuous(limits = c(0.5, 3.5)) +
labs(title = "scale_size()", x = NULL, y = NULL) +
theme(legend.position = "none", axis.text = element_blank())
ggplot(three_bubbles, aes(x = x, y = y, size = viewers)) +
geom_point(color = "#F7C33C") +
scale_radius(range = c(0, 20), limits = c(0, 40)) +
scale_x_continuous(limits = c(0.5, 3.5)) +
labs(title = "scale_radius()", x = NULL, y = NULL) +
theme(legend.position = "none", axis.text = element_blank())

scale_size() is the default for size in ggplot2, and it’s the right choice for bubble charts.
NAME IT:
The viewers come from a lognormal distribution (normal on the log scale), with a median that grows each season:
\[ \log(\text{viewers}) \sim \text{Normal}\left(\log(10 + 3(\text{season} - 1)), \; 0.15\right) \]
On the log scale every season has the same spread (0.15), which is why the cv stays near 0.15 in every season: a lognormal variable wobbles by about the same percent, not the same number of viewers. When we map viewers to size, scale_size() sets each bubble’s area in proportion to its value:
\[ \text{area} \propto \text{viewers}, \quad \text{radius} \propto \sqrt{\text{viewers}} \]
A season nine episode with about 34 million viewers gets a bubble about 3.2 times the area of a season one episode (10 million), but only about 1.8 times as wide. Try changing sd = 0.15 to sd = 0.5 and the bubbles within each season range from tiny to huge.
37.4 Grammar
CODE:
Create labels with
labs()Map
kramer_minutestoxaxisMap
laughs_per_mintoyaxisMap
viewerstosizeSet
alphato1/3Add
scale_size(), set therangetoc(0.1, 10)andnameto"Viewers (millions)"Move the legend to the bottom of the graph with
theme(legend.position = "bottom")
show/hide
labs_bubble <- labs(
title = "Seinfeld episodes",
subtitle = "Kramer's screen time vs. laughs per minute",
caption = "Simulated data",
x = "Kramer screen time (minutes)", y = "Laughs per minute",
size = "Viewers (millions)")
ggp2_bubble <- seinfeld_eps |>
ggplot(
aes(x = kramer_minutes, y = laughs_per_min)) +
geom_point(aes(size = viewers), alpha = 1/3) +
scale_size(range = c(.1, 10),
name = "Viewers (millions)") +
theme(legend.position = "bottom")
ggp2_bubble +
labs_bubbleGRAPH:
