medOS ultra

Biomarker Graph Platform

Reusable graphs over the central biomarker cache: one renderer + pure cores, five mounts, cross-signal overlay, provenance-rendered.

7 min read diagramsUpdated 2026-06-10docs/architecture/biomarker-graph-platform.md

Status: Design 2026-06-11, not built. Companion to docs/architecture/icu-flowsheet-world-best-master-plan.md (§5). One-sentence pitch: any biomarker that is properly saved (flowsheet, lab, device, assessment) becomes graphable anywhere — as a standalone miniapp, a DynamicContentRenderer module, a widget-rail widget, a draggable card in patient-profile, or a floating cross-overlay window — because graphs bind to the central biomarker cache, never to the surface that charted the value.


1. The core principle: graphs read the cache, not the chart

Today every surface that wants a trend builds its own renderer over its own data (flowsheet RowTimelineSparkline/AlignedTimelineChart/News2TrendChart/IoBalanceChart, lab LabResultsWidget, Patient 360 vitals, dynamic-scheduler dose visualizers). That couples the graph to the writer and makes “show me this HR trend on another page” impossible.

The platform inverts this:

WRITERS (already converge — no new work)                READERS (this doc)
flowsheet mirrorSheetVitals ─┐
lab pipeline (ORU/LIS)      ─┤→ observations            useBiomarkerSeries(patientId, codes, window)
device sync                 ─┤→ lab_results_ts                 │
HL7v2 / FHIR write API      ─┤   (hypertable)            BiomarkerGraphCard (ONE renderer)
assessments / vitals forms  ─┘                                 │
                                              every surface mounts the same card
  • Cache = observations (+ lab_results_ts hypertable) — already realtime, already fed by every write path (see docs/architecture/lab-data-pipeline.md). The flowsheet already writes through (mirrorSheetVitals → writeThroughVitals).
  • Read hook = web/src/services/observation-core/useObservations.ts — the existing central read; useBiomarkerSeries is a thin selector over it (group by code, sort by effective time, attach reference ranges).
  • Cross-over math exists: web/src/services/ai/clinical-query/correlate.ts already correlates signals; the platform exposes it as a UI affordance, not just an AI tool.

Invariant #1 — identity is the canonical code. A graph definition references biomarkers by canonical key (LOINC/SNOMED or the observation code), never by flowsheet row id (gtXXXX), never by sheet/template id. The flowsheet’s id→canonical mapping problem (see master plan Tier-1 #12) is solved ONCE in the write path / a mapping table, and every reader benefits.

Invariant #2 — graphs are read-only. No graph surface ever writes observations. Charting stays in the owning miniapps; the platform renders.

Invariant #3 — provenance is rendered. Carried-forward / device-unvalidated / amended points draw differently (hollow marker, stripe). This is the read-side twin of the master plan’s provenance work (§1.2/§4.1 there).


2. Package layout (new kit, additive)

web/packages/medical-kit/src/biomarker-graphs/
├── core/
│   ├── series-core.ts          # pure: resolveSeries(observations, code) → {points, unit, range, provenance[]}
│   ├── overlay-core.ts         # pure: composeOverlay(seriesA, seriesB, mode) — dual-axis | normalized | ratio
│   ├── graph-config.ts         # GraphConfig type + (de)serialize + registry
│   └── correlate-adapter.ts    # wraps services/ai/clinical-query/correlate.ts → r, lag, scatter pairs
├── components/
│   ├── BiomarkerGraphCard.tsx  # THE renderer: line/area/sparkline/scatter + range bands + event pins
│   ├── GraphOverlayLegend.tsx  # per-series chips: colour, unit, axis side, remove ✕
│   ├── GraphConfigPanel.tsx    # add/remove biomarkers, window, representation, thresholds (bilingual)
│   └── FloatingGraphWindow.tsx # draggable/resizable floating shell (see §4.4)
├── hooks/
│   ├── useBiomarkerSeries.ts   # selector over useObservations (realtime) + per-code mock fallback (isLive badge)
│   └── useGraphLayouts.ts      # persisted layouts via the master plan's usePersistedPref (server-backed)
└── __tests__/                  # pure-core jest per module

GraphConfig is data, not code (same philosophy as policy_gates/cds_rules/inventory_rules):

type GraphConfig = {
  id: string;                       // slug
  title: string; titleTh?: string;  // bilingual (project rule)
  series: Array<{
    code: string;                   // canonical biomarker key — Invariant #1
    label?: string; color?: string;
    axis?: 'left' | 'right';
    representation?: 'line' | 'area' | 'dots' | 'bars';
  }>;
  windowHours: number;              // 6 | 24 | 72 | sinceAdmission
  showRangeBands?: boolean; showEvents?: boolean;  // hospital_events pins (reuses events-core)
  thresholds?: Array<{ code: string; op: 'gt'|'lt'; value: number; color: string }>;
};

Persistence: a graph_configs table (app-owned, like dynamic_sheet_templates) for shared/admin-authored presets + per-user pinned layouts via useGraphLayouts. Seeds ship bilingual presets: Hemodynamics (HR+MAP+SBP), Respiratory (SpO₂+RR+FiO₂), Sepsis watch (Temp+WBC+Lactate+MAP), Renal (Cr+UO+K), Glucose-insulin.

Render engine: reuse what’s installed — SVG like the proven resource-timeline-core/AlignedTimelineChart approach for the card (no new chart dependency); the pure cores stay renderer-agnostic so a heavier engine can swap in later without touching configs.


3. Cross-overs: the doctor’s compare workflow

