The change in Tennessee Congressional districts from 2021 to 2022 on the maps and graphs below show that the overall percentage of white people in the majority of the districts increased with the redistricting. Often, race and politics are tied together, and this is clearly shown by the scatterplot below. As the percentage for nonwhite voters in various precincts increases, so does the percentage of votes for 2024 Democratic presidential candidate Kamala Harris in Davidson County. By splitting Davidson County into so many parts, it partially disenfranchised nonwhite voters by making it more difficult for their votes to be considered against the larger white populations.
The map shows that the largest percentage of nonwhite voters used to exist in district 5 in 2021, but with the redistricting, it changed to district 7 holding the largest percentage. What used to be district 5 became divided among the other districts, decreasing the percentage of nonwhite voters in districts overall.
# ----------------------------------------------------------
# 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("e96c46601fca77f6f3fcb0f72b673a75aed0ff2a")
# ----------------------------------------------------------
# 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)
# ----------------------------------------------------------
# Fetching the data
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("C")
# 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 = "Greens", # 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("e96c46601fca77f6f3fcb0f72b673a75aed0ff2a")
# ----------------------------------------------------------
# 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)
# ----------------------------------------------------------
# Fetching the data
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("C")
# 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 = "Greens", # 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("plotly"))
install.packages("plotly")
# ----------------------------------------------------------
# Step 2: Load libraries
# ----------------------------------------------------------
library(tidyverse)
library(plotly)
# ----------------------------------------------------------
# Step 3: Read data from CSV file
# ----------------------------------------------------------
mydata <- read.csv("https://github.com/drkblake/Data/raw/refs/heads/main/Davidson2024.csv")
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 = "#227c40" # 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. for Harris"),
showlegend = FALSE
)
Scatterplot