34  Instance chart

Graphs in this chapter use fmggp2::theme_gushers(), built from Fruit Gushers: a white plate background, a red frame and title, red, green, teal, orange, and pink fruit snack marks, and a Chalkboard title.


Requirements:

✅ a categorical variable

✅ a numeric (count) variable

✅ a numeric (date) variable

34.1 Description

An instance chart shows each time a group comes up as time goes on.1

Time goes on the x axis, and each group gets its own place on the y axis.2 Each time a group comes up, we most often show it with a short mark that stands straight up, using geom_point() with shape 124 (the "|" mark).3

Color and how much we can see through each mark4 also show the groups and how many times each one comes up.

34.2 Set up

PACKAGES:

Bring in the code we’ll use.5

show/hide
library(ggplot2)
library(dplyr) # for data manipulation
library(tidyr) # for expand_grid()

DATA:

Fruit Gushers are small, soft fruit snacks with juice inside, in many flavors, and they came out in 1991. To sell them, they showed a kid whose head turned into a big piece of fruit.6 Fruit snacks were already the money7 at the lunch table, where kids ate in the middle of the school day, and a new Gushers flavor made its fruit worth more. We’ll make up8 how many times each flavor changed hands at one lunch table each year from 1980 to 2005.9 Each flavor in snack_flavors has a mean number of trades each year before the year Gushers made it (debut, a year we made up) and a mean after it (berry punch comes in 1992, sour apple in 1998, and mystery in 2001).10 A count is how many times something happens. rpois() picks each year’s count (n) using the mean that goes with that year.11 Starting with set.seed()12 means you’ll get the same 208 flavor years every time you run the code below.

show/hide
snack_flavors <- tibble(
  flavor = c("Strawberry", "Lime", "Blue raspberry", "Orange", # eight Gushers flavors
           "Watermelon", "Berry punch", "Sour apple", "Mystery"),
  before = c(4, 0.3, 1, 1.5, 0.2, 1, 0.05, 0.05), # average yearly trades before the debut
  after = c(8, 1.5, 4, 1.5, 0.8, 1.5, 2, 3), # average yearly trades after the debut
  debut = c(1991, 1991, 1991, 1991, 1991, 1992, 1998, 2001) # pretend debut year for each flavor
  )
set.seed(1993) # same trades every time the code runs
lunch_trades <- expand_grid(year = 1980:2005, snack_flavors) |> # every flavor in every year
  mutate(
    flavor = factor(flavor, levels = snack_flavors$flavor), # keep the order above
    avg_n = if_else(year < debut, before, after), # pick the average for that year
    n = rpois(n(), lambda = avg_n) # number of trades that year
    ) |>
  select(year, flavor, debut, avg_n, n)
glimpse(lunch_trades)
#> Rows: 208
#> Columns: 5
#> $ year   <int> 1980, 1980, 1980, 1980, 1980, 198…
#> $ flavor <fct> Strawberry, Lime, Blue raspberry,…
#> $ debut  <dbl> 1991, 1991, 1991, 1991, 1991, 199…
#> $ avg_n  <dbl> 4.00, 0.30, 1.00, 1.50, 0.20, 1.0…
#> $ n      <int> 3, 0, 0, 2, 0, 1, 0, 0, 6, 0, 1, …

An instance chart only shows the years a flavor changed hands, so we’ll take out the years with no trades13 and keep the rest in gushers_trades.

show/hide
gushers_trades <- lunch_trades |>
  filter(n > 0) # keep only the years with at least one trade
glimpse(gushers_trades)
#> Rows: 128
#> Columns: 5
#> $ year   <int> 1980, 1980, 1980, 1981, 1981, 198…
#> $ flavor <fct> Strawberry, Orange, Berry punch, …
#> $ debut  <dbl> 1991, 1991, 1992, 1991, 1991, 199…
#> $ avg_n  <dbl> 4.0, 1.5, 1.0, 4.0, 1.0, 1.5, 1.0…
#> $ n      <int> 3, 2, 1, 6, 1, 3, 3, 2, 1, 3, 1, …

34.3 Distribution

An instance chart comes from two things: whether a flavor shows up in a year (the mark), and how many times it shows up (how much we can see through it).14 The open spaces between the marks matter just as much as the marks, and the number of open spaces turns on how much kids want the flavor.15 Below we’ll move from the counts, to a graph, to the rule behind it.16

COUNT IT:

Start with lime in the five years before and after Gushers came out.

show/hide
lunch_trades |>
  filter(flavor == "Lime", year >= 1986, year <= 1995)

Before 1991, most years have no lime trades at all. Below we count the years with no trades (zero_years) for each flavor, before and after the year it came out.

