Updated readme
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -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`
|
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user