Inyo County, California

totals and means
LADWP data
OVGA
time series
LADWP water-year monthly volumetric flow data transfer. Code automates extracting volumetric flow data from ZRXP ASCII data exchange format, transforming and merging updates with the active database, and exporting a csv file formatted for the OVGA data management system for groundwater production and surface water gage totals.
Author

Inyo County Water Department

Published

January 31, 2026

Modified

January 31, 2026

code
source(here('code','R','functions.R'))

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 parsingtotals-and-means integrationOVGA 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
Note

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:

  1. Extracts station IDs (staids) from #TSPATH metadata
  2. Reads monthly volume values in acre-feet
  3. Separates records by staid type:
    • Numeric staids (e.g., 01234) → Surface water gaging stations
    • Alpha staids (e.g., F033, W426) → Production wells

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)
separate date read column
#separate the data column and assign numeric class to value
ann.update2 <- ann.update %>% separate(dateread, c("date", "read"),sep = " ") 
ann.update2$read <- as.numeric(ann.update2$read)
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)
}
split into ovga and ic updates
update.merged.ic <- bind_rows(sstations,ann.update4)
update.merged <- ann.update4

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:

  1. Add water year (WY), runoff year (RY), and irrigation season (April–September) date columns
  2. Bind WY2025 records to the master totals table
  3. Verify no duplicate month-staid combinations
  4. 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

IC date columns
update.merged.ic <- update.merged.ic %>% 
  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))
update IC DB
totals_means <- bind_rows(update.merged.ic, monthlies) %>%
  mutate(date = as.Date(date))
write updated IC DB
totals_means %>% write_csv(here('data','hydro','totals_means_thru_sep_2025.csv'))
out_icwd <- here("output", "2025wy", "ICWD")
dir.create(out_icwd, recursive = TRUE, showWarnings = FALSE)
totals_means %>% write_csv(file.path(out_icwd, "totals_means_thru_sep_2025.csv"))

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.

filter no value -777
tm <- ann.update5 %>% filter(read != -777)
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"
  )
find/remove duplicates
dupes <- sw.flow %>% get_dupes() 

swflow2 <- sw.flow %>% anti_join(dupes)
write ovga flow
out_ovga <- here("output", "2025wy", "OVGA")
dir.create(out_ovga, recursive = TRUE, showWarnings = FALSE)
swflow2 %>% write_csv(file.path(out_ovga, paste0("ovga_swflow_import_wy", wy_short, ".csv")))
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
DT::datatable(
  flow_export_new,
  options = list(pageLength = 10, lengthChange = TRUE),
  caption = "Flow gages in WY2025 export but not in WY2024 export"
)

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:

  1. Joining production metadata (method, coordinates, wellfield associations)
  2. Filtering to OVGA-approved monitoring points
  3. Mapping to OVGA column schema (WellName, StartDate, EndDate, MIUsageAF, Method)
  4. Defaulting missing method values to “Unknown”
  5. 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
dupeswell <- tm.well.flow %>% get_dupes()
if (nrow(dupeswell) == 0) {
  cat("No duplicates found.\n")
} else {
  cat("Duplicate rows found: ", nrow(dupeswell), "\n")
  dupeswell
}
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
DT::datatable(
  production_export_new,
  options = list(pageLength = 10, lengthChange = TRUE),
  caption = "Production wells in WY2025 export but not in WY2024 export"
)

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
DT::datatable(
  raw_flow_excluded_current,
  options = list(pageLength = 10, lengthChange = TRUE, scrollX = TRUE),
  caption = "Flow staids in WY2025 raw data excluded from WY2025 export"
)
code
DT::datatable(
  raw_prod_excluded_prev,
  options = list(pageLength = 10, lengthChange = TRUE, scrollX = TRUE),
  caption = "Production staids in WY2024 raw data excluded from WY2024 export"
)
code
DT::datatable(
  raw_prod_excluded_current,
  options = list(pageLength = 10, lengthChange = TRUE, scrollX = TRUE),
  caption = "Production staids in WY2025 raw data excluded from WY2025 export"
)

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
DT::datatable(
  prod_exclusions,
  options = list(pageLength = 10, lengthChange = TRUE, scrollX = TRUE),
  class = "compact stripe",
  caption = "Production staids excluded from OVGA export (WY2025)"
)

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

code
tm2 <- totals_means
monthly_uses_syncplot(staid_p = "OVR" ,data = tm2, group_name = 'ov')

Owens Valley Runoff

OVPW Owens Valley Pumped Water

code
monthly_uses_syncplot(staid_p = "OVPW" ,data = tm2, group_name = 'ov')

Owens Valley Pumped Water

FTC L.A.A. Total Flow to the City

