
32 Stream plots
theme_hypercolor()
Graphs in this chapter use fmggp2::theme_hypercolor(), built from Generra Hypercolor shirts: a deep purple background, a magenta frame, heat changing magenta, orange, purple, blue, green, and yellow marks, and a Gill Sans title.
32.1 Description
A stream plot shows how a number changes over time for each group,1 with the groups put on top of each other. The number most often goes up the y axis, and time goes along the x axis.2 Stream plots are good when the number goes up and down a lot over time (or isn’t always there).3
Each group gets its own color, and the size of its colored part shows how its y value changes.4 In ggplot2, we can make stream plots with ggstream.
32.2 Set up
PACKAGES:
Bring in the code we’ll use.5
show/hide
remotes::install_github("davidsjoberg/ggstream")
library(ggstream)
library(ggplot2)
library(dplyr) # for data manipulation
library(tidyr) # for expand_grid()DATA:
A fad is a thing that a lot of people want for a short time. The 1990s were ten years of fads that showed up in a day and went away just as fast. Schools would not let kids wear slap bracelets, Hypercolor tops changed color when you touched them, and you saw Koosh balls and scrunchies in every place. A thousand is 1,000. We’ll make up6 the sales for each month (in thousands) for four fads from 1990 through 1999. Each fad in fads gets a peak (the year most people wanted it), a spread (how long it lasted, in years),7 and a total (how many it sold in all). A hill is a line that is high in the middle and low on both sides. The mean sales for each month (avg_sales) follow a hill around the peak,8 and rpois() picks the real sales for each month.9 Starting with set.seed()10 means you’ll get the same 120 months of sales every time you run the code below.
show/hide
fads <- tibble(
fad = c("Slap bracelets", "Hypercolor", "Koosh balls", "Scrunchies"), # four early 90s fads
peak = c(1990.8, 1991.5, 1992.5, 1994), # when each fad peaked
spread = c(0.4, 0.6, 1.5, 2.2), # how long each fad lasted (in years)
total = c(3000, 4000, 5000, 9000) # total sales over the decade
)
set.seed(1991) # same monthly sales every time the code runs
fad_sales <- expand_grid(
month = seq(as.Date("1990-01-01"), as.Date("1999-12-01"), by = "month"), # 120 months
fads # every fad in every month
) |>
mutate(
fad = factor(fad, levels = fads$fad), # keep the order above
t = lubridate::year(month) + (lubridate::month(month) - 1) / 12, # month as a decimal year
avg_sales = total / 12 * dnorm(t, mean = peak, sd = spread), # bell shaped rise and fall
sales = rpois(n(), lambda = avg_sales) # monthly sales around the curve
) |>
select(month, fad, avg_sales, sales)
glimpse(fad_sales)
#> Rows: 480
#> Columns: 4
#> $ month <date> 1990-01-01, 1990-01-01, 1990-…
#> $ fad <fct> Slap bracelets, Hypercolor, Ko…
#> $ avg_sales <dbl> 33.744354, 9.737945, 27.632539…
#> $ sales <int> 44, 11, 32, 24, 51, 18, 24, 22…32.3 Distribution
A stream plot puts each group’s numbers on top of each other11 and lets them run like water over time. Each fad’s colored part has its own form: a short up and down for fads that ended fast, and a long, low hill for fads that stayed around. Below we’ll move from the sales for each month, to a graph, to the rule behind it,12 and see where those forms come from.
COUNT IT:
Start with the Hypercolor sales for the first six months of 1991.
show/hide
fad_sales |>
filter(fad == "Hypercolor", month >= as.Date("1991-01-01")) |>
head(6)From one month to the next the sales go up and down a little, but over these six months they go up, just like avg_sales. Below we add up each fad’s sales and find its best month.
show/hide
fad_peaks <- fad_sales |>
group_by(fad) |>
summarise(total_sales = sum(sales),
best_month = month[which.max(sales)],
best_sales = max(sales))
fad_peaksScrunchies sold the most in all (8,748 thousand), but slap bracelets had the biggest single month (255 thousand). Selling the most and having the highest month are two different things.
DRAW IT:
The graph below shows each fad’s sales for each month as points and its avg_sales as a line. Every fad follows the same form of hill. Slap bracelets and Hypercolor have high hills that are not very wide, and scrunchies have a wide, low one.
show/hide
ggplot(fad_sales, aes(x = month, color = fad)) +
geom_point(aes(y = sales), alpha = 1 / 2, size = 1) +
geom_line(aes(y = avg_sales), linewidth = 1) +
facet_wrap(~ fad, ncol = 2) +
labs(x = "Month", y = "Sales (thousands)") +
theme(legend.position = "none")
A stream plot puts these four hills on top of each other (and puts the middle of the colored parts at 0),13 which is why the stream is widest in the early 1990s and gets small by 1999.
NAME IT:
The sales for each month are whole numbers that land near a mean,14 and the mean for each month follows the form of a hill over time:15
\[ \text{sales}_t \sim \text{Poisson}(\lambda_t), \quad \lambda_t = \frac{\text{total}}{12} \times \frac{1}{\text{spread}\sqrt{2\pi}} \, e^{-\frac{(t - \text{peak})^2}{2 \, \text{spread}^2}} \]
Here we only use the hill for its form (it shows when the sales happen, not a number picked by chance).16 The space under the whole hill adds up to the total,17 so the high point is \(\frac{\text{total}}{12 \times \text{spread} \sqrt{2\pi}}\). Our ten years cut off the ends of the hills, so the avg_sales we keep add up to a little less (2,947 of the 3,000 for slap bracelets). Slap bracelets sold 3000 thousand over a spread of 0.4 years, so their high point is about 249 thousand a month. Scrunchies sold three times as much, but over a spread of 2.2 years, so their high point is only about 136 thousand a month. Try changing the spread for scrunchies to 0.5 and watch their colored part turn into a high point that comes and goes fast.
32.4 Grammar
CODE:
Make the words around the graph18 with
labs()- Use
paste0()in thesubtitleso the years change by themselves whenfad_saleschanges
- Use
Start the graph with
ggplot()and give itdataPut
monthon thexaxis andsaleson theyaxis, and give eachfadits own color withfill19Add
geom_stream()20Finally, move the key21 under the graph with
theme(legend.position = "bottom"), and take away the numbers on theyaxis withaxis.text.y = element_blank(). The stream is on both sides of 0, so half of those numbers would be below 0, even though no fad sold less than nothing.
show/hide
labs_stream <- labs(
title = "90s fads",
caption = "Simulated data",
subtitle =
paste0("Monthly sales between ",
format(min(fad_sales$month), "%Y"),
" and ",
format(max(fad_sales$month), "%Y")),
x = "Month",
y = "Sales (thousands)",
fill = "Fad")
ggp2_stream <- ggplot(data = fad_sales,
mapping = aes(x = month,
y = sales,
fill = fad)) +
ggstream::geom_stream()
ggp2_stream +
labs_stream +
theme(legend.position = "bottom",
axis.text.y = element_blank())GRAPH:

