• Steven Ponce
  • About
  • Data Visualizations
  • Projects
  • Resume
  • Email

On this page

  • Original
  • Makeover
  • Steps to Create this Graphic
    • 1. Load Packages & Setup
    • 2. Read in the Data
    • 3. Examine the Data
    • 4. Tidy Data
    • 5. Visualization Parameters
    • 6. Plot
    • 7. Save
    • 8. Session Info
    • 9. GitHub Repository
    • 10. References
    • 11. Custom Functions Documentation

Recessions and Recovery: Not All Crises Are Equal

  • Show All Code
  • Hide All Code

  • View Source

Prime-age labor force participation drops during recessions but recovers at vastly different speeds.

Analyzing how the 2001, 2008, and 2020 recessions impacted prime-age labor force participation differently. While 2020 had the steepest drop, it recovered fastest—2008 never did.
Published

October 14, 2025

Original

The original visualization Figure A | Since 2000, prime-age LFPR sinks as recessions hit and recovers only when unemployment is low again comes from Better things come to those who wait

Original visualization

Makeover

Figure 1: Two-panel chart comparing recession impacts on prime-age labor force participation (ages 25-54). The left panel shows that the 2020 recession had the steepest drop at -3.3 percentage points, compared to -1.7pp in 2001 and -1.6pp in 2008. The right panel displays recovery trajectories over 60 months: 2020 recovered fastest, exceeding pre-recession levels by +0.6pp; 2008 never recovered, remaining -1.7pp below; 2001 shows a gradual decline to -1.1pp below baseline.

Steps to Create this Graphic

1. Load Packages & Setup

Show code
```{r}
#| label: load
#| warning: false
#| message: false
#| results: "hide"

## 1. LOAD PACKAGES & SETUP ----
suppressPackageStartupMessages({
  if (!require("pacman")) install.packages("pacman")
  pacman::p_load(
    tidyverse,  # Easily Install and Load the 'Tidyverse'
    ggtext,     # Improved Text Rendering Support for 'ggplot2'
    showtext,   # Using Fonts More Easily in R Graphs
    scales,     # Scale Functions for Visualization
    glue,       # Interpreted String Literals
    patchwork   # Using Fonts More Easily in R Graphs
  )
})

### |- figure size ----
camcorder::gg_record(
    dir    = here::here("temp_plots"),
    device = "png",
    width  = 16,
    height = 9,
    units  = "in",
    dpi    = 320
)

# Source utility functions
suppressMessages(source(here::here("R/utils/fonts.R")))
source(here::here("R/utils/social_icons.R"))
source(here::here("R/utils/image_utils.R"))
source(here::here("R/themes/base_theme.R"))
```

2. Read in the Data

Show code
```{r}
#| label: read
#| include: true
#| eval: true
#| warning: false
#|
labor_force_raw <- readxl::read_excel(
  here::here('data/MakeoverMonday/2025/SeriesReport-20251013201453_5eccfc.xlsx'), skip = 11) |> 
  janitor::clean_names()
```

3. Examine the Data

Show code
```{r}
#| label: examine
#| include: true
#| eval: true
#| results: 'hide'
#| warning: false

glimpse(labor_force_raw)
skimr::skim_without_charts(labor_force_raw)
```

4. Tidy Data

Show code
```{r}
#| label: tidy
#| warning: false

labor_force_clean <- labor_force_raw |>
  pivot_longer(
    cols = jan:dec,
    names_to = "month",
    values_to = "lfpr"
  ) |>
  mutate(
    month_num = match(month, tolower(month.abb)),
    date = make_date(year, month_num, 1)
  ) |>
  drop_na(lfpr) |>
  arrange(date) |>
  select(date, year, month, lfpr)

# Define recession periods
recessions <- tribble(
  ~start, ~end, ~label,
  "2001-03-01", "2001-11-01", "2001", # Dot-com
  "2007-12-01", "2009-06-01", "2008", # Housing
  "2020-02-01", "2020-04-01", "2020"  # Pandemic
) |>
  mutate(
    start = ymd(start),
    end = ymd(end)
  )

### |- P1: Dumbbell Chart Data ----
# Calculate peak before and trough after each recession
recession_impact <- recessions |>
  mutate(
    peak_before = map_dbl(start, ~ {
      labor_force_clean |>
        filter(date >= .x - years(2), date < .x) |>
        pull(lfpr) |>
        max()
    }),
    trough_after = map_dbl(start, ~ {
      labor_force_clean |>
        filter(date >= .x, date <= .x + years(3)) |>
        pull(lfpr) |>
        min()
    }),
    drop = peak_before - trough_after,
    label_full = paste0(label, " Recession")
  ) |>
  arrange(desc(year(start)))

### |- P2: Line Chart Data ----
# Create normalized recession cycles data
recession_cycles <- recessions |>
  mutate(
    cycle_data = map2(start, label, ~ {
      labor_force_clean |>
        filter(date >= (.x - years(2)), date <= (.x + years(5))) |>
        mutate(
          months_from_start = interval(.x, date) %/% months(1),
          recession = .y
        )
    })
  ) |>
  unnest(cycle_data) |>
  select(recession, months_from_start, date, lfpr, label)

# Calculate baseline and change
recession_cycles <- recession_cycles |>
  group_by(recession) |>
  mutate(
    baseline = lfpr[which.min(abs(months_from_start))],
    change_from_baseline = lfpr - baseline,
    label_full = paste0(recession, " Recession")
  ) |>
  ungroup() |>
  arrange(desc(recession))

recovery_annotation <- tibble(
  label_full = "2020 Recession",
  months_from_start = 30,
  change_from_baseline = 0.95,
  label_text = "Exceeded\npre-recession level"
)

endpoint_labels <- recession_cycles |> 
  group_by(label_full, recession) |> 
  filter(months_from_start == max(months_from_start)) |>
  ungroup() |>
  mutate(
    label_text = paste0(
      ifelse(change_from_baseline > 0, "+", ""), 
      round(change_from_baseline, 1), "pp"
    )
  )
```

