# Phase 10 Design Specification ## Aesthetic Direction **Utilitarian clinical** — authoritative, data-dense, no decoration. Every element earns its screen real estate. The NHS brand palette is law. The hierarchy is: 1. Header (identity + live metrics) 2. Sub-header (global controls — always visible, always the same) 3. Sidebar (view switching) 4. Content (view-specific) Vertical rhythm: header 56px → sub-header 44px → content starts at 100px from top. --- ## 1. Header Redesign ### Layout ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ [NHS] HCD Analysis │ 3,847 / 11,118 39 / 42 £48.2M / £130.6M │ ● 11,118 patients Updated 2h ago │ │ BRAND │ patients drugs cost │ FRESHNESS │ └─────────────────────────────────────────────────────────────────────────┘ ``` The header stays 56px tall. The breadcrumb is REMOVED (it was redundant — the sidebar shows where you are). The middle section becomes **3 inline fraction KPIs**. The right section stays as data freshness. ### HTML Structure (Dash) ```python html.Header(className="top-header", children=[ # Left: brand (unchanged) html.Div(className="top-header__brand", children=[ html.Div("NHS", className="top-header__logo"), html.Div(html.Div("HCD Analysis", className="top-header__title")), ]), # Center: fraction KPIs html.Div(className="top-header__kpis", children=[ html.Div(className="header-kpi", children=[ html.Span("—", id="kpi-filtered-patients", className="header-kpi__num"), html.Span(" / ", className="header-kpi__sep"), html.Span("—", id="kpi-total-patients", className="header-kpi__den"), html.Span("patients", className="header-kpi__label"), ]), html.Div(className="header-kpi", children=[ html.Span("—", id="kpi-filtered-drugs", className="header-kpi__num"), html.Span(" / ", className="header-kpi__sep"), html.Span("—", id="kpi-total-drugs", className="header-kpi__den"), html.Span("drugs", className="header-kpi__label"), ]), html.Div(className="header-kpi", children=[ html.Span("—", id="kpi-filtered-cost", className="header-kpi__num"), html.Span(" / ", className="header-kpi__sep"), html.Span("—", id="kpi-total-cost", className="header-kpi__den"), html.Span("cost", className="header-kpi__label"), ]), ]), # Right: data freshness (unchanged structure, same IDs) html.Div(className="top-header__right", children=[ html.Span(children=[ html.Span(className="status-dot"), html.Span("...", id="header-record-count"), ]), html.Span(children=[ "Updated: ", html.Span("...", id="header-last-updated"), ]), ]), ]) ``` ### CSS — New Classes ```css /* ── Header KPIs ── */ .top-header__kpis { display: flex; align-items: center; gap: 24px; } .header-kpi { display: flex; align-items: baseline; gap: 3px; color: rgba(255, 255, 255, 0.6); font-size: 13px; font-weight: 400; white-space: nowrap; } .header-kpi__num { color: var(--nhs-white); font-size: 16px; font-weight: 700; font-variant-numeric: tabular-nums; } .header-kpi__sep { color: rgba(255, 255, 255, 0.35); font-weight: 300; font-size: 14px; margin: 0 1px; } .header-kpi__den { color: rgba(255, 255, 255, 0.5); font-size: 13px; font-weight: 400; font-variant-numeric: tabular-nums; } .header-kpi__label { color: rgba(255, 255, 255, 0.4); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; margin-left: 4px; } ``` ### CSS — Modified Classes Remove `.top-header__breadcrumb` usage (delete from header.py, CSS can stay for backward compat or be removed). ### Callback IDs - **Outputs (filtered values from chart-data)**: `kpi-filtered-patients`, `kpi-filtered-drugs`, `kpi-filtered-cost` - **Outputs (total values from reference-data)**: `kpi-total-patients`, `kpi-total-drugs`, `kpi-total-cost` - **Existing (unchanged)**: `header-record-count`, `header-last-updated` --- ## 2. Global Filter Sub-Header ### Layout ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ VIEW [By Directory] [By Indication] │ INITIATED [All years ▾] LAST SEEN [Last 6 months ▾] │ └─────────────────────────────────────────────────────────────────────────┘ ``` Sits directly below the header. Fixed position. Full width minus sidebar. Light blue-grey background (`#E8F0FE` — the same tint used for active sidebar items) with a subtle bottom border. Contains ONLY the chart type toggle and date filters — no drug/trust/directorate buttons. ### HTML Structure (Dash) ```python html.Div(className="sub-header", children=[ # Chart type toggle html.Div(className="sub-header__group", children=[ html.Span("View", className="sub-header__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="sub-header__divider"), # Date filters html.Div(className="sub-header__group", children=[ html.Span("Initiated", className="sub-header__label"), dcc.Dropdown(id="filter-initiated", ...same options..., className="filter-dropdown"), ]), html.Div(className="sub-header__group", children=[ html.Span("Last seen", className="sub-header__label"), dcc.Dropdown(id="filter-last-seen", ...same options..., className="filter-dropdown"), ]), ]) ``` ### CSS — New Classes ```css /* ── Global Filter Sub-Header ── */ .sub-header { position: fixed; top: 56px; /* below main header */ left: var(--sidebar-w); /* right of sidebar */ right: 0; z-index: 150; height: 44px; background: #E8F0FE; border-bottom: 1px solid #C5D4E8; display: flex; align-items: center; padding: 0 24px; gap: 16px; } .sub-header__group { display: flex; align-items: center; gap: 8px; } .sub-header__label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--nhs-dark-blue); white-space: nowrap; opacity: 0.6; } .sub-header__divider { width: 1px; height: 24px; background: rgba(0, 48, 135, 0.15); } ``` ### CSS — Modified Classes `.main` top margin increases from 56px to 100px (56px header + 44px sub-header): ```css .main { margin-left: var(--sidebar-w); margin-top: 100px; /* was 56px */ padding: 24px; min-height: calc(100vh - 100px); /* was 56px */ display: flex; flex-direction: column; gap: 20px; } ``` `.sidebar` top position increases to 56px (stays below main header, sub-header floats over content area): Actually, the sidebar should start below the header (56px), and the sub-header should start at the right of the sidebar. The sidebar extends from 56px to bottom. The sub-header is only in the content area. ``` ┌──────────────────────────────────────────────────┐ │ HEADER (56px) │ ├────────┬─────────────────────────────────────────┤ │ │ SUB-HEADER (44px) │ │ SIDE ├─────────────────────────────────────────┤ │ BAR │ │ │ (240) │ CONTENT AREA │ │ │ │ └────────┴─────────────────────────────────────────┘ ``` --- ## 3. Trust Comparison Landing Page ### Layout A clean selector grid. Each button is a card-like element showing the directorate/indication name. Arranged in a responsive grid — 3 columns for ~14 directorates, 4 columns for ~32 indications. ``` ┌─────────────────────────────────────────────────────┐ │ Trust Comparison │ │ Select a directorate to compare drug usage across │ │ trusts. │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │CARDIOLOGY│ │DERMATOL- │ │DIABETIC │ │ │ │ │ │OGY │ │MEDICINE │ │ │ │ 847 pts │ │ 423 pts │ │ 312 pts │ │ │ │ 12 drugs│ │ 8 drugs│ │ 6 drugs│ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │GASTRO- │ │CLINICAL │ │MEDICAL │ │ │ │ENTEROLOGY│ │HAEMATOL..│ │ONCOLOGY │ │ │ │ 298 pts │ │ 567 pts │ │ 234 pts │ │ │ │ 11 drugs│ │ 15 drugs│ │ 9 drugs│ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ ... │ └─────────────────────────────────────────────────────┘ ``` Each card shows: directorate name (bold), patient count, drug count. Sorted by patient count descending. The blue left border on hover provides the NHS accent. ### HTML Structure (Dash) ```python html.Div(className="tc-landing", id="trust-comparison-landing", children=[ # Header 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", ), ]), # Grid of directorate cards html.Div(className="tc-landing__grid", id="tc-landing-grid", children=[ # Populated by callback — one per directorate/indication # Each card: html.Button( className="tc-card", id={"type": "tc-selector", "index": "CARDIOLOGY"}, n_clicks=0, children=[ html.Div("CARDIOLOGY", className="tc-card__name"), html.Div(className="tc-card__stats", children=[ html.Span("847 patients", className="tc-card__stat"), html.Span("·", className="tc-card__dot"), html.Span("12 drugs", className="tc-card__stat"), ]), ], ), # ... more cards ]), ]) ``` ### CSS — New Classes ```css /* ── 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-bottom: 4px; } .tc-landing__desc { font-size: 14px; color: var(--nhs-mid-grey); font-weight: 400; } .tc-landing__grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; } /* 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); } ``` For indication mode (~32 buttons), switch to 4 columns: ```css /* Use this class when chart_type == "indication" */ .tc-landing__grid--wide { grid-template-columns: repeat(4, 1fr); } ``` --- ## 4. Trust Comparison 6-Chart Dashboard ### Layout 2-column × 3-row grid of chart cards. Each card has a small title and a `dcc.Graph`. A sticky top bar shows the selected directorate name + back button. ``` ┌─────────────────────────────────────────────────────┐ │ ← Back RHEUMATOLOGY — Trust Comparison │ ├────────────────────────┬────────────────────────────┤ │ Market Share │ Cost Waterfall │ │ ┌──────────────────┐ │ ┌──────────────────────┐ │ │ │ dcc.Graph │ │ │ dcc.Graph │ │ │ └──────────────────┘ │ └──────────────────────┘ │ ├────────────────────────┼────────────────────────────┤ │ Dosing Intervals │ Drug × Trust Heatmap │ │ ┌──────────────────┐ │ ┌──────────────────────┐ │ │ │ dcc.Graph │ │ │ dcc.Graph │ │ │ └──────────────────┘ │ └──────────────────────┘ │ ├────────────────────────┼────────────────────────────┤ │ Treatment Duration │ Cost Effectiveness │ │ ┌──────────────────┐ │ ┌──────────────────────┐ │ │ │ dcc.Graph │ │ │ dcc.Graph │ │ │ └──────────────────┘ │ └──────────────────────┘ │ └────────────────────────┴────────────────────────────┘ ``` ### HTML Structure (Dash) ```python html.Div(className="tc-dashboard", id="trust-comparison-dashboard", children=[ # Dashboard header with back button html.Div(className="tc-dashboard__header", children=[ html.Button("← Back", id="tc-back-btn", className="tc-dashboard__back", n_clicks=0), html.H2(id="tc-dashboard-title", className="tc-dashboard__title", children="RHEUMATOLOGY — Trust Comparison"), ]), # 6-chart grid 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 × Trust Heatmap", "tc-chart-heatmap"), _tc_chart_cell("Treatment Duration", "tc-chart-duration"), _tc_chart_cell("Cost Effectiveness", "tc-chart-cost-effectiveness"), ]), ]) ``` Helper for each chart cell: ```python def _tc_chart_cell(title, graph_id): 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"}, ), ]), ]) ``` ### CSS — New Classes ```css /* ── 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); } /* 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); } ``` --- ## 5. Patient Pathways Filter Placement ### Approach The drug/trust/directorate filter buttons sit in a **secondary filter strip** directly below the global sub-header. This strip is ONLY rendered when `active_view == "patient-pathways"`. It's a slimmer, lighter bar that reads as "view-specific controls" vs the sub-header's "global controls." ``` ┌──────────────────────────────────────────────────────┐ ← HEADER (always) ├────────────────────────────────────────────────────── │ ← SUB-HEADER (always) ├──────────────────────────────────────────────────────┤ │ Drugs (3) Trusts (2) Directorates │ Clear All │ ← PATHWAY FILTERS (Patient Pathways only) ├──────────────────────────────────────────────────────┤ │ │ │ [chart card with tabs + graph] │ │ │ └──────────────────────────────────────────────────────┘ ``` This strip uses the existing `.filter-btn` classes. It's rendered as part of the Patient Pathways view content (not fixed position) — it scrolls with the content. ### HTML Structure (Dash) ```python # This goes inside the Patient Pathways view, at the top of its content area html.Div(className="pathway-filters", id="pathway-filters", children=[ html.Div(className="pathway-filters__buttons", children=[ html.Button(children=[ "Drugs", html.Span(id="drug-count-badge", className="filter-btn__badge filter-btn__badge--hidden"), ], id="open-drug-modal", className="filter-btn", n_clicks=0), html.Button(children=[ "Trusts", html.Span(id="trust-count-badge", className="filter-btn__badge filter-btn__badge--hidden"), ], id="open-trust-modal", className="filter-btn", n_clicks=0), html.Button(children=[ "Directorates", html.Span(id="directorate-count-badge", className="filter-btn__badge filter-btn__badge--hidden"), ], id="open-directorate-modal", className="filter-btn", n_clicks=0), ]), html.Button("Clear All", id="clear-all-filters", className="filter-btn filter-btn--clear", n_clicks=0), ]) ``` ### CSS — New Classes ```css /* ── Patient Pathways Filter Strip ── */ .pathway-filters { background: var(--nhs-white); border: 1px solid var(--nhs-pale-grey); border-bottom: 2px solid var(--nhs-blue); padding: 8px 20px; display: flex; align-items: center; justify-content: space-between; } .pathway-filters__buttons { display: flex; align-items: center; gap: 8px; } ``` The bottom border `2px solid nhs-blue` gives it a subtle "active" feel that connects it visually to the chart content below. --- ## Page Structure Summary ### app.py Layout Assembly (Phase 10) ```python app.layout = dmc.MantineProvider(children=[ # State stores dcc.Store(id="app-state", storage_type="session", data={ "chart_type": "directory", "initiated": "all", "last_seen": "6mo", "date_filter_id": "all_6mo", "selected_drugs": [], "selected_directorates": [], "selected_trusts": [], "active_view": "patient-pathways", "selected_comparison_directorate": None, }), dcc.Store(id="chart-data", storage_type="memory"), dcc.Store(id="reference-data", storage_type="session"), dcc.Store(id="active-tab", storage_type="memory", data="icicle"), dcc.Location(id="url", refresh=False), # Page structure make_header(), # Fixed, 56px, dark blue make_sidebar(), # Fixed, 240px left, below header make_sub_header(), # Fixed, 44px, light blue, right of sidebar make_modals(), # Filter modals (drug, trust, directorate) html.Main(className="main", children=[ # Content switched by active_view html.Div(id="view-container", children=[ # Patient Pathways view html.Div(id="patient-pathways-view", children=[ make_pathway_filters(), # Drug/trust/directorate buttons make_chart_card(), # Tab bar + chart (Icicle + Sankey only) ]), # Trust Comparison view html.Div(id="trust-comparison-view", style={"display": "none"}, children=[ make_tc_landing(), # Directorate selector grid make_tc_dashboard(), # 6-chart dashboard (hidden initially) ]), ]), make_footer(), ]), ]) ``` ### Sidebar Changes ```python def make_sidebar(): return html.Nav(className="sidebar", **{"aria-label": "Main navigation"}, children=[ html.Div(className="sidebar__section", children=[ html.Div("Analysis", className="sidebar__label"), _sidebar_item("Patient Pathways", "pathway", active=True, item_id="nav-patient-pathways"), _sidebar_item("Trust Comparison", "compare", active=False, item_id="nav-trust-comparison"), ]), html.Div(className="sidebar__footer", children=[ "NHS Norfolk & Waveney ICB", html.Br(), "High Cost Drugs Programme", ]), ]) ``` New icon needed for "compare": ```python _ICONS = { "pathway": '...', # existing "compare": '', # bar chart icon } ``` ### View Switching Callback ```python @app.callback( Output("patient-pathways-view", "style"), Output("trust-comparison-view", "style"), Output("nav-patient-pathways", "className"), Output("nav-trust-comparison", "className"), Input("app-state", "data"), ) def switch_view(app_state): view = app_state.get("active_view", "patient-pathways") show = {} hide = {"display": "none"} active_cls = "sidebar__item sidebar__item--active" inactive_cls = "sidebar__item" if view == "patient-pathways": return show, hide, active_cls, inactive_cls else: return hide, show, inactive_cls, active_cls ``` --- ## CSS Variable Additions ```css :root { /* ... existing ... */ --sub-header-h: 44px; --header-total-h: 100px; /* 56px header + 44px sub-header */ } ``` Update `.main`: ```css .main { margin-left: var(--sidebar-w); margin-top: var(--header-total-h); padding: 24px; min-height: calc(100vh - var(--header-total-h)); display: flex; flex-direction: column; gap: 20px; } ``` --- ## Responsive Adjustments ```css @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: 768px) { .sidebar { display: none; } .main { margin-left: 0; } .sub-header { left: 0; } .tc-landing__grid { grid-template-columns: 1fr; } .tc-dashboard__grid { grid-template-columns: 1fr; } } ```