Updated readme

This commit is contained in:
Andrew Charlwood
2026-02-10 16:59:47 +00:00
parent 393ce11994
commit 2f75efa964
6 changed files with 0 additions and 1685 deletions
-6
View File
@@ -229,12 +229,6 @@ python -m data_processing.migrate
2. A browser window will open for SSO authentication 2. A browser window will open for SSO authentication
3. Verify your network allows Snowflake connections 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 ## License
Internal NHS use only. Not for distribution. Internal NHS use only. Not for distribution.
-296
View File
@@ -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)
-194
View File
@@ -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`
-740
View File
@@ -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": '<rect x="3" y="3" width="7" height="7"/>...', # existing
"compare": '<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>', # 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; }
}
```
-192
View File
@@ -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
-257
View File
@@ -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