Skip to content

Instantly share code, notes, and snippets.

@tor-gu
Last active November 11, 2025 21:15
Show Gist options
  • Select an option

  • Save tor-gu/3888beb2b4a43afdf0cc40ea96cd9008 to your computer and use it in GitHub Desktop.

Select an option

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
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