Remove legacy/ralph/temp files from tracking

This commit is contained in:
Andrew Charlwood
2026-02-10 15:25:53 +00:00
parent 7e63e6ea45
commit c18027ffa2
46 changed files with 0 additions and 124001 deletions
-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
-253
View File
@@ -1,253 +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**: Phase E — Redesign temporal trends as a standalone 3rd view with directorate overview + drug drill-down, fix chart height, rename cost labels. See IMPLEMENTATION_PLAN.md for the full task list organized into Phases AE.
## 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 working on the Trends view** (Phase E), also read:
- `dash_app/components/trends.py` — Trends landing + detail components (create if doesn't exist)
- `dash_app/callbacks/trends.py` — Trends view callbacks (create if doesn't exist)
- `dash_app/components/sidebar.py` — Sidebar navigation (3 items: Patient Pathways, Trust Comparison, Trends)
- `dash_app/callbacks/navigation.py` — View switching (3-way)
- `dash_app/callbacks/filters.py``update_app_state()` handles nav clicks
- `dash_app/app.py` — Layout with 3 view containers + app-state initial data
**When modifying UI components**, read:
- `dash_app/components/trust_comparison.py` — TC landing + dashboard layout (reference for Trends landing/detail pattern).
- `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 Skill
**IMPORTANT**: When creating or modifying chart functions in `plotly_generator.py`, invoke the `/plotly` skill first. This loads Plotly reference documentation (chart types, graph objects, layouts, interactivity) that helps produce better chart code. Use it before writing any Plotly figure code.
### 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
- **3-view architecture** — Patient Pathways, Trust Comparison, Trends (Phase E). View switching via `active_view` in app-state.
- 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
-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.
-154
View File
@@ -1,154 +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 from Dash callbacks
- **When**: Querying the database from Dash callbacks
- **Rule**: Read-only access from Dash. Use `sqlite3.connect(db_path)` with SELECT queries only. Never INSERT, UPDATE, DELETE, or ALTER from the Dash app.
- **Exception**: The standalone `cli/compute_trends.py` script may CREATE and INSERT into the `pathway_trends` table. This is a separate CLI command, not part of the Dash app or the main refresh pipeline.
- **Why**: pathways.db is populated by CLI commands. The Dash app is a read-only consumer.
### Trend computation uses existing pipeline functions as-is
- **When**: Building `cli/compute_trends.py`
- **Rule**: Import and call `fetch_and_transform_data()` and `process_pathway_for_date_filter()` from `pathway_pipeline.py`. Do NOT modify these functions. Do NOT modify `schema.py`, `reference_data.py`, or `refresh_pathways.py`. The new script creates its own table via `CREATE TABLE IF NOT EXISTS`.
- **Why**: The historical snapshot approach works by calling existing functions with different `max_date` values. No pipeline changes needed.
---
## 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.
### 3-view navigation pattern
- **When**: Modifying `switch_view()` in `navigation.py` or `update_app_state()` in `filters.py`
- **Rule**: There are 3 views: `patient-pathways`, `trust-comparison`, `trends`. The `switch_view()` callback has 6 Outputs (3 view styles + 3 nav classNames). The `update_app_state()` callback has 3 nav Inputs. When updating either callback, ensure ALL return paths handle all 3 views correctly. Every return statement must include values for all 6 outputs / handle all 3 active_view values.
- **Why**: Adding a 3rd view to a previously binary toggle is error-prone — missing a return path causes Dash callback errors.
### Trends view state in app-state
- **When**: Working on the Trends view (E.2E.4)
- **Rule**: `selected_trends_directorate` must be initialized as `None` in the `app-state` dcc.Store initial data in `app.py`. The Trends view uses landing/detail toggle based on this value (same pattern as Trust Comparison's `selected_comparison_directorate`).
- **Why**: Missing initial state causes KeyError on first page load.
### Removing callback Outputs/Inputs requires updating ALL return paths
- **When**: Removing Outputs or Inputs from an existing callback (e.g., E.1 removing trends toggle from update_chart)
- **Rule**: When removing an Output from a callback, you MUST update EVERY `return` statement in that callback to match the new Output count. Count the number of return statements before editing and verify the same count after. The `update_chart()` callback currently has 4+ return paths.
- **Why**: Mismatched return tuple length causes `InvalidCallbackReturnValue` at runtime.
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
-362
View File
@@ -1,362 +0,0 @@
<#
.SYNOPSIS
Ralph Wiggum Loop - Visualization Improvements variant.
.DESCRIPTION
Outer loop for iterative chart improvement (bug fixes, polish, new analytics).
Each iteration spawns a fresh `claude --print` invocation.
Memory persists via filesystem only: git commits, progress.txt, IMPLEMENTATION_PLAN.md, guardrails.md.
Runs until completion (<promise>COMPLETE</promise>) or circuit breaker trips.
No arbitrary iteration limit — the loop continues until done.
Circuit breakers prevent runaway costs:
- No git changes for N consecutive iterations (stalled)
- Same error repeated N consecutive iterations (stuck)
.PARAMETER Model
Claude model to use. Default: "opus".
.PARAMETER BranchName
Optional git branch name. If provided, creates/checks out the branch before starting.
.PARAMETER MaxNoProgress
Number of consecutive iterations with no git changes before circuit breaker trips. Default: 3.
.PARAMETER MaxSameError
Number of consecutive iterations with the same error before circuit breaker trips. Default: 3.
.EXAMPLE
.\ralph.ps1 -Model "opus" -BranchName "feature/dash-migration"
.EXAMPLE
.\ralph.ps1 -Model "sonnet" -MaxNoProgress 2
#>
param(
[string]$Model = "opus",
[string]$BranchName,
[int]$MaxNoProgress = 3,
[int]$MaxSameError = 3
)
$ErrorActionPreference = "Stop"
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$promptFile = Join-Path $scriptDir "RALPH_PROMPT.md"
$planFile = Join-Path $scriptDir "IMPLEMENTATION_PLAN.md"
$guardrailsFile = Join-Path $scriptDir "guardrails.md"
$progressFile = Join-Path $scriptDir "progress.txt"
$logDir = Join-Path $scriptDir "logs"
# --- Validation ---
if (-not (Test-Path $promptFile)) {
Write-Error "RALPH_PROMPT.md not found at $promptFile"
exit 1
}
if (-not (Test-Path $planFile)) {
Write-Error "IMPLEMENTATION_PLAN.md not found at $planFile"
exit 1
}
if (-not (Test-Path $guardrailsFile)) {
Write-Warning "guardrails.md not found at $guardrailsFile - loop may miss known failure patterns"
}
# Ensure progress.txt exists
if (-not (Test-Path $progressFile)) {
@"
# Progress Log
## Design Context
<!-- Design decisions and context go here -->
## Reflex Patterns
<!-- Reusable Reflex patterns discovered during development -->
## Iteration Log
<!-- Each iteration appends a structured entry below. See RALPH_PROMPT.md for format. -->
"@ | Set-Content -Path $progressFile -Encoding UTF8
Write-Host "Created progress.txt"
}
# Ensure logs directory exists
if (-not (Test-Path $logDir)) {
New-Item -ItemType Directory -Path $logDir | Out-Null
Write-Host "Created logs directory"
}
# --- Git Setup ---
$gitInitialised = $false
try {
$result = git rev-parse --is-inside-work-tree 2>&1
if ($LASTEXITCODE -eq 0 -and $result -eq "true") {
$gitInitialised = $true
}
} catch {
# Not a git repo — expected on first run
}
if (-not $gitInitialised) {
Write-Host "Initialising git repository..."
git init
git add -A
git commit -m "Initial commit before Ralph loop"
}
if ($BranchName) {
$currentBranch = git branch --show-current
if ($currentBranch -ne $BranchName) {
$branchExists = git branch --list $BranchName
if ($branchExists) {
Write-Host "Switching to existing branch: $BranchName"
git checkout $BranchName
} else {
Write-Host "Creating branch: $BranchName"
git checkout -b $BranchName
}
}
}
# --- Circuit Breaker State ---
$noProgressCount = 0
$lastErrorSignature = ""
$sameErrorCount = 0
# Capture the HEAD commit hash before the loop starts
$preLoopHead = git rev-parse HEAD 2>$null
# --- Main Loop ---
$promptContent = Get-Content -Path $promptFile -Raw
# Count existing iterations from progress.txt to track total across runs
$existingIterations = 0
if (Test-Path $progressFile) {
$existingIterations = (Select-String -Path $progressFile -Pattern "## Iteration" -AllMatches | Measure-Object).Count
}
Write-Host ""
Write-Host "===== Ralph Wiggum Loop (Visualization Improvements) =====" -ForegroundColor Cyan
Write-Host "Model: $Model | Runs until COMPLETE" -ForegroundColor Cyan
Write-Host "Circuit breakers: no-progress=$MaxNoProgress, same-error=$MaxSameError" -ForegroundColor Cyan
if ($BranchName) { Write-Host "Branch: $BranchName" -ForegroundColor Cyan }
if ($existingIterations -gt 0) { Write-Host "Previous iterations: $existingIterations" -ForegroundColor Cyan }
Write-Host "===========================================" -ForegroundColor Cyan
Write-Host ""
$i = 0
while ($true) {
$i++
$totalIteration = $existingIterations + $i
Write-Host ""
Write-Host "--- Iteration $i (Total: $totalIteration) ---" -ForegroundColor Yellow
# Record HEAD before this iteration
$headBefore = git rev-parse HEAD 2>$null
# Show start time and status
$iterStart = Get-Date
Write-Host " Started: $($iterStart.ToString('HH:mm:ss'))" -ForegroundColor DarkGray
Write-Host " Spawning Claude ($Model)..." -ForegroundColor DarkGray
Write-Host ""
# Spawn fresh Claude instance with stream-json for tool call visibility
$logFile = Join-Path $logDir "iteration_$totalIteration.log"
$rawLogFile = Join-Path $logDir "iteration_$totalIteration.raw.jsonl"
$maxRetries = 10
$retryCount = 0
$outputString = ""
$apiOverloaded = $false
do {
$apiOverloaded = $false
$textBuilder = [System.Text.StringBuilder]::new()
$toolCount = 0
# Clear raw log file for this attempt
if (Test-Path $rawLogFile) { Remove-Item $rawLogFile -Force }
if ($retryCount -gt 0) {
$backoffSeconds = [Math]::Pow(2, $retryCount - 1)
Write-Host " [Retry $retryCount/$maxRetries] API overloaded, waiting $backoffSeconds seconds..." -ForegroundColor DarkYellow
Start-Sleep -Seconds $backoffSeconds
Write-Host " Retrying Claude invocation..." -ForegroundColor DarkGray
}
$promptContent | claude --print --verbose --dangerously-skip-permissions --model $Model --output-format stream-json 2>&1 | ForEach-Object {
$line = $_.ToString().Trim()
if (-not $line) { return }
# Save raw event for debugging (with error handling for stream closure)
try {
Add-Content -Path $rawLogFile -Value $line -Encoding UTF8 -ErrorAction SilentlyContinue
} catch {
# Stream closed or file locked - ignore and continue
}
try {
$evt = $line | ConvertFrom-Json -ErrorAction Stop
# --- Tool use start (show tool name) ---
if ($evt.type -eq 'content_block_start' -and $evt.content_block.type -eq 'tool_use') {
$toolCount++
$toolName = $evt.content_block.name
Write-Host " [$toolName]" -ForegroundColor DarkCyan
}
# --- Assistant text content (streaming deltas) ---
elseif ($evt.type -eq 'content_block_delta' -and $evt.delta.type -eq 'text_delta' -and $evt.delta.text) {
Write-Host -NoNewline $evt.delta.text
[void]$textBuilder.Append($evt.delta.text)
}
# --- Result event (error display + text capture for circuit breakers) ---
elseif ($evt.type -eq 'result') {
if ($evt.subtype -eq 'error_result' -and $evt.error) {
Write-Host " [ERROR] $($evt.error)" -ForegroundColor Red
[void]$textBuilder.AppendLine("ERROR: $($evt.error)")
}
elseif ($evt.result) {
# Capture for circuit breaker detection; don't print
# (text already displayed via streaming deltas above)
[void]$textBuilder.AppendLine($evt.result)
}
}
# --- Message-level content (final message summary) ---
elseif ($evt.message -and $evt.message.content) {
foreach ($block in $evt.message.content) {
if ($block.type -eq 'text' -and $block.text) {
Write-Host $block.text
[void]$textBuilder.AppendLine($block.text)
}
elseif ($block.type -eq 'tool_use') {
$toolCount++
Write-Host " [$($block.name)]" -ForegroundColor DarkCyan
}
# Silently ignore tool_result and other block types
}
}
# All other JSON events (input_json_delta, content_block_stop,
# message_start, message_stop, ping, etc.) are silently ignored
} catch {
# Not valid JSON — only print if it looks like meaningful stderr
# (filter out JSON fragments from multi-line events)
if ($line -and $line -notmatch '^\s*[\{\[\}\]"]') {
Write-Host $line -ForegroundColor DarkYellow
[void]$textBuilder.AppendLine($line)
}
}
}
$outputString = $textBuilder.ToString()
# Check for 529 overloaded error
if ($outputString -match "529.*overloaded|overloaded_error") {
$apiOverloaded = $true
$retryCount++
if ($retryCount -ge $maxRetries) {
Write-Host " [ERROR] API overloaded after $maxRetries retries, giving up." -ForegroundColor Red
}
}
# Check for usage limit with cooldown (e.g. "Usage limit reached. Reset at 3 pm")
elseif ($outputString -match "(?i)usage limit reached.*reset at (\d{1,2})(?::(\d{2}))?\s*(am|pm)") {
$resetHour = [int]$Matches[1]
$resetMinute = if ($Matches[2]) { [int]$Matches[2] } else { 0 }
$resetAmPm = $Matches[3]
if ($resetAmPm -ieq "pm" -and $resetHour -ne 12) { $resetHour += 12 }
elseif ($resetAmPm -ieq "am" -and $resetHour -eq 12) { $resetHour = 0 }
$now = Get-Date
$resetTime = Get-Date -Hour $resetHour -Minute $resetMinute -Second 0
if ($resetTime -le $now) { $resetTime = $resetTime.AddDays(1) }
$resetTime = $resetTime.AddMinutes(2)
$waitSeconds = [Math]::Ceiling(($resetTime - $now).TotalSeconds)
$waitMinutes = [Math]::Ceiling($waitSeconds / 60)
Write-Host ""
Write-Host " [USAGE LIMIT] Reset at $($Matches[1]) $resetAmPm. Cooling down ~$waitMinutes minutes (until $($resetTime.ToString('HH:mm')))..." -ForegroundColor Yellow
Start-Sleep -Seconds $waitSeconds
Write-Host " [USAGE LIMIT] Cooldown complete. Retrying iteration..." -ForegroundColor Green
$apiOverloaded = $true
# Don't increment retryCount — deterministic wait, not a flaky error
}
} while ($apiOverloaded -and $retryCount -lt $maxRetries)
$outputString | Set-Content -Path $logFile -Encoding UTF8
# Show elapsed time and tool count
$elapsed = (Get-Date) - $iterStart
Write-Host ""
Write-Host " Finished: $(Get-Date -Format 'HH:mm:ss') (elapsed: $($elapsed.ToString('mm\:ss')), tools: $toolCount)" -ForegroundColor DarkGray
# --- Circuit Breaker: No Progress ---
$headAfter = git rev-parse HEAD 2>$null
if ($headAfter -eq $headBefore) {
$noProgressCount++
Write-Host " [Circuit Breaker] No git commits this iteration ($noProgressCount/$MaxNoProgress)" -ForegroundColor DarkYellow
if ($noProgressCount -ge $MaxNoProgress) {
Write-Host ""
Write-Host "===== CIRCUIT BREAKER: NO PROGRESS =====" -ForegroundColor Red
Write-Host "No git commits for $MaxNoProgress consecutive iterations. The loop is stalled." -ForegroundColor Red
Write-Host "Check progress.txt and logs/ for details on what went wrong." -ForegroundColor Red
exit 1
}
} else {
$noProgressCount = 0
}
# --- Circuit Breaker: Repeated Error ---
$errorLines = $outputString | Select-String -Pattern "(?i)(error|exception|failed|fatal)[:.].*" -AllMatches
if ($errorLines) {
$filteredErrors = $errorLines.Matches | Where-Object { $_.Value -notmatch "529|overloaded" } | Select-Object -First 3
$currentErrorSignature = ($filteredErrors | ForEach-Object { $_.Value }) -join "|"
if ($currentErrorSignature -and $currentErrorSignature -eq $lastErrorSignature) {
$sameErrorCount++
Write-Host " [Circuit Breaker] Same error pattern repeated ($sameErrorCount/$MaxSameError)" -ForegroundColor DarkYellow
if ($sameErrorCount -ge $MaxSameError) {
Write-Host ""
Write-Host "===== CIRCUIT BREAKER: REPEATED ERROR =====" -ForegroundColor Red
Write-Host "Same error pattern for $MaxSameError consecutive iterations:" -ForegroundColor Red
Write-Host " $currentErrorSignature" -ForegroundColor Red
Write-Host "Check progress.txt and logs/ for details." -ForegroundColor Red
exit 1
}
} elseif ($currentErrorSignature) {
$sameErrorCount = 0
}
$lastErrorSignature = $currentErrorSignature
} else {
$sameErrorCount = 0
$lastErrorSignature = ""
}
# --- Push to Remote ---
$hasRemote = git remote 2>$null
if ($hasRemote) {
$currentBranch = git branch --show-current
git push origin $currentBranch 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Host " Pushed to remote." -ForegroundColor Green
} else {
Write-Host " Push failed or no remote configured - continuing." -ForegroundColor DarkYellow
}
}
# --- Check for Completion ---
if ($outputString -match "<promise>COMPLETE</promise>") {
Write-Host ""
Write-Host "===== COMPLETE =====" -ForegroundColor Green
Write-Host "Visualization improvements finished after $i iteration(s) this run ($totalIteration total)." -ForegroundColor Green
exit 0
}
# Brief pause between iterations
Start-Sleep -Seconds 2
}
-99
View File
@@ -1,99 +0,0 @@
@echo off
setlocal EnableDelayedExpansion
title HCD Patient Pathway Analysis
echo.
echo ==========================================
echo HCD Patient Pathway Analysis
echo NHS High-Cost Drug Treatment Pathways
echo ==========================================
echo.
:: -------------------------------------------------------
:: First run vs subsequent run
:: -------------------------------------------------------
if exist ".venv\Scripts\activate.bat" (
echo Ready to launch.
goto :run_app
)
echo First-time setup detected. This will:
echo 1. Install uv (Python package manager)
echo 2. Install Python 3.12 and dependencies
echo 3. Build and start the application
echo.
echo Requires internet access. May take 3-5 minutes.
echo.
pause
:: -------------------------------------------------------
:: Install uv if not available
:: -------------------------------------------------------
where uv >nul 2>&1
if %ERRORLEVEL% neq 0 (
echo.
echo [1/3] Installing uv...
powershell -ExecutionPolicy Bypass -Command "irm https://astral.sh/uv/install.ps1 | iex"
set "PATH=%USERPROFILE%\.local\bin;%PATH%"
set "PATH=%USERPROFILE%\.cargo\bin;%PATH%"
where uv >nul 2>&1
if !ERRORLEVEL! neq 0 (
echo.
echo ERROR: uv installation failed.
echo Try installing manually: https://docs.astral.sh/uv/getting-started/installation/
echo Then re-run this script.
pause
exit /b 1
)
echo uv installed successfully.
) else (
echo [1/3] uv already installed.
)
:: -------------------------------------------------------
:: Sync dependencies
:: -------------------------------------------------------
echo.
echo [2/3] Installing Python and dependencies...
echo (First run only — please wait)
echo.
uv sync
if %ERRORLEVEL% neq 0 (
echo.
echo ERROR: Dependency installation failed.
echo Check your internet connection and try again.
pause
exit /b 1
)
echo.
echo Setup complete.
:: -------------------------------------------------------
:: Run application
:: -------------------------------------------------------
:run_app
echo.
echo [3/3] Starting application...
echo.
echo App will open at: http://localhost:3000
echo First launch builds the frontend (~60 seconds).
echo Subsequent launches are fast.
echo.
echo To stop: close this window or press Ctrl+C
echo ==========================================
echo.
start "" cmd /c "timeout /t 8 /nobreak >nul && start http://localhost:3000"
uv run reflex run
if %ERRORLEVEL% neq 0 (
echo.
echo Application exited with an error.
echo Try deleting .web\ and running again.
echo.
pause
)