#Data 608 Story 6
library(tidyverse)
library(janitor)
library(scales)
library(ggplot2)
library(ggtext)
library(ggrepel)
library(patchwork)
library(ggtext)
library(maps)
library(mapproj)
dir.create("plots", showWarnings = FALSE)
# Theme --------------------------------------------------------------------
bg <- "#0f1923"
bg_panel <- "#131f2b"
clr_spine <- "#1e2d3d"
clr_g_bright <- "#2ecc71"
clr_g_fill <- "#1a4a2e"
clr_g_bar_fill <- "#0d3320"
clr_r_bright <- "#e74c3c"
clr_r_fill <- "#4a1a1a"
clr_r_bar_fill <- "#3d0d0d"
clr_text <- "#dce6f0"
clr_text2 <- "#7f99b2"
clr_gold <- "#f1c40f"
clr_gold_bg <- "#1a1200"
theme_hunger_dark <- function(base_size = 13) {
theme_minimal(base_size = base_size) %+replace%
theme(
plot.background = element_rect(fill = bg, color = NA),
panel.background = element_rect(fill = bg_panel, color = NA),
panel.grid.major = element_line(color = clr_spine, linewidth = 0.35),
panel.grid.minor = element_blank(),
plot.title = element_text(face = "bold", size = base_size * 1.45,
color = clr_text, margin = margin(b = 6)),
plot.subtitle = element_text(size = base_size * 0.95, color = clr_text2,
margin = margin(b = 12), lineheight = 1.35),
plot.caption = element_text(size = base_size * 0.72, color = clr_text2,
hjust = 0, margin = margin(t = 10)),
axis.title = element_text(size = base_size * 0.9, color = clr_text2,
face = "bold"),
axis.text = element_text(size = base_size * 0.82, color = clr_text2),
legend.background = element_rect(fill = bg, color = NA),
legend.position = "bottom",
legend.title = element_text(face = "bold", color = clr_text),
legend.text = element_text(color = clr_text2),
strip.background = element_rect(fill = clr_spine, color = NA),
strip.text = element_text(face = "bold", color = clr_text,
size = base_size * 0.9),
plot.margin = margin(16, 20, 12, 16)
)
}
theme_set(theme_hunger_dark())
# Data loading --------------------------------------------------------------------
food_insecurity_state <- read_csv("data/food_insecurity_state.csv") |>
clean_names() |>
mutate(fi_rate_pct = fi_rate / 100, vlfs_rate_pct = vlfs_rate / 100)
poverty_state <- read_csv("data/poverty_state.csv") |> clean_names()
demographics <- read_csv("data/demographics.csv") |> clean_names()
pipeline <- read_csv("data/pipeline.csv") |> clean_names() |>
mutate(stage_label = factor(stage_label, levels = stage_label))
cost_data <- read_csv("data/cost_data.csv") |> clean_names()
trend_data <- read_csv("data/trend_data.csv") |> clean_names()
combined <- food_insecurity_state |>
left_join(poverty_state |> select(state_abbr, poverty_rate), by = "state_abbr")
cor_val <- cor(combined$poverty_rate, combined$fi_rate, use = "complete.obs")
cat(sprintf("Pearson r: %.3f\n", cor_val))
## Pearson r: 0.968
# Slide 1 : Map --------------------------------------------------------------------
us_map <- map_data("state") |> mutate(state = str_to_title(region))
map_data_full <- us_map |>
left_join(
food_insecurity_state |>
mutate(state = str_to_title(state)) |>
select(state, fi_rate, vlfs_rate, region),
by = "state"
)
state_labels <- food_insecurity_state |>
mutate(state_lower = str_to_lower(state)) |>
left_join(
map_data("state") |>
group_by(region) |>
summarise(long = mean(long), lat = mean(lat), .groups = "drop"),
by = c("state_lower" = "region")
)
p_slide1 <- ggplot(map_data_full, aes(x = long, y = lat, group = group)) +
geom_polygon(aes(fill = fi_rate), color = bg, linewidth = 0.35) +
geom_text(
data = state_labels |> filter(fi_rate >= 16),
aes(x = long, y = lat,
label = paste0(state_abbr, "\n", fi_rate, "%"), group = NULL),
size = 2.8, fontface = "bold", color = "white"
) +
coord_map("albers", lat0 = 39, lat1 = 45) +
scale_fill_gradient(
low = "#f5e6e6", # near-white with a warm tint
high = "#c0392b", # deep red for highest severity
name = "Food Insecurity Rate (%)", labels = label_number(suffix = "%")
) +
labs(
title = "Hunger Is Here",
subtitle = paste0(
"47 million Americans struggle to put food on the table : right here, right now.\n",
"Darkest states exceed 19% food insecurity. National average: 13.5%."
),
caption = ""
) +
theme_void(base_size = 13) +
theme(
plot.background = element_rect(fill = bg, color = NA),
panel.background = element_rect(fill = bg, color = NA),
plot.title = element_text(face = "bold", size = 18, color = clr_text,
margin = margin(l = 10, t = 10, b = 4)),
plot.subtitle = element_text(size = 12, color = clr_text2,
margin = margin(l = 10, b = 10), lineheight = 1.35),
plot.caption = element_text(size = 9, color = clr_text2,
margin = margin(b = 8, l = 10)),
legend.background = element_rect(fill = bg, color = NA),
legend.position = "bottom",
legend.title = element_text(face = "bold", color = clr_text),
legend.text = element_text(color = clr_text2),
legend.key.width = unit(2.5, "cm"),
plot.margin = margin(10, 10, 10, 10)
)
p_slide1

