Skip to main content

GIS Explorer v2 Phase 1: From COVID Dashboard to Disease-Agnostic Spatial Analytics

· 5 min read
Creator, Parthenon
AI Development Assistant

Today was a focused, high-output session centered entirely on one major architectural shift: evolving the GIS Explorer from a hardcoded COVID-19 dashboard into a fully generalized spatial analytics tool capable of visualizing any condition in the OMOP CDM. Eighteen commits across the full stack — Python AI service, Laravel backend, and React frontend — tell the story of a component suite that went from COVID-specific to condition-agnostic in a single day.

The Core Problem We Solved

The original GIS Explorer was built fast and built for one thing: COVID-19 county-level choropleth maps. Hardcoded concept IDs, covid_-prefixed metric enums, a CovidSummaryBar component — it was useful but brittle. The vision for v2 was always to make this work for any condition in the CDM: hypertension, Type 2 diabetes, COPD, atrial fibrillation, whatever a researcher needs. Phase 1 of that vision shipped today.

Backend: Python AI Service Generalization

The heaviest lifting happened in the Python AI service layer. The key files touched were cdm_spatial_query.py, cdm_spatial.py (both models and router), and the new solr_spatial.py read service.

cdm_spatial_query.py was the most surgically changed file — every COVID-specific assumption was replaced with a concept_id parameter. The refresh_county_stats function now computes five metrics (cases, deaths, CFR, hospitalizations, prevalence) for any condition by querying condition_occurrence joined against person and visit_occurrence, then pushes the resulting documents into the gis_spatial Solr core.

Solr schema design was a meaningful architectural decision today. Rather than querying PostgreSQL on every map render, we're building a gis_spatial core with a configset designed for condition-county aggregate documents. The schema includes fields for time series, population denominators, and prevalence — giving us the flexibility to support temporal sliders and demographic breakdowns without hammering the CDM directly. A new Artisan command (solr:index-gis-spatial) was wired up on the Laravel side to trigger reindexing on demand.

SNOMED category mapping deserves a callout: we built a curated mapping of 11 ancestor concept_ids to clinical categories (Cardiovascular, Respiratory, Metabolic, and more) using the concept_ancestor hierarchy. This powers the category faceting in the new DiseaseSelector component — users don't just get a flat list of thousands of conditions, they get organized clinical groupings.

The new Pydantic models (ConditionItem, ConditionCategory, ConditionSummary, RefreshResult) clean up the data contracts considerably, and the renamed metric enum values (dropping the covid_ prefix) make the intent clear throughout the codebase.

New API endpoints added to the router:

  • GET /conditions — full conditions list via Solr pivot facets with stats
  • GET /conditions/categories — SNOMED-mapped clinical categories
  • GET /summary — aggregate summary stats via Solr stats component
  • POST /reindex-all — trigger a full CDM → Solr reindex

All three read endpoints follow a Solr-first with PostgreSQL fallback pattern, which is a good resilience principle to carry forward as we expand Solr usage across the platform.

Backend: Laravel Proxy Routes

The Laravel layer got a clean GisController expansion with four new proxy methods mirroring the Python endpoints: cdmConditions(), cdmConditionCategories(), cdmSummary(), and cdmReindexAll(). Routes were registered under /api/v1/gis/cdm/ following existing GIS conventions. This keeps the frontend talking exclusively to Laravel, which handles auth, rate limiting, and service proxying consistently.

A notable fix also landed here: a Solr schema copyField error that was causing indexing failures was resolved, and condition patient count accuracy was improved — both quality-of-life fixes that would have caused confusing data discrepancies during QA.

Frontend: A New Component Suite

Four React components were refactored or built from scratch to support disease-agnostic data flow:

  • DiseaseSelector — the flagship new component. Supports quick picks (common conditions surfaced by prevalence), category browsing (powered by the SNOMED mapping), and free-text search. This replaces what was previously just a hardcoded COVID selection.
  • DiseaseSummaryBar — replaces CovidSummaryBar, rendering summary statistics dynamically based on whichever condition is active.
  • TimeSlider, CountyDetail, MetricSelector — all parameterized to accept a condition or concept_id prop rather than assuming COVID data shapes.

GisPage was updated to wire everything together: DiseaseSelector drives the active condition state, which flows down as props into every subordinate component. The frontend types, API client, and hooks were all generalized in a single commit (4a748171) before the UI components were layered on top — the right order of operations.

Documentation

Both an implementation plan and a devlog entry for GIS Explorer v2 Phase 1 were committed to the docs directory today. This is part of a broader push to keep architectural decisions captured close to the code, rather than living only in linear chat threads.

What's Next

Phase 1 established the foundation. Phase 2 will likely focus on:

  • Demographic breakdowns — age/sex stratification per condition in the county detail panel
  • Temporal analytics — ensuring the TimeSlider connects to real time-period facets from the Solr index rather than static date ranges
  • Reindex scheduling — automating solr:index-gis-spatial on a configurable cadence so the spatial index stays fresh without manual intervention
  • Performance profiling — the refresh_county_stats job touching all condition-county pairs could get expensive at scale; we'll need to evaluate incremental update strategies
  • Search UX polish — debounce tuning and fuzzy matching on the DiseaseSelector search input

Today's work was a satisfying architectural unlock. The GIS Explorer is no longer a one-condition curiosity — it's a real spatial analytics surface for the full breadth of OMOP CDM data.