The maps and graphics show concentrated minority populations in urban areas around Nashville and Memphis, and a clear positive relationship between percent nonwhite and percent voting for Harris, indicating cohesive minority voting behavior. The maps and graphics together suggest the 2022 lines fragmented those urban concentrations across multiple districts rather than preserving a single minority‑influenced district. That is consistent with voting dilution of minority communities seen in other places across the country, indicating that there was intention behind the new district lines.
# ----------------------------------------------------------
# Step 1: Install required packages (if missing)
# ----------------------------------------------------------
if (!require("tidyverse"))
install.packages("tidyverse")
if (!require("tidycensus"))
install.packages("tidycensus")
if (!require("sf"))
install.packages("sf")
if (!require("leaflet"))
install.packages("leaflet")
if (!require("htmlwidgets"))
install.packages("htmlwidgets")
if (!require("plotly"))
install.packages("plotly") # For the interactive dot plot
# ----------------------------------------------------------
# Step 2: Load libraries
# ----------------------------------------------------------
library(tidyverse)
library(tidycensus)
library(sf)
library(leaflet)
library(htmlwidgets)
library(plotly)
# ----------------------------------------------------------
# Step 3: Transmit Census API key
# ----------------------------------------------------------
census_api_key("b2b546278d54eb1ced62b21ec507abf728af01c2")
# ----------------------------------------------------------
# Step 4: Fetch ACS codebooks (for variable lookup if needed)
# ----------------------------------------------------------
DetailedTables <- load_variables(2024, "acs5", cache = TRUE)
SubjectTables <- load_variables(2024, "acs5/subject", cache = TRUE)
ProfileTables <- load_variables(2024, "acs5/profile", cache = TRUE)
# ----------------------------------------------------------
# Step 5: Specify target variable(s)
# ----------------------------------------------------------
VariableList <- c(Estimate_ = "DP05_0037P")
# ----------------------------------------------------------
# Step 6: Fetch ACS data (county subdivision, Tennessee)
# ----------------------------------------------------------
mydata <- get_acs(
geography = "congressional district",
state = c("TN"),
variables = VariableList,
year = 2021,
survey = "acs1",
output = "wide",
geometry = TRUE
)
# ----------------------------------------------------------
# Step 7: Rename the "NAME" field as "Area"
# ----------------------------------------------------------
mydata <- mydata %>%
rename(Area = NAME)
# ----------------------------------------------------------
# Step 8: Filter flexibly, based on the "Area" field
# ----------------------------------------------------------
search_terms <- c("ALL")
# Note: use c("ALL") to capture all areas
if (length(search_terms) == 1 && toupper(search_terms) == "ALL") {
filtereddata <- mydata
} else {
or_pattern <- paste(
stringr::str_replace_all(search_terms, "([\\W])", "\\\\\\1"),
collapse = "|"
)
filtereddata <- mydata %>%
dplyr::filter(
stringr::str_detect(
Area %>% tidyr::replace_na(""),
stringr::regex(or_pattern, ignore_case = TRUE)
)
)
}
# ----------------------------------------------------------
# Step 9: Prepare data for mapping (rename, as sf, CRS)
# ----------------------------------------------------------
mapdata <- filtereddata %>%
rename(
Estimate = Estimate_E,
Range = Estimate_M
) %>%
st_as_sf()
# Ensure CRS is WGS84 for Leaflet
mapdata <- st_transform(mapdata, 4326)
# ----------------------------------------------------------
# Step 10: Build color palette with quantile-based breaks
# ----------------------------------------------------------
qs <- quantile(mapdata$Estimate, probs = seq(0, 1, length.out = 6), na.rm = TRUE)
pal <- colorBin(
palette = "Blues", # Can specify other palettes here
domain = mapdata$Estimate,
bins = qs,
pretty = FALSE
)
# ----------------------------------------------------------
# Step 11: Build the plotly dot plot with error bars
# ----------------------------------------------------------
plotdf <- filtereddata %>%
sf::st_drop_geometry() %>%
dplyr::mutate(
point_color = pal(Estimate_E),
y_ordered = reorder(Area, Estimate_E),
hover_text = paste0("Area: ", Area)
)
mygraph <- plotly::plot_ly(
data = plotdf,
x = ~Estimate_E,
y = ~as.character(y_ordered),
type = "scatter",
mode = "markers",
showlegend = FALSE,
marker = list(
color = ~point_color,
size = 8,
line = list(color = "rgba(120,120,120,0.9)", width = 0.5)
),
error_x = list(
type = "data",
array = ~Estimate_M,
arrayminus = ~Estimate_M,
color = "rgba(0,0,0,0.65)",
thickness = 1
),
text = ~hover_text,
hovertemplate = "%{text}<br>%{x:,}<extra></extra>"
) %>%
plotly::layout(
title = list(
text = "Estimates by area<br><sup>County subdivisions. Brackets show error margins.</sup>"
),
xaxis = list(
title = "ACS estimate",
automargin = TRUE,
tickformat = ",.0f"
),
yaxis = list(
title = "",
automargin = TRUE,
categoryorder = "array",
# preserve the reorder() order on the y-axis
categoryarray = levels(plotdf$y_ordered)
),
margin = list(l = 180, r = 20, b = 60, t = 60, pad = 2)
)
mygraph
# ----------------------------------------------------------
# Step 12: Create popup content for the map
# ----------------------------------------------------------
mapdata$popup <- paste0(
"<strong>", mapdata$Area, "</strong><br/>",
"<hr>",
"Estimate: ", format(mapdata$Estimate, big.mark = ","), "<br/>",
"Plus/Minus: ", format(mapdata$Range, big.mark = ",")
)
# ----------------------------------------------------------
# Step 13: Build the Leaflet map
# ----------------------------------------------------------
DivisionMap <- leaflet(mapdata) %>%
# Choose one basemap:
addProviderTiles(providers$CartoDB.Positron) %>%
# addProviderTiles(providers$Esri.WorldStreetMap, group = "Streets (Esri World Street Map)") %>%
# addProviderTiles(providers$Esri.WorldImagery, group = "Satellite (Esri World Imagery)") %>%
addPolygons(
fillColor = ~pal(Estimate),
fillOpacity = 0.5,
color = "black",
weight = 1,
popup = ~popup
) %>%
addLegend(
pal = pal,
values = ~Estimate,
title = "Estimate",
labFormat = labelFormat(big.mark = ",")
)
DivisionMap
# ----------------------------------------------------------
# Step 14: Export graph as a standalone HTML file
# ----------------------------------------------------------
# This creates a fully self-contained HTML file for the dot plot.
saveWidget(
widget = as_widget(mygraph),
file = "ACSGraph.html",
selfcontained = TRUE
)
# ----------------------------------------------------------
# Step 15: Export map as a standalone HTML file
# ----------------------------------------------------------
# This creates a fully self-contained HTML you can open or share.
# Adjust the path/filename as you like.
saveWidget(
widget = DivisionMap,
file = "ACSMap.html",
selfcontained = TRUE
)
# ----------------------------------------------------------
# Step 1: Install required packages (if missing)
# ----------------------------------------------------------
if (!require("tidyverse"))
install.packages("tidyverse")
if (!require("tidycensus"))
install.packages("tidycensus")
if (!require("sf"))
install.packages("sf")
if (!require("leaflet"))
install.packages("leaflet")
if (!require("htmlwidgets"))
install.packages("htmlwidgets")
if (!require("plotly"))
install.packages("plotly") # For the interactive dot plot
# ----------------------------------------------------------
# Step 2: Load libraries
# ----------------------------------------------------------
library(tidyverse)
library(tidycensus)
library(sf)
library(leaflet)
library(htmlwidgets)
library(plotly)
# ----------------------------------------------------------
# Step 3: Transmit Census API key
# ----------------------------------------------------------
census_api_key("b2b546278d54eb1ced62b21ec507abf728af01c2")
# ----------------------------------------------------------
# Step 4: Fetch ACS codebooks (for variable lookup if needed)
# ----------------------------------------------------------
DetailedTables <- load_variables(2024, "acs5", cache = TRUE)
SubjectTables <- load_variables(2024, "acs5/subject", cache = TRUE)
ProfileTables <- load_variables(2024, "acs5/profile", cache = TRUE)
# ----------------------------------------------------------
# Step 5: Specify target variable(s)
# ----------------------------------------------------------
VariableList <- c(Estimate_ = "DP05_0037P")
# ----------------------------------------------------------
# Step 6: Fetch ACS data (county subdivision, Tennessee)
# ----------------------------------------------------------
mydata <- get_acs(
geography = "congressional district",
state = c("TN"),
variables = VariableList,
year = 2022,
survey = "acs1",
output = "wide",
geometry = TRUE
)
# ----------------------------------------------------------
# Step 7: Rename the "NAME" field as "Area"
# ----------------------------------------------------------
mydata <- mydata %>%
rename(Area = NAME)
# ----------------------------------------------------------
# Step 8: Filter flexibly, based on the "Area" field
# ----------------------------------------------------------
search_terms <- c("ALL")
# Note: use c("ALL") to capture all areas
if (length(search_terms) == 1 && toupper(search_terms) == "ALL") {
filtereddata <- mydata
} else {
or_pattern <- paste(
stringr::str_replace_all(search_terms, "([\\W])", "\\\\\\1"),
collapse = "|"
)
filtereddata <- mydata %>%
dplyr::filter(
stringr::str_detect(
Area %>% tidyr::replace_na(""),
stringr::regex(or_pattern, ignore_case = TRUE)
)
)
}
# ----------------------------------------------------------
# Step 9: Prepare data for mapping (rename, as sf, CRS)
# ----------------------------------------------------------
mapdata <- filtereddata %>%
rename(
Estimate = Estimate_E,
Range = Estimate_M
) %>%
st_as_sf()
# Ensure CRS is WGS84 for Leaflet
mapdata <- st_transform(mapdata, 4326)
# ----------------------------------------------------------
# Step 10: Build color palette with quantile-based breaks
# ----------------------------------------------------------
qs <- quantile(mapdata$Estimate, probs = seq(0, 1, length.out = 6), na.rm = TRUE)
pal <- colorBin(
palette = "Blues", # Can specify other palettes here
domain = mapdata$Estimate,
bins = qs,
pretty = FALSE
)
# ----------------------------------------------------------
# Step 11: Build the plotly dot plot with error bars
# ----------------------------------------------------------
plotdf <- filtereddata %>%
sf::st_drop_geometry() %>%
dplyr::mutate(
point_color = pal(Estimate_E),
y_ordered = reorder(Area, Estimate_E),
hover_text = paste0("Area: ", Area)
)
mygraph <- plotly::plot_ly(
data = plotdf,
x = ~Estimate_E,
y = ~as.character(y_ordered),
type = "scatter",
mode = "markers",
showlegend = FALSE,
marker = list(
color = ~point_color,
size = 8,
line = list(color = "rgba(120,120,120,0.9)", width = 0.5)
),
error_x = list(
type = "data",
array = ~Estimate_M,
arrayminus = ~Estimate_M,
color = "rgba(0,0,0,0.65)",
thickness = 1
),
text = ~hover_text,
hovertemplate = "%{text}<br>%{x:,}<extra></extra>"
) %>%
plotly::layout(
title = list(
text = "Estimates by area<br><sup>County subdivisions. Brackets show error margins.</sup>"
),
xaxis = list(
title = "ACS estimate",
automargin = TRUE,
tickformat = ",.0f"
),
yaxis = list(
title = "",
automargin = TRUE,
categoryorder = "array",
# preserve the reorder() order on the y-axis
categoryarray = levels(plotdf$y_ordered)
),
margin = list(l = 180, r = 20, b = 60, t = 60, pad = 2)
)
mygraph
# ----------------------------------------------------------
# Step 12: Create popup content for the map
# ----------------------------------------------------------
mapdata$popup <- paste0(
"<strong>", mapdata$Area, "</strong><br/>",
"<hr>",
"Estimate: ", format(mapdata$Estimate, big.mark = ","), "<br/>",
"Plus/Minus: ", format(mapdata$Range, big.mark = ",")
)
# ----------------------------------------------------------
# Step 13: Build the Leaflet map
# ----------------------------------------------------------
DivisionMap <- leaflet(mapdata) %>%
# Choose one basemap:
addProviderTiles(providers$CartoDB.Positron) %>%
# addProviderTiles(providers$Esri.WorldStreetMap, group = "Streets (Esri World Street Map)") %>%
# addProviderTiles(providers$Esri.WorldImagery, group = "Satellite (Esri World Imagery)") %>%
addPolygons(
fillColor = ~pal(Estimate),
fillOpacity = 0.5,
color = "black",
weight = 1,
popup = ~popup
) %>%
addLegend(
pal = pal,
values = ~Estimate,
title = "Estimate",
labFormat = labelFormat(big.mark = ",")
)
DivisionMap
# ----------------------------------------------------------
# Step 14: Export graph as a standalone HTML file
# ----------------------------------------------------------
# This creates a fully self-contained HTML file for the dot plot.
saveWidget(
widget = as_widget(mygraph),
file = "ACSGraph.html",
selfcontained = TRUE
)
# ----------------------------------------------------------
# Step 15: Export map as a standalone HTML file
# ----------------------------------------------------------
# This creates a fully self-contained HTML you can open or share.
# Adjust the path/filename as you like.
saveWidget(
widget = DivisionMap,
file = "ACSMap.html",
selfcontained = TRUE
)
Scatterplot <- plot_ly(
data = mydata,
x = ~Pct_Nonwhite,
y = ~Pct_Harris,
type = "scatter",
mode = "markers",
text = ~Precinct,
hoverinfo = "text+x+y",
marker = list(
size = 8,
opacity = 0.7,
color = "#4C78A8" # single color for all points
)
) %>%
add_trace(
type = "scatter",
mode = "lines",
x = ~Pct_Nonwhite,
y = fitted(lm(Pct_Harris ~ Pct_Nonwhite, data = mydata)),
name = "OLS trend",
line = list(color = "black", width = 2),
inherit = FALSE
) %>%
layout(
title = "Pct. for Harris by Pct. Nonwhite",
xaxis = list(title = "Pct. Nonwhite"),
yaxis = list(title = "Pct. Harris"),
showlegend = FALSE
)
Scatterplot