Storytelling with {ggplot2}: Bar chart sequence

Author

McCall Pitcher

Published

August 10, 2025

library(tidyverse)
library(tidytext)

1. Load data

dat <- read_csv("fs_outcome_data.csv") 
Rows: 78 Columns: 3
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (2): college, metric
dbl (1): rate

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
head(dat)
# A tibble: 6 × 3
  college metric                    rate
  <chr>   <chr>                    <dbl>
1 AAA     Credit Accumulation Rate -0.05
2 BBB     Credit Accumulation Rate -1   
3 CCC     Credit Accumulation Rate -1.3 
4 DDD     Credit Accumulation Rate -0.5 
5 EEE     Credit Accumulation Rate  1.1 
6 FFF     Credit Accumulation Rate  1.7 

2. Build fill and color variables

dat <- dat |> 
  mutate(metric = factor(metric, levels = c("Credit Accumulation Rate",
                                            "Retention Rate",
                                            "Graduation Rate"),
                         labels = c("CAR","Retention Rate","Graduation Rate")),
         positive = case_when(
                         metric == "CAR"             & rate > 0 ~"a",
                         metric == "Retention Rate"  & rate > 0 ~"b",
                         metric == "Graduation Rate" & rate > 0 ~"c",
                         T ~ "d"),
         negative = case_when(
                         metric == "CAR"             & rate < 0 ~"a",
                         metric == "Retention Rate"  & rate < 0 ~"b",
                         metric == "Graduation Rate" & rate < 0 ~"c",
                         T ~ "d"),
         goal = case_when(
                         metric == "CAR"             & rate >= 1 ~"a",
                         metric == "Retention Rate"  & rate >= 1 ~"b",
                         metric == "Graduation Rate" & rate >= 1 ~"c",
                         T~"d"),
         case = case_when(
                          metric == "CAR"            & college %in% 
                            c("GGG","MMM","FFF")~"a",
                          metric == "Retention Rate" & college %in% 
                            c("GGG","MMM","FFF")~"b",
                          metric == "Graduation Rate"& college %in% 
                            c("GGG","MMM","FFF")~"c",
                          T~"d"),
         positive_color  =    ifelse(positive != "d", "a","b"),
         negative_color  =    ifelse(negative != "d", "a","b"),
         goal_color      =    ifelse(goal     != "d", "a","b"),
         case_color      =    ifelse(case     != "d", "a","b"))

2. Set bracket positions

(Ugh! There has to be a better way to do this … But manually setting for now)

college_count <- function(x) {
  dat |> 
    filter(metric == x,
           rate >= 1) |> 
    count() |>  pull(n)
}

bracket_floor <- function(y) {
  26.5 - college_count(y)
}

bracket_floors <- c(bracket_floor("CAR"),
                    bracket_floor("Retention Rate"),
                    bracket_floor("Graduation Rate"))


brackets <- data.frame(
                  x = c(2.05, 1.35, 2.95,
                        2.25, 1.55, 3.15,
                        2.05, 1.35, 2.95),
                  xend = rep(c(2.25, 1.55, 3.15),3),
                  y = c(rep(26.5,6),
                        bracket_floors),
                  yend = c(26.5, 26.5, 26.5,
                           rep(bracket_floors, 2)),
                  metric = rep(c("CAR", 
                                 "Retention Rate", 
                                 "Graduation Rate"),3)) |> 
  mutate(metric = factor(metric, levels = c("CAR",
                                            "Retention Rate",
                                            "Graduation Rate")))

3. Set annotation labels/positions

annotations <- data.frame(metric = factor(c("CAR",
                                  "Retention Rate",
                                  "Graduation Rate"),
                                levels = c("CAR",
                                           "Retention Rate",
                                           "Graduation Rate")),
                lab = c(paste0(college_count("CAR"), " colleges"),
                        paste0(college_count("Retention Rate"), " colleges"),
                        paste0(college_count("Graduation Rate"), " colleges")),
                x = c(2.33, 1.65, 1.95),
                y = c(27 - (college_count("CAR")/2), 
                      27 - (college_count("Retention Rate")/2), 
                      27 - (college_count("Graduation Rate")/2)))

4. Build function

build_chart <- function(fill_var, color_var, annotation_color) {
  
  ggplot(dat, 
         aes(x = rate, y = reorder_within(college, rate, metric))) +
    geom_col(aes(fill = {{ fill_var }}), 
             width = .85, show.legend = F) +
    geom_text(dat |> filter(rate < 0),
              mapping = aes(x = .1, y = reorder_within(college, rate, metric),
                            label = college, color = {{ color_var }}),
              hjust = 0, vjust = .4, size = 3.3, show.legend = F) +
    geom_text(dat |> filter(rate > 0),
              mapping = aes(x = -.1, y = reorder_within(college, rate, metric),
                            label = college, color = {{ color_var }}),
              hjust = 1, vjust = .4, size = 3.3, show.legend = F) +
    geom_segment(brackets, 
                 mapping = aes(x = x, xend = xend, y = y, yend = yend),
                 show.legend = F, linewidth = .5, 
                 color = annotation_color) +
    geom_text(annotations, 
              mapping = aes(x = x, y = y, label = lab), hjust = 0,
              show.legend = F, fontface = "bold", size = 3.3, 
              color = annotation_color) +
    facet_wrap(~metric, scales = 'free_y') +
    labs(x = "KPI Improvement Rate",
         caption = "Note: Improvement rate is based on the slope of a regression line (percentage points per year) fitted to the annual KPI trend values.") +
    scale_y_reordered() +
    scale_fill_manual(values = c("#949e48","#ea9f2c","#0290c0","grey90")) +
    scale_color_manual(values = c("grey40","grey90")) +
    theme_minimal() +
    theme(strip.text = element_text(face = "bold",size = 10, color = "#444",
                                    margin = margin(b=10)),
          panel.spacing = unit(.7,"cm"),
          panel.grid = element_blank(),
          axis.ticks.x = element_line(color = "#999"),
          axis.line.x = element_line(color = "#999"),
          axis.text.y = element_blank(),
          axis.title.y = element_blank(),
          axis.text.x = element_text(size = 10),
          axis.title.x = element_text(face = "bold", size = 10, color = "#444", 
                                      margin = margin(t=10)),
          plot.caption = element_text(size = 8, color = "darkgrey",hjust = 0,
                                      margin = margin(t=10)),
          plot.caption.position = "plot")

}

5. Run function

build_chart(positive, positive_color,"transparent")

build_chart(negative, negative_color,"transparent")

build_chart(goal, goal_color, "grey50") +
  geom_vline(aes(xintercept = 1), 
             color = "grey70", 
             linetype = "dotted",
             linewidth = .65)

build_chart(case, case_color, "grey90") +
  geom_vline(aes(xintercept = 1), 
             color = "grey90", 
             linetype = "dotted",
             linewidth = .65) +
  geom_col(aes(fill = case), 
           width = .85, show.legend = F)