show/hide
zero_counts <- lunch_trades |>
  mutate(era = if_else(year < debut, "before", "after")) |>
  group_by(flavor, era) |>
  summarise(years = n(), zero_years = sum(n == 0), .groups = "drop") |>
  pivot_wider(names_from = era, values_from = c(years, zero_years)) |>
  select(flavor, years_before, zero_years_before,
         years_after, zero_years_after)
zero_counts

Lime went 8 of 11 years without a trade before Gushers, and only 3 of 15 years after it. Strawberry, the flavor kids wanted most, never had a year without a trade.

DRAW IT:

The graph below shows lime’s count for each year as a bar (even the years with 0) and shows the same years as instance marks under the bars. Every open space in the marks is a year whose bar stops at 0.

show/hide
lime <- lunch_trades |>
  filter(flavor == "Lime")
ggplot(lime, aes(x = year)) +
  geom_col(aes(y = n), fill = "#4E6B44") +
  geom_point(data = filter(lime, n > 0), aes(y = -0.5),
             shape = 124, size = 5, color = "#D01D27") +
  geom_vline(xintercept = 1990.5, linetype = "dashed") +
  annotate("text", x = 1990.3, y = max(lime$n), label = "Gushers come out", hjust = 1) +
  labs(title = "Lime trades each year", x = "Year", y = "Lime trades")

NAME IT:

Each year’s count comes from a rule for counts17 that uses the mean for that flavor, before or after the year it came out:

\[ n_{\text{flavor, year}} \sim \text{Poisson}(\lambda), \quad P(n = 0) = e^{-\lambda} \]

The chance of a year with no trades turns only on \(\lambda\), the mean. Before Gushers, lime’s mean is 0.3 trades a year, so \(P(n = 0) = e^{-0.3} \approx 0.74\), and we’d look for about \(11 \times 0.74 \approx 8.1\) years with no trades out of 11.18 After Gushers, the mean goes up to 1.5, so \(P(n = 0) = e^{-1.5} \approx 0.22\) and we’d look for about 3.3 years with no trades out of 15. Strawberry’s mean is 8 trades a year after Gushers, so a year with no trades has a chance of \(e^{-8} \approx 0.0003\) (almost never). That’s why flavors that kids don’t often trade look like marks with spaces between them, and flavors they trade a lot look like lines with no breaks. Try changing lime’s after number to 5, and lime gets a mark in almost every year from 1991 on.

34.4 Grammar

CODE:

  • Make the words around the graph19 with labs()

  • Start the graph with ggplot() and give it data

  • Put year on the x axis and flavor on the y axis, and give each flavor its own color20

  • Add geom_point(), let n set how much we can see through each mark (alpha),21 and set the shape to 124 and size to 5

  • Most years have only a few trades, so we make the lightest marks stronger with scale_alpha_continuous(range = c(0.35, 1)) (if we don’t, the lightest marks are hard to see)

  • Take away the color key22 with guides() (the flavors are already on the y axis)

show/hide
labs_inst_gushers <- labs(
  title = "Fruit Gushers",
  caption = "Simulated data",
  subtitle = "Fruit snack trades at one lunch table, 1980 - 2005",
  x = "Year",
  y = "Flavor",
  alpha = "Trades per year")
ggp2_inst_gushers <- ggplot(data = gushers_trades,
    mapping = aes(x = year,
                  y = flavor,
                  color = flavor)) +
  geom_point(aes(alpha = n),
              shape = 124,
              size = 5) +
  scale_alpha_continuous(range = c(0.35, 1)) +
  guides(color = "none")
ggp2_inst_gushers +
  labs_inst_gushers

GRAPH:

34.5 More info

34.5.1 Color with cut_interval()

ggplot2 has three ways of cutting a column of numbers into groups: cut_interval(), cut_number(), and cut_width().23 We’ll use cut_interval() to make an n_range column, which cuts the counts for each year (n) into groups that are each as wide as length.24

show/hide
gushers_trades_range <- gushers_trades |>
  mutate(
    n_range = ggplot2::cut_interval(x = n, length = 3) # group trade counts into bins 3 wide
    )
gushers_trades_range |>
  count(n_range) # rows in each bin

Below we can see that each group of counts is written with its lowest and highest numbers:25 "(a,b]"

  • Most of the marks fall in the lowest group, and every count over 9 comes from strawberry after Gushers came out.
show/hide
labs_inst_gushers_range <- labs(
  title = "Fruit Gushers",
  caption = "Simulated data",
  subtitle = "Fruit snack trades at one lunch table, 1980 - 2005",
  x = "Year",
  y = "Flavor",
  color = "Trades per year")
