From 9c971c083ba2f81a4afafc69395e760af67d317c Mon Sep 17 00:00:00 2001 From: Andrew Charlwood Date: Fri, 6 Feb 2026 13:38:11 +0000 Subject: [PATCH] feat: add KPI update callback with formatted patient/drug/cost display (Task 3.3) --- IMPLEMENTATION_PLAN.md | 2 +- dash_app/callbacks/__init__.py | 2 ++ dash_app/callbacks/kpi.py | 41 ++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 dash_app/callbacks/kpi.py diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 3c42aa4..5a4ef20 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -179,7 +179,7 @@ Drawer selection → update_drug_selection → app-state store → load_pathway_ - **Checkpoint**: Changing date filter updates chart-data store with new pathway nodes ✓ ### 3.3 KPI update callback -- [ ] Create `dash_app/callbacks/kpi.py`: +- [x] Create `dash_app/callbacks/kpi.py`: - `update_kpis` callback: Input=`chart-data` store, Output=KPI card values (4 outputs) - Extracts `unique_patients`, `total_drugs`, `total_cost` from chart-data - Formats numbers: patients with commas, cost as "£XXX.XM", drugs as plain number diff --git a/dash_app/callbacks/__init__.py b/dash_app/callbacks/__init__.py index 514f5de..35265e8 100644 --- a/dash_app/callbacks/__init__.py +++ b/dash_app/callbacks/__init__.py @@ -5,6 +5,8 @@ def register_callbacks(app): """Register all Dash callbacks with the app instance.""" from dash_app.callbacks.filters import register_filter_callbacks from dash_app.callbacks.chart import register_chart_callbacks + from dash_app.callbacks.kpi import register_kpi_callbacks register_filter_callbacks(app) register_chart_callbacks(app) + register_kpi_callbacks(app) diff --git a/dash_app/callbacks/kpi.py b/dash_app/callbacks/kpi.py new file mode 100644 index 0000000..5fb83bf --- /dev/null +++ b/dash_app/callbacks/kpi.py @@ -0,0 +1,41 @@ +"""Callback for updating KPI card values from chart-data store.""" +from dash import Input, Output, no_update + + +def _format_cost(cost): + """Format cost as £X.XM or £X.XK.""" + if cost >= 1_000_000: + return f"\u00a3{cost / 1_000_000:.1f}M" + if cost >= 1_000: + return f"\u00a3{cost / 1_000:.1f}K" + return f"\u00a3{cost:.0f}" + + +def register_kpi_callbacks(app): + """Register KPI update callback.""" + + @app.callback( + Output("kpi-patients", "children"), + Output("kpi-drugs", "children"), + Output("kpi-cost", "children"), + Output("kpi-match", "children"), + Input("chart-data", "data"), + Input("app-state", "data"), + ) + def update_kpis(chart_data, app_state): + """Update KPI card values when chart-data changes.""" + if not chart_data: + return no_update, no_update, no_update, no_update + + patients = chart_data.get("unique_patients", 0) + drugs = chart_data.get("total_drugs", 0) + cost = chart_data.get("total_cost", 0.0) + + patients_str = f"{patients:,}" if patients else "\u2014" + drugs_str = str(drugs) if drugs else "\u2014" + cost_str = _format_cost(cost) if cost else "\u2014" + + chart_type = (app_state or {}).get("chart_type", "directory") + match_str = "~93%" if chart_type == "indication" else "\u2014" + + return patients_str, drugs_str, cost_str, match_str