Step-by-step: building the DM T1 & T2 demo app
A concrete, ordered build plan for the longitudinal pilot (Section 7b). Written so each step can be handed to Claude as a standalone prompt — run them in order in a fresh Claude session (Claude Code or a project with file-creation enabled).
Stack constraint: the entire demo is built with HTML, JSON, and JavaScript only — no Python, no Node/build step, no server. Every step below runs client-side, directly in the browser, and hands its output forward as a downloadable JSON file (or files) that the next step's HTML page loads. Section 8’s original “FHIR-to-graph ingestion script (Python + NetworkX/Neo4j)” note and Step 2’s original wording are superseded by this constraint.
Step 1 — Generate the synthetic patient corpus
Ask Claude to create a small synthetic dataset before any graph or UI work starts. This avoids any real PHI and gives every later step something concrete to run against.
- 8–10 synthetic T1D patients, 8–10 synthetic T2D patients.
- Each patient as a FHIR-flavored JSON bundle:
Patient,Observation,Condition,MedicationRequest,Encounter. - T1D records should include irregular acute-spike events (DKA admission, hypoglycemic event, pump/CGM failure) with timestamps.
- T2D records should include a long, slow-branching regimen (lifestyle → oral agent → combination → insulin) with comorbidity onset dates (hypertension, dyslipidemia, CKD, retinopathy).
- Add the tagging schema from Section 7’s action plan to every case:
facility_tier,resource_status,presentation_delay(or, for T2D,time_to_insulin_initiation).
Outcome (done): step1_corpus_generator.html is a self-contained, in-browser generator (no Python) that produces dm_corpus.json — 9 synthetic T1D + 9 synthetic T2D patients (seeded, reproducible), each a FHIR-flavored bundle (Patient, Condition, Observation, MedicationRequest, Encounter) plus the tags block from Section 7’s schema (facility_tier, resource_status, and, per-disease, presentation_delay_hours/spike_event_count for T1D or time_to_insulin_initiation_years/comorbidity_count for T2D). All data is explicitly marked synthetic (meta.description, synthetic-demo-data resource tags) — no real PHI. This JSON file is the fixed input every later step reads.
Step 2 — Build the FHIR-to-graph ingestion page
Have Claude build an in-browser (HTML + JS, no Python) ingestion tool that reads the JSON corpus and instantiates the graph from Section 3.1:
- Nodes:
PatientNode,ObservationNode,DiagnosisNode,InterventionNode,OutcomeNode— attributes per the table in Section 3.2. - Edges:
SUGGESTS,INDICATES_PLAN,PRODUCED_OUTCOME,MODULATES,CONSTRAINS— with time carried as an edge property (timestamp,onset,time_to_outcome_delta) per Section 4, not as a rigid node sequence. - Output: one graph object per patient, plus a merged cohort graph for comparative aggregation, exported as JSON (not a live database).
Outcome (done): step2_graph_builder.html loads dm_corpus.json and builds the graph entirely client-side — explicitly labeled “runs entirely in-browser, no Python/Node required.” It instantiates the five node types and the patient-anchor edges (HAS_OBSERVATION, HAS_DIAGNOSIS, RECEIVES_PLAN, RESULTS_IN) plus the relational edges (SUGGESTS, INDICATES_PLAN, PRODUCED_OUTCOME, MODULATES, CONSTRAINS), merges per-patient graphs into one cohort graph, and renders a node/edge table in the page. It offers five downloads, all generated in-browser: cohort_graph.json (node-link format), cohort_graph.graphml, neo4j_nodes.csv / neo4j_edges.csv (Neo4j-ready, per the original Section 8 note, but produced without touching Neo4j or Python), and graph_summary.json. This satisfies Step 2 under the HTML/JSON/JS-only constraint — the “script” is a browser page, not a Python script.
Step 3 — The two dashboard modes, as built
Built as a single self-contained page (step3_dashboard.html) with no external libraries. It takes a manual file-load of cohort_graph.json (the Step 2 output) via a plain file input, then renders that same graph two ways behind one toggle:
- Mode A (Atemporal/Gestalt): strips timestamps and clusters each patient’s full observation/diagnosis/plan/outcome set into one profile view, plus a cards-based cohort overview when no single patient is selected.
- Mode B (Temporal trajectory): renders the timestamped edges on a shared axis (years since primary diagnosis), with small-multiple timelines in overview and a full per-patient trajectory view — spike markers, severity sizing, and comorbidity role are all driven by generic node attributes rather than disease-specific code branches.
- One toggle, one rendering path, for both T1D and T2D — the generalization test named in Section 7b. A disease filter (All / T1D only / T2D only) and a patient picker sit alongside the mode toggle; a running node/edge/patient-count summary and a click-through detail panel (raw node attributes) round out the page.
- This stage is read-only: it does not transform or re-save the graph, so there is no export button. The same
cohort_graph.jsonloaded here carries forward unchanged as the input to Step 4.
Step 4 — The pivot lens selector, as built
Built as its own self-contained page (step4_pivot_lens.html), loading the same cohort_graph.json from Step 2/3 with no re-ingestion or schema change. A lens toggle re-indexes the identical node/edge set around a chosen anchor instead of a chosen patient:
- Disease-centered — anchor is a Diagnosis node, picked from a dropdown populated with every diagnosis term present in the loaded cohort (“given patients diagnosed with X, what walked in and what did we do?”).
- Intervention-centered — anchor is an Intervention node, picked from a dropdown of intervention groups found in the data, with an outcome-variance table (“when we deploy Y, who receives it and how do outcomes vary?”).
- Disease + Intervention combo — anchor is the composite hyper-node, with paired diagnosis and intervention dropdowns; it defaults to Type 1 diabetes mellitus × insulin therapy when that combination is present, satisfying the “T1D + insulin pump therapy” combo case named in Section 5.
- Switching lenses swaps which dropdown(s) are visible and re-renders the view; no lens re-derives or special-cases T1D vs. T2D in code — every anchor and grouping is read off generic node/edge attributes. The same read-only, click-for-raw-attributes detail panel from Step 3 carries over here.
Step 5 — The comparator filter, as built
Built as its own page (step5_comparator_filter.html), loading the same cohort_graph.json from Steps 2–4 with no re-ingestion. Three independent filter layers sit over the cohort, and each one only constrains the population it applies to — patients it doesn’t apply to pass through unfiltered, so mixed T1D+T2D views stay meaningful:
- Population filter: All / T1D only / T2D only.
- Intervention-timing filter, disease-appropriate: for T2D, insulin-initiation timing bucketed from
time_to_insulin_initiation_yearsinto Early (<5 years) / Delayed (≥5 years) / Not yet recorded; for T1D, pump-failure evidence vs. none, using the same text-inferred approximation introduced in Step 4 (and flagged again here rather than presented as a first-class field). - Comparator matrix: outcome (complication-free vs. complicated, where “complicated” means at least one Outcome node with status
stabilized_with_sequelae), facility tier, and resource status — each independently selectable, plus a group-by control (patient / time-to-first-complication bucket / facility tier / resource status). - A one-click “Load Section 6 example query” button reproduces the worked example end-to-end (“T2D patients on delayed insulin initiation in resource-constrained settings, grouped by time-to-first-complication”) and surfaces a data-completeness note inline: only a handful of T2D patients in this synthetic corpus carry a recorded insulin-initiation timing, so the result set is small — called out as a real data gap rather than hidden, per Step 7.
Step 6 — The clinically-readable output view, as built
Built as its own page (step6_clinical_output_view.html), loading the same cohort_graph.json from Steps 2–5 with no re-ingestion or new derivations — it re-presents what Steps 3–5 already established rather than re-analyzing anything:
- Evidence: a T1D / T2D side-by-side pair of cards, each with patient count, complication rate, top recorded interventions, and a bar list of the most common outcome reasons for that diagnosis group.
- Variation: complication-rate tables broken out by facility tier and by resource status, with rows at 50%+ complication highlighted, and an explicit note that small subgroup sizes in this synthetic cohort make these rates signal-to-investigate rather than validated effect estimates.
- Confounders: the approximations and data gaps introduced in Steps 4–5 — the pump-vs-injection text-inference proxy, the mostly-null T2D insulin-initiation timing field, small subgroup denominators, and the schema-level (not clinically adjudicated) definition of “complicated” — collected in one place instead of scattered across pages, each with a live count against the loaded cohort.
- Per Section 2, the Section 0 scope note is carried into the UI itself as a persistent, sticky banner (not a one-time notice) stating this is an audit/analytics view and not a decision-support recommendation for any individual patient.
Step 7 — The generalization sanity check, as written
Delivered as its own written report (step7_generalization_check.html) rather than an interactive tool, since Section 7b asked for a confirmation in writing. It audits the actual source of Steps 2–6 for every place condition_type, "T1D", or "T2D" appears, and classifies each occurrence as either generic (used only to filter, count, or label) or special-cased (the rendering/filtering logic itself branches by disease). Verdict: mostly confirmed, with two flagged exceptions rather than none.
- Steps 2, 3, and 6 are fully generalized — one code path dispatches on generic node attributes (
node_type,term,target,status) for both diseases. - Step 4’s pivot lenses are generalized except for the pump-evidence sub-branch of the combo lens, which is meaningful only for T1D by construction.
- Step 5’s intervention-timing filter axis is an accepted, by-design exception: it reads a different field per disease (T2D insulin-initiation timing vs. T1D pump evidence) because the spec asked for disease-appropriate timing questions, not because the same field was handled differently.
- Both exceptions are surfaced in-UI as flagged assumptions in Steps 4 and 5 rather than hidden — the “useful signal” Section 7b’s closing paragraph asked this step to produce.
Prompt block to paste into Claude
Copy this into a new Claude conversation (ideally with code execution / file creation enabled) to kick off Step 1. Steps 2–7 can each be pasted in turn as the prior step's output is reviewed.
Build a small demo app for the "Person-Centered Clinical Analytics" concept,
scoped to the Type 1 vs. Type 2 diabetes longitudinal pilot.
Stack constraint: HTML, JSON, and JavaScript only. No Python, no Node/build
step, no server/database. Every stage is a standalone HTML page that runs
in-browser and hands its output to the next stage as a downloadable JSON file.
Step 1: Generate a synthetic patient corpus.
- 8-10 synthetic T1D patients, 8-10 synthetic T2D patients (clearly synthetic,
no real PHI).
- Each as a FHIR-flavored JSON bundle: Patient, Observation, Condition,
MedicationRequest, Encounter.
- T1D cases: include irregular acute-spike events (DKA admission,
hypoglycemic event, pump/CGM failure) with timestamps.
- T2D cases: include a long, slow-branching regimen (lifestyle -> oral
agent -> combination -> insulin) with comorbidity onset dates
(hypertension, dyslipidemia, CKD, retinopathy).
- Tag every case with: facility_tier (primary/secondary/tertiary/quaternary),
resource_status (plenty/constrained), and a relevant temporal delay field.
Once this is generated and I've reviewed it, I'll ask you to build the graph
ingestion script, then the dashboard (atemporal/temporal toggle, pivot lens,
comparator filter) on top of it.