Gerrymandering and Tennessee

Gerrymandering is defined as the practice of redrawing electoral district boundaries to maximize the voting power of the party in control of redistricting while minimizing the voting power of the opposition. This tactic is not illegal, unless it is used to restrict the voting rights of minorities. However any gerrymandering can be seen as a grey area, as it still aims to essentially strip entire area’s of people of their right to choose. in 2022 Tennessee was victim to Gerrymandering. As you can see by the two maps depicting the before and after of Tennessee’s gerrymandering, some of the districs were altered drastically. Most notably it meant that residents of Davidson County could no longer vote for the same U.S. House representative. Instead, the altering of districts split Davidson County across the map. The Southern part of the Davidson County remained in District five, however the district was expanded to include parts of Williamson, Maury, and Wilson counties. Meanwhile, the northern part of Davidson landed in District seven, which stretches into West Tennessee. Finally, the Eastern part of the county went into District six, which runs as far east as Scott County. This all begs the question, is this legal? its a question not even the supreme court really knows the answer too. In my humble opinion, I’m going to say it shouldn’t be legal, stripping the voting rights of any U.S. citizen should be a crime, minority or not. This practice is not ethical in any way and I don’t think it should be legal.


U.S. House districts in Tennessee, 2021

This map depicts the Tennessee house districts as they were before the gerrymandering took place. As you can see, the city of Nashville is still in one district. Districts six and seven are also bigger.

# ----------------------------------------------------------
# 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("694fbfdda50f7e056ef5047f0387c6c23749827f")

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

U.S. House districts in Tennessee, 2022

This map is showing what the house districts look like today, after the gerrymandering took place. as mentioned above, Davidson County was split between districts five (where it was before) six, and seven, making it substantially harder for the predominately blue area of Tennessee elect a democrat.

# ----------------------------------------------------------
# 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("694fbfdda50f7e056ef5047f0387c6c23749827f")

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

Pct. for Harris by Pct. Nonwhite for Davidson County, Tennessee

As mentioned above, Davidson County, Tennessee tends to vote blue. The scatterplot below shows each Davidson County precinct by both its percentage of nonwhite voters and its percentage of voters who backed Democratic presidential candidate Kamala Harris in the 2024 general election. Given the trend line, you can deduct that the majority of nonwhite voters gave candidate Harris their vote.

#Scatterplot Code

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 = "#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. for Harris"),
    showlegend = FALSE
  )

Scatterplot