Compare commits

..

11 Commits

Author SHA1 Message Date
admin 09be4c2472 demo docker file 2026-02-17 11:32:25 +00:00
Andrew Charlwood 2f75efa964 Updated readme 2026-02-10 16:59:47 +00:00
Andrew Charlwood 393ce11994 Updated gitignore for sharing 2026-02-10 16:57:29 +00:00
Andrew Charlwood b9fe89a4ee Cleaned up old reflex files/legacy code etc 2026-02-10 16:27:36 +00:00
Andrew Charlwood a496c01eb4 Remove claude working folders 2026-02-10 15:30:27 +00:00
Andrew Charlwood f96950111b Merge branch 'master' of http://192.168.8.143:3000/admin/HighCostDrugs 2026-02-10 15:26:46 +00:00
Andrew Charlwood c18027ffa2 Remove legacy/ralph/temp files from tracking 2026-02-10 15:25:53 +00:00
admin fcbde7c689 Restructured src to more logical heirachy 2026-02-09 16:22:05 +00:00
Andrew Charlwood 7e63e6ea45 feat: add desktop packaging (pywebview + PyInstaller)
- resource_path.py: frozen/dev path resolution for bundled data files
- app_desktop.py: pywebview entry point (Dash in daemon thread)
- app.spec: PyInstaller onedir config with data files and hidden imports
- Updated queries.py, card_browser.py, app.py to use get_resource_path()
- Added pywebview + pyinstaller to project dependencies
- Fixed unresolved merge conflict in .gitignore
- Removed stale 01_nhs_classic.html and AdditionalAnalytics.md
2026-02-09 14:53:22 +00:00
admin ee56595292 Update gitignore 2026-02-08 22:45:16 +00:00
Andrew Charlwood b98ab1a5c6 test 2026-02-08 22:41:46 +00:00
111 changed files with 1225 additions and 126614 deletions
-26
View File
@@ -1,26 +0,0 @@
{
"permissions": {
"allow": [
"Bash(python*)",
"Bash(git*)",
"Bash(cd*)",
"Bash(ls*)",
"Bash(cat*)",
"Bash(head*)",
"Bash(tail*)",
"Bash(mkdir*)",
"Bash(touch*)",
"Bash(rm*)",
"Bash(mv*)",
"Bash(cp*)",
"Bash(timeout*)",
"Bash(reflex*)",
"Read",
"Write",
"Edit",
"Glob",
"Grep"
],
"deny": []
}
}
-22
View File
@@ -1,22 +0,0 @@
{
"permissions": {
"allow": [
"WebSearch",
"Bash(wc:*)",
"WebFetch(domain:flet.dev)",
"WebFetch(domain:github.com)",
"WebFetch(domain:docs.flet.dev)",
"Bash(python -c:*)",
"Bash(Remove-Item -Path \"C:\\\\Users\\\\charlwoodand\\\\Ralph local\\\\Tasks\\\\UI Redesign\\\\logs\\\\*\" -Force)",
"Bash(git commit -m \"$\\(cat <<''EOF''\ndocs: update CLAUDE.md to reflect slimmed database architecture\n\nRemove references to deleted tables \\(fact_interventions,\nmv_patient_treatment_summary, ref_drug_snomed_mapping, processed_files\\),\ndeleted files \\(patient_data.py, load_snomed_mapping.py\\), and removed\nclasses \\(SQLiteDataLoader\\). Update package structure, data loaders,\ndatabase schema, fallback chain, and AppState descriptions.\nEOF\n\\)\")",
"Bash(Get-ChildItem -Recurse -Force)",
"Bash(Select-Object FullName)",
"Bash(uv run python:*)",
"Bash(uv run:*)",
"Bash(PYTHONPATH=src uv run python:*)",
"Bash(uv pip install:*)",
"Bash(uv lock:*)",
"Bash(git commit:*)"
]
}
}
+16
View File
@@ -0,0 +1,16 @@
.git
__pycache__
*.pyc
*.pyo
*.db
.pytest_cache
.coverage
htmlcov
tests
.venv
*.egg-info
dist
build
.mypy_cache
.ruff_cache
*.spec
+20 -20
View File
@@ -9,6 +9,7 @@ build/
dist/
wheels/
*.egg-info
*claude*
# Virtual environments
.venv
@@ -18,10 +19,6 @@ wheels/
.mypy_cache/
.pytest_cache/
# Data files (large)
hcd_20250411.csv
hcd_20250411.parquet
# IDE
.idea
@@ -32,30 +29,33 @@ hcd_20250411.parquet
logs/*.log
logs/*.jsonl
# Reflex build artifacts (future)
# Reflex build artifacts (legacy)
.web/
.states/
# SQLite database (will contain local data)
# Legacy files (old Reflex/CustomTkinter app)
archive/
images/
run.bat
# Ralph loop files
ralph.ps1
RALPH_PROMPT.md
progress.txt
# Planning/process docs (not used by application)
guardrails.md
IMPLEMENTATION_PLAN.md
# Windows artifacts
NUL
# SQLite databases (except pathways.db which contains pre-computed data)
*.db
*.sqlite
# Snowflake result cache
data/cache/
# Uploaded data files
data/uploads/
# Exported analysis results
data/exports/
# Analysis output files
output/*.html
output/*.csv
*.html
# VS Code workspace settings
.vscode/
# User uploaded files
uploaded_files/
-154
View File
@@ -1,154 +0,0 @@
# Additional Analytics Charts — Implementation Plan
## UI Approach: Tabbed Chart Area
Extend existing `chart_card.py` tab bar. Currently has Icicle (active), Sankey (disabled), Timeline (disabled). Replace/extend with new tabs.
## Charts to Build (Priority Order)
### Tab 1: Icicle (existing — no change)
### Tab 2: First-Line Market Share — Horizontal Bar Chart
**What**: % of patients starting on each first-line drug, grouped by directorate or indication
**Data source**: `pathway_nodes WHERE level = 3` (drug level). The `colour` column already holds proportion of parent. `value` = patient count.
**Query**: Filter by `chart_type`, `date_filter_id`, optionally `directory` or `trust_name`. Group by `directory`, then show drugs as bars.
**Viz**: Horizontal grouped bar chart. One cluster per directorate/indication (top N), bars within = drugs, length = % of patients. Sorted by total patients desc. NHS blue palette.
**Interaction**: Responds to all existing filters (date, chart type, trust, drug, directorate). Clicking a directorate cluster could filter the icicle.
### Tab 3: Pathway Cost Effectiveness — Lollipop/Dot Plot
**What**: Compare annualized cost per patient across complete treatment pathways within a directorate/indication. Highlights most vs least cost-effective pathways.
**Data source**: `pathway_nodes WHERE level >= 4` (pathway nodes). Fields: `cost_pp_pa` (annualized), `value` (patient count), `ids` (parse to get pathway sequence), `directory`.
**Calculation**: `cost_pp_pa` is already computed as `(total_cost / patients) * (365 / avg_days)` — this IS the "total cost over N years / N years" the user described.
**Query**: Filter to a specific directorate/indication, then show all pathway variants ranked by `cost_pp_pa`.
**Viz**: Horizontal lollipop chart (dot on stick). Y-axis = pathway label (e.g., "Adalimumab → Secukinumab → Rituximab"), X-axis = £ per patient per annum. Dot size = patient count. Colour gradient: green (cheap) → amber → red (expensive).
**Interaction**: Directorate/indication selector drives which pathways are shown. Could also compare across directorates at the drug level (level 3).
**Bonus metric — "Pathway Retention" (fewest switches)**:
- For each 2nd-line pathway (e.g., "Drug A → Drug B"), calculate what % of patients escalate to a 3rd line
- Derivation: `value("Drug A → Drug B") - SUM(value("Drug A → Drug B → *"))` = patients who stayed on 2nd line
- Show as a secondary annotation or companion chart: "Drug B retains 72% of patients (no 3rd-line switch needed)"
- This identifies the most effective 2nd-line choices
### Tab 4: Cost Waterfall — Waterfall Chart
**What**: Break down £ per patient per annum by directorate, showing relative cost contribution
**Data source**: `pathway_nodes WHERE level = 2` (directorate/indication level). Field: `cost_pp_pa`, `value`.
**Viz**: Plotly waterfall chart. Each bar = one directorate's average cost_pp_pa. Sorted highest to lowest. Running reference line optional. Use NHS colours.
**Note**: User specifically wants cost_pp_pa (annualized), not total cost.
**Interaction**: Responds to chart_type toggle, date filter, trust filter.
### Tab 5: Drug Switching Sankey — Sankey Diagram
**What**: Flow of patients from 1st-line → 2nd-line → 3rd-line drugs
**Data source**: `pathway_nodes WHERE level >= 3`. Parse `ids` to extract drug transition sequences.
**Parsing**: `ids` format at level 4+: `"TRUST - DIRECTORY - DRUG_A - DRUG_A|DRUG_B"`. Split by " - ", take segments from level 3 onwards, split by "|" to get ordered drug list.
**Viz**: Plotly Sankey. Left nodes = 1st-line drugs, middle = 2nd-line, right = 3rd-line. Link width = patient count. Colour by drug or by directorate.
**Interaction**: Filter by directorate/indication to see switching within a specialty. Filter by trust to compare switching patterns.
### Tab 6: Dosing Interval Comparison — Grouped Bar Chart
**What**: Compare average dosing frequency/weekly interval for a drug across trusts or directorates
**Data source**: Level 3+ nodes, `average_spacing` (HTML string), `average_administered` (JSON array)
**Parsing needed**:
- `average_spacing`: regex to extract weekly interval number from `"given X times with Y weekly interval"`
- `average_administered`: `json.loads()` to get dose counts
**Viz**: Horizontal grouped bars. Y-axis = trust or directorate, X-axis = weekly interval (or total administrations). One colour per drug if comparing multiple.
**Interaction**: Drug selector to pick which drug to compare. Group-by selector (trust vs directorate).
### Tab 7: Directorate × Drug Heatmap
**What**: Matrix showing which drugs are used in which directorates, cells coloured by patient count or cost_pp_pa
**Data source**: Level 3 nodes, pivot `directory` × drug (parsed from `labels` or `ids`)
**Viz**: Plotly heatmap. Rows = directorates (sorted by total patients), columns = drugs (sorted by frequency). Cell colour = patient count or cost. Hover shows details.
**Interaction**: Toggle between patient count / cost / cost_pp_pa colouring.
### Tab 8: Treatment Duration Bars
**What**: Compare average treatment durations across drugs within a directorate
**Data source**: Level 3 nodes, `avg_days` field
**Viz**: Horizontal bar chart. Y-axis = drug, X-axis = average days. Colour intensity by patient count.
**Interaction**: Directorate filter drives which drugs are shown.
---
## Data Layer Changes
### New query functions needed (in `src/data_processing/pathway_queries.py`):
```python
def get_drug_market_share(db_path, date_filter_id, chart_type, directory=None, trust=None):
"""Level 3 nodes grouped by directory, returning drug, value, colour."""
def get_pathway_costs(db_path, date_filter_id, chart_type, directory=None):
"""Level 4+ nodes with cost_pp_pa, parsed pathway labels, patient counts."""
def get_cost_waterfall(db_path, date_filter_id, chart_type, trust=None):
"""Level 2 nodes with cost_pp_pa per directorate/indication."""
def get_drug_transitions(db_path, date_filter_id, chart_type, directory=None):
"""Level 3+ nodes parsed into source→target drug transitions with patient counts."""
def get_dosing_intervals(db_path, date_filter_id, chart_type, drug=None):
"""Level 3 nodes for a specific drug, parsed average_spacing by trust/directory."""
def get_drug_directory_matrix(db_path, date_filter_id, chart_type):
"""Level 3 nodes pivoted as directory × drug with value/cost metrics."""
def get_treatment_durations(db_path, date_filter_id, chart_type, directory=None):
"""Level 3 nodes with avg_days by drug within a directorate."""
```
### Parsing utilities needed:
```python
def parse_average_spacing(spacing_html: str) -> dict:
"""Extract drug_name, dose_count, weekly_interval, total_weeks from HTML string."""
def parse_pathway_drugs(ids: str, level: int) -> list[str]:
"""Extract ordered drug list from ids column at level 4+."""
def calculate_retention_rate(nodes: list[dict]) -> dict:
"""For each N-drug pathway, calculate % not escalating to N+1 drugs."""
```
---
## Callback Architecture
Each tab gets its own callback triggered by `chart-data` store + `active-tab` state:
```
active-tab change → render selected chart
chart-data change → re-render active chart
```
Only the active tab's chart is computed (lazy rendering). Store the active tab in `app-state`.
New callback per chart type in `dash_app/callbacks/`:
- `market_share.py` — builds bar chart from level 3 data
- `pathway_costs.py` — builds lollipop + retention annotations
- `cost_waterfall.py` — builds waterfall from level 2 data
- `sankey.py` — builds Sankey from parsed transitions
- `dosing.py` — builds grouped bars from parsed spacing
- `heatmap.py` — builds heatmap from pivoted matrix
- `duration.py` — builds bar chart from avg_days
---
## Implementation Order
1. **Data parsing utilities** — shared parsing for spacing, pathway drugs, retention
2. **Query functions** — one per chart type in pathway_queries.py
3. **Tab infrastructure** — extend chart_card.py with all tab labels, lazy rendering
4. **Charts one at a time** (in priority order):
- First-Line Market Share (simplest, validates the tab pattern)
- Pathway Cost Effectiveness + Retention (user's key insight)
- Cost Waterfall
- Drug Switching Sankey
- Dosing Interval
- Heatmap
- Treatment Duration
---
## Verification
- Run `python run_dash.py` after each chart addition
- Verify each chart responds to filter changes (date, chart type, trust, directorate, drug)
- Test with both "directory" and "indication" chart types
- Verify icicle chart still works correctly (no regressions)
- Check tab switching is smooth with no unnecessary recomputation
-655
View File
@@ -1,655 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
NHS High-Cost Drug Patient Pathway Analysis Tool - a web-based application that analyzes secondary care patient treatment pathways. It processes clinical activity data to visualize hierarchical treatment patterns as interactive Plotly icicle charts.
**Key Features:**
- **Dual chart types**: Directory-based (Trust → Directory → Drug → Pathway) and Indication-based (Trust → GP Diagnosis → Drug → Pathway) views with toggle
- **Pre-computed pathway architecture**: Treatment pathways pre-processed and stored in SQLite for instant filtering
- **GP diagnosis matching**: Patient indications matched from GP records using SNOMED cluster codes queried directly from Snowflake (~93% match rate)
- Data pipeline: Snowflake → pre-computed SQLite pathway nodes (CSV/Parquet file loading retained for legacy compatibility)
- Interactive browser-based UI using Dash (Plotly) + Dash Mantine Components
- 6 pre-defined date filter combinations × 2 chart types = 12 pre-computed datasets with sub-50ms response times
## Running the Application
```bash
# Install dependencies
uv sync
# One-time dev setup: adds src/ to Python path via .pth file
uv run python setup_dev.py
# Initialize/migrate the database (creates pathway tables)
python -m data_processing.migrate
# Refresh pathway data from Snowflake (requires SSO auth)
python -m cli.refresh_pathways
# Run the Dash web application
python run_dash.py
```
The application requires Python 3.10+ and runs on http://localhost:8050 by default.
### CLI Commands
**Refresh Pathway Data:**
```bash
# Full refresh — both chart types (directory + indication), all date filters
python -m cli.refresh_pathways --chart-type all
# Directory charts only (faster, skips GP diagnosis lookup)
python -m cli.refresh_pathways --chart-type directory
# Indication charts only
python -m cli.refresh_pathways --chart-type indication
# Dry run (test without database changes)
python -m cli.refresh_pathways --chart-type all --dry-run -v
# Custom minimum patient threshold
python -m cli.refresh_pathways --minimum-patients 10
# Help
python -m cli.refresh_pathways --help
```
The `--chart-type` argument controls which pathway types are processed:
- `all` (default) — generates both directory and indication charts (~15 minutes)
- `directory` — directory-based charts only (~5 minutes)
- `indication` — indication-based charts only (~12 minutes, includes GP lookup)
The refresh command:
1. Fetches activity data from Snowflake (656K+ records, ~7 seconds)
2. Applies UPID, drug name, and directory transformations (~6 minutes)
3. For indication charts: queries GP records via SNOMED clusters (~9 minutes for 37K patients)
4. Processes 6 date filter combinations × selected chart types
5. Inserts pathway nodes to SQLite for fast Dash filtering
## Architecture
### Package Structure
```
.
├── src/ # All application library code
│ ├── core/ # Foundation: paths, models, logging
│ │ ├── config.py # PathConfig dataclass for file paths
│ │ ├── models.py # AnalysisFilters dataclass
│ │ └── logging_config.py # Structured logging setup
│ │
│ ├── config/ # Service configuration
│ │ ├── __init__.py # SnowflakeConfig + loader
│ │ └── snowflake.toml # Connection settings (co-located with loader)
│ │
│ ├── data_processing/ # Data layer
│ │ ├── database.py # SQLite connection management
│ │ ├── schema.py # Database schema (reference + pathway tables)
│ │ ├── pathway_pipeline.py # Pipeline: Snowflake → SQLite
│ │ ├── transforms.py # Data transformations (UPID, drug names, directory)
│ │ ├── loader.py # FileDataLoader for CSV/Parquet files
│ │ ├── reference_data.py # Reference data migration
│ │ ├── snowflake_connector.py # Snowflake integration
│ │ ├── cache.py # Query result caching
│ │ ├── data_source.py # Data source fallback chain
│ │ ├── diagnosis_lookup.py # GP diagnosis lookup (SNOMED clusters)
│ │ └── parsing.py # Parse average_spacing HTML, pathway drugs, retention rates
│ │
│ ├── analysis/ # Analysis pipeline
│ │ ├── pathway_analyzer.py # prepare_data, calculate_statistics, build_hierarchy
│ │ └── statistics.py # Statistical calculation functions
│ │
│ ├── visualization/ # Chart generation
│ │ └── plotly_generator.py # Icicle, market share, cost effectiveness, waterfall, Sankey, dosing, heatmap, duration figures
│ │
│ └── cli/ # CLI tools
│ └── refresh_pathways.py # Data refresh command
├── dash_app/ # Dash web application
│ ├── app.py # Dash app, layout root, dcc.Store, register_callbacks
│ ├── assets/
│ │ └── nhs.css # NHS design system CSS
│ ├── data/
│ │ ├── queries.py # Thin wrapper calling src/data_processing/pathway_queries.py
│ │ └── card_browser.py # DimSearchTerm.csv → directorate tree for modals
│ ├── components/
│ │ ├── header.py # Top header bar with fraction KPIs + data freshness
│ │ ├── sidebar.py # Left nav: Patient Pathways + Trust Comparison
│ │ ├── sub_header.py # Global filter bar (date dropdowns + chart type toggle)
│ │ ├── filter_bar.py # Patient Pathways filter buttons (drugs, trusts, directorates)
│ │ ├── chart_card.py # Chart area with Icicle/Sankey tabs + dcc.Graph
│ │ ├── modals.py # dmc.Modal dialogs for drug/trust/directorate selection
│ │ ├── trust_comparison.py # Trust Comparison landing page + 6-chart dashboard
│ │ └── footer.py # Page footer
│ ├── callbacks/
│ │ ├── __init__.py # register_callbacks(app)
│ │ ├── filters.py # Reference data loading + filter state management
│ │ ├── chart.py # Tab switching, pathway data loading, chart dispatch
│ │ ├── modals.py # Modal open/close + drug/trust/directorate selection
│ │ ├── navigation.py # Sidebar view switching + Trust Comparison navigation
│ │ ├── trust_comparison.py # 6 Trust Comparison chart callbacks
│ │ └── kpi.py # Header fraction KPI updates
│ └── utils/
│ └── __init__.py
├── run_dash.py # Entry point: python run_dash.py
├── tests/ # Test suite (113 tests)
├── data/ # Reference data + SQLite DB
├── docs/ # Documentation
├── assets/ # Static assets (logo, favicon)
├── archive/ # Historical/deprecated (includes old Reflex app)
└── logs/ # Runtime logs
```
**Path resolution**: `src/` is added to `sys.path` via a `.pth` file (created by `setup_dev.py`).
All imports use package names directly: `from core import ...`, `from data_processing import ...`, etc.
### Pathway Data Architecture
The application uses a pre-computed pathway architecture for performance:
**Architecture:** `Snowflake → Pathway Processing → SQLite (pre-computed) → Dash (filter & view)`
**Key Benefits:**
- **Performance**: Pathway calculation done once during data refresh, not on every filter change
- **Simplicity**: Dash callbacks filter pre-computed data with simple SQL WHERE clauses
- **Full Pathways**: Sequential treatment pathways (drug_0 → drug_1 → drug_2...) with statistics
**Chart Types:**
| Type | Hierarchy | Level 2 Source |
|------|-----------|----------------|
| `directory` | Trust → Directory → Drug → Pathway | Assigned directorate (5-level fallback) |
| `indication` | Trust → GP Diagnosis → Drug → Pathway | SNOMED cluster Search_Term from GP records |
For indication charts, ~93% of patients are matched to a GP diagnosis (Search_Term). Unmatched patients use their directorate as a fallback label (e.g., "RHEUMATOLOGY (no GP dx)").
**Date Filter Combinations:**
| ID | Initiated | Last Seen | Default |
|----|-----------|-----------|---------|
| `all_6mo` | All years | Last 6 months | Yes |
| `all_12mo` | All years | Last 12 months | No |
| `1yr_6mo` | Last 1 year | Last 6 months | No |
| `1yr_12mo` | Last 1 year | Last 12 months | No |
| `2yr_6mo` | Last 2 years | Last 6 months | No |
| `2yr_12mo` | Last 2 years | Last 12 months | No |
Total pre-computed datasets: 6 date filters × 2 chart types = 12 datasets (~3,600 pathway nodes).
**Pathway Node Structure:**
Each node in `pathway_nodes` contains:
- Routing: `chart_type` ("directory" or "indication"), `date_filter_id`
- Hierarchy: `parents`, `ids`, `labels`, `level` (0=Root, 1=Trust, 2=Directory/Indication, 3=Drug, 4+=Pathway)
- Counts: `value` (patient count)
- Costs: `cost`, `costpp`, `cost_pp_pa` (per patient per annum)
- Dates: `first_seen`, `last_seen`, `first_seen_parent`, `last_seen_parent`
- Statistics: `average_spacing`, `average_administered`, `avg_days`
- Denormalized: `trust_name`, `directory`, `drug_sequence` (for efficient filtering)
- Unique constraint: `UNIQUE(date_filter_id, chart_type, ids)`
### Core Module (`core/`)
- **PathConfig** - Dataclass encapsulating all file paths, with `validate()` method
- **AnalysisFilters** - Dataclass for filter state (dates, drugs, trusts, directories)
- **logging_config** - Structured logging with file and console output
### CLI Module (`cli/`)
- **refresh_pathways.py** - Command-line tool to refresh pre-computed pathway data:
- `refresh_pathways()` - Main function orchestrating the full pipeline
- `insert_pathway_records()` - SQLite insertion with parameterized queries
- `log_refresh_start/complete/failed()` - Refresh tracking in `pathway_refresh_log`
- `get_default_filters()` - Load trusts/drugs/directories from CSV files
### Data Processing Module (`data_processing/`)
**Database Management:**
- `DatabaseManager` - SQLite connection pooling and transaction management
- **Reference Tables**: `ref_drug_names`, `ref_organizations`, `ref_directories`, `ref_drug_directory_map`, `ref_drug_indication_clusters`
- **Pathway Tables**: `pathway_date_filters`, `pathway_nodes`, `pathway_refresh_log`
**Pathway Pipeline (`pathway_pipeline.py`):**
- `DateFilterConfig` - Dataclass for date filter configuration
- `DATE_FILTER_CONFIGS` - All 6 pre-defined date combinations
- `compute_date_ranges(config, max_date)` - Computes actual ISO dates from config
- `fetch_and_transform_data()` - Snowflake fetch + UPID/drug/directory transformations
- Directory chart functions:
- `process_pathway_for_date_filter()` - Processes single date filter using `generate_icicle_chart()`
- `extract_denormalized_fields()` - Parses `ids` column to extract trust, directory, drug_sequence
- Indication chart functions:
- `process_indication_pathway_for_date_filter()` - Processes single date filter using `generate_icicle_chart_indication()`
- `extract_indication_fields()` - Parses `ids` for indication charts (trust, search_term, drug_sequence)
- Shared functions:
- `convert_to_records(ice_df, chart_type)` - Converts ice_df to list of dicts with `chart_type` column
- `process_all_date_filters()` - Convenience function to process all 6 filters
**Data Loaders:**
- `FileDataLoader` - Loads from CSV/Parquet files (used by legacy pipeline, not by Dash app)
- Factory function `get_loader()` creates a `FileDataLoader`
**Snowflake Integration:**
- SSO authentication via `externalbrowser` authenticator
- `fetch_activity_data(start_date, end_date, provider_codes)` method
- Query caching with TTL-based invalidation
**GP Diagnosis Lookup (`diagnosis_lookup.py`):**
- `CLUSTER_MAPPING_SQL` - Embedded SQL constant with ~148 Search_Term → Cluster_ID mappings plus explicit SNOMED codes
- `get_patient_indication_groups(patient_pseudonyms)` - Batch queries Snowflake to match patients to GP diagnoses:
- Embeds cluster mapping as CTE, joins with `PrimaryCareClinicalCoding`
- Uses `PseudoNHSNoLinked` (not PersonKey) to match `PatientPseudonym` in GP records
- Returns most recent match per patient via `QUALIFY ROW_NUMBER()`
- Batches 500 patients per query, returns DataFrame with PatientPseudonym, Search_Term, EventDateTime
- `patient_has_indication(patient_pseudonym, cluster_ids)` - Single-patient GP record check (legacy)
- `validate_indication(patient_pseudonym, drug_name)` - Full validation result with source tracking (legacy)
### Analysis Module (`analysis/`)
Refactored from the original 267-line `generate_graph()` function:
- **prepare_data()** - Filter DataFrame by date range, trusts, drugs, directories (copies df to prevent mutation)
- **calculate_statistics()** - Compute frequency, cost, duration statistics
- **build_hierarchy()** - Create Trust → Directory → Drug → Pathway structure
- **prepare_chart_data()** - Format data for Plotly icicle chart
- **generate_icicle_chart_indication(df, indication_df, ...)** - Build indication-based hierarchy using Search_Term instead of Directory. Takes an `indication_df` (UPID → Search_Term mapping) alongside the main activity DataFrame.
### Visualization Module (`visualization/`)
- **create_icicle_figure(ice_df)** - Generate Plotly icicle chart from DataFrame (legacy/pipeline use)
- **create_icicle_from_nodes(nodes, title)** - Generate icicle chart from list-of-dicts (Dash use). Accepts JSON-serializable node dicts from `dcc.Store`. Uses NHS blue gradient colorscale, 10-field customdata, Source Sans 3 font.
- **create_market_share_figure(data, title)** - Horizontal stacked bar chart: drugs grouped by directorate/indication, bar length = % patients
- **create_cost_effectiveness_figure(data, retention, title)** - Lollipop chart: pathway cost_pp_pa with dot size = patient count, retention annotations
- **create_cost_waterfall_figure(data, title)** - Waterfall chart: directorate-level cost_pp_pa sorted highest to lowest
- **create_sankey_figure(data, title)** - Sankey diagram: drug switching flows across treatment lines (1st → 2nd → 3rd)
- **create_dosing_figure(data, title, group_by)** - Grouped horizontal bar chart: dosing intervals by drug or trust
- **create_heatmap_figure(data, title, metric)** - Matrix heatmap: directorate × drug with patient/cost/cost_pp_pa colouring
- **create_duration_figure(data, title, show_directory)** - Horizontal bar chart: average treatment duration in days per drug
- **create_trust_market_share_figure(data, title)** - Trust Comparison: horizontal stacked bars grouped by trust, drugs as segments
- **create_trust_heatmap_figure(data, title, metric)** - Trust Comparison: trust × drug matrix with NHS blue colorscale
- **create_trust_duration_figure(data, title)** - Trust Comparison: grouped horizontal bars with one trace per trust
- **save_figure_html()** - Save interactive HTML file
- **open_figure_in_browser()** - Open chart in default browser
### Parsing Utilities (`data_processing/parsing.py`)
- **parse_average_spacing(spacing_html)** - Extract drug_name, dose_count, weekly_interval, total_weeks from HTML string
- **parse_pathway_drugs(ids, level)** - Extract ordered drug list from ids column at level 4+
- **calculate_retention_rate(nodes)** - For each N-drug pathway, calculate % not escalating to N+1 drugs
### Shared Data Queries (`data_processing/pathway_queries.py`)
Shared query functions used by the Dash app (via thin wrappers in `dash_app/data/queries.py`):
- **load_initial_data(db_path)** - Returns available drugs (42), directorates (14), indications (32), trusts (7), total_patients, last_updated
- **load_pathway_nodes(db_path, filter_id, chart_type, selected_drugs, selected_directorates, selected_trusts)** - Returns pathway nodes, unique_patients, total_drugs, total_cost, last_updated. Parameterized SQL with optional drug/directorate/trust filters.
- **get_drug_market_share(db_path, filter_id, chart_type, directory, trust)** - Level 3 nodes grouped by directory, returns drug, value, colour
- **get_pathway_costs(db_path, filter_id, chart_type, directory, trust)** - Level 4+ nodes with cost_pp_pa, pathway labels, patient counts
- **get_cost_waterfall(db_path, filter_id, chart_type, trust)** - Level 2 nodes with cost_pp_pa per directorate/indication
- **get_drug_transitions(db_path, filter_id, chart_type, directory, trust)** - Level 3+ nodes parsed into source→target drug transitions
- **get_dosing_intervals(db_path, filter_id, chart_type, drug, trust)** - Level 3 nodes with parsed average_spacing intervals
- **get_drug_directory_matrix(db_path, filter_id, chart_type, trust)** - Level 3 nodes pivoted as directory × drug matrix
- **get_treatment_durations(db_path, filter_id, chart_type, directory, trust)** - Level 3 nodes with avg_days by drug
- **get_trust_market_share(db_path, filter_id, chart_type, directory)** - Trust Comparison: drugs by trust within a single directorate
- **get_trust_cost_waterfall(db_path, filter_id, chart_type, directory)** - Trust Comparison: one bar per trust showing cost_pp within directorate
- **get_trust_dosing(db_path, filter_id, chart_type, directory)** - Trust Comparison: drug dosing intervals broken down by trust
- **get_trust_heatmap(db_path, filter_id, chart_type, directory)** - Trust Comparison: trust × drug matrix for one directorate
- **get_trust_durations(db_path, filter_id, chart_type, directory)** - Trust Comparison: drug durations by trust within directorate
- **get_directorate_summary(db_path, filter_id, chart_type, directory)** - Summary stats for a directorate (total patients, drugs, cost)
### Dash Application (`dash_app/`)
**Two-View Architecture:**
The application is split into two analytical perspectives, selectable via the sidebar:
- **Patient Pathways**: Pathway-focused analysis (Icicle + Sankey charts) with drug/trust/directorate filters
- **Trust Comparison**: Per-directorate analysis comparing drugs across trusts (6 charts for a selected directorate)
**State Management** via 4 `dcc.Store` components:
- **app-state** (session): `chart_type`, `initiated`, `last_seen`, `date_filter_id`, `selected_drugs`, `selected_directorates`, `selected_trusts`, `active_view` ("patient-pathways" | "trust-comparison"), `selected_comparison_directorate` (null | directorate name)
- **chart-data** (memory): `nodes[]`, `unique_patients`, `total_drugs`, `total_cost`, `last_updated`
- **reference-data** (session): `available_drugs`, `available_directorates`, `available_indications`, `available_trusts`, `total_patients`, `last_updated`
- **active-tab** (memory): Currently selected chart tab within Patient Pathways ("icicle" | "sankey")
**Callback Chain** (unidirectional):
```
Page Load → load_reference_data → reference-data store + header indicators
→ update_app_state → app-state store (default filters)
→ load_pathway_data → chart-data store
├→ update_kpis → header fraction KPIs
└→ update_chart → dcc.Graph (Icicle or Sankey)
Filter change → update_app_state → app-state → load_pathway_data → (chain above)
Modal selection → drug/trust chips → update_app_state → (chain above)
Tab click → switch_tab → active-tab store → update_chart → dcc.Graph (lazy rendering)
Sidebar click → switch_view → active_view in app-state → show/hide views
Trust Comparison:
Landing page → directorate button click → selected_comparison_directorate → 6 chart callbacks
Back button → clear selected_comparison_directorate → return to landing
```
**Key Components:**
- **Header** (`header.py`): NHS branding, fraction KPIs (X/X patients, X/X drugs, £X/£X cost), data freshness indicator
- **Sidebar** (`sidebar.py`): 2 navigation items — "Patient Pathways" (default), "Trust Comparison"
- **Sub-Header** (`sub_header.py`): Global filter bar — date dropdowns (Initiated, Last Seen) + chart type toggle pills (By Directory / By Indication). Constant across both views.
- **Filter Bar** (`filter_bar.py`): Patient Pathways-only filter buttons — Drugs (with count badge), Trusts (with count badge), Directorates (with count badge), Clear All. Only visible on Patient Pathways view.
- **Chart Card** (`chart_card.py`): 2-tab chart area (Icicle, Sankey) with `dcc.Loading` spinner, dynamic subtitle, and `dcc.Store(id="active-tab")`
- **Modals** (`modals.py`): 3 `dmc.Modal` dialogs for drug selection (ChipGroup), trust selection (ChipGroup), directorate browser (Accordion with indication sub-items and drug fragment badges)
- **Trust Comparison** (`trust_comparison.py`): Landing page (directorate/indication button grid) + 6-chart dashboard (Market Share, Cost Waterfall, Dosing, Heatmap, Duration, Cost Effectiveness)
- **Footer** (`footer.py`): NHS Norfolk and Waveney ICB branding
**Filter Modals:**
- Drug Modal: flat `dmc.ChipGroup` with 42 drugs from pathway_nodes level 3
- Trust Modal: `dmc.ChipGroup` with 7 trusts
- Directorate Modal: nested `dmc.Accordion` — 19 directorates → indications → drug fragment `dmc.Badge` items
- Clicking a drug fragment badge selects all full drug names containing that fragment (substring match)
- "Clear All Filters" button resets drug and trust selections
**Trust Comparison Dashboard (6 Charts):**
All scoped to a single selected directorate, comparing drugs across trusts:
1. **Market Share**: Drug breakdown per trust (stacked bars per trust)
2. **Cost Waterfall**: Per-trust cost within directorate
3. **Dosing**: Drug dosing intervals by trust
4. **Heatmap**: Trust × drug matrix
5. **Duration**: Drug durations by trust
6. **Cost Effectiveness**: Pathway costs within directorate (NOT split by trust)
### Data Transformations (`data_processing/transforms.py`)
Core data transformation functions used by the pipeline:
- `patient_id()` - Creates UPID = Provider Code (first 3 chars) + PersonKey
- `drug_names()` - Standardizes via drugnames.csv lookup
- `department_identification()` - 5-level fallback chain for directory assignment
### Data Flow
**Pre-Computed Pathway Architecture (Current):**
```
[CLI: python -m cli.refresh_pathways --chart-type all]
Snowflake Data Warehouse
▼ (fetch_and_transform_data)
┌──────────────────────────────────────────┐
│ Data Transformations (data_processing/transforms.py) │
│ → patient_id() creates UPID │
│ → drug_names() standardizes names │
│ → department_identification() → Dir │
└──────────────────────────────────────────┘
├─── Directory Charts ──────────────────────────────────────┐
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ For each of 6 date filter combos: │ │
│ │ → generate_icicle_chart() │ │
│ │ → extract_denormalized_fields() │ │
│ │ → convert_to_records("directory") │ │
│ └──────────────────────────────────────────┘ │
│ │
├─── Indication Charts ─────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ GP Diagnosis Lookup (diagnosis_lookup.py)│ │
│ │ → Extract PseudoNHSNoLinked from HCD │ │
│ │ → get_patient_indication_groups() │ │
│ │ (SNOMED cluster CTE + GP records) │ │
│ │ → Build indication_df: UPID → Search │ │
│ │ Term (matched) or Directorate (no GP)│ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ For each of 6 date filter combos: │ │
│ │ → generate_icicle_chart_indication() │ │
│ │ → extract_indication_fields() │ │
│ │ → convert_to_records("indication") │ │
│ └──────────────────────────────────────────┘ │
│ │
└───────────────────────┬───────────────────────────────────┘
▼ (insert_pathway_records)
┌──────────────────────────────────────────┐
│ SQLite: pathway_nodes table │
│ → ~3,600 nodes across 12 datasets │
│ → UNIQUE(date_filter_id, chart_type, │
│ ids) prevents cross-type overwrites │
│ → Indexed for fast filtering │
└──────────────────────────────────────────┘
[Dash App: python run_dash.py]
┌──────────────────────────────────────────┐
│ Global Sub-Header (date dropdowns, │
│ chart type toggle pills) │
│ → Triggers update_app_state callback │
└──────────────────────────────────────────┘
├─── Patient Pathways View ─────────────────────────────┐
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ Filter Bar (Drugs/Trusts/Directorates) │ │
│ │ → Modal selections → app-state │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ load_pathway_data callback │ │
│ │ → chart-data store │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ├──────────────────────────────┐ │
│ ▼ ▼ │
│ ┌────────────────────┐ ┌──────────────────────┐ │
│ │ update_kpis │ │ update_chart │ │
│ │ → header KPIs │ │ → Icicle or Sankey │ │
│ └────────────────────┘ └──────────────────────┘ │
│ │
├─── Trust Comparison View ─────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ Landing Page │ │
│ │ → Directorate/Indication buttons │ │
│ │ → Click → selected_comparison_dir │ │
│ └──────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ 6-Chart Dashboard │ │
│ │ → Market Share, Cost Waterfall, Dosing │ │
│ │ → Heatmap, Duration, Cost Effectiveness│ │
│ │ → All per-trust within one directorate │ │
│ └──────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────┘
```
### Reference Data Files (`data/`)
| File | Purpose |
|------|---------|
| `include.csv` | Drug filter list with default selections (Include=1) |
| `defaultTrusts.csv` | NHS Trust list for filter |
| `directory_list.csv` | Medical specialties/directories |
| `drugnames.csv` | Drug name standardization mapping |
| `org_codes.csv` | Provider code to organization name mapping |
| `drug_directory_list.csv` | Valid drug-to-directory mappings (pipe-separated) |
| `treatment_function_codes.csv` | NHS treatment function code mappings |
| `drug_indication_clusters.csv` | Drug to SNOMED cluster mappings |
| `ta-recommendations.xlsx` | NICE TA recommendations |
| `pathways.db` | SQLite database (~3.5 MB: reference tables + pathway nodes) |
### Key Patterns
**Department Identification Fallback Chain:**
The `department_identification()` function has 5 levels of fallback:
1. **SINGLE_VALID_DIR** - Drug has only one valid directory
2. **EXTRACTED** - Extracted from Additional Detail/Description fields
3. **CALCULATED_MOST_FREQ** - Most frequent valid directory for UPID/Drug
4. **UPID_INFERENCE** - Inferred from other records with same UPID
5. **UNDEFINED** - No directory could be determined
**Indication Lookup Workflow (for indication charts):**
1. Extract unique `PseudoNHSNoLinked` values from HCD activity data
2. Query Snowflake in batches of 500 patients:
- Embed `CLUSTER_MAPPING_SQL` (~148 Search_Term → Cluster_ID mappings) as CTE
- Join `ClinicalCodingClusterSnomedCodes` to get SNOMED codes per cluster
- Join `PrimaryCareClinicalCoding` on `PatientPseudonym` = `PseudoNHSNoLinked`
- Use `QUALIFY ROW_NUMBER() OVER (PARTITION BY PatientPseudonym ORDER BY EventDateTime DESC) = 1` for most recent match
3. Build `indication_df` mapping UPID → Search_Term (matched) or Directorate + " (no GP dx)" (unmatched)
4. Pass to `generate_icicle_chart_indication()` for pathway hierarchy building
**Data Source Fallback Chain** (for raw data loading, not used by Dash app):
1. Query cache for recent results
2. Attempt Snowflake connection
3. Fall back to CSV/Parquet files
## Database Schema (~3.5 MB)
### Reference Tables
- `ref_drug_names` - Drug name standardization
- `ref_organizations` - Provider code to name mapping
- `ref_directories` - Valid directory names
- `ref_drug_directory_map` - Valid drug-directory pairs
- `ref_drug_indication_clusters` - Drug to SNOMED cluster mapping
### Pathway Tables
- `pathway_date_filters` - 6 pre-defined date filter combinations
- Columns: `id`, `initiated`, `last_seen`, `is_default`, `description`
- Auto-populated via migration
- `pathway_nodes` - Pre-computed pathway hierarchy nodes (~3,600 rows for 12 datasets)
- Routing: `chart_type` ("directory" or "indication"), `date_filter_id`
- Hierarchy: `parents`, `ids`, `labels`, `level`
- Metrics: `value`, `cost`, `costpp`, `cost_pp_pa`, `colour`
- Dates: `first_seen`, `last_seen`, `first_seen_parent`, `last_seen_parent`
- Statistics: `average_spacing`, `average_administered`, `avg_days`
- Denormalized: `trust_name`, `directory`, `drug_sequence`
- Foreign key: `date_filter_id``pathway_date_filters.id`
- Unique constraint: `UNIQUE(date_filter_id, chart_type, ids)` — critical for INSERT OR REPLACE correctness
- Indexed for: date_filter_id, chart_type, trust_name, directory, level
- `pathway_refresh_log` - Tracks data refresh status
- Columns: `refresh_id`, `started_at`, `completed_at`, `status`, `records_processed`, `error_message`, `source_row_count`
## Input Data Requirements
The input data (CSV/Parquet) must contain columns including:
- `Provider Code`, `PersonKey` - Used to create UPID
- `PseudoNHSNoLinked` - NHS pseudonym for GP record matching (indication charts)
- `Drug Name`, `Intervention Date`, `Price Actual`
- `OrganisationName`
- Various `Additional Detail/Description` columns for directory extraction
- `Treatment Function Code`
## Output
Two-view Dash application with distinct analytical perspectives:
**Patient Pathways View** (2 tabs):
1. **Icicle** — Hierarchical pathway view (Directory: Trust → Directorate → Drug → Pathway; Indication: Trust → GP Diagnosis → Drug → Pathway)
2. **Sankey** — Drug switching flows across 1st → 2nd → 3rd treatment lines
Patient Pathways supports:
- Directory / Indication toggle
- Date filter combinations (6 options)
- Trust, drug, and directorate filters via modals
- Lazy rendering (only active tab computed)
**Trust Comparison View** (6 charts in dashboard):
Landing page with directorate/indication buttons → 6-chart dashboard for selected directorate:
1. **Market Share** — Drug breakdown per trust (stacked bars)
2. **Cost Waterfall** — Per-trust cost within directorate
3. **Dosing** — Drug dosing intervals by trust
4. **Heatmap** — Trust × drug matrix
5. **Duration** — Drug durations by trust
6. **Cost Effectiveness** — Pathway costs within directorate (not split by trust)
Trust Comparison supports:
- Directory / Indication toggle (changes landing page buttons)
- Date filter combinations (6 options)
- All 6 charts scoped to selected directorate
## Testing
```bash
# Run all tests with coverage
python -m pytest tests/ -v --cov=core --cov=analysis
# Run specific test file
python -m pytest tests/test_config.py -v
# Run specific test class
python -m pytest tests/test_data_transformations.py::TestPatientId -v
```
Test coverage includes:
- PathConfig validation (23 tests)
- AnalysisFilters validation (26 tests)
- Data transformation functions (23 tests)
- Directory assignment logic (19 tests)
## Configuration
### Snowflake Connection (`src/config/snowflake.toml`)
```toml
[snowflake]
account = "your-account"
database = "DATA_HUB"
schema = "CDM"
warehouse = "your-warehouse"
authenticator = "externalbrowser" # Required for NHS SSO
```
### Logging
Logs are written to `logs/` directory with structured format.
Configure via `src/core/logging_config.py`.
## Breaking Changes from Original App
The pre-computed pathway architecture introduces these changes:
### Date Filters
- **Old**: Date pickers for arbitrary `start_date` and `end_date`
- **New**: Two dropdowns:
- "Treatment Initiated": All years, Last 2 years, Last 1 year
- "Last Seen": Last 6 months, Last 12 months
- **Reason**: Pre-computed pathways require fixed date combinations for performance
### Data Refresh
- **Old**: Real-time pathway calculation on each filter change
- **New**: Pre-computed pathways stored in SQLite, refreshed via CLI command
- **Impact**: Data is as fresh as the last `python -m cli.refresh_pathways` run
- **Benefit**: Sub-50ms filter response time vs multi-minute calculations
### State Management (Dash)
- State lives in 4 `dcc.Store` components: `app-state`, `chart-data`, `reference-data`, `active-tab`
- Filter state: `chart_type`, `initiated`, `last_seen`, `date_filter_id`, `selected_drugs`, `selected_directorates`, `selected_trusts`
- View state: `active_view` ("patient-pathways" | "trust-comparison"), `selected_comparison_directorate` (null | directorate name)
- Chart type toggle: "By Directory" / "By Indication" pills in global sub-header
- Drug/trust/directorate selection via `dmc.Modal` dialogs (Patient Pathways only)
- Fraction KPIs in header (X/X patients, X/X drugs, £X/£X cost)
### Icicle Chart (Patient Pathways)
- Full 10-field customdata structure (value, colour, cost, costpp, first_seen, last_seen, first_seen_parent, last_seen_parent, average_spacing, cost_pp_pa)
- NHS blue gradient colorscale: Heritage Blue #003087 → Pale Blue #E3F2FD
- Treatment statistics (average_spacing, cost_pp_pa) in hover tooltips
- First/last seen dates for drug nodes
- `create_icicle_from_nodes()` in `src/visualization/plotly_generator.py` — shared function accepting list-of-dicts
## Development
### Adding New Analysis Features
1. Add statistical functions to `src/analysis/statistics.py`
2. Integrate into pipeline in `src/analysis/pathway_analyzer.py`
3. Update visualization in `src/visualization/plotly_generator.py`
### Adding New Reference Data
1. Add CSV file to `data/` directory
2. Define schema in `src/data_processing/schema.py`
3. Create migration function in `src/data_processing/reference_data.py`
4. Add path to `PathConfig` in `src/core/config.py`
+20
View File
@@ -0,0 +1,20 @@
FROM python:3.11-slim
WORKDIR /app
COPY . .
# Runtime deps only — excludes snowflake, pywebview, pyinstaller, pyarrow, fastparquet
RUN pip install --no-cache-dir \
dash>=2.14.0 \
dash-mantine-components>=0.14.0 \
plotly>=5.15.0 \
pandas>=2.0.3 \
numpy>=1.25.0 \
gunicorn>=21.0.0
# Generate synthetic database at build time
RUN python scripts/generate_demo_db.py
EXPOSE 8050
CMD ["gunicorn", "--bind", "0.0.0.0:8050", "--workers", "2", "--timeout", "120", "dash_app.app:server"]
-404
View File
@@ -1,404 +0,0 @@
# Implementation Plan — Dashboard Visualization Improvements
## Project Overview
Comprehensive review and improvement of all Plotly charts in the Dash dashboard. Four tiers: bug fixes, visual polish, new analytics from existing data, and new analytics requiring backend work.
**Primary file**: `src/visualization/plotly_generator.py`
**Palette policy**: Broader than NHS brand (maximally-distinct colors for trust comparisons)
**Constraint**: `python run_dash.py` must work after every task
### What Changes
- `src/visualization/plotly_generator.py` — shared styling constants, bug fixes, new chart functions
- `src/data_processing/pathway_queries.py` — new/modified query functions
- `dash_app/data/queries.py` — thin wrappers for new queries
- `dash_app/callbacks/chart.py` — remove Trends tab, fix chart height
- `dash_app/callbacks/trust_comparison.py` — trust color palette, heatmap metric toggle
- `dash_app/callbacks/trends.py` — NEW: Trends view callbacks (directorate overview + drug drill-down)
- `dash_app/callbacks/__init__.py` — register new trends callbacks
- `dash_app/components/chart_card.py` — remove Trends tab, metric toggle cleanup
- `dash_app/components/trust_comparison.py` — metric toggle component
- `dash_app/components/trends.py` — NEW: Trends landing + detail components
- `dash_app/components/sidebar.py` — add Trends nav item
- `dash_app/callbacks/navigation.py` — 3-way view switching
- `dash_app/callbacks/filters.py` — add nav-trends input
- `dash_app/app.py` — add trends-view to layout, add selected_trends_directorate to app-state
- `dash_app/assets/nhs.css` — chart height CSS for responsive sizing
### What Stays (DO NOT MODIFY)
- Pipeline/analysis logic: `pathway_pipeline.py`, `transforms.py`, `diagnosis_lookup.py`, `pathway_analyzer.py`
- Database schema and `pathway_nodes` table
- CLI refresh command and `cli/compute_trends.py`
- Existing callback chain architecture (app-state → chart-data → UI)
- Trust Comparison view (unchanged)
---
## Phase A: Core Fixes + Shared Constants
### A.1 Extract shared styling constants + `_base_layout()` helper
- [x] Add module-level constants to top of `src/visualization/plotly_generator.py`:
```python
CHART_FONT_FAMILY = "Source Sans 3, system-ui, sans-serif"
CHART_TITLE_SIZE = 18
CHART_TITLE_COLOR = "#1E293B"
GRID_COLOR = "#E2E8F0"
ANNOTATION_COLOR = "#768692"
TRUST_PALETTE = [
"#005EB8", "#DA291C", "#009639", "#ED8B00",
"#7C2855", "#00A499", "#330072",
]
DRUG_PALETTE = [
"#005EB8", "#DA291C", "#009639", "#ED8B00", "#7C2855",
"#00A499", "#330072", "#E06666", "#6FA8DC", "#93C47D",
"#F6B26B", "#8E7CC3", "#C27BA0", "#76A5AF", "#FFD966",
]
```
- [x] Create `_base_layout(title, **overrides)` helper returning a dict with shared layout properties (title font, hoverlabel, paper/plot bgcolor, autosize, font family)
- [x] Apply `_base_layout()` to `create_icicle_from_nodes()` as a proof-of-concept (keep all existing behavior, just DRY the layout dict)
- **Checkpoint**: `python run_dash.py` starts, icicle chart unchanged visually
### A.2 Fix heatmap colorscale + cell annotations (Patient Pathways)
- [x] In `create_heatmap_figure()` (~L1189):
1. Replace non-linear colorscale with linear 5-stop: `[0.0 #E3F2FD, 0.25 #90CAF9, 0.5 #42A5F5, 0.75 #1E88E5, 1.0 #003087]`
2. Add `text=text_values, texttemplate="%{text}"` with formatted values per metric (patients: `"N"`, cost: `"£Nk"`, cost_pp_pa: `"£N"`)
3. Set `zmin=0` explicitly
4. Remove explicit `width`, use `autosize=True`
5. Replace `l=200` with `l=8` + `yaxis automargin=True`
6. Add subtitle annotation when 25-drug cap is hit: `"Showing top 25 of N drugs"`
7. Reduce `xgap/ygap` from 2→1 when >15 columns
- [x] Apply same fixes to `create_trust_heatmap_figure()` (~L1582)
- [x] Apply `_base_layout()` to both heatmap functions
- **Checkpoint**: Heatmaps show linear color gradient, cell text visible, no fixed width overflow
### A.3 Fix legend overflow in 4 charts
- [x] Create `_smart_legend(n_items)` helper that returns legend dict:
- When >15 items: vertical legend on right (`orientation="v", x=1.02, y=1, xanchor="left"`) with dynamic right margin
- When ≤15: horizontal legend with dynamic bottom margin based on estimated row count
- [x] Also created `_smart_legend_margin(n_items)` helper returning margin dict with dynamic b/r values
- [x] Apply to `create_market_share_figure()` — also replaced local nhs_colours with DRUG_PALETTE
- [x] Apply to `create_trust_market_share_figure()` — also replaced local nhs_colours with DRUG_PALETTE, fixed Unicode escapes to literal chars
- [x] Apply to `create_dosing_figure()` — replaced local nhs_colours with DRUG_PALETTE, legend adapts to trace count
- [x] Apply to `create_trust_duration_figure()` — replaced local nhs_colours with TRUST_PALETTE, fixed l=200→l=8+automargin
- [x] Apply `_base_layout()` to all 4 functions
- **Checkpoint**: Legends don't overlap chart content with 42 drugs or 7 trusts
### A.4 Fix trust comparison color differentiation
- [x] In `create_trust_duration_figure()`: replace `nhs_colours` list with `TRUST_PALETTE` (done in A.3)
- [x] Add `is_trust_comparison=False` param to `create_cost_waterfall_figure()` — use `TRUST_PALETTE` when True
- [x] Update `tc_cost_waterfall` callback in `dash_app/callbacks/trust_comparison.py` to pass `is_trust_comparison=True`
- [x] Fix `_dosing_by_drug()` blue→blue interpolation: replaced with `plotly.colors.sample_colorscale("Viridis", ...)` for meaningful gradient
- [x] Replace `nhs_colours` in `create_trust_market_share_figure()` with `DRUG_PALETTE` for drug traces (done in A.3)
- [x] Apply `_base_layout()` to all affected functions (done in A.3 for trust_market_share and trust_duration)
- **Checkpoint**: Trust Comparison charts have 7 visually distinct trust colors; dosing has meaningful gradient
---
## Phase B: Visual Polish
### B.1 Fix title inconsistencies across all charts
- [x] Sankey: replaced local nhs_colours with DRUG_PALETTE, title color `"#003087"` → `CHART_TITLE_COLOR` via `_base_layout()`
- [x] Dosing: already converted in A.3 — uses `_base_layout()` with CHART_TITLE_COLOR
- [x] Patient Pathways heatmap: already converted in A.2 — uses `_base_layout()` with CHART_TITLE_COLOR
- [x] Duration: title color `"#003087"` → `CHART_TITLE_COLOR`, fixed l=200→l=8+automargin, used constants for annotations
- [x] All Trust Comparison functions: already use `_base_layout()` (A.2-A.4), title size=18 via CHART_TITLE_SIZE
- [x] Applied `_base_layout()` to all remaining chart functions: Sankey, Cost Effectiveness, Duration
- [x] Cost Effectiveness: replaced 38-line manual layout with `_base_layout()`, hardcoded colors/fonts → constants
- **Checkpoint**: All chart titles use consistent font, size, and color
### B.2 Cost effectiveness smooth gradient
- [x] In `create_cost_effectiveness_figure()`:
- Replaced 3-bin hard threshold with smooth `_lerp_color()` RGB interpolation
- Green (#009639) → Amber (#ED8B00) for ratio 00.5
- Amber (#ED8B00) → Red (#DA291C) for ratio 0.51.0
- [x] `_base_layout()` already applied in B.1
- **Checkpoint**: Lollipop dots show smooth green→amber→red gradient
### B.3 Sankey narrow-screen fix
- [x] In `create_sankey_figure()` (~L808):
- Changed `arrangement="snap"` → `arrangement="freeform"`
- Increased `pad` from 20 → 25
- **Checkpoint**: Sankey nodes don't overlap on narrow viewports
### B.4 Heatmap metric toggle (both views)
- [x] Add `dmc.SegmentedControl` component next to Patient Pathways heatmap:
- Options: Patients, Cost, Cost p.a.
- ID: `heatmap-metric-toggle`
- Added to `dash_app/components/chart_card.py` in header, hidden by default, shown when heatmap tab active
- Also added "heatmap" tab to TAB_DEFINITIONS (was only in ALL_TAB_DEFINITIONS before)
- [x] Add `dmc.SegmentedControl` next to Trust Comparison heatmap:
- ID: `tc-heatmap-metric-toggle`
- Added to `dash_app/components/trust_comparison.py` inline in heatmap chart cell header
- [x] Update `_render_heatmap()` in `dash_app/callbacks/chart.py` to accept metric param, `update_chart` passes toggle value + controls toggle visibility via `heatmap-metric-wrapper` style output
- [x] Update `tc_heatmap` callback in `dash_app/callbacks/trust_comparison.py` to read `tc-heatmap-metric-toggle` value and pass to `create_trust_heatmap_figure()`
- **Checkpoint**: Heatmap metric toggles work in both views, switching between patients/cost/cost_pp_pa
---
## Phase C: New Analytics (Existing Data)
### C.1 Retention funnel chart
- [x] Create `get_retention_funnel()` in `src/data_processing/pathway_queries.py`:
- Query level 3+ nodes, aggregate patient counts by treatment line depth (level 3=1st drug, 4=2nd, 5=3rd)
- Return: `[{depth: 1, label: "1st drug", patients: N, pct: 100.0}, ...]`
- Supports directory/trust filters
- [x] Add thin wrapper in `dash_app/data/queries.py`
- [x] Create `create_retention_funnel_figure(data, title)` in `src/visualization/plotly_generator.py`:
- Uses `go.Funnel` with NHS blue gradient (#003087 → #1E88E5)
- Shows absolute patient count + percentage retained as text inside bars
- Uses `_base_layout()` for consistent styling
- [x] Add "Funnel" tab to `TAB_DEFINITIONS` in `chart_card.py` (4 tabs: Icicle, Sankey, Heatmap, Funnel)
- [x] Add `_render_funnel()` helper and tab dispatch in `dash_app/callbacks/chart.py`
- **Checkpoint**: Funnel tab shows retention by treatment line depth, responds to filters
### C.2 Pathway depth distribution chart
- [x] Create `get_pathway_depth_distribution()` in `src/data_processing/pathway_queries.py`:
- Aggregate patient counts at level 3 (1-drug), level 4 (2-drug), etc.
- Subtract child counts to get patients who STOPPED at each depth
- Return: `[{depth: 1, label: "1 drug only", patients: N, pct: 80.2}, ...]`
- [x] Add thin wrapper in `dash_app/data/queries.py`
- [x] Create `create_pathway_depth_figure(data, title)` in `src/visualization/plotly_generator.py`:
- Horizontal bar chart with NHS blue gradient by depth
- Text shows "N (pct%)" inside bars
- Uses `_base_layout()` for consistent styling
- [x] Add "Depth" tab to `TAB_DEFINITIONS` in `chart_card.py` (5 tabs: Icicle, Sankey, Heatmap, Funnel, Depth)
- [x] Add `_render_depth()` helper and tab dispatch in `dash_app/callbacks/chart.py`
- **Checkpoint**: Depth tab shows patient distribution by treatment line count
### C.3 Duration vs Cost scatter plot
- [x] Create `get_duration_cost_scatter()` in `src/data_processing/pathway_queries.py`:
- Query level 3 nodes for drug-level data with avg_days and cost_pp_pa
- Aggregates across trusts using weighted averages
- Return: `[{drug, directory, avg_days, cost_pp_pa, patients}, ...]`
- [x] Add thin wrapper in `dash_app/data/queries.py`
- [x] Create `create_duration_cost_scatter_figure(data, title)` in `src/visualization/plotly_generator.py`:
- Scatter: x=avg_days, y=cost_pp_pa, size=patients (global max), color=directory
- One trace per directory for legend grouping using DRUG_PALETTE
- Quadrant lines at median values with annotations
- Hover shows drug name, directory, all values
- [x] Add "Scatter" tab to `TAB_DEFINITIONS` in `chart_card.py` (6 tabs: Icicle, Sankey, Heatmap, Funnel, Depth, Scatter)
- [x] Add `_render_scatter()` helper and tab dispatch in `dash_app/callbacks/chart.py`
- **Checkpoint**: Scatter tab shows drugs by duration vs cost with directorate coloring
### C.4 Drug switching network graph
- [x] Create `get_drug_network()` in pathway_queries.py — undirected edges without ordinal suffixes, node patients from level 3, edge co-occurrence from level 4+
- [x] Add thin wrapper in `dash_app/data/queries.py`
- [x] Create `create_drug_network_figure(data, title)` in `src/visualization/plotly_generator.py`:
- Circular layout using `go.Scatter` for nodes + individual edge traces as lines
- Node size = total patients (1250px), edge width = switching flow (0.56px), edge opacity scales with strength
- `DRUG_PALETTE` for node colors, NHS Blue (`rgba(0,94,184,...)`) for edges
- [x] Added as separate "Network" tab (7th tab: Icicle, Sankey, Heatmap, Funnel, Depth, Scatter, Network)
- [x] Added `_render_network()` helper and dispatch case in `chart.py`
- **Checkpoint**: Network view shows drug switching as a graph alternative to Sankey
---
## Phase D: New Analytics (Backend Work)
### D.1 Temporal trend analysis (historical snapshots approach)
- [x] **D.1a — Create `cli/compute_trends.py` CLI script**:
- Creates `pathway_trends` table via `CREATE TABLE IF NOT EXISTS` (no schema.py changes):
```
pathway_trends(period_end TEXT, drug TEXT, directory TEXT, patients INTEGER,
total_cost REAL, cost_pp_pa REAL, PRIMARY KEY(period_end, drug, directory))
```
- Imports existing `fetch_and_transform_data()` and `process_pathway_for_date_filter()` from `pathway_pipeline.py` — does NOT modify them
- Fetches all activity data once from Snowflake
- Loops over 6-month historical endpoints (2021-06-30 through 2025-12-31, ~10 periods)
- For each endpoint: calls `process_pathway_for_date_filter()` with `max_date=endpoint` using `all_6mo` config
- Extracts level 3 summary stats (drug, directory, patients, cost, cost_pp_pa) from resulting DataFrame
- Inserts aggregated rows into `pathway_trends` table
- Run separately: `python -m cli.compute_trends` (not part of main refresh)
- [x] **D.1b — Add Trends tab to Dash** (standard 6-step pattern):
1. Create `get_trend_data(db_path, metric, directory, drug)` in `pathway_queries.py` — query `pathway_trends` table, return time-series data
2. Add thin wrapper in `dash_app/data/queries.py`
3. Create `create_trend_figure(data, title, metric)` in `plotly_generator.py` — line chart: x=period_end, y=metric, one line per drug (or per directory). Uses `_base_layout()` + `_smart_legend()`. Add `dmc.SegmentedControl` for metric toggle (patients / cost / cost_pp_pa)
4. Add "Trends" tab to `TAB_DEFINITIONS` in `chart_card.py`
5. Add `_render_trends()` helper + dispatch case in `chart.py`
6. Handle empty state: if `pathway_trends` table doesn't exist or is empty, show "Run `python -m cli.compute_trends` to generate trend data" message
- **Checkpoint**: Trends tab shows drug/directory trends over 10 historical periods, responds to filters. Empty state handled gracefully if trends not yet computed.
### D.2 Average administered doses analysis
- [x] Create `get_dosing_distribution()` query in `pathway_queries.py`:
- Level 3 nodes with parsed `average_administered` JSON (position 0 = avg doses for drug)
- Aggregates across trusts using weighted averages by patient count
- Supports directory/trust filters. Returns `[{drug, directory, avg_doses, patients}]`
- [x] Add thin wrapper in `dash_app/data/queries.py`
- [x] Create `create_dosing_distribution_figure(data, title)` in plotly_generator.py:
- Horizontal bar chart (avg doses per drug, one bar per drug x directory)
- Colored by directory using DRUG_PALETTE, `_base_layout()` + `_smart_legend()`
- Dynamic height, patient count in hover
- [x] Add "Doses" tab to TAB_DEFINITIONS (9th tab)
- [x] Add `_render_doses()` helper + dispatch in `chart.py`
- **Checkpoint**: Doses tab shows average administered doses per drug, responds to filters
### D.3 Drug timeline (Gantt chart)
- [x] Create `get_drug_timeline()` query in `pathway_queries.py`:
- Level 3 nodes with `first_seen`, `last_seen`, `labels`, `value` per drug × directory
- Aggregates across trusts: MIN(first_seen), MAX(last_seen), SUM(value), weighted avg cost_pp_pa
- Supports directory/trust filters
- [x] Create `create_drug_timeline_figure(data, title)` in plotly_generator.py:
- Gantt-style using `go.Bar` (horizontal bars from first_seen to last_seen)
- One trace per bar, grouped by directory with legend grouping
- Colored by directory using `DRUG_PALETTE`, patient count as bar text
- Dynamic height (28px per bar), `_base_layout()` + `_smart_legend()`
- [x] Add "Timeline" tab to `TAB_DEFINITIONS` in `chart_card.py` (8th tab)
- [x] Add `_render_timeline()` helper + dispatch case in `chart.py`
- **Checkpoint**: Timeline tab shows when each drug cohort was active
---
## Phase E: Trends View Redesign + Chart Height
### E.1 Remove Trends tab from Patient Pathways
- [x] Remove `("trends", "Trends")` from `TAB_DEFINITIONS` in `dash_app/components/chart_card.py`
- [x] Remove `trends-metric-wrapper` div and `trends-metric-toggle` SegmentedControl from `chart_card.py`
- [x] Remove `_render_trends()` helper from `dash_app/callbacks/chart.py`
- [x] Remove `elif active_tab == "trends"` dispatch case from `update_chart()`
- [x] Remove `Output("trends-metric-wrapper", "style")` and `Input("trends-metric-toggle", "value")` from `update_chart()` callback signature — updated ALL 4 return paths to return 3 values instead of 4
- [x] Remove thin wrapper `get_trend_data()` from `dash_app/data/queries.py` (will be re-imported by the new Trends view callbacks)
- [x] Keep `get_trend_data()` in `pathway_queries.py` — it's still used by the new Trends view
- [x] Keep `create_trend_figure()` in `plotly_generator.py` — it's still used by the new Trends view
- **Checkpoint**: Patient Pathways has 9 tabs (Icicle through Doses, no Trends). `python run_dash.py` starts cleanly. PASSED.
### E.2 Add Trends sidebar nav item + view container
- [x] Add `"trends"` icon SVG to `_ICONS` dict in `dash_app/components/sidebar.py` — use a line chart icon: `<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>`
- [x] Add `_sidebar_item("Trends", "trends", active=False, item_id="nav-trends")` to sidebar children
- [x] Add `html.Div(id="trends-view", style={"display": "none"}, children=[...])` to `app.py` layout inside `view-container`, after `trust-comparison-view`
- [x] Update `switch_view()` in `dash_app/callbacks/navigation.py`:
- Add `Output("trends-view", "style")` and `Output("nav-trends", "className")` — now 3 views, 3 nav items (6 outputs total)
- Handle 3-way switching: `"patient-pathways"`, `"trust-comparison"`, `"trends"`
- [x] Update `update_app_state()` in `dash_app/callbacks/filters.py`:
- Add `Input("nav-trends", "n_clicks")`
- Add `elif triggered_id == "nav-trends": active_view = "trends"` case
- **Checkpoint**: 3 sidebar items visible. Clicking "Trends" switches to empty trends view. `python run_dash.py` starts cleanly. PASSED.
### E.3 Create Trends landing page — directorate-level trends
- [x] Create `dash_app/components/trends.py`:
- `make_trends_landing()` — container with title, description, metric toggle (`dmc.SegmentedControl` id: `trends-view-metric-toggle`, options: Patients / Cost per Patient / Cost per Patient p.a.), and `dcc.Graph(id="trends-overview-chart")` wrapped in `dcc.Loading`
- `make_trends_detail()` — hidden container with back button (id: `trends-back-btn`), title (id: `trends-detail-title`), same metric toggle, and `dcc.Graph(id="trends-detail-chart")` wrapped in `dcc.Loading`
- [x] Update `get_trend_data()` in `pathway_queries.py` to support `group_by` parameter:
- `group_by="drug"` (default, existing behavior): one line per drug
- `group_by="directory"`: one line per directory (aggregate drugs within each directory)
- When `group_by="directory"`: `SELECT period_end, directory AS name, SUM(...) ... GROUP BY period_end, directory`
- [x] Update thin wrapper in `dash_app/data/queries.py` to pass `group_by` param
- [x] Create `dash_app/callbacks/trends.py` with `register_trends_callbacks(app)`:
- Callback to render directorate-level chart: Input `app-state` + `trends-view-metric-toggle` → Output `trends-overview-chart` figure. Calls `get_trend_data(group_by="directory", metric=...)` → `create_trend_figure(data, title, metric)`.
- Only fires when `active_view == "trends"` and `selected_trends_directorate` is None.
- [x] Register in `dash_app/callbacks/__init__.py`
- [x] Rename "Cost" label to "Cost per Patient" in the metric toggle options (value stays `total_cost`)
- [x] Wire `trends-view` div in `app.py` to contain `make_trends_landing()` + `make_trends_detail()`
- **Checkpoint**: Trends view shows directorate-level line chart. Metric toggle switches y-axis. Lines show one per directorate. PASSED.
### E.4 Add drug drill-down within Trends view
- [x] Add `selected_trends_directorate` key (default `None`) to `app-state` initial data in `app.py` (already done in E.2)
- [x] Add `customdata=[name]*len(periods)` to each trace in `create_trend_figure()` so directorate name is accessible from clickData
- [x] Add `Input("trends-overview-chart", "clickData")` and `Input("trends-back-btn", "n_clicks")` to `update_app_state()` in `filters.py`:
- Clicking a trace point extracts directorate name from `clickData["points"][0]["customdata"]`
- Back button clears `selected_trends_directorate` to None
- Chart type change also clears `selected_trends_directorate`
- [x] Landing/detail toggle callback already exists in `trends.py` (`toggle_trends_subviews`) — handles show/hide based on `selected_trends_directorate`
- [x] Add `render_trends_detail()` callback in `trends.py`:
- Input: `app-state` + `trends-detail-metric-toggle` → Output `trends-detail-chart`
- Calls `get_trend_data(directory=selected, metric=..., group_by="drug")` → `create_trend_figure()`
- Guards: only fires when `active_view == "trends"` and `selected_trends_directorate` is not None
- **Checkpoint**: Click a directorate line → drill into drug-level trends. Back button returns to overview. `python run_dash.py` starts cleanly. PASSED.
### E.5 Fix chart height to fill viewport
- [x] In `create_trend_figure()` in `plotly_generator.py`: removed explicit `height=500`, `autosize=True` from `_base_layout()` handles it
- [x] Reviewed ALL chart functions — removed 4 fixed heights: `create_cost_waterfall_figure()` (500), `create_duration_cost_scatter_figure()` (550), `create_drug_network_figure()` (600), `create_trend_figure()` (500). Kept 13 dynamic heights (`max(...)`, `fig_height`, `dynamic_height`).
- [x] Added CSS rules: `#pathway-chart .js-plotly-plot, .plot-container, .svg-container { height: 100% !important }` to propagate flex container height
- [x] Verified CSS flex chain: `.chart-card` → `.dash-loading-callback > div` → `#chart-container` → `#pathway-chart` → `.js-plotly-plot` — all flex with `min-height: 0`
- [x] Renamed "Cost" to "Cost per Patient" and "Cost p.a." to "Cost per Patient p.a." in heatmap toggles in `chart_card.py` and `trust_comparison.py`
- **Checkpoint**: Charts fill available viewport height in Patient Pathways. No fixed 500px cutoff. `python run_dash.py` starts cleanly.
---
## Completion Criteria
### Phase A
- [x] All charts use `_base_layout()` for consistent styling
- [x] Heatmaps have linear colorscale + cell annotations + autosize
- [x] Legends don't overflow at any drug/trust count
- [x] Trust Comparison charts use 7 maximally-distinct colors
- [x] `python run_dash.py` starts cleanly
### Phase B
- [x] All chart titles use `CHART_TITLE_SIZE` and `CHART_TITLE_COLOR`
- [x] Cost effectiveness uses smooth gradient
- [x] Sankey handles narrow viewports
- [x] Heatmap metric toggle works in both views
- [x] `python run_dash.py` starts cleanly
### Phase C
- [x] Retention funnel renders with real data
- [x] Pathway depth distribution renders with real data
- [x] Duration vs cost scatter renders with quadrant lines
- [x] Drug network graph renders as Sankey alternative
- [x] All new tabs respond to existing filters
- [x] `python run_dash.py` starts cleanly
### Phase D
- [x] Temporal trends computed via historical snapshots (CLI script + Dash tab)
- [x] Dose distribution shows average administered doses per drug
- [x] Drug timeline shows Gantt-style cohort activity
- [x] `python run_dash.py` starts cleanly
### Phase E
- [x] Trends tab removed from Patient Pathways (9 tabs remain)
- [x] 3rd sidebar item "Trends" visible and functional
- [x] Trends landing page shows directorate-level line chart with metric toggle
- [x] Clicking a directorate drills into drug-level trends
- [x] Back button returns to directorate overview
- [x] Charts fill available viewport height (no fixed 500px cutoff)
- [x] "Cost" renamed to "Cost per Patient" in metric toggles
- [x] `python run_dash.py` starts cleanly
---
## Key Reference Files
| File | Purpose |
|------|---------|
| `src/visualization/plotly_generator.py` | PRIMARY — all chart generation functions |
| `src/data_processing/pathway_queries.py` | All SQLite query functions |
| `src/data_processing/parsing.py` | HTML/JSON parsing utilities |
| `dash_app/callbacks/chart.py` | Patient Pathways tab dispatch + chart rendering |
| `dash_app/callbacks/trust_comparison.py` | Trust Comparison 6-chart callbacks |
| `dash_app/components/chart_card.py` | Tab definitions + chart card component |
| `dash_app/components/trust_comparison.py` | TC landing + dashboard layout |
| `dash_app/data/queries.py` | Thin wrappers around shared query functions |
## Key Patterns
### plotly_generator.py structure
- Module-level palettes: `TRUST_PALETTE` (7 colors), `DRUG_PALETTE` (15 colors)
- `_base_layout(title, **overrides)` helper for DRY layout dicts
- `_smart_legend(n_items)` helper for adaptive legend positioning
- Each `create_*_figure()` function accepts list-of-dicts, returns `go.Figure`
### Adding a new chart tab (Patient Pathways)
1. Add query function to `src/data_processing/pathway_queries.py`
2. Add thin wrapper to `dash_app/data/queries.py`
3. Add figure function to `src/visualization/plotly_generator.py`
4. Add tab to `TAB_DEFINITIONS` in `dash_app/components/chart_card.py`
5. Add `_render_*()` helper in `dash_app/callbacks/chart.py`
6. Add dispatch case in `update_chart()` callback
### Existing chart functions in plotly_generator.py
- `create_icicle_from_nodes(nodes, title)` — L113
- `create_market_share_figure(data, title)` — L247
- `create_cost_effectiveness_figure(data, retention, title)` — L384
- `create_cost_waterfall_figure(data, title)` — L562
- `create_sankey_figure(data, title)` — L706
- `create_dosing_figure(data, title, group_by)` — L837
- `_dosing_by_drug(data, colours)` — L926
- `_dosing_by_trust(data, colours)` — L1007
- `create_heatmap_figure(data, title, metric)` — L1189
- `create_duration_figure(data, title, show_directory)` — L1329
- `create_trust_market_share_figure(data, title)` — L1481
- `create_trust_heatmap_figure(data, title, metric)` — L1582
- `create_trust_duration_figure(data, title)` — L1689
-240
View File
@@ -1,240 +0,0 @@
# Ralph Wiggum Loop — Dashboard Visualization Improvements
You are operating inside an automated loop improving Plotly charts in an NHS patient pathway analysis Dash application. Each iteration you receive fresh context — you have NO memory of previous iterations. Your only memory is the filesystem.
**Current Focus**: Fix chart bugs, improve visual polish, add new analytics charts. See IMPLEMENTATION_PLAN.md for the full task list organized into Phases AD.
## First Actions Every Iteration
Read these files in this order before doing anything else:
1. `progress.txt` — What previous iterations accomplished, what's blocked, and what to do next.
2. `IMPLEMENTATION_PLAN.md` — Task list with status markers, architecture overview, and completion criteria.
3. `guardrails.md` — Known failure patterns to avoid. You MUST read and follow these.
4. `CLAUDE.md` — Project architecture and backend code patterns.
Then run `git log --oneline -5` to see recent commits.
## Key Files for This Phase
**When modifying chart functions**, always read first:
- `src/visualization/plotly_generator.py` — PRIMARY file. All chart generation functions live here (~1782 lines).
- `dash_app/callbacks/chart.py` — Patient Pathways tab dispatch and chart rendering helpers.
- `dash_app/callbacks/trust_comparison.py` — Trust Comparison 6-chart callbacks.
**When adding new analytics charts**, also read:
- `src/data_processing/pathway_queries.py` — All SQLite query functions. New queries go here.
- `dash_app/data/queries.py` — Thin wrappers. Add wrapper for each new query.
- `dash_app/components/chart_card.py` — TAB_DEFINITIONS for Patient Pathways tabs.
**When modifying UI components**, read:
- `dash_app/components/trust_comparison.py` — TC landing + dashboard layout.
- `dash_app/assets/nhs.css` — All CSS styles.
## Narration
Narrate your work as you go. Your output is the only visibility the operator has into what's happening. For every significant action, explain what you're doing and why:
- **Reading files**: "Reading plotly_generator.py to locate the heatmap colorscale..."
- **Creating code**: "Adding _base_layout() helper to DRY shared layout properties..."
- **Debugging**: "Chart title color is #003087 instead of CHART_TITLE_COLOR..."
- **Testing**: "Running python run_dash.py to verify the app starts..."
- **Committing**: "Committing heatmap fixes."
Do NOT just output a summary at the end. Narrate throughout.
## Task Selection
1. Read ALL tasks in IMPLEMENTATION_PLAN.md — understand the full picture
2. Skip any marked `[x]` (complete) or `[B]` (blocked)
3. Check progress.txt for guidance — the previous iteration may have recommendations
4. **Choose a task** based on:
- Dependencies (A.1 shared constants before A.2-A.4 which use them)
- Phase ordering (Phase A before B, B before C, C before D)
- Previous iteration's recommendations
5. **Document your reasoning**: Before starting, explain WHY you chose this task
6. Mark your chosen task `[~]` (in progress) in IMPLEMENTATION_PLAN.md
If your chosen task is blocked:
- Mark it `[B]` with a reason
- Document the blocker in progress.txt
- Move to a different ready task
## Development
Work on ONE task per iteration. Build incrementally and verify as you go.
### Key Technologies
- **Dash 4.0.0**: `from dash import Dash, html, dcc, Input, Output, State, ctx, ALL`
- **Dash Mantine Components 2.5.1**: `import dash_mantine_components as dmc``MantineProvider` wraps layout
- **Plotly**: `import plotly.graph_objects as go` — all chart figures
- **SQLite**: `import sqlite3` — read-only access to `data/pathways.db`
- **CSS**: All in `dash_app/assets/nhs.css` — auto-served by Dash
### plotly_generator.py Patterns
All chart functions follow the same pattern:
```python
def create_CHART_figure(data: list[dict], title: str = "", ...) -> go.Figure:
"""Create CHART from prepared data."""
if not data:
return go.Figure()
# Build traces from data
fig = go.Figure(data=traces)
# Apply layout
layout = _base_layout(display_title)
layout.update({...chart-specific overrides...})
fig.update_layout(**layout)
return fig
```
### Adding a New Chart Tab
1. Add query function to `src/data_processing/pathway_queries.py` (accept `db_path` param)
2. Add thin wrapper to `dash_app/data/queries.py` (resolve DB_PATH and delegate)
3. Add figure function to `src/visualization/plotly_generator.py`
4. Add tab to `TAB_DEFINITIONS` in `dash_app/components/chart_card.py`
5. Add `_render_*()` helper in `dash_app/callbacks/chart.py`
6. Add elif case in `update_chart()` callback
### Database Access Pattern
```python
# In src/data_processing/pathway_queries.py
def get_something(db_path: Path, filter_id: str, chart_type: str, ...) -> list[dict]:
conn = sqlite3.connect(str(db_path))
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("SELECT ... WHERE date_filter_id = ? AND chart_type = ?", [filter_id, chart_type])
rows = [dict(row) for row in cursor.fetchall()]
conn.close()
return rows
# In dash_app/data/queries.py (thin wrapper)
from data_processing.pathway_queries import get_something as _get_something
DB_PATH = Path(__file__).resolve().parents[2] / "data" / "pathways.db"
def get_something(filter_id="all_6mo", chart_type="directory", ...):
return _get_something(DB_PATH, filter_id, chart_type, ...)
```
### Verification Steps
After writing code, ALWAYS verify:
1. **Import check**: `python -c "from dash_app.app import app"` (or specific module)
2. **App starts**: `python run_dash.py` — must start without errors
3. **Visual check** (when modifying charts): describe what you expect to see at localhost:8050
4. **For callbacks**: verify the callback chain fires correctly
If any step fails, fix the issue before proceeding.
## Validation Protocol
Every task MUST pass validation before being marked complete:
### Tier 1: Code Validation (MANDATORY)
- Code compiles without Python syntax errors
- Imports work without errors
- `python run_dash.py` starts without exceptions
### Tier 2: Visual Validation (for chart modification tasks)
- Chart renders in the browser
- Colors, labels, legend layout match expectations
- No overflow or overlap issues
### Tier 3: Functional Validation (for callback/toggle tasks)
- Callbacks fire when inputs change
- Metric toggles switch correctly
- New tabs appear and render data
### Validation Failure
If any tier fails:
- DO NOT mark the task complete
- Document the failure in progress.txt
- Fix the issue within this iteration if possible
- If you cannot fix it, mark the task `[B]` with details
## Quality Gates
Before marking ANY task `[x]`, ALL of these must be true:
1. Code is saved to the appropriate file(s)
2. Tier 1 validation passed (imports + app starts)
3. Tier 2/3 validation passed (as applicable)
4. All changes committed to git with a descriptive message
These are non-negotiable.
## Update Progress
After completing your work, append to progress.txt using this format:
```
## Iteration [N] — [YYYY-MM-DD]
### Task: [which task you worked on]
### Why this task:
- [Brief explanation of why you chose this task over others]
### Status: COMPLETE | BLOCKED | IN PROGRESS
### What was done:
- [Specific actions taken]
### Validation results:
- Tier 1 (Code): [import check, app starts]
- Tier 2 (Visual): [chart renders, colors correct]
- Tier 3 (Functional): [callbacks fire, toggles work]
### Files changed:
- [list of files created/modified]
### Committed: [git hash] "[commit message]"
### Patterns discovered:
- [Any reusable learnings — Plotly quirks, layout gotchas, Dash patterns]
### Next iteration should:
- [Explicit guidance for what the next fresh instance should do first]
- [Note any context that would be lost without writing it here]
### Blocked items:
- [Any tasks that are blocked and why]
```
If you discover a failure pattern, add it to `guardrails.md`.
## Commit Changes
1. Stage changed files
2. Use a descriptive commit message referencing the task (e.g., "fix: heatmap colorscale + cell annotations (Task A.2)")
3. Commit after your task is validated and complete
4. If you updated progress.txt with a blocked status, commit that too
## Completion Check
If ALL tasks in IMPLEMENTATION_PLAN.md are marked `[x]`:
1. Run `python run_dash.py` to verify app starts cleanly
2. Verify all completion criteria at the bottom of IMPLEMENTATION_PLAN.md are satisfied
3. Only then output the completion signal on its own line:
```
<promise>COMPLETE</promise>
```
DO NOT output this string under any other circumstances.
DO NOT output it if any task is still `[ ]` or `[B]` or `[~]`.
## Rules
- Complete ONE task per iteration, then update progress and stop
- ALWAYS read progress.txt, guardrails.md before starting work
- **Read plotly_generator.py** when modifying ANY chart function (line numbers shift!)
- **DO NOT modify pipeline/analysis logic** in src/ (pathway_pipeline, transforms, diagnosis_lookup, pathway_analyzer, refresh_pathways)
- **DO add/modify** chart functions in `src/visualization/plotly_generator.py`
- **DO add** new query functions in `src/data_processing/pathway_queries.py`
- **New figure functions** go in `src/visualization/`, not in `dash_app/callbacks/`
- **New query functions** go in `src/data_processing/pathway_queries.py` with thin wrappers in `dash_app/data/queries.py`
- **dcc.Store for state** — no server-side globals
- **Lazy tab rendering** — only compute the active tab's chart
- Keep commits atomic and well-described
- If stuck for 2+ attempts, document in progress.txt and move on
- `python run_dash.py` must work after every task
+69 -43
View File
@@ -4,12 +4,13 @@ A web-based application for analyzing secondary care patient treatment pathways.
## Features
- **Desktop App**: Native window experience via pywebview (no browser needed)
- **Interactive Visualization**: Plotly icicle charts showing patient treatment hierarchies with cost and frequency statistics
- **Dual Chart Types**: Directory-based (Trust → Directorate → Drug → Pathway) and Indication-based (Trust → GP Diagnosis → Drug → Pathway) views
- **Pre-computed Pathways**: Treatment pathways pre-processed and stored in SQLite for sub-50ms filter response times
- **GP Diagnosis Matching**: Patient indications matched from GP records using SNOMED cluster codes (~93% match rate)
- **Modern Web Interface**: Browser-based UI using Dash (Plotly) + Dash Mantine Components with NHS branding
- **Drug Browser**: Drawer-based card browser organized by clinical directorate for drug/indication selection
- **Trend Analysis**: Historical trend views showing how drug usage and costs change over time
- **Modern Web Interface**: Dash (Plotly) + Dash Mantine Components with NHS branding
- **Flexible Filtering**: Filter by date range, NHS trusts, drugs, and medical directories
## Requirements
@@ -29,20 +30,21 @@ cd patient-pathway-analysis
# Install dependencies
uv sync
# One-time dev setup: adds src/ to Python path via .pth file
uv run python setup_dev.py
```
## Quick Start
### Run the Web Application
### Run the Application
```bash
# Run as desktop app (recommended)
python app_desktop.py
# Run in browser (development)
python run_dash.py
```
Open http://localhost:8050 in your browser.
The desktop app opens automatically in a native window. For browser mode, open http://localhost:8050.
The application loads pre-computed pathway data from SQLite on startup. No additional configuration is needed for viewing existing data.
@@ -65,6 +67,19 @@ python -m cli.refresh_pathways --chart-type indication
python -m cli.refresh_pathways --chart-type all --dry-run -v
```
**Compute Trends (for Trends view):**
```bash
# Compute historical trend snapshots
python -m cli.compute_trends
# Custom date range
python -m cli.compute_trends --start 2022-01-01 --end 2025-06-30
# Help
python -m cli.compute_trends --help
```
## Usage
### Interface Overview
@@ -73,21 +88,28 @@ The application has a single-page layout with:
| Component | Purpose |
|-----------|---------|
| **Header** | NHS branding, data freshness indicator (patient count + relative time) |
| **Sidebar** | Navigation items with drawer triggers for Drug Selection, Trust Selection, Indications |
| **KPI Row** | 4 cards: Unique Patients, Drug Types, Total Cost, Indication Match Rate |
| **Filter Bar** | Chart type toggle (By Directory / By Indication) + date filter dropdowns |
| **Chart Card** | Interactive Plotly icicle chart with loading spinner |
| **Drawer** | Right-side panel with drug chips, trust chips, and directorate card browser |
| **Header** | NHS branding, fraction KPIs (patients, drugs, cost), data freshness indicator |
| **Sidebar** | Navigation: Patient Pathways, Trust Comparison, Trends |
| **Sub-Header** | Chart type toggle (By Directory / By Indication) + date filter dropdowns |
| **Filter Bar** | Patient Pathways drug/trust/directorate filter buttons with modals |
| **Chart Card** | 9-tab chart area (Icicle, Sankey, Heatmap, Funnel, Depth, Scatter, Network, Timeline, Doses) |
| **Trust Comparison** | Per-directorate 6-chart dashboard comparing drugs across trusts |
| **Trends** | Historical trend analysis with directorate overview + drug drill-down |
### Filtering Data
1. **Chart Type**: Toggle between "By Directory" and "By Indication" views
2. **Date Filters**: Select treatment initiation period and last-seen window
3. **Drug Selection**: Open the drawer to select specific drugs via chips
4. **Trust Selection**: Open the drawer to filter by NHS trusts
5. **Directorate Browser**: Navigate directorates → indications → drug fragments in the drawer
6. **Clear Filters**: Reset all selections to show full dataset
The application has three analytical views:
1. **Patient Pathways**: Icicle chart + 8 additional analytics tabs with drug/trust/directorate filtering
2. **Trust Comparison**: Per-directorate analysis comparing drugs across trusts
3. **Trends**: Historical trend analysis showing directorate and drug-level changes over time
Common controls across all views:
- **Chart Type**: Toggle between "By Directory" and "By Indication" views
- **Date Filters**: Select treatment initiation period and last-seen window
- **Drug/Trust/Directorate Selection**: Open modals to filter by specific drugs, trusts, or directorates (Patient Pathways)
- **Clear Filters**: Reset all selections to show full dataset
### Understanding the Pathway Chart
@@ -121,22 +143,23 @@ Root (Regional Total)
```
.
├── src/ # All application library code
├── core/ # Foundation: paths, models, logging
│ ├── config/ # Snowflake connection settings
│ ├── data_processing/ # Data layer (SQLite, Snowflake, transforms)
│ ├── analysis/ # Analysis pipeline
│ ├── visualization/ # Plotly chart generation
│ └── cli/ # CLI tools (refresh_pathways)
├── core/ # Foundation: paths, models, logging
├── config/ # Snowflake connection settings
├── data_processing/ # Data layer (SQLite, Snowflake, transforms)
├── analysis/ # Analysis pipeline
├── visualization/ # Plotly chart generation
├── cli/ # CLI tools (refresh_pathways, compute_trends)
├── dash_app/ # Dash web application
│ ├── app.py # App entry point, layout, stores
│ ├── assets/nhs.css # NHS design system CSS
│ ├── data/ # Query wrappers + card browser data
│ ├── components/ # UI components (header, sidebar, etc.)
│ └── callbacks/ # Dash callbacks (filters, chart, KPI, drawer)
├── run_dash.py # Entry point: python run_dash.py
│ ├── components/ # UI components (header, sidebar, chart_card, trends, etc.)
│ └── callbacks/ # Dash callbacks (filters, chart, KPI, trends, etc.)
├── app_desktop.py # Desktop entry point (pywebview native window)
├── run_dash.py # Browser entry point
├── app.spec # PyInstaller packaging spec
├── data/ # Reference data + SQLite DB (pathways.db)
├── tests/ # Test suite (113 tests)
├── tests/ # Test suite (114 tests)
├── docs/ # Documentation
└── archive/ # Historical/deprecated code
```
@@ -149,7 +172,7 @@ See `CLAUDE.md` for detailed architecture documentation.
# Run all tests
python -m pytest tests/ -v
# Run with coverage
# Run with coverage (114 tests)
python -m pytest tests/ -v --cov=core --cov=data_processing --cov=analysis
# Run only fast tests
@@ -158,7 +181,16 @@ python -m pytest tests/ -v -m "not slow"
## Configuration
### Snowflake Connection (`src/config/snowflake.toml`)
### Desktop Packaging
```bash
# Build standalone executable (Windows)
pyinstaller app.spec
# Output: dist/NHS_Pathway_Analysis/NHS_Pathway_Analysis.exe
```
### Snowflake Connection (`config/snowflake.toml`)
```toml
[snowflake]
@@ -177,11 +209,11 @@ authenticator = "externalbrowser" # Required for NHS SSO
# Ensure dependencies are installed
uv sync
# Ensure src/ is on Python path
uv run python setup_dev.py
# Try desktop mode
python app_desktop.py
# Try running with uv
uv run python run_dash.py
# Or browser mode
python run_dash.py
```
### Database not found
@@ -193,16 +225,10 @@ python -m data_processing.migrate
### Snowflake connection issues
1. Ensure `src/config/snowflake.toml` has the correct account identifier
1. Ensure `config/snowflake.toml` has the correct account identifier
2. A browser window will open for SSO authentication
3. Verify your network allows Snowflake connections
## Documentation
- [CLAUDE.md](CLAUDE.md) — Technical architecture documentation
- [docs/USER_GUIDE.md](docs/USER_GUIDE.md) — End-user guide
- [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) — Deployment guide
## License
Internal NHS use only. Not for distribution.
+137
View File
@@ -0,0 +1,137 @@
# -*- mode: python ; coding: utf-8 -*-
"""PyInstaller spec for NHS Pathway Analysis desktop app."""
import os
block_cipher = None
# Project root (where this spec file lives)
ROOT = os.path.abspath(os.path.dirname(SPECPATH)) if 'SPECPATH' in dir() else os.path.abspath('.')
a = Analysis(
['app_desktop.py'],
pathex=[ROOT],
binaries=[],
datas=[
('data/pathways.db', 'data'),
('data/DimSearchTerm.csv', 'data'),
('data/defaultTrusts.csv', 'data'),
('dash_app/assets/nhs.css', 'dash_app/assets'),
],
hiddenimports=[
# Dash internals
'dash',
'dash.dash',
'dash.dcc',
'dash.html',
'dash.exceptions',
'dash._utils',
'dash.dependencies',
'dash_mantine_components',
# Plotly
'plotly',
'plotly.graph_objects',
'plotly.io',
'plotly.express',
'plotly.subplots',
# Data
'pandas',
'pandas._libs.tslibs',
'numpy',
'sqlite3',
# App packages (project root on pathex)
'core',
'core.config',
'core.models',
'core.logging_config',
'core.resource_path',
'data_processing',
'data_processing.database',
'data_processing.schema',
'data_processing.pathway_queries',
'data_processing.parsing',
'data_processing.diagnosis_lookup',
'analysis',
'analysis.pathway_analyzer',
'analysis.statistics',
'visualization',
'visualization.plotly_generator',
# Dash app
'dash_app',
'dash_app.app',
'dash_app.data',
'dash_app.data.queries',
'dash_app.data.card_browser',
'dash_app.callbacks',
'dash_app.callbacks.filters',
'dash_app.callbacks.chart',
'dash_app.callbacks.modals',
'dash_app.callbacks.navigation',
'dash_app.callbacks.trust_comparison',
'dash_app.callbacks.kpi',
'dash_app.callbacks.trends',
'dash_app.components',
'dash_app.components.header',
'dash_app.components.sub_header',
'dash_app.components.sidebar',
'dash_app.components.filter_bar',
'dash_app.components.chart_card',
'dash_app.components.footer',
'dash_app.components.modals',
'dash_app.components.trust_comparison',
'dash_app.components.trends',
# pywebview backend
'webview',
'clr',
'pythonnet',
# Flask (Dash's server)
'flask',
'flask.json',
'flask.json.provider',
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[
'snowflake',
'snowflake.connector',
'cli',
'pytest',
'tests',
'tkinter',
'matplotlib',
'IPython',
'jupyter',
],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='NHS_Pathway_Analysis',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False, # --windowed (no console)
icon=None,
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='NHS_Pathway_Analysis',
)
+62
View File
@@ -0,0 +1,62 @@
"""Desktop entry point: Dash app inside a pywebview native window."""
import sys
import socket
import threading
import time
from pathlib import Path
# Ensure project root is on sys.path so that core/, data_processing/, etc. are importable
_project_root = str(Path(__file__).resolve().parent)
if _project_root not in sys.path:
sys.path.insert(0, _project_root)
import webview
from dash_app.app import app
def find_free_port(start: int = 8050) -> int:
"""Find the first available port starting from *start*."""
for port in range(start, start + 100):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.bind(("127.0.0.1", port))
return port
except OSError:
continue
raise RuntimeError("No free port found")
def wait_for_server(port: int, timeout: float = 30.0) -> None:
"""Block until the Dash server accepts connections."""
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
if s.connect_ex(("127.0.0.1", port)) == 0:
return
time.sleep(0.1)
raise TimeoutError(f"Server did not start within {timeout}s")
def main() -> None:
port = find_free_port()
server_thread = threading.Thread(
target=app.run,
kwargs={"debug": False, "port": port, "use_reloader": False},
daemon=True,
)
server_thread.start()
wait_for_server(port)
webview.create_window(
"NHS Pathway Analysis",
f"http://127.0.0.1:{port}",
width=1400,
height=900,
)
webview.start()
if __name__ == "__main__":
main()
-859
View File
@@ -1,859 +0,0 @@
# Patient Pathway Analysis - Improvement Recommendations
This document outlines recommended improvements to modernize the Patient Pathway Analysis application, based on multi-domain expert analysis.
---
## Executive Summary
| Area | Current State | Recommended Change | Priority |
|------|--------------|-------------------|----------|
| **GUI Framework** | CustomTkinter | **Reflex** (browser-based, native Plotly) | High |
| **Data Storage** | CSV files (90MB+) | SQLite with caching | High |
| **Data Source** | Manual CSV export | Direct Snowflake connection | Medium |
| **Directory Assignment** | Multi-stage fallback | GP diagnosis codes as primary | Medium |
| **Code Quality** | Monolithic, no types | Modular, typed, tested | Low |
---
## 1. GUI Framework: Replace CustomTkinter with Reflex or Flet
### What
Replace the CustomTkinter-based GUI with a modern Python framework. Two strong options:
- **[Reflex](https://reflex.dev)** - React-based, runs in browser
- **[Flet](https://flet.dev)** - Flutter-based, native desktop or browser
### Why
Since Python is approved and standalone `.exe` distribution isn't required, **both frameworks are viable**.
| Criterion | CustomTkinter | Reflex | Flet |
|-----------|---------------|--------|------|
| UI paradigm | Native desktop | Browser (localhost) | Desktop or browser |
| Component richness | Limited | 60+ React components | Material Design |
| Styling | Manual/limited | Full CSS/Tailwind | Flutter theming |
| Plotly integration | External HTML | **Native embed** | WebView needed |
| State management | Manual | Automatic re-render | Manual updates |
| Learning curve | Low | Moderate (React-like) | Low-moderate |
| Community | Small | 22k+ GitHub stars | 12k+ GitHub stars |
| Maturity | Stable | Active (v0.6+) | Active (v0.80+) |
### Recommendation: **Reflex**
Given that:
1. Python is approved for users
2. Standalone `.exe` not required
3. **Interactive Plotly is required** (Reflex has native `rx.plotly()` component)
Reflex is now the better choice because:
- **Native Plotly support** - no need to open external browser windows
- **Modern React-based UI** - cleaner, more customizable
- **Simpler state management** - automatic re-rendering on state changes
- **Better for data apps** - designed for dashboards and data visualization
### How (Reflex)
**Basic app structure:**
```python
import reflex as rx
class State(rx.State):
"""Application state."""
start_date: str = "2019-04-01"
end_date: str = "2025-04-30"
selected_drugs: list[str] = []
selected_trusts: list[str] = []
analysis_running: bool = False
chart_data: dict = {}
async def run_analysis(self):
self.analysis_running = True
yield # Update UI
# Run analysis (async)
df = await self.load_and_process_data()
self.chart_data = generate_plotly_figure(df)
self.analysis_running = False
def index() -> rx.Component:
return rx.box(
rx.hstack(
# Sidebar with filters
rx.vstack(
rx.date_picker(
value=State.start_date,
on_change=State.set_start_date,
),
rx.checkbox_group(
items=drug_list,
value=State.selected_drugs,
on_change=State.set_selected_drugs,
),
rx.button(
"Run Analysis",
on_click=State.run_analysis,
loading=State.analysis_running,
),
width="300px",
),
# Main content - interactive Plotly chart
rx.plotly(data=State.chart_data, layout=chart_layout),
width="100%",
)
)
app = rx.App()
app.add_page(index)
```
**Key components mapping:**
| Current Component | Reflex Equivalent |
|-------------------|-------------------|
| `CTkFrame` | `rx.box`, `rx.vstack`, `rx.hstack` |
| `CTkButton` | `rx.button` |
| `CTkCheckBox` | `rx.checkbox` |
| `CTkSlider` | `rx.slider` |
| `DateEntry` | `rx.date_picker` |
| `CTkScrollableFrame` | `rx.scroll_area` |
| `filedialog` | `rx.upload` |
| Plotly HTML file | **`rx.plotly()`** - native embed! |
**Running the app:**
```bash
# Install
pip install reflex
# Initialize (first time)
reflex init
# Run development server
reflex run
# Opens http://localhost:3000 in browser
```
**Background tasks with progress:**
```python
class State(rx.State):
progress: int = 0
status: str = ""
async def run_analysis(self):
self.status = "Loading data..."
self.progress = 10
yield
df = load_data()
self.status = "Processing..."
self.progress = 50
yield
result = process_data(df)
self.status = "Complete"
self.progress = 100
yield
```
### Alternative: Flet
If you prefer a more desktop-like feel, Flet remains a good option:
```python
import flet as ft
def main(page: ft.Page):
page.title = "HCD Analysis"
async def run_analysis(e):
# Background task
page.run_task(do_analysis)
page.add(
ft.Row([
# Sidebar
ft.Column([
ft.DatePicker(),
ft.ElevatedButton("Run", on_click=run_analysis),
]),
# Chart area (opens in browser for interactivity)
ft.ElevatedButton("View Chart", on_click=open_chart),
])
)
ft.app(target=main) # Desktop window
# OR
ft.app(target=main, view=ft.WEB_BROWSER) # Browser
```
### Effort Estimate
- Learning Reflex basics: 2-3 days
- Rewriting GUI: 1-2 weeks
- Testing and polish: 3-5 days
---
## 2. Data Storage: SQLite Architecture
### What
Replace CSV-based data loading with a SQLite database that stores reference data in normalized tables and caches processed patient data.
### Why
| Aspect | Current (CSV) | SQLite |
|--------|---------------|--------|
| Startup time | 90MB+ file read + full processing | Load reference data once (< 1MB) |
| Memory usage | Entire dataset in memory | Incremental queries |
| Incremental updates | Full reprocess required | Only process new/changed records |
| Query performance | Pandas groupby/merge | Indexed SQL with CTEs |
| Data consistency | Multiple CSVs can drift | Single source of truth with FK constraints |
| Caching | None | Materialized views |
**Expected improvements:**
- 60-80% faster startup
- 50-70% memory reduction
- 90%+ time savings on incremental updates
### How
**Recommended schema (simplified):**
```sql
-- Reference tables
CREATE TABLE ref_drug_names (
drug_name_raw TEXT PRIMARY KEY,
drug_name_std TEXT NOT NULL
);
CREATE TABLE ref_organizations (
org_code TEXT PRIMARY KEY,
org_name TEXT NOT NULL
);
CREATE TABLE ref_directories (
directory_id INTEGER PRIMARY KEY,
directory_name TEXT UNIQUE NOT NULL
);
CREATE TABLE ref_drug_directory_map (
drug_name_std TEXT,
directory_id INTEGER,
is_single_valid BOOLEAN DEFAULT FALSE,
PRIMARY KEY (drug_name_std, directory_id)
);
-- Patient data (fact table)
CREATE TABLE fact_interventions (
intervention_id INTEGER PRIMARY KEY,
upid TEXT NOT NULL,
provider_code TEXT,
drug_name_std TEXT NOT NULL,
intervention_date DATE NOT NULL,
price_actual REAL,
directory_id INTEGER,
directory_assignment_method TEXT,
data_load_batch_id INTEGER
);
-- Critical indexes
CREATE INDEX idx_upid ON fact_interventions(upid);
CREATE INDEX idx_upid_drug ON fact_interventions(upid, drug_name_std);
CREATE INDEX idx_intervention_date ON fact_interventions(intervention_date);
-- Materialized view for patient summaries (cached aggregations)
CREATE TABLE mv_patient_treatment_summary (
upid TEXT PRIMARY KEY,
first_seen DATE,
last_seen DATE,
total_cost REAL,
drug_count INTEGER,
last_refresh TIMESTAMP
);
-- File tracking for incremental updates
CREATE TABLE processed_files (
file_path TEXT PRIMARY KEY,
file_hash TEXT NOT NULL,
last_processed TIMESTAMP
);
```
**Migration strategy:**
1. **Phase 1**: Create schema, load reference tables from existing CSVs
2. **Phase 2**: Develop incremental load scripts for patient data
3. **Phase 3**: Build materialized views for aggregations
4. **Phase 4**: Modify `dashboard_gui.py` to query SQLite instead of processing CSVs
**Key query replacing pandas aggregation:**
```sql
-- Replaces ~200 lines of pandas groupby/merge
WITH patient_drugs AS (
SELECT
upid,
drug_name_std,
MIN(intervention_date) as first_date,
MAX(intervention_date) as last_date,
COUNT(*) as intervention_count,
SUM(price_actual) as drug_cost
FROM fact_interventions
WHERE intervention_date BETWEEN :start_date AND :end_date
AND provider_code IN (:trust_filters)
GROUP BY upid, drug_name_std
)
SELECT * FROM patient_drugs;
```
### Effort Estimate
- Schema design and setup: 2-3 days
- Migration scripts: 3-4 days
- Query optimization: 2-3 days
- Integration testing: 2-3 days
---
## 3. Snowflake Integration
### What
Enable direct download of HCD activity data from Snowflake servers, replacing manual CSV exports.
### Why
- Eliminates manual export step
- Enables date-range filtering at query level (faster)
- Automatic caching with TTL
- Graceful fallback to local files if Snowflake unavailable
### How
**Authentication: SSO Browser Login**
Using `externalbrowser` authenticator - opens system browser for SSO authentication:
```python
import snowflake.connector
conn = snowflake.connector.connect(
account="your_account.region",
user="your.email@nhs.net",
authenticator="externalbrowser",
warehouse="ANALYTICS_WH",
database="data_hub",
schema="dwh"
)
```
**Note**: User will see browser popup on first connection each session.
**Configuration (`config/snowflake.toml`):**
```toml
[snowflake]
account = "your_account.region"
warehouse = "ANALYTICS_WH"
database = "DataWarehouse"
schema = "dwh"
[query]
default_timeout = 300
chunk_size = 100000
[cache]
enabled = true
ttl_hours = 24
directory = "./data/cache"
```
**Core connector pattern:**
```python
from snowflake.connector import connect
class SnowflakeConnector:
def fetch_activity_data(self, start_date, end_date, provider_codes=None):
query = """
SELECT
"Provider Code",
"PersonKey",
"ProductDescription" as "Drug Name",
"Intervention Date",
"Price Actual",
-- ... other columns
FROM DataWarehouse.dwh.FactHighCostDrugs
WHERE "Intervention Date" BETWEEN :start_date AND :end_date
"""
with self.connect() as conn:
cursor = conn.cursor()
cursor.execute(query, {'start_date': start_date, 'end_date': end_date})
return cursor.fetch_pandas_all()
```
**Caching strategy:**
| Scenario | Action |
|----------|--------|
| Same date range within 24 hours | Use cache |
| Date range includes today | Query Snowflake (data may be updating) |
| User clicks "Refresh" | Query Snowflake |
| Snowflake unavailable | Fallback to local CSV/Parquet |
**Data loader with fallback:**
```python
class DataLoader:
def load_data(self, start_date, end_date, force_refresh=False):
# 1. Try cache
if self.cache and not force_refresh:
cached = self.cache.get(start_date, end_date)
if cached is not None:
return cached, "cache"
# 2. Try Snowflake
try:
df = self.snowflake.fetch_activity_data(start_date, end_date)
self.cache.set(df, start_date, end_date)
return df, "snowflake"
except SnowflakeConnectionError:
pass
# 3. Fallback to local files
if self.fallback_file.exists():
return pd.read_parquet(self.fallback_file), "local_file"
raise RuntimeError("No data source available")
```
**Dependencies to add:**
```toml
dependencies = [
"snowflake-connector-python[pandas]>=3.12.0",
"cryptography>=42.0.0",
]
```
### Effort Estimate
- Snowflake connector setup: 2-3 days
- Caching layer: 1-2 days
- GUI integration (data source selector): 1-2 days
- Testing with real data: 2-3 days
---
## 4. GP Diagnosis Code Integration
### What
Use GP diagnosis codes as the **primary source** for directory/specialty assignment, with existing logic as fallback.
### Why
- More accurate: Diagnosis directly indicates specialty
- Reduces "Undefined" assignments
- Leverages existing NHS data linkage
- Maintains current logic as safety net
### How
**NHS diagnosis code landscape:**
| Code System | Usage | Notes |
|-------------|-------|-------|
| **SNOMED CT** | GP systems (mandatory since 2018) | Primary source |
| **ICD-10** | Secondary care | Maps FROM SNOMED CT |
| **Read Codes** | Legacy only | Historical records |
**New priority chain:**
```
1. Drug has single valid directory → use that (unchanged)
2. [NEW] GP diagnosis available → map SNOMED/ICD-10 to directory
3. Extract from clinical data fields (existing)
4. Most frequent for same patient/drug (existing)
5. UPID-based inference (existing)
6. Default to "Undefined" (existing)
```
**ICD-10 to Directory mapping (examples):**
```python
ICD10_TO_DIRECTORY = {
# Neoplasms (Chapter II)
"C": ["MEDICAL ONCOLOGY", "CLINICAL ONCOLOGY", "CLINICAL HAEMATOLOGY"],
# Blood diseases (Chapter III)
"D5": ["CLINICAL HAEMATOLOGY"],
"D6": ["CLINICAL HAEMATOLOGY"],
# Endocrine (Chapter IV)
"E10": ["DIABETIC MEDICINE"], # Type 1 diabetes
"E11": ["DIABETIC MEDICINE"], # Type 2 diabetes
# Eye (Chapter VII)
"H0": ["OPHTHALMOLOGY"],
"H1": ["OPHTHALMOLOGY"],
"H2": ["OPHTHALMOLOGY"],
"H3": ["OPHTHALMOLOGY"],
# Musculoskeletal (Chapter XIII)
"M05": ["RHEUMATOLOGY"], # Rheumatoid arthritis
"M06": ["RHEUMATOLOGY"],
"M32": ["RHEUMATOLOGY"], # SLE
# Genitourinary (Chapter XIV)
"N0": ["NEPHROLOGY"],
"N1": ["NEPHROLOGY"],
"N18": ["NEPHROLOGY"], # CKD
}
```
**Multi-diagnosis resolution:**
```python
def resolve_directory_from_diagnoses(diagnoses, drug_valid_dirs):
"""
When patient has multiple diagnoses:
1. Filter to diagnoses mapping to directories valid for this drug
2. Oncology diagnoses take priority (ICD-10 chapter C)
3. Use most recent active diagnosis
4. Default to first alphabetically (deterministic)
"""
valid_matches = []
for dx in diagnoses:
icd10_prefix = dx.icd10_code[:3]
possible_dirs = ICD10_TO_DIRECTORY.get(icd10_prefix, [])
matching = set(possible_dirs) & set(drug_valid_dirs)
if matching:
valid_matches.append({
'directories': matching,
'is_oncology': dx.icd10_code.startswith('C'),
'date': dx.diagnosis_date
})
if not valid_matches:
return None # Fall back to existing logic
# Oncology priority
oncology = [m for m in valid_matches if m['is_oncology']]
if oncology:
return sorted(oncology[0]['directories'])[0]
# Most recent
valid_matches.sort(key=lambda x: x['date'], reverse=True)
return sorted(valid_matches[0]['directories'])[0]
```
**Data source options:**
1. **Snowflake linked data** (recommended): Query `data_hub.dwh.DimClinicalCoding` joined via `PatientPseudo`
2. **Local CSV cache**: Pre-extracted GP diagnosis data for offline use
3. **Hybrid**: Cache with Snowflake refresh
**GP Diagnosis Query (confirm column names via Snowflake MCP):**
```sql
SELECT
PatientPseudo,
SNOMEDCode, -- or similar
ICD10Code, -- may need mapping from SNOMED
DiagnosisDate,
DiagnosisStatus -- Active/Resolved if available
FROM data_hub.dwh.DimClinicalCoding
WHERE PatientPseudo IN (:patient_pseudo_list)
ORDER BY DiagnosisDate DESC
```
**New reference file needed (`./data/diagnosis_directory_map.csv`):**
```csv
icd10_prefix,directory,priority,notes
C,MEDICAL ONCOLOGY,1,All malignancies
C81,CLINICAL HAEMATOLOGY,1,Hodgkin lymphoma
C90,CLINICAL HAEMATOLOGY,1,Multiple myeloma
E10,DIABETIC MEDICINE,1,Type 1 diabetes
E11,DIABETIC MEDICINE,1,Type 2 diabetes
G35,NEUROLOGY,1,Multiple sclerosis
H0,OPHTHALMOLOGY,1,Eye disorders
M05,RHEUMATOLOGY,1,Rheumatoid arthritis
N18,NEPHROLOGY,1,Chronic kidney disease
```
**Tracking assignment source (for audit):**
```python
df['Directory_Source'] = pd.NA # New column
# After each assignment step:
df.loc[assigned_mask, 'Directory_Source'] = 'DRUG_SINGLE' # Step 1
df.loc[assigned_mask, 'Directory_Source'] = 'GP_DIAGNOSIS' # Step 2 (NEW)
df.loc[assigned_mask, 'Directory_Source'] = 'CLINICAL_EXTRACT' # Step 3
# ... etc
```
### Prerequisites
- Explore `data_hub.dwh.DimClinicalCoding` schema to confirm exact column names (use Snowflake MCP)
- Map `PatientPseudo` to your HCD data (may need to add PatientPseudo to your data extract)
- Obtain SNOMED CT to ICD-10 mapping table from NHS TRUD (if DimClinicalCoding only has SNOMED)
### Effort Estimate
- Mapping table creation: 2-3 days
- Snowflake GP query development: 2-3 days
- Integration with existing logic: 2-3 days
- Validation and testing: 3-5 days
---
## 5. Code Quality Improvements
### What
Modernize the codebase with better structure, type hints, error handling, and testing.
### Why
- `generate_graph()` is 267 lines with complexity >30
- Zero type hints across entire codebase
- Global variables create hidden state
- No automated tests
- Print statements instead of logging
### How
**Quick wins (implement first):**
1. **Replace global variables** with dataclass:
```python
@dataclass
class AnalysisFilters:
start_date: date
end_date: date
last_seen: date
minimum_patients: int
selected_trusts: list[str]
selected_drugs: list[str]
selected_directories: list[str]
custom_title: str = ""
def validate(self) -> list[str]:
errors = []
if self.start_date >= self.end_date:
errors.append("Start date must be before end date")
return errors
```
2. **Externalize configuration:**
```python
@dataclass
class PathConfig:
data_dir: Path = Path("./data")
@property
def drug_names_file(self) -> Path:
return self.data_dir / "include.csv"
@property
def org_codes_file(self) -> Path:
return self.data_dir / "org_codes.csv"
# ... etc for all 7 reference files
def validate(self) -> list[str]:
"""Check all required files exist at startup."""
errors = []
for file_path in [self.drug_names_file, self.org_codes_file, ...]:
if not file_path.exists():
errors.append(f"Required file not found: {file_path}")
return errors
```
3. **Add logging:**
```python
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("./logs/analysis.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger("PatientPathway")
# Replace all print() with:
logger.info("Starting analysis...")
logger.error(f"Failed to load file: {e}")
```
4. **Extract `generate_graph()` into smaller functions:**
```python
def generate_graph(df, filters: AnalysisFilters, config: PathConfig):
df = prepare_data(df, filters) # ~50 lines
stats = calculate_statistics(df) # ~80 lines
hierarchy = build_hierarchy(df, stats) # ~60 lines
chart_data = prepare_chart_data(hierarchy) # ~40 lines
return render_icicle_chart(chart_data, filters.custom_title) # ~40 lines
```
**Recommended project structure:**
```
project/
├── gui.py # Entry point only
├── core/
│ ├── config.py # PathConfig, AnalysisFilters
│ ├── models.py # Data models
│ └── exceptions.py # Custom exceptions
├── data_processing/
│ ├── loader.py # File/Snowflake loading
│ ├── transformer.py # Data transformations
│ └── validator.py # Data validation
├── analysis/
│ ├── pathway_analyzer.py # Patient pathway calculations
│ └── statistics.py # Statistical calculations
├── visualization/
│ └── plotly_generator.py # Graph generation
└── tests/
├── test_data_processing.py
├── test_analysis.py
└── test_config.py
```
**Add development dependencies:**
```toml
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-cov>=4.1.0",
"mypy>=1.8.0",
"black>=24.0.0",
"ruff>=0.2.0",
]
```
**Priority tests to write:**
```python
# tests/test_data_processing.py
def test_drop_duplicate_treatments_ascending():
"""Verify first intervention kept when ascending=True."""
# ...
def test_drop_duplicate_treatments_descending():
"""Verify last intervention kept when ascending=False."""
# ...
# tests/test_config.py
def test_path_config_validates_missing_files():
"""Verify validation catches missing reference files."""
# ...
def test_analysis_filters_validates_date_range():
"""Verify start date must be before end date."""
# ...
```
### Effort Estimate
- Dataclasses and config: 1-2 days
- Logging setup: 0.5 days
- Extract `generate_graph()`: 2-3 days
- Add type hints (public API): 1-2 days
- Basic test coverage: 2-3 days
---
## Implementation Roadmap
### Phase 1: Foundation (2-3 weeks)
1. Create `PathConfig` and `AnalysisFilters` dataclasses
2. Set up logging infrastructure
3. Design and create SQLite schema
4. Migrate reference data CSVs to SQLite
### Phase 2: Data Layer (2-3 weeks)
1. Implement Snowflake connector with SSO browser auth
2. Build caching layer with TTL
3. Create data loader with fallback chain
4. Migrate `dashboard_gui.py` to use SQLite queries
### Phase 3: Diagnosis Integration (2-3 weeks)
1. Explore `data_hub.dwh.DimClinicalCoding` schema via Snowflake MCP
2. Create ICD-10 to directory mapping table
3. Implement GP diagnosis lookup using `PatientPseudo` linkage
4. Integrate into `department_identification()` as step 2
5. Add `Directory_Source` tracking column
### Phase 4: GUI Modernization (3-4 weeks)
1. Learn Reflex fundamentals
2. Recreate main window and navigation with `rx.vstack`/`rx.hstack`
3. Implement filter panels (date pickers, checkbox groups)
4. Integrate Plotly charts with native `rx.plotly()` component
5. Test with `reflex run`
### Phase 5: Quality & Polish (1-2 weeks)
1. Add type hints to public API
2. Write priority unit tests
3. Extract `generate_graph()` into smaller functions
4. Documentation and cleanup
---
## Configuration Decisions
Based on requirements, the following decisions have been made:
| Question | Decision |
|----------|----------|
| **Snowflake auth** | SSO browser login (`authenticator='externalbrowser'`) |
| **GP diagnosis data** | `data_hub.dwh.DimClinicalCoding` |
| **Patient linkage** | Use `PatientPseudo` (anonymized identifier) - NOT UPID |
| **Plotly interactivity** | Must be interactive - **Reflex has native `rx.plotly()` component** |
| **Distribution** | Python script (`reflex run`) - no .exe needed |
### Implications
**Snowflake SSO**: Connection code becomes:
```python
conn = snowflake.connector.connect(
account="your_account.region",
user=os.environ.get("SNOWFLAKE_USER"),
authenticator="externalbrowser", # Opens browser for SSO
warehouse="ANALYTICS_WH",
database="data_hub",
schema="dwh"
)
```
**Patient Linkage**: The GP diagnosis query needs to join on `PatientPseudo`, not UPID:
```sql
SELECT
cc.PatientPseudo,
cc.SNOMEDCode, -- Confirm actual column names
cc.ICD10Code,
cc.DiagnosisDate
FROM data_hub.dwh.DimClinicalCoding cc
WHERE cc.PatientPseudo IN (:patient_list)
```
**Note**: You'll need to confirm the exact column names in `DimClinicalCoding` - explore via Snowflake MCP or SQL client.
**Plotly Interactivity**: Reflex solves this elegantly with native embedding:
```python
# Interactive Plotly chart directly in the Reflex app
rx.plotly(data=State.chart_data, layout=chart_layout)
```
Full interactivity (zoom, pan, hover tooltips) works in the browser-based app - no external HTML files needed.
---
## References
- [Reflex Documentation](https://reflex.dev/docs/)
- [Reflex Plotly Component](https://reflex.dev/docs/library/graphing/plotly/)
- [Flet Documentation](https://flet.dev/docs/) (alternative)
- [Snowflake Python Connector](https://docs.snowflake.com/en/developer-guide/python-connector/python-connector)
- [NHS SNOMED CT](https://digital.nhs.uk/services/terminology-and-classifications/snomed-ct)
- [NHS ICD-10 Classifications](https://isd.digital.nhs.uk/trud/users/guest/filters/0/categories/28)
-165
View File
@@ -1,165 +0,0 @@
,Search_Term,CleanedDrugName
0,acute coronary syndrome,ABCIXIMAB|CLOPIDOGREL|PRASUGREL|RIVAROXABAN|TICAGRELOR
1,acute lymphoblastic leukaemia,BLINATUMOMAB|DASATINIB|INOTUZUMAB|PEGASPARGASE|PONATINIB|TISAGENLECLEUCEL
2,acute myeloid leukaemia,AZACITIDINE|DECITABINE|GEMTUZUMAB|GILTERITINIB|GLASDEGIB|LIPOSOMAL|MIDOSTAURIN|ORAL|VENETOCLAX
3,acute promyelocytic leukaemia,ARSENIC|GEMTUZUMAB
4,allergic asthma,OMALIZUMAB
5,allergic rhinitis,SQ
6,alzheimer's disease,DONEPEZIL
7,amyloidosis,VUTRISIRAN
8,anaemia,ERYTHROPOIESIS-STIMULATING|ERYTHROPOIETIN
9,anaplastic large cell lymphoma,BRENTUXIMAB
10,ankylosing spondylitis,ADALIMUMAB|GOLIMUMAB|SECUKINUMAB|UPADACITINIB
11,apixaban,ANDEXANET
12,aplastic anaemia,ELTROMBOPAG
13,arthritis,ETANERCEPT
14,asthma,BENRALIZUMAB|DUPILUMAB|INHALED|MEPOLIZUMAB|OMALIZUMAB|RESLIZUMAB
15,atopic dermatitis,ABROCITINIB|ALCLOMETASONE|BARICITINIB|CRISABOROLE|DUPILUMAB|PIMECROLIMUS
16,atrial fibrillation,APIXABAN|DABIGATRAN|DRONEDARONE|EDOXABAN|RIVAROXABAN|VERNAKALANT
17,attention deficit hyperactivity disorder,ATOMOXETINE
18,attention-deficit hyperactivity disorder,METHYLPHENIDATE
19,axial spondyloarthritis,ADALIMUMAB|GOLIMUMAB|IXEKIZUMAB|SECUKINUMAB|UPADACITINIB
20,basal cell carcinoma,VISMODEGIB
21,bipolar disorder,LOXAPINE|OLANZAPINE
22,bladder,MIRABEGRON
23,brca,OLAPARIB
24,breast cancer,ABEMACICLIB|ALPELISIB|ANASTROZOLE|ATEZOLIZUMAB|BEVACIZUMAB|CAPECITABINE|DENOSUMAB|DOCETAXEL|ERIBULIN|EVEROLIMUS|FULVESTRANT|GEMCITABINE|INTRABEAM|LAPATINIB|NERATINIB|OLAPARIB|PACLITAXEL|PALBOCICLI|PALBOCICLIB|PEMBROLIZUMAB|PERTUZUMAB|RIBOCICLIB|SACITUZUMAB|TRASTUZUMAB|TUCATINIB|VINORELBINE
25,cardiomyopathy,TAFAMIDIS
26,cardiovascular disease,ATORVASTATIN
27,cervical cancer,TOPOTECAN
28,cholangiocarcinoma,PEMIGATINIB
29,choroidal neovascularisation,AFLIBERCEPT|RANIBIZUMAB
30,chronic kidney disease,DAPAGLIFLOZIN|IMLIFIDASE|ROXADUSTAT
31,chronic liver disease,AVATROMBOPAG|LUSUTROMBOPAG
32,chronic lymphocytic leukaemia,ACALABRUTINIB|BENDAMUSTINE|DUVELISIB|IBRUTINIB|IDELALISIB|OBINUTUZUMAB|OFATUMUMAB|RITUXIMAB|VENETOCLAX
33,chronic myeloid leukaemia,ASCIMINIB|BOSUTINIB|STANDARD-DOSE|DASATINIB|DASITINIB|NILOTINIB|PONATINIB
34,chronic obstructive pulmonary disease,ROFLUMILAST
35,colon cancer,CAPECITABINE
36,colorectal cancer,BEVACIZUMAB|CAPECITABINE|IRINOTECAN
37,constipation,LUBIPROSTONE|METHYLNALTREXONE|NALDEMEDINE|NALOXEGOL|PRUCALOPRIDE
38,covid-19,NIRMATRELVIR
39,crohn's disease,INFLIXIMAB|VEDOLIZUMAB
40,cutaneous t-cell lymphoma,BRENTUXIMAB|CHLORMETHINE
41,cystic fibrosis,COLISTIMETHATE|LUMACAFTOR|MANNITOL
42,cytomegalovirus,LETERMOVIR|MARIBAVIR
43,deep vein thrombosis,APIXABAN|DABIGATRAN|EDOXABAN|RIVAROXABAN
44,depression,ESKETAMINE
45,diabetes,ERTUGLIFLOZIN|INHALED|AFLIBERCEPT|BROLUCIZUMAB|DEXAMETHASONE|FARICIMAB|FLUOCINOLONE|RANIBIZUMAB
46,diabetic retinopathy,RANIBUZIMAB
47,diffuse large b-cell lymphoma,AXICABTAGENE|POLATUZUMAB|TISAGENLECLEUCEL
48,dravet syndrome,CANNABIDIOL|FENFLURAMINE
49,drug misuse,BUPRENORPHINE|NALTREXONE
50,dry eye,CICLOSPORIN
51,dyspepsia,LANSOPRAZOLE
52,endometrial cancer,DOSTARLIMAB
53,epilepsy,CENOBAMATE|GABAPENTIN|RETIGABINE
54,fallopian tube,BEVACIZUMAB|NIRAPARIB|OLAPARIB|RUCAPARIB
55,follicular lymphoma,DUVELISIB|IDELALISIB|LENALIDOMIDE|OBINUTUZUMAB|RITUXIMAB|TISAGENLECLEUCEL
56,gastric cancer,CAPECITABINE|RAMUCIRUMAB|TRASTUZUMAB|TRIFLURIDINE
57,gastro-oesophageal junction,NIVOLUMAB|PEMBROLIZUMAB
58,giant cell arteritis,TOCILIZUMAB
59,glioma,CARMUSTINE
60,gout,CANAKINUMAB|FEBUXOSTAT|LESINURAD
61,graft versus host disease,RUXOLITINIB
62,granulomatosis with polyangiitis,AVACOPAN|MEPOLIZUMAB
63,growth hormone deficiency,SOMATROPIN
64,hand eczema,ALITRETINOIN
65,heart failure,DAPAGLIFLOZIN|EMPAGLIFLOZIN|IVABRADINE|SACUBITRIL|VERICIGUAT
66,hepatitis b,ADEFOVIR
67,hepatitis c,BOCEPREVIR|DACLATASVIR|ELBASVIR|GLECAPREVIR|INTERFERON|LEDIPASVIR|OMBITASVIR|PEGINTERFERON|PEGYLATED|SIMEPREVIR|SOFOSBUVIR|TELAPREVIR
68,hepatocellular carcinoma,ATEZOLIZUMAB|CABOZANTINIB|LENVATINIB|RAMUCIRUMAB|REGORAFENIB|SELECTIVE|SORAFENIB
69,hiv,CABOTEGRAVIR
70,hodgkin lymphoma,BRENTUXIMAB|NIVOLUMAB|PEMBROLIZUMAB
71,hormone receptor,ABEMACICLIB
72,hypercholesterolaemia,EZETIMIBE
73,hyperparathyroidism,CINACALCET|ETELCALCETIDE
74,immune thrombocytopenia,AVATROMBOPAG|FOSTAMATINIB
75,influenza,AMANTADINE|ZANAMIVIR|BALOXAVIR
76,insomnia,ZALEPLON
77,irritable bowel syndrome,ELUXADOLINE
78,ischaemic stroke,ALTEPLASE
79,juvenile idiopathic arthritis,ABATECEPT|CANAKINUMAB|TOCILIZUMAB|TOFACITINIB
80,kidney transplant,BASILIXIMAB
81,leukaemia,FLUDARABINE|IMATINIB
82,lung cancer,ATEZOLIZUMAB|DURVALUMAB|GEFITINIB|ORAL|NINTEDANIB
83,lymphoma,BENDAMUSTINE|CRIZOTINIB|PIXANTRONE|RITUXIMAB
84,macular degeneration,AFLIBERCEPT|BROLUCIZUMAB|FARICIMAB|RANIBIZUMAB
85,macular oedema,AFLIBERCEPT|RANIBIZUMAB
86,major depressive episodes,AGOMELATINE|VORTIOXETINE
87,malignant melanoma,VEMURAFENIB
88,malignant pleural mesothelioma,NIVOLUMAB|PEMETREXED
89,manic episode,ARIPIPRAZOLE
90,mantle cell lymphoma,AUTOLOGOUS|BORTEZOMIB|IBRUTINIB|LENALIDOMIDE|TEMSIROLIMUS
91,melanoma,COBIMETINIB|DABRAFENIB|ENCORAFENIB|IPILIMUMAB|NIVOLUMAB|PEMBROLIZUMAB|TALIMOGENE|TRAMETINIB
92,merkel cell carcinoma,AVELUMAB
93,migraine,BOTULINUM|EPTINEZUMAB|ERENUMAB|FREMANEZUMAB|GALCANEZUMAB
94,motor neurone disease,RILUZOLE
95,multiple myeloma,BORTEZOMIB|THALIDOMIDE|CARFILZOMIB|DARATUMUMAB|DENOSUMAB|ELOTUZUMAB|ISATUXIMAB|IXAZOMIB|LENALIDOMIDE|PANOBINOSTAT|POMALIDOMIDE|SELINEXOR|TECLISTAMAB
96,multiple sclerosis,ALEMTUZUMAB|BETA|CLADRIBINE|DACLIZUMAB|DIMETHYL|DIROXIMEL|FINGOLIMOD|INTERFERON|NATALIZUMAB|OCRELIZUMAB|OZANIMOD|PEGINTERFERON|PONESIMOD|SIPONIMOD|TERIFLUNOMIDE
97,myelodysplastic,LENALIDOMIDE|LUSPATERCEPT
98,myelofibrosis,FEDRATINIB|RUXOLITINIB
99,myocardial infarction,ALTEPLASE|BIVALIRUDIN|TICAGRELOR
100,myotonia,MEXILETINE
101,narcolepsy,SOLRIAMFETOL
102,neuroendocrine tumour,EVEROLIMUS|LUTETIUM
103,non-small cell lung cancer,ATEZOLIZMAB|DOCETAXEL|ERLOTINIB|PEMETREXED
104,non-small-cell lung cancer,AFATINIB|ALECTINIB|AMIVANTAMAB|ATEZOLIZUMAB|BEVACIZUMAB|BRIGATINIB|CEMIPLIMAB|CERITINIB|CRIZOTINIB|DABRAFENIB|DACOMITINIB|DURVALUMAB|ENTRECTINIB|ERLOTINIB|GEFITINIB|LORLATINIB|MOBOCERTINIB|NECITUMUMAB|NIVOLUMAB|OSIMERTINIB|PACLITAXEL|PEMBROLIZUMAB|PEMETREXED|PRALSETINIB|RAMUCIRUMAB|SELPERCATINIB|SOTORASIB|TEPOTINIB
105,obesity,LIRAGLUTIDE|NALTREXONE|ORLISTAT|SEMAGLUTIDE|SIBUTRAMINE
106,oesophageal cancer,NIVOLUMAB
107,osteoarthritis,CELECOXIB
108,osteoporosis,ALENDRONATE|DENOSUMAB|ORAL|ROMOSOZUMAB
109,osteosarcoma,MIFAMURTIDE
110,ovarian cancer,BEVACIZUMAB|PACLITAXEL|PEGYLATED|TOPOTECAN|TRABECTEDIN
111,overweight,RIMONABANT
112,pancreatic cancer,GEMCITABINE|OLAPARIB|PACLITAXEL|PEGYLATED
113,paroxysmal nocturnal haemoglobinuria,PEGCETACOPLAN|RAVULIZUMAB
114,peripheral arterial disease,NAFTIDROFYRYL
115,plaque psoriasis,ADALIMUMAB|APREMILAST|BIMEKIZUMAB|BRODALUMAB|CERTOLIZUMAB|GUSELKUMAB|INFLIXIMAB|IXEKIZUMAB|RISANKIZUMAB|SECUKINUMAB|TILDRAKIZUAMB|USTEKINUMAB
116,polycystic kidney disease,TOLVAPTAN
117,polycythaemia vera,RUXOLITINIB
118,pregnancy,ROUTINE
119,primary biliary cholangitis,OBETICHOLIC
120,primary hypercholesterolaemia,ALIROCUMAB|EVOLOCUMAB
121,prostate cancer,ABIRATERONE|APALUTAMIDE|CABAZITAXEL|DAROLUTAMIDE|DEGARELIX|DENOSUMAB|DOCETAXEL|ENZALUTAMIDE|OLAPARIB|PADELIPORFIN|RADIUM-|RADIUM|SIPULEUCEL-T
122,psoriasis,EFALUZIMAB
123,psoriatic arthritis,ABATACEPT|ADALIMUMAB|APREMILAST|CERTOLIZUMAB|ETANERCEPT|GOLIMUMAB|GUSELKUMAB|IXEKIZUMAB|RISANKIZUMAB|TOFACITINIB|UPADACITINIB|USTEKINUMAB
124,pulmonary embolism,APIXABAN|DABIGATRAN|EDOXABAN|RIVAROXABAN
125,pulmonary fibrosis,NINTEDANIB|PIRFENIDONE
126,relapsing multiple sclerosis,OFATUMUMAB
127,renal cell carcinoma,AVELUMAB|AXITINIB|BEVACIZUMAB|CABOZANTINIB|EVEROLIMUS|LENVATINIB|NIVOLUMAB|PAZOPANIB|PEMBROLIZUMAB|SUNITINIB|TIVOZANIB
128,renal transplantation,BASILIXIMAB|INDUCTION
129,retinal vein occlusion,AFLIBERCEPT|DEXAMETHASONE|RANIBIZUMAB
130,rheumatoid arthritis,ABATACEPT|ADALIMUMAB|ANAKINRA|BARICITINIB|CELECOXIB|CERTOLIZUMAB|ETANERCEPT|FILGOTINIB|GOLIMUMAB|RITUXIMAB|SARILUMAB|TOCILIZUMAB|TOFACITINIB|UPADACITINIB
131,rivaroxaban,ANDEXANET
132,schizophrenia,AMISULPRIDE|ARIPIPRAZOLE|LOXAPINE
133,seizures,CANNABIDIOL
134,sepsis,DROTRECOGIN
135,severe persistent allergic asthma,OMALIZUMAB
136,short bowel syndrome,TEDUGLUTIDE
137,sickle cell disease,CRIZANLIZUMAB
138,sleep apnoea,PITOLISANT|SOLRIAMFETOL
139,smoking cessation,NICOTINE|VARENICLINE
140,soft tissue sarcoma,INTRAVENOUS|NBTXR-|OLARATUMAB
141,spinal muscular atrophy,NUSINERSEN|RISDIPLAM
142,squamous cell,CETUXIMAB
143,squamous cell carcinoma,CEMIPLIMAB|NIVOLUMAB|PEMBROLIZUMAB
144,stem cell transplant,MELPHALAN|TREOSULFAN
145,stroke,APIXABAN|DABIGATRAN|EDOXABAN|RIVAROXABAN
146,systemic lupus erythematosus,ANIFROLUMAB|ETANERCEPT
147,systemic mastocytosis,MIDOSTAURIN
148,thrombocytopenic purpura,ELTROMBOPAG|ROMIPLOSTIM
149,thrombotic thrombocytopenic purpura,CAPLACIZUMAB
150,thyroid cancer,CABOZANTINIB|LENVATINIB|SELPERCATINIB|VANDETANIB
151,tophaceous gout,PEGLOTICASE
152,transitional cell carcinoma,VINFLUNINE
153,tuberous sclerosis,CANNABIDIOL
154,type 1 diabetes,CONTINUOUS|DAPAGLIFLOZIN|INSULIN|SOTAGLIFLOZIN
155,type 2 diabetes,CANAGLIFLOZIN|CONTINUOUS|DAPAGLIFLOZIN|EMPAGLIFLOZIN|ERTUGLIFLOZIN|EXENATIDE|FINERENONE|INSULIN|LIRAGLUTIDE|PIOGLITAZONE|ROSIGLITAZONE
156,ulcerative colitis,ADALIMUMAB|INFLIXIMAB|FILGOTINIB|OZANIMOD|TOFACITINIB|UPADACITINIB|USTEKINUMAB|VEDOLIZUMAB
157,urothelial carcinoma,ATEZOLIZUMAB|PEMBROLIZUMAB
158,urticaria,OMALIZUMAB
159,uterine fibroids,RELUGOLIX
160,uveitis,ADALIMUMAB|FLUOCINOLONE
161,vascular disease,MODIFIED-RELEASE|CLOPIDOGREL
162,vasculitis,RITUXIMAB
163,venous thromboembolism,APIXABAN|DABIGATRAN|RIVAROXABAN
1 Search_Term CleanedDrugName
2 0 acute coronary syndrome ABCIXIMAB|CLOPIDOGREL|PRASUGREL|RIVAROXABAN|TICAGRELOR
3 1 acute lymphoblastic leukaemia BLINATUMOMAB|DASATINIB|INOTUZUMAB|PEGASPARGASE|PONATINIB|TISAGENLECLEUCEL
4 2 acute myeloid leukaemia AZACITIDINE|DECITABINE|GEMTUZUMAB|GILTERITINIB|GLASDEGIB|LIPOSOMAL|MIDOSTAURIN|ORAL|VENETOCLAX
5 3 acute promyelocytic leukaemia ARSENIC|GEMTUZUMAB
6 4 allergic asthma OMALIZUMAB
7 5 allergic rhinitis SQ
8 6 alzheimer's disease DONEPEZIL
9 7 amyloidosis VUTRISIRAN
10 8 anaemia ERYTHROPOIESIS-STIMULATING|ERYTHROPOIETIN
11 9 anaplastic large cell lymphoma BRENTUXIMAB
12 10 ankylosing spondylitis ADALIMUMAB|GOLIMUMAB|SECUKINUMAB|UPADACITINIB
13 11 apixaban ANDEXANET
14 12 aplastic anaemia ELTROMBOPAG
15 13 arthritis ETANERCEPT
16 14 asthma BENRALIZUMAB|DUPILUMAB|INHALED|MEPOLIZUMAB|OMALIZUMAB|RESLIZUMAB
17 15 atopic dermatitis ABROCITINIB|ALCLOMETASONE|BARICITINIB|CRISABOROLE|DUPILUMAB|PIMECROLIMUS
18 16 atrial fibrillation APIXABAN|DABIGATRAN|DRONEDARONE|EDOXABAN|RIVAROXABAN|VERNAKALANT
19 17 attention deficit hyperactivity disorder ATOMOXETINE
20 18 attention-deficit hyperactivity disorder METHYLPHENIDATE
21 19 axial spondyloarthritis ADALIMUMAB|GOLIMUMAB|IXEKIZUMAB|SECUKINUMAB|UPADACITINIB
22 20 basal cell carcinoma VISMODEGIB
23 21 bipolar disorder LOXAPINE|OLANZAPINE
24 22 bladder MIRABEGRON
25 23 brca OLAPARIB
26 24 breast cancer ABEMACICLIB|ALPELISIB|ANASTROZOLE|ATEZOLIZUMAB|BEVACIZUMAB|CAPECITABINE|DENOSUMAB|DOCETAXEL|ERIBULIN|EVEROLIMUS|FULVESTRANT|GEMCITABINE|INTRABEAM|LAPATINIB|NERATINIB|OLAPARIB|PACLITAXEL|PALBOCICLI|PALBOCICLIB|PEMBROLIZUMAB|PERTUZUMAB|RIBOCICLIB|SACITUZUMAB|TRASTUZUMAB|TUCATINIB|VINORELBINE
27 25 cardiomyopathy TAFAMIDIS
28 26 cardiovascular disease ATORVASTATIN
29 27 cervical cancer TOPOTECAN
30 28 cholangiocarcinoma PEMIGATINIB
31 29 choroidal neovascularisation AFLIBERCEPT|RANIBIZUMAB
32 30 chronic kidney disease DAPAGLIFLOZIN|IMLIFIDASE|ROXADUSTAT
33 31 chronic liver disease AVATROMBOPAG|LUSUTROMBOPAG
34 32 chronic lymphocytic leukaemia ACALABRUTINIB|BENDAMUSTINE|DUVELISIB|IBRUTINIB|IDELALISIB|OBINUTUZUMAB|OFATUMUMAB|RITUXIMAB|VENETOCLAX
35 33 chronic myeloid leukaemia ASCIMINIB|BOSUTINIB|STANDARD-DOSE|DASATINIB|DASITINIB|NILOTINIB|PONATINIB
36 34 chronic obstructive pulmonary disease ROFLUMILAST
37 35 colon cancer CAPECITABINE
38 36 colorectal cancer BEVACIZUMAB|CAPECITABINE|IRINOTECAN
39 37 constipation LUBIPROSTONE|METHYLNALTREXONE|NALDEMEDINE|NALOXEGOL|PRUCALOPRIDE
40 38 covid-19 NIRMATRELVIR
41 39 crohn's disease INFLIXIMAB|VEDOLIZUMAB
42 40 cutaneous t-cell lymphoma BRENTUXIMAB|CHLORMETHINE
43 41 cystic fibrosis COLISTIMETHATE|LUMACAFTOR|MANNITOL
44 42 cytomegalovirus LETERMOVIR|MARIBAVIR
45 43 deep vein thrombosis APIXABAN|DABIGATRAN|EDOXABAN|RIVAROXABAN
46 44 depression ESKETAMINE
47 45 diabetes ERTUGLIFLOZIN|INHALED|AFLIBERCEPT|BROLUCIZUMAB|DEXAMETHASONE|FARICIMAB|FLUOCINOLONE|RANIBIZUMAB
48 46 diabetic retinopathy RANIBUZIMAB
49 47 diffuse large b-cell lymphoma AXICABTAGENE|POLATUZUMAB|TISAGENLECLEUCEL
50 48 dravet syndrome CANNABIDIOL|FENFLURAMINE
51 49 drug misuse BUPRENORPHINE|NALTREXONE
52 50 dry eye CICLOSPORIN
53 51 dyspepsia LANSOPRAZOLE
54 52 endometrial cancer DOSTARLIMAB
55 53 epilepsy CENOBAMATE|GABAPENTIN|RETIGABINE
56 54 fallopian tube BEVACIZUMAB|NIRAPARIB|OLAPARIB|RUCAPARIB
57 55 follicular lymphoma DUVELISIB|IDELALISIB|LENALIDOMIDE|OBINUTUZUMAB|RITUXIMAB|TISAGENLECLEUCEL
58 56 gastric cancer CAPECITABINE|RAMUCIRUMAB|TRASTUZUMAB|TRIFLURIDINE
59 57 gastro-oesophageal junction NIVOLUMAB|PEMBROLIZUMAB
60 58 giant cell arteritis TOCILIZUMAB
61 59 glioma CARMUSTINE
62 60 gout CANAKINUMAB|FEBUXOSTAT|LESINURAD
63 61 graft versus host disease RUXOLITINIB
64 62 granulomatosis with polyangiitis AVACOPAN|MEPOLIZUMAB
65 63 growth hormone deficiency SOMATROPIN
66 64 hand eczema ALITRETINOIN
67 65 heart failure DAPAGLIFLOZIN|EMPAGLIFLOZIN|IVABRADINE|SACUBITRIL|VERICIGUAT
68 66 hepatitis b ADEFOVIR
69 67 hepatitis c BOCEPREVIR|DACLATASVIR|ELBASVIR|GLECAPREVIR|INTERFERON|LEDIPASVIR|OMBITASVIR|PEGINTERFERON|PEGYLATED|SIMEPREVIR|SOFOSBUVIR|TELAPREVIR
70 68 hepatocellular carcinoma ATEZOLIZUMAB|CABOZANTINIB|LENVATINIB|RAMUCIRUMAB|REGORAFENIB|SELECTIVE|SORAFENIB
71 69 hiv CABOTEGRAVIR
72 70 hodgkin lymphoma BRENTUXIMAB|NIVOLUMAB|PEMBROLIZUMAB
73 71 hormone receptor ABEMACICLIB
74 72 hypercholesterolaemia EZETIMIBE
75 73 hyperparathyroidism CINACALCET|ETELCALCETIDE
76 74 immune thrombocytopenia AVATROMBOPAG|FOSTAMATINIB
77 75 influenza AMANTADINE|ZANAMIVIR|BALOXAVIR
78 76 insomnia ZALEPLON
79 77 irritable bowel syndrome ELUXADOLINE
80 78 ischaemic stroke ALTEPLASE
81 79 juvenile idiopathic arthritis ABATECEPT|CANAKINUMAB|TOCILIZUMAB|TOFACITINIB
82 80 kidney transplant BASILIXIMAB
83 81 leukaemia FLUDARABINE|IMATINIB
84 82 lung cancer ATEZOLIZUMAB|DURVALUMAB|GEFITINIB|ORAL|NINTEDANIB
85 83 lymphoma BENDAMUSTINE|CRIZOTINIB|PIXANTRONE|RITUXIMAB
86 84 macular degeneration AFLIBERCEPT|BROLUCIZUMAB|FARICIMAB|RANIBIZUMAB
87 85 macular oedema AFLIBERCEPT|RANIBIZUMAB
88 86 major depressive episodes AGOMELATINE|VORTIOXETINE
89 87 malignant melanoma VEMURAFENIB
90 88 malignant pleural mesothelioma NIVOLUMAB|PEMETREXED
91 89 manic episode ARIPIPRAZOLE
92 90 mantle cell lymphoma AUTOLOGOUS|BORTEZOMIB|IBRUTINIB|LENALIDOMIDE|TEMSIROLIMUS
93 91 melanoma COBIMETINIB|DABRAFENIB|ENCORAFENIB|IPILIMUMAB|NIVOLUMAB|PEMBROLIZUMAB|TALIMOGENE|TRAMETINIB
94 92 merkel cell carcinoma AVELUMAB
95 93 migraine BOTULINUM|EPTINEZUMAB|ERENUMAB|FREMANEZUMAB|GALCANEZUMAB
96 94 motor neurone disease RILUZOLE
97 95 multiple myeloma BORTEZOMIB|THALIDOMIDE|CARFILZOMIB|DARATUMUMAB|DENOSUMAB|ELOTUZUMAB|ISATUXIMAB|IXAZOMIB|LENALIDOMIDE|PANOBINOSTAT|POMALIDOMIDE|SELINEXOR|TECLISTAMAB
98 96 multiple sclerosis ALEMTUZUMAB|BETA|CLADRIBINE|DACLIZUMAB|DIMETHYL|DIROXIMEL|FINGOLIMOD|INTERFERON|NATALIZUMAB|OCRELIZUMAB|OZANIMOD|PEGINTERFERON|PONESIMOD|SIPONIMOD|TERIFLUNOMIDE
99 97 myelodysplastic LENALIDOMIDE|LUSPATERCEPT
100 98 myelofibrosis FEDRATINIB|RUXOLITINIB
101 99 myocardial infarction ALTEPLASE|BIVALIRUDIN|TICAGRELOR
102 100 myotonia MEXILETINE
103 101 narcolepsy SOLRIAMFETOL
104 102 neuroendocrine tumour EVEROLIMUS|LUTETIUM
105 103 non-small cell lung cancer ATEZOLIZMAB|DOCETAXEL|ERLOTINIB|PEMETREXED
106 104 non-small-cell lung cancer AFATINIB|ALECTINIB|AMIVANTAMAB|ATEZOLIZUMAB|BEVACIZUMAB|BRIGATINIB|CEMIPLIMAB|CERITINIB|CRIZOTINIB|DABRAFENIB|DACOMITINIB|DURVALUMAB|ENTRECTINIB|ERLOTINIB|GEFITINIB|LORLATINIB|MOBOCERTINIB|NECITUMUMAB|NIVOLUMAB|OSIMERTINIB|PACLITAXEL|PEMBROLIZUMAB|PEMETREXED|PRALSETINIB|RAMUCIRUMAB|SELPERCATINIB|SOTORASIB|TEPOTINIB
107 105 obesity LIRAGLUTIDE|NALTREXONE|ORLISTAT|SEMAGLUTIDE|SIBUTRAMINE
108 106 oesophageal cancer NIVOLUMAB
109 107 osteoarthritis CELECOXIB
110 108 osteoporosis ALENDRONATE|DENOSUMAB|ORAL|ROMOSOZUMAB
111 109 osteosarcoma MIFAMURTIDE
112 110 ovarian cancer BEVACIZUMAB|PACLITAXEL|PEGYLATED|TOPOTECAN|TRABECTEDIN
113 111 overweight RIMONABANT
114 112 pancreatic cancer GEMCITABINE|OLAPARIB|PACLITAXEL|PEGYLATED
115 113 paroxysmal nocturnal haemoglobinuria PEGCETACOPLAN|RAVULIZUMAB
116 114 peripheral arterial disease NAFTIDROFYRYL
117 115 plaque psoriasis ADALIMUMAB|APREMILAST|BIMEKIZUMAB|BRODALUMAB|CERTOLIZUMAB|GUSELKUMAB|INFLIXIMAB|IXEKIZUMAB|RISANKIZUMAB|SECUKINUMAB|TILDRAKIZUAMB|USTEKINUMAB
118 116 polycystic kidney disease TOLVAPTAN
119 117 polycythaemia vera RUXOLITINIB
120 118 pregnancy ROUTINE
121 119 primary biliary cholangitis OBETICHOLIC
122 120 primary hypercholesterolaemia ALIROCUMAB|EVOLOCUMAB
123 121 prostate cancer ABIRATERONE|APALUTAMIDE|CABAZITAXEL|DAROLUTAMIDE|DEGARELIX|DENOSUMAB|DOCETAXEL|ENZALUTAMIDE|OLAPARIB|PADELIPORFIN|RADIUM-|RADIUM|SIPULEUCEL-T
124 122 psoriasis EFALUZIMAB
125 123 psoriatic arthritis ABATACEPT|ADALIMUMAB|APREMILAST|CERTOLIZUMAB|ETANERCEPT|GOLIMUMAB|GUSELKUMAB|IXEKIZUMAB|RISANKIZUMAB|TOFACITINIB|UPADACITINIB|USTEKINUMAB
126 124 pulmonary embolism APIXABAN|DABIGATRAN|EDOXABAN|RIVAROXABAN
127 125 pulmonary fibrosis NINTEDANIB|PIRFENIDONE
128 126 relapsing multiple sclerosis OFATUMUMAB
129 127 renal cell carcinoma AVELUMAB|AXITINIB|BEVACIZUMAB|CABOZANTINIB|EVEROLIMUS|LENVATINIB|NIVOLUMAB|PAZOPANIB|PEMBROLIZUMAB|SUNITINIB|TIVOZANIB
130 128 renal transplantation BASILIXIMAB|INDUCTION
131 129 retinal vein occlusion AFLIBERCEPT|DEXAMETHASONE|RANIBIZUMAB
132 130 rheumatoid arthritis ABATACEPT|ADALIMUMAB|ANAKINRA|BARICITINIB|CELECOXIB|CERTOLIZUMAB|ETANERCEPT|FILGOTINIB|GOLIMUMAB|RITUXIMAB|SARILUMAB|TOCILIZUMAB|TOFACITINIB|UPADACITINIB
133 131 rivaroxaban ANDEXANET
134 132 schizophrenia AMISULPRIDE|ARIPIPRAZOLE|LOXAPINE
135 133 seizures CANNABIDIOL
136 134 sepsis DROTRECOGIN
137 135 severe persistent allergic asthma OMALIZUMAB
138 136 short bowel syndrome TEDUGLUTIDE
139 137 sickle cell disease CRIZANLIZUMAB
140 138 sleep apnoea PITOLISANT|SOLRIAMFETOL
141 139 smoking cessation NICOTINE|VARENICLINE
142 140 soft tissue sarcoma INTRAVENOUS|NBTXR-|OLARATUMAB
143 141 spinal muscular atrophy NUSINERSEN|RISDIPLAM
144 142 squamous cell CETUXIMAB
145 143 squamous cell carcinoma CEMIPLIMAB|NIVOLUMAB|PEMBROLIZUMAB
146 144 stem cell transplant MELPHALAN|TREOSULFAN
147 145 stroke APIXABAN|DABIGATRAN|EDOXABAN|RIVAROXABAN
148 146 systemic lupus erythematosus ANIFROLUMAB|ETANERCEPT
149 147 systemic mastocytosis MIDOSTAURIN
150 148 thrombocytopenic purpura ELTROMBOPAG|ROMIPLOSTIM
151 149 thrombotic thrombocytopenic purpura CAPLACIZUMAB
152 150 thyroid cancer CABOZANTINIB|LENVATINIB|SELPERCATINIB|VANDETANIB
153 151 tophaceous gout PEGLOTICASE
154 152 transitional cell carcinoma VINFLUNINE
155 153 tuberous sclerosis CANNABIDIOL
156 154 type 1 diabetes CONTINUOUS|DAPAGLIFLOZIN|INSULIN|SOTAGLIFLOZIN
157 155 type 2 diabetes CANAGLIFLOZIN|CONTINUOUS|DAPAGLIFLOZIN|EMPAGLIFLOZIN|ERTUGLIFLOZIN|EXENATIDE|FINERENONE|INSULIN|LIRAGLUTIDE|PIOGLITAZONE|ROSIGLITAZONE
158 156 ulcerative colitis ADALIMUMAB|INFLIXIMAB|FILGOTINIB|OZANIMOD|TOFACITINIB|UPADACITINIB|USTEKINUMAB|VEDOLIZUMAB
159 157 urothelial carcinoma ATEZOLIZUMAB|PEMBROLIZUMAB
160 158 urticaria OMALIZUMAB
161 159 uterine fibroids RELUGOLIX
162 160 uveitis ADALIMUMAB|FLUOCINOLONE
163 161 vascular disease MODIFIED-RELEASE|CLOPIDOGREL
164 162 vasculitis RITUXIMAB
165 163 venous thromboembolism APIXABAN|DABIGATRAN|RIVAROXABAN
-231
View File
@@ -1,231 +0,0 @@
Search_Term,PrimaryDirectorate,AllDirectorates
acute coronary syndrome,CARDIOLOGY,CARDIOLOGY
acute coronary syndromes,CARDIOLOGY,CARDIOLOGY
acute lymphoblastic leukaemia,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|PAEDIATRICS
acute myeloid leukaemia,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|MEDICAL ONCOLOGY
acute promyelocytic leukaemia,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY
advanced breast cancer,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|BREAST SURGERY|CLINICAL ONCOLOGY
allergic asthma,THORACIC MEDICINE,THORACIC MEDICINE|CLINICAL IMMUNOLOGY
allergic rhinitis,ENT,ENT|CLINICAL IMMUNOLOGY
alzheimer's disease,NEUROLOGY,NEUROLOGY|GERIATRIC MEDICINE|MENTAL HEALTH
amyloidosis,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|CARDIOLOGY|NEPHROLOGY
anaemia,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|GENERAL MEDICINE
anaplastic large cell lymphoma,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|MEDICAL ONCOLOGY
angioedema,CLINICAL IMMUNOLOGY,CLINICAL IMMUNOLOGY|ACCIDENT & EMERGENCY
ankylosing spondylitis,RHEUMATOLOGY,RHEUMATOLOGY
apixaban,CARDIOLOGY,CARDIOLOGY|CLINICAL HAEMATOLOGY
aplastic anaemia,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY
arthritis,RHEUMATOLOGY,RHEUMATOLOGY
asthma,THORACIC MEDICINE,THORACIC MEDICINE|PAEDIATRICS
atopic dermatitis,DERMATOLOGY,DERMATOLOGY|PAEDIATRICS|CLINICAL IMMUNOLOGY
atrial fibrillation,CARDIOLOGY,CARDIOLOGY
attention deficit hyperactivity disorder,MENTAL HEALTH,MENTAL HEALTH|PAEDIATRICS
attention-deficit hyperactivity disorder,MENTAL HEALTH,MENTAL HEALTH|PAEDIATRICS
axial spondyloarthritis,RHEUMATOLOGY,RHEUMATOLOGY
basal cell carcinoma,DERMATOLOGY,DERMATOLOGY|PLASTIC SURGERY|MEDICAL ONCOLOGY
beta-thalassaemia,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|PAEDIATRICS
biliary cholangitis,GASTROENTEROLOGY,GASTROENTEROLOGY
bipolar disorder,MENTAL HEALTH,MENTAL HEALTH
bladder,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|UROLOGY|CLINICAL ONCOLOGY
braf,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|CLINICAL ONCOLOGY
brca,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|GYNAECOLOGICAL ONCOLOGY|BREAST SURGERY
breast cancer,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|BREAST SURGERY|CLINICAL ONCOLOGY
cardiomyopathy,CARDIOLOGY,CARDIOLOGY
cardiovascular disease,CARDIOLOGY,CARDIOLOGY|VASCULAR SURGERY
cervical cancer,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|GYNAECOLOGICAL ONCOLOGY|CLINICAL ONCOLOGY
cholangiocarcinoma,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|GASTROENTEROLOGY|CLINICAL ONCOLOGY
choroidal neovascularisation,OPHTHALMOLOGY,OPHTHALMOLOGY|MEDICAL OPHTHALMOLOGY
chronic hepatitis b,GASTROENTEROLOGY,GASTROENTEROLOGY|INFECTIOUS DISEASES
chronic kidney disease,NEPHROLOGY,NEPHROLOGY
chronic liver disease,GASTROENTEROLOGY,GASTROENTEROLOGY
chronic lymphocytic leukaemia,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY
chronic myeloid leukaemia,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY
chronic obstructive pulmonary disease,THORACIC MEDICINE,THORACIC MEDICINE
colon cancer,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|COLORECTAL SURGERY|CLINICAL ONCOLOGY
colorectal cancer,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|COLORECTAL SURGERY|CLINICAL ONCOLOGY
constipation,GASTROENTEROLOGY,GASTROENTEROLOGY|GENERAL MEDICINE
coronary syndrome,CARDIOLOGY,CARDIOLOGY
covid,INFECTIOUS DISEASES,INFECTIOUS DISEASES|THORACIC MEDICINE
covid-19,INFECTIOUS DISEASES,INFECTIOUS DISEASES|THORACIC MEDICINE
crohn's disease,GASTROENTEROLOGY,GASTROENTEROLOGY|PAEDIATRIC GASTROENTEROLOGY|COLORECTAL SURGERY
cutaneous t-cell lymphoma,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|DERMATOLOGY
cystic fibrosis,THORACIC MEDICINE,THORACIC MEDICINE|PAEDIATRICS|GASTROENTEROLOGY
cytomegalovirus,INFECTIOUS DISEASES,INFECTIOUS DISEASES|TRANSPLANTATION SURGERY
deep vein thrombosis,VASCULAR SURGERY,VASCULAR SURGERY|CLINICAL HAEMATOLOGY
depression,MENTAL HEALTH,MENTAL HEALTH
depressive episode,MENTAL HEALTH,MENTAL HEALTH
diabetes,DIABETIC MEDICINE,DIABETIC MEDICINE|ENDOCRINOLOGY
diabetic macular,OPHTHALMOLOGY,OPHTHALMOLOGY|MEDICAL OPHTHALMOLOGY
diabetic macular oedema,OPHTHALMOLOGY,OPHTHALMOLOGY|MEDICAL OPHTHALMOLOGY
diabetic retinopathy,OPHTHALMOLOGY,OPHTHALMOLOGY|MEDICAL OPHTHALMOLOGY|DIABETIC MEDICINE
diffuse large b-cell lymphoma,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|MEDICAL ONCOLOGY
dravet syndrome,NEUROLOGY,NEUROLOGY|PAEDIATRICS
drug misuse,MENTAL HEALTH,MENTAL HEALTH|ADDICTION MEDICINE
dry eye,OPHTHALMOLOGY,OPHTHALMOLOGY
dupuytren's contracture,TRAUMA & ORTHOPAEDICS,TRAUMA & ORTHOPAEDICS|PLASTIC SURGERY
dyslipidaemia,CARDIOLOGY,CARDIOLOGY|ENDOCRINOLOGY
dyspepsia,GASTROENTEROLOGY,GASTROENTEROLOGY|GENERAL MEDICINE
eczema,DERMATOLOGY,DERMATOLOGY|PAEDIATRICS
endometrial cancer,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|GYNAECOLOGICAL ONCOLOGY|CLINICAL ONCOLOGY
epilepsy,NEUROLOGY,NEUROLOGY|PAEDIATRICS
fallopian tube,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|GYNAECOLOGICAL ONCOLOGY|CLINICAL ONCOLOGY
fibroids,GYNAECOLOGY,GYNAECOLOGY
follicular lymphoma,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|MEDICAL ONCOLOGY
fragility fracture,RHEUMATOLOGY,RHEUMATOLOGY|TRAUMA & ORTHOPAEDICS|GERIATRIC MEDICINE
gastric cancer,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|UPPER GASTROINTESTINAL SURGERY|CLINICAL ONCOLOGY
gastro-oesophageal,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|UPPER GASTROINTESTINAL SURGERY|CLINICAL ONCOLOGY
gastro-oesophageal junction,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|UPPER GASTROINTESTINAL SURGERY|CLINICAL ONCOLOGY
gastrointestinal stromal tumour,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|UPPER GASTROINTESTINAL SURGERY|CLINICAL ONCOLOGY
gastrointestinal stromal tumours,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|UPPER GASTROINTESTINAL SURGERY|CLINICAL ONCOLOGY
giant cell arteritis,RHEUMATOLOGY,RHEUMATOLOGY
glioma,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|NEUROSURGERY|CLINICAL ONCOLOGY
gout,RHEUMATOLOGY,RHEUMATOLOGY
graft versus host disease,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|TRANSPLANTATION SURGERY
granulomatosis with polyangiitis,RHEUMATOLOGY,RHEUMATOLOGY|THORACIC MEDICINE|NEPHROLOGY
growth failure,ENDOCRINOLOGY,ENDOCRINOLOGY|PAEDIATRICS
growth hormone deficiency,ENDOCRINOLOGY,ENDOCRINOLOGY|PAEDIATRICS
haemoglobinuria,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY
hand eczema,DERMATOLOGY,DERMATOLOGY
head and neck,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|ENT|CLINICAL ONCOLOGY
heart failure,CARDIOLOGY,CARDIOLOGY
hepatic encephalopathy,GASTROENTEROLOGY,GASTROENTEROLOGY
hepatitis b,GASTROENTEROLOGY,GASTROENTEROLOGY|INFECTIOUS DISEASES
hepatitis c,GASTROENTEROLOGY,GASTROENTEROLOGY|INFECTIOUS DISEASES
hepatocellular carcinoma,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|GASTROENTEROLOGY|CLINICAL ONCOLOGY
her2,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|BREAST SURGERY|CLINICAL ONCOLOGY
her2-positive,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|BREAST SURGERY|CLINICAL ONCOLOGY
hereditary angioedema,CLINICAL IMMUNOLOGY,CLINICAL IMMUNOLOGY
hidradenitis suppurativa,DERMATOLOGY,DERMATOLOGY
hiv,INFECTIOUS DISEASES,INFECTIOUS DISEASES
hodgkin lymphoma,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|MEDICAL ONCOLOGY
hormone receptor,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|BREAST SURGERY|CLINICAL ONCOLOGY
hypercholesterolaemia,CARDIOLOGY,CARDIOLOGY|ENDOCRINOLOGY|CHEMICAL PATHOLOGY
hyperparathyroidism,ENDOCRINOLOGY,ENDOCRINOLOGY
hyperuricaemia,RHEUMATOLOGY,RHEUMATOLOGY
immune thrombocytopenia,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY
influenza,INFECTIOUS DISEASES,INFECTIOUS DISEASES|GENERAL MEDICINE
insomnia,NEUROLOGY,NEUROLOGY|MENTAL HEALTH
interstitial lung disease,THORACIC MEDICINE,THORACIC MEDICINE
irritable bowel syndrome,GASTROENTEROLOGY,GASTROENTEROLOGY
ischaemic stroke,STROKE MEDICINE,STROKE MEDICINE|NEUROLOGY
juvenile idiopathic arthritis,RHEUMATOLOGY,RHEUMATOLOGY|PAEDIATRICS
keratitis,OPHTHALMOLOGY,OPHTHALMOLOGY
kidney disease,NEPHROLOGY,NEPHROLOGY
kidney transplant,NEPHROLOGY,NEPHROLOGY|TRANSPLANTATION SURGERY
large b-cell lymphoma,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|MEDICAL ONCOLOGY
leukaemia,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|MEDICAL ONCOLOGY
limbal stem cell deficiency,OPHTHALMOLOGY,OPHTHALMOLOGY
liver disease,GASTROENTEROLOGY,GASTROENTEROLOGY
lung cancer,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|THORACIC MEDICINE|CLINICAL ONCOLOGY
lymphoblastic leukaemia,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|PAEDIATRICS
lymphocytic leukaemia,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY
lymphoma,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|MEDICAL ONCOLOGY
macular degeneration,OPHTHALMOLOGY,OPHTHALMOLOGY|MEDICAL OPHTHALMOLOGY
macular oedema,OPHTHALMOLOGY,OPHTHALMOLOGY|MEDICAL OPHTHALMOLOGY
major depressive episodes,MENTAL HEALTH,MENTAL HEALTH
malignant melanoma,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|DERMATOLOGY|CLINICAL ONCOLOGY
malignant pleural mesothelioma,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|THORACIC MEDICINE|CLINICAL ONCOLOGY
manic episode,MENTAL HEALTH,MENTAL HEALTH
mantle cell lymphoma,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|MEDICAL ONCOLOGY
mastocytosis,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|CLINICAL IMMUNOLOGY
melanoma,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|DERMATOLOGY|CLINICAL ONCOLOGY
merkel cell,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|DERMATOLOGY|CLINICAL ONCOLOGY
merkel cell carcinoma,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|DERMATOLOGY|CLINICAL ONCOLOGY
mesothelioma,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|THORACIC MEDICINE|CLINICAL ONCOLOGY
metastatic colorectal cancer,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|COLORECTAL SURGERY|CLINICAL ONCOLOGY
migraine,NEUROLOGY,NEUROLOGY
motor neurone disease,NEUROLOGY,NEUROLOGY|REHABILITATION|PALLIATIVE CARE
multiple myeloma,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|MEDICAL ONCOLOGY
multiple sclerosis,NEUROLOGY,NEUROLOGY|REHABILITATION
myelodysplastic,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY
myelodysplastic syndromes,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY
myelofibrosis,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY
myeloid leukaemia,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY
myocardial infarction,CARDIOLOGY,CARDIOLOGY
myotonia,NEUROLOGY,NEUROLOGY
narcolepsy,NEUROLOGY,NEUROLOGY
nasal polyps,ENT,ENT|THORACIC MEDICINE|CLINICAL IMMUNOLOGY
neuroblastoma,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|PAEDIATRICS|CLINICAL ONCOLOGY
neuroendocrine tumour,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|ENDOCRINOLOGY|CLINICAL ONCOLOGY
non-small cell lung cancer,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|THORACIC MEDICINE|CLINICAL ONCOLOGY
non-small-cell lung cancer,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|THORACIC MEDICINE|CLINICAL ONCOLOGY
obesity,ENDOCRINOLOGY,ENDOCRINOLOGY|DIABETIC MEDICINE|GENERAL MEDICINE
oesophageal cancer,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|UPPER GASTROINTESTINAL SURGERY|CLINICAL ONCOLOGY
osteoarthritis,RHEUMATOLOGY,RHEUMATOLOGY|TRAUMA & ORTHOPAEDICS|GERIATRIC MEDICINE
osteoporosis,RHEUMATOLOGY,RHEUMATOLOGY|ENDOCRINOLOGY|GERIATRIC MEDICINE
osteosarcoma,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|TRAUMA & ORTHOPAEDICS|CLINICAL ONCOLOGY
ovarian cancer,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|GYNAECOLOGICAL ONCOLOGY|CLINICAL ONCOLOGY
overweight,ENDOCRINOLOGY,ENDOCRINOLOGY|DIABETIC MEDICINE
pancreatic cancer,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|UPPER GASTROINTESTINAL SURGERY|CLINICAL ONCOLOGY
pancreatic neuroendocrine,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|ENDOCRINOLOGY|CLINICAL ONCOLOGY
paroxysmal nocturnal haemoglobinuria,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY
peanut allergy,CLINICAL IMMUNOLOGY,CLINICAL IMMUNOLOGY|PAEDIATRICS
perianal fistula,GASTROENTEROLOGY,GASTROENTEROLOGY|COLORECTAL SURGERY
peripheral arterial disease,VASCULAR SURGERY,VASCULAR SURGERY|CARDIOLOGY
plaque psoriasis,DERMATOLOGY,DERMATOLOGY
polycystic kidney,NEPHROLOGY,NEPHROLOGY
polycystic kidney disease,NEPHROLOGY,NEPHROLOGY
polycythaemia,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY
polycythaemia vera,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY
pouchitis,GASTROENTEROLOGY,GASTROENTEROLOGY|COLORECTAL SURGERY
pregnancy,OBSTETRICS,OBSTETRICS
primary biliary cholangitis,GASTROENTEROLOGY,GASTROENTEROLOGY
primary hypercholesterolaemia,CARDIOLOGY,CARDIOLOGY|ENDOCRINOLOGY|CHEMICAL PATHOLOGY
promyelocytic leukaemia,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY
prostate cancer,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|UROLOGY|CLINICAL ONCOLOGY
psoriasis,DERMATOLOGY,DERMATOLOGY
psoriatic arthritis,RHEUMATOLOGY,RHEUMATOLOGY|DERMATOLOGY
pulmonary embolism,THORACIC MEDICINE,THORACIC MEDICINE|CARDIOLOGY|CLINICAL HAEMATOLOGY
pulmonary fibrosis,THORACIC MEDICINE,THORACIC MEDICINE
relapsing multiple sclerosis,NEUROLOGY,NEUROLOGY|REHABILITATION
renal cell,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|UROLOGY|CLINICAL ONCOLOGY
renal cell carcinoma,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|UROLOGY|CLINICAL ONCOLOGY
renal transplantation,NEPHROLOGY,NEPHROLOGY|TRANSPLANTATION SURGERY
retinal vein occlusion,OPHTHALMOLOGY,OPHTHALMOLOGY|MEDICAL OPHTHALMOLOGY
rheumatoid arthritis,RHEUMATOLOGY,RHEUMATOLOGY|CLINICAL IMMUNOLOGY|GERIATRIC MEDICINE
rhinosinusitis with nasal polyps,ENT,ENT|THORACIC MEDICINE|CLINICAL IMMUNOLOGY
rivaroxaban,CARDIOLOGY,CARDIOLOGY|CLINICAL HAEMATOLOGY
schizophrenia,MENTAL HEALTH,MENTAL HEALTH
seizures,NEUROLOGY,NEUROLOGY|PAEDIATRICS
sepsis,INFECTIOUS DISEASES,INFECTIOUS DISEASES|CRITICAL CARE MEDICINE
severe persistent allergic asthma,THORACIC MEDICINE,THORACIC MEDICINE|CLINICAL IMMUNOLOGY
short bowel syndrome,GASTROENTEROLOGY,GASTROENTEROLOGY|COLORECTAL SURGERY
sickle cell,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|PAEDIATRICS
sickle cell disease,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|PAEDIATRICS
sleep apnoea,THORACIC MEDICINE,THORACIC MEDICINE|ENT
smoking cessation,THORACIC MEDICINE,THORACIC MEDICINE|GENERAL MEDICINE
soft tissue sarcoma,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|CLINICAL ONCOLOGY
spinal muscular atrophy,NEUROLOGY,NEUROLOGY|PAEDIATRICS
splenomegaly,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|GASTROENTEROLOGY
spondyloarthritis,RHEUMATOLOGY,RHEUMATOLOGY
squamous cell,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|DERMATOLOGY|ENT|CLINICAL ONCOLOGY
squamous cell carcinoma,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|DERMATOLOGY|ENT|CLINICAL ONCOLOGY
stem cell transplant,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|TRANSPLANTATION SURGERY
stroke,STROKE MEDICINE,STROKE MEDICINE|NEUROLOGY
systemic lupus erythematosus,RHEUMATOLOGY,RHEUMATOLOGY|CLINICAL IMMUNOLOGY|NEPHROLOGY
systemic mastocytosis,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|CLINICAL IMMUNOLOGY
t-cell lymphoma,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|MEDICAL ONCOLOGY
thalassaemia,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|PAEDIATRICS
thrombocytopenia,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY
thrombocytopenic purpura,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY
thromboembolism,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|CARDIOLOGY
thrombotic thrombocytopenic purpura,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY
thyroid cancer,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|ENDOCRINOLOGY|CLINICAL ONCOLOGY
tophaceous gout,RHEUMATOLOGY,RHEUMATOLOGY
transitional cell carcinoma,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|UROLOGY|CLINICAL ONCOLOGY
transthyretin amyloidosis,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|CARDIOLOGY|NEUROLOGY
triple-negative,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|BREAST SURGERY|CLINICAL ONCOLOGY
tuberous sclerosis,NEUROLOGY,NEUROLOGY|PAEDIATRICS
type 1 diabetes,DIABETIC MEDICINE,DIABETIC MEDICINE|ENDOCRINOLOGY|PAEDIATRICS
type 2 diabetes,DIABETIC MEDICINE,DIABETIC MEDICINE|ENDOCRINOLOGY
ulcerative colitis,GASTROENTEROLOGY,GASTROENTEROLOGY|COLORECTAL SURGERY
urothelial,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|UROLOGY|CLINICAL ONCOLOGY
urothelial cancer,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|UROLOGY|CLINICAL ONCOLOGY
urothelial carcinoma,MEDICAL ONCOLOGY,MEDICAL ONCOLOGY|UROLOGY|CLINICAL ONCOLOGY
urticaria,DERMATOLOGY,DERMATOLOGY|CLINICAL IMMUNOLOGY
uterine fibroids,GYNAECOLOGY,GYNAECOLOGY
uveitis,OPHTHALMOLOGY,OPHTHALMOLOGY|MEDICAL OPHTHALMOLOGY|RHEUMATOLOGY
vascular disease,VASCULAR SURGERY,VASCULAR SURGERY|CARDIOLOGY
vasculitis,RHEUMATOLOGY,RHEUMATOLOGY|CLINICAL IMMUNOLOGY
venom allergy,CLINICAL IMMUNOLOGY,CLINICAL IMMUNOLOGY
venous thromboembolism,CLINICAL HAEMATOLOGY,CLINICAL HAEMATOLOGY|VASCULAR SURGERY
1 Search_Term PrimaryDirectorate AllDirectorates
2 acute coronary syndrome CARDIOLOGY CARDIOLOGY
3 acute coronary syndromes CARDIOLOGY CARDIOLOGY
4 acute lymphoblastic leukaemia CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|PAEDIATRICS
5 acute myeloid leukaemia CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|MEDICAL ONCOLOGY
6 acute promyelocytic leukaemia CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY
7 advanced breast cancer MEDICAL ONCOLOGY MEDICAL ONCOLOGY|BREAST SURGERY|CLINICAL ONCOLOGY
8 allergic asthma THORACIC MEDICINE THORACIC MEDICINE|CLINICAL IMMUNOLOGY
9 allergic rhinitis ENT ENT|CLINICAL IMMUNOLOGY
10 alzheimer's disease NEUROLOGY NEUROLOGY|GERIATRIC MEDICINE|MENTAL HEALTH
11 amyloidosis CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|CARDIOLOGY|NEPHROLOGY
12 anaemia CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|GENERAL MEDICINE
13 anaplastic large cell lymphoma CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|MEDICAL ONCOLOGY
14 angioedema CLINICAL IMMUNOLOGY CLINICAL IMMUNOLOGY|ACCIDENT & EMERGENCY
15 ankylosing spondylitis RHEUMATOLOGY RHEUMATOLOGY
16 apixaban CARDIOLOGY CARDIOLOGY|CLINICAL HAEMATOLOGY
17 aplastic anaemia CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY
18 arthritis RHEUMATOLOGY RHEUMATOLOGY
19 asthma THORACIC MEDICINE THORACIC MEDICINE|PAEDIATRICS
20 atopic dermatitis DERMATOLOGY DERMATOLOGY|PAEDIATRICS|CLINICAL IMMUNOLOGY
21 atrial fibrillation CARDIOLOGY CARDIOLOGY
22 attention deficit hyperactivity disorder MENTAL HEALTH MENTAL HEALTH|PAEDIATRICS
23 attention-deficit hyperactivity disorder MENTAL HEALTH MENTAL HEALTH|PAEDIATRICS
24 axial spondyloarthritis RHEUMATOLOGY RHEUMATOLOGY
25 basal cell carcinoma DERMATOLOGY DERMATOLOGY|PLASTIC SURGERY|MEDICAL ONCOLOGY
26 beta-thalassaemia CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|PAEDIATRICS
27 biliary cholangitis GASTROENTEROLOGY GASTROENTEROLOGY
28 bipolar disorder MENTAL HEALTH MENTAL HEALTH
29 bladder MEDICAL ONCOLOGY MEDICAL ONCOLOGY|UROLOGY|CLINICAL ONCOLOGY
30 braf MEDICAL ONCOLOGY MEDICAL ONCOLOGY|CLINICAL ONCOLOGY
31 brca MEDICAL ONCOLOGY MEDICAL ONCOLOGY|GYNAECOLOGICAL ONCOLOGY|BREAST SURGERY
32 breast cancer MEDICAL ONCOLOGY MEDICAL ONCOLOGY|BREAST SURGERY|CLINICAL ONCOLOGY
33 cardiomyopathy CARDIOLOGY CARDIOLOGY
34 cardiovascular disease CARDIOLOGY CARDIOLOGY|VASCULAR SURGERY
35 cervical cancer MEDICAL ONCOLOGY MEDICAL ONCOLOGY|GYNAECOLOGICAL ONCOLOGY|CLINICAL ONCOLOGY
36 cholangiocarcinoma MEDICAL ONCOLOGY MEDICAL ONCOLOGY|GASTROENTEROLOGY|CLINICAL ONCOLOGY
37 choroidal neovascularisation OPHTHALMOLOGY OPHTHALMOLOGY|MEDICAL OPHTHALMOLOGY
38 chronic hepatitis b GASTROENTEROLOGY GASTROENTEROLOGY|INFECTIOUS DISEASES
39 chronic kidney disease NEPHROLOGY NEPHROLOGY
40 chronic liver disease GASTROENTEROLOGY GASTROENTEROLOGY
41 chronic lymphocytic leukaemia CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY
42 chronic myeloid leukaemia CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY
43 chronic obstructive pulmonary disease THORACIC MEDICINE THORACIC MEDICINE
44 colon cancer MEDICAL ONCOLOGY MEDICAL ONCOLOGY|COLORECTAL SURGERY|CLINICAL ONCOLOGY
45 colorectal cancer MEDICAL ONCOLOGY MEDICAL ONCOLOGY|COLORECTAL SURGERY|CLINICAL ONCOLOGY
46 constipation GASTROENTEROLOGY GASTROENTEROLOGY|GENERAL MEDICINE
47 coronary syndrome CARDIOLOGY CARDIOLOGY
48 covid INFECTIOUS DISEASES INFECTIOUS DISEASES|THORACIC MEDICINE
49 covid-19 INFECTIOUS DISEASES INFECTIOUS DISEASES|THORACIC MEDICINE
50 crohn's disease GASTROENTEROLOGY GASTROENTEROLOGY|PAEDIATRIC GASTROENTEROLOGY|COLORECTAL SURGERY
51 cutaneous t-cell lymphoma CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|DERMATOLOGY
52 cystic fibrosis THORACIC MEDICINE THORACIC MEDICINE|PAEDIATRICS|GASTROENTEROLOGY
53 cytomegalovirus INFECTIOUS DISEASES INFECTIOUS DISEASES|TRANSPLANTATION SURGERY
54 deep vein thrombosis VASCULAR SURGERY VASCULAR SURGERY|CLINICAL HAEMATOLOGY
55 depression MENTAL HEALTH MENTAL HEALTH
56 depressive episode MENTAL HEALTH MENTAL HEALTH
57 diabetes DIABETIC MEDICINE DIABETIC MEDICINE|ENDOCRINOLOGY
58 diabetic macular OPHTHALMOLOGY OPHTHALMOLOGY|MEDICAL OPHTHALMOLOGY
59 diabetic macular oedema OPHTHALMOLOGY OPHTHALMOLOGY|MEDICAL OPHTHALMOLOGY
60 diabetic retinopathy OPHTHALMOLOGY OPHTHALMOLOGY|MEDICAL OPHTHALMOLOGY|DIABETIC MEDICINE
61 diffuse large b-cell lymphoma CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|MEDICAL ONCOLOGY
62 dravet syndrome NEUROLOGY NEUROLOGY|PAEDIATRICS
63 drug misuse MENTAL HEALTH MENTAL HEALTH|ADDICTION MEDICINE
64 dry eye OPHTHALMOLOGY OPHTHALMOLOGY
65 dupuytren's contracture TRAUMA & ORTHOPAEDICS TRAUMA & ORTHOPAEDICS|PLASTIC SURGERY
66 dyslipidaemia CARDIOLOGY CARDIOLOGY|ENDOCRINOLOGY
67 dyspepsia GASTROENTEROLOGY GASTROENTEROLOGY|GENERAL MEDICINE
68 eczema DERMATOLOGY DERMATOLOGY|PAEDIATRICS
69 endometrial cancer MEDICAL ONCOLOGY MEDICAL ONCOLOGY|GYNAECOLOGICAL ONCOLOGY|CLINICAL ONCOLOGY
70 epilepsy NEUROLOGY NEUROLOGY|PAEDIATRICS
71 fallopian tube MEDICAL ONCOLOGY MEDICAL ONCOLOGY|GYNAECOLOGICAL ONCOLOGY|CLINICAL ONCOLOGY
72 fibroids GYNAECOLOGY GYNAECOLOGY
73 follicular lymphoma CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|MEDICAL ONCOLOGY
74 fragility fracture RHEUMATOLOGY RHEUMATOLOGY|TRAUMA & ORTHOPAEDICS|GERIATRIC MEDICINE
75 gastric cancer MEDICAL ONCOLOGY MEDICAL ONCOLOGY|UPPER GASTROINTESTINAL SURGERY|CLINICAL ONCOLOGY
76 gastro-oesophageal MEDICAL ONCOLOGY MEDICAL ONCOLOGY|UPPER GASTROINTESTINAL SURGERY|CLINICAL ONCOLOGY
77 gastro-oesophageal junction MEDICAL ONCOLOGY MEDICAL ONCOLOGY|UPPER GASTROINTESTINAL SURGERY|CLINICAL ONCOLOGY
78 gastrointestinal stromal tumour MEDICAL ONCOLOGY MEDICAL ONCOLOGY|UPPER GASTROINTESTINAL SURGERY|CLINICAL ONCOLOGY
79 gastrointestinal stromal tumours MEDICAL ONCOLOGY MEDICAL ONCOLOGY|UPPER GASTROINTESTINAL SURGERY|CLINICAL ONCOLOGY
80 giant cell arteritis RHEUMATOLOGY RHEUMATOLOGY
81 glioma MEDICAL ONCOLOGY MEDICAL ONCOLOGY|NEUROSURGERY|CLINICAL ONCOLOGY
82 gout RHEUMATOLOGY RHEUMATOLOGY
83 graft versus host disease CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|TRANSPLANTATION SURGERY
84 granulomatosis with polyangiitis RHEUMATOLOGY RHEUMATOLOGY|THORACIC MEDICINE|NEPHROLOGY
85 growth failure ENDOCRINOLOGY ENDOCRINOLOGY|PAEDIATRICS
86 growth hormone deficiency ENDOCRINOLOGY ENDOCRINOLOGY|PAEDIATRICS
87 haemoglobinuria CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY
88 hand eczema DERMATOLOGY DERMATOLOGY
89 head and neck MEDICAL ONCOLOGY MEDICAL ONCOLOGY|ENT|CLINICAL ONCOLOGY
90 heart failure CARDIOLOGY CARDIOLOGY
91 hepatic encephalopathy GASTROENTEROLOGY GASTROENTEROLOGY
92 hepatitis b GASTROENTEROLOGY GASTROENTEROLOGY|INFECTIOUS DISEASES
93 hepatitis c GASTROENTEROLOGY GASTROENTEROLOGY|INFECTIOUS DISEASES
94 hepatocellular carcinoma MEDICAL ONCOLOGY MEDICAL ONCOLOGY|GASTROENTEROLOGY|CLINICAL ONCOLOGY
95 her2 MEDICAL ONCOLOGY MEDICAL ONCOLOGY|BREAST SURGERY|CLINICAL ONCOLOGY
96 her2-positive MEDICAL ONCOLOGY MEDICAL ONCOLOGY|BREAST SURGERY|CLINICAL ONCOLOGY
97 hereditary angioedema CLINICAL IMMUNOLOGY CLINICAL IMMUNOLOGY
98 hidradenitis suppurativa DERMATOLOGY DERMATOLOGY
99 hiv INFECTIOUS DISEASES INFECTIOUS DISEASES
100 hodgkin lymphoma CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|MEDICAL ONCOLOGY
101 hormone receptor MEDICAL ONCOLOGY MEDICAL ONCOLOGY|BREAST SURGERY|CLINICAL ONCOLOGY
102 hypercholesterolaemia CARDIOLOGY CARDIOLOGY|ENDOCRINOLOGY|CHEMICAL PATHOLOGY
103 hyperparathyroidism ENDOCRINOLOGY ENDOCRINOLOGY
104 hyperuricaemia RHEUMATOLOGY RHEUMATOLOGY
105 immune thrombocytopenia CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY
106 influenza INFECTIOUS DISEASES INFECTIOUS DISEASES|GENERAL MEDICINE
107 insomnia NEUROLOGY NEUROLOGY|MENTAL HEALTH
108 interstitial lung disease THORACIC MEDICINE THORACIC MEDICINE
109 irritable bowel syndrome GASTROENTEROLOGY GASTROENTEROLOGY
110 ischaemic stroke STROKE MEDICINE STROKE MEDICINE|NEUROLOGY
111 juvenile idiopathic arthritis RHEUMATOLOGY RHEUMATOLOGY|PAEDIATRICS
112 keratitis OPHTHALMOLOGY OPHTHALMOLOGY
113 kidney disease NEPHROLOGY NEPHROLOGY
114 kidney transplant NEPHROLOGY NEPHROLOGY|TRANSPLANTATION SURGERY
115 large b-cell lymphoma CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|MEDICAL ONCOLOGY
116 leukaemia CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|MEDICAL ONCOLOGY
117 limbal stem cell deficiency OPHTHALMOLOGY OPHTHALMOLOGY
118 liver disease GASTROENTEROLOGY GASTROENTEROLOGY
119 lung cancer MEDICAL ONCOLOGY MEDICAL ONCOLOGY|THORACIC MEDICINE|CLINICAL ONCOLOGY
120 lymphoblastic leukaemia CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|PAEDIATRICS
121 lymphocytic leukaemia CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY
122 lymphoma CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|MEDICAL ONCOLOGY
123 macular degeneration OPHTHALMOLOGY OPHTHALMOLOGY|MEDICAL OPHTHALMOLOGY
124 macular oedema OPHTHALMOLOGY OPHTHALMOLOGY|MEDICAL OPHTHALMOLOGY
125 major depressive episodes MENTAL HEALTH MENTAL HEALTH
126 malignant melanoma MEDICAL ONCOLOGY MEDICAL ONCOLOGY|DERMATOLOGY|CLINICAL ONCOLOGY
127 malignant pleural mesothelioma MEDICAL ONCOLOGY MEDICAL ONCOLOGY|THORACIC MEDICINE|CLINICAL ONCOLOGY
128 manic episode MENTAL HEALTH MENTAL HEALTH
129 mantle cell lymphoma CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|MEDICAL ONCOLOGY
130 mastocytosis CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|CLINICAL IMMUNOLOGY
131 melanoma MEDICAL ONCOLOGY MEDICAL ONCOLOGY|DERMATOLOGY|CLINICAL ONCOLOGY
132 merkel cell MEDICAL ONCOLOGY MEDICAL ONCOLOGY|DERMATOLOGY|CLINICAL ONCOLOGY
133 merkel cell carcinoma MEDICAL ONCOLOGY MEDICAL ONCOLOGY|DERMATOLOGY|CLINICAL ONCOLOGY
134 mesothelioma MEDICAL ONCOLOGY MEDICAL ONCOLOGY|THORACIC MEDICINE|CLINICAL ONCOLOGY
135 metastatic colorectal cancer MEDICAL ONCOLOGY MEDICAL ONCOLOGY|COLORECTAL SURGERY|CLINICAL ONCOLOGY
136 migraine NEUROLOGY NEUROLOGY
137 motor neurone disease NEUROLOGY NEUROLOGY|REHABILITATION|PALLIATIVE CARE
138 multiple myeloma CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|MEDICAL ONCOLOGY
139 multiple sclerosis NEUROLOGY NEUROLOGY|REHABILITATION
140 myelodysplastic CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY
141 myelodysplastic syndromes CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY
142 myelofibrosis CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY
143 myeloid leukaemia CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY
144 myocardial infarction CARDIOLOGY CARDIOLOGY
145 myotonia NEUROLOGY NEUROLOGY
146 narcolepsy NEUROLOGY NEUROLOGY
147 nasal polyps ENT ENT|THORACIC MEDICINE|CLINICAL IMMUNOLOGY
148 neuroblastoma MEDICAL ONCOLOGY MEDICAL ONCOLOGY|PAEDIATRICS|CLINICAL ONCOLOGY
149 neuroendocrine tumour MEDICAL ONCOLOGY MEDICAL ONCOLOGY|ENDOCRINOLOGY|CLINICAL ONCOLOGY
150 non-small cell lung cancer MEDICAL ONCOLOGY MEDICAL ONCOLOGY|THORACIC MEDICINE|CLINICAL ONCOLOGY
151 non-small-cell lung cancer MEDICAL ONCOLOGY MEDICAL ONCOLOGY|THORACIC MEDICINE|CLINICAL ONCOLOGY
152 obesity ENDOCRINOLOGY ENDOCRINOLOGY|DIABETIC MEDICINE|GENERAL MEDICINE
153 oesophageal cancer MEDICAL ONCOLOGY MEDICAL ONCOLOGY|UPPER GASTROINTESTINAL SURGERY|CLINICAL ONCOLOGY
154 osteoarthritis RHEUMATOLOGY RHEUMATOLOGY|TRAUMA & ORTHOPAEDICS|GERIATRIC MEDICINE
155 osteoporosis RHEUMATOLOGY RHEUMATOLOGY|ENDOCRINOLOGY|GERIATRIC MEDICINE
156 osteosarcoma MEDICAL ONCOLOGY MEDICAL ONCOLOGY|TRAUMA & ORTHOPAEDICS|CLINICAL ONCOLOGY
157 ovarian cancer MEDICAL ONCOLOGY MEDICAL ONCOLOGY|GYNAECOLOGICAL ONCOLOGY|CLINICAL ONCOLOGY
158 overweight ENDOCRINOLOGY ENDOCRINOLOGY|DIABETIC MEDICINE
159 pancreatic cancer MEDICAL ONCOLOGY MEDICAL ONCOLOGY|UPPER GASTROINTESTINAL SURGERY|CLINICAL ONCOLOGY
160 pancreatic neuroendocrine MEDICAL ONCOLOGY MEDICAL ONCOLOGY|ENDOCRINOLOGY|CLINICAL ONCOLOGY
161 paroxysmal nocturnal haemoglobinuria CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY
162 peanut allergy CLINICAL IMMUNOLOGY CLINICAL IMMUNOLOGY|PAEDIATRICS
163 perianal fistula GASTROENTEROLOGY GASTROENTEROLOGY|COLORECTAL SURGERY
164 peripheral arterial disease VASCULAR SURGERY VASCULAR SURGERY|CARDIOLOGY
165 plaque psoriasis DERMATOLOGY DERMATOLOGY
166 polycystic kidney NEPHROLOGY NEPHROLOGY
167 polycystic kidney disease NEPHROLOGY NEPHROLOGY
168 polycythaemia CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY
169 polycythaemia vera CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY
170 pouchitis GASTROENTEROLOGY GASTROENTEROLOGY|COLORECTAL SURGERY
171 pregnancy OBSTETRICS OBSTETRICS
172 primary biliary cholangitis GASTROENTEROLOGY GASTROENTEROLOGY
173 primary hypercholesterolaemia CARDIOLOGY CARDIOLOGY|ENDOCRINOLOGY|CHEMICAL PATHOLOGY
174 promyelocytic leukaemia CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY
175 prostate cancer MEDICAL ONCOLOGY MEDICAL ONCOLOGY|UROLOGY|CLINICAL ONCOLOGY
176 psoriasis DERMATOLOGY DERMATOLOGY
177 psoriatic arthritis RHEUMATOLOGY RHEUMATOLOGY|DERMATOLOGY
178 pulmonary embolism THORACIC MEDICINE THORACIC MEDICINE|CARDIOLOGY|CLINICAL HAEMATOLOGY
179 pulmonary fibrosis THORACIC MEDICINE THORACIC MEDICINE
180 relapsing multiple sclerosis NEUROLOGY NEUROLOGY|REHABILITATION
181 renal cell MEDICAL ONCOLOGY MEDICAL ONCOLOGY|UROLOGY|CLINICAL ONCOLOGY
182 renal cell carcinoma MEDICAL ONCOLOGY MEDICAL ONCOLOGY|UROLOGY|CLINICAL ONCOLOGY
183 renal transplantation NEPHROLOGY NEPHROLOGY|TRANSPLANTATION SURGERY
184 retinal vein occlusion OPHTHALMOLOGY OPHTHALMOLOGY|MEDICAL OPHTHALMOLOGY
185 rheumatoid arthritis RHEUMATOLOGY RHEUMATOLOGY|CLINICAL IMMUNOLOGY|GERIATRIC MEDICINE
186 rhinosinusitis with nasal polyps ENT ENT|THORACIC MEDICINE|CLINICAL IMMUNOLOGY
187 rivaroxaban CARDIOLOGY CARDIOLOGY|CLINICAL HAEMATOLOGY
188 schizophrenia MENTAL HEALTH MENTAL HEALTH
189 seizures NEUROLOGY NEUROLOGY|PAEDIATRICS
190 sepsis INFECTIOUS DISEASES INFECTIOUS DISEASES|CRITICAL CARE MEDICINE
191 severe persistent allergic asthma THORACIC MEDICINE THORACIC MEDICINE|CLINICAL IMMUNOLOGY
192 short bowel syndrome GASTROENTEROLOGY GASTROENTEROLOGY|COLORECTAL SURGERY
193 sickle cell CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|PAEDIATRICS
194 sickle cell disease CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|PAEDIATRICS
195 sleep apnoea THORACIC MEDICINE THORACIC MEDICINE|ENT
196 smoking cessation THORACIC MEDICINE THORACIC MEDICINE|GENERAL MEDICINE
197 soft tissue sarcoma MEDICAL ONCOLOGY MEDICAL ONCOLOGY|CLINICAL ONCOLOGY
198 spinal muscular atrophy NEUROLOGY NEUROLOGY|PAEDIATRICS
199 splenomegaly CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|GASTROENTEROLOGY
200 spondyloarthritis RHEUMATOLOGY RHEUMATOLOGY
201 squamous cell MEDICAL ONCOLOGY MEDICAL ONCOLOGY|DERMATOLOGY|ENT|CLINICAL ONCOLOGY
202 squamous cell carcinoma MEDICAL ONCOLOGY MEDICAL ONCOLOGY|DERMATOLOGY|ENT|CLINICAL ONCOLOGY
203 stem cell transplant CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|TRANSPLANTATION SURGERY
204 stroke STROKE MEDICINE STROKE MEDICINE|NEUROLOGY
205 systemic lupus erythematosus RHEUMATOLOGY RHEUMATOLOGY|CLINICAL IMMUNOLOGY|NEPHROLOGY
206 systemic mastocytosis CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|CLINICAL IMMUNOLOGY
207 t-cell lymphoma CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|MEDICAL ONCOLOGY
208 thalassaemia CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|PAEDIATRICS
209 thrombocytopenia CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY
210 thrombocytopenic purpura CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY
211 thromboembolism CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|CARDIOLOGY
212 thrombotic thrombocytopenic purpura CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY
213 thyroid cancer MEDICAL ONCOLOGY MEDICAL ONCOLOGY|ENDOCRINOLOGY|CLINICAL ONCOLOGY
214 tophaceous gout RHEUMATOLOGY RHEUMATOLOGY
215 transitional cell carcinoma MEDICAL ONCOLOGY MEDICAL ONCOLOGY|UROLOGY|CLINICAL ONCOLOGY
216 transthyretin amyloidosis CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|CARDIOLOGY|NEUROLOGY
217 triple-negative MEDICAL ONCOLOGY MEDICAL ONCOLOGY|BREAST SURGERY|CLINICAL ONCOLOGY
218 tuberous sclerosis NEUROLOGY NEUROLOGY|PAEDIATRICS
219 type 1 diabetes DIABETIC MEDICINE DIABETIC MEDICINE|ENDOCRINOLOGY|PAEDIATRICS
220 type 2 diabetes DIABETIC MEDICINE DIABETIC MEDICINE|ENDOCRINOLOGY
221 ulcerative colitis GASTROENTEROLOGY GASTROENTEROLOGY|COLORECTAL SURGERY
222 urothelial MEDICAL ONCOLOGY MEDICAL ONCOLOGY|UROLOGY|CLINICAL ONCOLOGY
223 urothelial cancer MEDICAL ONCOLOGY MEDICAL ONCOLOGY|UROLOGY|CLINICAL ONCOLOGY
224 urothelial carcinoma MEDICAL ONCOLOGY MEDICAL ONCOLOGY|UROLOGY|CLINICAL ONCOLOGY
225 urticaria DERMATOLOGY DERMATOLOGY|CLINICAL IMMUNOLOGY
226 uterine fibroids GYNAECOLOGY GYNAECOLOGY
227 uveitis OPHTHALMOLOGY OPHTHALMOLOGY|MEDICAL OPHTHALMOLOGY|RHEUMATOLOGY
228 vascular disease VASCULAR SURGERY VASCULAR SURGERY|CARDIOLOGY
229 vasculitis RHEUMATOLOGY RHEUMATOLOGY|CLINICAL IMMUNOLOGY
230 venom allergy CLINICAL IMMUNOLOGY CLINICAL IMMUNOLOGY
231 venous thromboembolism CLINICAL HAEMATOLOGY CLINICAL HAEMATOLOGY|VASCULAR SURGERY
File diff suppressed because it is too large Load Diff
-4
View File
@@ -1,4 +0,0 @@
# Re-export app from pathways_app
from pathways_app.pathways_app import app
__all__ = ["app"]
@@ -1,17 +0,0 @@
"""
UI components for the Patient Pathway Analysis Reflex application.
This module exports reusable layout and navigation components.
"""
from .layout import sidebar, navbar, content_area, main_layout
from .navigation import nav_item, nav_section
__all__ = [
"sidebar",
"navbar",
"content_area",
"main_layout",
"nav_item",
"nav_section",
]
-262
View File
@@ -1,262 +0,0 @@
"""
Layout components for the Patient Pathway Analysis tool.
Provides the main application layout with sidebar navigation and content area.
Includes accessibility features: skip links, ARIA landmarks, keyboard navigation.
"""
import reflex as rx
from .navigation import nav_item
# NHS Color scheme
NHS_BLUE = "rgb(0, 94, 184)"
NHS_DARK_BLUE = "rgb(0, 48, 135)"
NHS_LIGHT_BLUE = "rgb(65, 182, 230)"
NHS_WHITE = "white"
NHS_GREY = "rgb(231, 231, 231)"
def skip_link() -> rx.Component:
"""
Skip link for keyboard users to bypass navigation.
Visually hidden until focused, allowing keyboard users to skip
directly to main content.
"""
return rx.link(
"Skip to main content",
href="#main-content",
position="absolute",
top="-40px",
left="0",
background=NHS_BLUE,
color="white",
padding="8px 16px",
z_index="1000",
text_decoration="none",
font_weight="bold",
_focus={
"top": "0",
},
)
def logo_section() -> rx.Component:
"""NHS branding logo section at top of sidebar."""
return rx.hstack(
rx.image(
src="/logo.png",
height="32px",
alt="NHS Norfolk and Waveney Logo",
),
rx.text(
"HCD Analysis",
size="5",
weight="bold",
color=NHS_BLUE,
),
padding="16px",
spacing="3",
align="center",
width="100%",
border_bottom=f"1px solid {NHS_GREY}",
)
def sidebar(current_page: str = "home") -> rx.Component:
"""
Create the sidebar navigation panel.
Args:
current_page: The current active page name for highlighting
Returns:
A sidebar component with navigation items and ARIA landmark
"""
return rx.el.nav(
rx.vstack(
# Logo section
logo_section(),
# Navigation items
rx.vstack(
nav_item(
"Home",
"/",
"home",
is_active=(current_page == "home"),
),
nav_item(
"Drug Selection",
"/drugs",
"pill",
is_active=(current_page == "drugs"),
),
nav_item(
"Trust Selection",
"/trusts",
"building",
is_active=(current_page == "trusts"),
),
nav_item(
"Directory Selection",
"/directories",
"folder",
is_active=(current_page == "directories"),
),
padding="8px",
spacing="1",
width="100%",
align="start",
),
# Spacer to push theme toggle to bottom
rx.spacer(),
# Theme toggle at bottom
rx.box(
rx.hstack(
rx.el.label(
"Theme:",
html_for="theme-toggle",
font_size="14px",
color="gray",
),
rx.color_mode.switch(id="theme-toggle"),
spacing="2",
align="center",
),
padding="16px",
border_top=f"1px solid {NHS_GREY}",
width="100%",
),
height="100vh",
width="100%",
spacing="0",
align="start",
),
aria_label="Main navigation",
width="240px",
min_width="240px",
background="white",
border_right=f"1px solid {NHS_GREY}",
position="fixed",
left="0",
top="0",
height="100vh",
overflow_y="auto",
z_index="100",
)
def navbar() -> rx.Component:
"""
Create a top navigation bar for mobile/smaller screens.
Returns:
A horizontal navbar component (collapsed sidebar for mobile) with ARIA support
"""
return rx.el.header(
rx.hstack(
rx.image(src="/logo.png", height="28px", alt="NHS Norfolk and Waveney Logo"),
rx.text("HCD Analysis", size="4", weight="bold"),
rx.spacer(),
rx.el.label(
rx.color_mode.switch(id="theme-toggle-mobile"),
html_for="theme-toggle-mobile",
aria_label="Toggle dark mode",
),
width="100%",
padding="12px 16px",
align="center",
justify="between",
),
background="white",
border_bottom=f"1px solid {NHS_GREY}",
display=["flex", "flex", "none"], # Show on mobile, hide on desktop
width="100%",
position="fixed",
top="0",
left="0",
z_index="100",
role="banner",
)
def content_area(*children, page_title: str = "") -> rx.Component:
"""
Create the main content area.
Args:
*children: Child components to render in the content area
page_title: Optional title to display at top of content
Returns:
A styled content area component with ARIA main landmark
"""
content_children = list(children)
if page_title:
content_children.insert(
0,
rx.heading(
page_title,
size="6",
weight="bold",
color=NHS_DARK_BLUE,
margin_bottom="16px",
),
)
return rx.el.main(
rx.vstack(
*content_children,
width="100%",
max_width="1200px",
padding="24px",
spacing="4",
align="start",
),
id="main-content",
tabindex="-1", # Allow focus for skip link
# Offset for sidebar on desktop
margin_left=["0", "0", "240px"],
# Offset for navbar on mobile
margin_top=["60px", "60px", "0"],
min_height="100vh",
background=rx.color_mode_cond(
light="rgb(249, 250, 251)", # Light gray background
dark="rgb(17, 24, 39)", # Dark background
),
width="100%",
_focus={
"outline": "none", # Hide focus ring on main (only accessible via skip link)
},
)
def main_layout(
content: rx.Component,
current_page: str = "home",
) -> rx.Component:
"""
Create the complete page layout with sidebar and content.
Args:
content: The main content to display
current_page: The current page name for navigation highlighting
Returns:
A complete page layout component with accessibility features
"""
return rx.fragment(
# Skip link for keyboard users
skip_link(),
# Sidebar (visible on desktop)
rx.box(
sidebar(current_page=current_page),
display=["none", "none", "block"], # Hide on mobile
),
# Navbar (visible on mobile)
navbar(),
# Main content
content,
)
@@ -1,86 +0,0 @@
"""
Navigation components for the Patient Pathway Analysis tool.
Provides sidebar navigation items with icons, matching the CustomTkinter design.
Includes accessibility features: ARIA labels, keyboard navigation, focus indicators.
"""
import reflex as rx
from typing import Callable
def nav_item(
text: str,
href: str,
icon: str,
is_active: bool = False,
) -> rx.Component:
"""
Create a navigation item with icon.
Args:
text: The display text for the nav item
href: The route to navigate to
icon: The Lucide icon name (e.g., "home", "pill", "building", "folder")
is_active: Whether this item is currently active
Returns:
A styled navigation button component with accessibility support
"""
# NHS colors - use blue for active state
active_bg = "rgb(0, 94, 184)" # NHS Blue
hover_bg = "rgb(0, 48, 135)" # NHS Dark Blue
return rx.link(
rx.hstack(
rx.icon(icon, size=20, aria_hidden="true"), # Hide decorative icon from screen readers
rx.text(text, size="3", weight="medium"),
width="100%",
padding="12px 16px",
spacing="3",
align="center",
border_radius="8px",
bg=rx.cond(is_active, active_bg, "transparent"),
color=rx.cond(is_active, "white", "inherit"),
_hover={
"background": rx.cond(is_active, active_bg, "rgba(0, 94, 184, 0.1)"),
},
_focus_visible={
"outline": "2px solid rgb(0, 94, 184)",
"outline_offset": "2px",
},
transition="background 0.2s ease",
),
href=href,
text_decoration="none",
width="100%",
aria_current=rx.cond(is_active, "page", ""),
)
def nav_section(title: str, children: list[rx.Component]) -> rx.Component:
"""
Create a labeled section of navigation items.
Args:
title: Section header text
children: List of nav_item components
Returns:
A styled section with header and items
"""
return rx.vstack(
rx.text(
title,
size="1",
weight="bold",
color="gray",
padding_x="16px",
padding_top="16px",
padding_bottom="8px",
),
*children,
width="100%",
spacing="1",
align="start",
)
File diff suppressed because it is too large Load Diff
-715
View File
@@ -1,715 +0,0 @@
"""
Design tokens and style helpers for HCD Analysis v2.1 (SaaS Redesign).
All visual styling should use these tokens for consistency.
Import: from pathways_app.styles import Colors, Spacing, Typography, etc.
Updated to match DESIGN_SYSTEM.md v2.1 with:
- Tighter spacing (25% reduction)
- Smaller typography (reduced headline sizes)
- Compact component variants for filters/KPIs
- Full-width chart support
"""
class Colors:
"""Color palette from DESIGN_SYSTEM.md"""
# Primary Blues (NHS-inspired, used sparingly)
HERITAGE_BLUE = "#003087" # Top bar background, strong accents
PRIMARY = "#0066CC" # Interactive elements, links, focus states
VIBRANT = "#1E88E5" # Hover states, active elements
SKY = "#4FC3F7" # Subtle accents, progress indicators
PALE = "#E3F2FD" # Selected states, subtle backgrounds
# Neutrals (refined for modern feel)
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 (modernized)
SUCCESS = "#10B981" # Positive (modern green)
WARNING = "#F59E0B" # Caution
ERROR = "#EF4444" # Errors
INFO = "#3B82F6" # Informational
# Chart Palette
CHART_SERIES = ["#003087", "#0066CC", "#1E88E5", "#4FC3F7", "#90CAF9"]
CHART_CATEGORICAL = ["#0066CC", "#10B981", "#F59E0B", "#8B5CF6", "#EC4899"]
class Typography:
"""Typography tokens from DESIGN_SYSTEM.md v2.1 - REDUCED sizes"""
# Font families
FONT_FAMILY = "Inter, system-ui, -apple-system, sans-serif"
FONT_MONO = "JetBrains Mono, monospace"
# Display: Page titles (REDUCED from 32px)
DISPLAY_SIZE = "28px"
DISPLAY_WEIGHT = "600"
DISPLAY_TRACKING = "-0.02em"
DISPLAY_LINE_HEIGHT = "1.2"
# Heading 1: Section headers (REDUCED from 24px)
H1_SIZE = "18px"
H1_WEIGHT = "600"
H1_TRACKING = "-0.01em"
H1_LINE_HEIGHT = "1.3"
# Heading 2: Card titles (REDUCED from 20px)
H2_SIZE = "16px"
H2_WEIGHT = "600"
H2_TRACKING = "normal"
H2_LINE_HEIGHT = "1.4"
# Heading 3: Subsections
H3_SIZE = "14px"
H3_WEIGHT = "600"
H3_TRACKING = "normal"
H3_LINE_HEIGHT = "1.4"
# Body: Default text
BODY_SIZE = "14px"
BODY_WEIGHT = "400"
BODY_LINE_HEIGHT = "1.5"
# Body Small: Secondary info
BODY_SMALL_SIZE = "13px"
BODY_SMALL_WEIGHT = "400"
BODY_SMALL_LINE_HEIGHT = "1.5"
# Caption: Labels, metadata (REDUCED from 12px)
CAPTION_SIZE = "11px"
CAPTION_WEIGHT = "500"
CAPTION_LINE_HEIGHT = "1.4"
# Mono: Data values, codes
MONO_SIZE = "13px"
MONO_WEIGHT = "500"
MONO_LINE_HEIGHT = "1.5"
class Spacing:
"""Spacing scale from DESIGN_SYSTEM.md v2.1 - TIGHTER values (~25% reduction)"""
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)
XXL = "24px" # Major gaps (was 32px)
XXXL = "32px" # Page margins (was 48px)
class Radii:
"""Border radius values from DESIGN_SYSTEM.md"""
SM = "4px" # Small elements
MD = "6px" # Inputs, buttons
LG = "8px" # Cards
XL = "16px" # Large containers
FULL = "9999px" # Pills, badges
class Shadows:
"""Shadow values from DESIGN_SYSTEM.md v2.1 - LIGHTER values"""
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
XL = "0 10px 15px rgba(0,0,0,0.1)" # Modals, popovers
class Transitions:
"""Transition values from DESIGN_SYSTEM.md v2.1 - FASTER (150ms)"""
DEFAULT = "150ms ease-out"
COLOR = "150ms ease-out"
TRANSFORM = "150ms ease-out"
SHADOW = "150ms ease-out"
OPACITY = "150ms ease-in-out"
# ==============================================================================
# Layout constants - UPDATED for SaaS redesign
# ==============================================================================
TOP_BAR_HEIGHT = "48px" # Reduced from 64px
FILTER_STRIP_HEIGHT = "48px" # Single row filter strip
PAGE_MAX_WIDTH = "1600px" # Keep for content areas (not chart)
PAGE_PADDING = Spacing.XXXL # 32px
# ==============================================================================
# Helper functions for common style patterns
# ==============================================================================
def card_style(hoverable: bool = False) -> dict:
"""
Card styling following DESIGN_SYSTEM.md specifications.
- Background: White
- Border: 1px Slate 300
- Border radius: lg (8px)
- Padding: xl (16px - reduced)
- Shadow: md at rest, lg on hover
"""
base_style = {
"background_color": Colors.WHITE,
"border": f"1px solid {Colors.SLATE_300}",
"border_radius": Radii.LG,
"padding": Spacing.XL,
"box_shadow": Shadows.MD,
}
if hoverable:
base_style.update({
"transition": f"box-shadow {Transitions.SHADOW}, transform {Transitions.TRANSFORM}",
"_hover": {
"box_shadow": Shadows.LG,
"transform": "translateY(-2px)",
}
})
return base_style
def button_primary_style() -> dict:
"""
Primary button styling following DESIGN_SYSTEM.md specifications.
Includes accessible focus ring.
"""
return {
"background_color": Colors.PRIMARY,
"color": Colors.WHITE,
"border_radius": Radii.MD,
"padding": "8px 16px",
"font_weight": "500",
"font_size": Typography.BODY_SIZE,
"cursor": "pointer",
"border": "none",
"transition": f"background-color {Transitions.COLOR}, transform {Transitions.TRANSFORM}, box-shadow {Transitions.SHADOW}",
"_hover": {
"background_color": Colors.VIBRANT,
"transform": "scale(1.02)",
},
"_focus": {
"outline": "none",
"box_shadow": f"0 0 0 2px {Colors.WHITE}, 0 0 0 4px {Colors.PRIMARY}",
},
"_focus_visible": {
"outline": "none",
"box_shadow": f"0 0 0 2px {Colors.WHITE}, 0 0 0 4px {Colors.PRIMARY}",
},
"_active": {
"transform": "scale(0.98)",
},
}
def button_secondary_style() -> dict:
"""
Secondary button styling following DESIGN_SYSTEM.md specifications.
Includes accessible focus ring.
"""
return {
"background_color": Colors.WHITE,
"color": Colors.PRIMARY,
"border": f"1px solid {Colors.PRIMARY}",
"border_radius": Radii.MD,
"padding": "8px 16px",
"font_weight": "500",
"font_size": Typography.BODY_SIZE,
"cursor": "pointer",
"transition": f"background-color {Transitions.COLOR}, box-shadow {Transitions.SHADOW}",
"_hover": {
"background_color": Colors.PALE,
},
"_focus": {
"outline": "none",
"box_shadow": f"0 0 0 2px {Colors.PALE}",
},
"_focus_visible": {
"outline": "none",
"box_shadow": f"0 0 0 2px {Colors.PALE}",
},
"_active": {
"background_color": Colors.SLATE_100,
},
}
def button_ghost_style() -> dict:
"""
Ghost button styling following DESIGN_SYSTEM.md specifications.
Includes accessible focus ring.
"""
return {
"background_color": "transparent",
"color": Colors.PRIMARY,
"border": "none",
"border_radius": Radii.MD,
"padding": "8px 16px",
"font_weight": "500",
"font_size": Typography.BODY_SIZE,
"cursor": "pointer",
"transition": f"background-color {Transitions.COLOR}, box-shadow {Transitions.SHADOW}",
"_hover": {
"background_color": Colors.PALE,
},
"_focus": {
"outline": "none",
"box_shadow": f"0 0 0 2px {Colors.PALE}",
},
"_focus_visible": {
"outline": "none",
"box_shadow": f"0 0 0 2px {Colors.PALE}",
},
"_active": {
"background_color": Colors.SLATE_100,
},
}
def input_style() -> dict:
"""
Form input styling following DESIGN_SYSTEM.md specifications.
"""
return {
"height": "32px",
"border": f"1px solid {Colors.SLATE_300}",
"border_radius": Radii.MD,
"padding": f"0 {Spacing.MD}",
"font_size": Typography.BODY_SMALL_SIZE,
"font_family": Typography.FONT_FAMILY,
"color": Colors.SLATE_900,
"background_color": Colors.WHITE,
"transition": f"border-color {Transitions.COLOR}, box-shadow {Transitions.COLOR}",
"_placeholder": {
"color": Colors.SLATE_500,
},
"_focus": {
"outline": "none",
"border_color": Colors.PRIMARY,
"box_shadow": f"0 0 0 2px {Colors.PALE}",
}
}
# ==============================================================================
# KPI Card styles - COMPACT variants for v2.1
# ==============================================================================
def kpi_card_style() -> dict:
"""
Standard KPI card styling (legacy, larger).
"""
return {
"background_color": Colors.WHITE,
"border": f"1px solid {Colors.SLATE_300}",
"border_radius": Radii.LG,
"padding": Spacing.XL,
"box_shadow": Shadows.SM,
"text_align": "center",
}
def kpi_value_style() -> dict:
"""Style for the large number in a KPI card (legacy)."""
return {
"font_family": Typography.FONT_MONO,
"font_size": "32px",
"font_weight": "600",
"color": Colors.SLATE_900,
"line_height": "1.2",
}
def kpi_label_style() -> dict:
"""Style for the label in a KPI card (legacy)."""
return {
"font_size": Typography.CAPTION_SIZE,
"font_weight": Typography.CAPTION_WEIGHT,
"color": Colors.SLATE_500,
"margin_top": Spacing.SM,
}
def kpi_badge_style() -> dict:
"""
KPI as inline pill/badge (Option A from design system).
Zero extra height - embeds in filter row.
Example: "12,345 patients"
Includes subtle hover state for interactivity feedback.
"""
return {
"display": "inline-flex",
"align_items": "center",
"gap": Spacing.XS,
"padding": f"{Spacing.XS} {Spacing.LG}", # 4px 12px
"background_color": Colors.SLATE_100,
"border_radius": Radii.FULL, # Pill shape
"transition": f"transform {Transitions.TRANSFORM}, box-shadow {Transitions.SHADOW}",
"cursor": "default",
"_hover": {
"transform": "translateY(-1px)",
"box_shadow": Shadows.SM,
},
}
def kpi_badge_value_style() -> dict:
"""Style for value text in KPI badge."""
return {
"font_family": Typography.FONT_MONO,
"font_size": "14px",
"font_weight": "600",
"color": Colors.SLATE_900,
}
def kpi_badge_label_style() -> dict:
"""Style for label text in KPI badge."""
return {
"font_size": Typography.CAPTION_SIZE,
"font_weight": "400",
"color": Colors.SLATE_500,
}
# ==============================================================================
# Filter strip styles - NEW for v2.1 redesign
# ==============================================================================
def filter_strip_style() -> dict:
"""
Horizontal single-row filter container style.
- Height: 48px
- All filters inline
- Slate 100 background (or transparent)
"""
return {
"display": "flex",
"align_items": "center",
"height": FILTER_STRIP_HEIGHT,
"gap": Spacing.LG, # 12px between filter groups
"padding": f"0 {Spacing.XL}", # 16px horizontal padding
"background_color": Colors.SLATE_100,
"border_bottom": f"1px solid {Colors.SLATE_300}",
"width": "100%",
}
def compact_dropdown_trigger_style() -> dict:
"""
Compact dropdown trigger for filter strip.
- Height: 32px
- Padding: 8px 12px
- Smaller font: 13px
- Accessible focus ring
"""
return {
"height": "32px",
"padding": f"{Spacing.MD} {Spacing.LG}", # 8px 12px
"border": f"1px solid {Colors.SLATE_300}",
"border_radius": Radii.MD,
"font_size": Typography.BODY_SMALL_SIZE, # 13px
"font_family": Typography.FONT_FAMILY,
"color": Colors.SLATE_900,
"background_color": Colors.WHITE,
"cursor": "pointer",
"display": "flex",
"align_items": "center",
"gap": Spacing.SM,
"transition": f"border-color {Transitions.COLOR}, box-shadow {Transitions.SHADOW}",
"_hover": {
"border_color": Colors.PRIMARY,
"background_color": Colors.SLATE_100,
},
"_focus": {
"outline": "none",
"border_color": Colors.PRIMARY,
"box_shadow": f"0 0 0 2px {Colors.PALE}",
},
"_focus_visible": {
"outline": "none",
"border_color": Colors.PRIMARY,
"box_shadow": f"0 0 0 2px {Colors.PALE}",
},
}
def searchable_dropdown_panel_style() -> dict:
"""
Dropdown panel for searchable multi-select.
- Max height: 200px for items
- Compact item spacing
"""
return {
"background_color": Colors.WHITE,
"border": f"1px solid {Colors.SLATE_300}",
"border_radius": Radii.LG,
"box_shadow": Shadows.LG,
"min_width": "240px",
"max_width": "320px",
"z_index": "50",
"overflow": "hidden",
}
def searchable_dropdown_item_style(selected: bool = False) -> dict:
"""
Individual item in searchable dropdown.
- Tighter padding: 6px 8px
- Visual selected state
- Accessible focus state
"""
base = {
"padding": f"{Spacing.SM} {Spacing.MD}", # 6px 8px
"font_size": Typography.BODY_SMALL_SIZE,
"cursor": "pointer",
"display": "flex",
"align_items": "center",
"gap": Spacing.SM,
"transition": f"background-color {Transitions.COLOR}",
"border_radius": Radii.SM, # Slight rounding for focus state
"_focus": {
"outline": "none",
"background_color": Colors.SLATE_100,
"box_shadow": f"inset 0 0 0 1px {Colors.PRIMARY}",
},
}
if selected:
base.update({
"background_color": Colors.PALE,
"color": Colors.PRIMARY,
"_hover": {
"background_color": Colors.PALE,
},
})
else:
base.update({
"background_color": Colors.WHITE,
"color": Colors.SLATE_900,
"_hover": {
"background_color": Colors.SLATE_100,
},
})
return base
# ==============================================================================
# Chart container styles - NEW for v2.1 redesign
# ==============================================================================
def chart_container_style() -> dict:
"""
Full-width, flex-grow chart wrapper.
- Width: full viewport minus padding (16px each side)
- Height: fills remaining space (min 500px)
- No max-width constraint
"""
return {
"width": "100%",
"padding": f"0 {Spacing.XL}", # 16px horizontal padding
"flex": "1",
"min_height": "500px",
"display": "flex",
"flex_direction": "column",
}
def chart_wrapper_style(overhead_height: str = "96px") -> dict:
"""
Inner chart wrapper with calculated height.
Args:
overhead_height: Total height of fixed elements above chart
(top bar + filter strip = 48px + 48px = 96px default)
"""
return {
"width": "100%",
"height": f"calc(100vh - {overhead_height})",
"min_height": "500px",
}
# ==============================================================================
# Typography helper functions
# ==============================================================================
def text_display() -> dict:
"""Display text style for page titles."""
return {
"font_size": Typography.DISPLAY_SIZE,
"font_weight": Typography.DISPLAY_WEIGHT,
"letter_spacing": Typography.DISPLAY_TRACKING,
"line_height": Typography.DISPLAY_LINE_HEIGHT,
"color": Colors.SLATE_900,
"font_family": Typography.FONT_FAMILY,
}
def text_h1() -> dict:
"""Heading 1 style for section headers."""
return {
"font_size": Typography.H1_SIZE,
"font_weight": Typography.H1_WEIGHT,
"letter_spacing": Typography.H1_TRACKING,
"line_height": Typography.H1_LINE_HEIGHT,
"color": Colors.SLATE_900,
"font_family": Typography.FONT_FAMILY,
}
def text_h2() -> dict:
"""Heading 2 style for card titles."""
return {
"font_size": Typography.H2_SIZE,
"font_weight": Typography.H2_WEIGHT,
"letter_spacing": Typography.H2_TRACKING,
"line_height": Typography.H2_LINE_HEIGHT,
"color": Colors.SLATE_900,
"font_family": Typography.FONT_FAMILY,
}
def text_h3() -> dict:
"""Heading 3 style for subsections."""
return {
"font_size": Typography.H3_SIZE,
"font_weight": Typography.H3_WEIGHT,
"letter_spacing": Typography.H3_TRACKING,
"line_height": Typography.H3_LINE_HEIGHT,
"color": Colors.SLATE_900,
"font_family": Typography.FONT_FAMILY,
}
def text_body() -> dict:
"""Default body text style."""
return {
"font_size": Typography.BODY_SIZE,
"font_weight": Typography.BODY_WEIGHT,
"line_height": Typography.BODY_LINE_HEIGHT,
"color": Colors.SLATE_900,
"font_family": Typography.FONT_FAMILY,
}
def text_body_small() -> dict:
"""Secondary/small body text style."""
return {
"font_size": Typography.BODY_SMALL_SIZE,
"font_weight": Typography.BODY_SMALL_WEIGHT,
"line_height": Typography.BODY_SMALL_LINE_HEIGHT,
"color": Colors.SLATE_700,
"font_family": Typography.FONT_FAMILY,
}
def text_caption() -> dict:
"""Caption style for labels and metadata."""
return {
"font_size": Typography.CAPTION_SIZE,
"font_weight": Typography.CAPTION_WEIGHT,
"line_height": Typography.CAPTION_LINE_HEIGHT,
"color": Colors.SLATE_500,
"font_family": Typography.FONT_FAMILY,
}
def text_mono() -> dict:
"""Monospace text style for data values and codes."""
return {
"font_size": Typography.MONO_SIZE,
"font_weight": Typography.MONO_WEIGHT,
"line_height": Typography.MONO_LINE_HEIGHT,
"color": Colors.SLATE_900,
"font_family": Typography.FONT_MONO,
}
# ==============================================================================
# Top bar styles - NEW for v2.1 redesign
# ==============================================================================
def top_bar_style() -> dict:
"""
Top bar container style.
- Height: 48px (reduced from 64px)
- Heritage Blue background
"""
return {
"height": TOP_BAR_HEIGHT,
"background_color": Colors.HERITAGE_BLUE,
"display": "flex",
"align_items": "center",
"justify_content": "space_between",
"padding": f"0 {Spacing.XL}",
"width": "100%",
}
def top_bar_tab_style(active: bool = False) -> dict:
"""
Tab/pill style for top bar navigation.
- Height: 28px
- Smaller pills
- Accessible focus ring
"""
base = {
"height": "28px",
"padding": f"{Spacing.XS} {Spacing.LG}", # 4px 12px
"border_radius": Radii.MD,
"font_size": Typography.BODY_SMALL_SIZE,
"font_weight": "500",
"cursor": "pointer",
"transition": f"background-color {Transitions.COLOR}, box-shadow {Transitions.SHADOW}",
"_focus": {
"outline": "none",
"box_shadow": f"0 0 0 2px rgba(255,255,255,0.4)",
},
"_focus_visible": {
"outline": "none",
"box_shadow": f"0 0 0 2px rgba(255,255,255,0.4)",
},
}
if active:
base.update({
"background_color": Colors.WHITE,
"color": Colors.HERITAGE_BLUE,
})
else:
base.update({
"background_color": "transparent",
"color": Colors.WHITE,
"_hover": {
"background_color": "rgba(255,255,255,0.15)",
}
})
return base
def logo_style() -> dict:
"""Logo style for top bar - 28px height (reduced from 36px)."""
return {
"height": "28px",
"width": "auto",
}
File diff suppressed because one or more lines are too long
-14
View File
@@ -1,14 +0,0 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent / "src"))
import reflex as rx
config = rx.Config(
app_name="pathways_app",
plugins=[
rx.plugins.SitemapPlugin(),
rx.plugins.TailwindV4Plugin(),
]
)
-186
View File
@@ -1,186 +0,0 @@
-- NICE TA Indication SNOMED Mapping Query (v2 - optimized clusters)
-- Excludes overly broad clusters (GDPPR_COD, GDPPR2YR_COD)
WITH SearchTermClusters AS (
SELECT Search_Term, Cluster_ID FROM (VALUES
('acute lymphoblastic leukaemia', 'HAEMCANMORPH_COD'),
('acute myeloid leukaemia', 'C19HAEMCAN_COD'),
('acute promyelocytic leukaemia', 'HAEMCANMORPH_COD'),
('allergic asthma', 'AST_COD'),
('allergic rhinitis', 'MILDINTAST_COD'),
('alzheimer''s disease', 'DEMALZ_COD'),
('amyloidosis', 'AMYLOID_COD'),
('anaemia', 'eFI2_AnaemiaTimeSensitive'),
('anaplastic large cell lymphoma', 'C19HAEMCAN_COD'),
('apixaban', 'DOACCON_COD'),
('aplastic anaemia', 'eFI2_AnaemiaEver'),
('arthritis', 'eFI2_InflammatoryArthritis'),
('asthma', 'eFI2_Asthma'),
('atopic dermatitis', 'ATOPDERM_COD'),
('atrial fibrillation', 'eFI2_AtrialFibrillation'),
('attention deficit hyperactivity disorder', 'ADHD_COD'),
('bipolar disorder', 'MH_COD'),
('bladder', 'eFI2_UrinaryIncontinence'),
('breast cancer', 'BRCANSCR_COD'),
('cardiomyopathy', 'eFI2_HarmfulDrinking'),
('cardiovascular disease', 'CVDRISKASS_COD'),
('cervical cancer', 'CSDEC_COD'),
('cholangiocarcinoma', 'eFI2_Cancer'),
('chronic kidney disease', 'CKD_COD'),
('chronic liver disease', 'eFI2_LiverProblems'),
('chronic lymphocytic leukaemia', 'EPPHAEMCAN_COD'),
('chronic myeloid leukaemia', 'EPPHAEMCAN_COD'),
('chronic obstructive pulmonary disease', 'eFI2_COPD'),
('colon cancer', 'eFI2_Cancer'),
('colorectal cancer', 'GICANREF_COD'),
('constipation', 'CHRONCONSTIP_COD'),
('covid-19', 'POSSPOSTCOVID_COD'),
('crohn''s disease', 'eFI2_InflammatoryBowelDisease'),
('cutaneous t-cell lymphoma', 'C19HAEMCAN_COD'),
('cystic fibrosis', 'CUST_ICB_CYSTIC_FIBROSIS'),
('deep vein thrombosis', 'VTE_COD'),
('depression', 'eFI2_Depression'),
('diabetes', 'eFI2_DiabetesEver'),
('diabetic retinopathy', 'DRSELIGIBILITY_COD'),
('diffuse large b-cell lymphoma', 'C19HAEMCAN_COD'),
('dravet syndrome', 'EPIL_COD'),
('drug misuse', 'ILLSUBINT_COD'),
('dyspepsia', 'eFI2_AbdominalPain'),
('epilepsy', 'eFI2_Seizures'),
('fallopian tube', 'STERIL_COD'),
('follicular lymphoma', 'C19HAEMCAN_COD'),
('gastric cancer', 'eFI2_Cancer'),
('giant cell arteritis', 'GCA_COD'),
('glioma', 'NHAEMCANMORPH_COD'),
('gout', 'eFI2_InflammatoryArthritis'),
('graft versus host disease', 'GVHD_COD'),
('granulomatosis with polyangiitis', 'WEGENERVASC_COD'),
('growth hormone deficiency', 'HYPOPITUITARY_COD'),
('hand eczema', 'ECZEMA_COD'),
('heart failure', 'eFI2_HeartFailure'),
('hepatitis b', 'HEPBCVAC_COD'),
('hepatocellular carcinoma', 'eFI2_Cancer'),
('hiv', 'PREFLANG_COD'),
('hodgkin lymphoma', 'HAEMCANMORPH_COD'),
('hormone receptor', 'eFI2_ThyroidProblems'),
('hypercholesterolaemia', 'CLASSFH_COD'),
('immune thrombocytopenia', 'ITP_COD'),
('influenza', 'FLUINVITE_COD'),
('insomnia', 'eFI2_SleepProblems'),
('irritable bowel syndrome', 'IBS_COD'),
('ischaemic stroke', 'OSTR_COD'),
('juvenile idiopathic arthritis', 'RARTHAD_COD'),
('kidney transplant', 'RENALTRANSP_COD'),
('leukaemia', 'eFI2_Cancer'),
('lung cancer', 'FTCANREF_COD'),
('lymphoma', 'C19HAEMCAN_COD'),
('macular degeneration', 'CUST_ICB_VISUAL_IMPAIRMENT'),
('macular oedema', 'CUST_ICB_VISUAL_IMPAIRMENT'),
('major depressive episodes', 'eFI2_Depression'),
('malignant melanoma', 'eFI2_Cancer'),
('malignant pleural mesothelioma', 'LUNGCAN_COD'),
('manic episode', 'MH_COD'),
('mantle cell lymphoma', 'HAEMCANMORPH_COD'),
('melanoma', 'eFI2_Cancer'),
('merkel cell carcinoma', 'C19CAN_COD'),
('migraine', 'eFI2_Headache'),
('motor neurone disease', 'MND_COD'),
('multiple myeloma', 'C19HAEMCAN_COD'),
('multiple sclerosis', 'MS_COD'),
('myelodysplastic', 'eFI2_AnaemiaEver'),
('myelofibrosis', 'MDS_COD'),
('myocardial infarction', 'eFI2_IschaemicHeartDisease'),
('myotonia', 'CNDATRISK2_COD'),
('narcolepsy', 'LD_COD'),
('neuroendocrine tumour', 'LUNGCAN_COD'),
('non-small cell lung cancer', 'LUNGCAN_COD'),
('non-small-cell lung cancer', 'FTCANREF_COD'),
('obesity', 'BMI30_COD'),
('osteoarthritis', 'CUST_ICB_OSTEOARTHRITIS'),
('osteoporosis', 'eFI2_Osteoporosis'),
('osteosarcoma', 'NHAEMCANMORPH_COD'),
('ovarian cancer', 'C19CAN_COD'),
('peripheral arterial disease', 'PADEXC_COD'),
('plaque psoriasis', 'PSORIASIS_COD'),
('polycystic kidney disease', 'EPPCONGMALF_COD'),
('polycythaemia vera', 'C19HAEMCAN_COD'),
('pregnancy', 'C19PREG_COD'),
('primary biliary cholangitis', 'eFI2_LiverProblems'),
('primary hypercholesterolaemia', 'FNFHYP_COD'),
('prostate cancer', 'EPPSOLIDCAN_COD'),
('psoriasis', 'PSORIASIS_COD'),
('psoriatic arthritis', 'RARTHAD_COD'),
('pulmonary embolism', 'eFI2_RespiratoryDiseaseTimeSensitive'),
('pulmonary fibrosis', 'ILD_COD'),
('relapsing multiple sclerosis', 'MS_COD'),
('renal cell carcinoma', 'C19CAN_COD'),
('renal transplantation', 'RENALTRANSP_COD'),
('retinal vein occlusion', 'CUST_ICB_VISUAL_IMPAIRMENT'),
('rheumatoid arthritis', 'eFI2_InflammatoryArthritis'),
('rivaroxaban', 'DOACCON_COD'),
('schizophrenia', 'MH_COD'),
('seizures', 'LSZFREQ_COD'),
('sepsis', 'C19ACTIVITY_COD'),
('severe persistent allergic asthma', 'SEVAST_COD'),
('sickle cell disease', 'SICKLE_COD'),
('sleep apnoea', 'CUST_ICB_NON_SEVERE_LDA'),
('smoking cessation', 'SMOKINGINT_COD'),
('soft tissue sarcoma', 'NHAEMCANMORPH_COD'),
('spinal muscular atrophy', 'MND_COD'),
('squamous cell', 'C19CAN_COD'),
('squamous cell carcinoma', 'C19CAN_COD'),
('stem cell transplant', 'ALLOTRANSP_COD'),
('stroke', 'eFI2_Stroke'),
('systemic lupus erythematosus', 'SLUPUS_COD'),
('systemic mastocytosis', 'HAEMCANMORPH_COD'),
('thrombocytopenic purpura', 'TTP_COD'),
('thrombotic thrombocytopenic purpura', 'TTP_COD'),
('thyroid cancer', 'C19CAN_COD'),
('tophaceous gout', 'CUST_ICB_OSTEOARTHRITIS'),
('transitional cell carcinoma', 'C19CAN_COD'),
('type 1 diabetes', 'DMTYPE1_COD'),
('type 2 diabetes', 'DMTYPE2_COD'),
('ulcerative colitis', 'eFI2_InflammatoryBowelDisease'),
('urothelial carcinoma', 'NHAEMCANMORPH_COD'),
('urticaria', 'XSAL_COD'),
('uveitis', 'CUST_ICB_VISUAL_IMPAIRMENT'),
('vascular disease', 'CVDINVITE_COD'),
('vasculitis', 'CRYOGLOBVASC_COD')
) AS t(Search_Term, Cluster_ID)
),
ClusterCodes AS (
SELECT
stc.Search_Term,
c."SNOMEDCode",
c."SNOMEDDescription"
FROM SearchTermClusters stc
JOIN DATA_HUB.PHM."ClinicalCodingClusterSnomedCodes" c
ON stc.Cluster_ID = c."Cluster_ID"
WHERE c."SNOMEDCode" IS NOT NULL
),
ExplicitCodes AS (
SELECT Search_Term, SNOMEDCode, SNOMEDDescription FROM (VALUES
('acute coronary syndrome', '837091000000100', 'Manual mapping'),
('ankylosing spondylitis', '162930007', 'Manual mapping'),
('ankylosing spondylitis', '239805001', 'Manual mapping'),
('ankylosing spondylitis', '239810002', 'Manual mapping'),
('ankylosing spondylitis', '239811003', 'Manual mapping'),
('ankylosing spondylitis', '394990003', 'Manual mapping'),
('ankylosing spondylitis', '429712009', 'Manual mapping'),
('ankylosing spondylitis', '441562009', 'Manual mapping'),
('ankylosing spondylitis', '441680005', 'Manual mapping'),
('ankylosing spondylitis', '441930001', 'Manual mapping'),
('axial spondyloarthritis', '723116002', 'Manual mapping'),
('choroidal neovascularisation', '380621000000102', 'Manual mapping'),
('choroidal neovascularisation', '733124000', 'Manual mapping')
) AS t(Search_Term, SNOMEDCode, SNOMEDDescription)
)
SELECT Search_Term, "SNOMEDCode" AS SNOMEDCode, "SNOMEDDescription" AS SNOMEDDescription
FROM ClusterCodes
UNION ALL
SELECT Search_Term, SNOMEDCode, SNOMEDDescription
FROM ExplicitCodes
ORDER BY Search_Term, SNOMEDCode;
Binary file not shown.
View File
@@ -26,7 +26,7 @@ from datetime import date, timedelta
from pathlib import Path
from typing import Optional
# Ensure src/ is on sys.path when run as `python -m cli.compute_trends`
# Ensure project root is on sys.path when run as `python -m cli.compute_trends`
_src_dir = str(Path(__file__).resolve().parent.parent)
if _src_dir not in sys.path:
sys.path.insert(0, _src_dir)
@@ -28,7 +28,7 @@ from datetime import datetime
from pathlib import Path
from typing import Optional
# Ensure src/ is on sys.path when run as `python -m cli.refresh_pathways`
# Ensure project root is on sys.path when run as `python -m cli.refresh_pathways`
_src_dir = str(Path(__file__).resolve().parent.parent)
if _src_dir not in sys.path:
sys.path.insert(0, _src_dir)
+18
View File
@@ -0,0 +1,18 @@
"""Resolve file paths for both development and PyInstaller frozen modes."""
import sys
from pathlib import Path
def get_resource_path(relative_path: str) -> Path:
"""Return absolute path to a bundled resource.
In frozen mode (PyInstaller), resolves from sys._MEIPASS.
In dev mode, resolves from the project root (2 parents up from this file:
core/resource_path.py → core → project root).
"""
if getattr(sys, "frozen", False):
base = Path(sys._MEIPASS)
else:
base = Path(__file__).resolve().parents[1]
return base / relative_path
+8 -4
View File
@@ -1,7 +1,10 @@
"""Dash application entry point with layout root and state stores."""
import sys
from dash import Dash, html, dcc
import dash_mantine_components as dmc
from core.resource_path import get_resource_path
from dash_app.components.header import make_header
from dash_app.components.sub_header import make_sub_header
from dash_app.components.sidebar import make_sidebar
@@ -12,10 +15,11 @@ from dash_app.components.modals import make_modals
from dash_app.components.trust_comparison import make_tc_landing, make_tc_dashboard
from dash_app.components.trends import make_trends_landing, make_trends_detail
app = Dash(
__name__,
suppress_callback_exceptions=True,
)
_app_kwargs = {"suppress_callback_exceptions": True}
if getattr(sys, "frozen", False):
_app_kwargs["assets_folder"] = str(get_resource_path("dash_app/assets"))
app = Dash(__name__, **_app_kwargs)
app.layout = dmc.MantineProvider(
children=[
+2 -8
View File
@@ -187,12 +187,12 @@ body {
margin-left: var(--sidebar-w);
margin-top: var(--header-total-h);
padding: 24px;
height: calc(100vh - var(--header-total-h));
min-height: calc(100vh - var(--header-total-h));
display: flex; flex-direction: column; gap: 20px;
overflow-y: auto;
}
/* View containers — flex chain for chart to fill height */
/* View containers */
#view-container {
flex: 1; display: flex; flex-direction: column; min-height: 0;
}
@@ -268,12 +268,6 @@ body {
#pathway-chart {
flex: 1; min-height: 0;
}
/* Propagate flex height into Plotly-rendered divs when no explicit figure height is set */
#pathway-chart .js-plotly-plot,
#pathway-chart .plot-container,
#pathway-chart .svg-container {
height: 100% !important;
}
.chart-card > .dash-loading-callback,
.chart-card > .dash-loading-callback > div {
flex: 1; display: flex; flex-direction: column; min-height: 0;
-2
View File
@@ -106,8 +106,6 @@ def make_chart_card():
children=[
dcc.Graph(
id="pathway-chart",
style={"flex": "1", "minHeight": "0"},
responsive=True,
config={
"displayModeBar": True,
"displaylogo": False,
+13
View File
@@ -18,6 +18,19 @@ def make_header():
],
),
# Demo banner
html.Div(
"SYNTHETIC DATA FOR DEMONSTRATION",
style={
"color": "#e53e3e",
"fontWeight": "bold",
"fontSize": "0.85rem",
"letterSpacing": "0.05em",
"textAlign": "center",
"whiteSpace": "nowrap",
},
),
# Center: 3 fraction KPIs (filtered / total)
html.Div(
className="top-header__kpis",
+4 -2
View File
@@ -46,7 +46,8 @@ def make_trends_landing():
dcc.Graph(
id="trends-overview-chart",
config={"displayModeBar": False, "displaylogo": False},
style={"height": "500px"},
style={"height": "calc(100vh - 220px)", "minHeight": "400px"},
responsive=True,
),
]),
],
@@ -105,7 +106,8 @@ def make_trends_detail():
dcc.Graph(
id="trends-detail-chart",
config={"displayModeBar": False, "displaylogo": False},
style={"height": "500px"},
style={"height": "calc(100vh - 220px)", "minHeight": "400px"},
responsive=True,
),
]),
],
+2 -3
View File
@@ -9,12 +9,11 @@ Also provides get_all_drugs() for the flat "All Drugs" card.
import csv
from collections import defaultdict
from pathlib import Path
from core.resource_path import get_resource_path
from data_processing.diagnosis_lookup import SEARCH_TERM_MERGE_MAP
DATA_DIR = Path(__file__).resolve().parents[2] / "data"
DIM_SEARCH_TERM_PATH = DATA_DIR / "DimSearchTerm.csv"
DIM_SEARCH_TERM_PATH = get_resource_path("data/DimSearchTerm.csv")
def build_directorate_tree() -> dict[str, dict[str, list[str]]]:
+2 -2
View File
@@ -5,9 +5,9 @@ Resolves the database path relative to this file's location and delegates
to the shared functions in src/data_processing/pathway_queries.py.
"""
from pathlib import Path
from typing import Optional
from core.resource_path import get_resource_path
from data_processing.pathway_queries import (
load_initial_data as _load_initial_data,
load_pathway_nodes as _load_pathway_nodes,
@@ -33,7 +33,7 @@ from data_processing.pathway_queries import (
get_trend_data as _get_trend_data,
)
DB_PATH = Path(__file__).resolve().parents[2] / "data" / "pathways.db"
DB_PATH = get_resource_path("data/pathways.db")
def load_initial_data() -> dict:
BIN
View File
Binary file not shown.
@@ -24,6 +24,8 @@ Usage:
connector.close()
"""
from __future__ import annotations
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import date, datetime
+6
View File
@@ -0,0 +1,6 @@
services:
hcd-demo:
build: .
ports:
- "8050:8050"
restart: unless-stopped
-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
-144
View File
@@ -1,144 +0,0 @@
# Guardrails
Known failure patterns. Read EVERY iteration. Follow ALL of these rules.
If you discover a new failure pattern during your work, add it to this file.
---
## Backend Isolation
### Do NOT modify pipeline/analysis logic in src/
- **When**: Improving charts or adding analytics
- **Rule**: Do NOT change the logic in these files — they are the data pipeline and must stay as-is:
- `data_processing/pathway_pipeline.py`, `transforms.py`, `diagnosis_lookup.py` (matching/query logic)
- `analysis/pathway_analyzer.py`, `statistics.py`
- `cli/refresh_pathways.py`
- `data_processing/schema.py`, `reference_data.py`, `cache.py`, `data_source.py`
- **Why**: The pipeline is complete and tested. Changing it risks breaking the data refresh workflow.
### DO use shared utilities in src/ rather than duplicating
- **When**: Adding chart functions or query functions
- **Rule**: Chart figure functions go in `src/visualization/plotly_generator.py`. Query functions go in `src/data_processing/pathway_queries.py`. Dash callbacks should CALL INTO `src/`, not duplicate the code.
- **Why**: Duplicating SQL queries and figure logic creates copies that drift apart.
### Do NOT modify pathways.db schema or data
- **When**: Querying the database from Dash callbacks
- **Rule**: Read-only access. Use `sqlite3.connect(db_path)` with SELECT queries only. Never INSERT, UPDATE, DELETE, or ALTER.
- **Exception**: Phase D tasks (D.1 trends) may add new tables — this requires explicit planning.
- **Why**: pathways.db is populated by `python -m cli.refresh_pathways`. The Dash app is a read-only consumer.
---
## Chart Generation (plotly_generator.py)
### Use _base_layout() for all chart functions
- **When**: Modifying or creating any chart function after Task A.1
- **Rule**: Call `_base_layout(title)` to get shared layout properties, then update with chart-specific overrides. Do NOT hardcode font family, title font size, bgcolor, hoverlabel, or autosize in individual functions.
- **Why**: DRY principle. Inconsistent styling was a bug category (Tier 2 fix).
### Use module-level palette constants
- **When**: Assigning colors to traces in any chart function
- **Rule**: Use `TRUST_PALETTE` (7 colors) for trust-comparison charts where bars/traces represent trusts. Use `DRUG_PALETTE` (15 colors) for charts where bars/traces represent drugs. Do NOT define local `nhs_colours` lists.
- **Why**: Local blue-heavy palettes made trusts indistinguishable (a reported bug).
### Heatmaps must have cell text annotations
- **When**: Modifying `create_heatmap_figure()` or `create_trust_heatmap_figure()`
- **Rule**: Always include `text=text_values, texttemplate="%{text}"` on the heatmap trace. Format text per metric: patients → `"N"`, cost → `"£Nk"`, cost_pp_pa → `"£N"`.
- **Why**: Without cell text, users must hover every cell to read values — a reported usability bug.
### Heatmaps must use linear colorscale
- **When**: Setting colorscale on heatmap traces
- **Rule**: Use linear 5-stop colorscale: `[0.0 #E3F2FD, 0.25 #90CAF9, 0.5 #42A5F5, 0.75 #1E88E5, 1.0 #003087]`. Always set `zmin=0`. Do NOT use non-linear stops like `[0.01, 0.1, 0.3, ...]`.
- **Why**: Non-linear stops compressed 99% of the value range into identical blues.
### Charts must use autosize, not fixed width
- **When**: Setting chart dimensions
- **Rule**: Use `autosize=True` instead of explicit `width=...`. Dynamic height is fine (calculated from data). Use `yaxis automargin=True` instead of fixed left margins.
- **Why**: Fixed widths overflow their containers on different screen sizes.
### Legends must adapt to item count
- **When**: Setting legend layout on charts with variable trace counts
- **Rule**: Use `_smart_legend(n_items)` helper (once created in Task A.3). >15 items = vertical right legend. ≤15 items = horizontal with dynamic bottom margin.
- **Why**: Horizontal legends with 42 drugs wrap 5+ rows and overlap chart content.
---
## Callback Architecture
### No circular callback dependencies
- **When**: Writing Dash callbacks
- **Rule**: Callbacks must flow unidirectionally: filter inputs → `app-state` store → `chart-data` store → UI components. Never have a component that is both Input and Output in the same callback chain without an intermediate store.
- **Why**: Dash raises `DuplicateCallback` errors for circular dependencies.
### Use dcc.Store for all state, not server-side globals
- **When**: Managing application state
- **Rule**: ALL state lives in `dcc.Store` components. Never use module-level globals or class variables for state. The 4 stores: `app-state` (session), `chart-data` (memory), `reference-data` (session), `active-tab` (memory).
- **Why**: Dash is stateless per request. Server-side state breaks with multiple users.
### Only render the active tab's chart
- **When**: Building tab switching or chart rendering callbacks
- **Rule**: Check `active-tab` store and ONLY compute the figure for the active tab. Return `no_update` or placeholder for inactive tabs.
- **Why**: Computing all charts on every filter change would be extremely slow.
### Chart figure functions go in src/visualization/, not dash_app/
- **When**: Creating new chart figures
- **Rule**: Create figure builder functions in `src/visualization/plotly_generator.py`. Dash callbacks call these shared functions. Do NOT put Plotly figure construction logic directly in `dash_app/callbacks/`.
- **Why**: Shared figure functions can be tested independently and reused.
### New query functions use same pattern as existing ones
- **When**: Adding query functions to `src/data_processing/pathway_queries.py`
- **Rule**: Follow the same pattern as `load_pathway_nodes()`: accept `db_path` parameter, use `sqlite3.connect()` with `row_factory = sqlite3.Row`, parameterized queries, return JSON-serializable dicts/lists. Add thin wrappers in `dash_app/data/queries.py`.
- **Why**: Consistency with existing code. The thin wrapper pattern ensures DB path resolution is centralized.
---
## Data Patterns
### Use parameterized queries for all filters
- **When**: Building WHERE clauses with user-selected values
- **Rule**: Use `?` placeholders and pass params as a list. Never use f-strings or string interpolation for filter values.
- **Why**: Prevents SQL injection and handles special characters in drug/directory names (e.g., "CROHN'S DISEASE").
### Parsing utilities must handle missing/null data gracefully
- **When**: Parsing `average_spacing` HTML strings, `average_administered` JSON, or `ids` column values
- **Rule**: Always handle `None`, empty string `""`, and malformed data. Return sensible defaults rather than raising exceptions.
- **Why**: Not all nodes have statistics populated. Level 0-2 nodes have no drug-level statistics.
---
## Process Guardrails
### One task per iteration
- **When**: Temptation to do additional tasks after completing the current one
- **Rule**: Complete ONE task, validate it, commit it, update progress, then stop
- **Why**: Multiple tasks increase error risk and make failures harder to diagnose
### Never mark complete without validation
- **When**: Task feels "done" but hasn't been tested
- **Rule**: All validation tiers must pass before marking `[x]`
- **Why**: "Feels done" is not "is done"
### Write explicit handoff notes
- **When**: Every iteration, before stopping
- **Rule**: The "Next iteration should" section must contain specific, actionable guidance
- **Why**: The next iteration has zero memory. If you don't write it down, it's lost.
### Validate with `python run_dash.py`
- **When**: After completing any task
- **Rule**: Run `python run_dash.py` (or `python -c "from dash_app.app import app"` for import checks). The app must start without errors after EVERY task.
- **Why**: Broken imports or circular dependencies compound across tasks. Catch them immediately.
### Re-read plotly_generator.py before editing
- **When**: Starting any task that modifies chart functions
- **Rule**: Always re-read `src/visualization/plotly_generator.py` at the start of the iteration. Line numbers in IMPLEMENTATION_PLAN.md are approximate and shift as edits accumulate. Search for function names, not line numbers.
- **Why**: Previous iterations may have changed the file, shifting all line numbers.
<!--
ADD NEW GUARDRAILS BELOW as failures are observed during the loop.
Format:
### [Short descriptive name]
- **When**: What situation triggers this guardrail?
- **Rule**: What must you do (or not do)?
- **Why**: What failure prompted adding this guardrail?
-->
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 885 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

-876
View File
@@ -1,876 +0,0 @@
# Progress Log — Dashboard Visualization Improvements
## Project Context
Working Dash application with 2 views (Patient Pathways + Trust Comparison), 13 chart functions in `plotly_generator.py`, and a complete callback chain. Now improving chart quality: bug fixes, visual polish, and new analytics.
**Current state**: Fully functional Dash app at http://localhost:8050 with icicle, Sankey, market share, cost effectiveness, cost waterfall, dosing, heatmap, and duration charts. Trust Comparison has 6 dedicated charts. All filters work.
**New goal**: Fix chart bugs (heatmap colorscale, legend overflow, trust color differentiation), add visual polish (consistent styling, smooth gradients), add new analytics (retention funnel, pathway depth, scatter, network), and new backend analytics (trends, dose distribution, timeline, NICE compliance).
## Key Architecture Patterns
### plotly_generator.py (PRIMARY target file)
- 13 chart functions, all accept list-of-dicts, return `go.Figure`
- Located at `src/visualization/plotly_generator.py` (~1782 lines)
- Key functions and approximate line numbers:
- `create_icicle_from_nodes(nodes, title)` — L113
- `create_market_share_figure(data, title)` — L247
- `create_cost_effectiveness_figure(data, retention, title)` — L384
- `create_cost_waterfall_figure(data, title)` — L562
- `create_sankey_figure(data, title)` — L706
- `create_dosing_figure(data, title, group_by)` — L837
- `_dosing_by_drug(data, colours)` — L926
- `_dosing_by_trust(data, colours)` — L1007
- `create_heatmap_figure(data, title, metric)` — L1189
- `create_duration_figure(data, title, show_directory)` — L1329
- `create_trust_market_share_figure(data, title)` — L1481
- `create_trust_heatmap_figure(data, title, metric)` — L1582
- `create_trust_duration_figure(data, title)` — L1689
- NOTE: Line numbers will shift as you edit. Re-read the file each iteration.
### Callback chain
- `dash_app/callbacks/chart.py` — Patient Pathways tab dispatch (`_render_*` helpers → `update_chart`)
- `dash_app/callbacks/trust_comparison.py` — 6 Trust Comparison chart callbacks
- Tab switching: `active-tab` dcc.Store, tab IDs = `"tab-{short_id}"`
- TAB_DEFINITIONS in `chart_card.py` — currently: icicle, sankey
### Adding a new Patient Pathways tab
1. Query function in `src/data_processing/pathway_queries.py` (accept `db_path` param)
2. Thin wrapper in `dash_app/data/queries.py` (resolve DB_PATH)
3. Figure function in `src/visualization/plotly_generator.py`
4. Add to `TAB_DEFINITIONS` in `dash_app/components/chart_card.py`
5. Add `_render_*()` helper in `dash_app/callbacks/chart.py`
6. Add elif case in `update_chart()` dispatch
### State management
- 4 `dcc.Store` components: app-state, chart-data, reference-data, active-tab
- Unidirectional: filter inputs → app-state → chart-data → UI
- 20 registered callbacks total
### DMC version
- Dash 4.0.0 + DMC 2.5.1 (Mantine v7 based)
- `dmc.MantineProvider` wraps layout
- `dmc.SegmentedControl` available for metric toggles
### Flex chain for chart filling viewport
- Full flex chain: `.main` → `#view-container` → `#patient-pathways-view` → `.chart-card` → loading wrapper → `#chart-container` → `#pathway-chart`
- `responsive=True` on dcc.Graph + `autosize=True` in figure layout
- `dcc.Loading` wraps children in `.dash-loading-callback > div` — CSS must propagate flex through both
### Known heatmap bugs (to fix)
- Non-linear colorscale compresses 99% of range into identical blues
- No cell text — must hover every cell
- Light end (#F0F4F8) invisible against transparent background
- Fixed width can overflow container
- Fixed l=200 left margin wastes space
### Known legend bugs (to fix)
- Horizontal legends at y=-0.15 with fixed bottom margins overflow with 42 drugs
- Affects: market_share, trust_market_share, dosing, trust_duration
### Known color bugs (to fix)
- First 6 of 10 trust palette colors are blue variants — nearly indistinguishable
- _dosing_by_drug interpolates from one blue to another blue
## Iteration Log
## Iteration 1 — 2026-02-07
### Task: A.1 — Extract shared styling constants + `_base_layout()` helper
### Why this task:
- A.1 is the foundation for all subsequent Phase A tasks (A.2-A.4 all reference `_base_layout()` and the palette constants). Must be done first.
### Status: COMPLETE
### What was done:
- Added 7 module-level constants after `logger` line: `CHART_FONT_FAMILY`, `CHART_TITLE_SIZE`, `CHART_TITLE_COLOR`, `GRID_COLOR`, `ANNOTATION_COLOR`, `TRUST_PALETTE` (7 colors), `DRUG_PALETTE` (15 colors)
- Created `_base_layout(title, **overrides)` helper returning dict with: title (font family/size/color, centered), hoverlabel (white bg, #CBD5E1 border, font), paper/plot bgcolor transparent, autosize=True, base font family
- Applied `_base_layout()` to `create_icicle_from_nodes()` — replaced 20-line explicit layout block with `_base_layout()` call + 3 overrides (margin, hoverlabel size=14, clickmode)
- Also replaced hardcoded `"Source Sans 3, system-ui, sans-serif"` in icicle textfont with `CHART_FONT_FAMILY` constant
### Validation results:
- Tier 1 (Code): all imports pass, `python run_dash.py` starts cleanly
- Tier 2 (Visual): icicle figure title correct ("Patient Pathways — By Directory"), font family/size/color all match expected values
### Files changed:
- `src/visualization/plotly_generator.py` — added constants + `_base_layout()` + refactored icicle layout
- `IMPLEMENTATION_PLAN.md` — marked A.1 subtasks [x]
### Committed: 63c1801 "feat: add shared styling constants and _base_layout() helper (Task A.1)"
### Patterns discovered:
- The `_base_layout()` returns a plain dict that gets unpacked via `fig.update_layout(**layout)`. Callers pass chart-specific overrides as kwargs.
- Icicle hoverlabel uses `size=14` (slightly larger than base `13`) — preserved as override.
- Constants are at module level, so all functions in the file can reference them directly.
- Line numbers in IMPLEMENTATION_PLAN.md are now stale (shifted ~70 lines due to constants/helper insertion). Future iterations should search by function name.
### Next iteration should:
- Start with Task A.2: Fix heatmap colorscale + cell annotations. Read `create_heatmap_figure()` and `create_trust_heatmap_figure()` functions by searching for their names (line numbers have shifted).
- The `_base_layout()` and palette constants are now available — A.2 should use `_base_layout()` for both heatmap functions.
- Key heatmap fixes: linear 5-stop colorscale, cell text annotations, zmin=0, autosize, automargin, subtitle when >25 drugs.
### Blocked items:
- None
## Iteration 2 — 2026-02-07
### Task: A.2 — Fix heatmap colorscale + cell annotations
### Why this task:
- A.2 is the next Phase A task after A.1. It depends on `_base_layout()` (done in A.1). It addresses the most-reported heatmap bugs: non-linear colorscale, missing cell text, fixed width overflow, fixed left margin.
### Status: COMPLETE
### What was done:
- **create_heatmap_figure()**: Replaced non-linear 7-stop colorscale with linear 5-stop `[0.0 #E3F2FD, 0.25 #90CAF9, 0.5 #42A5F5, 0.75 #1E88E5, 1.0 #003087]`. Added `text=text_values, texttemplate="%{text}"` with metric-specific formatting (patients: `N`, cost: `£Nk`, cost_pp_pa: `£N`). Set `zmin=0`. Removed fixed `width`, using `autosize=True` from `_base_layout()`. Replaced `l=200` with `l=8` + `yaxis automargin=True`. Added subtitle annotation when 25-drug cap is hit. Reduced `xgap/ygap` to 1 when >15 columns.
- **create_trust_heatmap_figure()**: Applied identical fixes. Also fixed Unicode escapes (`\u00a3`, `\u00d7`, `\u2014`) back to literal characters.
- Both functions now use `_base_layout(chart_title)` for consistent styling — title color is now `#1E293B` (CHART_TITLE_COLOR) instead of old hardcoded `#003087`, title size is 18 (CHART_TITLE_SIZE) for both (trust heatmap was 16 before).
### Validation results:
- Tier 1 (Code): imports pass, `uv run python -c "from dash_app.app import app"` OK, `uv run python run_dash.py` starts cleanly
- Tier 2 (Visual): Functional test confirms: zmin=0, text present, texttemplate="%{text}", 5-stop colorscale, autosize=True, yaxis automargin=True, title color #1E293B
### Files changed:
- `src/visualization/plotly_generator.py` — both heatmap functions rewritten
- `IMPLEMENTATION_PLAN.md` — marked A.2 subtasks [x]
### Committed: 56ca11e "fix: heatmap linear colorscale, cell annotations, autosize (Task A.2)"
### Patterns discovered:
- The `_base_layout()` helper makes chart function updates significantly easier — just `layout = _base_layout(title)` + `layout.update(...)` + `fig.update_layout(**layout)`. No need to repeat font/bgcolor/autosize.
- `textfont=dict(size=10)` on the heatmap trace controls cell annotation size — needs to be small enough not to overflow cells.
- Cost formatting: `£Nk` for cost ≥ 1000, `£N` for small values. This avoids `£125,000` flooding cells.
### Next iteration should:
- Start with Task A.3: Fix legend overflow in 4 charts. This requires creating `_smart_legend(n_items)` helper and applying it + `_base_layout()` to: `create_market_share_figure()`, `create_trust_market_share_figure()`, `create_dosing_figure()`, `create_trust_duration_figure()`.
- Search for these function names by `def create_market_share_figure` etc. — line numbers have shifted again.
- The `_smart_legend()` helper should: >15 items → vertical right legend; ≤15 → horizontal with dynamic bottom margin.
### Blocked items:
- None
## Iteration 3 — 2026-02-07
### Task: A.3 — Fix legend overflow in 4 charts
### Why this task:
- A.3 is the next Phase A task after A.1 and A.2. It has no blockers and was explicitly recommended by Iteration 2. It addresses the legend overflow bug reported for charts with 42 drugs.
### Status: COMPLETE
### What was done:
- **Created `_smart_legend(n_items, legend_title)` helper**: Returns legend dict with adaptive positioning. >15 items → vertical right legend (orientation="v", x=1.02, xanchor="left"). ≤15 items → horizontal below chart (orientation="h", y=-0.12, xanchor="center").
- **Created `_smart_legend_margin(n_items)` helper**: Returns margin dict. >15 items → r=140, b=40. ≤15 items → dynamic b based on estimated row count (~6 items per row), r=24.
- **`create_market_share_figure()`**: Replaced local `nhs_colours` with `DRUG_PALETTE`. Replaced manual layout block with `_base_layout()` + `_smart_legend()`. Replaced hardcoded `GRID_COLOR` reference.
- **`create_trust_market_share_figure()`**: Same treatment. Also replaced Unicode escapes (`\u00a3`, `\u2014`) with literal characters (`£`, `—`).
- **`create_dosing_figure()`**: Replaced local `nhs_colours` with `DRUG_PALETTE`. Legend adapts to trace count using `sum(1 for t in fig.data if t.showlegend is not False)`. Uses `_base_layout()`.
- **`create_trust_duration_figure()`**: Replaced local `nhs_colours` with `TRUST_PALETTE` (7 maximally-distinct colors — also satisfies A.4 subtask). Fixed `l=200` → `l=8` + automargin. Uses `_base_layout()`.
- **Note**: This also completed two A.4 subtasks: `create_trust_duration_figure()` now uses `TRUST_PALETTE`, and `create_trust_market_share_figure()` now uses `DRUG_PALETTE`.
### Validation results:
- Tier 1 (Code): All imports pass. `from dash_app.app import app` OK. `python run_dash.py` starts cleanly, HTTP 200.
- Tier 2 (Visual): Functional tests confirm: 20-drug chart → vertical right legend; 5-drug → horizontal; 7-trust → horizontal with TRUST_PALETTE colors [#005EB8, #DA291C, #009639, #ED8B00, #7C2855, #00A499, #330072].
- Tier 3 (Functional): All 4 chart functions produce correct figures with sample data. Legend orientation switches at 15-item threshold.
### Files changed:
- `src/visualization/plotly_generator.py` — added `_smart_legend()` + `_smart_legend_margin()`, refactored 4 chart functions
- `IMPLEMENTATION_PLAN.md` — marked A.3 subtasks [x], updated A.4 for completed subtasks
### Committed: 90de24c "fix: adaptive legends + _base_layout for 4 chart functions (Task A.3)"
### Patterns discovered:
- Plotly trace.showlegend defaults to None (not True), so checking `t.showlegend is not False` correctly identifies visible legend items.
- The `_smart_legend_margin()` helper returns only `b` and `r` keys, allowing callers to merge with their own `t` and `l` via `dict(t=60, l=8, **legend_margins)`.
- Net code reduction of 48 lines across 4 functions by replacing repeated layout blocks with `_base_layout()`.
- Several A.4 subtasks were completed opportunistically during A.3 (replacing nhs_colours with palette constants).
### Next iteration should:
- Start with Task A.4: Fix remaining trust comparison color differentiation. Two subtasks remain:
1. Add `is_trust_comparison=False` param to `create_cost_waterfall_figure()` — use `TRUST_PALETTE` when True. Update `tc_cost_waterfall` callback to pass `is_trust_comparison=True`.
2. Fix `_dosing_by_drug()` blue→blue interpolation: replace with `plotly.colors.sample_colorscale("Viridis", ...)` for meaningful gradient.
- The `create_trust_duration_figure()` TRUST_PALETTE fix and `create_trust_market_share_figure()` DRUG_PALETTE fix are already done (marked [x] in plan).
- Search for `create_cost_waterfall_figure` and `_dosing_by_drug` by function name (line numbers have shifted).
### Blocked items:
- None
## Iteration 4 — 2026-02-07
### Task: A.4 — Fix trust comparison color differentiation (remaining 3 subtasks)
### Why this task:
- A.4 is the last Phase A task. A.1-A.3 are complete. Two A.4 subtasks were already done opportunistically in A.3. The remaining 3 subtasks complete Phase A.
### Status: COMPLETE
### What was done:
- **`create_cost_waterfall_figure()`**: Added `is_trust_comparison=False` parameter. When True, uses `TRUST_PALETTE` (7 distinct colors) instead of `DRUG_PALETTE`. Replaced local `nhs_colours` list. Applied `_base_layout()` — removed 20+ lines of hardcoded layout. Updated annotation fonts to use `ANNOTATION_COLOR` and `CHART_FONT_FAMILY` constants. Used `GRID_COLOR` constant for gridcolor.
- **`tc_cost_waterfall` callback**: Updated call to pass `is_trust_comparison=True`, so Trust Comparison cost waterfall now uses 7 maximally-distinct trust colors.
- **`_dosing_by_drug()`**: Replaced manual RGB interpolation (blue `#005EB8` → blue `#41B6E6`) with `plotly.colors.sample_colorscale("Viridis", ratios)`. Result: bars now range from yellow (high interval) through teal to purple (low interval) — clearly distinguishable.
### Validation results:
- Tier 1 (Code): `from dash_app.app import app` OK. `python run_dash.py` starts cleanly, HTTP 200.
- Tier 2 (Visual): Cost waterfall normal mode uses DRUG_PALETTE colors (#005EB8, #DA291C, #009639...). Trust mode uses TRUST_PALETTE. Dosing by drug uses Viridis: rgb(253,231,37), rgb(34,144,140), rgb(59,81,138) — visually distinct.
- Tier 3 (Functional): is_trust_comparison=False (default) preserves existing behavior. is_trust_comparison=True switches to TRUST_PALETTE. Viridis sampling produces correct gradients.
### Files changed:
- `src/visualization/plotly_generator.py` — `create_cost_waterfall_figure()` + `_dosing_by_drug()`
- `dash_app/callbacks/trust_comparison.py` — `tc_cost_waterfall` callback
- `IMPLEMENTATION_PLAN.md` — marked A.4 subtasks [x]
### Committed: 950d93b "fix: trust palette for cost waterfall + Viridis dosing gradient (Task A.4)"
### Patterns discovered:
- `plotly.colors.sample_colorscale("Viridis", ratios)` returns a list of `rgb(r,g,b)` strings — can be passed directly to `marker.color` as a list. Very clean replacement for manual interpolation.
- The `_base_layout()` + `layout.update()` pattern removed ~30 lines from `create_cost_waterfall_figure()` (net -33 lines in diff). Each function converted gets simpler.
- Phase A is now COMPLETE. All 4 tasks (A.1-A.4) are done.
### Next iteration should:
- Start with Task B.1: Fix title inconsistencies across all charts. This requires:
1. Apply `_base_layout()` to remaining unconverted chart functions: `create_sankey_figure()`, `create_cost_effectiveness_figure()`, `create_duration_figure()`
2. These functions still have hardcoded `#003087` title colors and `"Source Sans 3"` font strings
3. Some Trust Comparison functions may still use `size=16` instead of `CHART_TITLE_SIZE` (18)
4. Search for `def create_sankey_figure`, `def create_cost_effectiveness_figure`, `def create_duration_figure` to find current line numbers
- After B.1, the next tasks are B.2 (cost effectiveness gradient) and B.3 (Sankey narrow-screen fix) — both are small and independent.
### Blocked items:
- None
## Iteration 5 — 2026-02-07
### Task: B.1 — Fix title inconsistencies across all charts
### Why this task:
- B.1 is the first Phase B task. Phase A is complete. Progress.txt from Iteration 4 explicitly recommended B.1 next. It ensures all chart functions use consistent styling via `_base_layout()`.
### Status: COMPLETE
### What was done:
- **`create_sankey_figure()`**: Replaced local `nhs_colours` (15 blue-heavy colors) with `DRUG_PALETTE`. Replaced 20-line hardcoded layout block (title color `#003087`, manual font/bgcolor) with `_base_layout()` call + 2 overrides (font size 12, margin/height).
- **`create_cost_effectiveness_figure()`**: Replaced 38-line manual layout block (title, xaxis, yaxis, margin, bgcolor, hoverlabel, font) with `_base_layout()` + 5-key update. Replaced hardcoded annotation font strings with `ANNOTATION_COLOR` and `CHART_FONT_FAMILY` constants. Replaced `gridcolor="#E2E8F0"` with `GRID_COLOR`.
- **`create_duration_figure()`**: Replaced 30-line manual layout (title color `#003087`, l=200 fixed margin, manual bgcolor/font) with `_base_layout()` + 6-key update. Fixed `margin.l` from 200 → 8 + `yaxis automargin=True`. Replaced hardcoded annotation font with constants. Used `ANNOTATION_COLOR` in subtitle HTML span.
- Net result: -52 lines (24 added, 76 removed). All 11 chart functions now use `_base_layout()`.
### Validation results:
- Tier 1 (Code): `from dash_app.app import app` OK. `python run_dash.py` starts cleanly, no errors.
- Tier 2 (Visual): All three functions produce figures with title color `#1E293B`, title size 18, font family `Source Sans 3, system-ui, sans-serif`, transparent bgcolor. Duration uses automargin instead of fixed l=200.
### Files changed:
- `src/visualization/plotly_generator.py` — converted 3 remaining chart functions to `_base_layout()`
- `IMPLEMENTATION_PLAN.md` — marked B.1 subtasks [x]
### Committed: 8d05adc "fix: consistent titles via _base_layout() for Sankey, Cost Effectiveness, Duration (Task B.1)"
### Patterns discovered:
- All 11 chart functions in plotly_generator.py now use `_base_layout()`. No more hardcoded `"Source Sans 3"` strings or `#003087` title colors exist outside of constants/colorscale definitions.
- Sankey's local `nhs_colours` was different from `DRUG_PALETTE` (had more blue variants like `#003087`, `#41B6E6`, `#0066CC`). Replacing with `DRUG_PALETTE` gives more distinguishable drug colors.
- The `_base_layout()` conversion is a reliable pattern: replace the entire `fig.update_layout(...)` call with `layout = _base_layout(title)` + `layout.update({chart-specific})` + `fig.update_layout(**layout)`. Removes ~15-30 lines per function.
### Next iteration should:
- Choose Task B.2 (cost effectiveness smooth gradient) or B.3 (Sankey narrow-screen fix) — both are small, independent tasks with no blockers.
- B.2: In `create_cost_effectiveness_figure()`, replace the 3-bin hard threshold (green/amber/red at 0.33/0.66) with smooth RGB interpolation. Green (#009639) → Amber (#ED8B00) at ratio 00.5, Amber → Red (#DA291C) at 0.51.0.
- B.3: In `create_sankey_figure()`, change `arrangement="snap"` → `arrangement="freeform"` and increase `pad` from 20 → 25.
- Both are quick changes — could potentially do B.2+B.3 together if scoped carefully, but the loop rules say one task per iteration.
### Blocked items:
- None
## Iteration 6 — 2026-02-07
### Task: B.2 — Cost effectiveness smooth gradient
### Why this task:
- B.2 is the next Phase B task after B.1. Iteration 5 explicitly recommended B.2 or B.3. B.2 addresses the crude 3-bin color threshold in the cost effectiveness lollipop chart.
### Status: COMPLETE
### What was done:
- **`create_cost_effectiveness_figure()`**: Replaced 3-bin hard threshold color assignment (green if ratio<0.33, amber if <0.66, red otherwise) with a smooth `_lerp_color()` inner function that does linear RGB interpolation:
- Ratio 00.5: Green (#009639) → Amber (#ED8B00)
- Ratio 0.51.0: Amber (#ED8B00) → Red (#DA291C)
- Includes clamping to [0,1] for safety
- Net change: +9 lines (replaced 8-line loop with 17-line function + 1-line list comprehension)
- `_base_layout()` was already applied in B.1 — no further layout changes needed
### Validation results:
- Tier 1 (Code): `from dash_app.app import app` OK. `python run_dash.py` starts cleanly, HTTP 200.
- Tier 2 (Visual): Functional test confirmed 5 gradient points: 1000→rgb(0,150,57) green, 3000→rgb(118,144,28) yellow-green, 5000→rgb(237,139,0) amber, 7000→rgb(227,90,14) orange-red, 9000→rgb(218,41,28) red. Smooth interpolation verified.
### Files changed:
- `src/visualization/plotly_generator.py` — replaced 3-bin color logic with `_lerp_color()` smooth gradient
- `IMPLEMENTATION_PLAN.md` — marked B.2 subtasks [x]
### Committed: cbac37e "fix: smooth green→amber→red gradient for cost effectiveness chart (Task B.2)"
### Patterns discovered:
- Inner function `_lerp_color()` defined inside `create_cost_effectiveness_figure()` keeps it scoped to where it's used. If other charts need similar gradients, could promote to module level.
- The data is reversed before coloring (line 486), so colors are assigned based on position in the reversed list. But since `_lerp_color` uses the actual cost value via `(c - min_cost) / cost_range`, the reversal doesn't affect color correctness.
### Next iteration should:
- Do Task B.3: Sankey narrow-screen fix. Very small change:
1. Search for `def create_sankey_figure` to find the function
2. Change `arrangement="snap"` → `arrangement="freeform"`
3. Increase `pad` from 20 → 25
- After B.3, Task B.4 (heatmap metric toggle) is the last Phase B task — it's more involved (adding SegmentedControl components + callback wiring).
### Blocked items:
- None
## Iteration 7 — 2026-02-07
### Task: B.3 — Sankey narrow-screen fix
### Why this task:
- B.3 is the next uncompleted Phase B task. Iteration 6 explicitly recommended it. It's a small, focused change with no dependencies.
### Status: COMPLETE
### What was done:
- **`create_sankey_figure()`** (line 808): Changed `arrangement="snap"` → `arrangement="freeform"` so users can drag nodes freely on narrow screens. Increased `pad` from 20 → 25 for more spacing between nodes.
### Validation results:
- Tier 1 (Code): `from dash_app.app import app` OK. `python run_dash.py` starts cleanly.
- Tier 2 (Visual): Functional test confirms arrangement="freeform" and pad=25 on generated figure.
### Files changed:
- `src/visualization/plotly_generator.py` — Sankey arrangement + pad
- `IMPLEMENTATION_PLAN.md` — marked B.3 [x]
### Committed: fb30f5f "fix: Sankey freeform arrangement + increased padding (Task B.3)"
### Patterns discovered:
- `arrangement="freeform"` allows users to manually reposition Sankey nodes by dragging, which is useful when nodes overlap on narrow viewports. `"snap"` constrains nodes to columns.
### Next iteration should:
- Do Task B.4: Heatmap metric toggle (both views). This is the last Phase B task and is more involved:
1. Add `dmc.SegmentedControl` component next to Patient Pathways heatmap (id: `heatmap-metric-toggle`, visible only when heatmap tab active). Add to `dash_app/components/chart_card.py`.
2. Add `dmc.SegmentedControl` next to Trust Comparison heatmap (id: `tc-heatmap-metric-toggle`). Add to `dash_app/components/trust_comparison.py`.
3. Update `_render_heatmap()` in `dash_app/callbacks/chart.py` to read the metric toggle value.
4. Update `tc_heatmap` callback in `dash_app/callbacks/trust_comparison.py` to read the metric toggle value.
5. Both heatmap functions (`create_heatmap_figure`, `create_trust_heatmap_figure`) already accept a `metric` parameter — the toggle just needs to pass the selected metric through.
- Key files to read: `dash_app/components/chart_card.py` (for PP toggle placement), `dash_app/components/trust_comparison.py` (for TC toggle placement), `dash_app/callbacks/chart.py` (for `_render_heatmap`), `dash_app/callbacks/trust_comparison.py` (for `tc_heatmap`).
### Blocked items:
- None
## Iteration 8 — 2026-02-07
### Task: B.4 — Heatmap metric toggle (both views)
### Why this task:
- B.4 is the last Phase B task. B.1-B.3 are complete. Iteration 7 explicitly recommended B.4. It requires adding `dmc.SegmentedControl` components and wiring them into callbacks.
### Status: COMPLETE
### What was done:
- **chart_card.py**: Added "heatmap" to `TAB_DEFINITIONS` (was only in `ALL_TAB_DEFINITIONS`). Added `dmc.SegmentedControl` (id: `heatmap-metric-toggle`) in `heatmap-metric-wrapper` div inside chart card header, hidden by default (`display: none`).
- **trust_comparison.py**: Replaced generic `_tc_chart_cell` for heatmap with custom inline layout containing `dmc.SegmentedControl` (id: `tc-heatmap-metric-toggle`) in a flex row next to the title.
- **chart.py**: Updated `_render_heatmap()` to accept `metric` parameter (default "patients"). Updated `update_chart` callback to:
- Add `Output("heatmap-metric-wrapper", "style")` — controls toggle visibility
- Add `Input("heatmap-metric-toggle", "value")` — reads metric selection
- Show toggle when `active_tab == "heatmap"`, hide otherwise
- Pass metric to `_render_heatmap()` when heatmap tab is active
- **trust_comparison.py callbacks**: Updated `tc_heatmap` callback to add `Input("tc-heatmap-metric-toggle", "value")` and pass `metric=heatmap_metric` to `create_trust_heatmap_figure()`.
- Both `create_heatmap_figure()` and `create_trust_heatmap_figure()` already accept `metric` param — no changes needed in plotly_generator.py.
### Validation results:
- Tier 1 (Code): `from dash_app.app import app` passes. `python run_dash.py` starts cleanly, HTTP 200.
- Tier 2 (Visual): N/A — toggle components rendered in layout, heatmap functions already tested in A.2.
- Tier 3 (Functional): Callback wiring verified — `update_chart` has 3 Outputs (figure, subtitle, toggle style) and 4 Inputs (chart-data, active-tab, app-state, heatmap-metric-toggle). TC heatmap callback has 2 Inputs (app-state, tc-heatmap-metric-toggle). Toggle options: patients, cost, cost_pp_pa.
### Files changed:
- `dash_app/components/chart_card.py` — added heatmap to TAB_DEFINITIONS, added SegmentedControl
- `dash_app/components/trust_comparison.py` — custom heatmap cell with SegmentedControl
- `dash_app/callbacks/chart.py` — updated _render_heatmap + update_chart callback
- `dash_app/callbacks/trust_comparison.py` — updated tc_heatmap callback
- `IMPLEMENTATION_PLAN.md` — marked B.4 subtasks [x]
### Committed: 8e2e2b7 "feat: heatmap metric toggle for both PP and TC views (Task B.4)"
### Patterns discovered:
- Adding a new Output to an existing callback (e.g., `heatmap-metric-wrapper` style) requires updating ALL return paths in that callback to include the new output value. The toggle_style is computed once at the top and returned in every code path.
- `dmc.SegmentedControl` with `size="xs"` fits nicely in chart headers without overwhelming the title.
- The heatmap tab was missing from PP `TAB_DEFINITIONS` — only in `ALL_TAB_DEFINITIONS`. Adding it to `TAB_DEFINITIONS` automatically creates the tab button and wires it into `switch_tab` because `_TAB_IDS` is built dynamically.
- Phase B is now COMPLETE. All 4 tasks (B.1-B.4) are done.
### Next iteration should:
- Start Phase C: New Analytics (Existing Data). Task C.1 (Retention funnel chart) is the first task.
- C.1 requires the 6-step pattern for adding a new chart tab:
1. Create `get_retention_funnel()` in `src/data_processing/pathway_queries.py` — query level 4+ nodes, aggregate by treatment line depth
2. Add thin wrapper in `dash_app/data/queries.py`
3. Create `create_retention_funnel_figure(data, title)` in `src/visualization/plotly_generator.py` — use `go.Funnel`
4. Add "Funnel" tab to `TAB_DEFINITIONS` in `chart_card.py`
5. Add `_render_funnel()` helper in `dash_app/callbacks/chart.py`
6. Add elif case in `update_chart()` dispatch
- Key: The query should count patients at each treatment line depth (1-drug, 2-drug, 3-drug, etc.) using the level column in pathway_nodes. Level 3 = 1st drug, level 4 = 2-drug pathway, etc.
- Use `_base_layout()` and NHS blue gradient for the funnel figure.
### Blocked items:
- None
## Iteration 9 — 2026-02-07
### Task: C.1 — Retention funnel chart
### Why this task:
- C.1 is the first Phase C task. Phases A and B are complete. Iteration 8 explicitly recommended C.1. It follows the 6-step pattern for adding a new chart tab.
### Status: COMPLETE
### What was done:
- **`get_retention_funnel()`** in `pathway_queries.py`: Queries level 3+ nodes grouped by level, aggregates patient counts. Level 3→depth 1 (1st drug), level 4→depth 2 (2nd drug), etc. Supports directory/trust filters. Returns list of dicts with depth, label, patients, pct.
- **Thin wrapper** in `dash_app/data/queries.py`: Imports and delegates to shared function with DB_PATH resolution.
- **`create_retention_funnel_figure()`** in `plotly_generator.py`: Uses `go.Funnel` with NHS blue gradient (#003087→#005EB8→#1E88E5). Text inside bars shows "N patients (X%)". Uses `_base_layout()` for consistent styling. Dynamic height based on depth count.
- **TAB_DEFINITIONS**: Added `("funnel", "Funnel")` — now 4 tabs: Icicle, Sankey, Heatmap, Funnel.
- **`_render_funnel()`** in `chart.py`: Reads filter state, calls query, passes to figure function. Handles empty data and errors.
- **Dispatch case**: Added `elif active_tab == "funnel"` in `update_chart()`.
### Validation results:
- Tier 1 (Code): `from dash_app.app import app` OK. `python run_dash.py` starts cleanly, HTTP 200.
- Tier 2 (Visual): Funnel shows 3 levels: 1st drug (10,819 patients, 100%), 2nd drug (2,142, 19.8%), 3rd drug (176, 1.6%). NHS blue gradient applied.
- Tier 3 (Functional): Responds to directory filter (RHEUMATOLOGY: 3,448→551→50). Works with indication chart type (10,782→1,519→125). Returns empty figure for nonexistent directory. Tab switching wired via dynamic `_TAB_IDS`.
### Files changed:
- `src/data_processing/pathway_queries.py` — added `get_retention_funnel()`
- `dash_app/data/queries.py` — added import + thin wrapper
- `src/visualization/plotly_generator.py` — added `create_retention_funnel_figure()`
- `dash_app/components/chart_card.py` — added funnel to TAB_DEFINITIONS
- `dash_app/callbacks/chart.py` — added `_render_funnel()` + dispatch case
- `IMPLEMENTATION_PLAN.md` — marked C.1 subtasks [x]
### Committed: a6cf6ef "feat: retention funnel chart tab with treatment line depth (Task C.1)"
### Patterns discovered:
- The 6-step pattern works cleanly: query → wrapper → figure → tab def → render helper → dispatch. No surprises.
- Level 3 sum (10,819) ≈ root value (11,118) — difference is due to minimum_patients thresholds. Close enough for funnel percentages.
- `go.Funnel` positions text automatically. `textposition="inside"` with white text on dark NHS blue is readable.
- Funnel connector lines use `GRID_COLOR` for visual consistency with other charts.
### Next iteration should:
- Do Task C.2: Pathway depth distribution chart. Same 6-step pattern.
1. Create `get_pathway_depth_distribution()` in `pathway_queries.py` — aggregate patients who STOPPED at each depth (not cumulative like funnel, but exclusive)
2. Key difference from funnel: subtract child counts. Patients at depth 1 only = level 3 total - level 4 total. Patients at depth 2 only = level 4 total - level 5 total.
3. Create `create_pathway_depth_figure(data, title)` — horizontal bar chart with NHS blue gradient
4. Add "Depth" tab to TAB_DEFINITIONS (will be 5th tab)
5. Wire callback helpers
- The query logic for "stopped at depth N" is: patients_at_level_N - patients_at_level_(N+1). The last level has no children so stopped = total.
### Blocked items:
- None
## Iteration 10 — 2026-02-07
### Task: C.2 — Pathway depth distribution chart
### Why this task:
- C.2 is the next Phase C task after C.1. Iteration 9 explicitly recommended it. Same 6-step pattern as C.1.
### Status: COMPLETE
### What was done:
- **`get_pathway_depth_distribution()`** in `pathway_queries.py`: Queries level 3+ nodes grouped by level, then subtracts next-level counts to get exclusive "stopped at depth N" patients. Supports directory/trust filters. Returns list of dicts with depth, label, patients, pct.
- **Thin wrapper** in `dash_app/data/queries.py`: Imports and delegates with DB_PATH resolution.
- **`create_pathway_depth_figure()`** in `plotly_generator.py`: Horizontal bar chart (`go.Bar` with `orientation="h"`) with NHS blue gradient (#003087→#005EB8→#1E88E5). Text inside bars shows "N (pct%)". Uses `_base_layout()`. Dynamic height based on depth count. Y-axis reversed so depth 1 (most patients) is at top.
- **TAB_DEFINITIONS**: Added `("depth", "Depth")` — now 5 tabs: Icicle, Sankey, Heatmap, Funnel, Depth.
- **`_render_depth()`** in `chart.py`: Reads filter state, calls query, passes to figure function. Handles empty data and errors.
- **Dispatch case**: Added `elif active_tab == "depth"` in `update_chart()`.
### Validation results:
- Tier 1 (Code): `from dash_app.app import app` OK. `python run_dash.py` starts cleanly. 20 callbacks registered.
- Tier 2 (Visual): Depth chart shows 3 levels: 1 drug only (8,677, 80.2%), 2 drugs only (1,966, 18.2%), 3 drugs only (176, 1.6%). NHS blue gradient applied. Autosize + automargin.
- Tier 3 (Functional): Directory filter works (RHEUMATOLOGY: 2,897/501/50). Indication chart type works (9,263/1,394/125). Empty data returns empty figure. Tab switching wired via dynamic `_TAB_IDS`.
### Files changed:
- `src/data_processing/pathway_queries.py` — added `get_pathway_depth_distribution()`
- `dash_app/data/queries.py` — added import + thin wrapper
- `src/visualization/plotly_generator.py` — added `create_pathway_depth_figure()`
- `dash_app/components/chart_card.py` — added depth to TAB_DEFINITIONS
- `dash_app/callbacks/chart.py` — added `_render_depth()` + dispatch case
- `IMPLEMENTATION_PLAN.md` — marked C.2 subtasks [x]
### Committed: 55c9af2 "feat: pathway depth distribution chart tab (Task C.2)"
### Patterns discovered:
- The depth calculation (cumulative - next level) is simple: iterate pairs and subtract. Total of exclusive counts equals the level 3 total, confirming correctness (8,677 + 1,966 + 176 = 10,819 = funnel total).
- `autorange="reversed"` on yaxis puts depth 1 at the top of the horizontal bar chart, matching the natural reading order (most patients first).
- The 6-step pattern continues to work cleanly for new tabs. Each step is small and independently verifiable.
### Next iteration should:
- Do Task C.3: Duration vs Cost scatter plot. Same 6-step pattern:
1. Create `get_duration_cost_scatter()` in `pathway_queries.py` — query level 3 nodes for drug-level data (drug, directory, avg_days, cost_pp_pa, patients)
2. Add thin wrapper in `queries.py`
3. Create `create_duration_cost_scatter_figure(data, title)` in `plotly_generator.py` — scatter: x=avg_days, y=cost_pp_pa, size=patients, color=directory. Add quadrant lines at median values.
4. Add "Scatter" tab to TAB_DEFINITIONS (6th tab)
5. Wire `_render_scatter()` + dispatch
- Key design decision: use `go.Scatter` with marker size proportional to patient count. Color by directory (use DRUG_PALETTE cycling or assign by directory). Quadrant lines use median avg_days and median cost_pp_pa as thresholds.
### Blocked items:
- None
## Iteration 11 — 2026-02-07
### Task: C.3 — Duration vs Cost scatter plot
### Why this task:
- C.3 is the next Phase C task after C.1 and C.2. Iteration 10 explicitly recommended it with design details. Same 6-step pattern.
### Status: COMPLETE
### What was done:
- **`get_duration_cost_scatter()`** in `pathway_queries.py`: Queries level 3 nodes with avg_days and cost_pp_pa, aggregates across trusts using weighted averages. Supports directory/trust filters. Returns list of dicts.
- **Thin wrapper** in `dash_app/data/queries.py`: Standard import + DB_PATH delegation.
- **`create_duration_cost_scatter_figure()`** in `plotly_generator.py`: `go.Scatter` with one trace per directory for legend grouping. Marker size proportional to patient count (global max for consistent sizing). DRUG_PALETTE for directory colors. Quadrant lines at median avg_days and median cost_pp_pa with annotations. Uses `_base_layout()` + `_smart_legend()`.
- **TAB_DEFINITIONS**: Added `("scatter", "Scatter")` — now 6 tabs: Icicle, Sankey, Heatmap, Funnel, Depth, Scatter.
- **`_render_scatter()`** in `chart.py`: Standard render helper with filter extraction and error handling.
- **Dispatch case**: Added `elif active_tab == "scatter"` in `update_chart()`.
### Validation results:
- Tier 1 (Code): `from dash_app.app import app` OK. `python run_dash.py` starts cleanly.
- Tier 2 (Visual): 59 data points across 12 directories. Days range 482237, cost range £994–£162k. Median quadrant lines at 928 days and £4,629. Marker sizes proportional (840px).
- Tier 3 (Functional): Directory filter works (RHEUMATOLOGY: 16 drugs). Indication chart type works (108 points). Empty data returns empty figure. Tab switching wired via dynamic `_TAB_IDS`.
### Files changed:
- `src/data_processing/pathway_queries.py` — added `get_duration_cost_scatter()`
- `dash_app/data/queries.py` — added import + thin wrapper
- `src/visualization/plotly_generator.py` — added `create_duration_cost_scatter_figure()`
- `dash_app/components/chart_card.py` — added scatter to TAB_DEFINITIONS
- `dash_app/callbacks/chart.py` — added `_render_scatter()` + dispatch case
- `IMPLEMENTATION_PLAN.md` — marked C.3 subtasks [x]
### Committed: d8df416 "feat: duration vs cost scatter plot tab (Task C.3)"
### Patterns discovered:
- `statistics.median()` imported inside the function to avoid module-level import for a stdlib module only used by one function.
- Marker size must use global max (not per-directory max) for consistent visual comparison across all directories. Initially coded per-directory, fixed before commit.
- `fig.add_hline()` and `fig.add_vline()` are the clean Plotly API for quadrant lines — they create shape objects and annotation objects automatically.
### Next iteration should:
- Do Task C.4: Drug switching network graph. This is the last Phase C task. Options from IMPLEMENTATION_PLAN.md:
1. Create `get_drug_network()` in `pathway_queries.py` — returns undirected edges (source, target, patients) and nodes (name, total_patients). Different from `get_drug_transitions()` which returns directed Sankey data.
2. Create `create_drug_network_figure(data, title)` using `go.Scatter` for circular layout nodes + edges as lines.
3. Add as separate "Network" tab or sub-toggle within Sankey tab.
4. The plan says "Add as sub-toggle within Sankey tab or as separate Network tab" — separate tab is simpler (follows established pattern).
- After C.4, Phase D begins (backend work: trends, dose distribution, timeline, NICE compliance).
### Blocked items:
- None
## Iteration 12 — 2026-02-07
### Task: C.4 — Drug switching network graph
### Why this task:
- C.4 is the last Phase C task. C.1-C.3 are complete. Iteration 11 explicitly recommended C.4. It follows the established 6-step pattern for adding a new chart tab.
### Status: COMPLETE
### What was done:
- **`get_drug_network()`** in `pathway_queries.py`: Queries level 4+ nodes for drug_sequence co-occurrence edges (undirected, sorted pairs to avoid duplicates). Also queries level 3 nodes for per-drug patient totals. Supports directory/trust filters. Returns `{nodes: [{name, total_patients}], edges: [{source, target, patients}]}`.
- **Thin wrapper** in `dash_app/data/queries.py`: Standard import + DB_PATH delegation.
- **`create_drug_network_figure()`** in `plotly_generator.py`: Circular layout using `math.cos/sin` for node positions. Individual `go.Scatter` traces for each edge (variable width 0.56px and opacity 0.150.7 scaled by patient count). Node scatter with `markers+text` mode, size 1250px proportional to patients, colors from `DRUG_PALETTE`. Uses `_base_layout()`. Axes hidden, `scaleanchor="y"` for square aspect ratio.
- **TAB_DEFINITIONS**: Added `("network", "Network")` — now 7 tabs: Icicle, Sankey, Heatmap, Funnel, Depth, Scatter, Network.
- **`_render_network()`** in `chart.py`: Standard render helper with filter extraction and error handling. Checks `data.get("nodes")` for empty state.
- **Dispatch case**: Added `elif active_tab == "network"` in `update_chart()`.
### Validation results:
- Tier 1 (Code): `from dash_app.app import app` OK. `python run_dash.py` starts cleanly, HTTP 200.
- Tier 2 (Visual): 39 drug nodes, 45 co-occurrence edges. Top connections: FARICIMAB↔RANIBIZUMAB (452), AFLIBERCEPT↔FARICIMAB (392), ADALIMUMAB↔ETANERCEPT (305). Figure has 46 traces (45 edges + 1 node scatter).
- Tier 3 (Functional): Directory filter works (RHEUMATOLOGY: 17 nodes, 20 edges). Indication chart type works (39 nodes, 28 edges). Empty data returns empty figure. Tab switching wired via dynamic `_TAB_IDS`.
### Files changed:
- `src/data_processing/pathway_queries.py` — added `get_drug_network()`
- `dash_app/data/queries.py` — added import + thin wrapper
- `src/visualization/plotly_generator.py` — added `create_drug_network_figure()`
- `dash_app/components/chart_card.py` — added network to TAB_DEFINITIONS
- `dash_app/callbacks/chart.py` — added `_render_network()` + dispatch case
- `IMPLEMENTATION_PLAN.md` — marked C.4 subtasks [x]
### Committed: 1405476 "feat: drug switching network graph tab (Task C.4)"
### Patterns discovered:
- Individual edge traces (one `go.Scatter` per edge) is necessary for variable width/opacity per edge. A single trace would only support uniform line properties.
- `scaleanchor="y", scaleratio=1` on xaxis ensures the circular layout is actually circular, not elliptical.
- The undirected edge approach (sort pair to canonical form) correctly deduplicates A→B and B→A transitions.
- Phase C is now COMPLETE. All 4 tasks (C.1-C.4) are done. 7 Patient Pathways tabs total.
### Next iteration should:
- Start Phase D: New Analytics (Backend Work). D.1 (Temporal trend analysis) is the first task.
- **Important**: D.1 requires schema changes (`pathway_trends` table) and CLI pipeline extension — this is backend work that has been avoided so far. Read guardrails.md carefully: DO NOT modify the existing pipeline logic, but adding new tables and extending refresh_pathways.py to compute trends is allowed per the plan.
- D.1 steps: (1) Design `pathway_trends` table schema in schema.py, (2) Add migration, (3) Extend refresh_pathways.py to insert trend snapshots, (4) Create query function, (5) Create figure function (line chart), (6) Add tab + callbacks.
- However: trends need at least 2 refresh cycles to show meaningful data. Consider whether D.1 is worth implementing vs. skipping to D.2 (dose distribution) or D.3 (drug timeline) which work with existing data.
- Alternative: D.3 (Drug timeline / Gantt chart) uses existing `first_seen`/`last_seen` data in pathway_nodes — no backend changes needed. Could be a better next task.
### Blocked items:
- None
## Iteration 13 — 2026-02-07
### Task: D.3 — Drug timeline (Gantt chart)
### Why this task:
- D.3 uses existing `first_seen`/`last_seen` data in pathway_nodes — no backend/schema changes needed. D.1 requires schema changes + pipeline extension (both in guardrail DO NOT MODIFY list) and needs ≥2 refresh cycles for meaningful data. D.2 requires parsing complex `average_administered` JSON. D.3 was the cleanest next task.
### Status: COMPLETE
### What was done:
- **`get_drug_timeline()`** in `pathway_queries.py`: Queries level 3 nodes aggregated across trusts — MIN(first_seen), MAX(last_seen), SUM(value), weighted avg cost_pp_pa per drug × directory. Supports directory/trust filters. Returns 59 entries for all-directory view.
- **Thin wrapper** in `dash_app/data/queries.py`: Standard import + DB_PATH delegation.
- **`create_drug_timeline_figure()`** in `plotly_generator.py`: Gantt-style using `go.Bar(orientation="h")` with `base` set to `first_seen` datetime and `x` as duration in milliseconds. One trace per bar, legend grouped by directory. Colors from `DRUG_PALETTE` (one color per directory). Patient count as white text inside bars. Hover shows drug, directory, first/last seen (month/year), duration in days, patients, cost p.a. Dynamic height (28px per bar). Uses `_base_layout()` + `_smart_legend()` + `_smart_legend_margin()`.
- **TAB_DEFINITIONS**: Added `("timeline", "Timeline")` — now 8 tabs: Icicle, Sankey, Heatmap, Funnel, Depth, Scatter, Network, Timeline.
- **`_render_timeline()`** in `chart.py`: Standard render helper with directory/trust filter extraction and error handling.
- **Dispatch case**: Added `elif active_tab == "timeline"` in `update_chart()`.
### Validation results:
- Tier 1 (Code): `from dash_app.app import app` OK. `python run_dash.py` starts cleanly, HTTP 200.
- Tier 2 (Visual): 59 data points across 12 directories. Date x-axis with 6-month ticks. Bars span 20192025. Newest drug DOCETAXEL (BREAST SURGERY) starts May 2025. Single-directory mode (RHEUMATOLOGY): 16 drugs, y-labels without directory suffix.
- Tier 3 (Functional): Directory filter works (RHEUMATOLOGY: 16 drugs). Trust filter works. Empty data returns empty figure. Tab switching wired via dynamic `_TAB_IDS`. 8 tabs visible.
### Files changed:
- `src/data_processing/pathway_queries.py` — added `get_drug_timeline()`
- `dash_app/data/queries.py` — added import + thin wrapper
- `src/visualization/plotly_generator.py` — added `create_drug_timeline_figure()`
- `dash_app/components/chart_card.py` — added timeline to TAB_DEFINITIONS
- `dash_app/callbacks/chart.py` — added `_render_timeline()` + dispatch case
- `IMPLEMENTATION_PLAN.md` — marked D.3 subtasks [x]
### Committed: 0a14f1f "feat: drug timeline Gantt chart tab (Task D.3)"
### Patterns discovered:
- Plotly `go.Bar` Gantt trick: set `base` to start datetime, `x` to duration in milliseconds (days × 86,400,000), `orientation="h"`. Plotly auto-detects date axis type from the datetime base values.
- `datetime.fromisoformat()` handles the `T00:00:00` suffix in ISO timestamps from SQLite without issue.
- Single-directory detection (`len(directories) == 1`) lets us simplify y-labels to just drug names, avoiding redundant "(RHEUMATOLOGY)" suffix when the user already filtered to that directory.
- With 59 bars, 12 directories → `_smart_legend()` uses horizontal mode (≤15 items), which works well since directory names aren't too long.
### Next iteration should:
- Choose between D.1 (Temporal trends), D.2 (Dose distribution), or D.4 (NICE TA compliance).
- **D.1** is problematic: requires modifying `schema.py` (guardrail protected), `reference_data.py` (guardrail protected), and `refresh_pathways.py` (guardrail protected). The plan allows it as an exception, but it also needs ≥2 refresh cycles for meaningful data. Consider marking D.1 as [B] (blocked on pipeline changes being out of scope).
- **D.2** (Dose distribution): Requires parsing `average_administered` JSON from pathway_nodes. Check if the data exists and is parseable first — run `SELECT average_administered FROM pathway_nodes WHERE average_administered IS NOT NULL AND average_administered != '' LIMIT 5` to inspect the format.
- **D.4** (NICE TA compliance): Requires parsing `data/ta-recommendations.xlsx` — check if this file exists and what it contains. This is also substantial (schema + migration + compliance scoring).
- Recommendation: Try D.2 next if `average_administered` data is available and parseable. If the JSON format is too complex or data is sparse, mark D.2 as [B] and assess D.4.
### Blocked items:
- D.1: Likely blocked — requires modifying guardrail-protected files (schema.py, reference_data.py, refresh_pathways.py) + needs multiple refresh cycles for meaningful data.
## Iteration 14 — 2026-02-07
### Task: D.2 — Average administered doses analysis
### Why this task:
- D.2 was explicitly recommended by Iteration 13. The `average_administered` JSON data exists (2031 rows, simple array format) and requires no schema changes. D.1 is blocked (guardrail-protected files). D.4 is complex (schema + migration + Excel parsing). D.2 was the cleanest ready task.
### Status: COMPLETE
### What was done:
- **`get_dosing_distribution()`** in `pathway_queries.py`: Queries level 3 nodes with `average_administered` JSON, parses position 0 (average dose count for the drug), aggregates across trusts using weighted averages by patient count. Supports directory/trust filters. Returns `[{drug, directory, avg_doses, patients}]`.
- **Thin wrapper** in `dash_app/data/queries.py`: Standard import + DB_PATH delegation.
- **`create_dosing_distribution_figure()`** in `plotly_generator.py`: Horizontal bar chart (`go.Bar` with `orientation="h"`) showing average administered doses per drug. One trace per bar with legend grouped by directory. Colors from `DRUG_PALETTE`. Dynamic height (24px per bar). `_base_layout()` + `_smart_legend()`. Hover shows drug, directory, avg doses, patients.
- **TAB_DEFINITIONS**: Added `("doses", "Doses")` — now 9 tabs: Icicle, Sankey, Heatmap, Funnel, Depth, Scatter, Network, Timeline, Doses.
- **`_render_doses()`** in `chart.py`: Standard render helper with directory/trust filter extraction and error handling.
- **Dispatch case**: Added `elif active_tab == "doses"` in `update_chart()`.
### Validation results:
- Tier 1 (Code): `from dash_app.app import app` OK. `uv run python run_dash.py` starts cleanly, HTTP 200.
- Tier 2 (Visual): 59 data points across 12 directories. Top: TOCILIZUMAB (RHEUMATOLOGY) avg 70.5 doses, INFLIXIMAB (OPHTHALMOLOGY) 47.7, EVOLOCUMAB (CHEMICAL PATHOLOGY) 46.6. Dynamic height 1536px for all, 504px for single directory.
- Tier 3 (Functional): Directory filter works (RHEUMATOLOGY: 16 drugs). Empty data returns empty figure. Tab switching wired via dynamic `_TAB_IDS`. 9 tabs visible.
### Files changed:
- `src/data_processing/pathway_queries.py` — added `get_dosing_distribution()`
- `dash_app/data/queries.py` — added import + thin wrapper
- `src/visualization/plotly_generator.py` — added `create_dosing_distribution_figure()`
- `dash_app/components/chart_card.py` — added doses to TAB_DEFINITIONS
- `dash_app/callbacks/chart.py` — added `_render_doses()` + dispatch case
- `IMPLEMENTATION_PLAN.md` — marked D.2 subtasks [x]
### Committed: c7e9398 "feat: average administered doses chart tab (Task D.2)"
### Patterns discovered:
- The `average_administered` column is a JSON array of floats (with NaN as string). Position 0 = average doses for the drug at that node. Level 3 nodes have only position 0, level 4 has positions 0+1, etc.
- `json.loads(s.replace("NaN", "null"))` safely handles NaN values in the JSON. Returns None for NaN positions.
- No need for a separate parsing function in `parsing.py` — the JSON parsing is simple enough to inline in the query function (3 lines).
- With 59 bars (one per drug×directory), the chart is readable with 24px per bar and legend grouped by directory.
### Next iteration should:
- Two Phase D tasks remain: D.1 (Temporal trends) and D.4 (NICE TA compliance).
- **D.1** is very likely blocked: requires modifying `schema.py`, `reference_data.py`, and `refresh_pathways.py` — all in the guardrails DO NOT MODIFY list. Also needs multiple refresh cycles. Recommend marking D.1 as [B] (blocked).
- **D.4** (NICE TA compliance): Check if `data/ta-recommendations.xlsx` exists and inspect its contents. This requires: (1) parsing the Excel into a reference table, (2) creating schema + migration (guardrail-protected), (3) compliance scoring algorithm, (4) figure function, (5) tab/callback wiring. Steps 1-2 are also likely blocked by guardrails.
- If both D.1 and D.4 are blocked, the project may be effectively complete for the current scope. Mark them [B] and check completion criteria.
- Alternatively: assess whether D.4 can be done without schema changes by keeping TA data in memory (load Excel directly from disk, no migration needed).
### Blocked items:
- D.1: Blocked — requires modifying guardrail-protected files + needs multiple refresh cycles.
- D.4: Potentially blocked — requires schema + migration for TA reference data (guardrail-protected files).
## Iteration 15 — 2026-02-07
### Task: D.1 + D.4 — Final assessment and project completion
### Why this task:
- Only D.1 and D.4 remain. Both were flagged as potentially blocked in Iteration 14. This iteration assesses their feasibility and makes a final determination.
### Status: COMPLETE (project finalization)
### What was done:
- **D.1 (Temporal trends)**: Confirmed BLOCKED. Requires modifying 3 guardrail-protected files (`schema.py`, `reference_data.py`, `refresh_pathways.py`) + needs ≥2 data refresh cycles to show meaningful trend data. Marked `[B]` in IMPLEMENTATION_PLAN.md.
- **D.4 (NICE TA compliance)**: Confirmed BLOCKED. `data/ta-recommendations.xlsx` does not exist — the source data file is missing entirely. Even if it existed, would require schema + migration changes to guardrail-protected files. Marked `[B]` in IMPLEMENTATION_PLAN.md.
- **Completion criteria**: Updated all achievable criteria to `[x]`. Phase D blocked items marked `[B]` with explanations.
- **Final validation**: `from dash_app.app import app` OK. `python run_dash.py` starts cleanly on http://127.0.0.1:8050/.
### Validation results:
- Tier 1 (Code): Import check passes, app starts without errors
### Files changed:
- `IMPLEMENTATION_PLAN.md` — marked D.1 and D.4 as [B] (blocked), updated all completion criteria
- `progress.txt` — this entry
### Summary of all completed work (14 iterations):
- **Phase A (4 tasks)**: Shared constants + `_base_layout()`, heatmap fixes (colorscale + annotations), legend overflow fix (4 charts), trust color differentiation
- **Phase B (4 tasks)**: Title consistency (all 11 charts), cost effectiveness gradient, Sankey freeform, heatmap metric toggle
- **Phase C (4 tasks)**: Retention funnel, pathway depth distribution, duration vs cost scatter, drug network graph
- **Phase D (2 of 4 tasks)**: Drug timeline Gantt chart, average administered doses chart
- **Total**: 14 tasks completed, 2 blocked, 9 Patient Pathways tabs, 15+ chart functions, all using shared styling
### Blocked items:
- D.1: BLOCKED — guardrail-protected file modifications required + needs ≥2 refresh cycles
- D.4: BLOCKED — source data file (`ta-recommendations.xlsx`) missing + guardrail-protected file modifications required
## Manual Intervention — 2026-02-07
### Reason: Unblock D.1 with historical snapshots approach, remove D.4
### Changes made:
- `IMPLEMENTATION_PLAN.md` — rewrote D.1 as two subtasks (D.1a: CLI script, D.1b: Dash tab), removed D.4 entirely, updated completion criteria
- `guardrails.md` — updated DB guardrail exception for trends CLI script, added new guardrail about using existing pipeline functions as-is
- `progress.txt` — this entry
### Tasks reset: None (D.1 was already blocked `[B]`, now unblocked as `[ ]`)
### Tasks added: None (D.1 rewritten in-place as D.1a + D.1b)
### Tasks removed: D.4 (NICE TA compliance — source data doesn't exist, not viable)
### Context for next iteration:
- D.1a is the next task: create `cli/compute_trends.py` standalone CLI script
- The script imports `fetch_and_transform_data()` and `process_pathway_for_date_filter()` from `pathway_pipeline.py` — calls them, does NOT modify them
- Creates `pathway_trends` table via `CREATE TABLE IF NOT EXISTS` in pathways.db
- Loops over 6-month endpoints (2021-06-30 through 2025-12-31) using `all_6mo` date filter config with different `max_date` values
- For each endpoint: run pathway computation, extract level 3 node summaries (drug, directory, patients, cost, cost_pp_pa)
- The key insight: `compute_date_ranges(config, max_date)` and `process_pathway_for_date_filter(df, config, ..., max_date=...)` already accept `max_date` as a parameter — no pipeline changes needed
- This requires Snowflake access (fetches all activity data once, then filters locally for each period)
- After D.1a, D.1b adds the Dash tab using the standard 6-step pattern
### New guardrails added:
- "Trend computation uses existing pipeline functions as-is" — import and call, don't modify
## Iteration 16 — 2026-02-07
### Task: D.1 — Temporal trend analysis (D.1a + D.1b combined)
### Why this task:
- D.1 is the only remaining task. D.1a (CLI script) and D.1b (Dash tab) are both needed to complete D.1. Manual intervention unblocked D.1 by rewriting it as a standalone CLI that imports existing pipeline functions without modifying them.
### Status: COMPLETE
### What was done:
- **D.1a — `cli/compute_trends.py`**: Standalone CLI script. Imports `fetch_and_transform_data()` and `process_pathway_for_date_filter()` from `pathway_pipeline.py`. Creates `pathway_trends` table via `CREATE TABLE IF NOT EXISTS`. Generates period endpoints from 2021-06-30 to 2025-12-31 at configurable intervals (default 6 months). For each endpoint: runs pathway computation with `max_date=endpoint`, extracts level 3 node summaries (drug, directory, patients, total_cost, cost_pp_pa). Supports `--dry-run`, `--start/--end`, `--interval`, `--verbose`.
- **D.1b — Trends tab (6-step pattern)**:
1. `get_trend_data()` in `pathway_queries.py` — checks table existence first, aggregates by drug (or directory), supports directory/drug filters, handles cost_pp_pa as weighted average
2. Thin wrapper in `dash_app/data/queries.py`
3. `create_trend_figure()` in `plotly_generator.py` — line chart with `go.Scatter` (lines+markers), one trace per drug/directory, `_base_layout()` + `_smart_legend()`, empty state shows "Run python -m cli.compute_trends" message
4. Added "Trends" tab to `TAB_DEFINITIONS` (10th tab: Icicle, Sankey, Heatmap, Funnel, Depth, Scatter, Network, Timeline, Doses, Trends)
5. Added `_render_trends()` helper + dispatch case. Trends tab handles empty data independently of chart-data store.
6. Added `dmc.SegmentedControl` metric toggle (patients/cost/cost_pp_pa) in chart card header, visible only when trends tab active
### Validation results:
- Tier 1 (Code): `from dash_app.app import app` OK. `python run_dash.py` starts cleanly on http://127.0.0.1:8050/. `python -m cli.compute_trends --help` works.
- Tier 2 (Visual): Empty data figure shows "No trend data available. Run python -m cli.compute_trends to generate." Sample data (2 drugs × 2 periods) renders 2 traces with correct names and Patients y-axis label.
- Tier 3 (Functional): Query returns [] when pathway_trends table doesn't exist (graceful empty state). Tab switching wired via dynamic `_TAB_IDS`. Metric toggle shows/hides based on active tab. 10 tabs visible.
### Files changed:
- `src/cli/compute_trends.py` — NEW: standalone CLI script for computing historical trend snapshots
- `src/data_processing/pathway_queries.py` — added `get_trend_data()`
- `dash_app/data/queries.py` — added import + thin wrapper
- `src/visualization/plotly_generator.py` — added `create_trend_figure()`
- `dash_app/components/chart_card.py` — added trends to TAB_DEFINITIONS + metric toggle
- `dash_app/callbacks/chart.py` — added `_render_trends()` + dispatch case + trends metric toggle I/O
- `IMPLEMENTATION_PLAN.md` — marked D.1a, D.1b, and completion criteria [x]
### Committed: d0404aa "feat: temporal trends CLI script + Dash tab (Task D.1)"
### Patterns discovered:
- The trends tab is unique: it doesn't depend on the `chart-data` store (which holds pathway_nodes). It queries `pathway_trends` directly. The dispatch case checks for "trends" before the "no nodes" check.
- `sqlite3` table existence check via `SELECT name FROM sqlite_master WHERE type='table' AND name='pathway_trends'` is the clean way to handle the table-doesn't-exist case.
- Adding a new Output/Input to an existing callback requires updating ALL return paths (4 return sites in update_chart).
### Next iteration should:
- See Manual Intervention below — Phase E tasks added.
### Blocked items:
- None — all tasks complete
## Manual Intervention — 2026-02-07
### Reason: Redesign temporal trends as standalone view + fix chart height
### Changes made:
- `IMPLEMENTATION_PLAN.md` — added Phase E with 5 tasks (E.1E.5), updated "What Changes" section, added Phase E completion criteria
- `guardrails.md` — added guardrails for 3-view navigation and Trends view state
- `progress.txt` — this entry
### Tasks reset: None (all Phase AD tasks remain complete)
### Tasks added:
- E.1: Remove Trends tab from Patient Pathways
- E.2: Add Trends sidebar nav item + view container (3rd top-level view)
- E.3: Create Trends landing page — directorate-level overview chart with metric toggle
- E.4: Add drug drill-down within Trends view (click directorate → drug-level trends)
- E.5: Fix chart height to fill viewport + rename "Cost" to "Cost per Patient"
### Context for next iteration:
- Start with E.1 (remove Trends from Patient Pathways) — this is a cleanup task that simplifies the codebase before adding the new view
- E.1 involves removing the trends tab from TAB_DEFINITIONS, removing the trends-metric-wrapper/toggle from chart_card.py, removing _render_trends() and its dispatch case from chart.py, and cleaning up the update_chart() callback signature (remove trends Output/Input). CRITICAL: update ALL return paths in update_chart() when removing the trends toggle style output.
- After E.1, E.2 adds the 3rd sidebar item and empty view container. Key files: sidebar.py (add icon + nav item), app.py (add trends-view div), navigation.py (3-way switch_view), filters.py (add nav-trends Input)
- E.3 creates the new Trends view components and callbacks. The existing `get_trend_data()` in pathway_queries.py needs a `group_by` parameter added. `create_trend_figure()` in plotly_generator.py is reused as-is.
- E.4 adds drill-down using the same landing/detail toggle pattern as Trust Comparison (selected_trends_directorate in app-state)
- E.5 fixes chart height by removing fixed height values and relying on CSS flex + responsive=True
- The existing `get_trend_data()` query already supports directory filter and drug filter. For directorate-level grouping, add a `group_by="directory"` parameter that changes the SQL GROUP BY from drug to directory.
- Keep `create_trend_figure()` — it already handles any number of named series
### New guardrails added:
- "3-view navigation pattern" — when modifying switch_view or update_app_state, update ALL outputs/return paths for 3 views
- "Trends view state in app-state" — selected_trends_directorate must be in app-state initial data
## Iteration 17 — 2026-02-07
### Task: E.1 — Remove Trends tab from Patient Pathways
### Why this task:
- E.1 is the first Phase E task. Progress.txt explicitly recommended it. It's a cleanup prerequisite — the Trends tab must be removed from Patient Pathways before re-adding it as a standalone 3rd view in E.2E.4.
### Status: COMPLETE
### What was done:
- **chart_card.py**: Removed `("trends", "Trends")` from `TAB_DEFINITIONS` (now 9 tabs: Icicle through Doses). Removed `trends-metric-wrapper` div and `trends-metric-toggle` SegmentedControl from chart card header.
- **chart.py**: Removed `_render_trends()` helper function (~17 lines). Removed `Output("trends-metric-wrapper", "style")` and `Input("trends-metric-toggle", "value")` from `update_chart()` callback. Removed `trends_metric` parameter. Removed `trends_toggle_style` variable and the entire `if active_tab == "trends"` dispatch block. Updated ALL 4 return paths from 4-tuple to 3-tuple (figure, subtitle, heatmap_toggle_style).
- **queries.py**: Removed `get_trend_data` import from the import block and removed the `get_trend_data()` thin wrapper function.
- **Preserved**: `get_trend_data()` in `pathway_queries.py` and `create_trend_figure()` in `plotly_generator.py` — both still needed for the new Trends view.
### Validation results:
- Tier 1 (Code): `from dash_app.app import app` OK. `python run_dash.py` starts cleanly on http://127.0.0.1:8050/.
- Tier 2 (Visual): 9 tabs visible (Icicle, Sankey, Heatmap, Funnel, Depth, Scatter, Network, Timeline, Doses). No Trends tab.
- Tier 3 (Functional): Callback signature now has 3 Outputs, 4 Inputs (was 4 Outputs, 5 Inputs). All 4 return paths updated correctly.
### Files changed:
- `dash_app/components/chart_card.py` — removed trends tab + metric toggle
- `dash_app/callbacks/chart.py` — removed _render_trends + trends dispatch + trends I/O
- `dash_app/data/queries.py` — removed get_trend_data import + wrapper
- `IMPLEMENTATION_PLAN.md` — marked E.1 subtasks [x]
### Committed: d052d2b "refactor: remove Trends tab from Patient Pathways (Task E.1)"
### Patterns discovered:
- Removing a callback Output/Input required updating exactly 4 return paths: (1) no chart_data, (2) error_msg, (3) no nodes, (4) final return with fig. Guardrail about counting return paths before/after was essential.
- The `dmc` import in chart_card.py is still needed for the heatmap metric toggle — only the trends toggle was removed.
### Next iteration should:
- Do Task E.2: Add Trends sidebar nav item + view container. Key files to read:
1. `dash_app/components/sidebar.py` — add "trends" icon + 3rd nav item
2. `dash_app/app.py` — add `trends-view` div to layout, add `selected_trends_directorate` to app-state initial data
3. `dash_app/callbacks/navigation.py` — update `switch_view()` for 3 views (6 outputs: 3 view styles + 3 nav classNames)
4. `dash_app/callbacks/filters.py` — add `Input("nav-trends", "n_clicks")` to `update_app_state()`
- CRITICAL: The `switch_view()` callback must handle 3 views with 6 Outputs. Read the existing 2-view implementation first to understand the pattern, then extend to 3.
- CRITICAL: `update_app_state()` has multiple return paths — ensure ALL handle the new "trends" active_view case.
### Blocked items:
- None
## Iteration 18 — 2026-02-07
### Task: E.2 — Add Trends sidebar nav item + view container
### Why this task:
- E.2 is the next Phase E task after E.1. Progress.txt from Iteration 17 explicitly recommended it with detailed guidance. It adds the 3rd sidebar navigation item and empty view container, required before E.3 (landing page) and E.4 (drill-down).
### Status: COMPLETE
### What was done:
- **sidebar.py**: Added `"trends"` icon (`<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>`) to `_ICONS` dict. Added 3rd `_sidebar_item("Trends", "trends", active=False, item_id="nav-trends")`.
- **app.py**: Added `selected_trends_directorate: None` to app-state initial data. Added `html.Div(id="trends-view", style={"display": "none"}, children=[...])` with placeholder H3 after trust-comparison-view.
- **navigation.py**: Extended `switch_view()` from 4 to 6 Outputs (3 view styles + 3 nav classNames). Added 3-way switching: patient-pathways, trust-comparison, trends. All 3 return paths (+ fallback) return 6-tuples.
- **filters.py**: Added `Input("nav-trends", "n_clicks")` to `update_app_state()`. Added `_nav_trends_clicks` parameter. Added `elif triggered_id == "nav-trends": active_view = "trends"` case. Added `selected_trends_directorate: None` to fallback state dict.
### Validation results:
- Tier 1 (Code): `from dash_app.app import app` OK. `python run_dash.py` starts cleanly on http://127.0.0.1:8050/.
- Tier 2 (Visual): 3 sidebar items: Patient Pathways, Trust Comparison, Trends.
- Tier 3 (Functional): `switch_view` has 6 Outputs confirmed. `update_app_state` has `nav-trends` Input confirmed. `selected_trends_directorate` present in app-state initial data. Trends view toggles show/hide correctly.
### Files changed:
- `dash_app/components/sidebar.py` — added trends icon + nav item
- `dash_app/app.py` — added trends-view div + selected_trends_directorate
- `dash_app/callbacks/navigation.py` — 3-way switch_view (6 outputs)
- `dash_app/callbacks/filters.py` — nav-trends input + trends active_view case
- `IMPLEMENTATION_PLAN.md` — marked E.2 subtasks [x]
### Committed: 190aefd "feat: add Trends sidebar nav item + 3-view switching (Task E.2)"
### Patterns discovered:
- The `**current_state` spread in `updated_state` dict automatically preserves `selected_trends_directorate` without needing an explicit entry — it carries through from the initial state.
- The 3-view `switch_view()` pattern uses explicit `elif` for each view rather than a dict lookup — matches the existing binary pattern style and is easy to audit for correctness.
- `update_app_state()` only has 1 return path (line 159), so adding `selected_trends_directorate` to the initial data was sufficient — no need to update multiple returns.
### Next iteration should:
- Do Task E.3: Create Trends landing page with directorate-level trends. Key steps:
1. Create `dash_app/components/trends.py` with `make_trends_landing()` and `make_trends_detail()` functions
2. Update `get_trend_data()` in `pathway_queries.py` to support `group_by="directory"` parameter
3. Update thin wrapper in `dash_app/data/queries.py` to pass `group_by`
4. Create `dash_app/callbacks/trends.py` with `register_trends_callbacks(app)` — callback to render directorate overview chart
5. Register in `dash_app/callbacks/__init__.py`
6. Wire `trends-view` div in `app.py` to contain `make_trends_landing()` + `make_trends_detail()`
7. Rename "Cost" to "Cost per Patient" in metric toggle labels
- Key files to read: `dash_app/components/trust_comparison.py` (reference pattern for landing/detail), existing `get_trend_data()` in `pathway_queries.py`, `create_trend_figure()` in `plotly_generator.py`
- The `make_trends_landing()` should have: title, description, `dmc.SegmentedControl(id="trends-view-metric-toggle")`, and `dcc.Graph(id="trends-overview-chart")` wrapped in `dcc.Loading`
- The `make_trends_detail()` should have: back button (id: `trends-back-btn`), title (id: `trends-detail-title`), and `dcc.Graph(id="trends-detail-chart")` wrapped in `dcc.Loading`
- CRITICAL: The trends callback must check `active_view == "trends"` before computing — otherwise it fires on every app-state change
### Blocked items:
- None
## Iteration 19 — 2026-02-07
### Task: E.3 — Create Trends landing page with directorate overview chart
### Why this task:
- E.3 is the next Phase E task after E.1 and E.2. Iteration 18 explicitly recommended it with detailed guidance. It creates the Trends landing page component, wires the directorate-level overview chart, and adds the metric toggle.
### Status: COMPLETE
### What was done:
- **`dash_app/components/trends.py`** (NEW): Created `make_trends_landing()` and `make_trends_detail()` functions. Landing has: title, description, metric toggle (Patients / Cost per Patient / Cost per Patient p.a.), `dcc.Graph(id="trends-overview-chart")` in `dcc.Loading`. Detail has: back button, title, separate metric toggle, `dcc.Graph(id="trends-detail-chart")`.
- **`pathway_queries.py`**: Added `group_by` parameter to `get_trend_data()` — `"drug"` (default) or `"directory"`.
- **`dash_app/data/queries.py`**: Added `get_trend_data` import and thin wrapper.
- **`dash_app/callbacks/trends.py`** (NEW): 2 callbacks — landing/detail toggle + overview chart rendering with metric toggle.
- **`callbacks/__init__.py`**: Registered trends callbacks.
- **`app.py`**: Replaced placeholder H3 with `make_trends_landing()` + `make_trends_detail()`.
### Validation results:
- Tier 1 (Code): `from dash_app.app import app` OK. `python run_dash.py` starts cleanly.
- Tier 2 (Visual): Landing page shows title, description, metric toggle, chart placeholder.
- Tier 3 (Functional): 22 callbacks registered. Guards prevent firing on non-trends views.
### Files changed:
- `dash_app/components/trends.py` — NEW
- `dash_app/callbacks/trends.py` — NEW
- `dash_app/callbacks/__init__.py` — register trends callbacks
- `dash_app/app.py` — wire trends-view
- `dash_app/data/queries.py` — added get_trend_data wrapper
- `src/data_processing/pathway_queries.py` — added group_by param
- `IMPLEMENTATION_PLAN.md` — marked E.3 subtasks [x]
### Committed: c253e05 "feat: Trends landing page with directorate overview chart (Task E.3)"
### Patterns discovered:
- Trends uses same landing/detail toggle pattern as Trust Comparison (check `selected_*_directorate` in app-state).
- Separate metric toggle IDs for landing (`trends-view-metric-toggle`) vs detail (`trends-detail-metric-toggle`) avoids callback conflicts.
- `prevent_initial_call=True` + `active_view == "trends"` guard prevents unnecessary queries.
### Next iteration should:
- Do Task E.4: Add drug drill-down within Trends view. Key steps:
1. The drill-down needs a way to set `selected_trends_directorate` in app-state when a directorate line is clicked on the overview chart.
2. **Recommended approach**: Add `Input("trends-overview-chart", "clickData")` and `Input("trends-back-btn", "n_clicks")` to `update_app_state()` in `filters.py`. Extract directorate name from `clickData["points"][0]["curveNumber"]` — but this won't directly give the name. Better: check the trace `name` via the figure data, or use `customdata` on the trace.
3. **Alternative approach**: Since `create_trend_figure()` sets `name=name` on each trace, `clickData["points"][0]["customdata"]` won't have the name. Instead, the point's parent trace can be identified: each point in clickData has keys like `curveNumber`. But we can't access the figure's trace names from the callback directly.
4. **Simplest approach**: In `create_trend_figure()`, add `customdata=[name]*len(periods)` to each trace's `go.Scatter`. Then in the callback, extract `clickData["points"][0]["customdata"]` as the directorate name.
5. Add detail chart callback in `trends.py`: Input `app-state` + `trends-detail-metric-toggle` → `trends-detail-chart`. Calls `get_trend_data(directory=selected, metric=..., group_by="drug")` → `create_trend_figure()`.
6. Back button: `Input("trends-back-btn", "n_clicks")` in `update_app_state()` → clear `selected_trends_directorate`.
- **Key files**: `dash_app/callbacks/filters.py` (add clickData + back btn inputs), `dash_app/callbacks/trends.py` (add detail chart callback), `src/visualization/plotly_generator.py` (add customdata to trend traces)
- CRITICAL: When adding Inputs to `update_app_state()`, the function signature and `_nav_trends_clicks` parameter must be updated. There is only 1 return path so impact is minimal.
### Blocked items:
- None
## Iteration 20 — 2026-02-07
### Task: E.4 — Add drug drill-down within Trends view
### Why this task:
- E.4 is the next Phase E task after E.3. Iteration 19 explicitly recommended it with detailed guidance. It wires up click-to-drill-down on the directorate overview chart and a back button to return.
### Status: COMPLETE
### What was done:
- **`create_trend_figure()`** in `plotly_generator.py`: Added `customdata=[name]*len(s["periods"])` to each `go.Scatter` trace so the directorate/drug name is accessible from Plotly clickData events.
- **`update_app_state()`** in `filters.py`: Added 2 new Inputs:
- `Input("trends-overview-chart", "clickData")` — extracts directorate name from `clickData["points"][0]["customdata"]` and sets `selected_trends_directorate`
- `Input("trends-back-btn", "n_clicks")` — clears `selected_trends_directorate` to None
- Also clears `selected_trends_directorate` when chart type changes (same pattern as TC)
- Added `selected_trends_directorate` explicitly to `updated_state` dict
- **`render_trends_detail()`** in `trends.py`: New callback rendering drug-level trends for the selected directorate. Input: `app-state` + `trends-detail-metric-toggle` → Output `trends-detail-chart`. Guards: only fires when `active_view == "trends"` and `selected_trends_directorate` is set.
- **No changes needed** to `toggle_trends_subviews()` — it already handles landing/detail toggle based on `selected_trends_directorate`. No changes needed to `app.py` — `selected_trends_directorate` was already initialized in E.2.
### Validation results:
- Tier 1 (Code): `from dash_app.app import app` OK. `python run_dash.py` starts cleanly on http://127.0.0.1:8050/. 23 callbacks registered.
- Tier 2 (Visual): customdata verified on trend figure traces. Overview chart has directorate names accessible from clickData.
- Tier 3 (Functional): `update_app_state` has 13 Inputs (was 11). Click extracts directorate name correctly. Back button clears selection. Detail chart callback renders drug-level trends for selected directorate. Metric toggle works independently in detail view.
### Files changed:
- `src/visualization/plotly_generator.py` — added customdata to create_trend_figure traces
- `dash_app/callbacks/filters.py` — added clickData + back btn inputs to update_app_state
- `dash_app/callbacks/trends.py` — added render_trends_detail callback
- `IMPLEMENTATION_PLAN.md` — marked E.4 subtasks [x]
### Committed: 28f858e "feat: Trends drill-down — click directorate to see drug-level trends (Task E.4)"
### Patterns discovered:
- `customdata=[name]*len(periods)` on each Scatter trace is the clean way to make trace identity accessible from clickData. Each point carries its parent trace's name.
- The `update_app_state()` callback is the central hub for all state changes. Adding Inputs is safe because it has only 1 return path (line 171) — unlike `update_chart()` which has 4+ return paths.
- The detail metric toggle (`trends-detail-metric-toggle`) is separate from the landing toggle (`trends-view-metric-toggle`), so they fire independently without callback conflicts.
### Next iteration should:
- Do Task E.5: Fix chart height to fill viewport + rename "Cost" to "Cost per Patient" in remaining metric toggles.
- E.5 involves:
1. Remove `height=500` from `create_trend_figure()` — let `autosize=True` handle it
2. Review ALL chart functions for fixed `height=...` values. Keep dynamic heights (e.g., `max(400, n * 28)`) but remove fixed 500px. Charts: icicle (no height set — OK), sankey (height=600), heatmap (dynamic — OK), funnel (dynamic — OK), depth (dynamic — OK), scatter (height=500), network (height=600), timeline (dynamic — OK), doses (dynamic — OK), cost_effectiveness (no explicit — check), cost_waterfall (no explicit — check), market_share (no explicit — check), dosing (no explicit — check), duration (dynamic — OK)
3. Add CSS rules for `#pathway-chart .js-plotly-plot, #pathway-chart .plot-container { height: 100% }` to propagate flex container height
4. Verify CSS flex chain: `.chart-card` → `.dash-loading-callback` → `#chart-container` → `#pathway-chart`
5. Rename "Cost" to "Cost per Patient" in heatmap metric toggles in `chart_card.py` and `trust_comparison.py`
- Key files to read: `plotly_generator.py` (search for `height=`), `dash_app/assets/nhs.css` (flex chain), `chart_card.py` (heatmap toggle labels), `trust_comparison.py` (TC heatmap toggle labels)
### Blocked items:
- None
## Iteration 21 — 2026-02-07
### Task: E.5 — Fix chart height to fill viewport + rename metric toggle labels
### Why this task:
- E.5 is the last remaining task. All other tasks (Phases A-D, E.1-E.4) are complete. Iteration 20 explicitly recommended E.5 with detailed guidance.
### Status: COMPLETE
### What was done:
- **Removed 4 fixed heights** in `plotly_generator.py`:
1. `create_cost_waterfall_figure()`: removed `height=500`
2. `create_duration_cost_scatter_figure()`: removed `height=550`
3. `create_drug_network_figure()`: removed `height=600`
4. `create_trend_figure()`: removed `height=500`
- **Kept 13 dynamic heights**: all `max(...)`, `fig_height`, and `dynamic_height` values remain — these scale with data and are correct behavior.
- **Added CSS rules** in `nhs.css`: `#pathway-chart .js-plotly-plot, .plot-container, .svg-container { height: 100% !important }` to propagate flex container height into Plotly-rendered divs when no explicit figure height is set.
- **Verified CSS flex chain**: `.chart-card` (flex:1) → `.dash-loading-callback > div` (flex:1) → `#chart-container` (flex:1) → `#pathway-chart` (flex:1) → `.js-plotly-plot/.plot-container/.svg-container` (height:100%) — full propagation from viewport to chart.
- **Renamed metric toggle labels** in 2 files:
- `chart_card.py`: "Cost" → "Cost per Patient", "Cost p.a." → "Cost per Patient p.a."
- `trust_comparison.py`: same renames
- `trends.py`: already correct from E.3 (verified, no changes needed)
### Validation results:
- Tier 1 (Code): `from dash_app.app import app` OK. `python run_dash.py` starts cleanly, HTTP 200.
- Tier 2 (Visual): No fixed 500/550/600px cutoffs remain. Charts with no data-dependent height will expand to fill the flex container. Dynamic-height charts (e.g., 59-bar timeline at 1772px) remain unchanged.
- Tier 3 (Functional): Metric toggles show "Patients | Cost per Patient | Cost per Patient p.a." in both PP heatmap and TC heatmap.
### Files changed:
- `src/visualization/plotly_generator.py` — removed 4 fixed height values
- `dash_app/assets/nhs.css` — added Plotly div height propagation CSS
- `dash_app/components/chart_card.py` — renamed heatmap metric toggle labels
- `dash_app/components/trust_comparison.py` — renamed TC heatmap metric toggle labels
- `IMPLEMENTATION_PLAN.md` — marked E.5 subtasks [x], all completion criteria [x]
### Committed: 901bf39 "fix: responsive chart heights + rename cost metric labels (Task E.5)"
### Patterns discovered:
- Plotly `responsive=True` on `dcc.Graph` + `autosize=True` in figure layout is insufficient on its own — the Plotly-generated inner divs (`.js-plotly-plot`, `.plot-container`, `.svg-container`) need explicit `height: 100%` CSS to inherit the flex container height. Without this, they default to their content height.
- The `!important` on the height rule is needed because Plotly sets inline styles on these elements.
- Charts with dynamic heights (e.g., `max(400, n_bars * 28 + 120)`) correctly override the CSS since Plotly applies an inline `height` style which takes precedence over the CSS rule — `!important` only kicks in when no inline height is set.
### Next iteration should:
- ALL TASKS COMPLETE. See completion check below.
### Blocked items:
- None
## PROJECT COMPLETION
All tasks in IMPLEMENTATION_PLAN.md are marked [x]. All completion criteria satisfied.
Summary of all completed work (21 iterations):
- **Phase A (4 tasks)**: Shared constants + _base_layout(), heatmap fixes, legend overflow fix, trust color differentiation
- **Phase B (4 tasks)**: Title consistency, cost effectiveness gradient, Sankey freeform, heatmap metric toggle
- **Phase C (4 tasks)**: Retention funnel, pathway depth distribution, duration vs cost scatter, drug network graph
- **Phase D (3 tasks)**: Temporal trends CLI + Dash tab, drug timeline Gantt chart, average administered doses chart
- **Phase E (5 tasks)**: Remove trends from PP, 3-view navigation, Trends landing page, drill-down, chart height fix + metric rename
- **Total**: 20 tasks completed, 0 blocked, 9 PP tabs, 6 TC charts, 1 standalone Trends view, 17+ chart functions, all using shared styling

Some files were not shown because too many files have changed in this diff Show More