code
monthly_uses_syncplot(staid_p = "FTC" ,data = tm2, group_name = 'ov')

LAA Total Flow to the City

LOLU Lower Owens River Project – Lakes and Ponds Use

code
monthly_uses_syncplot(staid_p = "LOLU" ,data = tm2, group_name = 'ov')

Lower Owens River Project – Lakes and Ponds Use

LOOU Lower Owens River Project – Operations

code
monthly_uses_syncplot(staid_p = "LOOU" ,data = tm2, group_name = 'ov')

Lower Owens River Project – Operations

LORPDU Lower Owens River – Delta Uses

code
monthly_uses_syncplot(staid_p = "LORPDU" ,data = tm2, group_name = 'ov')

Lower Owens River – Delta Uses

LORPRU Lower Owens River – River Uses

code
monthly_uses_syncplot(staid_p = "LORPRU" ,data = tm2, group_name = 'ov')

Lower Owens River – River Uses

LORPTU Lower Owens River Project Total Uses

code
monthly_uses_syncplot(staid_p = "LORPTU" ,data = tm2, group_name = 'ov')

Lower Owens River Project Total Uses

LOWU Lower Owens River Project – Waterfowl Uses

code
monthly_uses_syncplot(staid_p = "LOWU" ,data = tm2, group_name = 'ov')

Lower Owens River Project – Waterfowl Uses

MBR Mono Basin Runoff

code
monthly_uses_syncplot(staid_p = "MBR" ,data = tm2, group_name = 'ov')

Mono Basin Runoff

MTWP Mono Tunnel at West Portal

code
monthly_uses_syncplot(staid_p = "MTWP" ,data = tm2, group_name = 'ov')

Mono Tunnel at West Portal

OLTU Owens Lake Total Uses

code
monthly_uses_syncplot(staid_p = "OLTU" ,data = tm2, group_name = 'ov')

Owens Lake Total Uses

OVFG Owens Valley Flowing Groundwater

code
monthly_uses_syncplot(staid_p = "OVFG" ,data = tm2, group_name = 'ov')

Owens Valley Flowing Groundwater

OVGR Owens Valley Groundwater Recharge

code
monthly_uses_syncplot(staid_p = "OVGR" ,data = tm2, group_name = 'ov')

Owens Valley Groundwater Recharge

OVIR Owens Valley Irrigation

code
monthly_uses_syncplot(staid_p = "OVIR" ,data = tm2, group_name = 'ov')

Owens Valley Irrigation

SHTO South Haiwee Total Outflow (1+2+3+BP)

code
monthly_uses_syncplot(staid_p = "SHTO" ,data = tm2, group_name = 'ov')

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.

Warning

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)

code
DT::datatable(
  seasonal_by_site$seasonal_wy,
  options = list(pageLength = 10, lengthChange = TRUE, scrollX = TRUE)
)

Runoff year totals by site (Apr-Mar)

code
DT::datatable(
  seasonal_by_site$runoff_ry,
  options = list(pageLength = 10, lengthChange = TRUE, scrollX = TRUE)
)

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.

code
DT::datatable(
  seasonal_by_site$percentiles_wy,
  options = list(pageLength = 10, lengthChange = TRUE, scrollX = TRUE)
)

RY2025 percentiles by site (runoff year Apr-Mar)

code
DT::datatable(
  seasonal_by_site$percentiles_ry,
  options = list(pageLength = 10, lengthChange = TRUE, scrollX = TRUE)
)

Seasonal pumping totals by wellfield (water year Oct-Sep)

code
DT::datatable(
  seasonal_by_wellfield$seasonal_wy,
  options = list(pageLength = 10, lengthChange = TRUE, scrollX = TRUE)
)

Runoff year totals by wellfield (Apr-Mar)

code
DT::datatable(
  seasonal_by_wellfield$runoff_ry,
  options = list(pageLength = 10, lengthChange = TRUE, scrollX = TRUE)
)

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.

code
DT::datatable(
  seasonal_by_wellfield$percentiles_wy,
  options = list(pageLength = 10, lengthChange = TRUE, scrollX = TRUE)
)

RY2025 percentiles by wellfield (runoff year Apr-Mar)

code
DT::datatable(
  seasonal_by_wellfield$percentiles_ry,
  options = list(pageLength = 10, lengthChange = TRUE, scrollX = TRUE)
)

Laws Monitoring Site #1 (LW1)

code
plot_linked_wells(tm2, "LW1", linked_wells_sites)

Laws Monitoring Site #2 (LW2)

code
plot_linked_wells(tm2, "LW2", linked_wells_sites)

Laws Monitoring Site #3 (LW3)

code
plot_linked_wells(tm2, "LW3", linked_wells_sites)

