Files
Andrew Charlwood 8e2e2b7125 feat: heatmap metric toggle for both PP and TC views (Task B.4)
- Add Heatmap tab to Patient Pathways TAB_DEFINITIONS (was only in ALL_TAB_DEFINITIONS)
- Add dmc.SegmentedControl (Patients/Cost/Cost p.a.) to PP chart card header, hidden by default
- update_chart callback controls toggle visibility via heatmap-metric-wrapper style output
- _render_heatmap() now accepts metric param from toggle
- Add dmc.SegmentedControl to TC heatmap chart cell inline
- tc_heatmap callback reads tc-heatmap-metric-toggle value and passes metric to figure fn
2026-02-07 03:05:41 +00:00

263 lines
11 KiB
Python

"""Callbacks for Trust Comparison landing page and dashboard navigation."""
from dash import html, Input, Output, State, ctx, no_update
import plotly.graph_objects as go
def register_trust_comparison_callbacks(app):
"""Register Trust Comparison view callbacks."""
@app.callback(
Output("tc-landing-grid", "children"),
Output("tc-landing-grid", "className"),
Output("tc-landing-desc", "children"),
Input("app-state", "data"),
)
def populate_landing_grid(app_state):
"""Populate the landing page grid with directorate/indication cards."""
if not app_state:
return [], "tc-landing__grid", "Select a directorate to compare drug usage across trusts."
from dash_app.data.queries import get_directorate_summary
chart_type = app_state.get("chart_type", "directory")
date_filter_id = app_state.get("date_filter_id", "all_6mo")
summaries = get_directorate_summary(date_filter_id, chart_type)
# Build card buttons
cards = []
for item in summaries:
name = item["name"]
patients = item["patients"]
drugs = item["drugs"]
cards.append(
html.Button(
className="tc-card",
id={"type": "tc-selector", "index": name},
n_clicks=0,
children=[
html.Div(name, className="tc-card__name"),
html.Div(
className="tc-card__stats",
children=[
html.Span(
f"{patients:,} patients",
className="tc-card__stat",
),
html.Span("\u00b7", className="tc-card__dot"),
html.Span(
f"{drugs} drugs",
className="tc-card__stat",
),
],
),
],
)
)
# Grid class: wider for indication mode (more items)
grid_cls = "tc-landing__grid"
if chart_type == "indication":
grid_cls += " tc-landing__grid--wide"
# Description text adapts to chart type
if chart_type == "indication":
desc = "Select an indication to compare drug usage across trusts."
else:
desc = "Select a directorate to compare drug usage across trusts."
return cards, grid_cls, desc
@app.callback(
Output("trust-comparison-landing", "style"),
Output("trust-comparison-dashboard", "style"),
Output("tc-dashboard-title", "children"),
Input("app-state", "data"),
)
def toggle_tc_subviews(app_state):
"""Toggle between landing page and 6-chart dashboard."""
if not app_state:
return {}, {"display": "none"}, ""
selected = app_state.get("selected_comparison_directorate")
show = {}
hide = {"display": "none"}
if selected:
chart_type = app_state.get("chart_type", "directory")
label = "Indication" if chart_type == "indication" else "Directorate"
title = f"{selected} \u2014 Trust Comparison"
return hide, show, title
else:
return show, hide, ""
# --- Trust Comparison dashboard charts (6 charts) ---
def _tc_empty(message):
"""Return a blank figure with a centered message for TC dashboard."""
fig = go.Figure()
fig.update_layout(
xaxis={"visible": False}, yaxis={"visible": False},
plot_bgcolor="rgba(0,0,0,0)", paper_bgcolor="rgba(0,0,0,0)",
margin={"t": 0, "l": 0, "r": 0, "b": 0}, height=300,
annotations=[{
"text": message, "xref": "paper", "yref": "paper",
"x": 0.5, "y": 0.5, "showarrow": False,
"font": {"size": 14, "color": "#768692", "family": "Source Sans 3"},
"xanchor": "center", "yanchor": "middle",
}],
)
return fig
def _tc_title(app_state):
"""Generate a short title suffix from global filter state."""
chart_type = (app_state or {}).get("chart_type", "directory")
label = "By Indication" if chart_type == "indication" else "By Directory"
initiated = (app_state or {}).get("initiated", "all")
last_seen = (app_state or {}).get("last_seen", "6mo")
i_labels = {"all": "All years", "1yr": "Last 1 yr", "2yr": "Last 2 yrs"}
l_labels = {"6mo": "6 mo", "12mo": "12 mo"}
return f"{label} | {i_labels.get(initiated, 'All')} / {l_labels.get(last_seen, '6 mo')}"
# 1. Market Share — drug breakdown per trust
@app.callback(
Output("tc-chart-market-share", "figure"),
Input("app-state", "data"),
prevent_initial_call=True,
)
def tc_market_share(app_state):
selected = (app_state or {}).get("selected_comparison_directorate")
if not selected:
return no_update
from dash_app.data.queries import get_trust_market_share
from visualization.plotly_generator import create_trust_market_share_figure
filter_id = app_state.get("date_filter_id", "all_6mo")
chart_type = app_state.get("chart_type", "directory")
try:
data = get_trust_market_share(filter_id, chart_type, selected)
except Exception:
return _tc_empty("Failed to load market share data.")
if not data:
return _tc_empty("No market share data for this selection.")
return create_trust_market_share_figure(data, _tc_title(app_state))
# 2. Cost Waterfall — cost per patient by trust
@app.callback(
Output("tc-chart-cost-waterfall", "figure"),
Input("app-state", "data"),
prevent_initial_call=True,
)
def tc_cost_waterfall(app_state):
selected = (app_state or {}).get("selected_comparison_directorate")
if not selected:
return no_update
from dash_app.data.queries import get_trust_cost_waterfall
from visualization.plotly_generator import create_cost_waterfall_figure
filter_id = app_state.get("date_filter_id", "all_6mo")
chart_type = app_state.get("chart_type", "directory")
try:
data = get_trust_cost_waterfall(filter_id, chart_type, selected)
except Exception:
return _tc_empty("Failed to load cost data.")
if not data:
return _tc_empty("No cost data for this selection.")
# Reuse existing waterfall figure — map trust_name to directory key
mapped = [{"directory": d["trust_name"], "patients": d["patients"],
"total_cost": d["total_cost"], "cost_pp": d["cost_pp"]} for d in data]
return create_cost_waterfall_figure(mapped, _tc_title(app_state), is_trust_comparison=True)
# 3. Dosing — drug dosing intervals by trust
@app.callback(
Output("tc-chart-dosing", "figure"),
Input("app-state", "data"),
prevent_initial_call=True,
)
def tc_dosing(app_state):
selected = (app_state or {}).get("selected_comparison_directorate")
if not selected:
return no_update
from dash_app.data.queries import get_trust_dosing
from visualization.plotly_generator import create_dosing_figure
filter_id = app_state.get("date_filter_id", "all_6mo")
chart_type = app_state.get("chart_type", "directory")
try:
data = get_trust_dosing(filter_id, chart_type, selected)
except Exception:
return _tc_empty("Failed to load dosing data.")
if not data:
return _tc_empty("No dosing data for this selection.")
# Add directory field expected by _dosing_by_trust helper
for d in data:
d["directory"] = selected
return create_dosing_figure(data, _tc_title(app_state), group_by="trust")
# 4. Heatmap — trust x drug matrix
@app.callback(
Output("tc-chart-heatmap", "figure"),
Input("app-state", "data"),
Input("tc-heatmap-metric-toggle", "value"),
prevent_initial_call=True,
)
def tc_heatmap(app_state, heatmap_metric):
selected = (app_state or {}).get("selected_comparison_directorate")
if not selected:
return no_update
from dash_app.data.queries import get_trust_heatmap
from visualization.plotly_generator import create_trust_heatmap_figure
filter_id = app_state.get("date_filter_id", "all_6mo")
chart_type = app_state.get("chart_type", "directory")
metric = heatmap_metric or "patients"
try:
data = get_trust_heatmap(filter_id, chart_type, selected)
except Exception:
return _tc_empty("Failed to load heatmap data.")
if not data.get("trusts") or not data.get("drugs"):
return _tc_empty("No heatmap data for this selection.")
return create_trust_heatmap_figure(data, _tc_title(app_state), metric=metric)
# 5. Duration — drug durations by trust
@app.callback(
Output("tc-chart-duration", "figure"),
Input("app-state", "data"),
prevent_initial_call=True,
)
def tc_duration(app_state):
selected = (app_state or {}).get("selected_comparison_directorate")
if not selected:
return no_update
from dash_app.data.queries import get_trust_durations
from visualization.plotly_generator import create_trust_duration_figure
filter_id = app_state.get("date_filter_id", "all_6mo")
chart_type = app_state.get("chart_type", "directory")
try:
data = get_trust_durations(filter_id, chart_type, selected)
except Exception:
return _tc_empty("Failed to load duration data.")
if not data:
return _tc_empty("No duration data for this selection.")
return create_trust_duration_figure(data, _tc_title(app_state))
# 6. Cost Effectiveness — pathway costs within directorate (NOT split by trust)
@app.callback(
Output("tc-chart-cost-effectiveness", "figure"),
Input("app-state", "data"),
prevent_initial_call=True,
)
def tc_cost_effectiveness(app_state):
selected = (app_state or {}).get("selected_comparison_directorate")
if not selected:
return no_update
from dash_app.data.queries import get_pathway_costs
from data_processing.parsing import calculate_retention_rate
from visualization.plotly_generator import create_cost_effectiveness_figure
filter_id = app_state.get("date_filter_id", "all_6mo")
chart_type = app_state.get("chart_type", "directory")
try:
data = get_pathway_costs(filter_id, chart_type, directory=selected)
except Exception:
return _tc_empty("Failed to load pathway cost data.")
if not data:
return _tc_empty("No pathway cost data for this selection.")
retention = calculate_retention_rate(data)
return create_cost_effectiveness_figure(data, retention, _tc_title(app_state))