if (!requireNamespace("tigris", quietly = TRUE)) {
install.packages("tigris")
}
library(tigris)
# Download NYC borough boundaries using tigris and cache
# Boroughs are counties in NYC — filter to the 5 NYC counties
options(tigris_use_cache = TRUE)
nyc_boroughs <- counties(state = "NY", cb = TRUE) |>
filter(NAME %in% c("New York", "Kings", "Queens", "Bronx", "Richmond")) |>
# Add borough name column to match our 311 data
mutate(
borough = case_when(
NAME == "New York" ~ "Manhattan",
NAME == "Kings" ~ "Brooklyn",
NAME == "Queens" ~ "Queens",
NAME == "Bronx" ~ "Bronx",
NAME == "Richmond" ~ "Staten Island"
)
)
# Aggregate 311 data to borough level for map popups
# One row per borough per weather category
map_data <- nyc_311_weather_analysis |>
filter(!is.na(borough)) |>
mutate(complaint_type = str_to_upper(str_trim(complaint_type))) |>
group_by(borough, weather_category) |>
summarise(
n_requests = n(),
median_hrs = median(resolution_hrs, na.rm = TRUE),
.groups = "drop"
)
# Top 5 complaint types per borough per weather category
top_complaints_by_borough <- nyc_311_weather_analysis |>
filter(!is.na(borough)) |>
mutate(complaint_type = str_to_upper(str_trim(complaint_type))) |>
group_by(borough, weather_category, complaint_type) |>
summarise(
n = n(),
median_hrs = median(resolution_hrs, na.rm = TRUE),
.groups = "drop"
) |>
group_by(borough, weather_category) |>
slice_max(n, n = 5) |>
summarise(
top_complaints = paste0(
complaint_type, " (", round(median_hrs, 1), " hrs)",
collapse = "<br/>"
),
.groups = "drop"
)
# Join map data with top complaints
map_data <- map_data |>
left_join(top_complaints_by_borough, by = c("borough", "weather_category"))
# Join map data to borough boundaries
# Need one sf object per weather category for the layer toggles
nyc_boroughs_normal <- nyc_boroughs |>
left_join(map_data |> filter(weather_category == "Normal"),
by = "borough")
nyc_boroughs_adverse <- nyc_boroughs |>
left_join(map_data |> filter(weather_category == "Adverse"),
by = "borough")
nyc_boroughs_extreme <- nyc_boroughs |>
left_join(map_data |> filter(weather_category == "Extreme"),
by = "borough")
# Colors for weather categories
pal <- colorNumeric(
palette = c("#2ecc71", "#f39c12", "#e74c3c"),
domain = map_data$median_hrs
)
# Popup template function
make_popup <- function(df) {
paste0(
"<b>", df$borough, "</b><br/>",
"<b>Weather Category:</b> ", df$weather_category, "<br/>",
"<b>Total Requests:</b> ", scales::comma(df$n_requests), "<br/>",
"<b>Median Resolution Time:</b> ", round(df$median_hrs, 1), " hrs<br/>",
"<hr/>",
"<b>Top 5 Complaint Types:</b><br/>",
df$top_complaints
)
}
# Build leaflet map with three toggleable layers
nyc_map <- leaflet(
options = leafletOptions(
minZoom = 10,
maxZoom = 14,
zoomSnap = 0.5
)
) |>
# Base map tile
addProviderTiles("CartoDB.Positron") |>
# Set initial view centered on NYC with zoom level that shows all 5 boroughs
setView(lng = -73.9857, lat = 40.7484, zoom = 10) |>
# Constrain map bounds to NYC area to prevent panning to outside NYC
setMaxBounds(
lng1 = -74.4, # Western bound
lat1 = 40.45, # Southern bound
lng2 = -73.65, # Eastern bound
lat2 = 40.95 # Northern bound
) |>
addPolygons(
data = nyc_boroughs_normal,
group = "Normal Weather",
fillColor = ~pal(median_hrs),
fillOpacity = 0.7,
color = "white",
weight = 2,
popup = make_popup(nyc_boroughs_normal),
highlight = highlightOptions(
weight = 3,
color = "#333",
bringToFront = TRUE
)
) |>
addPolygons(
data = nyc_boroughs_adverse,
group = "Adverse Weather",
fillColor = ~pal(median_hrs),
fillOpacity = 0.7,
color = "white",
weight = 2,
popup = make_popup(nyc_boroughs_adverse),
highlight = highlightOptions(
weight = 3,
color = "#333",
bringToFront = TRUE
)
) |>
addPolygons(
data = nyc_boroughs_extreme,
group = "Extreme Weather",
fillColor = ~pal(median_hrs),
fillOpacity = 0.7,
color = "white",
weight = 2,
popup = make_popup(nyc_boroughs_extreme),
highlight = highlightOptions(
weight = 3,
color = "#333",
bringToFront = TRUE
)
) |>
addLayersControl(
baseGroups = c("Normal Weather", "Adverse Weather", "Extreme Weather"),
options = layersControlOptions(collapsed = FALSE)
) |>
addLegend(
pal = pal,
values = map_data$median_hrs,
title = "Median Resolution<br/>Time (hrs)",
position = "bottomright"
) |>
hideGroup("Adverse Weather") |>
hideGroup("Extreme Weather")
nyc_map