feat: Trends drill-down — click directorate to see drug-level trends (Task E.4)
This commit is contained in:
+13
-17
@@ -294,22 +294,18 @@ Comprehensive review and improvement of all Plotly charts in the Dash dashboard.
|
|||||||
- **Checkpoint**: Trends view shows directorate-level line chart. Metric toggle switches y-axis. Lines show one per directorate. PASSED.
|
- **Checkpoint**: Trends view shows directorate-level line chart. Metric toggle switches y-axis. Lines show one per directorate. PASSED.
|
||||||
|
|
||||||
### E.4 Add drug drill-down within Trends view
|
### E.4 Add drug drill-down within Trends view
|
||||||
- [ ] Add `selected_trends_directorate` key (default `None`) to `app-state` initial data in `app.py`
|
- [x] Add `selected_trends_directorate` key (default `None`) to `app-state` initial data in `app.py` (already done in E.2)
|
||||||
- [ ] Add directorate selection callback in `dash_app/callbacks/trends.py`:
|
- [x] Add `customdata=[name]*len(periods)` to each trace in `create_trend_figure()` so directorate name is accessible from clickData
|
||||||
- Clicking a line/trace on the overview chart sets `selected_trends_directorate` in app-state
|
- [x] Add `Input("trends-overview-chart", "clickData")` and `Input("trends-back-btn", "n_clicks")` to `update_app_state()` in `filters.py`:
|
||||||
- Use `clickData` from `trends-overview-chart` as Input
|
- Clicking a trace point extracts directorate name from `clickData["points"][0]["customdata"]`
|
||||||
- Extract directorate name from the clicked trace's `name` attribute
|
- Back button clears `selected_trends_directorate` to None
|
||||||
- Update `app-state` with `selected_trends_directorate`
|
- Chart type change also clears `selected_trends_directorate`
|
||||||
- [ ] Add landing/detail toggle callback:
|
- [x] Landing/detail toggle callback already exists in `trends.py` (`toggle_trends_subviews`) — handles show/hide based on `selected_trends_directorate`
|
||||||
- Input: `app-state` → show/hide `trends-landing` vs `trends-detail`
|
- [x] Add `render_trends_detail()` callback in `trends.py`:
|
||||||
- When `selected_trends_directorate` is set: hide landing, show detail with title "[Directorate] — Drug Trends"
|
- Input: `app-state` + `trends-detail-metric-toggle` → Output `trends-detail-chart`
|
||||||
- [ ] Add detail chart callback:
|
|
||||||
- Input: `app-state` + `trends-view-metric-toggle` → Output `trends-detail-chart`
|
|
||||||
- Calls `get_trend_data(directory=selected, metric=..., group_by="drug")` → `create_trend_figure()`
|
- Calls `get_trend_data(directory=selected, metric=..., group_by="drug")` → `create_trend_figure()`
|
||||||
- Only fires when `selected_trends_directorate` is not None
|
- Guards: only fires when `active_view == "trends"` and `selected_trends_directorate` is not None
|
||||||
- [ ] Add back button callback:
|
- **Checkpoint**: Click a directorate line → drill into drug-level trends. Back button returns to overview. `python run_dash.py` starts cleanly. PASSED.
|
||||||
- Clicking `trends-back-btn` clears `selected_trends_directorate` in app-state → returns to landing
|
|
||||||
- **Checkpoint**: Click a directorate line → drill into drug-level trends. Back button returns to overview. `python run_dash.py` starts cleanly.
|
|
||||||
|
|
||||||
### E.5 Fix chart height to fill viewport
|
### E.5 Fix chart height to fill viewport
|
||||||
- [ ] In `create_trend_figure()` in `plotly_generator.py`: remove explicit `height=500`, let `autosize=True` (from `_base_layout()`) handle it
|
- [ ] In `create_trend_figure()` in `plotly_generator.py`: remove explicit `height=500`, let `autosize=True` (from `_base_layout()`) handle it
|
||||||
@@ -357,8 +353,8 @@ Comprehensive review and improvement of all Plotly charts in the Dash dashboard.
|
|||||||
- [x] Trends tab removed from Patient Pathways (9 tabs remain)
|
- [x] Trends tab removed from Patient Pathways (9 tabs remain)
|
||||||
- [x] 3rd sidebar item "Trends" visible and functional
|
- [x] 3rd sidebar item "Trends" visible and functional
|
||||||
- [x] Trends landing page shows directorate-level line chart with metric toggle
|
- [x] Trends landing page shows directorate-level line chart with metric toggle
|
||||||
- [ ] Clicking a directorate drills into drug-level trends
|
- [x] Clicking a directorate drills into drug-level trends
|
||||||
- [ ] Back button returns to directorate overview
|
- [x] Back button returns to directorate overview
|
||||||
- [ ] Charts fill available viewport height (no fixed 500px cutoff)
|
- [ ] Charts fill available viewport height (no fixed 500px cutoff)
|
||||||
- [x] "Cost" renamed to "Cost per Patient" in metric toggles
|
- [x] "Cost" renamed to "Cost per Patient" in metric toggles
|
||||||
- [ ] `python run_dash.py` starts cleanly
|
- [ ] `python run_dash.py` starts cleanly
|
||||||
|
|||||||
@@ -78,12 +78,15 @@ def register_filter_callbacks(app):
|
|||||||
Input("nav-trends", "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"),
|
||||||
|
Input("trends-overview-chart", "clickData"),
|
||||||
|
Input("trends-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, _nav_trends_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, trends_click_data,
|
||||||
|
_trends_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."""
|
||||||
if not current_state:
|
if not current_state:
|
||||||
@@ -134,6 +137,21 @@ def register_filter_callbacks(app):
|
|||||||
if chart_type != prev_chart_type and selected_comparison_directorate is not None:
|
if chart_type != prev_chart_type and selected_comparison_directorate is not None:
|
||||||
selected_comparison_directorate = None
|
selected_comparison_directorate = None
|
||||||
|
|
||||||
|
# Trends directorate drill-down
|
||||||
|
selected_trends_directorate = current_state.get("selected_trends_directorate")
|
||||||
|
|
||||||
|
if triggered_id == "trends-overview-chart" and trends_click_data:
|
||||||
|
points = trends_click_data.get("points", [])
|
||||||
|
if points:
|
||||||
|
selected_trends_directorate = points[0].get("customdata")
|
||||||
|
|
||||||
|
if triggered_id == "trends-back-btn":
|
||||||
|
selected_trends_directorate = None
|
||||||
|
|
||||||
|
# If chart type changed while trends directorate is selected, return to landing
|
||||||
|
if chart_type != prev_chart_type and selected_trends_directorate is not None:
|
||||||
|
selected_trends_directorate = None
|
||||||
|
|
||||||
# Compute date_filter_id from dropdown values
|
# Compute date_filter_id from dropdown values
|
||||||
date_filter_id = f"{initiated}_{last_seen}"
|
date_filter_id = f"{initiated}_{last_seen}"
|
||||||
|
|
||||||
@@ -148,6 +166,7 @@ def register_filter_callbacks(app):
|
|||||||
"selected_trusts": selected_trusts or [],
|
"selected_trusts": selected_trusts or [],
|
||||||
"active_view": active_view,
|
"active_view": active_view,
|
||||||
"selected_comparison_directorate": selected_comparison_directorate,
|
"selected_comparison_directorate": selected_comparison_directorate,
|
||||||
|
"selected_trends_directorate": selected_trends_directorate,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Toggle pill CSS classes
|
# Toggle pill CSS classes
|
||||||
|
|||||||
@@ -92,3 +92,49 @@ def register_trends_callbacks(app):
|
|||||||
title = f"Directorate Trends \u2014 {metric_labels.get(metric, 'Patients')}"
|
title = f"Directorate Trends \u2014 {metric_labels.get(metric, 'Patients')}"
|
||||||
|
|
||||||
return create_trend_figure(data, title, metric)
|
return create_trend_figure(data, title, metric)
|
||||||
|
|
||||||
|
# --- Drug detail chart (drill-down) ---
|
||||||
|
@app.callback(
|
||||||
|
Output("trends-detail-chart", "figure"),
|
||||||
|
Input("app-state", "data"),
|
||||||
|
Input("trends-detail-metric-toggle", "value"),
|
||||||
|
prevent_initial_call=True,
|
||||||
|
)
|
||||||
|
def render_trends_detail(app_state, metric):
|
||||||
|
"""Render drug-level trends for the selected directorate."""
|
||||||
|
if not app_state:
|
||||||
|
return no_update
|
||||||
|
|
||||||
|
active_view = app_state.get("active_view", "")
|
||||||
|
if active_view != "trends":
|
||||||
|
return no_update
|
||||||
|
|
||||||
|
selected = app_state.get("selected_trends_directorate")
|
||||||
|
if not selected:
|
||||||
|
return no_update
|
||||||
|
|
||||||
|
metric = metric or "patients"
|
||||||
|
|
||||||
|
from dash_app.data.queries import get_trend_data
|
||||||
|
from visualization.plotly_generator import create_trend_figure
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = get_trend_data(
|
||||||
|
metric=metric,
|
||||||
|
directory=selected,
|
||||||
|
group_by="drug",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return _trends_empty("Failed to load trend data.")
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return _trends_empty("No trend data for this directorate.")
|
||||||
|
|
||||||
|
metric_labels = {
|
||||||
|
"patients": "Patients",
|
||||||
|
"total_cost": "Cost per Patient",
|
||||||
|
"cost_pp_pa": "Cost per Patient p.a.",
|
||||||
|
}
|
||||||
|
title = f"{selected} \u2014 {metric_labels.get(metric, 'Patients')}"
|
||||||
|
|
||||||
|
return create_trend_figure(data, title, metric)
|
||||||
|
|||||||
@@ -2343,6 +2343,7 @@ def create_trend_figure(
|
|||||||
y=s["values"],
|
y=s["values"],
|
||||||
mode="lines+markers",
|
mode="lines+markers",
|
||||||
name=name,
|
name=name,
|
||||||
|
customdata=[name] * len(s["periods"]),
|
||||||
line=dict(color=colour, width=2),
|
line=dict(color=colour, width=2),
|
||||||
marker=dict(color=colour, size=6),
|
marker=dict(color=colour, size=6),
|
||||||
hovertemplate=(
|
hovertemplate=(
|
||||||
|
|||||||
Reference in New Issue
Block a user