feat: temporal trends CLI script + Dash tab (Task D.1)

D.1a: cli/compute_trends.py — standalone CLI that imports existing pipeline
functions to replay pathway computation for ~10 historical 6-month endpoints.
Creates pathway_trends table via CREATE TABLE IF NOT EXISTS.

D.1b: Trends tab (10th PP tab) with metric toggle (patients/cost/cost_pp_pa).
Query gracefully returns empty when table doesn't exist, figure shows
instruction message to run compute_trends.
This commit is contained in:
Andrew Charlwood
2026-02-07 18:24:34 +00:00
parent d892c9fba8
commit d0404aa18a
7 changed files with 603 additions and 32 deletions
+36 -7
View File
@@ -417,6 +417,25 @@ def _render_doses(app_state, title):
return create_dosing_distribution_figure(data, title)
def _render_trends(app_state, title, metric="patients"):
"""Build the temporal trends line chart."""
from dash_app.data.queries import get_trend_data
from visualization.plotly_generator import create_trend_figure
selected_dirs = (app_state or {}).get("selected_directorates") or []
selected_drugs = (app_state or {}).get("selected_drugs") or []
directory = selected_dirs[0] if len(selected_dirs) == 1 else None
drug = selected_drugs[0] if len(selected_drugs) == 1 else None
try:
data = get_trend_data(metric=metric, directory=directory, drug=drug)
except Exception:
log.exception("Failed to load trend data")
return _empty_figure("Failed to load trend data.")
return create_trend_figure(data, title, metric=metric)
def register_chart_callbacks(app):
"""Register tab switching, pathway data loading, and chart rendering callbacks."""
@@ -496,18 +515,21 @@ def register_chart_callbacks(app):
Output("pathway-chart", "figure"),
Output("chart-subtitle", "children"),
Output("heatmap-metric-wrapper", "style"),
Output("trends-metric-wrapper", "style"),
Input("chart-data", "data"),
Input("active-tab", "data"),
Input("app-state", "data"),
Input("heatmap-metric-toggle", "value"),
Input("trends-metric-toggle", "value"),
)
def update_chart(chart_data, active_tab, app_state, heatmap_metric):
def update_chart(chart_data, active_tab, app_state, heatmap_metric, trends_metric):
"""Render the active tab's chart from chart-data nodes."""
active_tab = active_tab or "icicle"
chart_type = (app_state or {}).get("chart_type", "directory")
# Show/hide heatmap metric toggle based on active tab
toggle_style = {} if active_tab == "heatmap" else {"display": "none"}
# Show/hide metric toggles based on active tab
heatmap_toggle_style = {} if active_tab == "heatmap" else {"display": "none"}
trends_toggle_style = {} if active_tab == "trends" else {"display": "none"}
if chart_type == "indication":
subtitle = "Trust \u2192 Indication \u2192 Drug \u2192 Patient Pathway"
@@ -515,17 +537,24 @@ def register_chart_callbacks(app):
subtitle = "Trust \u2192 Directorate \u2192 Drug \u2192 Patient Pathway"
if not chart_data:
return no_update, no_update, toggle_style
return no_update, no_update, heatmap_toggle_style, trends_toggle_style
error_msg = chart_data.get("error")
if error_msg:
return _empty_figure(error_msg), subtitle, toggle_style
return _empty_figure(error_msg), subtitle, heatmap_toggle_style, trends_toggle_style
# Trends tab doesn't depend on chart-data nodes
if active_tab == "trends":
title = _generate_chart_title(app_state) if app_state else ""
metric = trends_metric or "patients"
fig = _render_trends(app_state, title, metric=metric)
return fig, subtitle, heatmap_toggle_style, trends_toggle_style
if not chart_data.get("nodes"):
return _empty_figure(
"No matching pathways found.\n"
"Try adjusting your filters."
), subtitle, toggle_style
), subtitle, heatmap_toggle_style, trends_toggle_style
# Lazy rendering — only compute the active tab's chart
title = _generate_chart_title(app_state) if app_state else ""
@@ -580,4 +609,4 @@ def register_chart_callbacks(app):
tab_label = dict(TAB_DEFINITIONS).get(active_tab, active_tab)
fig = _empty_figure(f"{tab_label} chart — coming soon")
return fig, subtitle, toggle_style
return fig, subtitle, heatmap_toggle_style, trends_toggle_style
+18
View File
@@ -14,6 +14,7 @@ TAB_DEFINITIONS = [
("network", "Network"),
("timeline", "Timeline"),
("doses", "Doses"),
("trends", "Trends"),
]
# Full set retained for Trust Comparison dashboard (Phase 10.8)
@@ -94,6 +95,23 @@ def make_chart_card():
),
],
),
# Trends metric toggle — visible only when trends tab active
html.Div(
id="trends-metric-wrapper",
style={"display": "none"},
children=[
dmc.SegmentedControl(
id="trends-metric-toggle",
data=[
{"value": "patients", "label": "Patients"},
{"value": "total_cost", "label": "Cost"},
{"value": "cost_pp_pa", "label": "Cost p.a."},
],
value="patients",
size="xs",
),
],
),
],
),
# Chart area with loading spinner
+10
View File
@@ -30,6 +30,7 @@ from data_processing.pathway_queries import (
get_drug_network as _get_drug_network,
get_drug_timeline as _get_drug_timeline,
get_dosing_distribution as _get_dosing_distribution,
get_trend_data as _get_trend_data,
)
DB_PATH = Path(__file__).resolve().parents[2] / "data" / "pathways.db"
@@ -249,3 +250,12 @@ def get_dosing_distribution(
) -> list[dict]:
"""Average administered dose counts per drug."""
return _get_dosing_distribution(DB_PATH, date_filter_id, chart_type, directory, trust)
def get_trend_data(
metric: str = "patients",
directory: Optional[str] = None,
drug: Optional[str] = None,
) -> list[dict]:
"""Time-series trend data from pathway_trends table."""
return _get_trend_data(DB_PATH, metric, directory, drug)