I made two modifications to the original crime map by adding a scale bar and implementing marker clustering. These changes improved the map’s usability and made it easier to interpret where assault incidents occured across Nashville.
The first modification was the addition of a scale bar in the top-right corner of the map. The original map did not have a reference for distance. With this addition, you can now quickly estimate how far apart crime locations are from one another in miles. The scale bar improves the map by giving a better understanding of geographic distance and the overall size of the study area.
The scale bar was added using the Leaflet
function:
addScaleBar(position = “topright”) ROW 190 OF CODE
This function automatically places a distance scale on the map and updates it as users zoom in and out.
The second and best modification was the addition of marker clustering. In the original map, many points overlapped in areas with a high concentration of incidents, making it difficult to distinguish individual locations and causing the map to appear cluttered. The marker clustering feature groups nearby incidents together when the map is zoomed out and separates them into individual points as the user zooms in. This reduces visual clutter while still allowing users to explore detailed information at larger scales.
The clustering was implemented within the
addCircleMarkers() function using:
This code instructs Leaflet to automatically create
clusters for points that are located close together. As a result, the
map is easier to navigate and hotspots can be identified more
quickly.
I used Copilot as a tool to help brainstorm possible map enhancements
and troubleshoot issues that came about. As mentioned above, two useful
suggestion were adding a scale bar and marker clustering to improve the
map’s clarity. Copilot also helped identify where the
clusterOptions = markerClusterOptions() code should be
placed within the addCircleMarkers() function.
Overall, Copilot provided several ideas, one of which I initially implemented but ultimately decided to remove. This suggestion was to add a title directly to the map. After testing the modification, I decided not to include it because the title took up valuable space and sometimes covered data points when the map was moved around. This made some incident locations more difficult to see. Instead, I kept the scale bar and marker clustering because they improved functionality without reducing the visible area used to display the data.
############################################################
# Lesson 1: 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
############################################################
############################################################
# Step 1: Install Required Packages
#
# Check whether the required packages are installed.
# Install any that are missing.
############################################################
required_packages <- c(
"jsonlite",
"tidyverse",
"leaflet"
)
installed_packages <- rownames(installed.packages())
for (pkg in required_packages) {
if (!pkg %in% installed_packages) {
install.packages(pkg)
}
}
############################################################
# Step 2: Load Required Packages
#
# jsonlite = download data from the API
# tidyverse = clean and summarize data
# leaflet = create interactive maps
############################################################
library(jsonlite)
library(tidyverse)
library(leaflet)
############################################################
# Step 3: Download Aggravated Assault Data
#
# Retrieve Summer 2026 aggravated assault incidents
# (NIBRS code 13A) from Nashville's crime API.
############################################################
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
#
# Convert date fields and remove records that are
# missing latitude or longitude coordinates.
############################################################
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
#
# Count the number of incidents occurring at each
# unique latitude-longitude coordinate pair.
############################################################
CrimeLocations <- CrimeData |>
count(
Latitude,
Longitude,
sort = TRUE,
name = "Incidents"
)
############################################################
# Step 6: Create Frequency Categories
#
# Group locations into three categories based on the
# number of incidents at each location.
############################################################
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
#
# Build the information that will appear when users
# click a point on the map.
############################################################
LocationPopup <- ~paste0(
"<strong>Incidents at This Location:</strong> ",
Incidents,
"<br><strong>Frequency Category:</strong> ",
FrequencyGroup,
"<br><br><strong>Coordinates for Google Maps:</strong><br>",
round(Latitude, 6),
", ",
round(Longitude, 6)
)
############################################################
# Step 8: Assign Colors to Categories
#
# Assign a color to each frequency category.
############################################################
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
#
# Symbol Size = Number of Incidents
# Symbol Color = Frequency Category
#
# This map highlights locations where assaults are
# concentrated across the city.
############################################################
FrequencyClassMap <- leaflet(CrimeLocations) |>
addProviderTiles("Esri.WorldStreetMap") |>
addScaleBar(position = "topright") |>
addCircleMarkers(
lng = ~Longitude,
lat = ~Latitude,
radius = ~sqrt(Incidents) * 3,
stroke = TRUE,
weight = 1,
color = "black",
fillColor = ~DotColor,
fillOpacity = 0.7,
popup = LocationPopup,
clusterOptions = markerClusterOptions()
) |>
addLegend(
position = "bottomright",
colors = c(
"gray",
"gold",
"red"
),
labels = c(
"One Incident",
"Two to Three Incidents",
"Four or More Incidents"
),
title = "Incident Frequency"
)
FrequencyClassMap