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
This commit is contained in:
@@ -115,15 +115,16 @@ Comprehensive review and improvement of all Plotly charts in the Dash dashboard.
|
|||||||
- **Checkpoint**: Sankey nodes don't overlap on narrow viewports
|
- **Checkpoint**: Sankey nodes don't overlap on narrow viewports
|
||||||
|
|
||||||
### B.4 Heatmap metric toggle (both views)
|
### B.4 Heatmap metric toggle (both views)
|
||||||
- [ ] Add `dmc.SegmentedControl` component next to Patient Pathways heatmap:
|
- [x] Add `dmc.SegmentedControl` component next to Patient Pathways heatmap:
|
||||||
- Options: Patients, Cost, Cost p.a.
|
- Options: Patients, Cost, Cost p.a.
|
||||||
- ID: `heatmap-metric-toggle`
|
- ID: `heatmap-metric-toggle`
|
||||||
- Add to `dash_app/components/chart_card.py` (visible only when heatmap tab active)
|
- Added to `dash_app/components/chart_card.py` in header, hidden by default, shown when heatmap tab active
|
||||||
- [ ] Add `dmc.SegmentedControl` next to Trust Comparison heatmap:
|
- Also added "heatmap" tab to TAB_DEFINITIONS (was only in ALL_TAB_DEFINITIONS before)
|
||||||
|
- [x] Add `dmc.SegmentedControl` next to Trust Comparison heatmap:
|
||||||
- ID: `tc-heatmap-metric-toggle`
|
- ID: `tc-heatmap-metric-toggle`
|
||||||
- Add to `dash_app/components/trust_comparison.py`
|
- Added to `dash_app/components/trust_comparison.py` inline in heatmap chart cell header
|
||||||
- [ ] Update `_render_heatmap()` in `dash_app/callbacks/chart.py` (~L239) to read metric toggle value
|
- [x] Update `_render_heatmap()` in `dash_app/callbacks/chart.py` to accept metric param, `update_chart` passes toggle value + controls toggle visibility via `heatmap-metric-wrapper` style output
|
||||||
- [ ] Update `tc_heatmap` callback in `dash_app/callbacks/trust_comparison.py` (~L214) to read metric toggle value
|
- [x] Update `tc_heatmap` callback in `dash_app/callbacks/trust_comparison.py` to read `tc-heatmap-metric-toggle` value and pass to `create_trust_heatmap_figure()`
|
||||||
- **Checkpoint**: Heatmap metric toggles work in both views, switching between patients/cost/cost_pp_pa
|
- **Checkpoint**: Heatmap metric toggles work in both views, switching between patients/cost/cost_pp_pa
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ def _render_dosing(app_state, title):
|
|||||||
return create_dosing_figure(data, title, group_by)
|
return create_dosing_figure(data, title, group_by)
|
||||||
|
|
||||||
|
|
||||||
def _render_heatmap(app_state, title):
|
def _render_heatmap(app_state, title, metric="patients"):
|
||||||
"""Build the directorate × drug heatmap from current filter state."""
|
"""Build the directorate × drug heatmap from current filter state."""
|
||||||
from dash_app.data.queries import get_drug_directory_matrix
|
from dash_app.data.queries import get_drug_directory_matrix
|
||||||
from visualization.plotly_generator import create_heatmap_figure
|
from visualization.plotly_generator import create_heatmap_figure
|
||||||
@@ -236,7 +236,7 @@ def _render_heatmap(app_state, title):
|
|||||||
if not data.get("directories") or not data.get("drugs"):
|
if not data.get("directories") or not data.get("drugs"):
|
||||||
return _empty_figure("No heatmap data available.\nTry adjusting your filters.")
|
return _empty_figure("No heatmap data available.\nTry adjusting your filters.")
|
||||||
|
|
||||||
return create_heatmap_figure(data, title, metric="patients")
|
return create_heatmap_figure(data, title, metric=metric)
|
||||||
|
|
||||||
|
|
||||||
def _render_duration(app_state, title):
|
def _render_duration(app_state, title):
|
||||||
@@ -345,32 +345,37 @@ def register_chart_callbacks(app):
|
|||||||
@app.callback(
|
@app.callback(
|
||||||
Output("pathway-chart", "figure"),
|
Output("pathway-chart", "figure"),
|
||||||
Output("chart-subtitle", "children"),
|
Output("chart-subtitle", "children"),
|
||||||
|
Output("heatmap-metric-wrapper", "style"),
|
||||||
Input("chart-data", "data"),
|
Input("chart-data", "data"),
|
||||||
Input("active-tab", "data"),
|
Input("active-tab", "data"),
|
||||||
Input("app-state", "data"),
|
Input("app-state", "data"),
|
||||||
|
Input("heatmap-metric-toggle", "value"),
|
||||||
)
|
)
|
||||||
def update_chart(chart_data, active_tab, app_state):
|
def update_chart(chart_data, active_tab, app_state, heatmap_metric):
|
||||||
"""Render the active tab's chart from chart-data nodes."""
|
"""Render the active tab's chart from chart-data nodes."""
|
||||||
active_tab = active_tab or "icicle"
|
active_tab = active_tab or "icicle"
|
||||||
chart_type = (app_state or {}).get("chart_type", "directory")
|
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"}
|
||||||
|
|
||||||
if chart_type == "indication":
|
if chart_type == "indication":
|
||||||
subtitle = "Trust \u2192 Indication \u2192 Drug \u2192 Patient Pathway"
|
subtitle = "Trust \u2192 Indication \u2192 Drug \u2192 Patient Pathway"
|
||||||
else:
|
else:
|
||||||
subtitle = "Trust \u2192 Directorate \u2192 Drug \u2192 Patient Pathway"
|
subtitle = "Trust \u2192 Directorate \u2192 Drug \u2192 Patient Pathway"
|
||||||
|
|
||||||
if not chart_data:
|
if not chart_data:
|
||||||
return no_update, no_update
|
return no_update, no_update, toggle_style
|
||||||
|
|
||||||
error_msg = chart_data.get("error")
|
error_msg = chart_data.get("error")
|
||||||
if error_msg:
|
if error_msg:
|
||||||
return _empty_figure(error_msg), subtitle
|
return _empty_figure(error_msg), subtitle, toggle_style
|
||||||
|
|
||||||
if not chart_data.get("nodes"):
|
if not chart_data.get("nodes"):
|
||||||
return _empty_figure(
|
return _empty_figure(
|
||||||
"No matching pathways found.\n"
|
"No matching pathways found.\n"
|
||||||
"Try adjusting your filters."
|
"Try adjusting your filters."
|
||||||
), subtitle
|
), subtitle, toggle_style
|
||||||
|
|
||||||
# Lazy rendering — only compute the active tab's chart
|
# Lazy rendering — only compute the active tab's chart
|
||||||
title = _generate_chart_title(app_state) if app_state else ""
|
title = _generate_chart_title(app_state) if app_state else ""
|
||||||
@@ -396,7 +401,8 @@ def register_chart_callbacks(app):
|
|||||||
fig = _render_dosing(app_state, title)
|
fig = _render_dosing(app_state, title)
|
||||||
|
|
||||||
elif active_tab == "heatmap":
|
elif active_tab == "heatmap":
|
||||||
fig = _render_heatmap(app_state, title)
|
metric = heatmap_metric or "patients"
|
||||||
|
fig = _render_heatmap(app_state, title, metric=metric)
|
||||||
|
|
||||||
elif active_tab == "duration":
|
elif active_tab == "duration":
|
||||||
fig = _render_duration(app_state, title)
|
fig = _render_duration(app_state, title)
|
||||||
@@ -406,4 +412,4 @@ def register_chart_callbacks(app):
|
|||||||
tab_label = dict(TAB_DEFINITIONS).get(active_tab, active_tab)
|
tab_label = dict(TAB_DEFINITIONS).get(active_tab, active_tab)
|
||||||
fig = _empty_figure(f"{tab_label} chart — coming soon")
|
fig = _empty_figure(f"{tab_label} chart — coming soon")
|
||||||
|
|
||||||
return fig, subtitle
|
return fig, subtitle, toggle_style
|
||||||
|
|||||||
@@ -195,9 +195,10 @@ def register_trust_comparison_callbacks(app):
|
|||||||
@app.callback(
|
@app.callback(
|
||||||
Output("tc-chart-heatmap", "figure"),
|
Output("tc-chart-heatmap", "figure"),
|
||||||
Input("app-state", "data"),
|
Input("app-state", "data"),
|
||||||
|
Input("tc-heatmap-metric-toggle", "value"),
|
||||||
prevent_initial_call=True,
|
prevent_initial_call=True,
|
||||||
)
|
)
|
||||||
def tc_heatmap(app_state):
|
def tc_heatmap(app_state, heatmap_metric):
|
||||||
selected = (app_state or {}).get("selected_comparison_directorate")
|
selected = (app_state or {}).get("selected_comparison_directorate")
|
||||||
if not selected:
|
if not selected:
|
||||||
return no_update
|
return no_update
|
||||||
@@ -205,13 +206,14 @@ def register_trust_comparison_callbacks(app):
|
|||||||
from visualization.plotly_generator import create_trust_heatmap_figure
|
from visualization.plotly_generator import create_trust_heatmap_figure
|
||||||
filter_id = app_state.get("date_filter_id", "all_6mo")
|
filter_id = app_state.get("date_filter_id", "all_6mo")
|
||||||
chart_type = app_state.get("chart_type", "directory")
|
chart_type = app_state.get("chart_type", "directory")
|
||||||
|
metric = heatmap_metric or "patients"
|
||||||
try:
|
try:
|
||||||
data = get_trust_heatmap(filter_id, chart_type, selected)
|
data = get_trust_heatmap(filter_id, chart_type, selected)
|
||||||
except Exception:
|
except Exception:
|
||||||
return _tc_empty("Failed to load heatmap data.")
|
return _tc_empty("Failed to load heatmap data.")
|
||||||
if not data.get("trusts") or not data.get("drugs"):
|
if not data.get("trusts") or not data.get("drugs"):
|
||||||
return _tc_empty("No heatmap data for this selection.")
|
return _tc_empty("No heatmap data for this selection.")
|
||||||
return create_trust_heatmap_figure(data, _tc_title(app_state))
|
return create_trust_heatmap_figure(data, _tc_title(app_state), metric=metric)
|
||||||
|
|
||||||
# 5. Duration — drug durations by trust
|
# 5. Duration — drug durations by trust
|
||||||
@app.callback(
|
@app.callback(
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
"""Chart card component — tab bar, header, and dcc.Graph for charts."""
|
"""Chart card component — tab bar, header, and dcc.Graph for charts."""
|
||||||
from dash import html, dcc
|
from dash import html, dcc
|
||||||
|
import dash_mantine_components as dmc
|
||||||
|
|
||||||
|
|
||||||
# Patient Pathways view: only Icicle + Sankey
|
# Patient Pathways view: Icicle, Sankey, Heatmap
|
||||||
TAB_DEFINITIONS = [
|
TAB_DEFINITIONS = [
|
||||||
("icicle", "Icicle"),
|
("icicle", "Icicle"),
|
||||||
("sankey", "Sankey"),
|
("sankey", "Sankey"),
|
||||||
|
("heatmap", "Heatmap"),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Full set retained for Trust Comparison dashboard (Phase 10.8)
|
# Full set retained for Trust Comparison dashboard (Phase 10.8)
|
||||||
@@ -69,6 +71,23 @@ def make_chart_card():
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
# Heatmap metric toggle — visible only when heatmap tab active
|
||||||
|
html.Div(
|
||||||
|
id="heatmap-metric-wrapper",
|
||||||
|
style={"display": "none"},
|
||||||
|
children=[
|
||||||
|
dmc.SegmentedControl(
|
||||||
|
id="heatmap-metric-toggle",
|
||||||
|
data=[
|
||||||
|
{"value": "patients", "label": "Patients"},
|
||||||
|
{"value": "cost", "label": "Cost"},
|
||||||
|
{"value": "cost_pp_pa", "label": "Cost p.a."},
|
||||||
|
],
|
||||||
|
value="patients",
|
||||||
|
size="xs",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
# Chart area with loading spinner
|
# Chart area with loading spinner
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""Trust Comparison view — landing page (directorate selector) + 6-chart dashboard."""
|
"""Trust Comparison view — landing page (directorate selector) + 6-chart dashboard."""
|
||||||
from dash import html, dcc
|
from dash import html, dcc
|
||||||
|
import dash_mantine_components as dmc
|
||||||
|
|
||||||
|
|
||||||
def _tc_chart_cell(title, graph_id):
|
def _tc_chart_cell(title, graph_id):
|
||||||
@@ -71,7 +72,34 @@ def make_tc_dashboard():
|
|||||||
_tc_chart_cell("Market Share", "tc-chart-market-share"),
|
_tc_chart_cell("Market Share", "tc-chart-market-share"),
|
||||||
_tc_chart_cell("Cost Waterfall", "tc-chart-cost-waterfall"),
|
_tc_chart_cell("Cost Waterfall", "tc-chart-cost-waterfall"),
|
||||||
_tc_chart_cell("Dosing Intervals", "tc-chart-dosing"),
|
_tc_chart_cell("Dosing Intervals", "tc-chart-dosing"),
|
||||||
_tc_chart_cell("Drug \u00d7 Trust Heatmap", "tc-chart-heatmap"),
|
html.Div(className="tc-chart-cell", children=[
|
||||||
|
html.Div(
|
||||||
|
className="tc-chart-cell__title-row",
|
||||||
|
style={"display": "flex", "alignItems": "center",
|
||||||
|
"justifyContent": "space-between", "gap": "8px"},
|
||||||
|
children=[
|
||||||
|
html.Div("Drug \u00d7 Trust Heatmap",
|
||||||
|
className="tc-chart-cell__title"),
|
||||||
|
dmc.SegmentedControl(
|
||||||
|
id="tc-heatmap-metric-toggle",
|
||||||
|
data=[
|
||||||
|
{"value": "patients", "label": "Patients"},
|
||||||
|
{"value": "cost", "label": "Cost"},
|
||||||
|
{"value": "cost_pp_pa", "label": "Cost p.a."},
|
||||||
|
],
|
||||||
|
value="patients",
|
||||||
|
size="xs",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
dcc.Loading(type="circle", color="#005EB8", children=[
|
||||||
|
dcc.Graph(
|
||||||
|
id="tc-chart-heatmap",
|
||||||
|
config={"displayModeBar": False, "displaylogo": False},
|
||||||
|
style={"height": "500px"},
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
_tc_chart_cell("Treatment Duration", "tc-chart-duration"),
|
_tc_chart_cell("Treatment Duration", "tc-chart-duration"),
|
||||||
_tc_chart_cell("Cost Effectiveness", "tc-chart-cost-effectiveness"),
|
_tc_chart_cell("Cost Effectiveness", "tc-chart-cost-effectiveness"),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user