feat: Trust Comparison landing page + directorate selector (Task 10.7)
- Add get_directorate_summary() query for per-directorate patient/drug counts - Create trust_comparison.py with landing grid and 6-chart dashboard layout - Wire directorate card clicks and back button through app-state callbacks - Add TC landing and dashboard CSS per Phase 10 design spec - Placeholder charts for 6 dashboard graphs (filled in Task 10.8) - Chart type toggle clears selected directorate when switching modes
This commit is contained in:
@@ -522,14 +522,14 @@ Additionally: KPI row removed, fraction KPIs moved to header, global filter sub-
|
|||||||
- **Checkpoint**: All 6 query functions return correct per-trust data for sample directorates
|
- **Checkpoint**: All 6 query functions return correct per-trust data for sample directorates
|
||||||
|
|
||||||
### 10.7 Trust Comparison landing page + directorate selector
|
### 10.7 Trust Comparison landing page + directorate selector
|
||||||
- [ ] Create Trust Comparison view component with two states:
|
- [x] Create Trust Comparison view component with two states:
|
||||||
- **Landing**: Grid of directorate/indication buttons (source: reference-data store)
|
- **Landing**: Grid of directorate/indication buttons (source: reference-data store)
|
||||||
- **Dashboard**: 6-chart layout for selected directorate (see 10.8)
|
- **Dashboard**: 6-chart layout for selected directorate (see 10.8)
|
||||||
- [ ] Directorate buttons: ~14 for "By Directory" mode, ~32 for "By Indication" mode (from chart type toggle)
|
- [x] Directorate buttons: ~14 for "By Directory" mode, ~32 for "By Indication" mode (from chart type toggle)
|
||||||
- [ ] Clicking a button sets `selected_comparison_directorate` in app-state, switching to dashboard view
|
- [x] Clicking a button sets `selected_comparison_directorate` in app-state, switching to dashboard view
|
||||||
- [ ] Back button to return to landing page (clears `selected_comparison_directorate`)
|
- [x] Back button to return to landing page (clears `selected_comparison_directorate`)
|
||||||
- [ ] Apply layout design from 10.1
|
- [x] Apply layout design from 10.1
|
||||||
- [ ] This view is shown when `active_view == "trust-comparison"`
|
- [x] This view is shown when `active_view == "trust-comparison"`
|
||||||
- **Checkpoint**: Landing page shows directorate buttons, clicking one transitions to dashboard state, back button works
|
- **Checkpoint**: Landing page shows directorate buttons, clicking one transitions to dashboard state, back button works
|
||||||
|
|
||||||
### 10.8 Trust Comparison 6-chart dashboard
|
### 10.8 Trust Comparison 6-chart dashboard
|
||||||
|
|||||||
+3
-27
@@ -9,6 +9,7 @@ from dash_app.components.filter_bar import make_filter_bar
|
|||||||
from dash_app.components.chart_card import make_chart_card
|
from dash_app.components.chart_card import make_chart_card
|
||||||
from dash_app.components.footer import make_footer
|
from dash_app.components.footer import make_footer
|
||||||
from dash_app.components.modals import make_modals
|
from dash_app.components.modals import make_modals
|
||||||
|
from dash_app.components.trust_comparison import make_tc_landing, make_tc_dashboard
|
||||||
|
|
||||||
app = Dash(
|
app = Dash(
|
||||||
__name__,
|
__name__,
|
||||||
@@ -59,33 +60,8 @@ app.layout = dmc.MantineProvider(
|
|||||||
id="trust-comparison-view",
|
id="trust-comparison-view",
|
||||||
style={"display": "none"},
|
style={"display": "none"},
|
||||||
children=[
|
children=[
|
||||||
html.Div(
|
make_tc_landing(),
|
||||||
className="tc-landing",
|
make_tc_dashboard(),
|
||||||
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
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -397,12 +397,153 @@ body {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Trust Comparison Landing ── */
|
||||||
|
.tc-landing {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
.tc-landing__header {
|
||||||
|
padding: 0 0 8px;
|
||||||
|
}
|
||||||
|
.tc-landing__title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--nhs-dark-blue);
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
.tc-landing__desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--nhs-mid-grey);
|
||||||
|
font-weight: 400;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.tc-landing__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.tc-landing__grid--wide {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Directorate selector cards */
|
||||||
|
.tc-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: var(--nhs-white);
|
||||||
|
border: 1px solid var(--nhs-pale-grey);
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
|
||||||
|
}
|
||||||
|
.tc-card:hover {
|
||||||
|
border-left-color: var(--nhs-blue);
|
||||||
|
background: #FAFCFF;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 48, 135, 0.08);
|
||||||
|
}
|
||||||
|
.tc-card:focus-visible {
|
||||||
|
box-shadow: 0 0 0 3px var(--nhs-yellow);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.tc-card__name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--nhs-dark-blue);
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.tc-card__stats {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--nhs-mid-grey);
|
||||||
|
}
|
||||||
|
.tc-card__stat {
|
||||||
|
font-weight: 400;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.tc-card__dot {
|
||||||
|
color: var(--nhs-pale-grey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Trust Comparison Dashboard ── */
|
||||||
|
.tc-dashboard {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.tc-dashboard__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.tc-dashboard__back {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: inherit;
|
||||||
|
color: var(--nhs-blue);
|
||||||
|
background: var(--nhs-white);
|
||||||
|
border: 1px solid var(--nhs-pale-grey);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.tc-dashboard__back:hover {
|
||||||
|
background: #E8F0FE;
|
||||||
|
}
|
||||||
|
.tc-dashboard__back:focus-visible {
|
||||||
|
box-shadow: 0 0 0 3px var(--nhs-yellow);
|
||||||
|
}
|
||||||
|
.tc-dashboard__title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--nhs-dark-blue);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2×3 chart grid */
|
||||||
|
.tc-dashboard__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Individual chart cell */
|
||||||
|
.tc-chart-cell {
|
||||||
|
background: var(--nhs-white);
|
||||||
|
border: 1px solid var(--nhs-pale-grey);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.tc-chart-cell__title {
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--nhs-dark-blue);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
border-bottom: 1px solid var(--nhs-pale-grey);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Responsive ── */
|
/* ── Responsive ── */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.tc-landing__grid { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.tc-landing__grid--wide { grid-template-columns: repeat(3, 1fr); }
|
||||||
|
}
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.kpi-row { grid-template-columns: repeat(2, 1fr); }
|
.kpi-row { grid-template-columns: repeat(2, 1fr); }
|
||||||
}
|
}
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.sidebar { display: none; }
|
.sidebar { display: none; }
|
||||||
.main { margin-left: 0; }
|
.main { margin-left: 0; }
|
||||||
|
.sub-header { left: 0; }
|
||||||
.kpi-row { grid-template-columns: 1fr; }
|
.kpi-row { grid-template-columns: 1fr; }
|
||||||
|
.tc-landing__grid { grid-template-columns: 1fr; }
|
||||||
|
.tc-dashboard__grid { grid-template-columns: 1fr; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ def register_callbacks(app):
|
|||||||
from dash_app.callbacks.kpi import register_kpi_callbacks
|
from dash_app.callbacks.kpi import register_kpi_callbacks
|
||||||
from dash_app.callbacks.modals import register_modal_callbacks
|
from dash_app.callbacks.modals import register_modal_callbacks
|
||||||
from dash_app.callbacks.navigation import register_navigation_callbacks
|
from dash_app.callbacks.navigation import register_navigation_callbacks
|
||||||
|
from dash_app.callbacks.trust_comparison import register_trust_comparison_callbacks
|
||||||
|
|
||||||
register_filter_callbacks(app)
|
register_filter_callbacks(app)
|
||||||
register_chart_callbacks(app)
|
register_chart_callbacks(app)
|
||||||
register_kpi_callbacks(app)
|
register_kpi_callbacks(app)
|
||||||
register_modal_callbacks(app)
|
register_modal_callbacks(app)
|
||||||
register_navigation_callbacks(app)
|
register_navigation_callbacks(app)
|
||||||
|
register_trust_comparison_callbacks(app)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Callbacks for reference data loading and filter state management."""
|
"""Callbacks for reference data loading and filter state management."""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from dash import Input, Output, State, callback, ctx, no_update
|
from dash import Input, Output, State, callback, ctx, no_update, ALL
|
||||||
|
|
||||||
|
|
||||||
def _format_relative_time(iso_timestamp: str) -> str:
|
def _format_relative_time(iso_timestamp: str) -> str:
|
||||||
@@ -75,13 +75,16 @@ 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({"type": "tc-selector", "index": ALL}, "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, current_state
|
selected_trusts, _nav_pp_clicks, _nav_tc_clicks,
|
||||||
|
_tc_selector_clicks, _tc_back_clicks, current_state
|
||||||
):
|
):
|
||||||
"""Update app-state when chart type toggle, date filters, drug/trust chips, or sidebar nav change."""
|
"""Update app-state when any filter, nav, or TC selector changes."""
|
||||||
if not current_state:
|
if not current_state:
|
||||||
current_state = {
|
current_state = {
|
||||||
"chart_type": "directory",
|
"chart_type": "directory",
|
||||||
@@ -99,6 +102,7 @@ def register_filter_callbacks(app):
|
|||||||
|
|
||||||
# Determine chart type from toggle pills
|
# Determine chart type from toggle pills
|
||||||
chart_type = current_state.get("chart_type", "directory")
|
chart_type = current_state.get("chart_type", "directory")
|
||||||
|
prev_chart_type = chart_type
|
||||||
if triggered_id == "chart-type-directory":
|
if triggered_id == "chart-type-directory":
|
||||||
chart_type = "directory"
|
chart_type = "directory"
|
||||||
elif triggered_id == "chart-type-indication":
|
elif triggered_id == "chart-type-indication":
|
||||||
@@ -111,6 +115,21 @@ def register_filter_callbacks(app):
|
|||||||
elif triggered_id == "nav-trust-comparison":
|
elif triggered_id == "nav-trust-comparison":
|
||||||
active_view = "trust-comparison"
|
active_view = "trust-comparison"
|
||||||
|
|
||||||
|
# Trust Comparison directorate selection
|
||||||
|
selected_comparison_directorate = current_state.get("selected_comparison_directorate")
|
||||||
|
|
||||||
|
# Handle TC card click (pattern-matching ID)
|
||||||
|
if isinstance(triggered_id, dict) and triggered_id.get("type") == "tc-selector":
|
||||||
|
selected_comparison_directorate = triggered_id["index"]
|
||||||
|
|
||||||
|
# Handle TC back button
|
||||||
|
if triggered_id == "tc-back-btn":
|
||||||
|
selected_comparison_directorate = None
|
||||||
|
|
||||||
|
# If chart type changed while a directorate is selected, return to landing
|
||||||
|
if chart_type != prev_chart_type and selected_comparison_directorate is not None:
|
||||||
|
selected_comparison_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}"
|
||||||
|
|
||||||
@@ -124,6 +143,7 @@ def register_filter_callbacks(app):
|
|||||||
"selected_drugs": selected_drugs or [],
|
"selected_drugs": selected_drugs or [],
|
||||||
"selected_trusts": selected_trusts or [],
|
"selected_trusts": selected_trusts or [],
|
||||||
"active_view": active_view,
|
"active_view": active_view,
|
||||||
|
"selected_comparison_directorate": selected_comparison_directorate,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Toggle pill CSS classes
|
# Toggle pill CSS classes
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
"""Callbacks for Trust Comparison landing page and dashboard navigation."""
|
||||||
|
from dash import html, Input, Output, State, ctx, no_update
|
||||||
|
import plotly.graph_objects as go
|
||||||
|
|
||||||
|
|
||||||
|
def register_trust_comparison_callbacks(app):
|
||||||
|
"""Register Trust Comparison view callbacks."""
|
||||||
|
|
||||||
|
@app.callback(
|
||||||
|
Output("tc-landing-grid", "children"),
|
||||||
|
Output("tc-landing-grid", "className"),
|
||||||
|
Output("tc-landing-desc", "children"),
|
||||||
|
Input("app-state", "data"),
|
||||||
|
)
|
||||||
|
def populate_landing_grid(app_state):
|
||||||
|
"""Populate the landing page grid with directorate/indication cards."""
|
||||||
|
if not app_state:
|
||||||
|
return [], "tc-landing__grid", "Select a directorate to compare drug usage across trusts."
|
||||||
|
|
||||||
|
from dash_app.data.queries import get_directorate_summary
|
||||||
|
|
||||||
|
chart_type = app_state.get("chart_type", "directory")
|
||||||
|
date_filter_id = app_state.get("date_filter_id", "all_6mo")
|
||||||
|
|
||||||
|
summaries = get_directorate_summary(date_filter_id, chart_type)
|
||||||
|
|
||||||
|
# Build card buttons
|
||||||
|
cards = []
|
||||||
|
for item in summaries:
|
||||||
|
name = item["name"]
|
||||||
|
patients = item["patients"]
|
||||||
|
drugs = item["drugs"]
|
||||||
|
cards.append(
|
||||||
|
html.Button(
|
||||||
|
className="tc-card",
|
||||||
|
id={"type": "tc-selector", "index": name},
|
||||||
|
n_clicks=0,
|
||||||
|
children=[
|
||||||
|
html.Div(name, className="tc-card__name"),
|
||||||
|
html.Div(
|
||||||
|
className="tc-card__stats",
|
||||||
|
children=[
|
||||||
|
html.Span(
|
||||||
|
f"{patients:,} patients",
|
||||||
|
className="tc-card__stat",
|
||||||
|
),
|
||||||
|
html.Span("\u00b7", className="tc-card__dot"),
|
||||||
|
html.Span(
|
||||||
|
f"{drugs} drugs",
|
||||||
|
className="tc-card__stat",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Grid class: wider for indication mode (more items)
|
||||||
|
grid_cls = "tc-landing__grid"
|
||||||
|
if chart_type == "indication":
|
||||||
|
grid_cls += " tc-landing__grid--wide"
|
||||||
|
|
||||||
|
# Description text adapts to chart type
|
||||||
|
if chart_type == "indication":
|
||||||
|
desc = "Select an indication to compare drug usage across trusts."
|
||||||
|
else:
|
||||||
|
desc = "Select a directorate to compare drug usage across trusts."
|
||||||
|
|
||||||
|
return cards, grid_cls, desc
|
||||||
|
|
||||||
|
@app.callback(
|
||||||
|
Output("trust-comparison-landing", "style"),
|
||||||
|
Output("trust-comparison-dashboard", "style"),
|
||||||
|
Output("tc-dashboard-title", "children"),
|
||||||
|
Input("app-state", "data"),
|
||||||
|
)
|
||||||
|
def toggle_tc_subviews(app_state):
|
||||||
|
"""Toggle between landing page and 6-chart dashboard."""
|
||||||
|
if not app_state:
|
||||||
|
return {}, {"display": "none"}, ""
|
||||||
|
|
||||||
|
selected = app_state.get("selected_comparison_directorate")
|
||||||
|
show = {}
|
||||||
|
hide = {"display": "none"}
|
||||||
|
|
||||||
|
if selected:
|
||||||
|
chart_type = app_state.get("chart_type", "directory")
|
||||||
|
label = "Indication" if chart_type == "indication" else "Directorate"
|
||||||
|
title = f"{selected} \u2014 Trust Comparison"
|
||||||
|
return hide, show, title
|
||||||
|
else:
|
||||||
|
return show, hide, ""
|
||||||
|
|
||||||
|
# Dashboard chart rendering will be added in Task 10.8.
|
||||||
|
# For now, register empty figure placeholders for the 6 chart IDs
|
||||||
|
# so the dcc.Graph components don't error on load.
|
||||||
|
_tc_chart_ids = [
|
||||||
|
"tc-chart-market-share",
|
||||||
|
"tc-chart-cost-waterfall",
|
||||||
|
"tc-chart-dosing",
|
||||||
|
"tc-chart-heatmap",
|
||||||
|
"tc-chart-duration",
|
||||||
|
"tc-chart-cost-effectiveness",
|
||||||
|
]
|
||||||
|
|
||||||
|
for chart_id in _tc_chart_ids:
|
||||||
|
@app.callback(
|
||||||
|
Output(chart_id, "figure"),
|
||||||
|
Input("app-state", "data"),
|
||||||
|
prevent_initial_call=True,
|
||||||
|
)
|
||||||
|
def _placeholder_chart(app_state, _cid=chart_id):
|
||||||
|
"""Placeholder — returns empty figure until Task 10.8 implements real charts."""
|
||||||
|
selected = (app_state or {}).get("selected_comparison_directorate")
|
||||||
|
if not selected:
|
||||||
|
return no_update
|
||||||
|
fig = go.Figure()
|
||||||
|
fig.update_layout(
|
||||||
|
template="plotly_white",
|
||||||
|
margin=dict(l=20, r=20, t=30, b=20),
|
||||||
|
height=300,
|
||||||
|
annotations=[
|
||||||
|
dict(
|
||||||
|
text="Chart will be implemented in Task 10.8",
|
||||||
|
xref="paper", yref="paper",
|
||||||
|
x=0.5, y=0.5, showarrow=False,
|
||||||
|
font=dict(size=14, color="#999"),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return fig
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
"""Trust Comparison view — landing page (directorate selector) + 6-chart dashboard."""
|
||||||
|
from dash import html, dcc
|
||||||
|
|
||||||
|
|
||||||
|
def _tc_chart_cell(title, graph_id):
|
||||||
|
"""Helper to create a single chart cell in the 6-chart dashboard grid."""
|
||||||
|
return html.Div(className="tc-chart-cell", children=[
|
||||||
|
html.Div(title, className="tc-chart-cell__title"),
|
||||||
|
dcc.Loading(type="circle", color="#005EB8", children=[
|
||||||
|
dcc.Graph(
|
||||||
|
id=graph_id,
|
||||||
|
config={"displayModeBar": False, "displaylogo": False},
|
||||||
|
style={"height": "320px"},
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def make_tc_landing():
|
||||||
|
"""Trust Comparison landing page — directorate/indication selector grid."""
|
||||||
|
return 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=[],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_tc_dashboard():
|
||||||
|
"""Trust Comparison 6-chart dashboard for a selected directorate."""
|
||||||
|
return html.Div(
|
||||||
|
className="tc-dashboard",
|
||||||
|
id="trust-comparison-dashboard",
|
||||||
|
style={"display": "none"},
|
||||||
|
children=[
|
||||||
|
html.Div(
|
||||||
|
className="tc-dashboard__header",
|
||||||
|
children=[
|
||||||
|
html.Button(
|
||||||
|
"\u2190 Back",
|
||||||
|
id="tc-back-btn",
|
||||||
|
className="tc-dashboard__back",
|
||||||
|
n_clicks=0,
|
||||||
|
),
|
||||||
|
html.H2(
|
||||||
|
id="tc-dashboard-title",
|
||||||
|
className="tc-dashboard__title",
|
||||||
|
children="",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
html.Div(
|
||||||
|
className="tc-dashboard__grid",
|
||||||
|
children=[
|
||||||
|
_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"),
|
||||||
|
_tc_chart_cell("Treatment Duration", "tc-chart-duration"),
|
||||||
|
_tc_chart_cell("Cost Effectiveness", "tc-chart-cost-effectiveness"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
@@ -23,6 +23,7 @@ from data_processing.pathway_queries import (
|
|||||||
get_trust_dosing as _get_trust_dosing,
|
get_trust_dosing as _get_trust_dosing,
|
||||||
get_trust_heatmap as _get_trust_heatmap,
|
get_trust_heatmap as _get_trust_heatmap,
|
||||||
get_trust_durations as _get_trust_durations,
|
get_trust_durations as _get_trust_durations,
|
||||||
|
get_directorate_summary as _get_directorate_summary,
|
||||||
)
|
)
|
||||||
|
|
||||||
DB_PATH = Path(__file__).resolve().parents[2] / "data" / "pathways.db"
|
DB_PATH = Path(__file__).resolve().parents[2] / "data" / "pathways.db"
|
||||||
@@ -168,3 +169,14 @@ def get_trust_durations(
|
|||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""Drug durations by trust within a single directorate."""
|
"""Drug durations by trust within a single directorate."""
|
||||||
return _get_trust_durations(DB_PATH, date_filter_id, chart_type, directory)
|
return _get_trust_durations(DB_PATH, date_filter_id, chart_type, directory)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Directorate summary for Trust Comparison landing page ---
|
||||||
|
|
||||||
|
|
||||||
|
def get_directorate_summary(
|
||||||
|
date_filter_id: str = "all_6mo",
|
||||||
|
chart_type: str = "directory",
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Per-directorate summary (name, patient count, drug count) for landing cards."""
|
||||||
|
return _get_directorate_summary(DB_PATH, date_filter_id, chart_type)
|
||||||
|
|||||||
@@ -1064,3 +1064,62 @@ def get_trust_durations(
|
|||||||
return []
|
return []
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
# --- Directorate/indication summary for Trust Comparison landing page ---
|
||||||
|
|
||||||
|
|
||||||
|
def get_directorate_summary(
|
||||||
|
db_path: Path,
|
||||||
|
date_filter_id: str,
|
||||||
|
chart_type: str,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Get per-directorate (or per-indication) summary stats for landing page cards.
|
||||||
|
|
||||||
|
Returns a list of dicts sorted by patient count descending:
|
||||||
|
[{"name": "RHEUMATOLOGY", "patients": 847, "drugs": 12}, ...]
|
||||||
|
|
||||||
|
Level 2 nodes provide patient counts; level 3 node count gives drug count.
|
||||||
|
"""
|
||||||
|
conn = sqlite3.connect(str(db_path))
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
try:
|
||||||
|
# Get patient counts from level 2 nodes
|
||||||
|
level2_query = """
|
||||||
|
SELECT labels AS name, SUM(value) AS patients
|
||||||
|
FROM pathway_nodes
|
||||||
|
WHERE date_filter_id = ? AND chart_type = ? AND level = 2
|
||||||
|
AND labels IS NOT NULL AND labels != ''
|
||||||
|
GROUP BY labels
|
||||||
|
"""
|
||||||
|
level2_rows = conn.execute(level2_query, (date_filter_id, chart_type)).fetchall()
|
||||||
|
|
||||||
|
# Get drug counts from level 3 nodes grouped by directory
|
||||||
|
level3_query = """
|
||||||
|
SELECT directory, COUNT(DISTINCT labels) AS drug_count
|
||||||
|
FROM pathway_nodes
|
||||||
|
WHERE date_filter_id = ? AND chart_type = ? AND level = 3
|
||||||
|
AND directory IS NOT NULL AND directory != ''
|
||||||
|
GROUP BY directory
|
||||||
|
"""
|
||||||
|
level3_rows = conn.execute(level3_query, (date_filter_id, chart_type)).fetchall()
|
||||||
|
drug_counts = {r["directory"]: r["drug_count"] for r in level3_rows}
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for r in level2_rows:
|
||||||
|
name = r["name"]
|
||||||
|
patients = r["patients"] or 0
|
||||||
|
if patients == 0:
|
||||||
|
continue
|
||||||
|
result.append({
|
||||||
|
"name": name,
|
||||||
|
"patients": patients,
|
||||||
|
"drugs": drug_counts.get(name, 0),
|
||||||
|
})
|
||||||
|
|
||||||
|
result.sort(key=lambda x: x["patients"], reverse=True)
|
||||||
|
return result
|
||||||
|
except sqlite3.Error:
|
||||||
|
return []
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|||||||
Reference in New Issue
Block a user