ggsave("plots/slide1_map.png", p_slide1, width = 12, height = 7, dpi = 150, bg = bg)
# Slikde 2 : Scatterplot --------------------------------------------------------------------
p_slide2 <- ggplot(combined, aes(x = poverty_rate, y = fi_rate)) +
# Crisis zone
annotate("rect", xmin = 14, xmax = Inf, ymin = 15, ymax = Inf,
fill = clr_r_bright, alpha = 0.07) +
# Crisis zone label
annotate("text", x = 19.5, y = 16.0, label = "CRISIS\nZONE",
size = 3.5, color = clr_r_bright, fontface = "bold",
alpha = 0.7, hjust = 1, vjust = 0) +
geom_smooth(method = "lm", se = TRUE, color = clr_text2,
fill = clr_spine, alpha = 0.35, linewidth = 1.2) +
geom_point(aes(fill = fi_rate), shape = 21, size = 4.5,
color = "#00000040", stroke = 0.3, alpha = 0.95) +
geom_text_repel(
data = combined |> filter(fi_rate > 15.5 | fi_rate < 9.3),
aes(label = state_abbr), size = 3.3, fontface = "bold", color = clr_text,
box.padding = 0.45, max.overlaps = 25,
segment.color = clr_text2, segment.size = 0.35
) +
scale_fill_gradient(
low = "#f5deb3", # wheat / near-white warm
high = "#c0392b", # deep red matching map
name = "Food Insecurity\nRate (%)",
guide = guide_colorbar(title.position = "top", barwidth = 8, barheight = 0.5)
) +
scale_x_continuous(labels = label_percent(scale = 1), breaks = seq(6, 20, 2)) +
scale_y_continuous(labels = label_percent(scale = 1), breaks = seq(8, 20, 2)) +
annotate("label", x = 7.5, y = 17.3,
label = sprintf("Pearson r = %.2f\nStrong positive\ncorrelation", cor_val),
hjust = 0, size = 3.8, color = clr_text, fontface = "bold",
fill = clr_spine, label.color = clr_text2,
label.size = 0.4, label.padding = unit(0.4, "lines")) +
labs(
title = "And Its Structural",
subtitle = "State poverty rates and food insecurity move hand in hand across all U.S. regions",
x = "Poverty Rate : Census ACS 2023 (%)",
y = "Food Insecurity Rate : USDA ERS 2022–24 (%)",
caption = "Sources: U.S. Census Bureau ACS 2023; USDA Economic Research Service 2022-2024"
)
print(p_slide2)

