Lake Mead sits within reach of several EDR services that describe the area in different ways. This workflow combines:
The interactive map uses a Parameter control to switch between population and elevation without rebuilding the widget. USGS and USBR stations remain above either grid, and each marker opens a time-series chart with a CSV download. A faceted plot compares the three water variables on separate y-scales.
The saved data were retrieved on 2026-07-14. Package builds use this
snapshot and do not contact the live services. Met Office Labs is a
technical demonstrator and may change independently of
edr4r.
library(edr4r)
library(ggplot2)
usgs <- edr_client(
"https://api.waterdata.usgs.gov/ogcapi/beta",
timeout = 60,
max_tries = 1
)
wwdh <- edr_client(
"https://api.wwdh.internetofwater.app",
timeout = 60,
max_tries = 1
)
met <- edr_client(
"https://labs.metoffice.gov.uk/edr",
timeout = 60,
max_tries = 1
)
study_bbox <- c(-115.30, 35.75, -114.55, 36.30)
# minx miny maxx maxyThe study area covers Las Vegas, Hoover Dam, and the western part of Lake Mead. Before downloading data, check that each collection still advertises the query used below:
capability_check <- data.frame(
endpoint = c("Met Office Labs", "Met Office Labs", "USGS waterdata", "WWDH"),
collection = c(
"global_pop_density", "copernicus_dem", "daily-edr", "rise-edr"
),
query = c("area", "area", "locations", "locations"),
supported = c(
edr_supports(met, "global_pop_density", query = "area"),
edr_supports(met, "copernicus_dem", query = "area"),
edr_supports(usgs, "daily-edr", query = "locations"),
edr_supports(wwdh, "rise-edr", query = "locations")
)
)
knitr::kable(capability_check)| endpoint | collection | query | supported |
|---|---|---|---|
| Met Office Labs | global_pop_density | area | TRUE |
| Met Office Labs | copernicus_dem | area | TRUE |
| USGS waterdata | daily-edr | locations | TRUE |
| WWDH | rise-edr | locations | TRUE |
The population collection requires an explicit CRS on its
area query. The four-row matrix is closed into a WKT
polygon by edr_area().
population_ring <- rbind(
c(study_bbox[1], study_bbox[2]),
c(study_bbox[3], study_bbox[2]),
c(study_bbox[3], study_bbox[4]),
c(study_bbox[1], study_bbox[4])
)
population_response <- edr_area(
met,
"global_pop_density",
coords = population_ring,
parameter_name = "Pop_Density",
crs = "EPSG:4326"
)
population <- covjson_to_tibble(population_response)
population$coverage_id <- "global_pop_density"
population$parameter_code <- population$parameter
population$parameter <- "2015 population density"
population$unit <- "people/km2"
stopifnot(
nrow(population) > 0L,
all(c("x", "y", "value") %in% names(population)),
any(is.finite(population$value), na.rm = TRUE)
)The population response is a regular 90 by 66 grid with 5,940 cells. The Copernicus DEM is much finer, so the refresh step samples its nearest cell at each population-grid center. The saved widget therefore carries two equally sized facets instead of millions of elevation cells.
elevation_response <- edr_area(
met,
"copernicus_dem",
coords = population_ring,
parameter_name = "Height",
crs = "EPSG:4326"
)
elevation_full <- covjson_to_tibble(elevation_response)
elevation_signature <- data.frame(
source = "Met Office / Copernicus elevation",
container = elevation_response$covjson$type,
domain = elevation_response$covjson$domain$domainType,
axes = paste(names(elevation_response$covjson$domain$axes), collapse = ", "),
coverages = 1L
)
nearest_grid_values <- function(source, target_x, target_y) {
source_x <- sort(unique(source$x))
source_y <- sort(unique(source$y))
ordered <- source[order(source$y, source$x), , drop = FALSE]
stopifnot(nrow(ordered) == length(source_x) * length(source_y))
value_matrix <- matrix(
ordered$value,
nrow = length(source_y),
ncol = length(source_x),
byrow = TRUE
)
target_x_unique <- unique(target_x)
target_y_unique <- unique(target_y)
source_x_index <- vapply(
target_x_unique,
function(value) which.min(abs(source_x - value)),
integer(1)
)
source_y_index <- vapply(
target_y_unique,
function(value) which.min(abs(source_y - value)),
integer(1)
)
value_matrix[cbind(
source_y_index[match(target_y, target_y_unique)],
source_x_index[match(target_x, target_x_unique)]
)]
}
elevation <- population
elevation$coverage_id <- "copernicus_dem"
elevation$parameter_code <- "Height"
elevation$parameter <- "Elevation above mean sea level"
elevation$parameter_label <- "Elevation above mean sea level"
elevation$unit <- "m"
elevation$value <- nearest_grid_values(
elevation_full,
population$x,
population$y
)
grid_facets <- rbind(population, elevation)
rm(elevation_full, elevation_response)
invisible(gc())
stopifnot(
nrow(elevation) == nrow(population),
all(is.finite(elevation$value)),
identical(sort(unique(grid_facets$parameter)), sort(c(
"2015 population density",
"Elevation above mean sea level"
)))
)Because both facets share the same x and y
centers, edr_map() can switch between them with its
built-in Parameter selector while keeping the station layers and map
extent fixed.
Two USGS gauges provide a stage series below Hoover Dam and a
discharge series on Las Vegas Wash. USGS currently ignores
datetime on individual location requests, so the example
asks for 31 records and uses the dates actually returned.
usgs_index <- edr_locations(
usgs,
"daily-edr",
bbox = study_bbox,
limit = 100
)
selected_usgs_ids <- c("USGS-09421500", "USGS-09419800")
usgs_sites <- usgs_index[
match(selected_usgs_ids, usgs_index$id),
c("id", "monitoring_location_name", "geometry")
]
hoover_response <- edr_location(
usgs,
"daily-edr",
location_id = "USGS-09421500",
parameter_name = "00065",
limit = 31
)
hoover_stage <- covjson_to_tibble(hoover_response)
hoover_stage <- hoover_stage[order(hoover_stage$datetime), ]
hoover_stage$parameter_code <- hoover_stage$parameter
hoover_stage$parameter <- "Gage height"
hoover_stage$coverage_id <- paste0("usgs:", hoover_stage$coverage_id)
wash_response <- edr_location(
usgs,
"daily-edr",
location_id = "USGS-09419800",
parameter_name = "00060",
limit = 31
)
wash_flow <- covjson_to_tibble(wash_response)
wash_flow <- wash_flow[order(wash_flow$datetime), ]
wash_flow$parameter_code <- wash_flow$parameter
wash_flow$parameter <- "Discharge"
wash_flow$coverage_id <- paste0("usgs:", wash_flow$coverage_id)
water_dates <- intersect(
as.Date(hoover_stage$datetime, tz = "UTC"),
as.Date(wash_flow$datetime, tz = "UTC")
)
stopifnot(length(water_dates) > 0L)WWDH treats the interval end as exclusive, so adding one day includes
the last USGS date. Location 3514 is the USBR/RISE Lake
Mead storage series.
water_interval <- paste(
min(water_dates),
max(water_dates) + 1,
sep = "/"
)
storage_response <- edr_location(
wwdh,
"rise-edr",
location_id = "3514",
datetime = water_interval,
parameter_name = "3"
)
storage <- covjson_to_tibble(storage_response)
storage <- storage[order(storage$datetime), ]
storage <- storage[
as.Date(storage$datetime, tz = "UTC") %in% water_dates,
]
storage$parameter_code <- storage$parameter
storage$parameter <- "Reservoir storage"
storage$coverage_id <- paste0("wwdh:", storage$coverage_id)
stopifnot(
nrow(hoover_stage) > 0L,
nrow(wash_flow) > 0L,
nrow(storage) > 0L,
all(vapply(
list(hoover_stage, wash_flow, storage),
function(x) all(c("datetime", "value", "unit") %in% names(x)),
logical(1)
))
)The WWDH location index currently ignores spatial/page limits, so the USBR marker is constructed from the coordinates already carried by the returned coverage instead of downloading the full index.
usbr_site <- sf::st_as_sf(
data.frame(
id = "USBR-RISE-3514",
monitoring_location_name =
"Lake Mead Hoover Dam and Powerplant (USBR via WWDH/RISE)",
longitude = storage$x[[1]],
latitude = storage$y[[1]]
),
coords = c("longitude", "latitude"),
crs = 4326
)
usgs_popup_data <- list(
"USGS-09421500" = hoover_stage,
"USGS-09419800" = wash_flow
)
usbr_popup_data <- list(
"USBR-RISE-3514" = storage
)Stage, discharge, and storage have different units and ranges.
edr_plot() uses the parameter metadata to put each variable
in its own panel and include the unit in the strip label. The shared
date axis still makes it easy to see which observations cover the same
period.
series_for_plot <- function(data, station) {
out <- data[, c(
"coverage_id", "parameter", "unit", "datetime", "value"
)]
out$station <- station
out
}
water_series <- rbind(
series_for_plot(hoover_stage, "USGS below Hoover Dam"),
series_for_plot(wash_flow, "USGS Las Vegas Wash"),
series_for_plot(storage, "USBR Lake Mead")
)
water_facets <- edr_plot(
water_series,
group = "station",
facet = "parameter",
scales = "free_y",
geom = "line",
view = "time"
) +
labs(
title = "Water conditions around Lake Mead",
subtitle = "Each parameter keeps its own unit and y-axis",
colour = "Station"
)
water_facetsStage, discharge, and reservoir storage use separate panels and y-axis scales.
The map combines two gridded coverages with three temporal point coverages. The container and domain metadata come directly from the responses:
coverage_signature <- function(source, response) {
body <- response$covjson
coverages <- if (identical(body$type, "CoverageCollection")) {
body$coverages
} else {
list(body)
}
domain_types <- vapply(coverages, function(coverage) {
domain_type <- coverage$domainType
if (is.null(domain_type) || length(domain_type) == 0L) {
domain_type <- coverage$domain$domainType
}
if (is.null(domain_type) || length(domain_type) == 0L) {
"not declared"
} else {
as.character(domain_type[[1]])
}
}, character(1))
axes <- unique(unlist(lapply(
coverages,
function(coverage) names(coverage$domain$axes)
)))
data.frame(
source = source,
container = body$type,
domain = paste(unique(domain_types), collapse = ", "),
axes = paste(axes, collapse = ", "),
coverages = length(coverages)
)
}
signatures <- rbind(
coverage_signature("Met Office population", population_response),
elevation_signature,
coverage_signature("USGS Hoover stage", hoover_response),
coverage_signature("USGS Las Vegas Wash", wash_response),
coverage_signature("WWDH / USBR storage", storage_response)
)
knitr::kable(signatures)| source | container | domain | axes | coverages |
|---|---|---|---|---|
| Met Office population | Coverage | Grid | x, y | 1 |
| Met Office / Copernicus elevation | Coverage | Grid | x, y | 1 |
| USGS Hoover stage | Coverage | PointSeries | t, x, y | 1 |
| USGS Las Vegas Wash | Coverage | PointSeries | t, x, y | 1 |
| WWDH / USBR storage | CoverageCollection | PointSeries | x, y, t | 1 |
snapshot <- data.frame(
layer = c(
"Met Office population",
"Met Office / Copernicus elevation",
"USGS Hoover stage",
"USGS Las Vegas Wash",
"WWDH / USBR storage"
),
rows = c(
nrow(population),
nrow(elevation),
nrow(hoover_stage),
nrow(wash_flow),
nrow(storage)
),
start = c(
"static 2015",
"static elevation",
format(min(hoover_stage$datetime), "%Y-%m-%d", tz = "UTC"),
format(min(wash_flow$datetime), "%Y-%m-%d", tz = "UTC"),
format(min(storage$datetime), "%Y-%m-%d", tz = "UTC")
),
end = c(
"static 2015",
"static elevation",
format(max(hoover_stage$datetime), "%Y-%m-%d", tz = "UTC"),
format(max(wash_flow$datetime), "%Y-%m-%d", tz = "UTC"),
format(max(storage$datetime), "%Y-%m-%d", tz = "UTC")
),
unit = c(
"people/km2",
"m",
unique(hoover_stage$unit)[1],
unique(wash_flow$unit)[1],
unique(storage$unit)[1]
)
)
knitr::kable(snapshot)| layer | rows | start | end | unit |
|---|---|---|---|---|
| Met Office population | 5940 | static 2015 | static 2015 | people/km2 |
| Met Office / Copernicus elevation | 5940 | static elevation | static elevation | m |
| USGS Hoover stage | 31 | 2026-06-13 | 2026-07-13 | ft |
| USGS Las Vegas Wash | 31 | 2026-06-13 | 2026-07-13 | ft3/s |
| WWDH / USBR storage | 30 | 2026-06-13 | 2026-07-12 | af |
edr_map() recognizes the two parameter values and adds a
selector for them. edr_add_stations() then places
independently styled source groups above the active grid. Coverage cells
live in a lower Leaflet pane, so the station markers stay visible and
clickable while the grid facet changes.
lake_mead_map <- edr_map(
grid_facets,
mode = "grid",
controls = TRUE,
initial = list(parameter = "2015 population density"),
grid_opacity = 0.58,
grid_transform = "sqrt",
tile_provider = "CartoDB.Positron"
)
lake_mead_map <- edr_add_stations(
lake_mead_map,
usbr_site,
data = usbr_popup_data,
popup = "plot+csv",
id_col = "id",
label_col = "monitoring_location_name",
group = "USBR / WWDH",
matched_color = "#0072B2",
marker_radius = 9,
legend = FALSE
)
lake_mead_map <- edr_add_stations(
lake_mead_map,
usgs_sites,
data = usgs_popup_data,
popup = "plot+csv",
id_col = "id",
label_col = "monitoring_location_name",
group = "USGS",
matched_color = "#D55E00",
marker_radius = 6,
legend = FALSE
)
lake_mead_map <- leaflet::addLayersControl(
lake_mead_map,
overlayGroups = c("USGS", "USBR / WWDH"),
options = leaflet::layersControlOptions(collapsed = TRUE)
)
lake_mead_map <- leaflet::addControl(
lake_mead_map,
html = paste0(
"<div style='background:rgba(255,255,255,.92);padding:7px;",
"border-radius:4px;font:12px system-ui,sans-serif'>",
"<strong>Lake Mead context</strong><br>",
"Choose a grid facet above<br>Click a station for its time series",
"</div>"
),
position = "bottomleft"
)
dir.create(
"cross-endpoint-water-context-full",
showWarnings = FALSE,
recursive = TRUE
)
edr_save_html(
lake_mead_map,
"cross-endpoint-water-context-full/lake-mead-map.html",
selfcontained = TRUE
)The website build renders the interactive widget below. The package vignette uses a faceted static preview of the same precomputed layers.
Use the Parameter selector to change the grid while leaving the station layers in place. The larger blue USBR/WWDH marker and the smaller orange USGS marker near Hoover Dam overlap at the initial extent; the layer control or a closer zoom makes each one easier to open. The Las Vegas Wash gauge is farther west.
The same widget can be written anywhere with:
The executable source is retained at
vignettes/cross-endpoint-water-context.Rmd.orig. Refresh
the baked data, static preview, and site widget deliberately with: