---
title: "The Stakes for U.S. Truck Manufacturing Under Shifting Federal Policy"
subtitle: "Employment impact analysis across clean vehicle scenarios and policy retreat alternatives"
description: "Analysis of how federal clean vehicle policies could create up to 19,122 manufacturing jobs or eliminate 3,332 jobs under policy retreat. Using EPI economic modeling data, this dual-chart visualization reveals the employment consequences of different policy scenarios across assembly and supply chain sectors in America's truck manufacturing industry."
author: "Steven Ponce"
date: "2025-06-02"
categories: ["MakeoverMonday", "2025"]
tags: [
"economic-policy", "manufacturing-jobs", "clean-vehicles", "employment-analysis",
"heatmap", "waterfall-chart", "policy-impact", "truck-manufacturing", "ggplot2",
"patchwork", "federal-policy", "EPI-data", "supply-chain", "union-wages"
]
image: "thumbnails/mm_2025_23.png"
format:
html:
toc: true
toc-depth: 5
code-link: true
code-fold: true
code-tools: true
code-summary: "Show code"
editor_options:
chunk_output_type: inline
execute:
freeze: true
cache: true
error: false
message: false
warning: false
eval: true
---
### Original
The original visualization **Figure B: Change in employment from baseline scenario, Job-years, 2024–2032** comes from [Economic Policy Institute+](https://www.epi.org/publication/future-clean-trucks-buses/)

### Makeover
{#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
lubridate, # Make Dealing with Dates a Little Easier
patchwork # The Composer of Plots
)
})
### |- figure size ----
camcorder::gg_record(
dir = here::here("temp_plots"),
device = "png",
width = 10,
height = 12,
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
## Original Chart
# Change in employment from baseline scenario, Job-years, 2024–2032, Figure B
# https://www.epi.org/publication/future-clean-trucks-buses/
## Article
# What future will U.S. truck manufacturing have under Trump?
# https://www.epi.org/publication/future-clean-trucks-buses/
## Data
# What future will U.S. truck manufacturing have under Trump?
# https://data.world/makeovermonday/what-future-will-us-truck-manufacturing-have-under-trump
truck_manufacturing_raw <- readxl::read_excel(
here::here('data/MakeoverMonday/2025/US truck manufacturing under Trump.xlsx')) |>
janitor::clean_names()
```
#### 3. Examine the Data
```{r}
#| label: examine
#| include: true
#| eval: true
#| results: 'hide'
#| warning: false
glimpse(truck_manufacturing_raw)
skimr::skim(truck_manufacturing_raw)
```
#### 4. Tidy Data
```{r}
#| label: tidy
#| warning: false
# P1. heatmap data----
heatmap_data <- truck_manufacturing_raw |>
mutate(
scenario_clean = case_when(
str_detect(scenario, "policy retreat") ~ "Policy Retreat",
str_detect(scenario, "union wages") ~ "Clean + Market + Union",
str_detect(scenario, "50% clean.*10% market") ~ "Clean + Market Share",
str_detect(scenario, "Baseline.*10%") ~ "Baseline + Market",
str_detect(scenario, "50% clean") ~ "50% Clean Vehicles",
TRUE ~ scenario
),
# Order scenarios logically
scenario_factor = factor(scenario_clean,
levels = c(
"50% Clean Vehicles",
"Baseline + Market",
"Clean + Market Share",
"Clean + Market + Union",
"Policy Retreat"
)
)
) |>
# Transform to long format
select(scenario_factor, assembly, supply_chain, total) |>
pivot_longer(
cols = -scenario_factor,
names_to = "job_category",
values_to = "jobs"
) |>
# Create clean category labels
mutate(
job_category_clean = case_when(
job_category == "assembly" ~ "Assembly",
job_category == "supply_chain" ~ "Supply Chain",
job_category == "total" ~ "Total"
),
job_category_factor = factor(job_category_clean,
levels = c("Assembly", "Supply Chain", "Total")
),
# Create scaled values for color intensity (-1 to 1 scale)
jobs_scaled = case_when(
jobs < 0 ~ pmax(jobs / abs(min(jobs)), -1), # negative values
jobs > 0 ~ pmin(jobs / max(jobs), 1), # positive values
TRUE ~ 0
),
# Text color based on intensity
text_color = ifelse(abs(jobs_scaled) > 0.6, "white", "black"),
# Formatted labels
jobs_label = case_when(
jobs == 0 ~ "0",
jobs > 0 ~ paste0("+", comma(jobs, accuracy = 1)),
jobs < 0 ~ comma(jobs, accuracy = 1)
)
)
# P2. waterfall chart data -----
waterfall_data <- tibble(
category = c(
"Baseline", "50% Clean Vehicles", "Add Market Share",
"Add Union Wages", "Policy Retreat Alternative"
),
# Calculate incremental values
value = c(
0, # Baseline starting point
truck_manufacturing_raw$total[1], # 50% clean vehicles: 11,182
truck_manufacturing_raw$total[2] - truck_manufacturing_raw$total[1], # Market share increment: 13,272 - 11,182 = 2,090
truck_manufacturing_raw$total[4] - truck_manufacturing_raw$total[3], # Union wages increment: 19,122 - 15,441 = 3,681
truck_manufacturing_raw$total[5] # Policy retreat: -3,332 (standalone negative scenario)
),
type = c("baseline", "positive", "positive", "positive", "negative")
) |>
# Calculate cumulative totals and bar positioning
mutate(
cumulative = case_when(
category == "Policy Retreat Alternative" ~ value,
TRUE ~ cumsum(value)
),
# Calculate where each bar should start and end
ymin = case_when(
category == "Baseline" ~ 0,
category == "Policy Retreat Alternative" ~ 0,
TRUE ~ lag(cumulative, default = 0)
),
ymax = case_when(
category == "Policy Retreat Alternative" ~ value,
TRUE ~ cumulative
),
# Labels
category_label = case_when(
category == "Baseline" ~ "Baseline\n(Starting Point)",
category == "50% Clean Vehicles" ~ "50% Clean\nVehicles",
category == "Add Market Share" ~ "Increase\nMarket Share",
category == "Add Union Wages" ~ "Add Union\nWages",
category == "Policy Retreat Alternative" ~ "Policy Retreat\n(Alternative Path)"
),
category = factor(category, levels = category),
category_label = factor(category_label, levels = category_label)
)
```
#### 5. Visualization Parameters
```{r}
#| label: params
#| include: true
#| warning: false
### |- plot aesthetics ----
# Get base colors with custom palette
colors <- get_theme_colors(palette = c(
"#DC143C", "#FFFFFF", "#2E8B57", # heatmap
"baseline" = "#708090", "positive" = "#2E8B57", "negative" = "#DC143C" # waterfall chart
))
### |- titles and caption ----
title_text <- str_glue("The Stakes for U.S. Truck Manufacturing Under Shifting Federal Policy")
subtitle_text <- str_glue("Employment impact analysis across clean vehicle scenarios and policy retreat alternatives")
# Create caption
caption_text <- create_mm_caption(
mm_year = 2025,
mm_week = 23,
source_text = "<br>EPI analysis of S&P Global (2024), IMPLAN (2024), and FRED data | Job-years, 2024-2032"
)
### |- 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(
# Legend formatting
legend.position = "plot",
legend.title = element_text(face = "bold"),
axis.ticks.y = element_blank(),
axis.ticks.x = element_line(color = "gray", linewidth = 0.5),
axis.ticks.length = unit(0.2, "cm"),
# Axis formatting
axis.title.x = element_text(face = "bold", size = rel(0.85)),
axis.title.y = element_text(face = "bold", size = rel(0.85)),
axis.text.y = element_text(face = "bold", size = rel(0.85)),
# Grid lines
panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank(),
# Margin
plot.margin = margin(20, 20, 20, 20)
)
)
# Set theme
theme_set(weekly_theme)
```
#### 6. Plot
```{r}
#| label: plot
#| warning: false
# P1. Heatmap ----
p1 <- heatmap_data |>
ggplot(aes(x = job_category_factor, y = scenario_factor)) +
# Geoms
geom_tile(aes(fill = jobs_scaled),
color = "white",
linewidth = 1.5,
alpha = 0.9
) +
geom_text(aes(label = jobs_label, color = text_color),
fontface = "bold",
size = 4.2
) +
# Scales
scale_fill_gradient2(
low = colors$palette[1],
mid = colors$palette[2],
high = colors$palette[3],
midpoint = 0,
name = "Job Impact\nIntensity",
labels = c("High Loss", "Neutral", "High Gain"),
breaks = c(-1, 0, 1),
guide = guide_colorbar(
title.position = "top",
title.hjust = 0.5,
barwidth = 1.2,
barheight = 8,
frame.colour = "gray70",
ticks.colour = "gray70"
)
) +
scale_color_identity() +
scale_x_discrete(expand = c(0, 0)) +
scale_y_discrete(expand = c(0, 0)) +
# Labs
labs(
subtitle = "Job breakdown shows potential gains and losses across Assembly and Supply Chain sectors",
x = "Job Category",
y = "Policy Scenario"
) +
# Theme
theme_minimal(base_size = 11) +
theme(
plot.subtitle = element_text(
size = 12,
color = "gray40",
margin = margin(b = 15)
),
axis.title = element_text(
size = 10,
face = "bold",
color = "gray30"
),
axis.title.x = element_text(margin = margin(t = 10)),
axis.title.y = element_text(margin = margin(r = 10)),
axis.text = element_text(
size = 9,
color = "gray20",
face = "bold"
),
legend.position = "right",
legend.title = element_text(
size = 9,
face = "bold",
color = "gray30"
),
legend.text = element_text(
size = 8,
color = "gray40"
),
legend.margin = margin(l = 15),
panel.grid = element_blank(),
plot.margin = margin(10, 15, 15, 15),
plot.background = element_rect(fill = colors$background, color = NA),
panel.background = element_rect(fill = colors$background, color = NA)
)
# P2. waterfall chart -----
p2 <- waterfall_data |>
ggplot(aes(x = category_label)) +
# Geoms
geom_segment(
data = waterfall_data[2:4, ],
aes(
x = as.numeric(category_label) + 0.45,
xend = as.numeric(category_label) + 1 - 0.45,
y = ymax,
yend = ymax
),
linetype = "dashed", alpha = 0.6, linewidth = 0.8, color = "gray50"
) +
geom_rect(
aes(
xmin = as.numeric(category_label) - 0.35,
xmax = as.numeric(category_label) + 0.35,
ymin = ymin,
ymax = ymax,
fill = type
),
alpha = 0.9, color = "white", linewidth = 0.5
) +
geom_text(
aes(
y = (ymin + ymax) / 2,
label = ifelse(value == 0, "0",
paste0(ifelse(value > 0, "+", ""), comma(value, accuracy = 1))
)
),
fontface = "bold", size = 4, color = "white"
) +
geom_text(
data = filter(waterfall_data, type != "baseline" & type != "negative"),
aes(
y = ymax + 500,
label = paste("Total:", comma(cumulative, accuracy = 1))
),
fontface = "bold", size = 3.5, color = "gray30"
) +
geom_hline(yintercept = 0, color = "black", linewidth = 1, alpha = 0.8) +
# Scales
scale_fill_manual(values = colors$palette, guide = "none") +
scale_y_continuous(
labels = function(x) {
case_when(
x >= 0 ~ paste0("+", comma(x)),
x < 0 ~ comma(x),
TRUE ~ as.character(x)
)
},
breaks = pretty_breaks(n = 6),
expand = expansion(mult = c(0.15, 0.1))
) +
scale_x_discrete() +
# Labs
labs(
subtitle = "Policy scenarios build progressively from baseline to maximum employment potential",
x = NULL,
y = "Change in Employment (Job-years)"
) +
# Theme
theme_minimal(base_size = 11) +
theme(
plot.subtitle = element_text(
size = 12,
color = "gray40",
margin = margin(b = 15)
),
axis.title.y = element_text(
size = 10,
face = "bold",
color = "gray30",
margin = margin(r = 15)
),
axis.text.x = element_text(
size = 9,
face = "bold",
color = "gray20",
lineheight = 1.1
),
axis.text.y = element_text(
size = 9,
color = "gray30"
),
panel.grid.major.x = element_blank(),
panel.grid.minor = element_blank(),
panel.grid.major.y = element_line(color = "gray90", linewidth = 0.5),
plot.margin = margin(15, 15, 10, 15),
plot.background = element_rect(fill = colors$background, color = NA),
panel.background = element_rect(fill = colors$background, color = NA)
)
# Combined Plot ----
combined_plot <- p1 / p2 +
plot_annotation(
title = title_text,
subtitle = subtitle_text,
caption = caption_text,
theme = theme(
plot.title = element_markdown(
size = rel(1.6),
family = fonts$title,
face = "bold",
color = colors$title,
lineheight = 1.1,
margin = margin(t = 5, b = 5)
),
plot.subtitle = element_text(
size = rel(1),
family = fonts$subtitle,
color = alpha(colors$subtitle, 0.9),
lineheight = 0.9,
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 = 10)
),
plot.margin = margin(25, 25, 25, 25),
)
)
```
#### 7. Save
```{r}
#| label: save
#| warning: false
### |- plot image ----
save_plot_patchwork(
plot = combined_plot,
type = "makeovermonday",
year = 2025,
week = 23,
width = 10,
height = 12
)
```
#### 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 [`mm_2025_23.qmd`](https://github.com/poncest/personal-website/blob/master/data_visualizations/MakeoverMonday/2025/mm_2025_23.qmd).
For the full repository, [click here](https://github.com/poncest/personal-website/).
:::
#### 10. References
::: {.callout-tip collapse="true"}
##### Expand for References
1. Data:
- Makeover Monday 2025 Week 23: [What future will U.S. truck manufacturing have under Trump?](https://data.world/makeovermonday/what-future-will-us-truck-manufacturing-have-under-trump)
2. Article
- Economic Policy Institute: [What future will U.S. truck manufacturing have under Trump?](https://www.epi.org/publication/future-clean-trucks-buses/)
:::