Last active
November 11, 2025 21:15
-
-
Save tor-gu/3888beb2b4a43afdf0cc40ea96cd9008 to your computer and use it in GitHub Desktop.
Generate maps comparing statewide elections in NJ, 2007-2012 vs 2019-2024
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| library(tigris) | |
| library(njmunicipalities) | |
| library(njelections) | |
| library(glue) | |
| library(dplyr) | |
| library(purrr) | |
| library(ggplot2) | |
| library(stringr) | |
| # Set our starting end ending points | |
| final_year <- 2024 | |
| first_year <- final_year - 17 | |
| # Get the couny names from njmunicipalities | |
| county_names <- njmunicipalities::counties |> pull(county) | |
| # Get the map of NJ with tigris | |
| options(tigris_use_cache = TRUE) | |
| nj_municipality_map <- county_names %>% | |
| map_df( ~ county_subdivisions("NJ", county = ., class = "sf")) |> | |
| filter(ALAND > 0) | |
| # We will use Republican share of two-party share of vote | |
| # (TPSOV) as our basic metric. | |
| # Get statewide TPSOV by six-year half-cycle | |
| tpsov_sw <- election_statewide |> | |
| filter(year >= first_year & year <= final_year) |> | |
| filter(party %in% c("Democratic", "Republican")) |> | |
| select(year, party, office, vote_sw = vote) |> | |
| # Aggregate party vote over half-cycles | |
| mutate(half_cycle = as.integer((year-first_year)/6)) |> | |
| group_by(half_cycle, party) |> | |
| summarize(vote_sw=sum(vote_sw), .groups="drop") |> | |
| # Compute TPSOV | |
| group_by(half_cycle) |> | |
| mutate(tpsov_sw = vote_sw/sum(vote_sw)) |> | |
| ungroup() |> | |
| # Select just the Republican | |
| filter(party == "Republican") | |
| # Now do the same for municipalities | |
| muni_tpsov <- election_by_municipality |> | |
| filter(party %in% c("Democratic", "Republican")) |> | |
| select(year, office, GEOID, party, vote) |> | |
| # Aggregate the Princetons, and account for changing | |
| # names and GEOIDs | |
| mutate(GEOID = if_else(GEOID == PRINCETON_TWP_GEOID, | |
| PRINCETON_BORO_GEOID, | |
| GEOID)) |> | |
| # Aggregate Pine Valley and Pine Hill, and account for changing GEOIDs | |
| mutate(GEOID = if_else(GEOID == PINE_VALLEY_BORO_GEOID, | |
| PINE_HILL_BORO_GEOID, | |
| GEOID)) |> | |
| left_join(get_geoid_cross_references(final_year, first_year:final_year), | |
| by=c("year", "GEOID")) |> | |
| left_join(get_municipalities(final_year), | |
| by=c("GEOID_ref"="GEOID")) |> | |
| select(-GEOID, GEOID=GEOID_ref) |> | |
| # Aggregate party vote over half-cycles | |
| mutate(half_cycle = as.integer((year-first_year)/6)) |> | |
| group_by(half_cycle, GEOID, party) |> | |
| summarize(vote=sum(vote), .groups="drop") |> | |
| mutate(half_cycle_name = paste0(first_year + half_cycle * 6, "-", first_year + half_cycle * 6 + 5)) |> | |
| # Compute TPSOV | |
| group_by(half_cycle, GEOID) |> | |
| mutate(tpsov = vote/sum(vote)) |> | |
| ungroup() |> | |
| # Select just the Republican | |
| filter(party == "Republican") |> | |
| # Get rid of the middle half-cycle | |
| filter(half_cycle_name != glue("{first_year + 6}-{final_year - 6}")) |> | |
| # Add in municipal names, and statewide TPSOV | |
| left_join(get_municipalities(final_year), by="GEOID") |> | |
| left_join(tpsov_sw) |> | |
| mutate(tpsov_delta = tpsov - tpsov_sw) | |
| # Also create a table of the change in vote from | |
| # {first_year}-{first_year+5} to {final_year - 5}-{final_year} | |
| muni_tpsov_delta <- muni_tpsov |> | |
| arrange(half_cycle) |> | |
| group_by(GEOID, party) |> | |
| mutate(tpsov_delta = tpsov - lag(tpsov)) |> | |
| ungroup() |> | |
| filter(!is.na(tpsov_delta)) | |
| # Now glue our election data to the map | |
| map_with_values <- nj_municipality_map |> | |
| left_join(muni_tpsov, by="GEOID") | |
| # Let's get a label for the the most extreme | |
| # municipalities | |
| labels <- map_with_values |> | |
| split(map_with_values$half_cycle) |> | |
| map(filter, vote/tpsov > 2000) |> | |
| map(~bind_rows( | |
| slice_min(.x, n=4, order_by=tpsov), | |
| slice_max(.x, n=4, order_by=tpsov) | |
| ) | |
| ) |> | |
| map_df(arrange, desc(INTPTLAT)) |> | |
| # Filter so we don't put the same label on both maps | |
| group_by(GEOID) |> | |
| filter(row_number() == 1) |> | |
| ungroup() | |
| # Plot the first map | |
| plot_tpsov <- map_with_values |> | |
| ggplot() + | |
| geom_sf(aes(fill=tpsov_delta, geometry=geometry, size=.2)) + | |
| scale_size_identity() + | |
| scale_fill_gradient2( | |
| na.value = "lightgrey", | |
| low = "blue", | |
| high = "red", | |
| mid = "lightgrey", | |
| midpoint = 0, | |
| limits = c(-.4,.4), | |
| oob = scales::squish, | |
| breaks = c(-.4, 0, .4), | |
| labels=c("More Democratic", "State Average", "More Republican"), | |
| name="Margin" | |
| ) + | |
| theme( | |
| axis.ticks = element_blank(), | |
| axis.text.x = element_blank(), | |
| axis.text.y = element_blank(), | |
| axis.title.x = element_blank(), | |
| axis.title.y = element_blank(), | |
| panel.background = element_rect(fill = "lightblue"), | |
| panel.grid.major = element_line(color = "lightblue"), | |
| legend.position = "bottom", | |
| legend.key.width = unit(2, "cm"), | |
| legend.title = element_blank(), | |
| plot.title = element_text(hjust=0.5), | |
| plot.subtitle = element_text(hjust=0.5), | |
| plot.title.position = "panel" | |
| ) + | |
| facet_wrap("half_cycle_name") + | |
| labs(title=str_wrap("Two party share of vote in NJ, relative to statewide average", width = 50), | |
| subtitle = "Elections for Governor, Senate and President", | |
| caption="Source: NJ Division of Elections") | |
| # Now glue our election data to the map | |
| map_with_values_delta <- nj_municipality_map |> | |
| left_join(muni_tpsov_delta, by="GEOID") | |
| # Get labels for extreme values | |
| labels_delta <- map_with_values_delta |> | |
| filter(vote/tpsov > 2000) |> | |
| slice_max(n=8, order_by=abs(tpsov_delta)) |> | |
| arrange(desc(INTPTLAT)) | |
| # We will get the statewide average shift for centering | |
| sw_shift <- tpsov_sw |> | |
| filter(half_cycle != 1) |> | |
| pull(tpsov_sw) |> | |
| diff() | |
| shift_desc <- ifelse( | |
| sw_shift < 0, | |
| glue("Democrats +{round(-sw_shift,3)}"), | |
| glue("Republicans +{round(sw_shift,3)}") | |
| ) | |
| # Plot our second map | |
| plot_tpsov_delta <- map_with_values_delta |> | |
| ggplot() + | |
| geom_sf(aes(fill=tpsov_delta, geometry=geometry, size=.2)) + | |
| scale_size_identity() + | |
| scale_fill_gradient2( | |
| na.value = "lightgrey", | |
| low = "blue", | |
| high = "red", | |
| mid = "lightgrey", | |
| midpoint = sw_shift, | |
| limits = c(-.2, .2) + sw_shift, | |
| oob = scales::squish, | |
| breaks = c(-.2, 0, .2) + sw_shift, | |
| labels=c("Democratic shift", glue("Average shift ({shift_desc})"), "Republican shift"), | |
| ) + | |
| theme( | |
| axis.ticks = element_blank(), | |
| axis.text.x = element_blank(), | |
| axis.text.y = element_blank(), | |
| axis.title.x = element_blank(), | |
| axis.title.y = element_blank(), | |
| panel.background = element_rect(fill = "lightblue"), | |
| panel.grid.major = element_line(color = "lightblue"), | |
| legend.position = "bottom", | |
| legend.key.width = unit(2, "cm"), | |
| legend.title = element_blank(), | |
| plot.title = element_text(hjust=0.5), | |
| plot.subtitle = element_text(hjust=0.5), | |
| plot.title.position = "panel", | |
| ) + | |
| coord_sf() + | |
| labs(title=str_wrap(glue( | |
| "Shift in share of two-party vote within New Jersey, {first_year}-{first_year + 5} to {final_year - 5}-{final_year}" | |
| ), width=50), | |
| subtitle = str_wrap("Elections for Governor, Senate and President", width=50), | |
| caption="Source: NJ Division of Elections") | |
| # Save plots | |
| # ggsave("tpsov.png", plot = plot_tpsov) | |
| # ggsave("tpsov_delta.png", plot_tpsov_delta) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment