Redistricting in Tennessee

The Maps below show the difference in the congressional districts in Tennessee from 2021 to the redistricitng that occurred in 2022. The redistricting looks to hurt minority voters, especially in the Nashville and Dickson areas. In 2021 Minority voters were in a district made up of about 60% of white voters, while in 2022 the redistricting moved those numbers up to 70% white voters in the same areas hurting minority voters. The redistricting made it harder for minority votes to matter making it so their voices are almost not heard.

These findings are further seen using a scatter plot that shows the percentage of votes for Kamala Harris in the 2024 election compared to the percentage of non-white voters. The highest percentages are coming from the districts on the map with the lowest white voter population.


U.S. House Districts in Tennessee, 2021


U.S. House Districts in Tennessee, 2022



CODE:

# ----------------------------------------------------------
# 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("5bb969fad7e39e89d2b686059660978d67b548b9")

# ----------------------------------------------------------
# Step 4: Fetch ACS codebooks (for variable lookup if needed)
# ----------------------------------------------------------

DetailedTables <- load_variables(2021, "acs5", cache = TRUE)
SubjectTables  <- load_variables(2021, "acs5/subject", cache = TRUE)
ProfileTables  <- load_variables(2021, "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("5bb969fad7e39e89d2b686059660978d67b548b9")

# ----------------------------------------------------------
# Step 4: Fetch ACS codebooks (for variable lookup if needed)
# ----------------------------------------------------------

DetailedTables <- load_variables(2022, "acs5", cache = TRUE)
SubjectTables  <- load_variables(2022, "acs5/subject", cache = TRUE)
ProfileTables  <- load_variables(2022, "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
)


# ----------------------------------------------------------
# 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")

# ----------------------------------------------------------
# Step 4: Create scatterplot with OLS trend line
# ----------------------------------------------------------

Scatterplot <- plot_ly(
  data = mydata,
  x = ~Pct_Nonwhite,
  y = ~Pct_Harris,
  type = "scatter",
  mode = "markers",
  text = ~Precinct,   # <-- FIXED COLUMN NAME
  hoverinfo = "text+x+y",
  marker = list(
    size = 8,
    opacity = 0.7,
    color = "#4C78A8"
  )
) %>%
  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