feat: Trends landing page with directorate overview chart (Task E.3)

This commit is contained in:
Andrew Charlwood
2026-02-07 22:19:52 +00:00
parent 6c5f9be776
commit c253e05046
7 changed files with 237 additions and 22 deletions
+3 -1
View File
@@ -10,6 +10,7 @@ from dash_app.components.chart_card import make_chart_card
from dash_app.components.footer import make_footer
from dash_app.components.modals import make_modals
from dash_app.components.trust_comparison import make_tc_landing, make_tc_dashboard
from dash_app.components.trends import make_trends_landing, make_trends_detail
app = Dash(
__name__,
@@ -70,7 +71,8 @@ app.layout = dmc.MantineProvider(
id="trends-view",
style={"display": "none"},
children=[
html.H3("Trends", style={"padding": "24px"}),
make_trends_landing(),
make_trends_detail(),
],
),
],
+2
View File
@@ -9,6 +9,7 @@ def register_callbacks(app):
from dash_app.callbacks.modals import register_modal_callbacks
from dash_app.callbacks.navigation import register_navigation_callbacks
from dash_app.callbacks.trust_comparison import register_trust_comparison_callbacks
from dash_app.callbacks.trends import register_trends_callbacks
register_filter_callbacks(app)
register_chart_callbacks(app)
@@ -16,3 +17,4 @@ def register_callbacks(app):
register_modal_callbacks(app)
register_navigation_callbacks(app)
register_trust_comparison_callbacks(app)
register_trends_callbacks(app)
+94
View File
@@ -0,0 +1,94 @@
"""Callbacks for Trends view — directorate overview + drug drill-down."""
from dash import Input, Output, State, no_update
import plotly.graph_objects as go
def register_trends_callbacks(app):
"""Register Trends view callbacks."""
def _trends_empty(message):
"""Return a blank figure with a centered message."""
fig = go.Figure()
fig.update_layout(
xaxis={"visible": False}, yaxis={"visible": False},
plot_bgcolor="rgba(0,0,0,0)", paper_bgcolor="rgba(0,0,0,0)",
margin={"t": 0, "l": 0, "r": 0, "b": 0}, height=400,
annotations=[{
"text": message, "xref": "paper", "yref": "paper",
"x": 0.5, "y": 0.5, "showarrow": False,
"font": {"size": 14, "color": "#768692",
"family": "Source Sans 3, system-ui, sans-serif"},
"xanchor": "center", "yanchor": "middle",
}],
)
return fig
# --- Landing / Detail toggle ---
@app.callback(
Output("trends-landing", "style"),
Output("trends-detail", "style"),
Output("trends-detail-title", "children"),
Input("app-state", "data"),
)
def toggle_trends_subviews(app_state):
"""Toggle between landing page and drug detail view."""
if not app_state:
return {}, {"display": "none"}, ""
selected = app_state.get("selected_trends_directorate")
show = {}
hide = {"display": "none"}
if selected:
title = f"{selected} \u2014 Drug Trends"
return hide, show, title
else:
return show, hide, ""
# --- Directorate overview chart (landing page) ---
@app.callback(
Output("trends-overview-chart", "figure"),
Input("app-state", "data"),
Input("trends-view-metric-toggle", "value"),
prevent_initial_call=True,
)
def render_trends_overview(app_state, metric):
"""Render directorate-level trends line chart on the landing page."""
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 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,
group_by="directory",
)
except Exception:
return _trends_empty("Failed to load trend data.")
if not data:
return _trends_empty(
"No trend data available.<br>"
"Run <b>python -m cli.compute_trends</b> to generate."
)
metric_labels = {
"patients": "Patients",
"total_cost": "Cost per Patient",
"cost_pp_pa": "Cost per Patient p.a.",
}
title = f"Directorate Trends \u2014 {metric_labels.get(metric, 'Patients')}"
return create_trend_figure(data, title, metric)
+112
View File
@@ -0,0 +1,112 @@
"""Trends view — directorate-level overview + drug-level drill-down."""
from dash import html, dcc
import dash_mantine_components as dmc
def make_trends_landing():
"""Trends landing page — directorate-level overview chart with metric toggle."""
return html.Div(
id="trends-landing",
children=[
html.Div(
className="trends-landing__header",
children=[
html.Div(
style={"display": "flex", "alignItems": "center",
"justifyContent": "space-between", "gap": "12px",
"flexWrap": "wrap"},
children=[
html.H2("Trends — Directorate Overview",
className="trends-landing__title",
style={"margin": "0", "color": "#1E293B",
"fontSize": "18px",
"fontFamily": "Source Sans 3, system-ui, sans-serif"}),
dmc.SegmentedControl(
id="trends-view-metric-toggle",
data=[
{"value": "patients", "label": "Patients"},
{"value": "total_cost", "label": "Cost per Patient"},
{"value": "cost_pp_pa", "label": "Cost per Patient p.a."},
],
value="patients",
size="xs",
),
],
),
html.P(
"Click a directorate line to drill down into drug-level trends.",
className="trends-landing__desc",
style={"margin": "4px 0 0", "color": "#768692",
"fontSize": "14px"},
),
],
style={"padding": "20px 24px 8px"},
),
dcc.Loading(type="circle", color="#005EB8", children=[
dcc.Graph(
id="trends-overview-chart",
config={"displayModeBar": False, "displaylogo": False},
style={"height": "500px"},
),
]),
],
)
def make_trends_detail():
"""Trends detail page — drug-level trends within a selected directorate."""
return html.Div(
id="trends-detail",
style={"display": "none"},
children=[
html.Div(
className="trends-detail__header",
children=[
html.Div(
style={"display": "flex", "alignItems": "center",
"justifyContent": "space-between", "gap": "12px",
"flexWrap": "wrap"},
children=[
html.Div(
style={"display": "flex", "alignItems": "center",
"gap": "12px"},
children=[
html.Button(
"\u2190 Back",
id="trends-back-btn",
className="tc-dashboard__back",
n_clicks=0,
),
html.H2(
id="trends-detail-title",
children="",
style={"margin": "0", "color": "#1E293B",
"fontSize": "18px",
"fontFamily": "Source Sans 3, system-ui, sans-serif"},
),
],
),
dmc.SegmentedControl(
id="trends-detail-metric-toggle",
data=[
{"value": "patients", "label": "Patients"},
{"value": "total_cost", "label": "Cost per Patient"},
{"value": "cost_pp_pa", "label": "Cost per Patient p.a."},
],
value="patients",
size="xs",
),
],
),
],
style={"padding": "20px 24px 8px"},
),
dcc.Loading(type="circle", color="#005EB8", children=[
dcc.Graph(
id="trends-detail-chart",
config={"displayModeBar": False, "displaylogo": False},
style={"height": "500px"},
),
]),
],
)
+12
View File
@@ -30,6 +30,7 @@ from data_processing.pathway_queries import (
get_drug_network as _get_drug_network,
get_drug_timeline as _get_drug_timeline,
get_dosing_distribution as _get_dosing_distribution,
get_trend_data as _get_trend_data,
)
DB_PATH = Path(__file__).resolve().parents[2] / "data" / "pathways.db"
@@ -251,3 +252,14 @@ def get_dosing_distribution(
return _get_dosing_distribution(DB_PATH, date_filter_id, chart_type, directory, trust)
# --- Trends query wrappers (Phase E) ---
def get_trend_data(
metric: str = "patients",
directory: Optional[str] = None,
drug: Optional[str] = None,
group_by: str = "drug",
) -> list[dict]:
"""Time-series trend data from pathway_trends table."""
return _get_trend_data(DB_PATH, metric, directory, drug, group_by)