The headline feature: drag one biomarker onto another graph → overlaid trends.

  • BiomarkerGraphCard is a drop target; every legend chip and every biomarker name anywhere (lab widget rows, flowsheet param names, Patient 360 cards) is a drag source carrying application/x-medos-biomarker: {code, label} (HTML5 DnD — no new dep; mirror the Patient 360 draggable-card pattern).
  • Drop → composeOverlay: units compatible → shared axis; incompatible → dual y-axes (left/right chips); >2 units → normalized 0-1 mode with a clear “normalized” badge (never silently fake a scale).
  • Correlation strip (opt-in): when exactly 2 series are shown, correlate-adapter renders r + lag under the graph and a scatter toggle. Display-only, recommender-stance — never a gate, never an auto-conclusion.
  • Time cursor is shared across every mounted graph on the page (hover one → vertical line on all; the pattern SheetTimelineOverview already proves).

4. Surfaces (one card, mounted five ways)

4.1 DynamicContentRenderer module (graph as a miniapp)

Add case 'BiomarkerGraph': to the module registry in web/src/common/components/shared-engine/dynamic-core/engine/DynamicContentRenderer.tsx (same pattern as Patient360 at ~:2558). Module props = a GraphConfig id or inline config → admins can place graphs in any workflow tab / Department Command Center / worklist row-detail via existing config systems, zero code per placement.

4.2 Patient-profile draggable cards

Patient-profile miniapp grid mounts BiomarkerGraphCard as cards; layouts per-user via useGraphLayouts. Drag-to-rearrange follows the Patient 360 draggable-card mechanics; “+” opens GraphConfigPanel to pin a new graph.

4.3 Widget rail

Export a widgetSurface from the kit barrel (per docs/architecture/widget-rail-surface-system.md) — a sparkline-density mini card in the 320 px flyout next to OrderActivityDock/LabResultsWidget. Adding it = one barrel export.

4.4 Floating cross-look window

FloatingGraphWindow — FAB + draggable/resizable floating shell (the QueueManagementFloater pattern, docs/architecture/queue-management-floater.md) that stays mounted across miniapps on the page, so a doctor can float a Lactate+MAP overlay while working inside the order system or e-Kardex. Drop targets remain live while floating → drag a lab row from LabResultsWidget straight onto the floating graph. One floater per page; position persisted per-user.

4.5 Flowsheet (the V2 sheet becomes a reader too)

Replace nothing; add an opt-in “Open in graph” action on a row (and in CellDetailPopover) that opens the floating window pre-loaded with that row’s canonical code — using the same id→canonical map as derive-core. The sheet’s own sparklines/timeline stay; the platform adds take-it-anywhere.

Also: Patient 360 (usePatientBodyData already reads observations — its vitals card can become a BiomarkerGraphCard), export/print (graph → the existing print-iframe pattern from sheet-export-dom), and the QR self-service pattern later (share a read-only trend with a patient — patient-qr-self-service-pattern.md).


5. Customizability for assessments & biomarkers

  • Any assessment that records numerically graphs for free — assessment forms already write through the observation pipeline (IpdNursingAssessmentwriteThroughVitals); pain scores, GCS components, NEWS2 — each is a code in the cache, so each is a series. New scores from the master plan’s Tier-2 #7 land graphable on day one.
  • Computed series: series-core accepts a derived spec ({ derive: 'map' | 'shock_index' | ..., inputs }) reusing derive-core’s formula registry — graph MAP even where only SBP/DBP were charted.
  • Thresholds are per-graph data (GraphConfig.thresholds) and render alongside — but clinical alarm limits remain the range/CDS systems’ job; graph thresholds are visual annotations only (no alerts fire from a graph).
  • Admin authoring: GraphConfigPanel doubles as the admin editor for shared presets (Builder Studio entry: /admin/builder-studio card), bilingual labels mandatory.

6. Rollout

Phase Deliverable Verify
P0 Kit skeleton: series-core + useBiomarkerSeries (mock fallback) + BiomarkerGraphCard single-series + sandbox ?target=BiomarkerGraph + jest sandbox e2e
P1 Overlay: drag-drop chips, dual-axis/normalized modes, overlay-core tests, shared time cursor sandbox e2e
P2 Surfaces: DynamicContentRenderer module + patient-profile card + widget-rail export module-load-clean + attended eyeball
P3 FloatingGraphWindow + “Open in graph” from flowsheet/lab rows + id→canonical map (shared with derive-core wiring) attended (cross-miniapp)
P4 graph_configs table + seeds (bilingual presets) + admin panel + per-user layouts on usePersistedPref server backend migration + Playwright
P5 Correlation strip + provenance rendering (pairs with master plan §1.2/§4.1) + print/export attended

Every phase: additive, fail-soft (no observations → mock + isLive=false badge, like usePatientBodyData), pure-core jest, bilingual, MUI icons only, zero writes to read-model tables from the frontend (read-only by Invariant #2; graph_configs is app-owned config, not a read model).

7. Which Claude model to build it with

Same matrix as the master plan §7: Fable 5 (claude-fable-5, 1M context) for P0-P1 core design + the cross-surface P2/P3 wiring (touches DynamicContentRenderer + patient-profile + floater patterns simultaneously — the 1M window earns its keep); Opus 4.8 (claude-opus-4-8) for the autonomous polish loops (P4/P5, representation variants, preset library) at effort high/xhigh; Sonnet 4.6 for seed presets, i18n fills, and test scaffolds; Haiku 4.5 for explore subagents. The runtime product itself needs no LLM — correlation is deterministic math; keep AI out of the render path.

Ask Anything