32  Stream plots

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.


Requirements:

✅ a categorical variable

✅ a numeric (continuous) variable

✅ a numeric (date) variable

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_peaks

Scrunchies 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 the subtitle so the years change by themselves when fad_sales changes
  • Start the graph with ggplot() and give it data

  • Put month on the x axis and sales on the y axis, and give each fad its own color with fill19

  • Add geom_stream()20

  • Finally, move the key21 under the graph with theme(legend.position = "bottom"), and take away the numbers on the y axis with axis.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 fad its own color with fill (inside forcats::fct_rev())23

  • We can change the look of the graph with type in geom_stream()

    • type: change type from "mirror" (what it uses if we don’t say)24 to "proportional"
  • Change the colors:

    • scale_fill_manual(): give values a list of colors, with a name for each one25
  • Finally, add the fill to the words around the graph and move the key under the graph with theme(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: set sorting to "inside_out" in both ggstream::geom_stream() and ggstream::geom_stream_label()

  • type: change type to "ridge" in both ggstream::geom_stream() and ggstream::geom_stream_label()

We can also put words on the colored parts with ggstream::geom_stream_label():28

  • Join fad (inside forcats::fct_rev()) to label in ggplot(), so every part of the graph uses it29

  • Inside ggstream::geom_stream_label():

    • join fad to color and set size to 3

    • set n_grid to 1000 (if we don’t, it uses 100 points,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 from fad_colors, which has a name for each color

  • Use scale_color_manual() to make the words white, but dark on the light color of the slap bracelets so we can read them

  • Finally, add the fill to the words around the graph and move the key under the graph with theme(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")


  1. The levels of a categorical variable.↩︎

  2. A numeric (continuous) variable, typically on the y axis, measured over time on the x axis.↩︎

  3. When the numeric value varies wildly (or isn’t always present) over the time measurement.↩︎

  4. Categorical groups are shown as color layers, and the area of each layer represents the change in the y value.↩︎

  5. Install and load the packages.↩︎

  6. Simulate.↩︎

  7. spread is the standard deviation (sd) of the bell shaped curve, in years.↩︎

  8. A bell shaped (normal) curve, scaled by total / 12.↩︎

  9. rpois() draws each month’s sales from a Poisson distribution with lambda = avg_sales.↩︎

  10. Setting the random seed.↩︎

  11. Stacks each group’s values.↩︎

  12. The formula.↩︎

  13. Centers the stack around 0 (type = "mirror", the default).↩︎

  14. Poisson counts.↩︎

  15. A normal distribution.↩︎

  16. The normal curve describes when the sales happen; it isn’t a random variable here.↩︎

  17. The area under the curve.↩︎

  18. Labels.↩︎

  19. Map month to x, sales to y, and fad to the fill aesthetic.↩︎

  20. Add the geom_stream() layer.↩︎

  21. The legend.↩︎

  22. Arguments.↩︎

  23. forcats::fct_rev() reverses the order of the factor levels.↩︎

  24. The default.↩︎

  25. A named vector.↩︎

  26. Each fad’s share (proportion).↩︎

  27. How the categorical areas are sorted.↩︎

  28. Text labels.↩︎

  29. Map fad to label globally, in ggplot().↩︎

  30. The default grid of 100 points.↩︎