diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index e324f8b..ef8839a 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -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 ### 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. - ID: `heatmap-metric-toggle` - - Add to `dash_app/components/chart_card.py` (visible only when heatmap tab active) -- [ ] Add `dmc.SegmentedControl` next to Trust Comparison heatmap: + - Added to `dash_app/components/chart_card.py` in header, hidden by default, shown when heatmap tab active + - 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` - - Add to `dash_app/components/trust_comparison.py` -- [ ] Update `_render_heatmap()` in `dash_app/callbacks/chart.py` (~L239) to read metric toggle value -- [ ] Update `tc_heatmap` callback in `dash_app/callbacks/trust_comparison.py` (~L214) to read metric toggle value + - Added to `dash_app/components/trust_comparison.py` inline in heatmap chart cell header +- [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 +- [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 --- diff --git a/dash_app/callbacks/chart.py b/dash_app/callbacks/chart.py index a85c2d9..e3f795f 100644 --- a/dash_app/callbacks/chart.py +++ b/dash_app/callbacks/chart.py @@ -216,7 +216,7 @@ def _render_dosing(app_state, title): 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.""" from dash_app.data.queries import get_drug_directory_matrix 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"): 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): @@ -345,32 +345,37 @@ def register_chart_callbacks(app): @app.callback( Output("pathway-chart", "figure"), Output("chart-subtitle", "children"), + Output("heatmap-metric-wrapper", "style"), Input("chart-data", "data"), Input("active-tab", "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.""" 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"} + if chart_type == "indication": subtitle = "Trust \u2192 Indication \u2192 Drug \u2192 Patient Pathway" else: subtitle = "Trust \u2192 Directorate \u2192 Drug \u2192 Patient Pathway" if not chart_data: - return no_update, no_update + return no_update, no_update, toggle_style error_msg = chart_data.get("error") if error_msg: - return _empty_figure(error_msg), subtitle + return _empty_figure(error_msg), subtitle, toggle_style if not chart_data.get("nodes"): return _empty_figure( "No matching pathways found.\n" "Try adjusting your filters." - ), subtitle + ), subtitle, toggle_style # Lazy rendering — only compute the active tab's chart 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) 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": 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) fig = _empty_figure(f"{tab_label} chart — coming soon") - return fig, subtitle + return fig, subtitle, toggle_style diff --git a/dash_app/callbacks/trust_comparison.py b/dash_app/callbacks/trust_comparison.py index 7e94a53..8372409 100644 --- a/dash_app/callbacks/trust_comparison.py +++ b/dash_app/callbacks/trust_comparison.py @@ -195,9 +195,10 @@ def register_trust_comparison_callbacks(app): @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): + def tc_heatmap(app_state, heatmap_metric): selected = (app_state or {}).get("selected_comparison_directorate") if not selected: return no_update @@ -205,13 +206,14 @@ def register_trust_comparison_callbacks(app): 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)) + return create_trust_heatmap_figure(data, _tc_title(app_state), metric=metric) # 5. Duration — drug durations by trust @app.callback( diff --git a/dash_app/components/chart_card.py b/dash_app/components/chart_card.py index cbbc2ab..51d65b3 100644 --- a/dash_app/components/chart_card.py +++ b/dash_app/components/chart_card.py @@ -1,11 +1,13 @@ """Chart card component — tab bar, header, and dcc.Graph for charts.""" 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 = [ ("icicle", "Icicle"), ("sankey", "Sankey"), + ("heatmap", "Heatmap"), ] # 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 diff --git a/dash_app/components/trust_comparison.py b/dash_app/components/trust_comparison.py index ea5129e..0b973f6 100644 --- a/dash_app/components/trust_comparison.py +++ b/dash_app/components/trust_comparison.py @@ -1,5 +1,6 @@ """Trust Comparison view — landing page (directorate selector) + 6-chart dashboard.""" from dash import html, dcc +import dash_mantine_components as dmc 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("Cost Waterfall", "tc-chart-cost-waterfall"), _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("Cost Effectiveness", "tc-chart-cost-effectiveness"), ],