test
This commit is contained in:
+2
-16
@@ -37,25 +37,11 @@ logs/*.jsonl
|
||||
.states/
|
||||
|
||||
# SQLite database (will contain local data)
|
||||
*.db
|
||||
*.sqlite
|
||||
#*.db
|
||||
#*.sqlite
|
||||
|
||||
# Snowflake result cache
|
||||
data/cache/
|
||||
|
||||
# Uploaded data files
|
||||
data/uploads/
|
||||
|
||||
# Exported analysis results
|
||||
data/exports/
|
||||
|
||||
# Analysis output files
|
||||
output/*.html
|
||||
output/*.csv
|
||||
*.html
|
||||
|
||||
# VS Code workspace settings
|
||||
.vscode/
|
||||
|
||||
# User uploaded files
|
||||
uploaded_files/
|
||||
|
||||
@@ -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 › <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 & 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">£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 → Directorate → Drug → 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 · £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 · 40%</small></span>
|
||||
</div>
|
||||
<div class="icicle__cell lvl-1b" style="flex: 28;">
|
||||
<span>Dermatology<small>10,596 · 28%</small></span>
|
||||
</div>
|
||||
<div class="icicle__cell lvl-1c" style="flex: 18;">
|
||||
<span>Gastroenterology<small>6,812 · 18%</small></span>
|
||||
</div>
|
||||
<div class="icicle__cell lvl-1d" style="flex: 14;">
|
||||
<span>Oncology<small>5,297 · 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→ADA<small>5,310</small></span></div>
|
||||
<div class="icicle__cell lvl-3b" style="flex: 8;"><span>ADA→ETA→SEC<small>3,015</small></span></div>
|
||||
<div class="icicle__cell lvl-3c" style="flex: 7;"><span>ETA→ETA<small>2,648</small></span></div>
|
||||
<div class="icicle__cell lvl-3d" style="flex: 5;"><span>ETA→ADA<small>1,893</small></span></div>
|
||||
<div class="icicle__cell lvl-3e" style="flex: 6;"><span>TOC→TOC<small>2,271</small></span></div>
|
||||
<div class="icicle__cell lvl-3f" style="flex: 10;"><span>SEC→SEC<small>3,785</small></span></div>
|
||||
<div class="icicle__cell lvl-3a" style="flex: 6;"><span>SEC→DUP<small>2,272</small></span></div>
|
||||
<div class="icicle__cell lvl-3g" style="flex: 6;"><span>DUP→DUP<small>2,267</small></span></div>
|
||||
<div class="icicle__cell lvl-3h" style="flex: 6;"><span>DUP→SEC<small>2,272</small></span></div>
|
||||
<div class="icicle__cell lvl-3c" style="flex: 6;"><span>INF→INF<small>2,272</small></span></div>
|
||||
<div class="icicle__cell lvl-3b" style="flex: 4;"><span>INF→VED<small>1,515</small></span></div>
|
||||
<div class="icicle__cell lvl-3d" style="flex: 5;"><span>VED→VED<small>1,893</small></span></div>
|
||||
<div class="icicle__cell lvl-3e" style="flex: 3;"><span>VED→INF<small>1,132</small></span></div>
|
||||
<div class="icicle__cell lvl-3f" style="flex: 5;"><span>PEM→PEM<small>1,893</small></span></div>
|
||||
<div class="icicle__cell lvl-3g" style="flex: 3;"><span>PEM→NIV<small>1,132</small></span></div>
|
||||
<div class="icicle__cell lvl-3h" style="flex: 4;"><span>NIV→NIV<small>1,514</small></span></div>
|
||||
<div class="icicle__cell lvl-3a" style="flex: 2;"><span>NIV→PEM<small>758</small></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="page-footer">
|
||||
NHS Norfolk and Waveney ICB — High Cost Drugs Analysis · 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
@@ -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.
|
||||
|
||||
**Current Focus**: Fix chart bugs, improve visual polish, add new analytics charts. See IMPLEMENTATION_PLAN.md for the full task list organized into Phases A–D.
|
||||
**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 A–E.
|
||||
|
||||
## 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/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.
|
||||
- `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
|
||||
@@ -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`
|
||||
- **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:
|
||||
@@ -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`
|
||||
- **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
|
||||
|
||||
@@ -187,12 +187,12 @@ body {
|
||||
margin-left: var(--sidebar-w);
|
||||
margin-top: var(--header-total-h);
|
||||
padding: 24px;
|
||||
height: calc(100vh - var(--header-total-h));
|
||||
min-height: calc(100vh - var(--header-total-h));
|
||||
display: flex; flex-direction: column; gap: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* View containers — flex chain for chart to fill height */
|
||||
/* View containers */
|
||||
#view-container {
|
||||
flex: 1; display: flex; flex-direction: column; min-height: 0;
|
||||
}
|
||||
@@ -268,12 +268,6 @@ body {
|
||||
#pathway-chart {
|
||||
flex: 1; min-height: 0;
|
||||
}
|
||||
/* Propagate flex height into Plotly-rendered divs when no explicit figure height is set */
|
||||
#pathway-chart .js-plotly-plot,
|
||||
#pathway-chart .plot-container,
|
||||
#pathway-chart .svg-container {
|
||||
height: 100% !important;
|
||||
}
|
||||
.chart-card > .dash-loading-callback,
|
||||
.chart-card > .dash-loading-callback > div {
|
||||
flex: 1; display: flex; flex-direction: column; min-height: 0;
|
||||
|
||||
@@ -106,8 +106,6 @@ def make_chart_card():
|
||||
children=[
|
||||
dcc.Graph(
|
||||
id="pathway-chart",
|
||||
style={"flex": "1", "minHeight": "0"},
|
||||
responsive=True,
|
||||
config={
|
||||
"displayModeBar": True,
|
||||
"displaylogo": False,
|
||||
|
||||
@@ -46,7 +46,8 @@ def make_trends_landing():
|
||||
dcc.Graph(
|
||||
id="trends-overview-chart",
|
||||
config={"displayModeBar": False, "displaylogo": False},
|
||||
style={"height": "500px"},
|
||||
style={"height": "calc(100vh - 220px)", "minHeight": "400px"},
|
||||
responsive=True,
|
||||
),
|
||||
]),
|
||||
],
|
||||
@@ -105,7 +106,8 @@ def make_trends_detail():
|
||||
dcc.Graph(
|
||||
id="trends-detail-chart",
|
||||
config={"displayModeBar": False, "displaylogo": False},
|
||||
style={"height": "500px"},
|
||||
style={"height": "calc(100vh - 220px)", "minHeight": "400px"},
|
||||
responsive=True,
|
||||
),
|
||||
]),
|
||||
],
|
||||
|
||||
+22
-12
@@ -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.
|
||||
- **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
|
||||
- **Rule**: Read-only access. Use `sqlite3.connect(db_path)` with SELECT queries only. Never INSERT, UPDATE, DELETE, or ALTER.
|
||||
- **Exception**: Phase D tasks (D.1 trends) may add new tables — this requires explicit planning.
|
||||
- **Why**: pathways.db is populated by `python -m cli.refresh_pathways`. The Dash app is a read-only consumer.
|
||||
- **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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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.
|
||||
- **Why**: Previous iterations may have changed the file, shifting all line numbers.
|
||||
|
||||
<!--
|
||||
ADD NEW GUARDRAILS BELOW as failures are observed during the loop.
|
||||
### 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.
|
||||
|
||||
Format:
|
||||
### [Short descriptive name]
|
||||
- **When**: What situation triggers this guardrail?
|
||||
- **Rule**: What must you do (or not do)?
|
||||
- **Why**: What failure prompted adding this guardrail?
|
||||
-->
|
||||
### Trends view state in app-state
|
||||
- **When**: Working on the Trends view (E.2–E.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.
|
||||
|
||||
@@ -262,6 +262,31 @@ while ($true) {
|
||||
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
|
||||
|
||||
@@ -135,6 +135,8 @@ def _base_layout(title: str, **overrides) -> dict:
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
autosize=True,
|
||||
font=dict(family=CHART_FONT_FAMILY),
|
||||
xaxis=dict(automargin=True),
|
||||
yaxis=dict(automargin=True),
|
||||
)
|
||||
layout.update(overrides)
|
||||
return layout
|
||||
@@ -337,6 +339,7 @@ def create_icicle_from_nodes(nodes: list[dict], title: str = "") -> go.Figure:
|
||||
layout = _base_layout(
|
||||
display_title,
|
||||
margin=dict(t=40, l=8, r=8, b=24),
|
||||
height=700,
|
||||
hoverlabel=dict(
|
||||
bgcolor="#FFFFFF",
|
||||
bordercolor="#CBD5E1",
|
||||
@@ -444,7 +447,7 @@ def create_market_share_figure(data: list[dict], title: str = "") -> go.Figure:
|
||||
yaxis=dict(title="", automargin=True),
|
||||
legend=_smart_legend(n_drugs, legend_title="Drug"),
|
||||
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)
|
||||
|
||||
@@ -607,8 +610,8 @@ def create_cost_effectiveness_figure(
|
||||
automargin=True,
|
||||
tickfont=dict(size=11),
|
||||
),
|
||||
margin=dict(t=50, l=8, r=24, b=40),
|
||||
height=max(450, len(filtered) * 28 + 150),
|
||||
margin=dict(t=50, l=8, r=80, b=40),
|
||||
height=max(600, len(filtered) * 28 + 150),
|
||||
)
|
||||
fig.update_layout(**layout)
|
||||
|
||||
@@ -719,8 +722,10 @@ def create_cost_waterfall_figure(
|
||||
gridcolor=GRID_COLOR,
|
||||
zeroline=True,
|
||||
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,
|
||||
bargap=0.25,
|
||||
)
|
||||
@@ -833,7 +838,7 @@ def create_sankey_figure(
|
||||
layout.update(
|
||||
font=dict(family=CHART_FONT_FAMILY, size=12),
|
||||
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)
|
||||
|
||||
@@ -889,7 +894,7 @@ def create_dosing_figure(
|
||||
),
|
||||
yaxis=dict(automargin=True, tickfont=dict(size=11)),
|
||||
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,
|
||||
bargroupgap=0.05,
|
||||
showlegend=True,
|
||||
@@ -1280,7 +1285,7 @@ def create_heatmap_figure(
|
||||
chart_title = f"{chart_title} — {title}"
|
||||
|
||||
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.update(
|
||||
@@ -1289,6 +1294,7 @@ def create_heatmap_figure(
|
||||
tickfont=dict(size=11, color="#425563"),
|
||||
tickangle=-45,
|
||||
side="bottom",
|
||||
automargin=True,
|
||||
),
|
||||
yaxis=dict(
|
||||
title="",
|
||||
@@ -1426,7 +1432,7 @@ def create_duration_figure(
|
||||
chart_title += f"<br><span style='font-size:13px;color:{ANNOTATION_COLOR}'>{title}</span>"
|
||||
|
||||
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.update(
|
||||
@@ -1444,7 +1450,7 @@ def create_duration_figure(
|
||||
automargin=True,
|
||||
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,
|
||||
showlegend=False,
|
||||
)
|
||||
@@ -1652,10 +1658,10 @@ def create_trust_heatmap_figure(
|
||||
|
||||
layout = _base_layout(chart_title)
|
||||
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),
|
||||
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)
|
||||
|
||||
@@ -1814,7 +1820,7 @@ def create_retention_funnel_figure(
|
||||
layout.update(
|
||||
margin=dict(t=60, l=8, r=8, b=40),
|
||||
yaxis=dict(automargin=True),
|
||||
height=max(300, len(data) * 80 + 120),
|
||||
height=max(600, len(data) * 80 + 120),
|
||||
)
|
||||
fig.update_layout(**layout)
|
||||
|
||||
@@ -1884,7 +1890,7 @@ def create_pathway_depth_figure(
|
||||
title="Patients",
|
||||
gridcolor=GRID_COLOR,
|
||||
),
|
||||
height=max(300, len(data) * 70 + 120),
|
||||
height=max(600, len(data) * 70 + 120),
|
||||
bargap=0.3,
|
||||
)
|
||||
fig.update_layout(**layout)
|
||||
@@ -1979,6 +1985,7 @@ def create_duration_cost_scatter_figure(
|
||||
layout = _base_layout(display_title)
|
||||
layout.update(
|
||||
margin=dict(t=60, l=8, **legend_margins),
|
||||
height=600,
|
||||
xaxis=dict(
|
||||
title="Average Treatment Duration (days)",
|
||||
gridcolor=GRID_COLOR,
|
||||
@@ -2076,6 +2083,7 @@ def create_drug_network_figure(data: dict, title: str = "") -> go.Figure:
|
||||
layout = _base_layout(display_title)
|
||||
layout.update(
|
||||
margin=dict(t=60, l=24, r=24, b=24),
|
||||
height=600,
|
||||
xaxis=dict(visible=False, scaleanchor="y", scaleratio=1),
|
||||
yaxis=dict(visible=False),
|
||||
)
|
||||
@@ -2175,7 +2183,7 @@ def create_drug_timeline_figure(data: list[dict], title: str = "") -> go.Figure:
|
||||
# Layout
|
||||
n_bars = len(data)
|
||||
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)
|
||||
legend_margins = _smart_legend_margin(n_dirs)
|
||||
@@ -2268,7 +2276,7 @@ def create_dosing_distribution_figure(
|
||||
|
||||
n_bars = len(sorted_data)
|
||||
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)
|
||||
legend_margins = _smart_legend_margin(n_dirs)
|
||||
@@ -2322,25 +2330,30 @@ def create_trend_figure(
|
||||
|
||||
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
|
||||
series = defaultdict(lambda: {"periods": [], "values": []})
|
||||
series = defaultdict(list)
|
||||
for row in data:
|
||||
name = row.get("name", "")
|
||||
series[name]["periods"].append(row["period_end"])
|
||||
series[name]["values"].append(row.get("value", 0))
|
||||
series[name].append((row["period_end"], 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)
|
||||
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)]
|
||||
fig.add_trace(go.Scatter(
|
||||
x=s["periods"],
|
||||
y=s["values"],
|
||||
x=periods,
|
||||
y=values,
|
||||
mode="lines+markers",
|
||||
name=name,
|
||||
customdata=[name] * len(s["periods"]),
|
||||
customdata=[name] * len(periods),
|
||||
line=dict(color=colour, width=2),
|
||||
marker=dict(color=colour, size=6),
|
||||
hovertemplate=(
|
||||
@@ -2365,7 +2378,9 @@ def create_trend_figure(
|
||||
xaxis=dict(
|
||||
title="Period",
|
||||
gridcolor=GRID_COLOR,
|
||||
type="category",
|
||||
type="date",
|
||||
dtick="M6",
|
||||
tickformat="%b %Y",
|
||||
),
|
||||
yaxis=dict(
|
||||
title=y_label,
|
||||
|
||||
Reference in New Issue
Block a user