Laws Wellfield Total Pumping

code
plot_wellfield_total(
  tm2,
  field_sites = c("LW1", "LW2", "LW3"),
  linked_wells_sites = linked_wells_sites,
  title = "Laws Wellfield Total Pumping"
)

Big Pine Monitoring Site #1 (BP1)

code
plot_linked_wells(tm2, "BP1", linked_wells_sites)

Big Pine Monitoring Site #2 (BP2)

code
plot_linked_wells(tm2, "BP2", linked_wells_sites)

Big Pine Monitoring Site #3 (BP3)

code
plot_linked_wells(tm2, "BP3", linked_wells_sites)

Big Pine Monitoring Site #4 (BP4)

code
plot_linked_wells_single(tm2, "BP4", linked_wells_sites)

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_exempt
Figure 1: hatchery wells 330/332 and exempt wells 218/219

Big 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_bp

Big Pine Wellfield Pumping

Taboose/Aberdeen Monitoring Site #3 (TA3)

code
plot_linked_wells(tm2, "TA3", linked_wells_sites)

Taboose/Aberdeen Monitoring Site #4 (TA4)

code
plot_linked_wells(tm2, "TA4", linked_wells_sites)

Taboose/Aberdeen Monitoring Site #5 (TA5)

code
plot_linked_wells_single(tm2, "TA5", linked_wells_sites)

Taboose/Aberdeen Monitoring Site #6 (TA6)

code
plot_linked_wells(tm2, "TA6", linked_wells_sites)

Taboose/Aberdeen Wellfield Total Pumping

code
plot_wellfield_total(
  tm2,
  field_sites = c("TA3", "TA4", "TA5", "TA6"),
  linked_wells_sites = linked_wells_sites,
  title = "Taboose/Aberdeen Wellfield Total Pumping"
)

Thibaut/Sawmill Monitoring Site #1 (TS1)

code
plot_linked_wells_single(tm2, "TS1", linked_wells_sites)

Thibaut/Sawmill Monitoring Site #2 (TS2)

code
plot_linked_wells_single(tm2, "TS2", linked_wells_sites)

Thibaut/Sawmill Monitoring Site #3 (TS3)

code
plot_linked_wells(tm2, "TS3", linked_wells_sites)

Thibaut/Sawmill Monitoring Site #4 (TS4)

code
plot_linked_wells(tm2, "TS4", linked_wells_sites)

Thibaut/Sawmill Wellfield Total Pumping

code
plot_wellfield_total(
  tm2,
  field_sites = c("TS1", "TS2", "TS3", "TS4"),
  linked_wells_sites = linked_wells_sites,
  title = "Thibaut/Sawmill Wellfield Total Pumping"
)

Independence/Oak Monitoring Site #1 (IO1)

code
plot_linked_wells(tm2, "IO1", linked_wells_sites)

Independence/Oak Monitoring Site #2 (IO2)

code
plot_linked_wells_single(tm2, "IO2", linked_wells_sites)

Independence/Oak Wellfield Total Pumping

code
plot_wellfield_total(
  tm2,
  field_sites = c("IO1", "IO2"),
  linked_wells_sites = linked_wells_sites,
  title = "Independence/Oak Wellfield Total Pumping"
)

Symmes/Shepherd Monitoring Site #1 (SS1)

code
plot_linked_wells(tm2, "SS1", linked_wells_sites)

Symmes/Shepherd Monitoring Site #2 (SS2)

code
plot_linked_wells(tm2, "SS2", linked_wells_sites)

Symmes/Shepherd Monitoring Site #3 (SS3)

code
plot_linked_wells(tm2, "SS3", linked_wells_sites)

Symmes/Shepherd Monitoring Site #4 (SS4)

code
plot_linked_wells(tm2, "SS4", linked_wells_sites)

Symmes/Shepherd Wellfield Total Pumping

code
plot_wellfield_total(
  tm2,
  field_sites = c("SS1", "SS2", "SS3", "SS4"),
  linked_wells_sites = linked_wells_sites,
  title = "Symmes/Shepherd Wellfield Total Pumping"
)

Bairs/Georges Monitoring Site #2 (BG2)

code
plot_linked_wells(tm2, "BG2", linked_wells_sites)

Bairs/Georges Wellfield Total Pumping

code
plot_wellfield_total(
  tm2,
  field_sites = c("BG2"),
  linked_wells_sites = linked_wells_sites,
  title = "Bairs/Georges Wellfield Total Pumping"
)

Citation

BibTeX 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}
}
For attribution, please cite this work as:
County Water Department, Inyo. 2026. “Flow Data.” Data Report. Inyo County Water Department. https://inyo-gov.github.io/hydro-data/.