diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 8e56cb4..b973310 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -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 diff --git a/dash_app/app.py b/dash_app/app.py index 8fbbfef..34d8d4d 100644 --- a/dash_app/app.py +++ b/dash_app/app.py @@ -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(), ], ), ], diff --git a/dash_app/assets/nhs.css b/dash_app/assets/nhs.css index 5bb1a29..d3b6872 100644 --- a/dash_app/assets/nhs.css +++ b/dash_app/assets/nhs.css @@ -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); diff --git a/dash_app/components/chart_card.py b/dash_app/components/chart_card.py new file mode 100644 index 0000000..04974c3 --- /dev/null +++ b/dash_app/components/chart_card.py @@ -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"], + }, + ), + ], + ) diff --git a/dash_app/components/filter_bar.py b/dash_app/components/filter_bar.py new file mode 100644 index 0000000..060cc98 --- /dev/null +++ b/dash_app/components/filter_bar.py @@ -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", + ), + ], + ), + ], + ) diff --git a/dash_app/components/kpi_row.py b/dash_app/components/kpi_row.py new file mode 100644 index 0000000..3f12159 --- /dev/null +++ b/dash_app/components/kpi_row.py @@ -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"), + ], + ) diff --git a/progress.txt b/progress.txt index 3f248c9..76f4aca 100644 --- a/progress.txt +++ b/progress.txt @@ -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