NDBC Salinity Map

Most recent readings from Everglades National Park buoys via SECOORA ERDDAP

Code
library(dplyr)
library(htmltools)
library(knitr)
library(leaflet)
library(readr)
library(scales)

source("R/fetch_ndbc_observations.R")
source("R/plot_ndbc_field.R")

ndbc_data <- fetch_ndbc_hourly_snapshots(
  param = "sea_water_practical_salinity",
  value_col = "salinity"
)

salinity_data <- ndbc_data$latest
start_time <- ndbc_data$start_time
end_time <- ndbc_data$end_time
data_as_of <- ndbc_data$data_as_of
report_time <- format(Sys.time(), "%Y-%m-%d %H:%M %Z", tz = ndbc_display_tz)
ref_date_label <- format(ndbc_data$reference_date, "%Y-%m-%d")
data_as_of_label <- format_ndbc_time(data_as_of)
query_window_label <- paste(
  format_ndbc_erddap_iso(start_time),
  "to",
  format_ndbc_erddap_iso(end_time)
)

Data are pulled from SECOORA ERDDAP: using a 48-hour window ending at the latest available ERDDAP time (2026-07-27 05:06 EDT). The buoy map below shows the most recent non-missing salinity value for each station. Interpolated fields use snapshots at 06:00, 12:00, 18:00, and 24:00 US Eastern on 2026-07-26.

Report generated: 2026-07-27 10:18 EDT
Latest ERDDAP data: 2026-07-27 05:06 EDT
Query window: 2026-07-25 05:06 EDT to 2026-07-27 05:06 EDT

Code
if (nrow(salinity_data) == 0) {
  stop("No salinity data returned from ERDDAP for the current time window.")
}

salinity_data |>
  arrange(desc(time)) |>
  mutate(
    time = format_ndbc_time(time),
    salinity = number(salinity, accuracy = 0.1)
  ) |>
  select(Station = station, `Station ID` = station_id, Time = time, `Salinity (PSU)` = salinity) |>
  kable()
Station Station ID Time Salinity (PSU)
Buoy_Key bkyf1 2026-07-27 05:00 EDT 46.3
Butternut_Kay bnkf1 2026-07-27 05:00 EDT 39.5
Bob_Allen_Key bobf1 2026-07-27 05:00 EDT 41.5
Blackwater_Sound bwsf1 2026-07-27 05:00 EDT 38.7
Duck_Key dkkf1 2026-07-27 05:00 EDT 38.2
Garfield_Bight gbtf1 2026-07-27 05:00 EDT 50.7
Highway_Creek hcef1 2026-07-27 05:00 EDT 31.5
Joe_Bay jbyf1 2026-07-27 05:00 EDT 30.8
Johnson_Key jkyf1 2026-07-27 05:00 EDT 45.4
Little_Blackwater_Sound lbsf1 2026-07-27 05:00 EDT 37.8
Little_Madeira_Bay lmdf1 2026-07-27 05:00 EDT 33.7
Little_Rabbit_Key lrkf1 2026-07-27 05:00 EDT 46.3
Long_Sound lsnf1 2026-07-27 05:00 EDT 35.3
Middle_Key mdkf1 2026-07-27 05:00 EDT 33.8
Murray_Key mukf1 2026-07-27 05:00 EDT 41.6
Peterson_Key pkyf1 2026-07-27 05:00 EDT 41.2
Terrapin_Bay tbyf1 2026-07-27 05:00 EDT 42.2
Thursday_Point thrf1 2026-07-27 05:00 EDT 36.6
Whipray_Basin wrbf1 2026-07-27 05:00 EDT 45.4
Code
salinity_palette <- colorNumeric(
  palette = c("#2166ac", "#67a9cf", "#d1e5f0", "#fddbc7", "#ef8a62", "#b2182b"),
  domain = salinity_data$salinity,
  na.color = "#999999"
)

leaflet(salinity_data) |>
  addProviderTiles("CartoDB.Positron") |>
  setView(
    lng = mean(salinity_data$longitude),
    lat = mean(salinity_data$latitude),
    zoom = 9
  ) |>
  addCircleMarkers(
    lng = ~longitude,
    lat = ~latitude,
    radius = 8,
    stroke = TRUE,
    weight = 1,
    color = "#333333",
    fillColor = ~salinity_palette(salinity),
    fillOpacity = 0.85,
    label = ~sprintf(
      "%s: %.1f PSU\n%s",
      location,
      salinity,
      format_ndbc_time(time)
    )
  ) |>
  addLegend(
    position = "bottomright",
    pal = salinity_palette,
    values = salinity_data$salinity,
    title = "Salinity (PSU)",
    opacity = 0.9
  )

Salinity is shown in practical salinity units (PSU, parts per thousand). Lower values (blue) indicate fresher water; higher values (red) indicate more marine conditions.

Interpolated salinity field

The fields below use DIVAnd (Data-Interpolating Variational Analysis) with bathymetry masking, following the same approach as sfer-mbon-oxygen: ~0.01° horizontal grid resolution (~1 km), a two-scale analysis (smooth 0.08° background + 0.035° local anomalies with epsilon2 = 0.01), and interpolation run separately within each connected sea component so distant buoys do not smooth over local features. Land is masked using Natural Earth 10 m coastlines plus NOAA BlueTopo 4 m bathymetry (8 m tiles fill gaps); cells with missing bathymetry or elevation ≥ −0.2 m are treated as land. Sea cells are extended along water paths from each buoy (not across islands), then short bridges are added only through BlueTopo water cells (elevation < −0.2 m) to connect nearby stations still isolated from the main field.

Four panels show the interpolated field at 06:00, 12:00, 18:00, and 24:00 US Eastern on 2026-07-26. Each panel uses buoy observations closest to that hour.

Code
salinity_fields <- interpolate_ndbc_hourly_fields(
  ndbc_data$snapshots,
  value_col = "salinity"
)

for (result in salinity_fields) {
  cat(
    sprintf(
      "Hour %02d: %d field cells, %d observations\n",
      result$hour,
      if (is.null(result$field)) 0L else nrow(result$field),
      nrow(result$observations)
    )
  )
}
Hour 06: 1194 field cells, 19 observations
Hour 12: 1194 field cells, 19 observations
Hour 18: 1194 field cells, 19 observations
Hour 24: 1194 field cells, 19 observations
Code
if (all(vapply(salinity_fields, function(x) is.null(x$field) || nrow(x$field) == 0, logical(1)))) {
  stop("No interpolated salinity fields were produced.")
}

plot_ndbc_field_panels(
  salinity_fields,
  value_col = "salinity",
  legend_title = "Salinity (PSU)",
  popup_suffix = " PSU",
  palette = c("#2166ac", "#67a9cf", "#d1e5f0", "#fddbc7", "#ef8a62", "#b2182b"),
  label_fn = format_hour_panel_label
)

2026-07-26 06:00 EDT

2026-07-26 12:00 EDT

2026-07-26 18:00 EDT

2026-07-26 24:00 EDT

The colored grids show the DIVAnd-interpolated field at each hour; circle markers show the buoy observations used as input.