32.5 More info
ggstream has many things we can set22 for changing the form and look of each group (and the words on them).
32.5.1 Type
Give each
fadits own color withfill(insideforcats::fct_rev())23We can change the look of the graph with
typeingeom_stream()type: changetypefrom"mirror"(what it uses if we don’t say)24 to"proportional"
Change the colors:
scale_fill_manual(): givevaluesa list of colors, with a name for each one25
Finally, add the
fillto the words around the graph and move the key under the graph withtheme(legend.position = "bottom")
show/hide
fad_colors <- c("Slap bracelets" = "#FFE135",
"Hypercolor" = "#E0218A",
"Koosh balls" = "#2DBE60",
"Scrunchies" = "#1F9BFF")
ggp2_stream_prp <- ggplot(data = fad_sales,
mapping = aes(x = month,
y = sales,
fill = forcats::fct_rev(fad))) +
ggstream::geom_stream(type = "proportional") +
scale_y_continuous(labels = scales::percent) +
scale_fill_manual(values = fad_colors)
ggp2_stream_prp +
labs_stream +
labs(y = "Share of sales", fill = "Fad") +
theme(legend.position = "bottom")
The "proportional" stream shows each fad’s part of the sales in each month.26 Scrunchies take up the whole stream by the end of the 1990s, because their wide hill is the last one still selling.
32.5.2 Sorting
To change the order of the colored parts,27 set sorting to "none", "onset", or "inside_out"
sorting: setsortingto"inside_out"in bothggstream::geom_stream()andggstream::geom_stream_label()type: changetypeto"ridge"in bothggstream::geom_stream()andggstream::geom_stream_label()
We can also put words on the colored parts with ggstream::geom_stream_label():28
Join
fad(insideforcats::fct_rev()) tolabelinggplot(), so every part of the graph uses it29Inside
ggstream::geom_stream_label():join
fadtocolorand setsizeto3set
n_gridto1000(if we don’t, it uses100points,30 which is fewer than our 120 months and makes R stop without making the graph)Keep the words out of the key with
show.legend = FALSE(the key for the colors stays)
Colors:
Use
scale_fill_manual()to set the colors by hand fromfad_colors, which has a name for each colorUse
scale_color_manual()to make the words white, but dark on the light color of the slap bracelets so we can read themFinally, add the
fillto the words around the graph and move the key under the graph withtheme(legend.position = "bottom")
show/hide
ggp2_stream_srt <- ggplot(data = fad_sales,
mapping = aes(x = month,
y = sales,
fill = forcats::fct_rev(fad),
label = forcats::fct_rev(fad))) +
ggstream::geom_stream(
type = "ridge",
sorting = "inside_out") +
ggstream::geom_stream_label(
type = "ridge",
mapping = aes(color = forcats::fct_rev(fad)),
sorting = "inside_out",
size = 3,
n_grid = 1000,
show.legend = FALSE) +
scale_fill_manual(values = fad_colors) +
scale_color_manual(values = c("Slap bracelets" = "#2A0A3D",
"Hypercolor" = "#FDF6FF",
"Koosh balls" = "#FDF6FF",
"Scrunchies" = "#FDF6FF"))
ggp2_stream_srt +
labs_stream +
labs(fill = "Fad") +
theme(legend.position = "bottom")
The levels of a categorical variable.↩︎
A numeric (continuous) variable, typically on the
yaxis, measured over time on thexaxis.↩︎When the numeric value varies wildly (or isn’t always present) over the time measurement.↩︎
Categorical groups are shown as color layers, and the area of each layer represents the change in the
yvalue.↩︎Install and load the packages.↩︎
Simulate.↩︎
spreadis the standard deviation (sd) of the bell shaped curve, in years.↩︎A bell shaped (normal) curve, scaled by
total / 12.↩︎rpois()draws each month’s sales from a Poisson distribution withlambda = avg_sales.↩︎Setting the random seed.↩︎
Stacks each group’s values.↩︎
The formula.↩︎
Centers the stack around 0 (
type = "mirror", the default).↩︎Poisson counts.↩︎
A normal distribution.↩︎
The normal curve describes when the sales happen; it isn’t a random variable here.↩︎
The area under the curve.↩︎
Labels.↩︎
Map
monthtox,salestoy, andfadto thefillaesthetic.↩︎Add the
geom_stream()layer.↩︎The legend.↩︎
Arguments.↩︎
forcats::fct_rev()reverses the order of the factor levels.↩︎The default.↩︎
A named vector.↩︎
Each fad’s share (proportion).↩︎
How the categorical areas are sorted.↩︎
Text labels.↩︎
Map
fadtolabelglobally, inggplot().↩︎The default grid of
100points.↩︎