This report displays an interactive web map using the ‘leaflet’ package of acoustic receiver detections of juvenile Chinook salmon in the Klamath River, California as they migrate to the ocean before (<=2024) and after (>2024) dam removals.
The map is meant to answer two questions:
The map includes a satellite basemap, custom markers colored by total detections, a legend, popups showing detections per year, and label of site IDs.
library(leaflet)
library(dplyr)
library(tidyr)
Citation: Smith, C.D., Burdick, S.M., Hatton, T.W., and Pullano, C.F., 2025, Juvenile Chinook Salmon Outmigration JSATS Telemetry Tagging and Detections in the Klamath River (ver. 3.0, September 2026): U.S. Geological Survey data release, https://doi.org/10.5066/P13S9QIM.
Two files were used:
detections_summary.csv: columns SITE_ID,
YEAR, n_detections (already summarized with
one row per receiver per year).receiver_locations.csv: columns SITE_ID,
LAT, LON. The detections file has no
coordinates, so it must be joined with the receiver location coordinates
file provided with the data.det <- read.csv("data/detections_summary.csv")
loc <- read.csv("data/Locations.csv")
For each site I compute:
# calculate total detections per site for popup and marker color
totals <- det %>%
group_by(SITE_ID) %>%
summarise(total = sum(n_detections), .groups = "drop")
# create HTML popups showing detections per year and total detections
popup_tbl <- det %>%
arrange(SITE_ID, YEAR) %>%
group_by(SITE_ID) %>%
summarise(
rows = paste0("<tr><td>", YEAR, "</td><td style='text-align:right'>",
format(n_detections, big.mark = ","), "</td></tr>",
collapse = ""), .groups = "drop")
# join tables by receiver ID so that all rows have location data
map_df <- loc %>%
inner_join(totals, by = "SITE_ID") %>%
inner_join(popup_tbl, by = "SITE_ID") %>%
mutate(popup = paste0(
"<div style='min-width:150px'>",
"<h4 style='margin:0 0 4px 0'>", SITE_ID, "</h4>",
"<table style='width:100%'>",
"<tr><th style='text-align:left'>Year</th>",
"<th style='text-align:right'>Detections</th></tr>",
rows,
"<tr style='border-top:1px solid #999'><td><b>Total</b></td>",
"<td style='text-align:right'><b>", format(total, big.mark = ","),
"</b></td></tr></table></div>"))
Total detections is colorized in 4 quanitles
# create quanitle breaks for color palette
brks <- quantile(map_df$total, probs = seq(0, 1, 0.25))
brks <- unique(round(brks))
# create labels for legend
labs <- paste0(format(head(brks, -1), big.mark = ","), " – ",
format(tail(brks, -1), big.mark = ","))
# create color pallettes
marker_cols <- c("beige", "orange", "red", "darkred")
legend_cols <- c("#FFCB92", "#F69730", "#D33D2A", "#A23336")
# create marker data frame
map_df <- map_df %>%
mutate(class = cut(total, breaks = brks, include.lowest = TRUE, labels =
FALSE), marker_color = marker_cols[class])
# create icons
icons <- awesomeIcons(
icon = "signal",
library = "fa",
markerColor = map_df$marker_color,
iconColor = "white")
The map is built in layers:
leaflet(map_df) %>%
# basemap
addProviderTiles(providers$Esri.WorldImagery, group = "Satellite") %>%
addProviderTiles(providers$Stadia.StamenTonerLabels) %>%
# markers with popups
addAwesomeMarkers(
lng = ~Longitude, lat = ~Latitude,
icon = icons,
popup = ~popup,
group = "Receivers") %>%
# site-ID labels (separate layer so they can be toggled)
addLabelOnlyMarkers(
lng = ~Longitude, lat = ~Latitude, label = ~SITE_ID,
group = "Site labels",
labelOptions = labelOptions(
noHide = TRUE, direction = "bottom", textOnly = FALSE,
offset = c(0,2),
style = list("font-size" = "10px", "font-weight" = "bold",
"padding" = "1px 4px"))) %>%
# legend
addLegend(
position = "bottomright",
colors = legend_cols[seq_along(labs)],
labels = labs,
title = "Total detections",
opacity = 1) %>%
# layer toggles, scale bar, inset map
addLayersControl(
baseGroups = "Satellite",
overlayGroups = c("Receivers", "Site labels"),
options = layersControlOptions(collapsed = FALSE)) %>%
addScaleBar(position = "bottomleft") %>%
addMiniMap(toggleDisplay = TRUE, position = "topright")
The map shows that either not all receivers were deployed every year or had no fish detections in every year. There are no noticeable patterns in fish detections before and after dam removals.
Claude Sonnet 5.5 was used to generate HTML popups and assist with code debugging