ggsave("plots/slide2_scatter.png", p_slide2, width = 11, height = 7, dpi = 150, bg = bg)
# Slide 3 : Vulnerable demographics --------------------------------------------------------------------
p_slide3 <- demographics |>
filter(category == "Household Type") |>
mutate(
group = fct_reorder(group, fi_rate),
# flag the children-present households for extra emphasis
has_child = str_detect(group, "children")
) |>
ggplot(aes(x = fi_rate, y = group, fill = fi_rate)) +
geom_col(width = 0.62, show.legend = FALSE) +
# National average reference line
geom_vline(xintercept = 13.5, linetype = "dashed",
color = clr_text2, linewidth = 0.7, alpha = 0.8) +
# Dollar labels just beyond bar tip
geom_text(aes(x = fi_rate + 0.6, label = paste0(fi_rate, "%")),
hjust = 0, fontface = "bold", size = 5.0, color = clr_text) +
# National avg annotation
annotate("text",
x = 14.0, y = 1.45,
label = "National avg\n13.5%",
color = clr_text2, fontface = "italic",
size = 3.2, hjust = 0, lineheight = 0.9) +
scale_fill_gradient(low = "#b7950b", high = clr_r_bright) +
scale_x_continuous(
limits = c(0, 46),
labels = label_percent(scale = 1),
expand = c(0, 0)
) +
labs(
title = "It Falls On Our Most Vulnerable",
subtitle = paste0(
"Households with children, especially single-mother homes \n",
" face rates more than double the national average."
),
x = "Food Insecurity Rate (%)",
y = NULL,
caption = "Source: USDA ERS Household Food Security in the United States, 2023"
) +
theme_hunger_dark(base_size = 14) +
theme(
panel.background = element_rect(fill = bg, color = NA),
panel.grid.major.y = element_blank(),
panel.grid.major.x = element_line(color = clr_spine, linewidth = 0.3),
axis.text.y = element_text(size = 12.5, color = clr_text,
face = "bold"),
axis.text.x = element_text(size = 11, color = clr_text2),
plot.margin = margin(16, 40, 12, 16)
)
print(p_slide3)

ggsave("plots/slide3_demographics.png", p_slide3,
width = 12, height = 6, dpi = 150, bg = bg)
# Slide 4 : Dependancy pipeline --------------------------------------------------------------------
n_stages <- nrow(pipeline)
ribbon_df <- map_dfr(seq_len(n_stages - 1), function(i) {
tibble(x = c(i, i, i+1, i+1),
y = c(0, pipeline$value[i], pipeline$value[i+1], 0),
grp = as.character(i))
})
p_slide4 <- ggplot() +
geom_polygon(data = ribbon_df, aes(x = x, y = y, group = grp),
fill = clr_r_bright, alpha = 0.15) +
geom_col(data = pipeline |> mutate(x = as.integer(stage_label)),
aes(x = x, y = value, fill = value),
width = 0.55, show.legend = FALSE) +
geom_text(data = pipeline |> mutate(x = as.integer(stage_label)),
aes(x = x, y = value / 2, label = paste0(value, "%")),
color = "white", fontface = "bold", size = 4.5) +
geom_text(data = pipeline |> mutate(x = as.integer(stage_label)),
aes(x = x, y = value + 5, label = annotation),
color = clr_text2, size = 3.0, lineheight = 0.9, fontface = "italic") +
scale_x_continuous(breaks = seq_len(n_stages), labels = levels(pipeline$stage_label)) +
scale_fill_gradient(low = "#e67e22", high = clr_r_bright) +
scale_y_continuous(limits = c(0, 130), labels = NULL) +
annotate("segment", x = 1.05, xend = 5.95, y = 112, yend = 112,
arrow = arrow(ends = "last", length = unit(0.25, "cm"), type = "closed"),
color = clr_text2, linewidth = 0.9) +
annotate("text", x = 3.5, y = 121,
label = "The hunger-to-dependency pipeline",
color = clr_text, fontface = "bold", size = 4.2) +
labs(
title = "And Traps Our Children",
subtitle = paste0(
"A Hungry Child Becomes a Dependent Adult\n",
"Food insecurity in childhood triggers a cascade of disadvantages."
),
x = NULL, y = NULL,
caption = "Sources: Health Affairs (2015); Children's Health Watch; CBPP (2024); USDA ERS"
) +
theme(axis.text.x = element_text(size = 10, color = clr_text,
face = "bold", lineheight = 0.9),
panel.grid.major.x = element_blank())
print(p_slide4)