ggp2_inst_gushers_range <- ggplot(data = gushers_trades_range,
    mapping = aes(x = year,
                  y = flavor,
              color = n_range)) +
  geom_point(aes(alpha = n),
                shape = 124,
                size = 4) +
  scale_alpha_continuous(range = c(0.35, 1)) +
  guides(alpha = "none") # the color key already shows the trade counts
ggp2_inst_gushers_range +
  labs_inst_gushers_range

34.5.2 Facets with cut_number()

To show small graphs,26 we’ll make two more columns: year_range and group:

  • year_range uses ggplot2::cut_number() to cut year into three groups with about the same number of marks in each, then we make a name for each group from its first and last year

  • group is a column with two set values:27 "Single fruit" and "Berry blends" (blue raspberry and berry punch)

show/hide
gushers_trades_fct <- gushers_trades |>
  mutate(
    year_grp = ggplot2::cut_number(year, n = 3, labels = FALSE) # three groups of years with similar counts
    ) |>
  group_by(year_grp) |>
  mutate(year_range = paste(min(year), "-", max(year))) |> # label each group with its years
  ungroup() |>
  mutate(
    year_range = factor(year_range, levels = unique(year_range[order(year)])), # earliest to latest
    group = if_else(flavor %in% c("Blue raspberry", "Berry punch"), "Berry blends", "Single fruit"), # two flavor groups
    group = factor(group, levels = c("Single fruit", "Berry blends"))
    )

Below we can see how many flavor years with at least one trade fall in each year_range and group28

show/hide
gushers_trades_fct |>
  count(year_range, group) |>
  pivot_wider(names_from = group,
    values_from = n)

In the graph, the words over each small graph show its years, so the words at the top29 show the first and last year of all the trades.

  • We can also change the shape in geom_point() to the straight up mark ("|")30

  • facet_grid() puts the flavor groups in rows and the year groups in columns. With scales = "free", each row keeps only its own flavors, so the flavors line up across the row, and each column picks its own numbers along the x axis,31 so we set them every five years with scale_x_continuous()

show/hide
labs_inst_gushers_facet <- labs(
  title = "Fruit Gushers",
  caption = "Simulated data",
  subtitle = "Fruit snack trades at one lunch table, 1980 - 2005",
  x = "Year",
  y = "Flavor")

ggp2_inst_gushers_facet <- ggplot(gushers_trades_fct,
  mapping = aes(x = year,
                y = flavor)) +
  geom_point(aes(alpha = n, color = flavor),
                 shape = "|",
                 size = 3,
                 show.legend = FALSE) +
  scale_x_continuous(breaks = seq(1980, 2005, by = 5)) +
  scale_alpha_continuous(range = c(0.35, 1)) +
  facet_grid(group ~ year_range,
             scales = "free")
ggp2_inst_gushers_facet +
  labs_inst_gushers_facet


  1. An instance chart (or instance graph) displays frequencies (or “instances”) of different categorical values over time.↩︎

  2. Each separate categorical item.↩︎

  3. Vertical hashes; shape 124 is the pipe character "|".↩︎

  4. Transparency (alpha).↩︎

  5. Install packages.↩︎

  6. Commercials.↩︎

  7. Currency.↩︎

  8. Simulate.↩︎

  9. How many packs of each flavor were traded.↩︎

  10. The average number of trades per year before and after each flavor’s pretend Gushers debut.↩︎

  11. rpois() draws each year’s count from a Poisson distribution with the matching average (lambda = avg_n).↩︎

  12. Setting the seed.↩︎

  13. Filter out.↩︎

  14. Transparency (alpha).↩︎

  15. How popular the flavor is.↩︎

  16. The formula.↩︎

  17. A Poisson distribution, with the average for that flavor and era.↩︎

  18. The expected number of empty years.↩︎

  19. Create labels.↩︎

  20. Map year to x, flavor to y, and color to flavor.↩︎

  21. Map n to alpha (transparency).↩︎

  22. Legend.↩︎

  23. Functions for splitting numerical variables into equal width intervals, groups with equal counts, or bins of a set width.↩︎

  24. The length argument sets the width of each interval.↩︎

  25. Interval notation: ( leaves a out of the interval and ] keeps b in it.↩︎

  26. Facets.↩︎

  27. A factor variable with two levels.↩︎

  28. A cross table of year_range and group; each cell counts rows (flavor years with at least one trade), not trades.↩︎

  29. Labels (here, the subtitle).↩︎

  30. The pipe operator.↩︎

  31. Breaks.↩︎