This map shows where aggravated assaults were reported in Nashville between June 1 and August 31, 2026, using data from the Metro Nashville Police Department’s public incident API. Each circle marks a location where at least one assault occurred. Circle size and color reflect how many incidents happened there: gray for one, gold for two to three, and red for four or more.
I made three changes to the original frequency-class map to make it more useful for someone exploring the data using Claude:
**Layer controls for each frequency category.** A checkbox panel in the top-right corner lets viewers turn each category on or off. In the original map, thousands of gray single-incident dots covered the few locations with repeated assaults. Now a viewer can uncheck “One Incident” and see the city’s repeat hotspots right away.
**A heat map layer.** The circles show exact locations. The heat map, built from every individual incident, shows which *areas* have the most assaults, even when the incidents didn’t happen at the same address. It starts hidden, and viewers can switch it on to see the broader pattern and then compare it with the point layers.
**A clickable Google Maps link in each popup.** The original popup listed coordinates that viewers had to copy and paste into Google Maps. Now each popup has an “Open in Google Maps” link that opens the location in a new tab, where Street View and nearby businesses can help explain why incidents cluster there.
I also changed the drawing order so the red high-frequency circles are drawn last and appear on top of the smaller gray ones instead of being buried beneath them.
############################################################
# Lesson 1 (Modified): Mapping Crime Incidents with Points
#
# This script creates a frequency-class map showing where
# aggravated assaults occurred during Summer 2026.
#
# Larger circles represent locations with more incidents.
#
# Circle colors represent frequency categories:
# Gray = One Incident
# Gold = Two to Three Incidents
# Red = Four or More Incidents
#
# MODIFICATIONS:
# 1. Layer control to turn each frequency category on/off
# 2. Heat map layer showing incident density
# 3. Clickable Google Maps link in each popup
############################################################
############################################################
# Step 1: Install Required Packages
############################################################
required_packages <- c(
"jsonlite",
"tidyverse",
"leaflet",
"leaflet.extras" # NEW: provides addHeatmap()
)
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(leaflet)
library(leaflet.extras) # NEW
############################################################
# 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"
),
Incident_Reported = as.POSIXct(
Incident_Reported / 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: Aggregate Incidents by Location
############################################################
CrimeLocations <- CrimeData |>
count(
Latitude,
Longitude,
name = "Incidents"
) |>
arrange(Incidents) # NEW: draw big/red dots last so they sit on top
############################################################
# Step 6: Create Frequency Categories
############################################################
CrimeLocations <- CrimeLocations |>
mutate(
FrequencyGroup = case_when(
Incidents == 1 ~ "One Incident",
Incidents <= 3 ~ "Two to Three Incidents",
TRUE ~ "Four or More Incidents"
)
)
############################################################
# Step 7: Create Popup Content
#
# NEW: The coordinates are now a clickable link that opens
# the location in Google Maps in a new browser tab.
############################################################
LocationPopup <- ~paste0(
"<strong>Incidents at This Location:</strong> ",
Incidents,
"<br><strong>Frequency Category:</strong> ",
FrequencyGroup,
"<br><br><a href='https://www.google.com/maps/search/?api=1&query=",
round(Latitude, 6), ",", round(Longitude, 6),
"' target='_blank'>Open in Google Maps</a>"
)
############################################################
# Step 8: Assign Colors to Categories
############################################################
CrimeLocations <- CrimeLocations |>
mutate(
DotColor = case_when(
FrequencyGroup == "One Incident" ~ "gray",
FrequencyGroup == "Two to Three Incidents" ~ "gold",
FrequencyGroup == "Four or More Incidents" ~ "red"
)
)
############################################################
# Step 9: Create the Frequency-Class Map
#
# NEW:
# - Heat map layer built from every individual incident
# (CrimeData), hidden by default
# - Each frequency category is its own layer group
# - Layer control lets users toggle any combination
############################################################
FrequencyClassMap <- leaflet() |>
addProviderTiles("Esri.WorldStreetMap") |>
# NEW: Heat map layer (uses raw incidents, not aggregated)
addHeatmap(
data = CrimeData,
lng = ~Longitude,
lat = ~Latitude,
radius = 12,
blur = 15,
group = "Heat Map"
) |>
addCircleMarkers(
data = CrimeLocations,
lng = ~Longitude,
lat = ~Latitude,
radius = ~sqrt(Incidents) * 3,
stroke = TRUE,
weight = 1,
color = "black",
fillColor = ~DotColor,
fillOpacity = 0.7,
popup = LocationPopup,
group = ~FrequencyGroup # NEW: one layer per category
) |>
addLegend(
position = "bottomright",
colors = c("gray", "gold", "red"),
labels = c(
"One Incident",
"Two to Three Incidents",
"Four or More Incidents"
),
title = "Incident Frequency"
) |>
# NEW: Checkbox panel to turn layers on and off
addLayersControl(
overlayGroups = c(
"One Incident",
"Two to Three Incidents",
"Four or More Incidents",
"Heat Map"
),
position = "topright",
options = layersControlOptions(collapsed = FALSE)
) |>
hideGroup("Heat Map") # NEW: start with heat map off
FrequencyClassMap