5. Visualization Parameters

Show code
```{r}
#| label: params
#| include: true
#| warning: false

### |-  plot aesthetics ----
# Get base colors with custom palette
colors <- get_theme_colors(palette = list(
  "2001" = "#5B8FA3",  
  "2008" = "#D4A373", 
  "2020" = "#6B5B73",   
  "peak" = "#2D5D4F",
  "trough" = "#B85C5C",
  "neutral" = "#4A5859"
))   

### |-  titles and caption ----
title_text <- str_glue("Recessions and Recovery: Not All Crises Are Equal")

subtitle_text <- str_glue(
  "Prime-age **labor force participation** drops during recessions but recovers at **vastly different speeds**. **2008** caused lasting damage, participation never recovered.<br>", 
  "**2020** was severe but brief bouncing back to exceed pre-recession levels"
)

# Create caption
caption_text <- create_mm_caption(
  mm_year = current_year,
  mm_week = current_week,
  source_text = paste0("U.S. Bureau of Labor Statistics | Prime-age workers (25-54 years) | Current Population Survey (LNS11300060)")
)

### |-  fonts ----
setup_fonts()
fonts <- get_font_families()

### |-  plot theme ----

# Start with base theme
base_theme <- create_base_theme(colors)

# Add weekly-specific theme elements
weekly_theme <- extend_weekly_theme(
  base_theme,
  theme(
    # Text styling
    plot.title = element_text(
      size = rel(1.4), family = fonts$title, face = "bold",
      color = colors$title, lineheight = 1.1, hjust = 0,
      margin = margin(t = 5, b = 10)
    ),
    plot.subtitle = element_text(
      size = rel(0.9), family = fonts$subtitle, face = "italic",
      color = alpha(colors$subtitle, 0.9), lineheight = 1.1,
      margin = margin(t = 0, b = 20)
    ),

    # Legend formatting
    legend.position = "plot",
    legend.justification = "top",
    legend.margin = margin(l = 12, b = 5),
    legend.key.size = unit(0.8, "cm"),
    legend.box.margin = margin(b = 10),
    legend.title = element_text(face = "bold"),

    # Axis formatting
    axis.ticks.y = element_blank(),
    axis.ticks.x = element_line(color = "gray", linewidth = 0.5),
    # axis.ticks.length = unit(0.2, "cm"),
    axis.title.x = element_text(
      face = "bold", size = rel(0.85),
      margin = margin(t = 10)
    ),
    axis.title.y = element_text(
      face = "bold", size = rel(0.85),
      margin = margin(r = 10)
    ),
    axis.text.x = element_text(
      size = rel(0.85), family = fonts$subtitle,
      color = colors$text
    ),
    axis.text.y = element_text(
      size = rel(0.85), family = fonts$subtitle,
      color = colors$text
    ),

    # Grid lines
    panel.grid.minor = element_line(color = "#ecf0f1", linewidth = 0.2),
    panel.grid.major = element_line(color = "#ecf0f1", linewidth = 0.4),

    # Margin
    plot.margin = margin(20, 20, 20, 20)
  )
)

# Set theme
theme_set(weekly_theme)
```

6. Plot