ggsave("plots/slide4_pipeline.png", p_slide4, width = 13, height = 7, dpi = 150, bg = bg)
# Slide 5 : Program by program --------------------------------------------------------------------
invest_bars <- cost_data |>
filter(type == "Investment") |>
arrange(amount_billions) |> # smallest at top
mutate(category = fct_inorder(category))
conseq_bars <- cost_data |>
filter(type == "Consequence") |>
arrange(amount_billions) |> # smallest at top
mutate(category = fct_inorder(category))
invest_total <- cost_data |> filter(type == "Investment") |> pull(amount_billions) |> sum()
conseq_total <- cost_data |> filter(type == "Consequence") |> pull(amount_billions) |> sum()
# Shared x limit — a little beyond the largest single bar
x_max_shared <- max(cost_data$amount_billions) * 1.18
# Investment -------------------
p_invest <- ggplot(invest_bars,
aes(x = amount_billions, y = category)) +
geom_col(fill = clr_g_fill, color = clr_g_bright,
linewidth = 0.7, width = 0.62) +
geom_text(aes(x = amount_billions / 2,
label = paste0("$", amount_billions, "B")),
color = clr_g_bright, fontface = "bold",
size = 4.2, hjust = 0.5) +
scale_x_continuous(
limits = c(0, x_max_shared),
expand = c(0, 0),
labels = label_dollar(suffix = "B", prefix = "$")
) +
scale_y_discrete(expand = expansion(add = c(0.8, 0.5))) +
labs(
title = "What We Could Invest In",
subtitle = "Annual federal nutrition program budgets",
x = NULL, y = NULL
) +
theme_hunger_dark(base_size = 13) +
theme(
panel.background = element_rect(fill = bg, color = NA),
panel.grid.major.y = element_blank(),
panel.grid.major.x = element_line(color = clr_spine, linewidth = 0.3),
axis.text.y = element_text(size = 11, color = clr_text,
face = "bold", hjust = 1),
axis.text.x = element_blank(),
plot.title = element_text(color = clr_g_bright, face = "bold",
size = 15, margin = margin(b = 4)),
plot.subtitle = element_text(color = clr_text2, size = 10,
margin = margin(b = 10)),
plot.margin = margin(12, 16, 24, 16)
)
# Consequences ------------------------------------
p_conseq <- ggplot(conseq_bars,
aes(x = amount_billions, y = category)) +
geom_col(fill = clr_r_fill, color = clr_r_bright,
linewidth = 0.7, width = 0.62) +
geom_text(aes(x = amount_billions / 2,
label = paste0("$", amount_billions, "B")),
color = clr_r_bright, fontface = "bold",
size = 4.2, hjust = 0.5) +
scale_x_continuous(
limits = c(0, x_max_shared),
expand = c(0, 0),
labels = label_dollar(suffix = "B", prefix = "$")
) +
scale_y_discrete(expand = expansion(add = c(0.8, 0.5))) +
labs(
title = "What We Lose",
subtitle = "Annual downstream costs of child food insecurity",
x = NULL, y = NULL
) +
theme_hunger_dark(base_size = 13) +
theme(
panel.background = element_rect(fill = bg, color = NA),
panel.grid.major.y = element_blank(),
panel.grid.major.x = element_line(color = clr_spine, linewidth = 0.3),
axis.text.y = element_text(size = 11, color = clr_text,
face = "bold", hjust = 1),
axis.text.x = element_blank(),
plot.title = element_text(color = clr_r_bright, face = "bold",
size = 15, margin = margin(b = 4)),
plot.subtitle = element_text(color = clr_text2, size = 10,
margin = margin(b = 10)),
plot.margin = margin(12, 16, 24, 16)
)
# Both together
p_slide5_bars <- (p_conseq | p_invest) +
plot_annotation(
title = paste0(
"The Consequences of <span style='color:", clr_r_bright, ";'>Inaction</span>",
" : Generational<br>",
"The Cost of <span style='color:", clr_g_bright, ";'>Action</span>",
" : A Modest Budget Line"
),
subtitle = "",
caption = "Sources: USDA FNS FY2024; CBPP 2024; Children's Health Watch; Health Affairs (2015)",
theme = theme(
plot.background = element_rect(fill = bg, color = NA),
plot.title = element_markdown(face = "bold", size = 18,
color = clr_text, hjust = 0.5,
margin = margin(b = 6),
lineheight = 1.4),
plot.subtitle = element_text(size = 11, color = clr_text2,
hjust = 0.5, margin = margin(b = 12)),
plot.caption = element_text(size = 9, color = clr_text2, hjust = 0)
)
)
print(p_slide5_bars)

