From 7d51efc25e3e5581593f36d28da76c354201bf1c Mon Sep 17 00:00:00 2001 From: Andrew Charlwood Date: Fri, 6 Feb 2026 21:38:12 +0000 Subject: [PATCH] feat: add two-view architecture with sidebar navigation (Task 10.2) - Add active_view and selected_comparison_directorate to app-state - Sidebar: rename to Patient Pathways + add Trust Comparison nav item - View container pattern: two view divs toggled by active_view - Navigation callback: sidebar clicks switch views + update active state - Trust Comparison placeholder landing page with tc-landing structure --- IMPLEMENTATION_PLAN.md | 14 ++++----- dash_app/app.py | 54 ++++++++++++++++++++++++++++++-- dash_app/callbacks/__init__.py | 2 ++ dash_app/callbacks/filters.py | 22 ++++++++++--- dash_app/callbacks/navigation.py | 29 +++++++++++++++++ dash_app/components/sidebar.py | 14 ++++++--- 6 files changed, 116 insertions(+), 19 deletions(-) create mode 100644 dash_app/callbacks/navigation.py diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 507b1aa..b0d1684 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -469,16 +469,16 @@ Additionally: KPI row removed, fraction KPIs moved to header, global filter sub- - **Checkpoint**: Design mockups/specifications ready for all 5 areas above ### 10.2 State management + sidebar restructure -- [ ] Add `active_view` to `app-state`: `"patient-pathways"` (default) or `"trust-comparison"` -- [ ] Add `selected_comparison_directorate` to `app-state`: `null` (landing page) or directorate name -- [ ] Update `dash_app/components/sidebar.py`: +- [x] Add `active_view` to `app-state`: `"patient-pathways"` (default) or `"trust-comparison"` +- [x] Add `selected_comparison_directorate` to `app-state`: `null` (landing page) or directorate name +- [x] Update `dash_app/components/sidebar.py`: - Rename "Pathway Overview" → "Patient Pathways" - Add "Trust Comparison" nav item below it - Active state tracks `active_view` -- [ ] Add callback: sidebar clicks → update `active_view` in app-state -- [ ] Main content area switches between Patient Pathways view and Trust Comparison view based on `active_view` -- [ ] Date filter + chart type toggle remain in global sub-header (visible in both views) -- **Checkpoint**: Sidebar switches between two views, active state highlights correctly, app starts without errors +- [x] Add callback: sidebar clicks → update `active_view` in app-state +- [x] Main content area switches between Patient Pathways view and Trust Comparison view based on `active_view` +- [x] Date filter + chart type toggle remain in global sub-header (visible in both views) +- **Checkpoint**: Sidebar switches between two views, active state highlights correctly, app starts without errors ✓ ### 10.3 Header redesign — remove KPI row, add fraction KPIs - [ ] Remove `dash_app/components/kpi_row.py` (or gut it) diff --git a/dash_app/app.py b/dash_app/app.py index e2947a2..6864824 100644 --- a/dash_app/app.py +++ b/dash_app/app.py @@ -26,6 +26,8 @@ app.layout = dmc.MantineProvider( "selected_drugs": [], "selected_directorates": [], "selected_trusts": [], + "active_view": "patient-pathways", + "selected_comparison_directorate": None, }), dcc.Store(id="chart-data", storage_type="memory"), dcc.Store(id="reference-data", storage_type="session"), @@ -39,9 +41,55 @@ app.layout = dmc.MantineProvider( html.Main( className="main", children=[ - make_kpi_row(), - make_filter_bar(), - make_chart_card(), + # View container — switched by active_view in app-state + html.Div( + id="view-container", + children=[ + # Patient Pathways view (default, visible) + html.Div( + id="patient-pathways-view", + children=[ + make_kpi_row(), + make_filter_bar(), + make_chart_card(), + ], + ), + # Trust Comparison view (hidden initially) + html.Div( + id="trust-comparison-view", + style={"display": "none"}, + children=[ + html.Div( + className="tc-landing", + id="trust-comparison-landing", + children=[ + html.Div( + className="tc-landing__header", + children=[ + html.H2( + "Trust Comparison", + className="tc-landing__title", + ), + html.P( + "Select a directorate to compare drug usage across trusts.", + className="tc-landing__desc", + id="tc-landing-desc", + ), + ], + ), + html.Div( + className="tc-landing__grid", + id="tc-landing-grid", + children=[ + # Populated by callback in later task + ], + ), + ], + ), + ], + ), + ], + ), make_footer(), ], ), diff --git a/dash_app/callbacks/__init__.py b/dash_app/callbacks/__init__.py index 25e4d32..f53c441 100644 --- a/dash_app/callbacks/__init__.py +++ b/dash_app/callbacks/__init__.py @@ -7,8 +7,10 @@ def register_callbacks(app): from dash_app.callbacks.chart import register_chart_callbacks from dash_app.callbacks.kpi import register_kpi_callbacks from dash_app.callbacks.modals import register_modal_callbacks + from dash_app.callbacks.navigation import register_navigation_callbacks register_filter_callbacks(app) register_chart_callbacks(app) register_kpi_callbacks(app) register_modal_callbacks(app) + register_navigation_callbacks(app) diff --git a/dash_app/callbacks/filters.py b/dash_app/callbacks/filters.py index 8352212..bb26649 100644 --- a/dash_app/callbacks/filters.py +++ b/dash_app/callbacks/filters.py @@ -73,13 +73,15 @@ def register_filter_callbacks(app): Input("filter-last-seen", "value"), Input("all-drugs-chips", "value"), Input("trust-chips", "value"), + Input("nav-patient-pathways", "n_clicks"), + Input("nav-trust-comparison", "n_clicks"), State("app-state", "data"), ) def update_app_state( _dir_clicks, _ind_clicks, initiated, last_seen, selected_drugs, - selected_trusts, current_state + selected_trusts, _nav_pp_clicks, _nav_tc_clicks, current_state ): - """Update app-state when chart type toggle, date filters, drug chips, or trust chips change.""" + """Update app-state when chart type toggle, date filters, drug/trust chips, or sidebar nav change.""" if not current_state: current_state = { "chart_type": "directory", @@ -89,6 +91,8 @@ def register_filter_callbacks(app): "selected_drugs": [], "selected_directorates": [], "selected_trusts": [], + "active_view": "patient-pathways", + "selected_comparison_directorate": None, } triggered_id = ctx.triggered_id @@ -100,6 +104,13 @@ def register_filter_callbacks(app): elif triggered_id == "chart-type-indication": chart_type = "indication" + # Determine active view from sidebar nav + active_view = current_state.get("active_view", "patient-pathways") + if triggered_id == "nav-patient-pathways": + active_view = "patient-pathways" + elif triggered_id == "nav-trust-comparison": + active_view = "trust-comparison" + # Compute date_filter_id from dropdown values date_filter_id = f"{initiated}_{last_seen}" @@ -112,12 +123,13 @@ def register_filter_callbacks(app): "date_filter_id": date_filter_id, "selected_drugs": selected_drugs or [], "selected_trusts": selected_trusts or [], + "active_view": active_view, } # Toggle pill CSS classes base = "toggle-pill" - active = f"{base} toggle-pill--active" - dir_class = active if chart_type == "directory" else base - ind_class = active if chart_type == "indication" else base + active_cls = f"{base} toggle-pill--active" + dir_class = active_cls if chart_type == "directory" else base + ind_class = active_cls if chart_type == "indication" else base return updated_state, dir_class, ind_class diff --git a/dash_app/callbacks/navigation.py b/dash_app/callbacks/navigation.py new file mode 100644 index 0000000..2bfe3ae --- /dev/null +++ b/dash_app/callbacks/navigation.py @@ -0,0 +1,29 @@ +"""Callbacks for view switching between Patient Pathways and Trust Comparison.""" +from dash import Input, Output + + +def register_navigation_callbacks(app): + """Register view switching callbacks.""" + + @app.callback( + Output("patient-pathways-view", "style"), + Output("trust-comparison-view", "style"), + Output("nav-patient-pathways", "className"), + Output("nav-trust-comparison", "className"), + Input("app-state", "data"), + ) + def switch_view(app_state): + """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 = {} + hide = {"display": "none"} + active_cls = "sidebar__item sidebar__item--active" + inactive_cls = "sidebar__item" + + if view == "patient-pathways": + return show, hide, active_cls, inactive_cls + else: + return hide, show, inactive_cls, active_cls diff --git a/dash_app/components/sidebar.py b/dash_app/components/sidebar.py index 777a43a..6197848 100644 --- a/dash_app/components/sidebar.py +++ b/dash_app/components/sidebar.py @@ -19,6 +19,7 @@ def _svg_icon(svg_body): # SVG icon bodies (Feather-style) _ICONS = { "pathway": '', + "compare": '', } @@ -28,15 +29,20 @@ def make_sidebar(): className="sidebar", **{"aria-label": "Main navigation"}, children=[ - # Overview section html.Div( className="sidebar__section", children=[ - html.Div("Overview", className="sidebar__label"), - _sidebar_item("Pathway Overview", "pathway", active=True), + html.Div("Analysis", className="sidebar__label"), + _sidebar_item( + "Patient Pathways", "pathway", + active=True, item_id="nav-patient-pathways", + ), + _sidebar_item( + "Trust Comparison", "compare", + active=False, item_id="nav-trust-comparison", + ), ], ), - # Footer html.Div( className="sidebar__footer", children=[