Show code
```{r}
#| label: plot
#| warning: false

### |- P1: Dumbbell Chart ----
p1 <-
ggplot(recession_impact, aes(y = reorder(label_full, -year(start)))) +
  # Geoms
  geom_segment(
    aes(x = trough_after, xend = peak_before, y = label_full, yend = label_full),
    linewidth = 3, color = "#E8E8E8", lineend = "round"
  ) +
  geom_segment(
    aes(x = trough_after, xend = peak_before, y = label_full, yend = label_full),
    linewidth = 1.5, color = colors$palette["neutral"], lineend = "round"
  ) +
  geom_point(aes(x = peak_before),
    size = 7, color = colors$palette["peak"], shape = 21,
    fill = colors$palette["peak"], stroke = 0
  ) +
  geom_point(aes(x = trough_after),
    size = 7, color = colors$palette["trough"], shape = 21,
    fill = colors$palette["trough"], stroke = 0
  ) +
  geom_text(
    aes(x = peak_before, label = paste0(round(peak_before, 1), "%")),
    hjust = -0.6, size = 3.8, fontface = "bold", color = colors$palette["peak"],
    family = "sans"
  ) +
  geom_text(
    aes(x = trough_after, label = paste0(round(trough_after, 1), "%")),
    hjust = 1.6, size = 3.8, fontface = "bold", color = colors$palette["trough"],
    family = "sans"
  ) +
  geom_text(
    aes(
      x = (peak_before + trough_after) / 2,
      label = paste0("▼ ", round(drop, 1), " pp")
    ),
    vjust = -1.2, size = 3.3, color = "#333333", fontface = "bold",
    family = "sans"
  ) +
  # Scales
  scale_x_continuous(
    labels = label_percent(scale = 1),
    limits = c(79, 85),
    expand = expansion(mult = c(0.02, 0.08))
  ) +
  # Labs
  labs(
    title = "2020's Drop Was Steepest at -3.3 Percentage Points",
    subtitle = NULL,  
    x = "Labor Force Participation Rate",
    y = NULL,
    caption = NULL
  ) +
  # Theme
  theme(
    plot.title = element_text(
      face = "bold", size = 17, color = "#1a1a1a",
      margin = margin(b = 8)
    ),
    plot.subtitle = element_text(
      size = 11.5, color = "#5a5a5a",
      margin = margin(b = 20), lineheight = 1.2
    ),
    plot.caption = element_text(
      size = 9, color = "#7a7a7a", hjust = 0,
      margin = margin(t = 15)
    ),
    axis.text.y = element_text(face = "bold", size = 11.5, color = "#2a2a2a"),
    axis.text.x = element_text(size = 10, color = "#4a4a4a"),
    axis.title.x = element_text(size = 11, color = "#4a4a4a", margin = margin(t = 10)),
    panel.grid.major.y = element_blank(),
    panel.grid.minor = element_blank(),
    panel.grid.major.x = element_line(color = "#f0f0f0", linewidth = 0.3),
  )

### |- P2: Line Chart ----
p2 <-
ggplot(recession_cycles, aes(x = months_from_start, y = change_from_baseline)) +
  # Geoms
  geom_hline(yintercept = 0, color = "#CCCCCC", linetype = "solid", linewidth = 0.6) +
  geom_vline(
    xintercept = 0, color = colors$palette["trough"],
    linetype = "solid", linewidth = 1.2, alpha = 0.7
  ) +
  geom_line(aes(color = recession), linewidth = 1.4, alpha = 0.9) +
  geom_point(
    data = recession_cycles |> filter(months_from_start == 0),
    aes(color = recession), size = 3.5, shape = 21, fill = "white", stroke = 1.5
  ) +
  geom_text(
    data = recession_cycles |> 
      group_by(recession) |> 
      filter(months_from_start == max(months_from_start)) |>
      ungroup(),
    aes(label = paste0(ifelse(change_from_baseline > 0, "+", ""), 
                       round(change_from_baseline, 1), "pp")),
    hjust = -0.2, size = 3.2, fontface = "bold",
    family = "sans"
  ) +
  geom_text(
    data = endpoint_labels,
    aes(x = months_from_start, y = change_from_baseline, 
        label = label_text, color = recession),
    hjust = -0.2, size = 3.2, fontface = "bold",
    show.legend = FALSE
  ) +
  geom_text(
    data = recovery_annotation,
    aes(x = months_from_start, y = change_from_baseline, label = label_text),
    size = 2.8, color = "#2D5D4F", fontface = "italic", 
    lineheight = 0.9, hjust = 0.5,
    inherit.aes = FALSE
  ) +
  # Scales
  scale_color_manual(values = unlist(colors$palette[c("2001", "2008", "2020")])) +
  scale_x_continuous(
    breaks = seq(-24, 60, 12),
    labels = function(x) ifelse(x == 0, "Start", paste0(x, "m")),
    expand = expansion(mult = c(0.02, 0.12))
  ) +
  scale_y_continuous(
    labels = label_number(suffix = " pp", style_positive = "plus"),
    breaks = seq(-4, 2, 1),
  ) +
  # Labs
  labs(
    title = "But 2020 Recovered Fastest—2008 Still Hasn't",
    subtitle = NULL,  
    x = "Months from Recession Start",
    y = "pp Change from Pre-Recession Level",
    caption = NULL
  ) +
  # Facets
  facet_wrap(~label_full, ncol = 1, scales = "free_x") +
  # Theme
  theme(
    plot.title = element_text(
      face = "bold", size = 17, color = "#1a1a1a",
      margin = margin(b = 8)
    ),
    plot.subtitle = element_markdown(
      size = 11.5, color = "#5a5a5a",
      margin = margin(b = 20), lineheight = 1.2
    ),
    plot.caption = element_text(
      size = 9, color = "#7a7a7a", hjust = 0,
      margin = margin(t = 15)
    ),
    strip.text = element_text(
      face = "bold", size = 12, color = "#2a2a2a",
      hjust = 0, margin = margin(b = 10)
    ),
    strip.background = element_blank(),
    axis.text = element_text(size = 10, color = "#4a4a4a"),
    axis.title = element_text(size = 11, color = "#4a4a4a"),
    axis.title.y = element_text(margin = margin(r = 10)),
    axis.title.x = element_text(margin = margin(t = 10)),
    panel.grid.minor = element_blank(),
    panel.grid.major = element_line(color = "#f0f0f0", linewidth = 0.3),
    panel.spacing.y = unit(1.5, "lines"),
  )

### |- Combined Plots ----
combined_plots <- p1 + p2 +
  plot_layout(ncol = 2, widths = c(0.8, 1.5))

combined_plots <- combined_plots +
  plot_annotation(
    title = title_text,
    subtitle = subtitle_text,
    caption = caption_text,
    theme = theme(
      plot.title = element_text(
        size = rel(1.6),
        family = fonts$title,
        face = "bold",
        color = colors$title,
        lineheight = 1.1,
        margin = margin(t = 5, b = 5)
      ),
      plot.subtitle = element_markdown(
        size = rel(0.9),
        family = 'sans',
        color = alpha(colors$subtitle, 0.9),
        lineheight = 1.2,
        margin = margin(t = 5, b = 10)
      ),
      plot.caption = element_markdown(
        size = rel(0.6),
        family = fonts$caption,
        color = colors$caption,
        hjust = 0.5,
        margin = margin(t = 05)
      )
    )
  )
```

