diff --git a/README.md b/README.md index f1dacea..240a083 100644 --- a/README.md +++ b/README.md @@ -229,12 +229,6 @@ python -m data_processing.migrate 2. A browser window will open for SSO authentication 3. Verify your network allows Snowflake connections -## Documentation - -- [CLAUDE.md](CLAUDE.md) — Technical architecture documentation -- [docs/USER_GUIDE.md](docs/USER_GUIDE.md) — End-user guide -- [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) — Deployment guide - ## License Internal NHS use only. Not for distribution. diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md deleted file mode 100644 index 07cbc5f..0000000 --- a/docs/DEPLOYMENT.md +++ /dev/null @@ -1,296 +0,0 @@ -# Deployment Guide - -This guide covers deployment options for the Patient Pathway Analysis web application built with Dash. - -## Overview - -The application is a single-process Python Dash app that serves both the frontend and API from one server. It reads pre-computed data from a local SQLite database. - -## Development Mode - -For local development: - -```bash -# Start development server with hot reload -python run_dash.py - -# Access the application at http://localhost:8050 -``` - -## Production Deployment Options - -### Option 1: Simple Production (Single Server) - -The simplest approach for internal deployments: - -```bash -# Run with Gunicorn (Linux/macOS) -gunicorn dash_app.app:server -b 0.0.0.0:8050 --workers 4 - -# Or directly with Python -python run_dash.py -``` - -For background execution: - -```bash -# Using nohup (Linux/macOS) -nohup gunicorn dash_app.app:server -b 0.0.0.0:8050 --workers 4 > dash.log 2>&1 & - -# Using PowerShell (Windows) -Start-Process -NoNewWindow -FilePath "python" -ArgumentList "run_dash.py" -``` - -### Option 2: Docker Deployment - -Create a `Dockerfile` for containerized deployment: - -```dockerfile -FROM python:3.11-slim - -WORKDIR /app - -# Install uv for fast dependency management -RUN pip install uv - -# Copy dependency files -COPY pyproject.toml uv.lock ./ - -# Install dependencies -RUN uv sync --no-dev - -# Copy application code -COPY src/ src/ -COPY dash_app/ dash_app/ -COPY data/ data/ -COPY run_dash.py setup_dev.py ./ - -# Set up Python path -RUN uv run python setup_dev.py - -# Expose port -EXPOSE 8050 - -# Start the application -CMD ["uv", "run", "gunicorn", "dash_app.app:server", "-b", "0.0.0.0:8050", "--workers", "4"] -``` - -Build and run: - -```bash -# Build the image -docker build -t pathway-analysis . - -# Run the container -docker run -p 8050:8050 \ - -v $(pwd)/data:/app/data \ - pathway-analysis -``` - -### Option 3: Docker Compose - -```yaml -version: '3.8' - -services: - app: - build: . - ports: - - "8050:8050" - volumes: - - ./data:/app/data - - ./src/config:/app/src/config - restart: unless-stopped -``` - -Run with: - -```bash -docker-compose up -d -``` - -## Reverse Proxy Configuration - -### Nginx - -For production deployments behind nginx: - -```nginx -server { - listen 80; - server_name your-server.nhs.uk; - - location / { - proxy_pass http://localhost:8050; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } -} -``` - -Enable the site: - -```bash -sudo ln -s /etc/nginx/sites-available/pathway-analysis /etc/nginx/sites-enabled/ -sudo nginx -t && sudo systemctl reload nginx -``` - -## Process Management - -### Systemd (Linux) - -```ini -# /etc/systemd/system/pathway-analysis.service -[Unit] -Description=Pathway Analysis Dash App -After=network.target - -[Service] -Type=simple -User=www-data -WorkingDirectory=/opt/pathway-analysis -ExecStart=/opt/pathway-analysis/.venv/bin/gunicorn dash_app.app:server -b 0.0.0.0:8050 --workers 4 -Restart=always -RestartSec=10 - -[Install] -WantedBy=multi-user.target -``` - -Enable and start: - -```bash -sudo systemctl daemon-reload -sudo systemctl enable pathway-analysis -sudo systemctl start pathway-analysis -``` - -### Windows Service - -Use NSSM (Non-Sucking Service Manager) on Windows: - -```powershell -# Install NSSM -choco install nssm - -# Create service -nssm install PathwayAnalysis "C:\Path\To\python.exe" "run_dash.py" -nssm set PathwayAnalysis AppDirectory "C:\Path\To\pathway-analysis" -nssm start PathwayAnalysis -``` - -## Environment Configuration - -### Production Environment Variables - -```bash -# Database path (if using custom location) -export PATHWAY_DB_PATH=/var/data/pathways.db - -# Snowflake (for data refresh only — not needed for the web app) -export SNOWFLAKE_ACCOUNT=your-account -export SNOWFLAKE_WAREHOUSE=your-warehouse -``` - -### Snowflake Configuration - -Snowflake is only needed for the data refresh CLI command, not for running the web application. Ensure `src/config/snowflake.toml` is configured: - -```toml -[snowflake] -account = "your-production-account" -warehouse = "ANALYTICS_WH" -database = "DATA_HUB" -schema = "CDM" -authenticator = "externalbrowser" -``` - -## Data Refresh - -The web application reads pre-computed data from SQLite. To update the data: - -```bash -# Full refresh (both chart types, all date filters) -python -m cli.refresh_pathways --chart-type all - -# The app will serve new data immediately — no restart needed -``` - -Schedule this as a cron job or Windows Task Scheduler task for periodic updates. - -## Security Considerations - -### Network Security - -1. **Firewall Rules**: Only expose port 8050 (or 80/443 behind reverse proxy) -2. **HTTPS**: Use TLS certificates via reverse proxy (nginx, Caddy) -3. **VPN**: Consider restricting access to NHS network only - -### Data Security - -1. **Database Access**: The app uses read-only SQLite access -2. **No file uploads**: The Dash app does not accept file uploads -3. **No authentication built in**: Add authentication via reverse proxy or middleware if needed - -## Monitoring - -### Health Checks - -The application serves at `/` — a 200 response indicates the app is running. - -### Logging - -Dash outputs request logs to stdout. Configure log aggregation as needed: - -```bash -# Redirect logs to file -gunicorn dash_app.app:server -b 0.0.0.0:8050 --access-logfile /var/log/pathway-analysis/access.log --error-logfile /var/log/pathway-analysis/error.log -``` - -## Troubleshooting - -### Port already in use - -```bash -# Find process using port 8050 -lsof -i :8050 # Linux/macOS -netstat -ano | findstr :8050 # Windows -``` - -### Database not found - -```bash -# Verify database exists -ls -la data/pathways.db -sqlite3 data/pathways.db ".tables" - -# Recreate if needed -python -m data_processing.migrate -python -m cli.refresh_pathways --chart-type all -``` - -### Import errors - -```bash -# Ensure src/ is on Python path -uv run python setup_dev.py - -# Verify imports -uv run python -c "from dash_app.app import app; print('OK')" -``` - ---- - -## Quick Reference - -| Environment | Command | Port | -|-------------|---------|------| -| Development | `python run_dash.py` | 8050 | -| Production | `gunicorn dash_app.app:server -b 0.0.0.0:8050 --workers 4` | 8050 | -| Docker | `docker run -p 8050:8050 pathway-analysis` | 8050 | - -For more information, see: -- [Dash Documentation](https://dash.plotly.com/) -- [Gunicorn Deployment](https://docs.gunicorn.org/en/stable/deploy.html) diff --git a/docs/DESIGN_SYSTEM.md b/docs/DESIGN_SYSTEM.md deleted file mode 100644 index 498b751..0000000 --- a/docs/DESIGN_SYSTEM.md +++ /dev/null @@ -1,194 +0,0 @@ -# Design System - HCD Analysis v2.1 (SaaS Redesign) - -This document defines the visual design language for the UI redesign. The goal is a **modern SaaS aesthetic** - think Stripe, Linear, Vercel - while staying thematically aligned with the blue color palette. - -**Design Philosophy**: -- The chart is the hero; everything else supports it -- Minimal chrome, maximum data visibility -- Clean, confident, spacious - not clinical or governmental -- Every pixel of vertical space matters - -## Color Palette - -### Primary Blues (kept from original, used sparingly) -| Name | Hex | Usage | -|------|-----|-------| -| Heritage Blue | `#003087` | Top bar background, strong accents | -| Primary Blue | `#0066CC` | Interactive elements, links, focus | -| Vibrant Blue | `#1E88E5` | Hover states, active elements | -| Sky Blue | `#4FC3F7` | Subtle accents, progress indicators | -| Pale Blue | `#E3F2FD` | Selected states, subtle backgrounds | - -### Neutrals (refined for modern feel) -| Name | Hex | Usage | -|------|-----|-------| -| Slate 900 | `#0F172A` | Primary text (slightly darker) | -| Slate 700 | `#334155` | Secondary text | -| Slate 500 | `#64748B` | Muted text, placeholders | -| Slate 300 | `#CBD5E1` | Borders, dividers | -| Slate 100 | `#F8FAFC` | Backgrounds (slightly lighter) | -| White | `#FFFFFF` | Card/modal backgrounds | - -### Semantic Colors -| Name | Hex | Usage | -|------|-----|-------| -| Success | `#10B981` | Positive (modern green) | -| Warning | `#F59E0B` | Caution | -| Error | `#EF4444` | Errors | -| Info | `#3B82F6` | Informational | - -## Typography - -**Font Family:** Inter (primary), system-ui (fallback) - -| Style | Size | Weight | Usage | -|-------|------|--------|-------| -| Display | 28px | 600 | Page titles (reduced from 32px) | -| Heading 1 | 18px | 600 | Section headers (reduced from 24px) | -| Heading 2 | 16px | 600 | Card titles (reduced from 20px) | -| Heading 3 | 14px | 600 | Subsections | -| Body | 14px | 400 | Default text | -| Body Small | 13px | 400 | Secondary info | -| Caption | 11px | 500 | Labels, metadata (reduced from 12px) | -| Mono | 13px | 500 | Data values (JetBrains Mono) | - -## Spacing Scale (Tighter) - -| Token | Value | Usage | -|-------|-------|-------| -| xs | 4px | Tight gaps | -| sm | 6px | Between related elements (was 8px) | -| md | 8px | Standard gaps (was 12px) | -| lg | 12px | Section padding (was 16px) | -| xl | 16px | Card padding (was 24px) | -| 2xl | 24px | Major gaps (was 32px) | -| 3xl | 32px | Page margins (was 48px) | - -## Layout Specifications - -### Page Structure (Target) -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Logo │ Tabs │ Freshness │ 48px -├─────────────────────────────────────────────────────────────────┤ -│ [Initiated▾] [LastSeen▾] │ [Drugs▾] [Ind▾] [Dir▾] │ KPI badges │ 48px -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ I C I C L E C H A R T │ flex -│ (full viewport width) │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### Top Bar -- **Height**: 48px (reduced from 64px) -- **Background**: Heritage Blue -- **Logo**: 28px height (reduced from 36px) -- **Tabs**: Small pills, 28px height - -### Filter Strip -- **Height**: 48px (single row) -- **Layout**: Horizontal flex, all filters inline -- **Dropdown triggers**: 32px height, 8px padding -- **No section header** - labels are in dropdown triggers -- **Background**: Slate 100 or transparent - -### KPI Section (Options) - -**Option A: Inline badges** (preferred - zero extra height) -``` -Filters row: [Initiated▾] [LastSeen▾] | [Drugs▾] ... | 12,345 patients • £45.2M • 89 drugs -``` - -**Option B: Compact strip** (48px max) -``` -┌─────┬─────┬─────┬─────┐ -│12.3K│£45M │ 89 │ 7 │ 28px value -│pts │cost │drugs│trust│ 14px label -└─────┴─────┴─────┴─────┘ -``` - -### Chart Container -- **Width**: Full viewport minus 32px (16px padding each side) -- **Height**: Fill remaining space (min 500px) -- **No max-width constraint** -- **Margins**: Minimal (t:40, l:8, r:8, b:24) - -## Component Specifications - -### Compact Dropdown Trigger -- Height: 32px -- Padding: 8px 12px -- Border: 1px Slate 300 -- Border radius: 6px -- Font: 13px -- Chevron: 14px icon - -### Compact KPI Badge -- Padding: 4px 12px -- Border radius: 16px (pill) -- Background: Slate 100 -- Value: 14px mono, weight 600 -- Label: 11px, Slate 500 - -### Searchable Dropdown Panel -- Max height: 200px (items area) -- Item padding: 6px 8px -- Search input height: 28px -- Width: 240px min - -## Shadows - -| Token | Value | Usage | -|-------|-------|-------| -| sm | `0 1px 2px rgba(0,0,0,0.04)` | Subtle (lighter) | -| md | `0 1px 3px rgba(0,0,0,0.06)` | Cards at rest | -| lg | `0 4px 8px rgba(0,0,0,0.08)` | Dropdowns, hover | - -## Border Radius - -| Token | Value | Usage | -|-------|-------|-------| -| sm | 4px | Small elements | -| md | 6px | Inputs, buttons | -| lg | 8px | Cards | -| full | 9999px | Pills, badges | - -## Transitions - -All transitions: 150ms ease-out (faster than before) - -## Implementation Notes - -### Key Changes from v2.0 -1. **Vertical space reduction**: ~210px saved (364px → ~156px overhead) -2. **Full-width chart**: Remove PAGE_MAX_WIDTH for chart -3. **Inline KPIs**: Either badges in filter row or minimal strip -4. **Smaller fonts**: Headlines and captions reduced -5. **Tighter spacing**: All spacing tokens reduced by ~25% - -### CSS Patterns -```css -/* Full-height chart container */ -.chart-container { - height: calc(100vh - 96px); /* viewport minus top bar + filter strip */ - min-height: 500px; - width: calc(100vw - 32px); - margin: 0 16px; -} - -/* Filter strip */ -.filter-strip { - display: flex; - align-items: center; - height: 48px; - gap: 12px; - padding: 0 16px; -} -``` - -### Dash Implementation -- Chart container uses `dcc.Loading` wrapper around `dcc.Graph` -- Full-width layout via CSS class `.chart-card` in `dash_app/assets/nhs.css` -- Minimum height set via CSS: `min-height: 500px` -- Margins controlled in `create_icicle_from_nodes()`: `t:40, l:8, r:8, b:24` diff --git a/docs/PHASE10_DESIGN.md b/docs/PHASE10_DESIGN.md deleted file mode 100644 index 6380019..0000000 --- a/docs/PHASE10_DESIGN.md +++ /dev/null @@ -1,740 +0,0 @@ -# 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; } -} -``` diff --git a/docs/SNOWFLAKE_REFERENCE.md b/docs/SNOWFLAKE_REFERENCE.md deleted file mode 100644 index 160bc6a..0000000 --- a/docs/SNOWFLAKE_REFERENCE.md +++ /dev/null @@ -1,192 +0,0 @@ -# Snowflake Reference - -Essential database context for querying NHS data. Read this every iteration when working with Snowflake. - ---- - -## Snowflake MCP Server - -Use `mcp__snowflake-mcp__*` functions to explore schema and test queries. - -### Schema Discovery (USE THESE FIRST) -- `test_connection()` - Verify connectivity -- `list_databases()` - List accessible databases -- `list_schemas(database_name)` - List schemas in a database -- `list_tables(database, schema)` - List tables with descriptions -- `list_views(schema_name, database)` - List views with descriptions -- `describe_table(table_name, database)` - Get detailed table schema -- `describe_query(query, database)` - Preview query output columns without execution - -### Query Execution -- `read_data(query, database, max_rows)` - Execute SELECT queries with row limits -- `read_data_paginated(query, database, page_size, page)` - Paginated results with total count -- `read_data_pandas(query, database, max_rows, output_format)` - Results in pandas-friendly formats - -### Async Query Support (long-running queries) -- `execute_async(query, database)` - Submit asynchronously, returns query_id -- `get_query_status(query_id, database)` - Check status -- `get_async_results(query_id, database, max_rows)` - Retrieve results - -### Usage Guidelines -- **ALWAYS** verify table structures and column names via MCP before writing queries -- Test with small result sets (`LIMIT 20`) before full execution -- Use `describe_query` to preview complex query outputs before running -- Use async queries for operations expected to take >30 seconds - ---- - -## Database Overview - -| Database | Purpose | -|----------|---------| -| `DATA_HUB` | **Analyst-curated** data warehouse - primary source for most queries | -| `PRIMARY_CARE` | Raw extracts from EMIS and TPP clinical systems | -| `NATIONAL` | NHS England national datasets (SUS, ECDS, MHSDS, etc.) | -| `FACTS_AND_DIMENSIONS_ALL_DATA` | External reference data (BNF, SNOMED, QOF clusters) | -| `REPORTING_DATASETS_ICB` | Reporting outputs and analyst workspaces (includes SCRATCHPAD) | - -**Avoid**: `SYSTEM` database. - ---- - -## Key Tables and Views - -### DATA_HUB.DWH (Dimensions) - -| View | Purpose | Key Columns | -|------|---------|-------------| -| `DimMedicineAndDevice` | Master medication/device reference | `ProductSnomedCode`, `TherapeuticMoietySnomedCode` (VTM), `BNFParagraphCode`, `StrengthDescription`, `ProductDescription` | -| `DimPerson` | Patient demographics | `PatientPseudonym`, `PersonKey`, `CurrentGeneralPractice`, `IsCurrentNWRegistered`, `YearMonthBirth` | -| `DimSnomedCode` | SNOMED code descriptions | `SnomedCode`, `SnomedDescription` | -| `DimOrganisationAndSite` | GP practices and NHS orgs | `SiteCode`, `OrganisationName`, `OrganisationSubType`, `IsSiteNorfolkAndWaveney`, `IsSiteActive` | -| `DimDate` | Date dimension | | -| `DimCondition` | Clinical conditions | Long-term condition flags | -| `DimDeprivation` | Deprivation rankings by area | | - -**CRITICAL**: -- `ProductDescription` is the correct column for product names. `ProductName` does NOT exist. -- `IsLatest` does NOT exist in `DimMedicineAndDevice`. - -### DATA_HUB.CDM (Common Data Model) - -| View | Purpose | Key Columns | -|------|---------|-------------| -| `Acute__Conmon__PatientLevelDrugs` | HCD activity data | `PseudoNHSNoLinked`, `InterventionDate`, `DrugName`, `Price Actual` | - -**Note**: HCD `PseudoNHSNoLinked` = GP `PatientPseudonym` for patient linkage. - -### DATA_HUB.PHM (Population Health Management) - -| View | Purpose | Key Columns | -|------|---------|-------------| -| `PrimaryCareClinicalCoding` | **Unified** clinical coding (EMIS + TPP, no duplicates) | `PatientPseudonym`, `SNOMEDCode`, `EventDateTime`, `NumericValue` | -| `PrimaryCareMedication` | **Unified** medication data (EMIS + TPP, no duplicates) | `PatientPseudonym`, `SNOMEDCode`, `DateMedicationStart`, `Quantity` | -| `ClinicalCodingClusterSnomedCodes` | SNOMED codes grouped by cluster | `ClusterId`, `SnomedCode` | -| `PersonCohort` | Pre-defined patient cohorts | | - -**Prefer DATA_HUB.PHM unified views** over raw PRIMARY_CARE tables. - ---- - -## Patient Identifiers - -| Identifier | Source | Usage | -|------------|--------|-------| -| `PatientPseudonym` | DATA_HUB, NATIONAL | Primary - use for most joins | -| `PseudoNHSNoLinked` | DATA_HUB.CDM (HCD data) | Links to PatientPseudonym | -| `PersonKey` | DATA_HUB.DWH.DimPerson | Integer key for person dimension | - -### Standard Join Patterns -```sql --- HCD Activity to GP Diagnosis -FROM DATA_HUB.CDM."Acute__Conmon__PatientLevelDrugs" hcd -LEFT JOIN DATA_HUB.PHM."PrimaryCareClinicalCoding" pcc - ON hcd."PseudoNHSNoLinked" = pcc."PatientPseudonym" - --- Activity to Person Demographics -FROM DATA_HUB.CDM."Acute__Conmon__PatientLevelDrugs" hcd -INNER JOIN DATA_HUB.DWH."DimPerson" dp - ON hcd."PseudoNHSNoLinked" = dp."PatientPseudonym" -``` - ---- - -## CRITICAL: Registered Population Filter - -**ALWAYS** apply when counting patients: - -```sql -WHERE dp."IsCurrentNWRegistered" = 'Yes' - AND dp."CurrentGeneralPractice" <> '*' -``` - -Without this filter, counts will be ~2x inflated (includes deceased, deregistered, out-of-area patients). - ---- - -## Query Development Patterns - -### Clinical Condition Detection (GP SNOMED Clusters) -```sql --- Get all SNOMED codes for a clinical cluster -SELECT "SnomedCode" -FROM DATA_HUB.PHM."ClinicalCodingClusterSnomedCodes" -WHERE "ClusterId" = 'RARTH_COD' -- Rheumatoid arthritis - --- Check if patient has condition -SELECT DISTINCT pcc."PatientPseudonym" -FROM DATA_HUB.PHM."PrimaryCareClinicalCoding" pcc -WHERE pcc."SNOMEDCode" IN (SELECT "SnomedCode" FROM cluster_codes) - AND pcc."PatientPseudonym" IS NOT NULL -``` - -### Available SNOMED Clusters for HCD Indications -- `RARTH_COD` (155 codes) - Rheumatoid arthritis -- `PSORIASIS_COD` (116 codes) - Psoriasis -- `CROHNS_COD` (93 codes) - Crohn's disease -- `ULCCOLITIS_COD` (62 codes) - Ulcerative colitis -- `MS_COD` (44 codes) - Multiple sclerosis -- `DM_COD` / `DMTYPE1_COD` / `DMTYPE2AUDIT_COD` - Diabetes - -### Sample HCD Activity Query -```sql -SELECT - hcd."PseudoNHSNoLinked" AS PatientPseudonym, - hcd."DrugName", - hcd."InterventionDate", - hcd."Provider Code", - hcd."OrganisationName" -FROM DATA_HUB.CDM."Acute__Conmon__PatientLevelDrugs" hcd -WHERE hcd."InterventionDate" >= '2024-01-01' -LIMIT 20 -``` - ---- - -## Snowflake SQL Syntax - -- Double-quote identifiers: `"PatientPseudonym"` -- Date literals: `'2025-04-01'::DATE` -- Date functions: `DATEADD('MONTH', -3, date)`, `DATEDIFF('YEAR', d1, d2)`, `LAST_DAY(date)` -- Boolean: `TRUE`/`FALSE` -- No `TOP N` - use `LIMIT N` -- `COALESCE()`, `NULLIF()`, `GREATEST()` work as expected - ---- - -## Troubleshooting - -### Column not found errors -1. Use `describe_table(table_name, database)` to get actual column names -2. Remember: Snowflake identifiers are case-sensitive when quoted -3. Common mistakes: `ProductName` (wrong) vs `ProductDescription` (correct) - -### Empty results -1. Check patient identifier filtering (`IS NOT NULL`) -2. Check date ranges -3. Test with `LIMIT 20` first to see sample data - -### Slow queries -1. Add `LIMIT` during development -2. Use `describe_query` to validate structure before execution -3. Consider async execution for large result sets diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md deleted file mode 100644 index f404722..0000000 --- a/docs/USER_GUIDE.md +++ /dev/null @@ -1,257 +0,0 @@ -# User Guide - NHS Patient Pathway Analysis Tool - -This guide explains how to use the NHS High-Cost Drug Patient Pathway Analysis Tool to analyze treatment pathways for secondary care patients. - -## Table of Contents - -1. [Getting Started](#getting-started) -2. [Interface Overview](#interface-overview) -3. [Filtering Data](#filtering-data) -4. [Using the Drug Browser](#using-the-drug-browser) -5. [Understanding the Pathway Chart](#understanding-the-pathway-chart) -6. [GP Indication Matching](#gp-indication-matching) -7. [Troubleshooting](#troubleshooting) - ---- - -## Getting Started - -### Accessing the Application - -Start the application by running: - -```bash -python run_dash.py -``` - -Then open your browser to **http://localhost:8050** - -The application automatically loads pre-computed pathway data from SQLite on startup. No additional setup is needed to view existing data. - -### Data Freshness - -The header bar shows when data was last refreshed: -- **Patient count**: Total patients in the dataset (e.g., "11,118 patients") -- **Last updated**: Relative time since the last data refresh (e.g., "2h ago") - -To refresh the data, run the CLI command (requires Snowflake access): - -```bash -python -m cli.refresh_pathways --chart-type all -``` - ---- - -## Interface Overview - -The application is a single-page layout with the following components: - -### Header -- NHS branding and application title ("HCD Analysis") -- Green status dot with patient count and last-updated time - -### Sidebar (Left) -Navigation items including: -- **Pathway Overview** — main view (always active) -- **Drug Selection** — opens the drug browser drawer -- **Trust Selection** — opens the drawer with trust chips -- **Indications** — opens the drawer with directorate browser - -### KPI Row -Four summary cards that update dynamically: -- **Unique Patients** — number of distinct patients matching current filters -- **Drug Types** — number of distinct drugs in filtered data -- **Total Cost** — total cost of treatments in the filtered dataset -- **Indication Match** — GP diagnosis match rate (~93% for indication charts, shown as "—" for directory charts) - -### Filter Bar -- **Chart type toggle**: "By Directory" / "By Indication" pills -- **Treatment Initiated**: All years, Last 2 years, or Last 1 year -- **Last Seen**: Last 6 months or Last 12 months - -### Chart Card -- Dynamic subtitle showing the current hierarchy (e.g., "Trust → Directorate → Drug → Pathway") -- Interactive Plotly icicle chart -- Loading spinner during data fetch - ---- - -## Filtering Data - -### Chart Type - -Toggle between two views using the pills in the filter bar: - -| View | Hierarchy | Best For | -|------|-----------|----------| -| **By Directory** | Trust → Directorate → Drug → Pathway | Understanding treatment by medical specialty | -| **By Indication** | Trust → GP Diagnosis → Drug → Pathway | Understanding treatment by patient condition | - -### Date Filters - -Two dropdowns control the time window: - -| Filter | Options | Effect | -|--------|---------|--------| -| **Treatment Initiated** | All years, Last 2 years, Last 1 year | When patients started treatment | -| **Last Seen** | Last 6 months, Last 12 months | Most recent activity window | - -The default is "All years / Last 6 months" — showing all patients who have been active in the last 6 months. - -### Drug and Trust Selection - -Open the drawer (right panel) by clicking "Drug Selection" or "Trust Selection" in the sidebar: - -- **Drug chips**: Click to select/deselect specific drugs. Selected drugs filter the chart. -- **Trust chips**: Click to select/deselect specific NHS trusts. -- **Clear All Filters**: Button at the bottom resets all drug and trust selections. - -**No selections = show everything.** Leaving chips unselected is the same as selecting all. - ---- - -## Using the Drug Browser - -The drawer contains three sections: - -### All Drugs -A flat list of all 42 available drugs as selectable chips. Click one or more to filter the chart to those drugs only. - -### Trusts -A list of 7 NHS trusts as selectable chips. Click to filter by specific organizations. - -### By Directorate -An accordion browser organized by clinical directorate: - -1. Click a **directorate** (e.g., "CARDIOLOGY") to expand it -2. Inside, click an **indication** (e.g., "heart failure") to expand further -3. Each indication shows **drug fragment badges** (e.g., "SACUBITRIL", "IVABRADINE") -4. Clicking a drug fragment badge selects all full drug names that contain that fragment - -For example, clicking the "ADALIMUMAB" badge would select "ADALIMUMAB" in the drug chips above. - -### Fragment Matching - -Drug fragments are substrings, not exact matches. The fragment "INHALED" would match drugs like "INHALED BECLOMETASONE" and "INHALED FLUTICASONE". - -Clicking a fragment toggles its matching drugs: -- **First click**: Selects all matching drugs -- **Second click**: Deselects all matching drugs (if all were already selected) - ---- - -## Understanding the Pathway Chart - -### Hierarchy Structure - -The icicle chart displays a hierarchical breakdown: - -**Directory view:** -``` -Root (Regional Total) - └─ Trust (e.g., "Norfolk and Norwich University Hospitals") - └─ Directorate (e.g., "RHEUMATOLOGY") - └─ Drug (e.g., "ADALIMUMAB") - └─ Pathway (e.g., "ADALIMUMAB → INFLIXIMAB") -``` - -**Indication view:** -``` -Root (Regional Total) - └─ Trust - └─ GP Diagnosis (e.g., "rheumatoid arthritis") - └─ Drug - └─ Pathway -``` - -### Reading the Chart - -- **Width** of each section indicates relative patient count -- **Color intensity** (NHS blue gradient) indicates proportion of parent group -- **Labels** show the name and patient count - -### Interacting with the Chart - -| Action | Effect | -|--------|--------| -| **Click** a section | Zoom in to show details for that branch | -| **Click** the parent/root | Zoom back out | -| **Hover** over a section | See tooltip with patient count, cost, dosing frequency, dates | - -### Hover Tooltip Information - -When hovering over a chart section, you'll see: -- Patient count and percentage of parent -- Total cost and cost per patient -- First and last seen dates -- Treatment dosing frequency (for drug nodes) -- Cost per patient per annum - ---- - -## GP Indication Matching - -When viewing "By Indication" charts, the application uses pre-computed GP diagnosis matches: - -### How It Works - -1. During data refresh, each patient's NHS pseudonym is queried against GP primary care records -2. SNOMED cluster codes map clinical conditions to drug indications -3. The most recent GP diagnosis match is used for each patient -4. ~93% of patients are matched to a GP diagnosis - -### Unmatched Patients - -Patients without a GP diagnosis match appear under their directorate with a "(no GP dx)" suffix (e.g., "RHEUMATOLOGY (no GP dx)"). - -Reasons for unmatched patients: -- GP is outside the data coverage area -- Diagnosis not yet recorded in GP system -- Condition managed only in secondary care -- Off-label prescribing - ---- - -## Troubleshooting - -### No data showing - -1. Check the filter bar — are filters too restrictive? -2. Try clearing all drug/trust selections in the drawer -3. Widen the date range (e.g., "All years / Last 12 months") - -### Chart shows "No matching pathways found" - -The current filter combination matches zero patients. Adjust filters or click "Clear All Filters" in the drawer. - -### App won't start - -```bash -# Ensure dependencies are installed -uv sync - -# Ensure src/ is on Python path -uv run python setup_dev.py - -# Run with uv -uv run python run_dash.py -``` - -### Stale data - -Data is as fresh as the last CLI refresh. Check the header's "Last updated" indicator. To refresh: - -```bash -python -m cli.refresh_pathways --chart-type all -``` - ---- - -## Getting Help - -If you encounter issues not covered in this guide: - -1. Check the [README](../README.md) for installation and setup -2. Review [DEPLOYMENT.md](./DEPLOYMENT.md) for server configuration -3. Consult [CLAUDE.md](../CLAUDE.md) for technical architecture details -4. Contact the Medicines Intelligence team for NHS-specific questions