feat: add Trends sidebar nav item + 3-view switching (Task E.2)

This commit is contained in:
Andrew Charlwood
2026-02-07 22:12:13 +00:00
parent 956ad8ca7b
commit 190aefdf56
5 changed files with 40 additions and 17 deletions
+8 -8
View File
@@ -265,16 +265,16 @@ Comprehensive review and improvement of all Plotly charts in the Dash dashboard.
- **Checkpoint**: Patient Pathways has 9 tabs (Icicle through Doses, no Trends). `python run_dash.py` starts cleanly. PASSED. - **Checkpoint**: Patient Pathways has 9 tabs (Icicle through Doses, no Trends). `python run_dash.py` starts cleanly. PASSED.
### E.2 Add Trends sidebar nav item + view container ### E.2 Add Trends sidebar nav item + view container
- [ ] Add `"trends"` icon SVG to `_ICONS` dict in `dash_app/components/sidebar.py` — use a line chart icon: `<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>` - [x] Add `"trends"` icon SVG to `_ICONS` dict in `dash_app/components/sidebar.py` — use a line chart icon: `<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>`
- [ ] Add `_sidebar_item("Trends", "trends", active=False, item_id="nav-trends")` to sidebar children - [x] Add `_sidebar_item("Trends", "trends", active=False, item_id="nav-trends")` to sidebar children
- [ ] Add `html.Div(id="trends-view", style={"display": "none"}, children=[...])` to `app.py` layout inside `view-container`, after `trust-comparison-view` - [x] Add `html.Div(id="trends-view", style={"display": "none"}, children=[...])` to `app.py` layout inside `view-container`, after `trust-comparison-view`
- [ ] Update `switch_view()` in `dash_app/callbacks/navigation.py`: - [x] Update `switch_view()` in `dash_app/callbacks/navigation.py`:
- Add `Output("trends-view", "style")` and `Output("nav-trends", "className")` — now 3 views, 3 nav items (6 outputs total) - Add `Output("trends-view", "style")` and `Output("nav-trends", "className")` — now 3 views, 3 nav items (6 outputs total)
- Handle 3-way switching: `"patient-pathways"`, `"trust-comparison"`, `"trends"` - Handle 3-way switching: `"patient-pathways"`, `"trust-comparison"`, `"trends"`
- [ ] Update `update_app_state()` in `dash_app/callbacks/filters.py`: - [x] Update `update_app_state()` in `dash_app/callbacks/filters.py`:
- Add `Input("nav-trends", "n_clicks")` - Add `Input("nav-trends", "n_clicks")`
- Add `elif triggered_id == "nav-trends": active_view = "trends"` case - Add `elif triggered_id == "nav-trends": active_view = "trends"` case
- **Checkpoint**: 3 sidebar items visible. Clicking "Trends" switches to empty trends view. `python run_dash.py` starts cleanly. - **Checkpoint**: 3 sidebar items visible. Clicking "Trends" switches to empty trends view. `python run_dash.py` starts cleanly. PASSED.
### E.3 Create Trends landing page — directorate-level trends ### E.3 Create Trends landing page — directorate-level trends
- [ ] Create `dash_app/components/trends.py`: - [ ] Create `dash_app/components/trends.py`:
@@ -354,8 +354,8 @@ Comprehensive review and improvement of all Plotly charts in the Dash dashboard.
- [x] `python run_dash.py` starts cleanly - [x] `python run_dash.py` starts cleanly
### Phase E ### Phase E
- [ ] Trends tab removed from Patient Pathways (9 tabs remain) - [x] Trends tab removed from Patient Pathways (9 tabs remain)
- [ ] 3rd sidebar item "Trends" visible and functional - [x] 3rd sidebar item "Trends" visible and functional
- [ ] Trends landing page shows directorate-level line chart with metric toggle - [ ] Trends landing page shows directorate-level line chart with metric toggle
- [ ] Clicking a directorate drills into drug-level trends - [ ] Clicking a directorate drills into drug-level trends
- [ ] Back button returns to directorate overview - [ ] Back button returns to directorate overview
+9
View File
@@ -29,6 +29,7 @@ app.layout = dmc.MantineProvider(
"selected_trusts": [], "selected_trusts": [],
"active_view": "patient-pathways", "active_view": "patient-pathways",
"selected_comparison_directorate": None, "selected_comparison_directorate": None,
"selected_trends_directorate": None,
}), }),
dcc.Store(id="chart-data", storage_type="memory"), dcc.Store(id="chart-data", storage_type="memory"),
dcc.Store(id="reference-data", storage_type="session"), dcc.Store(id="reference-data", storage_type="session"),
@@ -64,6 +65,14 @@ app.layout = dmc.MantineProvider(
make_tc_dashboard(), make_tc_dashboard(),
], ],
), ),
# Trends view (hidden initially)
html.Div(
id="trends-view",
style={"display": "none"},
children=[
html.H3("Trends", style={"padding": "24px"}),
],
),
], ],
), ),
make_footer(), make_footer(),
+5 -1
View File
@@ -75,13 +75,14 @@ def register_filter_callbacks(app):
Input("trust-chips", "value"), Input("trust-chips", "value"),
Input("nav-patient-pathways", "n_clicks"), Input("nav-patient-pathways", "n_clicks"),
Input("nav-trust-comparison", "n_clicks"), Input("nav-trust-comparison", "n_clicks"),
Input("nav-trends", "n_clicks"),
Input({"type": "tc-selector", "index": ALL}, "n_clicks"), Input({"type": "tc-selector", "index": ALL}, "n_clicks"),
Input("tc-back-btn", "n_clicks"), Input("tc-back-btn", "n_clicks"),
State("app-state", "data"), State("app-state", "data"),
) )
def update_app_state( def update_app_state(
_dir_clicks, _ind_clicks, initiated, last_seen, selected_drugs, _dir_clicks, _ind_clicks, initiated, last_seen, selected_drugs,
selected_trusts, _nav_pp_clicks, _nav_tc_clicks, selected_trusts, _nav_pp_clicks, _nav_tc_clicks, _nav_trends_clicks,
_tc_selector_clicks, _tc_back_clicks, current_state _tc_selector_clicks, _tc_back_clicks, current_state
): ):
"""Update app-state when any filter, nav, or TC selector changes.""" """Update app-state when any filter, nav, or TC selector changes."""
@@ -96,6 +97,7 @@ def register_filter_callbacks(app):
"selected_trusts": [], "selected_trusts": [],
"active_view": "patient-pathways", "active_view": "patient-pathways",
"selected_comparison_directorate": None, "selected_comparison_directorate": None,
"selected_trends_directorate": None,
} }
triggered_id = ctx.triggered_id triggered_id = ctx.triggered_id
@@ -114,6 +116,8 @@ def register_filter_callbacks(app):
active_view = "patient-pathways" active_view = "patient-pathways"
elif triggered_id == "nav-trust-comparison": elif triggered_id == "nav-trust-comparison":
active_view = "trust-comparison" active_view = "trust-comparison"
elif triggered_id == "nav-trends":
active_view = "trends"
# Trust Comparison directorate selection # Trust Comparison directorate selection
selected_comparison_directorate = current_state.get("selected_comparison_directorate") selected_comparison_directorate = current_state.get("selected_comparison_directorate")
+13 -8
View File
@@ -1,4 +1,4 @@
"""Callbacks for view switching between Patient Pathways and Trust Comparison.""" """Callbacks for view switching between Patient Pathways, Trust Comparison, and Trends."""
from dash import Input, Output from dash import Input, Output
@@ -8,22 +8,27 @@ def register_navigation_callbacks(app):
@app.callback( @app.callback(
Output("patient-pathways-view", "style"), Output("patient-pathways-view", "style"),
Output("trust-comparison-view", "style"), Output("trust-comparison-view", "style"),
Output("trends-view", "style"),
Output("nav-patient-pathways", "className"), Output("nav-patient-pathways", "className"),
Output("nav-trust-comparison", "className"), Output("nav-trust-comparison", "className"),
Output("nav-trends", "className"),
Input("app-state", "data"), Input("app-state", "data"),
) )
def switch_view(app_state): def switch_view(app_state):
"""Show/hide views and update sidebar active state based on active_view.""" """Show/hide views and update sidebar active state based on active_view."""
if not app_state:
return {}, {"display": "none"}, "sidebar__item sidebar__item--active", "sidebar__item"
view = app_state.get("active_view", "patient-pathways")
show = {} show = {}
hide = {"display": "none"} hide = {"display": "none"}
active_cls = "sidebar__item sidebar__item--active" active_cls = "sidebar__item sidebar__item--active"
inactive_cls = "sidebar__item" inactive_cls = "sidebar__item"
if not app_state:
return show, hide, hide, active_cls, inactive_cls, inactive_cls
view = app_state.get("active_view", "patient-pathways")
if view == "patient-pathways": if view == "patient-pathways":
return show, hide, active_cls, inactive_cls return show, hide, hide, active_cls, inactive_cls, inactive_cls
else: elif view == "trust-comparison":
return hide, show, inactive_cls, active_cls return hide, show, hide, inactive_cls, active_cls, inactive_cls
else: # trends
return hide, hide, show, inactive_cls, inactive_cls, active_cls
+5
View File
@@ -20,6 +20,7 @@ def _svg_icon(svg_body):
_ICONS = { _ICONS = {
"pathway": '<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>', "pathway": '<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>',
"compare": '<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>', "compare": '<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>',
"trends": '<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>',
} }
@@ -41,6 +42,10 @@ def make_sidebar():
"Trust Comparison", "compare", "Trust Comparison", "compare",
active=False, item_id="nav-trust-comparison", active=False, item_id="nav-trust-comparison",
), ),
_sidebar_item(
"Trends", "trends",
active=False, item_id="nav-trends",
),
], ],
), ),
html.Div( html.Div(