7. Save

Show code
```{r}
#| label: save
#| warning: false

### |-  plot image ----  
save_plot_patchwork(
  plot = combined_plots, 
  type = "makeovermonday", 
  year = current_year,
  week = current_week,
  width = 16, 
  height = 9
  )
```

8. Session Info

TipExpand for Session Info
R version 4.4.1 (2024-06-14 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 11 x64 (build 26100)

Matrix products: default


locale:
[1] LC_COLLATE=English_United States.utf8 
[2] LC_CTYPE=English_United States.utf8   
[3] LC_MONETARY=English_United States.utf8
[4] LC_NUMERIC=C                          
[5] LC_TIME=English_United States.utf8    

time zone: America/New_York
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices datasets  utils     methods   base     

other attached packages:
 [1] here_1.0.1      patchwork_1.3.0 glue_1.8.0      scales_1.3.0   
 [5] showtext_0.9-7  showtextdb_3.0  sysfonts_0.8.9  ggtext_0.1.2   
 [9] lubridate_1.9.3 forcats_1.0.0   stringr_1.5.1   dplyr_1.1.4    
[13] purrr_1.0.2     readr_2.1.5     tidyr_1.3.1     tibble_3.2.1   
[17] ggplot2_3.5.1   tidyverse_2.0.0 pacman_0.5.1   

loaded via a namespace (and not attached):
 [1] gtable_0.3.6       xfun_0.49          htmlwidgets_1.6.4  tzdb_0.5.0        
 [5] yulab.utils_0.1.8  vctrs_0.6.5        tools_4.4.0        generics_0.1.3    
 [9] curl_6.0.0         gifski_1.32.0-1    fansi_1.0.6        pkgconfig_2.0.3   
[13] ggplotify_0.1.2    skimr_2.1.5        readxl_1.4.3       lifecycle_1.0.4   
[17] compiler_4.4.0     farver_2.1.2       munsell_0.5.1      repr_1.1.7        
[21] janitor_2.2.0      codetools_0.2-20   snakecase_0.11.1   htmltools_0.5.8.1 
[25] yaml_2.3.10        pillar_1.9.0       camcorder_0.1.0    magick_2.8.5      
[29] commonmark_1.9.2   tidyselect_1.2.1   digest_0.6.37      stringi_1.8.4     
[33] labeling_0.4.3     rsvg_2.6.1         rprojroot_2.0.4    fastmap_1.2.0     
[37] grid_4.4.0         colorspace_2.1-1   cli_3.6.4          magrittr_2.0.3    
[41] base64enc_0.1-3    utf8_1.2.4         withr_3.0.2        timechange_0.3.0  
[45] rmarkdown_2.29     cellranger_1.1.0   hms_1.1.3          evaluate_1.0.1    
[49] knitr_1.49         markdown_1.13      gridGraphics_0.5-1 rlang_1.1.6       
[53] gridtext_0.1.5     Rcpp_1.0.13-1      xml2_1.3.6         renv_1.0.3        
[57] svglite_2.1.3      rstudioapi_0.17.1  jsonlite_1.8.9     R6_2.5.1          
[61] fs_1.6.5           systemfonts_1.1.0 

9. GitHub Repository

TipExpand for GitHub Repo

The complete code for this analysis is available in mm_2025_40.qmd.

For the full repository, click here.

10. References

TipExpand for References
  1. Data:
  • Makeover Monday 2025 Week 40: Better things come to those who wait
  1. Article
  • Better things come to those who wait

11. Custom Functions Documentation

Note📦 Custom Helper Functions

This analysis uses custom functions from my personal module library for efficiency and consistency across projects.

Functions Used:

  • fonts.R: setup_fonts(), get_font_families() - Font management with showtext
  • social_icons.R: create_social_caption() - Generates formatted social media captions
  • image_utils.R: save_plot() - Consistent plot saving with naming conventions
  • base_theme.R: create_base_theme(), extend_weekly_theme(), get_theme_colors() - Custom ggplot2 themes

Why custom functions?
These utilities standardize theming, fonts, and output across all my data visualizations. The core analysis (data tidying and visualization logic) uses only standard tidyverse packages.

Source Code:
View all custom functions → GitHub: R/utils

Back to top
Source Code
---
title: "Recessions and Recovery: Not All Crises Are Equal"
subtitle: "Prime-age labor force participation drops during recessions but recovers at vastly different speeds."
description: "Analyzing how the 2001, 2008, and 2020 recessions impacted prime-age labor force participation differently. While 2020 had the steepest drop, it recovered fastest—2008 never did."
date: "2025-10-14" 
tags: [
  "labor-force-participation",
  "recession-analysis", 
  "economic-recovery",
  "ggplot2",
  "dumbbell-chart",
  "time-series",
  "BLS-data",
  "pandemic-recession",
  "financial-crisis",
  "comparative-analysis",
  "patchwork"
]
image: "thumbnails/mm_2025_40.png"
format:
  html:
    toc: true
    toc-depth: 5
    code-link: true
    code-fold: true
    code-tools: true
    code-summary: "Show code"
    self-contained: true
    theme: 
      light: [flatly, assets/styling/custom_styles.scss]
      dark: [darkly, assets/styling/custom_styles_dark.scss]
editor_options: 
  chunk_output_type: inline
execute: 
  freeze: true                                      
  cache: true                                       
  error: false
  message: false
  warning: false
  eval: true
---

```{r}
#| label: setup-links
#| include: false

# CENTRALIZED LINK MANAGEMENT

## Project-specific info 
current_year <- 2025
current_week <- 40
project_file <- "mm_2025_40.qmd"
project_image <- "mm_2025_40.png"

## Data Sources
data_main <- "https://data.bls.gov/timeseries/LNS11300060"
data_secondary <- "https://data.bls.gov/timeseries/LNS11300060"

## Repository Links  
repo_main <- "https://github.com/poncest/personal-website/"
repo_file <- paste0("https://github.com/poncest/personal-website/blob/master/data_visualizations/MakeoverMonday/", current_year, "/", project_file)

## External Resources/Images
chart_original <- "https://raw.githubusercontent.com/poncest/MakeoverMonday/refs/heads/master/2025/Week_40/original_chart.png"

## Organization/Platform Links
org_primary <- "https://www.epi.org/publication/better-things-come-to-those-who-wait-the-importance-of-patience-in-diagnosing-labor-force-participation-rates-and-prescribing-policy-solutions/"
org_secondary <- "https://www.epi.org/"

# Helper function to create markdown links
create_link <- function(text, url) {
  paste0("[", text, "](", url, ")")
}

# Helper function for citation-style links
create_citation_link <- function(text, url, title = NULL) {
  if (is.null(title)) {
    paste0("[", text, "](", url, ")")
  } else {
    paste0("[", text, "](", url, ' "', title, '")')
  }
}
```

### Original

The original visualization **Figure A \| Since 2000, prime-age LFPR sinks as recessions hit and recovers only when unemployment is low again** comes from `r create_link("Better things come to those who wait", data_secondary)`

<!-- ![Original visualization](`r chart_original`) -->

![Original visualization](https://raw.githubusercontent.com/poncest/MakeoverMonday/refs/heads/master/2025/Week_40/original_chart.png)

### Makeover

![Two-panel chart comparing recession impacts on prime-age labor force participation (ages 25-54). The left panel shows that the 2020 recession had the steepest drop at -3.3 percentage points, compared to -1.7pp in 2001 and -1.6pp in 2008. The right panel displays recovery trajectories over 60 months: 2020 recovered fastest, exceeding pre-recession levels by +0.6pp; 2008 never recovered, remaining -1.7pp below; 2001 shows a gradual decline to -1.1pp below baseline.](mm_2025_40.png){#fig-1}

### <mark> **Steps to Create this Graphic** </mark>

#### 1. Load Packages & Setup

```{r}
#| label: load
#| warning: false
#| message: false      
#| results: "hide"     

## 1. LOAD PACKAGES & SETUP ----
suppressPackageStartupMessages({
  if (!require("pacman")) install.packages("pacman")
  pacman::p_load(
    tidyverse,  # Easily Install and Load the 'Tidyverse'
    ggtext,     # Improved Text Rendering Support for 'ggplot2'
    showtext,   # Using Fonts More Easily in R Graphs
    scales,     # Scale Functions for Visualization
    glue,       # Interpreted String Literals
    patchwork   # Using Fonts More Easily in R Graphs
  )
})

### |- figure size ----
camcorder::gg_record(
    dir    = here::here("temp_plots"),
    device = "png",
    width  = 16,
    height = 9,
    units  = "in",
    dpi    = 320
)

# Source utility functions
suppressMessages(source(here::here("R/utils/fonts.R")))
source(here::here("R/utils/social_icons.R"))
source(here::here("R/utils/image_utils.R"))
source(here::here("R/themes/base_theme.R"))
```

#### 2. Read in the Data

```{r}
#| label: read
#| include: true
#| eval: true
#| warning: false
#| 
labor_force_raw <- readxl::read_excel(
  here::here('data/MakeoverMonday/2025/SeriesReport-20251013201453_5eccfc.xlsx'), skip = 11) |> 
  janitor::clean_names()
```

#### 3. Examine the Data

```{r}
#| label: examine
#| include: true
#| eval: true
#| results: 'hide'
#| warning: false

glimpse(labor_force_raw)
skimr::skim_without_charts(labor_force_raw)
```

#### 4. Tidy Data

```{r}
#| label: tidy
#| warning: false

labor_force_clean <- labor_force_raw |>
  pivot_longer(
    cols = jan:dec,
    names_to = "month",
    values_to = "lfpr"
  ) |>
  mutate(
    month_num = match(month, tolower(month.abb)),
    date = make_date(year, month_num, 1)
  ) |>
  drop_na(lfpr) |>
  arrange(date) |>
  select(date, year, month, lfpr)

# Define recession periods
recessions <- tribble(
  ~start, ~end, ~label,
  "2001-03-01", "2001-11-01", "2001", # Dot-com
  "2007-12-01", "2009-06-01", "2008", # Housing
  "2020-02-01", "2020-04-01", "2020"  # Pandemic
) |>
  mutate(
    start = ymd(start),
    end = ymd(end)
  )

### |- P1: Dumbbell Chart Data ----
# Calculate peak before and trough after each recession
recession_impact <- recessions |>
  mutate(
    peak_before = map_dbl(start, ~ {
      labor_force_clean |>
        filter(date >= .x - years(2), date < .x) |>
        pull(lfpr) |>
        max()
    }),
    trough_after = map_dbl(start, ~ {
      labor_force_clean |>
        filter(date >= .x, date <= .x + years(3)) |>
        pull(lfpr) |>
        min()
    }),
    drop = peak_before - trough_after,
    label_full = paste0(label, " Recession")
  ) |>
  arrange(desc(year(start)))

### |- P2: Line Chart Data ----
# Create normalized recession cycles data
recession_cycles <- recessions |>
  mutate(
    cycle_data = map2(start, label, ~ {
      labor_force_clean |>
        filter(date >= (.x - years(2)), date <= (.x + years(5))) |>
        mutate(
          months_from_start = interval(.x, date) %/% months(1),
          recession = .y
        )
    })
  ) |>
  unnest(cycle_data) |>
  select(recession, months_from_start, date, lfpr, label)

# Calculate baseline and change
recession_cycles <- recession_cycles |>
  group_by(recession) |>
  mutate(
    baseline = lfpr[which.min(abs(months_from_start))],
    change_from_baseline = lfpr - baseline,
    label_full = paste0(recession, " Recession")
  ) |>
  ungroup() |>
  arrange(desc(recession))

recovery_annotation <- tibble(
  label_full = "2020 Recession",
  months_from_start = 30,
  change_from_baseline = 0.95,
  label_text = "Exceeded\npre-recession level"
)

endpoint_labels <- recession_cycles |> 
  group_by(label_full, recession) |> 
  filter(months_from_start == max(months_from_start)) |>
  ungroup() |>
  mutate(
    label_text = paste0(
      ifelse(change_from_baseline > 0, "+", ""), 
      round(change_from_baseline, 1), "pp"
    )
  )
```

#### 5. Visualization Parameters

```{r}
#| label: params
#| include: true
#| warning: false

### |-  plot aesthetics ----
# Get base colors with custom palette
colors <- get_theme_colors(palette = list(
  "2001" = "#5B8FA3",  
  "2008" = "#D4A373", 
  "2020" = "#6B5B73",   
  "peak" = "#2D5D4F",
  "trough" = "#B85C5C",
  "neutral" = "#4A5859"
))   

### |-  titles and caption ----
title_text <- str_glue("Recessions and Recovery: Not All Crises Are Equal")

subtitle_text <- str_glue(
  "Prime-age **labor force participation** drops during recessions but recovers at **vastly different speeds**. **2008** caused lasting damage, participation never recovered.<br>", 
  "**2020** was severe but brief bouncing back to exceed pre-recession levels"
)

# Create caption
caption_text <- create_mm_caption(
  mm_year = current_year,
  mm_week = current_week,
  source_text = paste0("U.S. Bureau of Labor Statistics | Prime-age workers (25-54 years) | Current Population Survey (LNS11300060)")
)

### |-  fonts ----
setup_fonts()
fonts <- get_font_families()

### |-  plot theme ----

# Start with base theme
base_theme <- create_base_theme(colors)

# Add weekly-specific theme elements
weekly_theme <- extend_weekly_theme(
  base_theme,
  theme(
    # Text styling
    plot.title = element_text(
      size = rel(1.4), family = fonts$title, face = "bold",
      color = colors$title, lineheight = 1.1, hjust = 0,
      margin = margin(t = 5, b = 10)
    ),
    plot.subtitle = element_text(
      size = rel(0.9), family = fonts$subtitle, face = "italic",
      color = alpha(colors$subtitle, 0.9), lineheight = 1.1,
      margin = margin(t = 0, b = 20)
    ),

    # Legend formatting
    legend.position = "plot",
    legend.justification = "top",
    legend.margin = margin(l = 12, b = 5),
    legend.key.size = unit(0.8, "cm"),
    legend.box.margin = margin(b = 10),
    legend.title = element_text(face = "bold"),

    # Axis formatting
    axis.ticks.y = element_blank(),
    axis.ticks.x = element_line(color = "gray", linewidth = 0.5),
    # axis.ticks.length = unit(0.2, "cm"),
    axis.title.x = element_text(
      face = "bold", size = rel(0.85),
      margin = margin(t = 10)
    ),
    axis.title.y = element_text(
      face = "bold", size = rel(0.85),
      margin = margin(r = 10)
    ),
    axis.text.x = element_text(
      size = rel(0.85), family = fonts$subtitle,
      color = colors$text
    ),
    axis.text.y = element_text(
      size = rel(0.85), family = fonts$subtitle,
      color = colors$text
    ),

    # Grid lines
    panel.grid.minor = element_line(color = "#ecf0f1", linewidth = 0.2),
    panel.grid.major = element_line(color = "#ecf0f1", linewidth = 0.4),

    # Margin
    plot.margin = margin(20, 20, 20, 20)
  )
)

# Set theme
theme_set(weekly_theme)
```

#### 6. Plot

```{r}
#| label: plot
#| warning: false

### |- P1: Dumbbell Chart ----
p1 <-
ggplot(recession_impact, aes(y = reorder(label_full, -year(start)))) +
  # Geoms
  geom_segment(
    aes(x = trough_after, xend = peak_before, y = label_full, yend = label_full),
    linewidth = 3, color = "#E8E8E8", lineend = "round"
  ) +
  geom_segment(
    aes(x = trough_after, xend = peak_before, y = label_full, yend = label_full),
    linewidth = 1.5, color = colors$palette["neutral"], lineend = "round"
  ) +
  geom_point(aes(x = peak_before),
    size = 7, color = colors$palette["peak"], shape = 21,
    fill = colors$palette["peak"], stroke = 0
  ) +
  geom_point(aes(x = trough_after),
    size = 7, color = colors$palette["trough"], shape = 21,
    fill = colors$palette["trough"], stroke = 0
  ) +
  geom_text(
    aes(x = peak_before, label = paste0(round(peak_before, 1), "%")),
    hjust = -0.6, size = 3.8, fontface = "bold", color = colors$palette["peak"],
    family = "sans"
  ) +
  geom_text(
    aes(x = trough_after, label = paste0(round(trough_after, 1), "%")),
    hjust = 1.6, size = 3.8, fontface = "bold", color = colors$palette["trough"],
    family = "sans"
  ) +
  geom_text(
    aes(
      x = (peak_before + trough_after) / 2,
      label = paste0("▼ ", round(drop, 1), " pp")
    ),
    vjust = -1.2, size = 3.3, color = "#333333", fontface = "bold",
    family = "sans"
  ) +
  # Scales
  scale_x_continuous(
    labels = label_percent(scale = 1),
    limits = c(79, 85),
    expand = expansion(mult = c(0.02, 0.08))
  ) +
  # Labs
  labs(
    title = "2020's Drop Was Steepest at -3.3 Percentage Points",
    subtitle = NULL,  
    x = "Labor Force Participation Rate",
    y = NULL,
    caption = NULL
  ) +
  # Theme
  theme(
    plot.title = element_text(
      face = "bold", size = 17, color = "#1a1a1a",
      margin = margin(b = 8)
    ),
    plot.subtitle = element_text(
      size = 11.5, color = "#5a5a5a",
      margin = margin(b = 20), lineheight = 1.2
    ),
    plot.caption = element_text(
      size = 9, color = "#7a7a7a", hjust = 0,
      margin = margin(t = 15)
    ),
    axis.text.y = element_text(face = "bold", size = 11.5, color = "#2a2a2a"),
    axis.text.x = element_text(size = 10, color = "#4a4a4a"),
    axis.title.x = element_text(size = 11, color = "#4a4a4a", margin = margin(t = 10)),
    panel.grid.major.y = element_blank(),
    panel.grid.minor = element_blank(),
    panel.grid.major.x = element_line(color = "#f0f0f0", linewidth = 0.3),
  )

### |- P2: Line Chart ----
p2 <-
ggplot(recession_cycles, aes(x = months_from_start, y = change_from_baseline)) +
  # Geoms
  geom_hline(yintercept = 0, color = "#CCCCCC", linetype = "solid", linewidth = 0.6) +
  geom_vline(
    xintercept = 0, color = colors$palette["trough"],
    linetype = "solid", linewidth = 1.2, alpha = 0.7
  ) +
  geom_line(aes(color = recession), linewidth = 1.4, alpha = 0.9) +
  geom_point(
    data = recession_cycles |> filter(months_from_start == 0),
    aes(color = recession), size = 3.5, shape = 21, fill = "white", stroke = 1.5
  ) +
  geom_text(
    data = recession_cycles |> 
      group_by(recession) |> 
      filter(months_from_start == max(months_from_start)) |>
      ungroup(),
    aes(label = paste0(ifelse(change_from_baseline > 0, "+", ""), 
                       round(change_from_baseline, 1), "pp")),
    hjust = -0.2, size = 3.2, fontface = "bold",
    family = "sans"
  ) +
  geom_text(
    data = endpoint_labels,
    aes(x = months_from_start, y = change_from_baseline, 
        label = label_text, color = recession),
    hjust = -0.2, size = 3.2, fontface = "bold",
    show.legend = FALSE
  ) +
  geom_text(
    data = recovery_annotation,
    aes(x = months_from_start, y = change_from_baseline, label = label_text),
    size = 2.8, color = "#2D5D4F", fontface = "italic", 
    lineheight = 0.9, hjust = 0.5,
    inherit.aes = FALSE
  ) +
  # Scales
  scale_color_manual(values = unlist(colors$palette[c("2001", "2008", "2020")])) +
  scale_x_continuous(
    breaks = seq(-24, 60, 12),
    labels = function(x) ifelse(x == 0, "Start", paste0(x, "m")),
    expand = expansion(mult = c(0.02, 0.12))
  ) +
  scale_y_continuous(
    labels = label_number(suffix = " pp", style_positive = "plus"),
    breaks = seq(-4, 2, 1),
  ) +
  # Labs
  labs(
    title = "But 2020 Recovered Fastest—2008 Still Hasn't",
    subtitle = NULL,  
    x = "Months from Recession Start",
    y = "pp Change from Pre-Recession Level",
    caption = NULL
  ) +
  # Facets
  facet_wrap(~label_full, ncol = 1, scales = "free_x") +
  # Theme
  theme(
    plot.title = element_text(
      face = "bold", size = 17, color = "#1a1a1a",
      margin = margin(b = 8)
    ),
    plot.subtitle = element_markdown(
      size = 11.5, color = "#5a5a5a",
      margin = margin(b = 20), lineheight = 1.2
    ),
    plot.caption = element_text(
      size = 9, color = "#7a7a7a", hjust = 0,
      margin = margin(t = 15)
    ),
    strip.text = element_text(
      face = "bold", size = 12, color = "#2a2a2a",
      hjust = 0, margin = margin(b = 10)
    ),
    strip.background = element_blank(),
    axis.text = element_text(size = 10, color = "#4a4a4a"),
    axis.title = element_text(size = 11, color = "#4a4a4a"),
    axis.title.y = element_text(margin = margin(r = 10)),
    axis.title.x = element_text(margin = margin(t = 10)),
    panel.grid.minor = element_blank(),
    panel.grid.major = element_line(color = "#f0f0f0", linewidth = 0.3),
    panel.spacing.y = unit(1.5, "lines"),
  )

### |- Combined Plots ----
combined_plots <- p1 + p2 +
  plot_layout(ncol = 2, widths = c(0.8, 1.5))

combined_plots <- combined_plots +
  plot_annotation(
    title = title_text,
    subtitle = subtitle_text,
    caption = caption_text,
    theme = theme(
      plot.title = element_text(
        size = rel(1.6),
        family = fonts$title,
        face = "bold",
        color = colors$title,
        lineheight = 1.1,
        margin = margin(t = 5, b = 5)
      ),
      plot.subtitle = element_markdown(
        size = rel(0.9),
        family = 'sans',
        color = alpha(colors$subtitle, 0.9),
        lineheight = 1.2,
        margin = margin(t = 5, b = 10)
      ),
      plot.caption = element_markdown(
        size = rel(0.6),
        family = fonts$caption,
        color = colors$caption,
        hjust = 0.5,
        margin = margin(t = 05)
      )
    )
  )
```

#### 7. Save

```{r}
#| label: save
#| warning: false

### |-  plot image ----  
save_plot_patchwork(
  plot = combined_plots, 
  type = "makeovermonday", 
  year = current_year,
  week = current_week,
  width = 16, 
  height = 9
  )
```

#### 8. Session Info

::: {.callout-tip collapse="true"}
##### Expand for Session Info

```{r, echo = FALSE}
#| eval: true
#| warning: false

sessionInfo()
```
:::

#### 9. GitHub Repository

::: {.callout-tip collapse="true"}
##### Expand for GitHub Repo

The complete code for this analysis is available in `r create_link(project_file, repo_file)`.

For the full repository, `r create_link("click here", repo_main)`.
:::

#### 10. References

::: {.callout-tip collapse="true"}
##### Expand for References

1.  Data:

-   Makeover Monday `r current_year` Week `r current_week`: `r create_link("Better things come to those who wait", data_main)`

2.  Article

-   `r create_link("Better things come to those who wait", data_secondary)`
:::

#### 11. Custom Functions Documentation

::: {.callout-note collapse="true"}
##### 📦 Custom Helper Functions

This analysis uses custom functions from my personal module library for efficiency and consistency across projects.

**Functions Used:**

-   **`fonts.R`**: `setup_fonts()`, `get_font_families()` - Font management with showtext
-   **`social_icons.R`**: `create_social_caption()` - Generates formatted social media captions
-   **`image_utils.R`**: `save_plot()` - Consistent plot saving with naming conventions
-   **`base_theme.R`**: `create_base_theme()`, `extend_weekly_theme()`, `get_theme_colors()` - Custom ggplot2 themes

**Why custom functions?**\
These utilities standardize theming, fonts, and output across all my data visualizations. The core analysis (data tidying and visualization logic) uses only standard tidyverse packages.

**Source Code:**\
View all custom functions → [GitHub: R/utils](https://github.com/poncest/personal-website/tree/master/R)
:::

© 2024 Steven Ponce

Source Issues