feat: add KPI row, filter bar, and chart card components (Task 2.2)
This commit is contained in:
+6
-5
@@ -4,6 +4,9 @@ import dash_mantine_components as dmc
|
||||
|
||||
from dash_app.components.header import make_header
|
||||
from dash_app.components.sidebar import make_sidebar
|
||||
from dash_app.components.kpi_row import make_kpi_row
|
||||
from dash_app.components.filter_bar import make_filter_bar
|
||||
from dash_app.components.chart_card import make_chart_card
|
||||
|
||||
app = Dash(
|
||||
__name__,
|
||||
@@ -30,11 +33,9 @@ app.layout = dmc.MantineProvider(
|
||||
html.Main(
|
||||
className="main",
|
||||
children=[
|
||||
html.H1("HCD Analysis", style={"color": "#003087"}),
|
||||
html.P(
|
||||
"Layout scaffolding with header and sidebar. "
|
||||
"KPIs, filter bar, and chart card will be added in Task 2.2."
|
||||
),
|
||||
make_kpi_row(),
|
||||
make_filter_bar(),
|
||||
make_chart_card(),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -200,6 +200,21 @@ body {
|
||||
}
|
||||
.filter-select:focus { outline: 3px solid var(--nhs-yellow); outline-offset: 0; }
|
||||
|
||||
/* Dash dcc.Dropdown overrides for filter bar */
|
||||
.filter-dropdown {
|
||||
min-width: 160px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
}
|
||||
.filter-dropdown .Select-control {
|
||||
height: 34px;
|
||||
border-color: var(--nhs-pale-grey);
|
||||
}
|
||||
.filter-dropdown .Select-value-label {
|
||||
color: var(--nhs-dark-grey) !important;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* ── Chart Area ── */
|
||||
.chart-card {
|
||||
background: var(--nhs-white);
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Chart card component — header, tabs, and dcc.Graph for icicle chart."""
|
||||
from dash import html, dcc
|
||||
|
||||
|
||||
def make_chart_card():
|
||||
"""Return a chart card matching 01_nhs_classic.html structure.
|
||||
|
||||
Contains:
|
||||
- Header with title and dynamic subtitle (hierarchy label)
|
||||
- Tab row (Icicle active, Sankey and Timeline as disabled placeholders)
|
||||
- dcc.Graph for the Plotly icicle figure
|
||||
"""
|
||||
return html.Section(
|
||||
className="chart-card",
|
||||
**{"aria-label": "Patient pathway chart"},
|
||||
children=[
|
||||
# Card header
|
||||
html.Div(
|
||||
className="chart-card__header",
|
||||
children=[
|
||||
html.Div(
|
||||
children=[
|
||||
html.Div(
|
||||
"Patient Pathway Visualization",
|
||||
className="chart-card__title",
|
||||
),
|
||||
html.Div(
|
||||
"Trust \u2192 Directorate \u2192 Drug \u2192 Patient Pathway",
|
||||
className="chart-card__subtitle",
|
||||
id="chart-subtitle",
|
||||
),
|
||||
]
|
||||
),
|
||||
],
|
||||
),
|
||||
# Tab row
|
||||
html.Div(
|
||||
className="chart-card__tabs",
|
||||
role="tablist",
|
||||
children=[
|
||||
html.Button(
|
||||
"Icicle",
|
||||
className="chart-tab chart-tab--active",
|
||||
role="tab",
|
||||
**{"aria-selected": "true"},
|
||||
),
|
||||
html.Button(
|
||||
"Sankey",
|
||||
className="chart-tab",
|
||||
role="tab",
|
||||
disabled=True,
|
||||
**{"aria-selected": "false"},
|
||||
),
|
||||
html.Button(
|
||||
"Timeline",
|
||||
className="chart-tab",
|
||||
role="tab",
|
||||
disabled=True,
|
||||
**{"aria-selected": "false"},
|
||||
),
|
||||
],
|
||||
),
|
||||
# Chart area
|
||||
dcc.Graph(
|
||||
id="pathway-chart",
|
||||
style={"minHeight": "500px", "flex": "1"},
|
||||
config={
|
||||
"displayModeBar": True,
|
||||
"displaylogo": False,
|
||||
"modeBarButtonsToRemove": ["lasso2d", "select2d"],
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Filter bar component — chart type toggle + date filter dropdowns."""
|
||||
from dash import html, dcc
|
||||
|
||||
|
||||
def make_filter_bar():
|
||||
"""Return a filter bar matching 01_nhs_classic.html structure.
|
||||
|
||||
Contains:
|
||||
- Chart type toggle pills (By Directory / By Indication)
|
||||
- Initiated dropdown (All years, Last 2 years, Last 1 year)
|
||||
- Last seen dropdown (Last 6 months, Last 12 months)
|
||||
|
||||
Drug/directorate filters are in the drawer (Phase 4), not here.
|
||||
"""
|
||||
return html.Section(
|
||||
className="filter-bar",
|
||||
**{"aria-label": "Filters"},
|
||||
children=[
|
||||
# Chart type toggle
|
||||
html.Div(
|
||||
className="filter-bar__group",
|
||||
children=[
|
||||
html.Span("View", className="filter-bar__label"),
|
||||
html.Div(
|
||||
className="toggle-pills",
|
||||
role="radiogroup",
|
||||
**{"aria-label": "Chart view type"},
|
||||
children=[
|
||||
html.Button(
|
||||
"By Directory",
|
||||
id="chart-type-directory",
|
||||
className="toggle-pill toggle-pill--active",
|
||||
role="radio",
|
||||
n_clicks=0,
|
||||
**{"aria-checked": "true"},
|
||||
),
|
||||
html.Button(
|
||||
"By Indication",
|
||||
id="chart-type-indication",
|
||||
className="toggle-pill",
|
||||
role="radio",
|
||||
n_clicks=0,
|
||||
**{"aria-checked": "false"},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
# Divider
|
||||
html.Div(className="filter-bar__divider"),
|
||||
# Initiated filter
|
||||
html.Div(
|
||||
className="filter-bar__group",
|
||||
children=[
|
||||
html.Span("Initiated", className="filter-bar__label"),
|
||||
dcc.Dropdown(
|
||||
id="filter-initiated",
|
||||
options=[
|
||||
{"label": "All years", "value": "all"},
|
||||
{"label": "Last 2 years", "value": "2yr"},
|
||||
{"label": "Last 1 year", "value": "1yr"},
|
||||
],
|
||||
value="all",
|
||||
clearable=False,
|
||||
searchable=False,
|
||||
className="filter-dropdown",
|
||||
),
|
||||
],
|
||||
),
|
||||
# Last seen filter
|
||||
html.Div(
|
||||
className="filter-bar__group",
|
||||
children=[
|
||||
html.Span("Last seen", className="filter-bar__label"),
|
||||
dcc.Dropdown(
|
||||
id="filter-last-seen",
|
||||
options=[
|
||||
{"label": "Last 6 months", "value": "6mo"},
|
||||
{"label": "Last 12 months", "value": "12mo"},
|
||||
],
|
||||
value="6mo",
|
||||
clearable=False,
|
||||
searchable=False,
|
||||
className="filter-dropdown",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
"""KPI row component — 4 metric cards with callback-updatable values."""
|
||||
from dash import html
|
||||
|
||||
|
||||
def make_kpi_row():
|
||||
"""Return a section with 4 KPI cards matching 01_nhs_classic.html structure."""
|
||||
return html.Section(
|
||||
className="kpi-row",
|
||||
**{"aria-label": "Key performance indicators"},
|
||||
children=[
|
||||
_kpi_card("Unique Patients", "kpi-patients", "—", "across all trusts"),
|
||||
_kpi_card("Drug Types", "kpi-drugs", "—", "high-cost drugs tracked"),
|
||||
_kpi_card("Total Cost", "kpi-cost", "—", "current period spend"),
|
||||
_kpi_card(
|
||||
"Indication Match",
|
||||
"kpi-match",
|
||||
"—",
|
||||
"GP diagnosis confirmed",
|
||||
modifier="kpi-card--green",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _kpi_card(label, value_id, default_value, sub_text, modifier=None):
|
||||
"""Build a single KPI card.
|
||||
|
||||
Args:
|
||||
label: uppercase label text
|
||||
value_id: HTML id for the value span (for callback Output)
|
||||
default_value: initial display value before callbacks fire
|
||||
sub_text: description below the value
|
||||
modifier: optional CSS modifier class (e.g. 'kpi-card--green')
|
||||
"""
|
||||
card_class = "kpi-card"
|
||||
if modifier:
|
||||
card_class += f" {modifier}"
|
||||
|
||||
return html.Div(
|
||||
className=card_class,
|
||||
children=[
|
||||
html.Div(label, className="kpi-card__label"),
|
||||
html.Div(default_value, className="kpi-card__value", id=value_id),
|
||||
html.Div(sub_text, className="kpi-card__sub"),
|
||||
],
|
||||
)
|
||||
Reference in New Issue
Block a user