ggsave("plots/slide5_breakdown.png", p_slide5_bars,
width = 13, height = 7, dpi = 150, bg = bg)
# Slide 6 --------------------------------------------------------------------
invest_total <- cost_data |> filter(type == "Investment") |> pull(amount_billions) |> sum()
consequence_total <- cost_data |> filter(type == "Consequence") |> pull(amount_billions) |> sum()
roi_ratio <- round(consequence_total / invest_total, 1)
gap_b <- 18 # visible gap = 18B equivalent height — big enough to see clearly
# Investment bar positions
invest_programs <- cost_data |>
filter(type == "Investment") |>
arrange(desc(amount_billions)) |>
mutate(
xc = c(3, 1.5, 4.5),
bar_w = 0.70,
ymin = gap_b,
ymax = gap_b + amount_billions,
dollar_y = gap_b + amount_billions / 2, # center of bar
name_y = gap_b + amount_billions + 6 # just above bar tip
)
# Consequence bar positions
conseq_programs <- cost_data |>
filter(type == "Consequence") |>
arrange(desc(amount_billions)) |>
mutate(
# 4 bars spread across x = 1 to 5
xc = c(3, 1.8, 4.2, 0.6),
bar_w = 0.70,
ymin = -(gap_b + amount_billions),
ymax = -gap_b,
dollar_y = -(gap_b + amount_billions / 2), # center of bar
name_y = -(gap_b + amount_billions + 6) # just below bar tip
)
y_top <- gap_b + max(invest_programs$amount_billions) + 28
y_bot <- -(gap_b + max(conseq_programs$amount_billions) + 28)
x_left <- -0.3
x_right <- 6.0
p_slide5_bars <- ggplot() +
# Middle gap
annotate("rect",
xmin = x_left, xmax = 5.5,
ymin = -gap_b, ymax = gap_b,
fill = bg, color = NA) +
# Gap label
annotate("text",
x = 2.5, y = 0,
label = "— this is the gap of inaction —",
color = clr_text2, fontface = "italic",
size = 4.5, hjust = 0.5, vjust = 0.5) +
# Upper gap line
annotate("segment",
x = x_left, xend = 5.5,
y = gap_b, yend = gap_b,
color = clr_g_bright, linewidth = 0.7, linetype = "solid") +
# Lower gap line
annotate("segment",
x = x_left, xend = 5.5,
y = -gap_b, yend = -gap_b,
color = clr_r_bright, linewidth = 0.7, linetype = "solid") +
# Investent bars
geom_rect(data = invest_programs,
aes(xmin = xc - bar_w / 2, xmax = xc + bar_w / 2,
ymin = ymin, ymax = ymax),
fill = clr_g_fill, color = clr_g_bright, linewidth = 0.8) +
geom_text(data = invest_programs,
aes(x = xc, y = dollar_y,
label = paste0("$", amount_billions, "B")),
color = clr_g_bright, fontface = "bold",
size = 5.0, hjust = 0.5) +
geom_text(data = invest_programs,
aes(x = xc, y = name_y, label = category),
color = clr_text, fontface = "bold",
size = 3.4, hjust = 0.5, vjust = 0, lineheight = 0.92) +
# Consequence bars
geom_rect(data = conseq_programs,
aes(xmin = xc - bar_w / 2, xmax = xc + bar_w / 2,
ymin = ymin, ymax = ymax),
fill = clr_r_fill, color = clr_r_bright, linewidth = 0.8) +
geom_text(data = conseq_programs,
aes(x = xc, y = dollar_y,
label = paste0("$", amount_billions, "B")),
color = clr_r_bright, fontface = "bold",
size = 5.0, hjust = 0.5) +
geom_text(data = conseq_programs,
aes(x = xc, y = name_y, label = category),
color = clr_text, fontface = "bold",
size = 3.4, hjust = 0.5, vjust = 1, lineheight = 0.92) +
# Scales
scale_x_continuous(limits = c(x_left, 7.4), expand = c(0, 0)) +
scale_y_continuous(limits = c(y_bot, y_top), expand = c(0, 0)) +
labs(x = NULL, y = NULL) +
theme_void() +
theme(
plot.background = element_rect(fill = bg, color = NA),
plot.margin = margin(4, 8, 4, 8)
)
# Header
p_s5_header <- ggplot() +
theme_void() +
theme(plot.background = element_rect(fill = bg, color = NA)) +
annotate("text",
x = 0.5, y = 0.80,
label = "The Choice Is Clear",
color = clr_text, fontface = "bold", size = 6.5, hjust = 0.5) +
annotate("segment",
x = 0.05, xend = 0.95, y = 0.44, yend = 0.44,
color = clr_spine, linewidth = 0.6) +
annotate("richtext",
x = 0.5, y = 0.18,
label = paste0(
"<span style='color:", clr_g_bright, ";'>$", invest_total,
"B invested in nutrition programs</span>",
" OR ",
"<span style='color:", clr_r_bright, ";'>$", consequence_total,
"B lost in downstream consequences</span>",
" : every year."
),
color = clr_text, size = 4.1,
hjust = 0.5, fill = NA, label.color = NA) +
scale_x_continuous(limits = c(0, 1), expand = c(0, 0)) +
scale_y_continuous(limits = c(0, 1), expand = c(0, 0))
# Caption
p_s5_caption <- ggplot() +
theme_void() +
theme(plot.background = element_rect(fill = bg, color = NA)) +
annotate("text",
x = 0.03, y = 0.5,
label = paste0(
"Sources: USDA FNS FY2024; CBPP 2024; Children's Health Watch; ",
"Robert Wood Johnson Foundation; Health Affairs (2015)"
),
color = clr_text2, size = 3.0, hjust = 0) +
scale_x_continuous(limits = c(0, 1), expand = c(0, 0)) +
scale_y_continuous(limits = c(0, 1), expand = c(0, 0))
# Together
p_slide5 <- p_s5_header / p_slide5_bars / p_s5_caption +
plot_layout(heights = c(0.9, 8.5, 0.3))
print(p_slide5)

ggsave("plots/slide5_dome.png", p_slide5,
width = 13, height = 11, dpi = 150, bg = bg)