feat: add KPI row, filter bar, and chart card components (Task 2.2)

This commit is contained in:
Andrew Charlwood
2026-02-06 13:20:42 +00:00
parent 5ebe75ad13
commit 307563bb31
7 changed files with 296 additions and 8 deletions
+3 -3
View File
@@ -131,17 +131,17 @@ Drawer selection → update_drug_selection → app-state store → load_pathway_
- **Checkpoint**: Components render in browser with correct NHS styling ✓
### 2.2 Main content area: KPI row + filter bar + chart card
- [ ] Create `dash_app/components/kpi_row.py``make_kpi_row()` function
- [x] Create `dash_app/components/kpi_row.py``make_kpi_row()` function
- 4 KPI cards: Unique Patients, Drug Types, Total Cost, Indication Match Rate
- Each card value has an ID for callback updates: `id="kpi-patients"`, `id="kpi-drugs"`, `id="kpi-cost"`, `id="kpi-match"`
- CSS classes: `.kpi-row`, `.kpi-card`, `.kpi-card__label`, `.kpi-card__value`, `.kpi-card__sub`
- [ ] Create `dash_app/components/filter_bar.py``make_filter_bar()` function
- [x] Create `dash_app/components/filter_bar.py``make_filter_bar()` function
- Chart type toggle pills ("By Directory" / "By Indication") — use `html.Button` with `.toggle-pill` CSS
- Initiated dropdown: All years, Last 2 years, Last 1 year — use `dcc.Dropdown` or `html.Select` with `.filter-select`
- Last seen dropdown: Last 6 months, Last 12 months
- NO drug/directorate dropdowns here (those are in the drawer)
- Component IDs: `id="chart-type-directory"`, `id="chart-type-indication"`, `id="filter-initiated"`, `id="filter-last-seen"`
- [ ] Create `dash_app/components/chart_card.py``make_chart_card()` function
- [x] Create `dash_app/components/chart_card.py``make_chart_card()` function
- Card header with title + dynamic subtitle (hierarchy label: "Trust → Directorate → Drug → Pathway")
- Tab row: Icicle (active), Sankey (disabled placeholder), Timeline (disabled placeholder)
- `dcc.Graph(id="pathway-chart")` filling the card body
+6 -5
View File
@@ -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(),
],
),
],
+15
View File
@@ -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);
+74
View File
@@ -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"],
},
),
],
)
+89
View File
@@ -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",
),
],
),
],
)
+46
View File
@@ -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"),
],
)
+63
View File
@@ -239,3 +239,66 @@ Migrating the HCD Analysis frontend from Reflex to Dash (Plotly) + Dash Mantine
- Filter IDs: `id="filter-initiated"`, `id="filter-last-seen"`
### Blocked items:
- None
## Iteration 5 — 2026-02-06
### Task: Phase 2 — Task 2.2 (Main content area: KPI row + filter bar + chart card)
### Why this task:
- Phase 2.1 (header + sidebar) complete. Task 2.2 fills the main content area with actual components.
- Previous iteration (4) explicitly recommended this as the next task.
- The main area was a placeholder — this task adds the three core content sections.
### Status: COMPLETE
### What was done:
- Created `dash_app/components/kpi_row.py` with `make_kpi_row()`:
- 4 KPI cards in a grid: Unique Patients, Drug Types, Total Cost, Indication Match
- Fourth card has `.kpi-card--green` modifier for green top border
- Each value has an ID for callback updates: `kpi-patients`, `kpi-drugs`, `kpi-cost`, `kpi-match`
- Default value is "—" (em dash) — populated by callbacks in Phase 3
- Helper `_kpi_card()` builds individual cards with CSS classes from nhs.css
- Created `dash_app/components/filter_bar.py` with `make_filter_bar()`:
- Chart type toggle pills: "By Directory" (active) / "By Indication" with `.toggle-pill` CSS
- Button IDs: `chart-type-directory`, `chart-type-indication` with `n_clicks=0` for callbacks
- Initiated dropdown via `dcc.Dropdown`: All years (default), Last 2 years, Last 1 year
- Last seen dropdown via `dcc.Dropdown`: Last 6 months (default), Last 12 months
- Used `dcc.Dropdown` (not `html.Select`) for native Dash callback compatibility
- Drug/directorate dropdowns omitted — those go in the drawer (Phase 4)
- Added `.filter-dropdown` CSS to nhs.css for dcc.Dropdown sizing within filter bar
- Created `dash_app/components/chart_card.py` with `make_chart_card()`:
- Card header with title "Patient Pathway Visualization" and dynamic subtitle (`id="chart-subtitle"`)
- Default subtitle: "Trust → Directorate → Drug → Patient Pathway"
- Tab row: Icicle (active), Sankey (disabled), Timeline (disabled)
- `dcc.Graph(id="pathway-chart")` with min-height 500px and display config
- Sankey/Timeline tabs are `disabled=True` placeholders for future expansion
- Updated `dash_app/app.py`:
- Added imports for `make_kpi_row`, `make_filter_bar`, `make_chart_card`
- Main content area now: `[make_kpi_row(), make_filter_bar(), make_chart_card()]`
- Removed placeholder text
### Validation results:
- Tier 1 (Code): All imports pass — kpi_row, filter_bar, chart_card, app all import OK
- Tier 1 (App starts): `python run_dash.py` → "Dash is running on http://127.0.0.1:8050/" — no errors
- Tier 2 (Layout): Verified programmatically:
- MantineProvider has 6 children (3 stores + header + sidebar + main)
- Main has 3 children: Section(kpi-row), Section(filter-bar), Section(chart-card)
- All 13 expected IDs found: app-state, chart-data, reference-data, kpi-patients, kpi-drugs, kpi-cost, kpi-match, chart-type-directory, chart-type-indication, filter-initiated, filter-last-seen, pathway-chart, chart-subtitle
### Files changed:
- `dash_app/components/kpi_row.py` — NEW: 4 KPI cards with callback-updatable values
- `dash_app/components/filter_bar.py` — NEW: toggle pills + date filter dropdowns
- `dash_app/components/chart_card.py` — NEW: chart header + tabs + dcc.Graph
- `dash_app/app.py` — Updated: imports and assembles new components in main area
- `dash_app/assets/nhs.css` — Added: `.filter-dropdown` CSS for dcc.Dropdown styling
- `IMPLEMENTATION_PLAN.md` — Task 2.2 items marked [x]
### Committed: [pending]
### Patterns discovered:
- `dcc.Dropdown` must be used instead of `html.Select` for Dash callback compatibility. `html.Select` doesn't have a `value` property that triggers callbacks. `dcc.Dropdown` renders its own React-Select widget that needs CSS customization to match the `.filter-select` styling from the HTML concept.
- `html.Button` with `n_clicks=0` works for toggle pills — callbacks can use `callback_context.triggered_id` to determine which button was clicked.
- `dcc.Graph` accepts a `config` dict for mode bar customization (removing lasso/select, hiding logo).
- Unicode arrow `\u2192` (→) used in chart subtitle for cross-platform compatibility.
### Next iteration should:
- Start Task 2.3 — Footer + full page assembly
- Create `dash_app/components/footer.py` with `make_footer()` using `.page-footer` CSS class
- Add footer to the main content area in `app.py` (below chart card)
- Verify full page layout matches `01_nhs_classic.html` visually
- The 3 dcc.Store components are already in place (added in Phase 0), so 2.3's store bullet is already done
- After 2.3, Phase 2 will be complete — Phase 3 (Core Callbacks) is next
- Phase 3.1 will need: reference data loading callback (on page load), filter state management callback (toggle pills + dropdowns → app-state), and `register_callbacks(app)` wiring
### Blocked items:
- None