diff --git a/_projects/2025/100570484/100570484.Rmd b/_projects/2025/100570484/100570484.Rmd
new file mode 100644
index 00000000..b827db1d
--- /dev/null
+++ b/_projects/2025/100570484/100570484.Rmd
@@ -0,0 +1,466 @@
+---
+title: "Changing Fortunes"
+description: Financial Times Graph Replication
+categories: "2025"
+author: Tommaso Accornero
+date: "`r Sys.Date()`"
+output:
+ distill::distill_article:
+ self_contained: false
+ toc: true
+---
+
+```{r}
+# ============================================================================
+# LIBRARIES
+# ============================================================================
+
+library(tidyverse)
+library(ggrepel)
+library(maps)
+library(sf)
+library(usmap)
+library(ggtext)
+library(cowplot)
+library(geomtextpath)
+library(sysfonts)
+library(showtext)
+
+# ============================================================================
+# LOAD CUSTOM FONT
+# ============================================================================
+
+sysfonts::font_add_google(name = "Open Sans", family = "open-sans")
+showtext::showtext_auto()
+
+# ============================================================================
+# READ AND PREPARE THE DATA
+# ============================================================================
+
+data <- read.csv2("Clean_Median_Data.csv", sep = ";")
+
+data <- data |>
+ mutate(
+ Median_1999 = as.numeric(Median_1999),
+ Median_2014 = as.numeric(Median_2014),
+ change = Median_2014 - Median_1999,
+ pct_change = ((Median_2014 - Median_1999) / Median_1999) * 100
+ )
+
+# Identify top 10 risers and fallers
+top_risers <- data |>
+ arrange(desc(pct_change)) |>
+ head(10) |>
+ pull(X)
+
+top_fallers <- data |>
+ arrange(desc(pct_change)) |>
+ tail(10) |>
+ pull(X)
+
+raleigh <- data |>
+ filter(X == "Raleigh, NC")
+
+# Add category column
+data <- data |>
+ mutate(category = case_when(
+ X %in% top_risers ~ "riser",
+ X %in% top_fallers ~ "faller",
+ X %in% raleigh ~ "Raleigh",
+ TRUE ~ "other"
+ ))
+
+# Prepare data for plotting (long format)
+plot_data <- data |>
+ select(X, Median_1999, Median_2014, category, pct_change) |>
+ pivot_longer(cols = c(Median_1999, Median_2014),
+ names_to = "year",
+ values_to = "income") |>
+ mutate(year_num = ifelse(year == "Median_1999", 1999, 2014))
+
+# ============================================================================
+# CREATE THE MAIN SLOPE CHART
+# ============================================================================
+
+main_plot <- ggplot(plot_data, aes(x = year_num, y = income, group = X)) +
+
+ # Manual dotted grid lines
+ annotate("segment", x = 1999, xend = 2014, y = 50, yend = 50,
+ color = "gray85", linetype = "dotted", linewidth = 0.3) +
+ annotate("segment", x = 1999, xend = 2014, y = 60, yend = 60,
+ color = "gray85", linetype = "dotted", linewidth = 0.3) +
+ annotate("segment", x = 1999, xend = 2014, y = 70, yend = 70,
+ color = "gray85", linetype = "dotted", linewidth = 0.3) +
+ annotate("segment", x = 1999, xend = 2014, y = 80, yend = 80,
+ color = "gray85", linetype = "dotted", linewidth = 0.3) +
+ annotate("segment", x = 1999, xend = 2014, y = 90, yend = 90,
+ color = "gray85", linetype = "dotted", linewidth = 0.3) +
+
+ # Background lines for "others"
+ geom_line(data = plot_data |> filter(category == "other"),
+ color = "#F2DFCE", size = 0.6, alpha = 0.4) +
+
+ # Lines for top fallers (red)
+ geom_line(data = plot_data |> filter(category == "faller"),
+ color = "#9D2B4D", size = 0.6) +
+
+ # Lines for top risers (blue)
+ geom_line(data = plot_data |> filter(category == "riser"),
+ color = "#477FB0", size = 0.6) +
+
+ # Line for Raleigh (gray)
+ geom_line(data = plot_data |> filter(category == "Raleigh"),
+ color = "#757472", size = 0.6) +
+
+ # POINTS "other" (bottom)
+ geom_point(data = plot_data |> filter(category == "other"),
+ color = "#EAE3DD", size = 1.3, alpha = 0.6) +
+
+ # POINTS fallers (on top of others)
+ geom_point(data = plot_data |> filter(category == "faller"),
+ color = "#9D2B4D", size = 1.3) +
+
+ # POINTS risers (on top of others)
+ geom_point(data = plot_data |> filter(category == "riser"),
+ color = "#477FB0", size = 1.3) +
+
+ # POINT Raleigh
+ geom_point(data = plot_data %>% filter(category == "Raleigh"),
+ color = "#757472", size = 1.5) +
+
+ # Metropolitan cities Labels
+ annotate("text", x = 2014.5, y = 90.743, label = "Midland, TX",
+ hjust = 0, size = 2.7, color = "#5d5d5c", fontface = "bold") +
+ annotate("text", x = 2014.5, y = 89.5, label = "37%",
+ hjust = 0, size = 3.5, color = "#5d5d5c", fontface = "bold") +
+ annotate("text", x = 2014.5, y = 80.966, label = "Barnstable Town, MA",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 76.074, label = "Burlington area, VT",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 74.5, label = "Raleigh, NC",
+ hjust = 0, size = 2.7, color = "#5d5d5c", fontface = "bold") +
+ annotate("text", x = 2014.5, y = 73.5, label = "Lebanon, PA",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 66.6, label = "Grand Junction, CO",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 65.6, label = "Odessa, TX",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 64.6, label = "Lafayette, LA",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 63.6, label = "Ocean City, NJ",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 62.6, label = "Wichita Falls, TX",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 61.6, label = "New Orleans area, LA",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 60.2, label = "Michigan City Area, IN",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 59.35, label = "Jackson, MI",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 58.3, label = "Jackson, TN",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 57.38, label = "Winston-Salem, NC",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 56.45, label = "Elkhart-Goshen, IN",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 55.2, label = "Springfield, OH",
+ hjust = 0, size = 2.7, color = "#5d5d5c", fontface = "bold") +
+ annotate("text", x = 2014.5, y = 53.957, label = "-27%",
+ hjust = 0, size = 3.5, color = "#5d5d5c", fontface = "bold") +
+ annotate("text", x = 2014.5, y = 52.8, label = "Hickory area, NC",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 51.278, label = "Burlington, NC",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 49.2, label = "Rocky Mount, NC",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 48.2, label = "Goldsboro, NC",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+
+ # y-axis numbers
+ annotate("text", x = 2013.7, y = 90.7, label = "90",
+ hjust = 1, size = 3.2, color = "#5d5d5c") +
+ annotate("text", x = 2013.7, y = 80.7, label = "80",
+ hjust = 1, size = 3.2, color = "#5d5d5c") +
+ annotate("text", x = 2013.7, y = 70.7, label = "70",
+ hjust = 1, size = 3.2, color = "#5d5d5c") +
+ annotate("text", x = 2013.7, y = 60.7, label = "60",
+ hjust = 1, size = 3.2, color = "#5d5d5c") +
+ annotate("text", x = 2013.7, y = 50.7, label = "50",
+ hjust = 1, size = 3.2, color = "#5d5d5c") +
+
+ # compact legend in upper-right corner
+ annotate("segment", x = 2015, xend = 2015.8, y = 88, yend = 88,
+ color = "#477FB0", size = 0.6) +
+ annotate("point", x = 2015, y = 88, color = "#477FB0", size = 1.3) +
+ annotate("point", x = 2015.8, y = 88, color = "#477FB0", size = 1.3) +
+ annotate("text", x = 2016, y = 88, label = "10 biggest risers",
+ hjust = 0, size = 2.5, color = "#5d5d5c") +
+
+ annotate("segment", x = 2015, xend = 2015.8, y = 86.5, yend = 86.5,
+ color = "#8B0000", size = 0.8) +
+ annotate("point", x = 2015, y = 86.5, color = "#8B0000", size = 1.3) +
+ annotate("point", x = 2015.8, y = 86.5, color = "#8B0000", size = 1.3) +
+ annotate("text", x = 2016, y = 86.5, label = "10 biggest fallers",
+ hjust = 0, size = 2.5, color = "#5d5d5c") +
+
+ annotate("segment", x = 2015, xend = 2015.8, y = 85, yend = 85,
+ color = "#EAE3DD", size = 0.8) +
+ annotate("point", x = 2015, y = 85, color = "#EAE3DD", size = 1.3) +
+ annotate("point", x = 2015.8, y = 85, color = "#EAE3DD", size = 1.3) +
+ annotate("text", x = 2016, y = 85, label = "Others",
+ hjust = 0, size = 2.5, color = "#5d5d5c") +
+
+ # Scale and theme
+ scale_x_continuous(breaks = c(1999, 2014),
+ limits = c(1998.7, 2050),
+ expand = c(0, 0),
+ position = "top",
+ labels = NULL) + # To avoid labels being outside chart
+ scale_y_continuous(breaks = seq(40, 100, by = 10),
+ limits = c(43, 92),
+ expand = c(0.023, 0.023)) +
+ scale_color_manual(values = c("other" = "#EAE3DD", "riser" = "#477FB0",
+ "faller" = "#8B0000")) +
+
+ # Titles
+ labs(title = "Changing fortunes",
+ subtitle = "US median adjusted household income ($'000)*",
+ x = NULL,
+ y = NULL) +
+
+ # Theme customization
+ theme_minimal() +
+ theme(
+ plot.title = element_text(size = 16),
+ plot.subtitle = element_text(size = 10, margin = margin(b = 5)),
+ panel.grid.major.x = element_blank(),
+ panel.grid.minor = element_blank(),
+ panel.grid.major.y = element_blank(),
+ axis.text.x.top = element_text(size = 10, face = "bold"),
+ axis.text.y = element_blank(),
+ axis.ticks.y = element_blank(),
+ legend.position = "none",
+ plot.margin = margin(10, 5, 5, 10)
+ )
+
+# ============================================================================
+# LOAD AND FIX GEOCODED DATA FOR MAP
+# ============================================================================
+
+geocoded_data <- read.csv("geocoded_data.csv")
+
+# Fix incorrectly geocoded metros (matched wrong cities outside US)
+corrections <- tribble(
+ ~X, ~lat_fix, ~lon_fix,
+ "Augusta-Richmond County, GA-SC", 33.4735, -82.0105,
+ "Birmingham-Hoover, AL", 33.5207, -86.8025,
+ "Canton-Massillon, OH", 40.7989, -81.3784,
+ "Hickory-Lenoir-Morganton, NC", 35.7331, -81.3412,
+ "Manchester-Nashua, NH", 42.9956, -71.4548,
+ "Naples-Immokalee-Marco Island, FL", 26.1420, -81.7948,
+ "Norwich-New London, CT", 41.3557, -72.0995,
+ "Olympia-Tumwater, WA", 47.0379, -122.9007,
+ "Santa Cruz-Watsonville, CA", 36.9741, -122.0308,
+ "Santa Maria-Santa Barbara, CA", 34.9530, -120.4357,
+ "Winston-Salem, NC", 36.0999, -80.2442,
+ "York-Hanover, PA", 39.9626, -76.7277
+)
+
+# Apply corrections
+geocoded_data <- geocoded_data |>
+ left_join(corrections, by = "X") |>
+ mutate(
+ lat = ifelse(!is.na(lat_fix), lat_fix, lat),
+ lon = ifelse(!is.na(lon_fix), lon_fix, lon)
+ ) |>
+ select(-lat_fix, -lon_fix)
+
+# ============================================================================
+# PREPARE MAP DATA
+# ============================================================================
+
+map_data <- geocoded_data |>
+ filter(!is.na(lat) & !is.na(lon)) |>
+ select(X, lon, lat, Median_1999, Median_2014, category, pct_change)
+
+# Transform coordinates to usmap projection
+map_data_transformed <- usmap_transform(map_data)
+
+# ============================================================================
+# CREATE US MAP
+# ============================================================================
+
+# usmap data as sf object, excluding Puerto Rico
+us_states <- us_map(regions = "states", exclude = "PR")
+
+# Convert to sf object
+us_states_sf <- st_as_sf(us_states)
+
+# Dissolve all states into one polygon (removes internal state boundaries)
+us_outline <- us_states_sf |>
+ st_union() |>
+ st_sf()
+
+# ============================================================================
+# SEPARATE MAP DATA BY CATEGORY
+# ============================================================================
+
+map_other <- map_data_transformed |> filter(category == "other")
+map_riser <- map_data_transformed |> filter(category == "riser")
+map_faller <- map_data_transformed |> filter(category == "faller")
+map_raleigh <- map_data_transformed |> filter(category == "Raleigh")
+
+# ============================================================================
+# CREATE THE MAP
+# ============================================================================
+
+us_map_plot <- ggplot() +
+ geom_sf(
+ data = us_outline,
+ fill = "white",
+ color = "#5d5d5c",
+ linewidth = 0.1
+ ) +
+ geom_sf(data = map_other, aes(geometry = geometry),
+ color = "#F2DFCE", size = 1.2, alpha = 1) +
+ geom_sf(data = map_riser, aes(geometry = geometry),
+ color = "#477FB0", size = 1.2) +
+ geom_sf(data = map_faller, aes(geometry = geometry),
+ color = "#9D2B4D", size = 1.2) +
+ geom_sf(data = map_raleigh, aes(geometry = geometry),
+ color = "#757472", size = 1.2) +
+ theme_void() +
+ theme(
+ plot.background = element_blank(),
+ panel.background = element_blank()
+ )
+
+# ============================================================================
+# CREATE TEXT BOX
+# ============================================================================
+
+text_box <- ggplot() +
+ annotate(
+ "richtext",
+ x = 1, y = 0.98,
+ label = "The 229 (of 381) metropolitan
areas shown, account for 76%
of the total population of the
United States",
+ hjust = 1, vjust = 1,
+ size = 3.5,
+ color = "#5d5d5c",
+ lineheight = 1.2,
+ label.color = NA,
+ fill = NA
+ ) +
+ annotate(
+ "richtext",
+ x = 1, y = 0.64,
+ label = "Of those 229, 190 have seen
their median income fall",
+ hjust = 1, vjust = 1,
+ size = 3.5,
+ color = "#5d5d5c",
+ lineheight = 1.2,
+ label.color = NA,
+ fill = NA
+ ) +
+ xlim(0, 1) +
+ ylim(0, 1) +
+ theme_void()
+```
+```{r plot, fig.width=6, fig.height=7.16, fig.showtext=TRUE}
+# ============================================================================
+# COMBINE EVERYTHING USING COWPLOT
+# ============================================================================
+
+final_plot <- ggdraw(main_plot) +
+
+ # Add white background first
+ theme(plot.background = element_rect(fill = "white", color = NA)) +
+
+ # Axis labels (1999 and 2014)
+ draw_label("1999", x = 0.055, y = 0.905, hjust = 0.5, size = 10,
+ fontface = "bold", color = "#5d5d5c") +
+ draw_label("2014", x = 0.295, y = 0.905, hjust = 0.5, size = 10,
+ fontface = "bold", color = "#5d5d5c") +
+
+ # map in top-right corner
+ draw_plot(us_map_plot, x = 0.48, y = 0.52, width = 0.48, height = 0.52) +
+ # text box below the map
+ draw_plot(text_box, x = 0.55, y = 0.08, width = 0.42, height = 0.40) +
+
+ # Arrow 1: Midland, TX (Biggest winner) - with curved text
+ geom_textcurve(
+ aes(x = 0.433, y = 0.868, xend = 0.707, yend = 0.747),
+ label = "Biggest winner",
+ curvature = -0.44,
+ arrow = arrow(length = unit(0.15, "cm"), type = "open"),
+ color = "#5d5d5c",
+ linewidth = 0.3,
+ size = 2.7,
+ fontface = "italic",
+ hjust = 0.05,
+ vjust = -0.3
+ ) +
+
+ # Arrow 2: Springfield, OH (Biggest loser) - first segment with text
+ geom_textcurve(
+ aes(x = 0.46, y = 0.243, xend = 0.65, yend = 0.53),
+ label = "Biggest loser",
+ curvature = 0.2,
+ arrow = arrow(length = unit(0, "cm")),
+ color = "#5d5d5c",
+ linewidth = 0.3,
+ size = 2.7,
+ fontface = "italic",
+ hjust = 0.02,
+ vjust = 1.5
+ ) +
+ # Arrow 2: Second segment without text
+ geom_curve(
+ aes(x = 0.65, y = 0.53, xend = 0.837, yend = 0.8135),
+ curvature = -0.2, # Negative curvature
+ arrow = arrow(length = unit(0.15, "cm"), type = "open"), # Arrow only at end
+ color = "#5d5d5c",
+ linewidth = 0.3
+ ) +
+
+ # Arrow 3: Raleigh, NC
+ geom_curve(
+ aes(x = 0.865, y = 0.70, xend = 0.887, yend = 0.787),
+ curvature = 0.3,
+ arrow = arrow(length = unit(0.15, "cm"), type = "open"),
+ color = "#5d5d5c",
+ linewidth = 0.3
+ ) +
+ # Label for Raleigh
+ draw_label("Raleigh, NC", x = 0.76, y = 0.70, hjust = 0, size = 8,
+ color = "#5d5d5c") +
+
+ # Semi-transparent white rectangle behind footer text
+draw_grob(
+ grid::rectGrob(
+ x = 0.03,
+ y = 0.044,
+ width = 0.285,
+ height = 0.10,
+ just = c("left", "bottom"),
+ gp = grid::gpar(fill = "#F2DFCE", alpha = 0.15, col = NA)
+ )
+) +
+
+ # Footer annotation (bottom left)
+ draw_label(
+ "* Scaled to 3-person household, adjusted for cost of living in the metro relative to the nation\nGraphic: Steven Bernard Source: Pew Research Center",
+ x = 0.03,
+ y = 0.067,
+ hjust = 0,
+ vjust = 0,
+ size = 7,
+ color = "#888888",
+ lineheight = 1.3
+ )
+
+# Display the final plot
+final_plot
+```
+
+
diff --git a/_projects/2025/100570484/100570484.html b/_projects/2025/100570484/100570484.html
new file mode 100644
index 00000000..1fbf205c
--- /dev/null
+++ b/_projects/2025/100570484/100570484.html
@@ -0,0 +1,2028 @@
+
+
+
+
+
Financial Times Graph Replication
+# ============================================================================
+# LIBRARIES
+# ============================================================================
+
+library(tidyverse)
+library(ggrepel)
+library(maps)
+library(sf)
+library(usmap)
+library(ggtext)
+library(cowplot)
+library(geomtextpath)
+library(sysfonts)
+library(showtext)
+
+# ============================================================================
+# LOAD CUSTOM FONT
+# ============================================================================
+
+sysfonts::font_add_google(name = "Open Sans", family = "open-sans")
+showtext::showtext_auto()
+
+# ============================================================================
+# READ AND PREPARE THE DATA
+# ============================================================================
+
+data <- read.csv2("Clean_Median_Data.csv", sep = ";")
+
+data <- data |>
+ mutate(
+ Median_1999 = as.numeric(Median_1999),
+ Median_2014 = as.numeric(Median_2014),
+ change = Median_2014 - Median_1999,
+ pct_change = ((Median_2014 - Median_1999) / Median_1999) * 100
+ )
+
+# Identify top 10 risers and fallers
+top_risers <- data |>
+ arrange(desc(pct_change)) |>
+ head(10) |>
+ pull(X)
+
+top_fallers <- data |>
+ arrange(desc(pct_change)) |>
+ tail(10) |>
+ pull(X)
+
+raleigh <- data |>
+ filter(X == "Raleigh, NC")
+
+# Add category column
+data <- data |>
+ mutate(category = case_when(
+ X %in% top_risers ~ "riser",
+ X %in% top_fallers ~ "faller",
+ X %in% raleigh ~ "Raleigh",
+ TRUE ~ "other"
+ ))
+
+# Prepare data for plotting (long format)
+plot_data <- data |>
+ select(X, Median_1999, Median_2014, category, pct_change) |>
+ pivot_longer(cols = c(Median_1999, Median_2014),
+ names_to = "year",
+ values_to = "income") |>
+ mutate(year_num = ifelse(year == "Median_1999", 1999, 2014))
+
+# ============================================================================
+# CREATE THE MAIN SLOPE CHART
+# ============================================================================
+
+main_plot <- ggplot(plot_data, aes(x = year_num, y = income, group = X)) +
+
+ # Manual dotted grid lines
+ annotate("segment", x = 1999, xend = 2014, y = 50, yend = 50,
+ color = "gray85", linetype = "dotted", linewidth = 0.3) +
+ annotate("segment", x = 1999, xend = 2014, y = 60, yend = 60,
+ color = "gray85", linetype = "dotted", linewidth = 0.3) +
+ annotate("segment", x = 1999, xend = 2014, y = 70, yend = 70,
+ color = "gray85", linetype = "dotted", linewidth = 0.3) +
+ annotate("segment", x = 1999, xend = 2014, y = 80, yend = 80,
+ color = "gray85", linetype = "dotted", linewidth = 0.3) +
+ annotate("segment", x = 1999, xend = 2014, y = 90, yend = 90,
+ color = "gray85", linetype = "dotted", linewidth = 0.3) +
+
+ # Background lines for "others"
+ geom_line(data = plot_data |> filter(category == "other"),
+ color = "#F2DFCE", size = 0.6, alpha = 0.4) +
+
+ # Lines for top fallers (red)
+ geom_line(data = plot_data |> filter(category == "faller"),
+ color = "#9D2B4D", size = 0.6) +
+
+ # Lines for top risers (blue)
+ geom_line(data = plot_data |> filter(category == "riser"),
+ color = "#477FB0", size = 0.6) +
+
+ # Line for Raleigh (gray)
+ geom_line(data = plot_data |> filter(category == "Raleigh"),
+ color = "#757472", size = 0.6) +
+
+ # POINTS "other" (bottom)
+ geom_point(data = plot_data |> filter(category == "other"),
+ color = "#EAE3DD", size = 1.3, alpha = 0.6) +
+
+ # POINTS fallers (on top of others)
+ geom_point(data = plot_data |> filter(category == "faller"),
+ color = "#9D2B4D", size = 1.3) +
+
+ # POINTS risers (on top of others)
+ geom_point(data = plot_data |> filter(category == "riser"),
+ color = "#477FB0", size = 1.3) +
+
+ # POINT Raleigh
+ geom_point(data = plot_data %>% filter(category == "Raleigh"),
+ color = "#757472", size = 1.5) +
+
+ # Metropolitan cities Labels
+ annotate("text", x = 2014.5, y = 90.743, label = "Midland, TX",
+ hjust = 0, size = 2.7, color = "#5d5d5c", fontface = "bold") +
+ annotate("text", x = 2014.5, y = 89.5, label = "37%",
+ hjust = 0, size = 3.5, color = "#5d5d5c", fontface = "bold") +
+ annotate("text", x = 2014.5, y = 80.966, label = "Barnstable Town, MA",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 76.074, label = "Burlington area, VT",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 74.5, label = "Raleigh, NC",
+ hjust = 0, size = 2.7, color = "#5d5d5c", fontface = "bold") +
+ annotate("text", x = 2014.5, y = 73.5, label = "Lebanon, PA",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 66.6, label = "Grand Junction, CO",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 65.6, label = "Odessa, TX",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 64.6, label = "Lafayette, LA",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 63.6, label = "Ocean City, NJ",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 62.6, label = "Wichita Falls, TX",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 61.6, label = "New Orleans area, LA",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 60.2, label = "Michigan City Area, IN",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 59.35, label = "Jackson, MI",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 58.3, label = "Jackson, TN",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 57.38, label = "Winston-Salem, NC",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 56.45, label = "Elkhart-Goshen, IN",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 55.2, label = "Springfield, OH",
+ hjust = 0, size = 2.7, color = "#5d5d5c", fontface = "bold") +
+ annotate("text", x = 2014.5, y = 53.957, label = "-27%",
+ hjust = 0, size = 3.5, color = "#5d5d5c", fontface = "bold") +
+ annotate("text", x = 2014.5, y = 52.8, label = "Hickory area, NC",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 51.278, label = "Burlington, NC",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 49.2, label = "Rocky Mount, NC",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+ annotate("text", x = 2014.5, y = 48.2, label = "Goldsboro, NC",
+ hjust = 0, size = 2.7, color = "#5d5d5c") +
+
+ # y-axis numbers
+ annotate("text", x = 2013.7, y = 90.7, label = "90",
+ hjust = 1, size = 3.2, color = "#5d5d5c") +
+ annotate("text", x = 2013.7, y = 80.7, label = "80",
+ hjust = 1, size = 3.2, color = "#5d5d5c") +
+ annotate("text", x = 2013.7, y = 70.7, label = "70",
+ hjust = 1, size = 3.2, color = "#5d5d5c") +
+ annotate("text", x = 2013.7, y = 60.7, label = "60",
+ hjust = 1, size = 3.2, color = "#5d5d5c") +
+ annotate("text", x = 2013.7, y = 50.7, label = "50",
+ hjust = 1, size = 3.2, color = "#5d5d5c") +
+
+ # compact legend in upper-right corner
+ annotate("segment", x = 2015, xend = 2015.8, y = 88, yend = 88,
+ color = "#477FB0", size = 0.6) +
+ annotate("point", x = 2015, y = 88, color = "#477FB0", size = 1.3) +
+ annotate("point", x = 2015.8, y = 88, color = "#477FB0", size = 1.3) +
+ annotate("text", x = 2016, y = 88, label = "10 biggest risers",
+ hjust = 0, size = 2.5, color = "#5d5d5c") +
+
+ annotate("segment", x = 2015, xend = 2015.8, y = 86.5, yend = 86.5,
+ color = "#8B0000", size = 0.8) +
+ annotate("point", x = 2015, y = 86.5, color = "#8B0000", size = 1.3) +
+ annotate("point", x = 2015.8, y = 86.5, color = "#8B0000", size = 1.3) +
+ annotate("text", x = 2016, y = 86.5, label = "10 biggest fallers",
+ hjust = 0, size = 2.5, color = "#5d5d5c") +
+
+ annotate("segment", x = 2015, xend = 2015.8, y = 85, yend = 85,
+ color = "#EAE3DD", size = 0.8) +
+ annotate("point", x = 2015, y = 85, color = "#EAE3DD", size = 1.3) +
+ annotate("point", x = 2015.8, y = 85, color = "#EAE3DD", size = 1.3) +
+ annotate("text", x = 2016, y = 85, label = "Others",
+ hjust = 0, size = 2.5, color = "#5d5d5c") +
+
+ # Scale and theme
+ scale_x_continuous(breaks = c(1999, 2014),
+ limits = c(1998.7, 2050),
+ expand = c(0, 0),
+ position = "top",
+ labels = NULL) + # To avoid labels being outside chart
+ scale_y_continuous(breaks = seq(40, 100, by = 10),
+ limits = c(43, 92),
+ expand = c(0.023, 0.023)) +
+ scale_color_manual(values = c("other" = "#EAE3DD", "riser" = "#477FB0",
+ "faller" = "#8B0000")) +
+
+ # Titles
+ labs(title = "Changing fortunes",
+ subtitle = "US median adjusted household income ($'000)*",
+ x = NULL,
+ y = NULL) +
+
+ # Theme customization
+ theme_minimal() +
+ theme(
+ plot.title = element_text(size = 16),
+ plot.subtitle = element_text(size = 10, margin = margin(b = 5)),
+ panel.grid.major.x = element_blank(),
+ panel.grid.minor = element_blank(),
+ panel.grid.major.y = element_blank(),
+ axis.text.x.top = element_text(size = 10, face = "bold"),
+ axis.text.y = element_blank(),
+ axis.ticks.y = element_blank(),
+ legend.position = "none",
+ plot.margin = margin(10, 5, 5, 10)
+ )
+
+# ============================================================================
+# LOAD AND FIX GEOCODED DATA FOR MAP
+# ============================================================================
+
+geocoded_data <- read.csv("geocoded_data.csv")
+
+# Fix incorrectly geocoded metros (matched wrong cities outside US)
+corrections <- tribble(
+ ~X, ~lat_fix, ~lon_fix,
+ "Augusta-Richmond County, GA-SC", 33.4735, -82.0105,
+ "Birmingham-Hoover, AL", 33.5207, -86.8025,
+ "Canton-Massillon, OH", 40.7989, -81.3784,
+ "Hickory-Lenoir-Morganton, NC", 35.7331, -81.3412,
+ "Manchester-Nashua, NH", 42.9956, -71.4548,
+ "Naples-Immokalee-Marco Island, FL", 26.1420, -81.7948,
+ "Norwich-New London, CT", 41.3557, -72.0995,
+ "Olympia-Tumwater, WA", 47.0379, -122.9007,
+ "Santa Cruz-Watsonville, CA", 36.9741, -122.0308,
+ "Santa Maria-Santa Barbara, CA", 34.9530, -120.4357,
+ "Winston-Salem, NC", 36.0999, -80.2442,
+ "York-Hanover, PA", 39.9626, -76.7277
+)
+
+# Apply corrections
+geocoded_data <- geocoded_data |>
+ left_join(corrections, by = "X") |>
+ mutate(
+ lat = ifelse(!is.na(lat_fix), lat_fix, lat),
+ lon = ifelse(!is.na(lon_fix), lon_fix, lon)
+ ) |>
+ select(-lat_fix, -lon_fix)
+
+# ============================================================================
+# PREPARE MAP DATA
+# ============================================================================
+
+map_data <- geocoded_data |>
+ filter(!is.na(lat) & !is.na(lon)) |>
+ select(X, lon, lat, Median_1999, Median_2014, category, pct_change)
+
+# Transform coordinates to usmap projection
+map_data_transformed <- usmap_transform(map_data)
+
+# ============================================================================
+# CREATE US MAP
+# ============================================================================
+
+# usmap data as sf object, excluding Puerto Rico
+us_states <- us_map(regions = "states", exclude = "PR")
+
+# Convert to sf object
+us_states_sf <- st_as_sf(us_states)
+
+# Dissolve all states into one polygon (removes internal state boundaries)
+us_outline <- us_states_sf |>
+ st_union() |>
+ st_sf()
+
+# ============================================================================
+# SEPARATE MAP DATA BY CATEGORY
+# ============================================================================
+
+map_other <- map_data_transformed |> filter(category == "other")
+map_riser <- map_data_transformed |> filter(category == "riser")
+map_faller <- map_data_transformed |> filter(category == "faller")
+map_raleigh <- map_data_transformed |> filter(category == "Raleigh")
+
+# ============================================================================
+# CREATE THE MAP
+# ============================================================================
+
+us_map_plot <- ggplot() +
+ geom_sf(
+ data = us_outline,
+ fill = "white",
+ color = "#5d5d5c",
+ linewidth = 0.1
+ ) +
+ geom_sf(data = map_other, aes(geometry = geometry),
+ color = "#F2DFCE", size = 1.2, alpha = 1) +
+ geom_sf(data = map_riser, aes(geometry = geometry),
+ color = "#477FB0", size = 1.2) +
+ geom_sf(data = map_faller, aes(geometry = geometry),
+ color = "#9D2B4D", size = 1.2) +
+ geom_sf(data = map_raleigh, aes(geometry = geometry),
+ color = "#757472", size = 1.2) +
+ theme_void() +
+ theme(
+ plot.background = element_blank(),
+ panel.background = element_blank()
+ )
+
+# ============================================================================
+# CREATE TEXT BOX
+# ============================================================================
+
+text_box <- ggplot() +
+ annotate(
+ "richtext",
+ x = 1, y = 0.98,
+ label = "The 229 (of 381) metropolitan<br>areas shown, account for <b>76%</b><br>of the total population of the<br>United States",
+ hjust = 1, vjust = 1,
+ size = 3.5,
+ color = "#5d5d5c",
+ lineheight = 1.2,
+ label.color = NA,
+ fill = NA
+ ) +
+ annotate(
+ "richtext",
+ x = 1, y = 0.64,
+ label = "Of those 229, <b>190</b> have seen<br>their median income fall",
+ hjust = 1, vjust = 1,
+ size = 3.5,
+ color = "#5d5d5c",
+ lineheight = 1.2,
+ label.color = NA,
+ fill = NA
+ ) +
+ xlim(0, 1) +
+ ylim(0, 1) +
+ theme_void()
+# ============================================================================
+# COMBINE EVERYTHING USING COWPLOT
+# ============================================================================
+
+final_plot <- ggdraw(main_plot) +
+
+ # Add white background first
+ theme(plot.background = element_rect(fill = "white", color = NA)) +
+
+ # Axis labels (1999 and 2014)
+ draw_label("1999", x = 0.055, y = 0.905, hjust = 0.5, size = 10,
+ fontface = "bold", color = "#5d5d5c") +
+ draw_label("2014", x = 0.295, y = 0.905, hjust = 0.5, size = 10,
+ fontface = "bold", color = "#5d5d5c") +
+
+ # map in top-right corner
+ draw_plot(us_map_plot, x = 0.48, y = 0.52, width = 0.48, height = 0.52) +
+ # text box below the map
+ draw_plot(text_box, x = 0.55, y = 0.08, width = 0.42, height = 0.40) +
+
+ # Arrow 1: Midland, TX (Biggest winner) - with curved text
+ geom_textcurve(
+ aes(x = 0.433, y = 0.868, xend = 0.707, yend = 0.747),
+ label = "Biggest winner",
+ curvature = -0.44,
+ arrow = arrow(length = unit(0.15, "cm"), type = "open"),
+ color = "#5d5d5c",
+ linewidth = 0.3,
+ size = 2.7,
+ fontface = "italic",
+ hjust = 0.05,
+ vjust = -0.3
+ ) +
+
+ # Arrow 2: Springfield, OH (Biggest loser) - first segment with text
+ geom_textcurve(
+ aes(x = 0.46, y = 0.243, xend = 0.65, yend = 0.53),
+ label = "Biggest loser",
+ curvature = 0.2,
+ arrow = arrow(length = unit(0, "cm")),
+ color = "#5d5d5c",
+ linewidth = 0.3,
+ size = 2.7,
+ fontface = "italic",
+ hjust = 0.02,
+ vjust = 1.5
+ ) +
+ # Arrow 2: Second segment without text
+ geom_curve(
+ aes(x = 0.65, y = 0.53, xend = 0.837, yend = 0.8135),
+ curvature = -0.2, # Negative curvature
+ arrow = arrow(length = unit(0.15, "cm"), type = "open"), # Arrow only at end
+ color = "#5d5d5c",
+ linewidth = 0.3
+ ) +
+
+ # Arrow 3: Raleigh, NC
+ geom_curve(
+ aes(x = 0.865, y = 0.70, xend = 0.887, yend = 0.787),
+ curvature = 0.3,
+ arrow = arrow(length = unit(0.15, "cm"), type = "open"),
+ color = "#5d5d5c",
+ linewidth = 0.3
+ ) +
+ # Label for Raleigh
+ draw_label("Raleigh, NC", x = 0.76, y = 0.70, hjust = 0, size = 8,
+ color = "#5d5d5c") +
+
+ # Semi-transparent white rectangle behind footer text
+draw_grob(
+ grid::rectGrob(
+ x = 0.03,
+ y = 0.044,
+ width = 0.285,
+ height = 0.10,
+ just = c("left", "bottom"),
+ gp = grid::gpar(fill = "#F2DFCE", alpha = 0.15, col = NA)
+ )
+) +
+
+ # Footer annotation (bottom left)
+ draw_label(
+ "* Scaled to 3-person household, adjusted for cost of living in the metro relative to the nation\nGraphic: Steven Bernard Source: Pew Research Center",
+ x = 0.03,
+ y = 0.067,
+ hjust = 0,
+ vjust = 0,
+ size = 7,
+ color = "#888888",
+ lineheight = 1.3
+ )
+
+# Display the final plot
+final_plot
+
`,e.githubCompareUpdatesUrl&&(t+=`View all changes to this article since it was first published.`),t+=` + If you see mistakes or want to suggest changes, please create an issue on GitHub.
+ `);const n=e.journal;return'undefined'!=typeof n&&'Distill'===n.title&&(t+=` +Diagrams and text are licensed under Creative Commons Attribution CC-BY 4.0 with the source available on GitHub, unless noted otherwise. The figures that have been reused from other sources don’t fall under this license and can be recognized by a note in their caption: “Figure from …”.
+ `),'undefined'!=typeof e.publishedDate&&(t+=` +For attribution in academic contexts, please cite this work as
+${e.concatenatedAuthors}, "${e.title}", Distill, ${e.publishedYear}.
+ BibTeX citation
+${m(e)}
+ `),t}var An=Math.sqrt,En=Math.atan2,Dn=Math.sin,Mn=Math.cos,On=Math.PI,Un=Math.abs,In=Math.pow,Nn=Math.LN10,jn=Math.log,Rn=Math.max,qn=Math.ceil,Fn=Math.floor,Pn=Math.round,Hn=Math.min;const zn=['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],Bn=['Jan.','Feb.','March','April','May','June','July','Aug.','Sept.','Oct.','Nov.','Dec.'],Wn=(e)=>10>e?'0'+e:e,Vn=function(e){const t=zn[e.getDay()].substring(0,3),n=Wn(e.getDate()),i=Bn[e.getMonth()].substring(0,3),a=e.getFullYear().toString(),d=e.getUTCHours().toString(),r=e.getUTCMinutes().toString(),o=e.getUTCSeconds().toString();return`${t}, ${n} ${i} ${a} ${d}:${r}:${o} Z`},$n=function(e){const t=Array.from(e).reduce((e,[t,n])=>Object.assign(e,{[t]:n}),{});return t},Jn=function(e){const t=new Map;for(var n in e)e.hasOwnProperty(n)&&t.set(n,e[n]);return t};class Qn{constructor(e){this.name=e.author,this.personalURL=e.authorURL,this.affiliation=e.affiliation,this.affiliationURL=e.affiliationURL,this.affiliations=e.affiliations||[]}get firstName(){const e=this.name.split(' ');return e.slice(0,e.length-1).join(' ')}get lastName(){const e=this.name.split(' ');return e[e.length-1]}}class Gn{constructor(){this.title='unnamed article',this.description='',this.authors=[],this.bibliography=new Map,this.bibliographyParsed=!1,this.citations=[],this.citationsCollected=!1,this.journal={},this.katex={},this.publishedDate=void 0}set url(e){this._url=e}get url(){if(this._url)return this._url;return this.distillPath&&this.journal.url?this.journal.url+'/'+this.distillPath:this.journal.url?this.journal.url:void 0}get githubUrl(){return this.githubPath?'https://github.com/'+this.githubPath:void 0}set previewURL(e){this._previewURL=e}get previewURL(){return this._previewURL?this._previewURL:this.url+'/thumbnail.jpg'}get publishedDateRFC(){return Vn(this.publishedDate)}get updatedDateRFC(){return Vn(this.updatedDate)}get publishedYear(){return this.publishedDate.getFullYear()}get publishedMonth(){return Bn[this.publishedDate.getMonth()]}get publishedDay(){return this.publishedDate.getDate()}get publishedMonthPadded(){return Wn(this.publishedDate.getMonth()+1)}get publishedDayPadded(){return Wn(this.publishedDate.getDate())}get publishedISODateOnly(){return this.publishedDate.toISOString().split('T')[0]}get volume(){const e=this.publishedYear-2015;if(1>e)throw new Error('Invalid publish date detected during computing volume');return e}get issue(){return this.publishedDate.getMonth()+1}get concatenatedAuthors(){if(2 tag. We found the following text: '+t);const n=document.createElement('span');n.innerHTML=e.nodeValue,e.parentNode.insertBefore(n,e),e.parentNode.removeChild(e)}}}}).observe(this,{childList:!0})}}var Ti='undefined'==typeof window?'undefined'==typeof global?'undefined'==typeof self?{}:self:global:window,_i=f(function(e,t){(function(e){function t(){this.months=['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec'],this.notKey=[',','{','}',' ','='],this.pos=0,this.input='',this.entries=[],this.currentEntry='',this.setInput=function(e){this.input=e},this.getEntries=function(){return this.entries},this.isWhitespace=function(e){return' '==e||'\r'==e||'\t'==e||'\n'==e},this.match=function(e,t){if((void 0==t||null==t)&&(t=!0),this.skipWhitespace(t),this.input.substring(this.pos,this.pos+e.length)==e)this.pos+=e.length;else throw'Token mismatch, expected '+e+', found '+this.input.substring(this.pos);this.skipWhitespace(t)},this.tryMatch=function(e,t){return(void 0==t||null==t)&&(t=!0),this.skipWhitespace(t),this.input.substring(this.pos,this.pos+e.length)==e},this.matchAt=function(){for(;this.input.length>this.pos&&'@'!=this.input[this.pos];)this.pos++;return!('@'!=this.input[this.pos])},this.skipWhitespace=function(e){for(;this.isWhitespace(this.input[this.pos]);)this.pos++;if('%'==this.input[this.pos]&&!0==e){for(;'\n'!=this.input[this.pos];)this.pos++;this.skipWhitespace(e)}},this.value_braces=function(){var e=0;this.match('{',!1);for(var t=this.pos,n=!1;;){if(!n)if('}'==this.input[this.pos]){if(0 =k&&(++x,i=k);if(d[x]instanceof n||d[T-1].greedy)continue;w=T-x,y=e.slice(i,k),v.index-=i}if(v){g&&(h=v[1].length);var S=v.index+h,v=v[0].slice(h),C=S+v.length,_=y.slice(0,S),L=y.slice(C),A=[x,w];_&&A.push(_);var E=new n(o,u?a.tokenize(v,u):v,b,v,f);A.push(E),L&&A.push(L),Array.prototype.splice.apply(d,A)}}}}}return d},hooks:{all:{},add:function(e,t){var n=a.hooks.all;n[e]=n[e]||[],n[e].push(t)},run:function(e,t){var n=a.hooks.all[e];if(n&&n.length)for(var d,r=0;d=n[r++];)d(t)}}},i=a.Token=function(e,t,n,i,a){this.type=e,this.content=t,this.alias=n,this.length=0|(i||'').length,this.greedy=!!a};if(i.stringify=function(e,t,n){if('string'==typeof e)return e;if('Array'===a.util.type(e))return e.map(function(n){return i.stringify(n,t,e)}).join('');var d={type:e.type,content:i.stringify(e.content,t,n),tag:'span',classes:['token',e.type],attributes:{},language:t,parent:n};if('comment'==d.type&&(d.attributes.spellcheck='true'),e.alias){var r='Array'===a.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(d.classes,r)}a.hooks.run('wrap',d);var l=Object.keys(d.attributes).map(function(e){return e+'="'+(d.attributes[e]||'').replace(/"/g,'"')+'"'}).join(' ');return'<'+d.tag+' class="'+d.classes.join(' ')+'"'+(l?' '+l:'')+'>'+d.content+''+d.tag+'>'},!t.document)return t.addEventListener?(t.addEventListener('message',function(e){var n=JSON.parse(e.data),i=n.language,d=n.code,r=n.immediateClose;t.postMessage(a.highlight(d,a.languages[i],i)),r&&t.close()},!1),t.Prism):t.Prism;var d=document.currentScript||[].slice.call(document.getElementsByTagName('script')).pop();return d&&(a.filename=d.src,document.addEventListener&&!d.hasAttribute('data-manual')&&('loading'===document.readyState?document.addEventListener('DOMContentLoaded',a.highlightAll):window.requestAnimationFrame?window.requestAnimationFrame(a.highlightAll):window.setTimeout(a.highlightAll,16))),t.Prism}();e.exports&&(e.exports=n),'undefined'!=typeof Ti&&(Ti.Prism=n),n.languages.markup={comment://,prolog:/<\?[\w\W]+?\?>/,doctype://i,cdata://i,tag:{pattern:/<\/?(?!\d)[^\s>\/=$<]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\\1|\\?(?!\1)[\w\W])*\1|[^\s'">=]+))?)*\s*\/?>/i,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=(?:('|")[\w\W]*?(\1)|[^\s>]+)/i,inside:{punctuation:/[=>"']/}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/?[\da-z]{1,8};/i},n.hooks.add('wrap',function(e){'entity'===e.type&&(e.attributes.title=e.content.replace(/&/,'&'))}),n.languages.xml=n.languages.markup,n.languages.html=n.languages.markup,n.languages.mathml=n.languages.markup,n.languages.svg=n.languages.markup,n.languages.css={comment:/\/\*[\w\W]*?\*\//,atrule:{pattern:/@[\w-]+?.*?(;|(?=\s*\{))/i,inside:{rule:/@[\w-]+/}},url:/url\((?:(["'])(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,selector:/[^\{\}\s][^\{\};]*?(?=\s*\{)/,string:{pattern:/("|')(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1/,greedy:!0},property:/(\b|\B)[\w-]+(?=\s*:)/i,important:/\B!important\b/i,function:/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:]/},n.languages.css.atrule.inside.rest=n.util.clone(n.languages.css),n.languages.markup&&(n.languages.insertBefore('markup','tag',{style:{pattern:/(
+
+
+ ${e.map(l).map((e)=>`
`)}}const Mi=`
+d-citation-list {
+ contain: layout style;
+}
+
+d-citation-list .references {
+ grid-column: text;
+}
+
+d-citation-list .references .title {
+ font-weight: 500;
+}
+`;class Oi extends HTMLElement{static get is(){return'd-citation-list'}connectedCallback(){this.hasAttribute('distill-prerendered')||(this.style.display='none')}set citations(e){x(this,e)}}var Ui=f(function(e){var t='undefined'==typeof window?'undefined'!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{}:window,n=function(){var e=/\blang(?:uage)?-(\w+)\b/i,n=0,a=t.Prism={util:{encode:function(e){return e instanceof i?new i(e.type,a.util.encode(e.content),e.alias):'Array'===a.util.type(e)?e.map(a.util.encode):e.replace(/&/g,'&').replace(/e.length)break tokenloop;if(!(y instanceof n)){c.lastIndex=0;var v=c.exec(y),w=1;if(!v&&f&&x!=d.length-1){if(c.lastIndex=i,v=c.exec(e),!v)break;for(var S=v.index+(g?v[1].length:0),C=v.index+v[0].length,T=x,k=i,p=d.length;T
+
+`);class Ni extends ei(Ii(HTMLElement)){renderContent(){if(this.languageName=this.getAttribute('language'),!this.languageName)return void console.warn('You need to provide a language attribute to your Footnotes
+
+`,!1);class Fi extends qi(HTMLElement){connectedCallback(){super.connectedCallback(),this.list=this.root.querySelector('ol'),this.root.style.display='none'}set footnotes(e){if(this.list.innerHTML='',e.length){this.root.style.display='';for(const t of e){const e=document.createElement('li');e.id=t.id+'-listing',e.innerHTML=t.innerHTML;const n=document.createElement('a');n.setAttribute('class','footnote-backlink'),n.textContent='[\u21A9]',n.href='#'+t.id,e.appendChild(n),this.list.appendChild(e)}}else this.root.style.display='none'}}const Pi=ti('d-hover-box',`
+
+
+