The one modification I made to the original choropleth map was adding hover labels to each ZIP Tabulation Area (ZCTA). In the original map, users had to click on a specific ZIP code area to see the number of aggravated assaults. The updated map allows users to quickly move their mouse over a ZCTA and immediately see the ZIP code and incident count. This makes the map easier and faster to access because information is available without additional clicks

The modification was implemented by adding a label argument within the addPolygons() function. The code uses paste0() to combine the ZIP code (ZCTA5CE20) and the number of aggravated assaults (Incidents) into a text string.

I used AI to help generate ideas for improving the map. The suggestion I thought was useful was adding hover labels to improve the user experience. Another suggestion that was offered but not used was to include ZIP codes with zero assaults. I chose not to use it because this map focuses on and only shows ZIP codes with aggravated assaults.

Here is a look at the choropleth map:

Code:

############################################################
# Lesson 2: Mapping Crime by Area with a Choropleth Map
#
# This script creates a choropleth map showing how
# aggravated assaults are distributed across ZIP Code
# Tabulation Areas (ZCTAs).
#
# Instead of mapping individual incidents, this lesson:
#
# 1. Assigns each incident to a ZCTA.
# 2. Counts incidents within each ZCTA.
# 3. Colors ZCTAs based on incident totals.
#
# Darker shades represent more incidents.
############################################################

############################################################
# Step 1: Install Required Packages
############################################################

required_packages <- c(
  "jsonlite",
  "tidyverse",
  "sf",
  "leaflet",
  "tigris"
)

installed_packages <- rownames(installed.packages())

for (pkg in required_packages) {
  if (!pkg %in% installed_packages) {
    install.packages(pkg)
  }
}

############################################################
# Step 2: Load Required Packages
############################################################

library(jsonlite)
library(tidyverse)
library(sf)
library(leaflet)

options(tigris_use_cache = TRUE)

library(tigris)

############################################################
# Step 3: Download Aggravated Assault Data
############################################################

base_url <- paste0(
  "https://services2.arcgis.com/HdTo6HJqh92wn4D8/",
  "arcgis/rest/services/",
  "Metro_Nashville_Police_Department_Incidents_view/",
  "FeatureServer/0/query"
)

query <- paste(
  "Incident_Occurred >= DATE '2026-06-01'",
  "AND Incident_Occurred < DATE '2026-09-01'",
  "AND Offense_NIBRS = '13A'"
)

crime_url <- paste0(
  base_url,
  "?where=",
  URLencode(query, reserved = TRUE),
  "&outFields=*",
  "&f=json"
)

CrimeData <- fromJSON(
  crime_url
)$features$attributes

############################################################
# Step 4: Prepare the Data
############################################################

CrimeData <- CrimeData |>
  mutate(
    Incident_Occurred = as.POSIXct(
      Incident_Occurred / 1000,
      origin = "1970-01-01",
      tz = "America/Chicago"
    )
  ) |>
  filter(
    !is.na(Longitude),
    !is.na(Latitude)
  )

cat(
  "Number of aggravated assault incidents:",
  nrow(CrimeData),
  "\n"
)

############################################################
# Step 5: Convert Incidents into a Spatial Layer
############################################################

CrimeData_sf <- st_as_sf(
  CrimeData,
  coords = c("Longitude", "Latitude"),
  crs = 4326,
  remove = FALSE
)

############################################################
# Step 6: Download ZCTA Boundaries
############################################################

TN_ZCTAs <- zctas(
  cb = TRUE,
  year = 2020
)

############################################################
# Step 7: Keep Tennessee ZCTAs
############################################################

TN_ZCTAs <- TN_ZCTAs |>
  filter(
    startsWith(ZCTA5CE20, "37")
  )

############################################################
# Step 8: Match Coordinate Systems
############################################################

TN_ZCTAs <- st_transform(
  TN_ZCTAs,
  st_crs(CrimeData_sf)
)

############################################################
# Step 9: Assign Each Incident to a ZCTA
############################################################

CrimeWithZCTA <- st_join(
  CrimeData_sf,
  TN_ZCTAs |>
    select(ZCTA5CE20)
)

############################################################
# Step 10: Verify the Spatial Join
############################################################

CrimeWithZCTA |>
  st_drop_geometry() |>
  count(is.na(ZCTA5CE20))

############################################################
# Step 11: Count Incidents by ZCTA
############################################################

ZCTACounts <- CrimeWithZCTA |>
  st_drop_geometry() |>
  count(
    ZCTA5CE20,
    name = "Incidents"
  ) |>
  arrange(desc(Incidents))

ZCTACounts

############################################################
# Step 12: Keep Only ZCTAs Found in the Data
############################################################

Nashville_ZCTAs <- TN_ZCTAs |>
  filter(
    !is.na(ZCTA5CE20),
    ZCTA5CE20 %in% unique(ZCTACounts$ZCTA5CE20)
  )

############################################################
# Step 13: Join Incident Counts to ZCTAs
############################################################

Nashville_ZCTAs <- Nashville_ZCTAs |>
  left_join(
    ZCTACounts,
    by = "ZCTA5CE20"
  )

############################################################
# Step 14: Create a Choropleth Color Palette
############################################################

pal <- colorNumeric(
  palette = "Reds",
  domain = Nashville_ZCTAs$Incidents
)

############################################################
# Step 15: Create Popup Content
############################################################

ZCTAPopup <- ~paste0(
  "<strong>ZCTA:</strong> ",
  ZCTA5CE20,
  
  "<br><strong>Aggravated Assaults:</strong> ",
  Incidents
)

############################################################
# Step 16: Create the Choropleth Map
#
# Added hover labels
############################################################

ChoroplethMap <- leaflet(Nashville_ZCTAs) |>
  
  addProviderTiles("Esri.WorldStreetMap") |>
  
  addPolygons(
    fillColor = ~pal(Incidents),
    fillOpacity = 0.7,
    color = "black",
    weight = 1,
    popup = ZCTAPopup,
    
    label = ~paste0(
      "ZCTA: ",
      ZCTA5CE20,
      " | Aggravated Assaults: ",
      Incidents
    )
  ) |>
  
  addLegend(
    position = "bottomright",
    pal = pal,
    values = ~Incidents,
    title = "Aggravated Assaults"
  )

############################################################
# Step 17: Display the Map
############################################################

ChoroplethMap