Flow Data
Inyo County, California
Abstract
This report documents the annual processing pipeline for LADWP monthly flow and production data, transforming raw ZRXP files into database-ready outputs for regional water management. The workflow consists of three phases: data ingestion and parsing → totals-and-means integration → OVGA export and validation.
Scope: We process two distinct datasets: (1) surface water flow from 16 gaging stations across the Owens Valley, and (2) groundwater production from 100+ wellfields. Flow data tracks runoff, diversions, and managed releases for habitat maintenance. Production data quantifies pumping volumes for LTWA compliance monitoring and OVGA reporting.
Key outputs:
- Updated totals-and-means table with WY2025 appended
- OVGA-formatted CSV uploads for flow and production
- Interactive hydrographs for QA/QC and trend visualization
- Linked-well diagnostics showing pumping coordination patterns
- Production well map with annual pumping symbology
For annual updates: See ANNUAL_UPDATE_GUIDE.md for step-by-step instructions. Update the wy_year variable in the setup chunk and verify new ZRXP file paths.
Phase 1: Data Ingestion and Parsing
STEP 1 Parse ZRXP files and separate flow from production records
WY2025 data arrives as a single ZRXP file (VolumeMonthAF_2024-25.dat) containing both surface water flow and groundwater production records. Our parser:
- Extracts station IDs (staids) from
#TSPATHmetadata
- Reads monthly volume values in acre-feet
- Separates records by staid type:
-
Numeric staids (e.g., 01234) → Surface water gaging stations
- Alpha staids (e.g., F033, W426) → Production wells
-
Numeric staids (e.g., 01234) → Surface water gaging stations
Why separate? Flow and production data have different metadata requirements, validation rules, and export formats. Flow records require location descriptions for stream gages; production records require well coordinates and wellfield associations.
read data
file_path_flow <-here('data','hydro','2024-25 Water Year Transfer Packet for ICWD','VolumeMonthAF_2024-25.dat')
sum_stations_path <- here('data','hydro','2024-25 Water Year Transfer Packet for ICWD','2025 - Inyo County Summary Stations.xlsx')
sum_stations <- if (file.exists(sum_stations_path)) {
readxl::read_excel(sum_stations_path, sheet = "Data", skip = 2) %>%
rename(date = 1) %>%
mutate(date = as.Date(date))
} else {
tibble(date = as.Date(character()))
}
staidlist <- read_csv(here('data','hydro','staid_current.csv')) %>%
mutate(staid = as.character(staid))
ovga_points <- read_csv(here("data","hydro","Owens_Monitoring_Points.csv"))
production.meta <- read_csv(here('data','hydro','Production.csv')) %>%
select(mon_pt_name, method) %>% unique()
monthlies <- read_csv(here('data','hydro','totals_means_thru_sep_2024.csv'))
flow.meta <- read_csv(here('data','hydro','Flows.csv')) %>%
select(mon_pt_name, LADWP_ID) %>%
unique() %>%
mutate(staid = as.character(LADWP_ID)) %>%
select(-LADWP_ID)parse .dat ascii
parse_volume_dat <- function(file_path) {
dat_content <- readLines(file_path)
data_list <- list()
is_data_line <- function(line) {
grepl("\\d+ \\d+\\.\\d+", line)
}
staid <- NA_character_
for (line in dat_content) {
if (startsWith(line, "#TSPATH")) {
staid <- sub(".*/([^/]+)/VOLUME/.*", "\\1", line)
}
if (!startsWith(line, "#") && is_data_line(line) && !is.na(staid)) {
data_list <- c(data_list, list(data.frame(staid, dateread = line)))
}
}
dplyr::bind_rows(data_list)
}
ann.update <- parse_volume_dat(file_path_flow)last year raw flow
file_path_flow_prev <- here("data", "hydro", "2023-24 Water Year Transfer Packet for ICWD", "VolumeMonthAF_2023-24.dat")
ann.update_prev <- if (file.exists(file_path_flow_prev)) {
parse_volume_dat(file_path_flow_prev)
} else {
tibble(staid = character(), dateread = character())
}
ann.update_prev2 <- if (nrow(ann.update_prev) > 0) {
prev2 <- ann.update_prev %>% separate(dateread, c("date", "read"), sep = " ")
prev2$read <- as.numeric(prev2$read)
prev2
} else {
tibble(staid = character(), date = character(), read = numeric())
}Raw input coverage
The table below summarizes the raw input coverage for the current and prior water years. Use this to confirm record counts and date ranges before merging with historical totals.
raw input coverage
raw_flow_coverage <- tibble(
File = c(basename(file_path_flow), basename(file_path_flow_prev)),
Records = c(nrow(ann.update2), nrow(ann.update_prev2)),
Unique_staids = c(
dplyr::n_distinct(ann.update2$staid),
dplyr::n_distinct(ann.update_prev2$staid)
),
Date_start = c(
min(ymd_hms(ann.update2$date), na.rm = TRUE),
if (nrow(ann.update_prev2) > 0) min(ymd_hms(ann.update_prev2$date), na.rm = TRUE) else as.POSIXct(NA)
),
Date_end = c(
max(ymd_hms(ann.update2$date), na.rm = TRUE),
if (nrow(ann.update_prev2) > 0) max(ymd_hms(ann.update_prev2$date), na.rm = TRUE) else as.POSIXct(NA)
)
)
knitr::kable(raw_flow_coverage)| File | Records | Unique_staids | Date_start | Date_end |
|---|---|---|---|---|
| VolumeMonthAF_2024-25.dat | 8853 | 744 | 2024-10-01 | 2025-09-01 |
| VolumeMonthAF_2023-24.dat | 8796 | 744 | 2023-10-01 | 2024-09-01 |
date columns
ann.update2$datetime <- ymd_hms(ann.update2$date)
ann.update3<-ann.update2 %>%
mutate(year = year(datetime),
month = month(datetime),
month_abbr = month(datetime, label = TRUE, abbr = TRUE),
day = mday(datetime),
date.y.m.d = make_date(year, month,day))
ann.update4<-ann.update3 %>%
select(-date) %>%
rename(date = date.y.m.d) %>%
left_join(staidlist, by = 'staid') %>%
select(staid,description,read,date,year,month,month_abbr)
ann.update5 <- ann.update4 %>%
mutate(wy = case_when(month >= 10 ~ year + 1,
month <= 9 ~ year),
roy = case_when(month <= 3 ~ year - 1,
month >= 4 ~ year),
irrag = case_when(month %in% c(4,5,6,7,8,9) ~ year))summary stations
if (nrow(sum_stations) > 0) {
sstations <- sum_stations %>%
gather(staid,read,-date) %>%
left_join(staidlist, by = 'staid') %>%
select(-desc_id) %>%
mutate(date = as.Date(date),
datetime = as.Date(date),
month = month(datetime),
year = year(datetime),
month_abbr = month(datetime, label = TRUE, abbr = TRUE))
} else {
sstations <- ann.update4 %>% slice(0)
}Phase 2: Totals-and-Means Integration
STEP 2 Merge new data with historical totals and compute water-year aggregates
This phase integrates WY2025 data with the historical totals-and-means table, which spans back to the 1970s for some stations. We:
- Add water year (WY), runoff year (RY), and irrigation season (April–September) date columns
- Bind WY2025 records to the master totals table
- Verify no duplicate month-staid combinations
- Export refreshed totals-and-means table for ICWD archives
The totals-and-means table is the authoritative dataset for long-term trend analysis, water-year comparisons, and LTWA compliance reporting.
ICWD flow and production exports
OVGA exports and QA/QC
OVGA flow export
Flow exports are derived from the monthly flow records after joining flow metadata and filtering to valid OVGA monitoring points. The output is formatted to the OVGA surface-water import template. The OVGA template reuses the “WellName” column for both wells and gages; in the flow export that column holds gage IDs (e.g. SW0373), not well names.
split flow vs production
normalize_flow_staid <- function(x) {
ifelse(str_detect(x, "^0"), str_replace(x, "^0+", ""), x)
}
flowdf <- tm %>%
filter(!str_detect(staid, "[[:alpha:]]")) %>%
mutate(staid = normalize_flow_staid(staid))
flow_ovga <- flowdf %>%
left_join(flow.meta, by = 'staid') %>%
filter(!is.na(mon_pt_name), mon_pt_name != "")
proddf <- tm %>% filter(str_detect(staid, "[[:alpha:]]"))ovga flow columns
sw.flow <- flow_ovga %>%
mutate(WellName = mon_pt_name,
FlowDate = date,
FlowRateAcFtPM = read,
FlowRateCFS = "",
FlowRateGPM = "") %>%
select(WellName,FlowDate,FlowRateAcFtPM, FlowRateCFS, FlowRateGPM)
sw.flow %>%
datatable(
options = list(pageLength = 10, lengthChange = TRUE, scrollX = TRUE),
class = "compact stripe"
)OVGA flow import metrics
flow_import_metrics <- tibble(
Metric = c(
"Flow records in upload",
"Unique monitoring points",
"Date range"
),
Value = c(
nrow(swflow2),
dplyr::n_distinct(swflow2$WellName),
paste0(min(swflow2$FlowDate, na.rm = TRUE), " to ", max(swflow2$FlowDate, na.rm = TRUE))
)
) %>% mutate(Value = format(Value, big.mark = ","))
knitr::kable(flow_import_metrics)| Metric | Value |
|---|---|
| Flow records in upload | 6611 |
| Unique monitoring points | 555 |
| Date range | 2024-10-01 to 2025-09-01 |
Flow export comparison (last year vs current)
This comparison highlights which flow gages were added or removed from the OVGA export between WY2024 and WY2025.
code
flow_export_last_path <- here("output", "ovga_swflow_import_wy24.csv")
flow_export_last <- if (file.exists(flow_export_last_path)) {
read_csv(flow_export_last_path, show_col_types = FALSE) %>% distinct(WellName)
} else {
tibble(WellName = character())
}
flow_export_current <- swflow2 %>% distinct(WellName)
flow_export_missing <- flow_export_last %>% anti_join(flow_export_current, by = "WellName")
flow_export_new <- flow_export_current %>% anti_join(flow_export_last, by = "WellName")
DT::datatable(
flow_export_missing,
options = list(pageLength = 10, lengthChange = TRUE),
caption = "Flow gages in WY2024 export but missing in WY2025 export"
)code
OVGA production export
STEP 3B Format and validate groundwater production exports
Production data represents monthly pumping volumes from 100+ wellfields. We transform these records to OVGA format by:
- Joining production metadata (method, coordinates, wellfield associations)
- Filtering to OVGA-approved monitoring points
- Mapping to OVGA column schema (WellName, StartDate, EndDate, MIUsageAF, Method)
- Defaulting missing method values to “Unknown”
- Checking for duplicate month-well combinations
ovga production columns
prod_ovga <- proddf %>%
left_join(production.meta, by = c('staid' = 'mon_pt_name')) %>%
semi_join(ovga_points, by = c("staid" = "mon_pt_name"))
tm.well.flow <- prod_ovga %>%
mutate(WellName = staid,
StartDate = date,
EndDate = ceiling_date(date,'month'),
AgUsageAF = '',
MIUsageAF = read,
TotalUsageAF = read,
Method = if_else(is.na(method) | method == "", "Unknown", method),
Notes = '') %>%
select(WellName,StartDate,EndDate, AgUsageAF, MIUsageAF, TotalUsageAF, Method, Notes)tm.well.flow %>%
datatable(
caption = "WY2025 groundwater production export — OVGA upload format preview",
options = list(
pageLength = 10,
lengthChange = TRUE,
scrollX = TRUE,
autoWidth = TRUE,
columnDefs = list(list(width = '90px', targets = "_all"))
),
class = "compact stripe hover",
filter = 'top'
)find/replace duplicates
No duplicates found.
write ovga production
out_ovga <- here("output", "2025wy", "OVGA")
dir.create(out_ovga, recursive = TRUE, showWarnings = FALSE)
tm.well.flow %>% write_csv(file.path(out_ovga, paste0("ovga_production_import_wy", wy_range, ".csv")))
writexl::write_xlsx(tm.well.flow, file.path(out_ovga, paste0("ovga_production_import_wy", wy_range, ".xlsx")))OVGA production import metrics
production_import_metrics <- tibble(
Metric = c(
"Production records in upload",
"Unique production wells",
"Date range"
),
Value = c(
nrow(tm.well.flow),
dplyr::n_distinct(tm.well.flow$WellName),
paste0(min(tm.well.flow$StartDate, na.rm = TRUE), " to ", max(tm.well.flow$EndDate, na.rm = TRUE))
)
) %>% mutate(Value = format(Value, big.mark = ","))
knitr::kable(production_import_metrics)| Metric | Value |
|---|---|
| Production records in upload | 1956 |
| Unique production wells | 162 |
| Date range | 2024-10-01 to 2025-10-01 |
Production export comparison (last year vs current)
This comparison highlights which production wells were added or removed from the OVGA export between WY2024 and WY2025.
code
production_export_last_path <- here("output", "ovga_production_import_wy23-24.csv")
production_export_last <- if (file.exists(production_export_last_path)) {
read_csv(production_export_last_path, show_col_types = FALSE) %>% distinct(WellName)
} else {
tibble(WellName = character())
}
production_export_current <- tm.well.flow %>% distinct(WellName)
production_export_missing <- production_export_last %>% anti_join(production_export_current, by = "WellName")
production_export_new <- production_export_current %>% anti_join(production_export_last, by = "WellName")
DT::datatable(
production_export_missing,
options = list(pageLength = 10, lengthChange = TRUE),
caption = "Production wells in WY2024 export but missing in WY2025 export"
)code
Raw data exclusions (last year vs current)
These tables show staids present in the raw input data but excluded from the export, which helps isolate metadata gaps or OVGA list mismatches.
code
raw_flow_current <- ann.update2 %>%
filter(!str_detect(staid, "[[:alpha:]]")) %>%
mutate(staid = normalize_flow_staid(staid))
raw_flow_prev <- ann.update_prev2 %>%
filter(!str_detect(staid, "[[:alpha:]]")) %>%
mutate(staid = normalize_flow_staid(staid))
raw_prod_current <- ann.update2 %>%
filter(str_detect(staid, "[[:alpha:]]"))
raw_prod_prev <- ann.update_prev2 %>%
filter(str_detect(staid, "[[:alpha:]]"))
raw_flow_current_joined <- raw_flow_current %>%
left_join(flow.meta, by = "staid")
raw_flow_prev_joined <- raw_flow_prev %>%
left_join(flow.meta, by = "staid")
raw_flow_excluded_current <- raw_flow_current_joined %>%
filter(is.na(mon_pt_name) | !(mon_pt_name %in% flow_export_current$WellName)) %>%
distinct(staid, mon_pt_name) %>%
arrange(staid)
raw_flow_excluded_prev <- raw_flow_prev_joined %>%
filter(is.na(mon_pt_name) | !(mon_pt_name %in% flow_export_last$WellName)) %>%
distinct(staid, mon_pt_name) %>%
arrange(staid)
raw_prod_excluded_current <- raw_prod_current %>%
distinct(staid) %>%
filter(!staid %in% production_export_current$WellName) %>%
arrange(staid)
raw_prod_excluded_prev <- raw_prod_prev %>%
distinct(staid) %>%
filter(!staid %in% production_export_last$WellName) %>%
arrange(staid)
DT::datatable(
raw_flow_excluded_prev,
options = list(pageLength = 10, lengthChange = TRUE, scrollX = TRUE),
caption = "Flow staids in WY2024 raw data excluded from WY2024 export"
)code
code
code
Removed staids not on the basin list are excluded from OVGA exports and reflected in the raw‑data exclusion tables above.
OVGA flow and production exclusions (download)
flow/production staids excluded from OVGA exports
flow_exclusions <- flowdf %>%
left_join(flow.meta, by = "staid") %>%
filter(is.na(mon_pt_name) | mon_pt_name == "") %>%
group_by(staid) %>%
summarise(
records = n(),
first_date = min(date, na.rm = TRUE),
last_date = max(date, na.rm = TRUE),
mon_pt_name = dplyr::first(mon_pt_name),
.groups = "drop"
) %>%
left_join(staidlist, by = "staid") %>%
mutate(
dataset = "flow",
reason = "Missing flow metadata mapping (Flows.csv)"
) %>%
select(
dataset,
staid,
description,
records,
first_date,
last_date,
mon_pt_name,
reason
) %>%
arrange(staid)
prod_exclusions <- proddf %>%
anti_join(ovga_points, by = c("staid" = "mon_pt_name")) %>%
left_join(production.meta, by = c("staid" = "mon_pt_name")) %>%
group_by(staid) %>%
summarise(
records = n(),
first_date = min(date, na.rm = TRUE),
last_date = max(date, na.rm = TRUE),
method = dplyr::first(method),
.groups = "drop"
) %>%
left_join(staidlist, by = "staid") %>%
mutate(
dataset = "production",
reason = "Not in OVGA monitoring points list"
) %>%
select(
dataset,
staid,
description,
records,
first_date,
last_date,
method,
reason
) %>%
arrange(staid)
out_ovga <- here("output", "2025wy", "OVGA")
dir.create(out_ovga, recursive = TRUE, showWarnings = FALSE)
flow_exclusions %>%
write_csv(file.path(out_ovga, paste0("ovga_flow_exclusions_wy", wy_year, ".csv")))
writexl::write_xlsx(flow_exclusions, file.path(out_ovga, paste0("ovga_flow_exclusions_wy", wy_year, ".xlsx")))
prod_exclusions %>%
write_csv(file.path(out_ovga, paste0("ovga_production_exclusions_wy", wy_year, ".csv")))
writexl::write_xlsx(prod_exclusions, file.path(out_ovga, paste0("ovga_production_exclusions_wy", wy_year, ".xlsx")))
dtw_exclusions_path <- file.path(out_ovga, "ovga_dtw_exclusions_wy2025.csv")
if (file.exists(dtw_exclusions_path)) {
dtw_exclusions <- read_csv(dtw_exclusions_path, show_col_types = FALSE)
ovga_exclusions <- bind_rows(dtw_exclusions, flow_exclusions, prod_exclusions)
ovga_exclusions %>%
write_csv(file.path(out_ovga, paste0("ovga_exclusions_wy", wy_year, ".csv")))
writexl::write_xlsx(ovga_exclusions, file.path(out_ovga, paste0("ovga_exclusions_wy", wy_year, ".xlsx")))
}
DT::datatable(
flow_exclusions,
options = list(pageLength = 10, lengthChange = TRUE, scrollX = TRUE),
class = "compact stripe",
caption = "Flow staids excluded from OVGA export (WY2025)"
)flow/production staids excluded from OVGA exports
Results: QA/QC and plots
This section provides QA/QC plots and summaries for flow, production, and linked well pumping. It includes time-series plots, seasonal totals, and percentile comparisons to support review before export.
Totals and Means - summary stations requested
Summary-station series (OVR, OVPW, etc.) are populated from the summary stations file (2025 - Inyo County Summary Stations.xlsx). These plots now include data through WY2025.
OVR Owens Valley Runoff
OVPW Owens Valley Pumped Water
FTC L.A.A. Total Flow to the City
LOLU Lower Owens River Project – Lakes and Ponds Use
LOOU Lower Owens River Project – Operations
LORPDU Lower Owens River – Delta Uses
LORPRU Lower Owens River – River Uses
LORPTU Lower Owens River Project Total Uses
LOWU Lower Owens River Project – Waterfowl Uses
MBR Mono Basin Runoff
MTWP Mono Tunnel at West Portal
OLTU Owens Lake Total Uses
OVFG Owens Valley Flowing Groundwater
OVGR Owens Valley Groundwater Recharge
OVIR Owens Valley Irrigation
SHTO South Haiwee Total Outflow (1+2+3+BP)
Stream flow
Top 10 stream gages by WY2025 volume (water year Oct-Sep)
code
flow_wy <- flowdf %>%
mutate(wy = if_else(month >= 10, year + 1, year))
top_streams <- flow_wy %>%
filter(wy == wy_year) %>%
group_by(staid) %>%
summarise(total_af = sum(read, na.rm = TRUE), .groups = "drop") %>%
arrange(desc(total_af)) %>%
slice_head(n = 10) %>%
left_join(flow.meta, by = "staid")
top_streams <- top_streams %>%
mutate(display_name = if_else(is.na(mon_pt_name) | mon_pt_name == "", staid, mon_pt_name))
plots <- purrr::map2(top_streams$staid, top_streams$display_name, ~{
plot_title <- paste0(.y, " (", .x, ")")
htmltools::tagList(
htmltools::tags$h4(plot_title),
monthly_flow_syncplot_staid(staid_p = .x, data = totals_means, group_name = "a", title = plot_title)
)
})
htmltools::tagList(plots)SW0050 (50)
SW0077 (77)
SW2066 (2066)
SW0049 (49)
413 (413)
SW3289 (3289)
SW0118 (118)
SW0116 (116)
SW3324 (3324)
SW0088 (88)
Groundwater Production Analysis
This section visualizes groundwater pumping patterns across wellfields and individual production wells. Runoff year (Apr–Mar) totals are used for pumping statistics to align with irrigation season and LTWA reporting conventions, while water year (Oct–Sep) is used for surface water flow series above.
Partial data note: RY2025 totals shown below are incomplete because Oct–Mar 2025–26 data have not yet been delivered. Final annual totals will be available after the spring 2026 LADWP data transfer.
Interactive Production Wells Map
Spatial distribution of WY2025 groundwater pumping — Point size represents total annual volume. Color indicates well type (green = F/V wells, blue = W wells). Red borders show wells linked to monitoring sites.
production well map by annual pumping
# Define linked wells to monitoring sites (needed for map)
linked_wells_sites_map <- data.frame(
Site = rep(
c(
"BP1", "BP2", "BP3", "BP4",
"LW1", "LW2", "LW3",
"TA3", "TA4", "TA5", "TA6",
"TS1", "TS2", "TS3", "TS4",
"IO1", "IO2",
"SS1", "SS2", "SS3", "SS4",
"BG2"
),
times = c(4, 4, 4, 1, 4, 4, 5, 4, 2, 1, 2, 1, 1, 3, 2, 3, 1, 3, 3, 2, 2, 4)
),
Linked_Well = c(
"W210", "W378", "W379", "W389", # BP1
"W220", "W229", "W374", "W375", # BP2
"W222", "W223", "W231", "W232", # BP3
"W331", # BP4
"W247", "W248", "W249", "W398", # LW1
"W236", "W239", "W243", "W244", # LW2
"W240", "W241", "W399", "W376", "W377", # LW3
"W106", "W110", "W111", "W114", # TA3
"W342", "W347", # TA4
"W349", # TA5
"W109", "W370", # TA6
"W159", # TS1
"W155", # TS2
"W103", "W104", "W382", # TS3
"W380", "W381", # TS4
"W061", "W391", "W400", # IO1
"W063", # IO2
"W069", "W392", "W393", # SS1
"W074", "W394", "W395", # SS2
"W092", "W396", # SS3
"W075", "W345", # SS4
"W076", "W403", "W343", "W348" # BG2
)
)
# Calculate WY2025 total pumping by well
wy2025_pumping <- proddf %>%
filter(year == 2024 & month >= 10 | year == 2025 & month <= 9) %>%
mutate(read = if_else(read == -777, 0, read)) %>%
group_by(staid) %>%
summarise(
total_pumping_af = sum(read, na.rm = TRUE),
n_months = n(),
.groups = "drop"
) %>%
filter(total_pumping_af > 0)
# Join with monitoring points for locations and linked well sites
pumping_map_data <- wy2025_pumping %>%
left_join(ovga_points, by = c("staid" = "mon_pt_name")) %>%
left_join(linked_wells_sites_map, by = c("staid" = "Linked_Well")) %>%
filter(!is.na(mon_pt_lat), !is.na(mon_pt_long),
mon_pt_lat > -999, mon_pt_long > -999,
mon_pt_lat < 90, mon_pt_long > -180) %>%
mutate(
well_type = substr(staid, 1, 1),
color = case_when(
well_type %in% c("F", "V") ~ "#2ecc71", # Green for F and V wells
well_type == "W" ~ "#2E86AB", # Blue for W wells
TRUE ~ "#95a5a6" # Gray for others
),
is_linked = !is.na(Site),
border_color = if_else(is_linked, "#e74c3c", color), # Red border for linked wells
border_weight = if_else(is_linked, 2, 1),
site_text = if_else(is_linked, paste0("<br>Linked to: ", Site), "")
)
# Create leaflet map with circle markers sized by pumping volume
if (nrow(pumping_map_data) > 0) {
# Scale radius: sqrt transformation for better visual distribution
max_pumping <- max(pumping_map_data$total_pumping_af, na.rm = TRUE)
pumping_map_data <- pumping_map_data %>%
mutate(
radius = sqrt(total_pumping_af / max_pumping) * 20 + 5,
popup_text = paste0(
"<b>", staid, "</b><br>",
"WY2025 Pumping: ", format(round(total_pumping_af, 0), big.mark = ","), " AF<br>",
"Months reported: ", n_months,
site_text
)
)
leaflet(pumping_map_data) %>%
addProviderTiles(providers$CartoDB.Positron) %>%
addCircleMarkers(
lng = ~mon_pt_long,
lat = ~mon_pt_lat,
radius = ~radius,
color = ~border_color,
fillColor = ~color,
fillOpacity = 0.6,
stroke = TRUE,
weight = ~border_weight,
popup = ~popup_text,
label = ~staid
) %>%
addLegend(
position = "bottomright",
colors = c("#2ecc71", "#2E86AB", "#e74c3c"),
labels = c("F/V wells (flow/volume)", "W wells (production)", "Linked wells (red border)"),
title = "Well Type",
opacity = 0.8
)
} else {
cat("No production wells with location data available for mapping.")
}Annual pumping by wellfield
code
tozoo <- function(x) zoo(x$total_pumping, x$roy2)
dyMultiColumn <- function(dygraph) {
dyPlotter(dygraph = dygraph,
name = "MultiColumn",
path = system.file("plotters/multicolumn.js",
package = "dygraphs"))
}
plot_wellfield_total <- function(data, field_sites, linked_wells_sites, title, value_range = NULL) {
linked_wells <- linked_wells_sites %>%
filter(Site %in% field_sites) %>%
pull(Linked_Well) %>%
unique()
if (length(linked_wells) == 0) {
return(NULL)
}
df <- data %>%
filter(staid %in% linked_wells) %>%
mutate(read = if_else(read == -777, 0, read)) %>%
group_by(roy, staid) %>%
summarise(total_pumping = sum(read, na.rm = TRUE), .groups = "drop") %>%
mutate(roy2 = make_date(roy, 4, 1))
df <- pad_series_bounds(df, "roy2", "staid", months_start = 6, months_end = 18)
year_totals <- df %>%
group_by(roy2) %>%
summarise(total_pumping = sum(total_pumping, na.rm = TRUE), .groups = "drop")
y_max <- max(year_totals$total_pumping, na.rm = TRUE)
plot_data <- do.call(merge, lapply(split(df, df$staid), tozoo))
plot <- plot_data %>%
dygraph(main = title, group = "a") %>%
dyStackedBarGroup(linked_wells) %>%
dyBarChart() %>%
dyAxis("y", label = "Total Annual Pumping (AF)", axisLabelWidth = 70)
if (is.finite(y_max)) {
plot <- plot %>% dyAxis("y", valueRange = c(0, y_max * 1.05))
}
if (!is.null(value_range)) {
plot <- plot %>% dyAxis("y", valueRange = value_range)
}
plot
}
compute_linked_seasonal <- function(data, linked_wells_sites, group_map, group_col, target_year = 2025) {
base <- data %>%
filter(staid %in% linked_wells_sites$Linked_Well) %>%
mutate(
read = if_else(read < 0, 0, read),
date = as.Date(date),
year = year(date),
month = month(date),
wy = if_else(month >= 10, year + 1, year),
runoff_year = if_else(month >= 4, year, year - 1),
season = case_when(
month %in% 4:9 ~ "Apr-Sep",
month %in% c(10, 11, 12, 1, 2, 3) ~ "Oct-Mar",
TRUE ~ NA_character_
)
) %>%
left_join(group_map, by = c("staid" = "Linked_Well")) %>%
filter(!is.na(.data[[group_col]]))
seasonal_wy <- base %>%
group_by(.data[[group_col]], wy, season) %>%
summarise(total_pumping = sum(read, na.rm = TRUE), .groups = "drop") %>%
pivot_wider(names_from = season, values_from = total_pumping) %>%
rename(Group = !!sym(group_col), WY = wy) %>%
arrange(Group, WY)
runoff_ry <- base %>%
group_by(.data[[group_col]], runoff_year) %>%
summarise(`Apr-Mar` = sum(read, na.rm = TRUE), .groups = "drop") %>%
rename(Group = !!sym(group_col), RY = runoff_year) %>%
arrange(Group, RY)
percentiles_wy <- seasonal_wy %>%
group_by(Group) %>%
mutate(
`Apr-Sep pct` = if_else(
is.na(`Apr-Sep`),
NA_real_,
round(dplyr::percent_rank(`Apr-Sep`) * 100, 1)
),
`Oct-Mar pct` = if_else(
is.na(`Oct-Mar`),
NA_real_,
round(dplyr::percent_rank(`Oct-Mar`) * 100, 1)
)
) %>%
ungroup() %>%
filter(WY == target_year) %>%
select(Group, WY, `Apr-Sep`, `Apr-Sep pct`, `Oct-Mar`, `Oct-Mar pct`)
percentiles_ry <- runoff_ry %>%
group_by(Group) %>%
mutate(
`Apr-Mar pct` = if_else(
is.na(`Apr-Mar`),
NA_real_,
round(dplyr::percent_rank(`Apr-Mar`) * 100, 1)
)
) %>%
ungroup() %>%
filter(RY == target_year) %>%
select(Group, RY, `Apr-Mar`, `Apr-Mar pct`)
list(
seasonal_wy = seasonal_wy,
runoff_ry = runoff_ry,
percentiles_wy = percentiles_wy,
percentiles_ry = percentiles_ry
)
}Linked Wells QA/QC
Linked-well pumping plots from the on/off management program support visual QA/QC of annual totals.
code
tm2 <- totals_means
linked_wells_sites <- data.frame(
Site = rep(
c(
"BP1", "BP2", "BP3", "BP4",
"LW1", "LW2", "LW3",
"TA3", "TA4", "TA5", "TA6",
"TS1", "TS2", "TS3", "TS4",
"IO1", "IO2",
"SS1", "SS2", "SS3", "SS4",
"BG2"
),
times = c(4, 4, 4, 1, 4, 4, 5, 4, 2, 1, 2, 1, 1, 3, 2, 3, 1, 3, 3, 2, 2, 4)
),
Linked_Well = c(
"W210", "W378", "W379", "W389", # BP1
"W220", "W229", "W374", "W375", # BP2
"W222", "W223", "W231", "W232", # BP3
"W331", # BP4
"W247", "W248", "W249", "W398", # LW1
"W236", "W239", "W243", "W244", # LW2
"W240", "W241", "W399", "W376", "W377", # LW3
"W106", "W110", "W111", "W114", # TA3
"W342", "W347", # TA4
"W349", # TA5
"W109", "W370", # TA6
"W159", # TS1
"W155", # TS2
"W103", "W104", "W382", # TS3
"W380", "W381", # TS4
"W061", "W391", "W400", # IO1
"W063", # IO2
"W069", "W392", "W393", # SS1
"W074", "W394", "W395", # SS2
"W092", "W396", # SS3
"W075", "W345", # SS4
"W076", "W403", "W343", "W348" # BG2
)
)
wellfield_sites <- tibble(
Wellfield = c(
"Laws",
"Big Pine",
"Taboose/Aberdeen",
"Thibaut/Sawmill",
"Independence/Oak",
"Symmes/Shepherd",
"Bairs/Georges"
),
Site = I(list(
c("LW1", "LW2", "LW3"),
c("BP1", "BP2", "BP3", "BP4"),
c("TA3", "TA4", "TA5", "TA6"),
c("TS1", "TS2", "TS3", "TS4"),
c("IO1", "IO2"),
c("SS1", "SS2", "SS3", "SS4"),
c("BG2")
))
) %>%
unnest(Site)
site_map <- linked_wells_sites %>% select(Linked_Well, Site)
wellfield_map <- linked_wells_sites %>%
left_join(wellfield_sites, by = "Site") %>%
select(Linked_Well, Wellfield)
seasonal_by_site <- compute_linked_seasonal(tm2, linked_wells_sites, site_map, "Site", target_year = wy_year)
seasonal_by_wellfield <- compute_linked_seasonal(tm2, linked_wells_sites, wellfield_map, "Wellfield", target_year = wy_year)Seasonal totals use water year (WY, Oct–Sep end year) for Apr–Sep and Oct–Mar. Runoff totals use runoff year (RY, Apr–Mar start year). Example: RY2025 = Apr 2025–Mar 2026.
Seasonal pumping totals by site (water year Oct-Sep)
Runoff year totals by site (Apr-Mar)
WY2025 percentiles by site (water year Oct-Sep)
Percentiles are calculated within each site using the historical distribution of Apr–Sep totals (and separately Oct–Mar totals). WY2025 percentiles therefore rank the 2025 six‑month total against all prior years for that site.
RY2025 percentiles by site (runoff year Apr-Mar)
Seasonal pumping totals by wellfield (water year Oct-Sep)
Runoff year totals by wellfield (Apr-Mar)
WY2025 percentiles by wellfield (water year Oct-Sep)
Percentiles are calculated within each wellfield using the historical distribution of Apr–Sep totals (and separately Oct–Mar totals). WY2025 percentiles therefore rank the 2025 six‑month total against all prior years for that wellfield.
RY2025 percentiles by wellfield (runoff year Apr-Mar)
Laws Monitoring Site #1 (LW1)
Laws Monitoring Site #2 (LW2)
Laws Monitoring Site #3 (LW3)
Laws Wellfield Total Pumping
Big Pine Monitoring Site #1 (BP1)
Big Pine Monitoring Site #2 (BP2)
Big Pine Monitoring Site #3 (BP3)
Big Pine Monitoring Site #4 (BP4)
Exempt Wells - Fish Hatchery and 218, 219
code
linked_wells <- c("W218","W219","W330","W332")
DF <- tm2 %>%
filter(staid %in% linked_wells) %>%
mutate(read = if_else(read == -777, 0, read)) %>%
group_by(roy, staid) %>%
summarise(total_pumping = sum(read, na.rm = TRUE), .groups = "drop") %>%
mutate(roy2 = make_date(roy, 4, 1))
DF <- pad_series_bounds(DF, "roy2", "staid", months_start = 6, months_end = 18)
Data <- do.call(merge, lapply(split(DF, DF$staid), tozoo))
year_totals_exempt <- DF %>%
group_by(roy2) %>%
summarise(total_pumping = sum(total_pumping, na.rm = TRUE), .groups = "drop")
y_max_exempt <- max(year_totals_exempt$total_pumping, na.rm = TRUE)
plot_exempt <- Data %>% dygraph(group = "a") %>%
dyStackedBarGroup(c("W218","W219","W330","W332")) %>%
dyBarChart() %>%
dyAxis("y", label = "Total Annual Pumping (AF)", axisLabelWidth = 70)
if (is.finite(y_max_exempt)) {
plot_exempt <- plot_exempt %>% dyAxis("y", valueRange = c(0, y_max_exempt * 1.05))
}
plot_exemptBig Pine Pumping Totals
code
linked_wells <- c("W210","W378","W379","W389","W220","W229","W374","W375","W222","W223","W231","W232","W331","W218","W219","W330","W332")
DF <- tm2 %>%
filter(staid %in% linked_wells) %>%
mutate(read = if_else(read == -777, 0, read)) %>%
group_by(roy, staid) %>%
summarise(total_pumping = sum(read, na.rm = TRUE), .groups = "drop") %>%
mutate(roy2 = make_date(roy, 4, 1))
DF <- pad_series_bounds(DF, "roy2", "staid", months_start = 6, months_end = 18)
Data <- do.call(merge, lapply(split(DF, DF$staid), tozoo))
year_totals_bp <- DF %>%
group_by(roy2) %>%
summarise(total_pumping = sum(total_pumping, na.rm = TRUE), .groups = "drop")
y_max_bp <- max(year_totals_bp$total_pumping, na.rm = TRUE)
plot_bp <- Data %>% dygraph(group = "a") %>%
dyStackedBarGroup(c("W210","W378","W379","W389","W220","W229","W374","W375","W222","W223","W231","W232","W331","W218","W219","W330","W332")) %>%
dyBarChart() %>%
dyAxis("y", label = "Total Annual Pumping (AF)", axisLabelWidth = 70)
if (is.finite(y_max_bp)) {
plot_bp <- plot_bp %>% dyAxis("y", valueRange = c(0, y_max_bp * 1.05))
}
plot_bpBig Pine Wellfield Pumping
Taboose/Aberdeen Monitoring Site #3 (TA3)
Taboose/Aberdeen Monitoring Site #4 (TA4)
Taboose/Aberdeen Monitoring Site #5 (TA5)
Taboose/Aberdeen Monitoring Site #6 (TA6)
Taboose/Aberdeen Wellfield Total Pumping
Thibaut/Sawmill Monitoring Site #1 (TS1)
Thibaut/Sawmill Monitoring Site #2 (TS2)
Thibaut/Sawmill Monitoring Site #3 (TS3)
Thibaut/Sawmill Monitoring Site #4 (TS4)
Thibaut/Sawmill Wellfield Total Pumping
Independence/Oak Monitoring Site #1 (IO1)
Independence/Oak Monitoring Site #2 (IO2)
Independence/Oak Wellfield Total Pumping
Symmes/Shepherd Monitoring Site #1 (SS1)
Symmes/Shepherd Monitoring Site #2 (SS2)
Symmes/Shepherd Monitoring Site #3 (SS3)
Symmes/Shepherd Monitoring Site #4 (SS4)
Symmes/Shepherd Wellfield Total Pumping
Bairs/Georges Monitoring Site #2 (BG2)
Bairs/Georges Wellfield Total Pumping
Citation
@report{county_water_department2026,
author = {County Water Department, Inyo},
publisher = {Inyo County Water Department},
title = {Flow {Data}},
date = {2026-01-31},
url = {https://inyo-gov.github.io/hydro-data/},
langid = {en}
}