This commit is contained in:
Andrew Charlwood
2026-02-08 22:41:46 +00:00
parent 1400fe7217
commit b98ab1a5c6
9 changed files with 660 additions and 66 deletions
+2 -16
View File
@@ -37,25 +37,11 @@ logs/*.jsonl
.states/ .states/
# SQLite database (will contain local data) # SQLite database (will contain local data)
*.db #*.db
*.sqlite #*.sqlite
# Snowflake result cache # Snowflake result cache
data/cache/ data/cache/
# Uploaded data files
data/uploads/
# Exported analysis results
data/exports/
# Analysis output files
output/*.html
output/*.csv
*.html
# VS Code workspace settings # VS Code workspace settings
.vscode/ .vscode/
# User uploaded files
uploaded_files/
+551
View File
@@ -0,0 +1,551 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HCD Analysis — NHS Classic</title>
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@300;400;600;700;900&display=swap" rel="stylesheet">
<style>
:root {
--nhs-blue: #005EB8;
--nhs-dark-blue: #003087;
--nhs-light-blue: #41B6E6;
--nhs-white: #FFFFFF;
--nhs-pale-grey: #E8EDEE;
--nhs-mid-grey: #768692;
--nhs-dark-grey: #425563;
--nhs-green: #009639;
--nhs-yellow: #FFB81C;
--nhs-red: #DA291C;
--sidebar-w: 240px;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Source Sans 3', Arial, sans-serif;
background: #F0F4F5;
color: var(--nhs-dark-grey);
line-height: 1.5;
min-height: 100vh;
}
/* ── Top Header ── */
.top-header {
position: fixed; top: 0; left: 0; right: 0; z-index: 200;
height: 56px;
background: var(--nhs-dark-blue);
display: flex; align-items: center; justify-content: space-between;
padding: 0 24px 0 0;
}
.top-header__brand {
display: flex; align-items: center; gap: 16px;
height: 100%; padding: 0 24px;
background: var(--nhs-blue);
clip-path: polygon(0 0, calc(100% - 16px) 0, 100% 100%, 0 100%);
padding-right: 40px;
}
.top-header__logo {
width: 40px; height: 40px;
background: var(--nhs-white);
border-radius: 4px;
display: grid; place-items: center;
font-weight: 900; font-size: 11px; color: var(--nhs-blue);
letter-spacing: 0.5px;
line-height: 1;
}
.top-header__title {
color: var(--nhs-white);
font-size: 20px; font-weight: 700;
letter-spacing: -0.01em;
}
.top-header__breadcrumb {
color: rgba(255,255,255,0.7);
font-size: 14px; font-weight: 400;
}
.top-header__breadcrumb strong { color: var(--nhs-white); font-weight: 600; }
.top-header__right {
display: flex; align-items: center; gap: 20px; color: rgba(255,255,255,0.8); font-size: 14px;
}
.top-header__right .status-dot {
width: 8px; height: 8px; border-radius: 50%; background: var(--nhs-green);
display: inline-block; margin-right: 4px;
}
/* ── Sidebar ── */
.sidebar {
position: fixed; top: 56px; left: 0; bottom: 0;
width: var(--sidebar-w);
background: var(--nhs-white);
border-right: 1px solid var(--nhs-pale-grey);
overflow-y: auto; z-index: 100;
display: flex; flex-direction: column;
}
.sidebar__section { padding: 16px 0; }
.sidebar__section + .sidebar__section { border-top: 1px solid var(--nhs-pale-grey); }
.sidebar__label {
padding: 0 20px 8px;
font-size: 11px; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.08em;
color: var(--nhs-mid-grey);
}
.sidebar__item {
display: flex; align-items: center; gap: 12px;
padding: 10px 20px;
font-size: 15px; font-weight: 400;
color: var(--nhs-dark-grey);
text-decoration: none;
border-left: 4px solid transparent;
transition: background 0.15s, border-color 0.15s;
cursor: pointer;
}
.sidebar__item:hover { background: #F0F4F5; }
.sidebar__item--active {
background: #E8F0FE;
border-left-color: var(--nhs-blue);
color: var(--nhs-blue);
font-weight: 600;
}
.sidebar__item svg { width: 18px; height: 18px; flex-shrink: 0; }
.sidebar__footer {
margin-top: auto; padding: 16px 20px;
border-top: 1px solid var(--nhs-pale-grey);
font-size: 12px; color: var(--nhs-mid-grey);
}
/* ── Main Content ── */
.main {
margin-left: var(--sidebar-w);
margin-top: 56px;
padding: 24px;
min-height: calc(100vh - 56px);
display: flex; flex-direction: column; gap: 20px;
}
/* ── KPI Row ── */
.kpi-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.kpi-card {
background: var(--nhs-white);
border: 1px solid var(--nhs-pale-grey);
border-top: 4px solid var(--nhs-blue);
padding: 20px;
display: flex; flex-direction: column; gap: 2px;
}
.kpi-card--green { border-top-color: var(--nhs-green); }
.kpi-card__label {
font-size: 12px; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.05em;
color: var(--nhs-mid-grey);
}
.kpi-card__value {
font-size: 32px; font-weight: 300;
color: var(--nhs-dark-blue);
line-height: 1.1;
font-variant-numeric: tabular-nums;
}
.kpi-card__sub {
font-size: 13px; color: var(--nhs-mid-grey); margin-top: 4px;
}
/* ── Filter Bar ── */
.filter-bar {
background: var(--nhs-white);
border: 1px solid var(--nhs-pale-grey);
padding: 12px 20px;
display: flex; align-items: center; gap: 16px;
flex-wrap: wrap;
}
.filter-bar__group {
display: flex; align-items: center; gap: 8px;
}
.filter-bar__divider {
width: 1px; height: 28px; background: var(--nhs-pale-grey);
}
.filter-bar__label {
font-size: 12px; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.04em;
color: var(--nhs-mid-grey);
white-space: nowrap;
}
/* Toggle pills */
.toggle-pills {
display: flex; border: 1px solid var(--nhs-pale-grey); overflow: hidden;
}
.toggle-pill {
padding: 6px 16px;
font-size: 14px; font-weight: 600;
color: var(--nhs-dark-grey);
background: var(--nhs-white);
cursor: pointer;
border: none; outline: none;
transition: background 0.15s, color 0.15s;
}
.toggle-pill + .toggle-pill { border-left: 1px solid var(--nhs-pale-grey); }
.toggle-pill--active {
background: var(--nhs-blue);
color: var(--nhs-white);
}
.toggle-pill:hover:not(.toggle-pill--active) { background: #F0F4F5; }
.toggle-pill:focus-visible { box-shadow: 0 0 0 3px var(--nhs-yellow); z-index: 1; }
/* Selects */
.filter-select {
height: 34px; padding: 0 12px;
border: 1px solid var(--nhs-pale-grey);
font-family: inherit; font-size: 14px;
color: var(--nhs-dark-grey);
background: var(--nhs-white);
cursor: pointer;
min-width: 140px;
}
.filter-select:focus { outline: 3px solid var(--nhs-yellow); outline-offset: 0; }
/* ── Chart Area ── */
.chart-card {
background: var(--nhs-white);
border: 1px solid var(--nhs-pale-grey);
flex: 1;
display: flex; flex-direction: column;
}
.chart-card__header {
padding: 16px 20px;
border-bottom: 1px solid var(--nhs-pale-grey);
display: flex; justify-content: space-between; align-items: baseline;
}
.chart-card__title {
font-size: 18px; font-weight: 700;
color: var(--nhs-dark-blue);
}
.chart-card__subtitle {
font-size: 13px; color: var(--nhs-mid-grey);
}
.chart-card__tabs {
display: flex; gap: 0;
border-bottom: 1px solid var(--nhs-pale-grey);
}
.chart-tab {
padding: 10px 24px;
font-size: 14px; font-weight: 600;
color: var(--nhs-mid-grey);
border: none; background: none; cursor: pointer;
border-bottom: 3px solid transparent;
margin-bottom: -1px;
transition: color 0.15s, border-color 0.15s;
}
.chart-tab--active {
color: var(--nhs-blue);
border-bottom-color: var(--nhs-blue);
}
.chart-tab:hover:not(.chart-tab--active) { color: var(--nhs-dark-grey); }
.chart-tab:focus-visible { box-shadow: inset 0 0 0 3px var(--nhs-yellow); }
/* ── Icicle Chart Mock ── */
.icicle {
flex: 1; padding: 20px;
display: flex; flex-direction: column; gap: 3px;
min-height: 420px;
}
.icicle__row {
display: flex; gap: 3px;
flex: 1;
}
.icicle__cell {
display: flex; align-items: center; justify-content: center;
padding: 8px 12px;
color: var(--nhs-white);
font-size: 13px; font-weight: 600;
text-align: center;
position: relative;
overflow: hidden;
transition: filter 0.2s, transform 0.15s;
cursor: pointer;
line-height: 1.2;
}
.icicle__cell:hover { filter: brightness(1.15); transform: scaleY(1.03); z-index: 2; }
.icicle__cell span { position: relative; z-index: 1; text-shadow: 0 1px 2px rgba(0,0,0,0.3); }
.icicle__cell small {
display: block; font-weight: 400; font-size: 11px; opacity: 0.85;
}
/* Level colors */
.lvl-0 { background: var(--nhs-dark-blue); }
.lvl-1a { background: #005EB8; }
.lvl-1b { background: #0072CE; }
.lvl-1c { background: #41B6E6; }
.lvl-1d { background: #768692; }
.lvl-2a { background: #003D7A; }
.lvl-2b { background: #004F9F; }
.lvl-2c { background: #0066B8; }
.lvl-2d { background: #007CC2; }
.lvl-2e { background: #2D9CDB; }
.lvl-2f { background: #5DADE2; }
.lvl-3a { background: #003060; }
.lvl-3b { background: #003D78; }
.lvl-3c { background: #004A90; }
.lvl-3d { background: #0057A8; }
.lvl-3e { background: #0064C0; }
.lvl-3f { background: #2080D0; }
.lvl-3g { background: #4098D8; }
.lvl-3h { background: #60B0E0; }
/* ── Footer ── */
.page-footer {
background: var(--nhs-pale-grey);
border-top: 1px solid #D0D5D6;
padding: 16px 20px;
font-size: 13px; color: var(--nhs-mid-grey);
text-align: center;
}
/* ── Responsive ── */
@media (max-width: 1024px) {
.kpi-row { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 768px) {
.sidebar { display: none; }
.main { margin-left: 0; }
.kpi-row { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<!-- Top Header -->
<header class="top-header">
<div class="top-header__brand">
<div class="top-header__logo">NHS</div>
<div>
<div class="top-header__title">HCD Analysis</div>
</div>
</div>
<div class="top-header__breadcrumb">
Dashboard &rsaquo; <strong>Pathway Analysis</strong>
</div>
<div class="top-header__right">
<span><span class="status-dot"></span> 656,247 records</span>
<span>Last updated: 2h ago</span>
</div>
</header>
<!-- Sidebar -->
<nav class="sidebar" aria-label="Main navigation">
<div class="sidebar__section">
<div class="sidebar__label">Analysis</div>
<a class="sidebar__item sidebar__item--active" href="#">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
Pathway Overview
</a>
<a class="sidebar__item" href="#">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
Drug Selection
</a>
<a class="sidebar__item" href="#">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/></svg>
Trust Selection
</a>
<a class="sidebar__item" href="#">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>
Directory Selection
</a>
<a class="sidebar__item" href="#">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14,2 14,8 20,8"/></svg>
Indications
</a>
</div>
<div class="sidebar__section">
<div class="sidebar__label">Reports</div>
<a class="sidebar__item" href="#">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
Cost Analysis
</a>
<a class="sidebar__item" href="#">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
Export Data
</a>
</div>
<div class="sidebar__footer">
NHS Norfolk &amp; Waveney ICB<br>High Cost Drugs Programme
</div>
</nav>
<!-- Main Content -->
<main class="main">
<!-- KPIs -->
<section class="kpi-row" aria-label="Key performance indicators">
<div class="kpi-card">
<div class="kpi-card__label">Unique Patients</div>
<div class="kpi-card__value">37,842</div>
<div class="kpi-card__sub">across all trusts</div>
</div>
<div class="kpi-card">
<div class="kpi-card__label">Drug Types</div>
<div class="kpi-card__value">89</div>
<div class="kpi-card__sub">high-cost drugs tracked</div>
</div>
<div class="kpi-card">
<div class="kpi-card__label">Total Cost</div>
<div class="kpi-card__value">&pound;145.2M</div>
<div class="kpi-card__sub">current period spend</div>
</div>
<div class="kpi-card kpi-card--green">
<div class="kpi-card__label">Indication Match</div>
<div class="kpi-card__value">93%</div>
<div class="kpi-card__sub">GP diagnosis confirmed</div>
</div>
</section>
<!-- Filter Bar -->
<section class="filter-bar" aria-label="Filters">
<div class="filter-bar__group">
<span class="filter-bar__label">View</span>
<div class="toggle-pills" role="radiogroup" aria-label="Chart view type">
<button class="toggle-pill toggle-pill--active" role="radio" aria-checked="true">By Directory</button>
<button class="toggle-pill" role="radio" aria-checked="false">By Indication</button>
</div>
</div>
<div class="filter-bar__divider"></div>
<div class="filter-bar__group">
<span class="filter-bar__label">Initiated</span>
<select class="filter-select" aria-label="Treatment initiated period">
<option selected>All years</option>
<option>Last 2 years</option>
<option>Last 1 year</option>
</select>
</div>
<div class="filter-bar__group">
<span class="filter-bar__label">Last seen</span>
<select class="filter-select" aria-label="Last seen period">
<option selected>Last 6 months</option>
<option>Last 12 months</option>
</select>
</div>
<div class="filter-bar__divider"></div>
<div class="filter-bar__group">
<span class="filter-bar__label">Drugs</span>
<select class="filter-select" aria-label="Drug filter">
<option>All 89 drugs</option>
</select>
</div>
<div class="filter-bar__group">
<span class="filter-bar__label">Directorates</span>
<select class="filter-select" aria-label="Directorate filter">
<option>All 14 directorates</option>
</select>
</div>
</section>
<!-- Chart Card -->
<section class="chart-card" aria-label="Patient pathway chart">
<div class="chart-card__header">
<div>
<div class="chart-card__title">Patient Pathway Visualization</div>
<div class="chart-card__subtitle">Trust &rarr; Directorate &rarr; Drug &rarr; Patient Pathway</div>
</div>
</div>
<div class="chart-card__tabs" role="tablist">
<button class="chart-tab chart-tab--active" role="tab" aria-selected="true">Icicle</button>
<button class="chart-tab" role="tab" aria-selected="false">Sankey</button>
<button class="chart-tab" role="tab" aria-selected="false">Timeline</button>
</div>
<!-- Icicle Chart -->
<div class="icicle" role="img" aria-label="Icicle chart showing patient treatment pathways">
<!-- Level 0: Root -->
<div class="icicle__row" style="flex: 0.8;">
<div class="icicle__cell lvl-0" style="flex: 1;">
<span>NHS Norfolk and Waveney ICB<small>37,842 patients &middot; &pound;145.2M</small></span>
</div>
</div>
<!-- Level 1: Directorates -->
<div class="icicle__row" style="flex: 1;">
<div class="icicle__cell lvl-1a" style="flex: 40;">
<span>Rheumatology<small>15,137 &middot; 40%</small></span>
</div>
<div class="icicle__cell lvl-1b" style="flex: 28;">
<span>Dermatology<small>10,596 &middot; 28%</small></span>
</div>
<div class="icicle__cell lvl-1c" style="flex: 18;">
<span>Gastroenterology<small>6,812 &middot; 18%</small></span>
</div>
<div class="icicle__cell lvl-1d" style="flex: 14;">
<span>Oncology<small>5,297 &middot; 14%</small></span>
</div>
</div>
<!-- Level 2: Drugs -->
<div class="icicle__row" style="flex: 1;">
<!-- Under Rheumatology -->
<div class="icicle__cell lvl-2a" style="flex: 22;"><span>ADALIMUMAB<small>8,325</small></span></div>
<div class="icicle__cell lvl-2b" style="flex: 12;"><span>ETANERCEPT<small>4,541</small></span></div>
<div class="icicle__cell lvl-2c" style="flex: 6;"><span>TOCILIZUMAB<small>2,271</small></span></div>
<!-- Under Dermatology -->
<div class="icicle__cell lvl-2d" style="flex: 16;"><span>SECUKINUMAB<small>6,057</small></span></div>
<div class="icicle__cell lvl-2e" style="flex: 12;"><span>DUPILUMAB<small>4,539</small></span></div>
<!-- Under Gastro -->
<div class="icicle__cell lvl-2c" style="flex: 10;"><span>INFLIXIMAB<small>3,787</small></span></div>
<div class="icicle__cell lvl-2a" style="flex: 8;"><span>VEDOLIZUMAB<small>3,025</small></span></div>
<!-- Under Oncology -->
<div class="icicle__cell lvl-2f" style="flex: 8;"><span>PEMBROLIZUMAB<small>3,025</small></span></div>
<div class="icicle__cell lvl-2b" style="flex: 6;"><span>NIVOLUMAB<small>2,272</small></span></div>
</div>
<!-- Level 3: Pathways -->
<div class="icicle__row" style="flex: 1.2;">
<div class="icicle__cell lvl-3a" style="flex: 14;"><span>ADA&rarr;ADA<small>5,310</small></span></div>
<div class="icicle__cell lvl-3b" style="flex: 8;"><span>ADA&rarr;ETA&rarr;SEC<small>3,015</small></span></div>
<div class="icicle__cell lvl-3c" style="flex: 7;"><span>ETA&rarr;ETA<small>2,648</small></span></div>
<div class="icicle__cell lvl-3d" style="flex: 5;"><span>ETA&rarr;ADA<small>1,893</small></span></div>
<div class="icicle__cell lvl-3e" style="flex: 6;"><span>TOC&rarr;TOC<small>2,271</small></span></div>
<div class="icicle__cell lvl-3f" style="flex: 10;"><span>SEC&rarr;SEC<small>3,785</small></span></div>
<div class="icicle__cell lvl-3a" style="flex: 6;"><span>SEC&rarr;DUP<small>2,272</small></span></div>
<div class="icicle__cell lvl-3g" style="flex: 6;"><span>DUP&rarr;DUP<small>2,267</small></span></div>
<div class="icicle__cell lvl-3h" style="flex: 6;"><span>DUP&rarr;SEC<small>2,272</small></span></div>
<div class="icicle__cell lvl-3c" style="flex: 6;"><span>INF&rarr;INF<small>2,272</small></span></div>
<div class="icicle__cell lvl-3b" style="flex: 4;"><span>INF&rarr;VED<small>1,515</small></span></div>
<div class="icicle__cell lvl-3d" style="flex: 5;"><span>VED&rarr;VED<small>1,893</small></span></div>
<div class="icicle__cell lvl-3e" style="flex: 3;"><span>VED&rarr;INF<small>1,132</small></span></div>
<div class="icicle__cell lvl-3f" style="flex: 5;"><span>PEM&rarr;PEM<small>1,893</small></span></div>
<div class="icicle__cell lvl-3g" style="flex: 3;"><span>PEM&rarr;NIV<small>1,132</small></span></div>
<div class="icicle__cell lvl-3h" style="flex: 4;"><span>NIV&rarr;NIV<small>1,514</small></span></div>
<div class="icicle__cell lvl-3a" style="flex: 2;"><span>NIV&rarr;PEM<small>758</small></span></div>
</div>
</div>
</section>
</main>
<!-- Footer -->
<footer class="page-footer">
NHS Norfolk and Waveney ICB &mdash; High Cost Drugs Analysis &middot; Data as of Feb 2026
</footer>
<script>
// Toggle pills interaction
document.querySelectorAll('.toggle-pills').forEach(group => {
group.querySelectorAll('.toggle-pill').forEach(pill => {
pill.addEventListener('click', () => {
group.querySelectorAll('.toggle-pill').forEach(p => {
p.classList.remove('toggle-pill--active');
p.setAttribute('aria-checked', 'false');
});
pill.classList.add('toggle-pill--active');
pill.setAttribute('aria-checked', 'true');
});
});
});
// Chart tabs interaction
document.querySelectorAll('.chart-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.chart-tab').forEach(t => {
t.classList.remove('chart-tab--active');
t.setAttribute('aria-selected', 'false');
});
tab.classList.add('chart-tab--active');
tab.setAttribute('aria-selected', 'true');
});
});
</script>
</body>
</html>
+15 -2
View File
@@ -2,7 +2,7 @@
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. You are operating inside an automated loop improving Plotly charts in an NHS patient pathway analysis Dash application. Each iteration you receive fresh context — you have NO memory of previous iterations. Your only memory is the filesystem.
**Current Focus**: Fix chart bugs, improve visual polish, add new analytics charts. See IMPLEMENTATION_PLAN.md for the full task list organized into Phases AD. **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 ## First Actions Every Iteration
@@ -27,8 +27,16 @@ Then run `git log --oneline -5` to see recent commits.
- `dash_app/data/queries.py` — Thin wrappers. Add wrapper for each new query. - `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. - `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: **When modifying UI components**, read:
- `dash_app/components/trust_comparison.py` — TC landing + dashboard layout. - `dash_app/components/trust_comparison.py` — TC landing + dashboard layout (reference for Trends landing/detail pattern).
- `dash_app/assets/nhs.css` — All CSS styles. - `dash_app/assets/nhs.css` — All CSS styles.
## Narration ## Narration
@@ -72,6 +80,10 @@ Work on ONE task per iteration. Build incrementally and verify as you go.
- **SQLite**: `import sqlite3` — read-only access to `data/pathways.db` - **SQLite**: `import sqlite3` — read-only access to `data/pathways.db`
- **CSS**: All in `dash_app/assets/nhs.css` — auto-served by Dash - **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 ### plotly_generator.py Patterns
All chart functions follow the same pattern: All chart functions follow the same pattern:
@@ -235,6 +247,7 @@ DO NOT output it if any task is still `[ ]` or `[B]` or `[~]`.
- **New query functions** go in `src/data_processing/pathway_queries.py` with thin wrappers in `dash_app/data/queries.py` - **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 - **dcc.Store for state** — no server-side globals
- **Lazy tab rendering** — only compute the active tab's chart - **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 - Keep commits atomic and well-described
- If stuck for 2+ attempts, document in progress.txt and move on - If stuck for 2+ attempts, document in progress.txt and move on
- `python run_dash.py` must work after every task - `python run_dash.py` must work after every task
+2 -8
View File
@@ -187,12 +187,12 @@ body {
margin-left: var(--sidebar-w); margin-left: var(--sidebar-w);
margin-top: var(--header-total-h); margin-top: var(--header-total-h);
padding: 24px; padding: 24px;
height: calc(100vh - var(--header-total-h)); min-height: calc(100vh - var(--header-total-h));
display: flex; flex-direction: column; gap: 20px; display: flex; flex-direction: column; gap: 20px;
overflow-y: auto; overflow-y: auto;
} }
/* View containers — flex chain for chart to fill height */ /* View containers */
#view-container { #view-container {
flex: 1; display: flex; flex-direction: column; min-height: 0; flex: 1; display: flex; flex-direction: column; min-height: 0;
} }
@@ -268,12 +268,6 @@ body {
#pathway-chart { #pathway-chart {
flex: 1; min-height: 0; flex: 1; min-height: 0;
} }
/* Propagate flex height into Plotly-rendered divs when no explicit figure height is set */
#pathway-chart .js-plotly-plot,
#pathway-chart .plot-container,
#pathway-chart .svg-container {
height: 100% !important;
}
.chart-card > .dash-loading-callback, .chart-card > .dash-loading-callback,
.chart-card > .dash-loading-callback > div { .chart-card > .dash-loading-callback > div {
flex: 1; display: flex; flex-direction: column; min-height: 0; flex: 1; display: flex; flex-direction: column; min-height: 0;
-2
View File
@@ -106,8 +106,6 @@ def make_chart_card():
children=[ children=[
dcc.Graph( dcc.Graph(
id="pathway-chart", id="pathway-chart",
style={"flex": "1", "minHeight": "0"},
responsive=True,
config={ config={
"displayModeBar": True, "displayModeBar": True,
"displaylogo": False, "displaylogo": False,
+4 -2
View File
@@ -46,7 +46,8 @@ def make_trends_landing():
dcc.Graph( dcc.Graph(
id="trends-overview-chart", id="trends-overview-chart",
config={"displayModeBar": False, "displaylogo": False}, config={"displayModeBar": False, "displaylogo": False},
style={"height": "500px"}, style={"height": "calc(100vh - 220px)", "minHeight": "400px"},
responsive=True,
), ),
]), ]),
], ],
@@ -105,7 +106,8 @@ def make_trends_detail():
dcc.Graph( dcc.Graph(
id="trends-detail-chart", id="trends-detail-chart",
config={"displayModeBar": False, "displaylogo": False}, config={"displayModeBar": False, "displaylogo": False},
style={"height": "500px"}, style={"height": "calc(100vh - 220px)", "minHeight": "400px"},
responsive=True,
), ),
]), ]),
], ],
+22 -12
View File
@@ -21,11 +21,16 @@ If you discover a new failure pattern during your work, add it to this file.
- **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. - **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. - **Why**: Duplicating SQL queries and figure logic creates copies that drift apart.
### Do NOT modify pathways.db schema or data ### Do NOT modify pathways.db schema or data from Dash callbacks
- **When**: Querying the database from Dash callbacks - **When**: Querying the database from Dash callbacks
- **Rule**: Read-only access. Use `sqlite3.connect(db_path)` with SELECT queries only. Never INSERT, UPDATE, DELETE, or ALTER. - **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**: Phase D tasks (D.1 trends) may add new tables — this requires explicit planning. - **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 `python -m cli.refresh_pathways`. The Dash app is a read-only consumer. - **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.
--- ---
@@ -133,12 +138,17 @@ If you discover a new failure pattern during your work, add it to this file.
- **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. - **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. - **Why**: Previous iterations may have changed the file, shifting all line numbers.
<!-- ### 3-view navigation pattern
ADD NEW GUARDRAILS BELOW as failures are observed during the loop. - **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.
Format: ### Trends view state in app-state
### [Short descriptive name] - **When**: Working on the Trends view (E.2E.4)
- **When**: What situation triggers this guardrail? - **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`).
- **Rule**: What must you do (or not do)? - **Why**: Missing initial state causes KeyError on first page load.
- **Why**: What failure prompted adding this guardrail?
--> ### 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.
+25
View File
@@ -262,6 +262,31 @@ while ($true) {
Write-Host " [ERROR] API overloaded after $maxRetries retries, giving up." -ForegroundColor Red 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) } while ($apiOverloaded -and $retryCount -lt $maxRetries)
$outputString | Set-Content -Path $logFile -Encoding UTF8 $outputString | Set-Content -Path $logFile -Encoding UTF8
+39 -24
View File
@@ -135,6 +135,8 @@ def _base_layout(title: str, **overrides) -> dict:
plot_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
autosize=True, autosize=True,
font=dict(family=CHART_FONT_FAMILY), font=dict(family=CHART_FONT_FAMILY),
xaxis=dict(automargin=True),
yaxis=dict(automargin=True),
) )
layout.update(overrides) layout.update(overrides)
return layout return layout
@@ -337,6 +339,7 @@ def create_icicle_from_nodes(nodes: list[dict], title: str = "") -> go.Figure:
layout = _base_layout( layout = _base_layout(
display_title, display_title,
margin=dict(t=40, l=8, r=8, b=24), margin=dict(t=40, l=8, r=8, b=24),
height=700,
hoverlabel=dict( hoverlabel=dict(
bgcolor="#FFFFFF", bgcolor="#FFFFFF",
bordercolor="#CBD5E1", bordercolor="#CBD5E1",
@@ -444,7 +447,7 @@ def create_market_share_figure(data: list[dict], title: str = "") -> go.Figure:
yaxis=dict(title="", automargin=True), yaxis=dict(title="", automargin=True),
legend=_smart_legend(n_drugs, legend_title="Drug"), legend=_smart_legend(n_drugs, legend_title="Drug"),
margin=dict(t=50, l=8, **legend_margins), margin=dict(t=50, l=8, **legend_margins),
height=max(400, len(seen_dirs) * 60 + 200), height=max(600, len(seen_dirs) * 60 + 200),
) )
fig.update_layout(**layout) fig.update_layout(**layout)
@@ -607,8 +610,8 @@ def create_cost_effectiveness_figure(
automargin=True, automargin=True,
tickfont=dict(size=11), tickfont=dict(size=11),
), ),
margin=dict(t=50, l=8, r=24, b=40), margin=dict(t=50, l=8, r=80, b=40),
height=max(450, len(filtered) * 28 + 150), height=max(600, len(filtered) * 28 + 150),
) )
fig.update_layout(**layout) fig.update_layout(**layout)
@@ -719,8 +722,10 @@ def create_cost_waterfall_figure(
gridcolor=GRID_COLOR, gridcolor=GRID_COLOR,
zeroline=True, zeroline=True,
zerolinecolor="#CBD5E1", zerolinecolor="#CBD5E1",
automargin=True,
), ),
margin=dict(t=60, l=8, r=24, b=40), margin=dict(t=60, l=8, r=24, b=80),
height=max(600, len(data) * 50 + 200),
showlegend=False, showlegend=False,
bargap=0.25, bargap=0.25,
) )
@@ -833,7 +838,7 @@ def create_sankey_figure(
layout.update( layout.update(
font=dict(family=CHART_FONT_FAMILY, size=12), font=dict(family=CHART_FONT_FAMILY, size=12),
margin=dict(t=60, l=30, r=30, b=30), margin=dict(t=60, l=30, r=30, b=30),
height=max(500, len(unique_bases) * 35 + 200), height=max(600, len(unique_bases) * 35 + 200),
) )
fig.update_layout(**layout) fig.update_layout(**layout)
@@ -889,7 +894,7 @@ def create_dosing_figure(
), ),
yaxis=dict(automargin=True, tickfont=dict(size=11)), yaxis=dict(automargin=True, tickfont=dict(size=11)),
margin=dict(t=60, l=20, **legend_margins), margin=dict(t=60, l=20, **legend_margins),
height=max(450, n_rows * 40 + 150), height=max(600, n_rows * 40 + 150),
bargap=0.15, bargap=0.15,
bargroupgap=0.05, bargroupgap=0.05,
showlegend=True, showlegend=True,
@@ -1280,7 +1285,7 @@ def create_heatmap_figure(
chart_title = f"{chart_title}{title}" chart_title = f"{chart_title}{title}"
n_dirs = len(directories) n_dirs = len(directories)
fig_height = max(400, 80 + n_dirs * 40) fig_height = max(600, 80 + n_dirs * 40)
layout = _base_layout(chart_title) layout = _base_layout(chart_title)
layout.update( layout.update(
@@ -1289,6 +1294,7 @@ def create_heatmap_figure(
tickfont=dict(size=11, color="#425563"), tickfont=dict(size=11, color="#425563"),
tickangle=-45, tickangle=-45,
side="bottom", side="bottom",
automargin=True,
), ),
yaxis=dict( yaxis=dict(
title="", title="",
@@ -1426,7 +1432,7 @@ def create_duration_figure(
chart_title += f"<br><span style='font-size:13px;color:{ANNOTATION_COLOR}'>{title}</span>" chart_title += f"<br><span style='font-size:13px;color:{ANNOTATION_COLOR}'>{title}</span>"
n_bars = len(data) n_bars = len(data)
fig_height = max(400, 40 + n_bars * 28) fig_height = max(600, 40 + n_bars * 28)
layout = _base_layout(chart_title) layout = _base_layout(chart_title)
layout.update( layout.update(
@@ -1444,7 +1450,7 @@ def create_duration_figure(
automargin=True, automargin=True,
autorange="reversed", autorange="reversed",
), ),
margin=dict(t=60, l=8, r=80, b=50), margin=dict(t=60, l=8, r=100, b=50),
height=fig_height, height=fig_height,
showlegend=False, showlegend=False,
) )
@@ -1652,10 +1658,10 @@ def create_trust_heatmap_figure(
layout = _base_layout(chart_title) layout = _base_layout(chart_title)
layout.update( layout.update(
xaxis=dict(title="", tickfont=dict(size=11, color="#425563"), tickangle=-45, side="bottom"), xaxis=dict(title="", tickfont=dict(size=11, color="#425563"), tickangle=-45, side="bottom", automargin=True),
yaxis=dict(title="", tickfont=dict(size=12, color="#425563"), autorange="reversed", automargin=True), yaxis=dict(title="", tickfont=dict(size=12, color="#425563"), autorange="reversed", automargin=True),
margin=dict(t=60, l=8, r=80, b=120), margin=dict(t=60, l=8, r=80, b=120),
height=max(300, 80 + n_trusts * 50), height=max(400, 80 + n_trusts * 50),
) )
fig.update_layout(**layout) fig.update_layout(**layout)
@@ -1814,7 +1820,7 @@ def create_retention_funnel_figure(
layout.update( layout.update(
margin=dict(t=60, l=8, r=8, b=40), margin=dict(t=60, l=8, r=8, b=40),
yaxis=dict(automargin=True), yaxis=dict(automargin=True),
height=max(300, len(data) * 80 + 120), height=max(600, len(data) * 80 + 120),
) )
fig.update_layout(**layout) fig.update_layout(**layout)
@@ -1884,7 +1890,7 @@ def create_pathway_depth_figure(
title="Patients", title="Patients",
gridcolor=GRID_COLOR, gridcolor=GRID_COLOR,
), ),
height=max(300, len(data) * 70 + 120), height=max(600, len(data) * 70 + 120),
bargap=0.3, bargap=0.3,
) )
fig.update_layout(**layout) fig.update_layout(**layout)
@@ -1979,6 +1985,7 @@ def create_duration_cost_scatter_figure(
layout = _base_layout(display_title) layout = _base_layout(display_title)
layout.update( layout.update(
margin=dict(t=60, l=8, **legend_margins), margin=dict(t=60, l=8, **legend_margins),
height=600,
xaxis=dict( xaxis=dict(
title="Average Treatment Duration (days)", title="Average Treatment Duration (days)",
gridcolor=GRID_COLOR, gridcolor=GRID_COLOR,
@@ -2076,6 +2083,7 @@ def create_drug_network_figure(data: dict, title: str = "") -> go.Figure:
layout = _base_layout(display_title) layout = _base_layout(display_title)
layout.update( layout.update(
margin=dict(t=60, l=24, r=24, b=24), margin=dict(t=60, l=24, r=24, b=24),
height=600,
xaxis=dict(visible=False, scaleanchor="y", scaleratio=1), xaxis=dict(visible=False, scaleanchor="y", scaleratio=1),
yaxis=dict(visible=False), yaxis=dict(visible=False),
) )
@@ -2175,7 +2183,7 @@ def create_drug_timeline_figure(data: list[dict], title: str = "") -> go.Figure:
# Layout # Layout
n_bars = len(data) n_bars = len(data)
bar_height = 28 bar_height = 28
dynamic_height = max(400, n_bars * bar_height + 120) dynamic_height = max(600, n_bars * bar_height + 120)
n_dirs = len(directories) n_dirs = len(directories)
legend_margins = _smart_legend_margin(n_dirs) legend_margins = _smart_legend_margin(n_dirs)
@@ -2268,7 +2276,7 @@ def create_dosing_distribution_figure(
n_bars = len(sorted_data) n_bars = len(sorted_data)
bar_height = 24 bar_height = 24
dynamic_height = max(400, n_bars * bar_height + 120) dynamic_height = max(600, n_bars * bar_height + 120)
n_dirs = len(directories) n_dirs = len(directories)
legend_margins = _smart_legend_margin(n_dirs) legend_margins = _smart_legend_margin(n_dirs)
@@ -2322,25 +2330,30 @@ def create_trend_figure(
display_title = title or "Temporal Trends" display_title = title or "Temporal Trends"
# Group data by name (drug or directory) # Group data by name (drug or directory), sorting periods chronologically
from collections import defaultdict from collections import defaultdict
series = defaultdict(lambda: {"periods": [], "values": []}) series = defaultdict(list)
for row in data: for row in data:
name = row.get("name", "") name = row.get("name", "")
series[name]["periods"].append(row["period_end"]) series[name].append((row["period_end"], row.get("value", 0)))
series[name]["values"].append(row.get("value", 0))
# Sort each series by period
for name in series:
series[name].sort(key=lambda x: x[0])
n_series = len(series) n_series = len(series)
fig = go.Figure() fig = go.Figure()
for i, (name, s) in enumerate(sorted(series.items())): for i, (name, points) in enumerate(sorted(series.items())):
periods = [p[0] for p in points]
values = [p[1] for p in points]
colour = DRUG_PALETTE[i % len(DRUG_PALETTE)] colour = DRUG_PALETTE[i % len(DRUG_PALETTE)]
fig.add_trace(go.Scatter( fig.add_trace(go.Scatter(
x=s["periods"], x=periods,
y=s["values"], y=values,
mode="lines+markers", mode="lines+markers",
name=name, name=name,
customdata=[name] * len(s["periods"]), customdata=[name] * len(periods),
line=dict(color=colour, width=2), line=dict(color=colour, width=2),
marker=dict(color=colour, size=6), marker=dict(color=colour, size=6),
hovertemplate=( hovertemplate=(
@@ -2365,7 +2378,9 @@ def create_trend_figure(
xaxis=dict( xaxis=dict(
title="Period", title="Period",
gridcolor=GRID_COLOR, gridcolor=GRID_COLOR,
type="category", type="date",
dtick="M6",
tickformat="%b %Y",
), ),
yaxis=dict( yaxis=dict(
title=y_label, title=y_label,