← Back to projects

Blue Thumb Dashboard

Blue Thumb Dashboard
370+
Monitoring sites
20+ yrs
Data unified
800+
Automated tests
Daily
Auto-sync
PythonGoogle CloudVertex AIArcGIS RESTDockerSQLite

The Problem

Since 1992, Blue Thumb volunteers have monitored water quality at 370+ stream locations across Oklahoma. Three decades of high-quality data had piled up, but it was scattered across agency portals, spreadsheets, and an ArcGIS server. The data was split across four different categories: water chemistry, fish surveys, macroinvertebrate surveys, and habitat assessments. Here is how I pulled all of it into one live, continuously updated map for public viewing.

Unifying four data streams

Four data sources, each with a distinct shape, had to land in a single normalized schema where a fish survey and a nitrate reading could be linked to the same monitoring location.

Chemistry data mixed range-based measurements, competing nutrient columns, and below-detection-limit values written as text. Fish and macroinvertebrate surveys arrived as bioassessment scores that needed their own indexing logic. Habitat assessments were a different shape again. I wrote a dedicated processing module per domain to clean, validate, and reshape each source, then collapse all four into one schema keyed on a shared set of sites.

Static data unificationfour sources · one schema
Chemical
range · nutrients · BDL
Fish
bioassessment
Macro
bioassessment
Habitat
assessment metrics
blue_thumb.db
site-keyed · QAQC-gated
Four heterogeneous CSV feeds converge onto one normalized, site-keyed schema.
  • One module per domain — chemical, fish, macroinvertebrate, and habitat data each get their own load-clean-normalize path, so each domain's quirks stay isolated.
  • Below-detection-limit handling — non-numeric "<0.01"-style readings are converted to consistent numeric values instead of being dropped.
  • Reproducible build — the whole database rebuilds from the raw files with a single command, so the pipeline is repeatable rather than a one-time hand-clean.
  • Validated before insert — every domain runs through QAQC gating and duplicate detection before a row is written.

One canonical map of sites

Unifying the measurements only works if every reading can be tied to the right physical place, and volunteer submissions do not carry clean IDs. The same creek shows up under different spellings and slightly different coordinates across all four data sources. So the spine of the whole database is one canonical site table, built before any measurement is loaded.

Building it runs in three passes. First I consolidated the site lists out of every source into one master list, using a priority ladder to settle conflicts when two sources disagree on a site's county, basin, or coordinates. Next I merged coordinate-duplicates: sites within 50 meters of each other are clustered with Haversine distance and folded into a single record, with all their monitoring data carried over. Only then are the master sites loaded, each classified as active or historic by how recent its chemistry is.

With that table in place, every incoming record resolves against it through a four-step ladder.

Site-name resolution4-step ladder · 50 m tolerance
1 · Normalize name
case · whitespace · punctuation
2 · Alias lookup
known synonym table
3 · Haversine match
nearest site within 50 m
4 · Auto-register
insert as new site
Geographic matching at 50 m catches re-spelled sites that string matching alone would miss.

Daily incremental sync

Once the historical database existed, the last piece was keeping it current. New chemical readings land in an ArcGIS Feature Service. A scheduled job pulls the new records once a day from the REST API, runs them through the chemical and site-resolution validation pipeline, then writes an updated copy of the database to cloud storage. An always-on Cloud Run app reads that file and serves it. The two stay decoupled, sharing nothing but the database, so a failed sync can just retry without affecting what visitors see.

① Build the database⏱ Cloud Scheduler · daily · serverless
ArcGIS API
Survey123 FeatureServer
Cloud Function
EditDate watermark
ETL + QAQC
validate · dedupe
SQLite write
normalized schema
blue_thumb.db
Cloud Storage · versioned + backup
② Serve it to usersalways on · stateless
Cloud Run
Dash app (Docker)
Interactive map
370+ sites
AI assistant
Vertex AI
The daily sync and the serve app share nothing but the database file, so a failed build can retry without touching what users see.

Key design decisions:

  • Incremental sync — the daily job uses an EditDate watermark to fetch only the records that changed since the last run, not the entire dataset.
  • Idempotent inserts — re-running the job never creates duplicates, so a retry is always safe.
  • QAQC gating — validation, below-detection-limit conversion, and duplicate detection all run before anything is written.
  • Backed up before every write — the previous database is versioned to cloud storage ahead of each update.

In production

The dashboard runs live on Cloud Run, refreshes automatically every day, and is backed by 800+ automated tests across the data-processing pipeline. A Vertex AI chatbot grounded in Blue Thumb's own documentation for context answers visitor questions about stream health. I continue to maintain and extend the dashboard alongside fellow Blue Thumb volunteer Miguel Ingram.

View live
View on GitHub