1. Goal

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:

  1. Where are the receivers located?
  2. How many individual fish detections did each receiver record in total and per year?

The map includes a satellite basemap, custom markers colored by total detections, a legend, popups showing detections per year, and label of site IDs.

2. Packages

library(leaflet)
library(dplyr)
library(tidyr)

3. Data

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:

det <- read.csv("data/detections_summary.csv")
loc <- read.csv("data/Locations.csv")

4. Preparing the data for mapping

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>"))

5. Colour scheme and custom markers

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")

6. Building the map

The map is built in layers:

  1. Satellite basemap: Esri World Imagery with reference layer for context
  2. Markers: custom icons with popups (click) and site-ID labels.
  3. Legend: quartile classes of total detections per site.
  4. Extras: layer control (toggle labels), scale bar, and an inset map.
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")

7. Results

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.

Citations:

Claude Sonnet 5.5 was used to generate HTML popups and assist with code debugging