Project

General

Profile

Activity #4403 » analytics_dashboard.html

Thara S Pillai, 11/12/2025 03:02 PM

 
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>DigiPatho — Camp Analytics (Prototype)</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<style>
:root{
--bg:#0f1724; --card:#0b1220; --muted:#94a3b8; --accent:#06b6d4; --accent-2:#60a5fa;
--card-radius:10px; --gap:12px; --text:#e6eef8;
}
html,body{height:100%; margin:0; font-family:Inter,system-ui,Segoe UI,Roboto,"Helvetica Neue",Arial; background:linear-gradient(180deg,#071226 0%, #071826 100%); color:var(--text);}
.container{max-width:1200px; margin:18px auto; padding:18px;}
.top-row{display:flex; gap:var(--gap); margin-bottom:12px;}
.map-card{flex:1; min-height:260px; border-radius:var(--card-radius); overflow:hidden; background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); box-shadow: 0 6px 18px rgba(2,6,23,0.6); border:1px solid rgba(255,255,255,0.03);}
#map{height:260px;}
.right-column{width:360px; display:flex; flex-direction:column; gap:var(--gap);}
.kpi-row{display:flex; gap:10px; margin-bottom:12px; flex-wrap:wrap;}
.kpi{flex:1; min-width:140px; background:var(--card); padding:14px; border-radius:8px; border:1px solid rgba(255,255,255,0.03); text-align:center;}
.kpi h3{margin:0; font-size:12px; color:var(--muted); font-weight:600;}
.kpi p{margin:6px 0 0; font-size:20px; font-weight:700;}
.controls{display:flex; gap:8px; align-items:center; margin-bottom:8px;}
.card{background:var(--card); padding:12px; border-radius:8px; border:1px solid rgba(255,255,255,0.03);}
.grid{display:grid; grid-template-columns: 2fr 1fr; gap:12px; margin-top:12px;}
canvas{background:transparent;}
.leaderboard{max-height:240px; overflow:auto;}
table{width:100%; border-collapse:collapse; font-size:13px;}
th,td{padding:8px; text-align:left; border-bottom:1px dashed rgba(255,255,255,0.03);}
th{font-size:12px; color:var(--muted); font-weight:600;}
.btn{background:var(--accent); color:#022; border:none; padding:8px 10px; border-radius:6px; cursor:pointer; font-weight:700;}
.small{font-size:12px; color:var(--muted);}
.camp-panel{position:fixed; right:18px; top:18%; width:420px; max-width:92%; background:linear-gradient(180deg,#071226,#061827); border-radius:12px; padding:0; box-shadow:0 12px 40px rgba(2,6,23,0.8); border:1px solid rgba(255,255,255,0.04); display:none; z-index:999;}
.camp-panel h4{margin:0 0 6px;}
.drag-header{background:linear-gradient(90deg,#06b6d4,#0891b2); padding:6px 14px; border-radius:12px 12px 0 0; cursor:move; user-select:none; text-align:center; color:#fff; font-weight:600; font-size:12px;}
.drag-header:active{cursor:grabbing; background:linear-gradient(90deg,#0891b2,#0e7490);}
.camp-panel > div:not(.drag-header){padding:0 14px 14px 14px;}
.pill{display:inline-block; padding:6px 8px; background:rgba(255,255,255,0.03); border-radius:999px; font-size:12px; margin-right:6px;}
.muted{color:var(--muted);}
.legend{display:flex; gap:8px; margin-top:8px; flex-wrap:wrap;}
.caveat{font-size:12px; color:#fffb; margin-top:10px; background:rgba(255,255,255,0.01); padding:8px; border-radius:6px;}
.top-nav{background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.005)); padding:12px 0; border-bottom:1px solid rgba(255,255,255,0.05); margin-bottom:0;}
.nav-center{max-width:1200px; margin:0 auto; display:flex; justify-content:center; gap:24px; flex-wrap:wrap; align-items:center; padding:0 18px;}
.nav-center a{color:var(--muted); text-decoration:none; font-weight:600; font-size:14px; padding:8px 12px; border-radius:6px; transition:color .2s ease, background .2s ease;}
.nav-center a:hover{color:var(--accent-2); background:rgba(96,165,250,0.08);}
.nav-center a.active{color:var(--accent); background:rgba(6,182,212,0.12); box-shadow:inset 0 -2px 0 var(--accent);}
</style>
</head>
<body>
<div class="top-nav">
<div class="nav-center">
<a href="#camp-analytics" class="active">Camp Analytics</a>
<a href="#disease-analytics">Disease Analytics</a>
<a href="#survey-analytics">Survey Analytics</a>
<a href="#sample-diagnostic">Sample & Diagnostic Analytics</a>
<a href="#operational">Operational Performance</a>
<a href="#impact-planning">Impact & Planning Insight</a>
</div>
</div>

<div class="container">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
<h2 style="margin:0;">DigiPatho — Camp Analytics</h2>
<div style="display:flex;gap:12px;align-items:center;">
<div>
<select id="windowSelector" style="padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,0.03);background:#071226;color:var(--text);">
<option value="30">30 days</option>
<option value="60">60 days</option>
<option value="90" selected>90 days</option>
</select>
</div>
<button class="btn" id="refreshBtn">Refresh</button>
</div>
</div>

<!-- Filtering Controls -->
<div class="card" style="margin-bottom:16px; padding:16px;">
<div style="display:flex; gap:12px; align-items:end; flex-wrap:wrap;">
<div style="flex:1; min-width:150px;">
<label style="display:block; font-size:12px; color:var(--muted); margin-bottom:6px; font-weight:600;">District</label>
<select id="districtFilter" style="width:100%; padding:8px; border-radius:8px; border:1px solid rgba(255,255,255,0.08); background:#071226; color:var(--text);">
<option value="all" selected>All</option>
<option value="wayanad">Wayanad</option>
<option value="thiruvananthapuram">Thiruvananthapuram</option>
<option value="kollam">Kollam</option>
<option value="pathanamthitta">Pathanamthitta</option>
<option value="alappuzha">Alappuzha</option>
<option value="kottayam">Kottayam</option>
<option value="idukki">Idukki</option>
<option value="ernakulam">Ernakulam</option>
<option value="thrissur">Thrissur</option>
<option value="palakkad">Palakkad</option>
<option value="malappuram">Malappuram</option>
<option value="kozhikode">Kozhikode</option>
<option value="kannur">Kannur</option>
<option value="kasaragod">Kasaragod</option>
</select>
</div>

<div style="flex:1; min-width:150px;">
<label style="display:block; font-size:12px; color:var(--muted); margin-bottom:6px; font-weight:600;">PHC/Hub</label>
<select id="hubFilter" style="width:100%; padding:8px; border-radius:8px; border:1px solid rgba(255,255,255,0.08); background:#071226; color:var(--text);">
<option value="all" selected>All</option>
<option value="manathavady">Manathavady Hub</option>
<option value="kalpetta">Kalpetta Hub</option>
<option value="sulthan-bathery">Sulthan Bathery Hub</option>
</select>
</div>

<div style="flex:1; min-width:140px;">
<label style="display:block; font-size:12px; color:var(--muted); margin-bottom:6px; font-weight:600;">Date From</label>
<input type="date" id="dateFrom" style="width:100%; padding:8px; border-radius:8px; border:1px solid rgba(255,255,255,0.08); background:#071226; color:var(--text);">
</div>

<div style="flex:1; min-width:140px;">
<label style="display:block; font-size:12px; color:var(--muted); margin-bottom:6px; font-weight:600;">Date To</label>
<input type="date" id="dateTo" style="width:100%; padding:8px; border-radius:8px; border:1px solid rgba(255,255,255,0.08); background:#071226; color:var(--text);">
</div>

<div>
<button class="btn" id="applyFilters" style="padding:9px 20px;">Apply</button>
</div>
</div>
</div>

<!-- KPI Cards at top -->
<div class="kpi-row" style="margin-bottom:12px;">
<div class="kpi">
<h3>Total Camps Conducted</h3>
<p id="kpiCamps"></p>
</div>
<div class="kpi">
<h3>Total Screening Visits</h3>
<p id="kpiVisits"></p>
</div>
<div class="kpi">
<h3>Total Samples Collected</h3>
<p id="kpiSamples"></p>
</div>
<div class="kpi">
<h3>Total Positives (camp-linked)</h3>
<p id="kpiPositives"></p>
</div>
<div class="kpi">
<h3>Referral Conversion Rate</h3>
<p id="kpiConversion"></p>
</div>
<div class="kpi">
<h3>Median Referral Lag (days)</h3>
<p id="kpiLag"></p>
</div>
</div>

<!-- top area: map left, leaderboard right -->
<div class="top-row">
<div class="map-card card">
<div id="map"></div>
</div>

<div class="right-column">
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;">
<div>
<div class="small muted">Top Camps</div>
<div style="font-weight:700;margin-top:6px;">Leaderboards</div>
</div>
<div class="small muted">sortable</div>
</div>
<div class="leaderboard" id="leaderboard" style="margin-top:8px;"></div>
</div>

</div>
</div>

<div class="grid">
<!-- left: charts -->
<div>
<div class="card" style="margin-bottom:12px;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<div>
<div class="small muted">Camp-wise Screening Volume</div>
<div style="font-weight:700;margin-top:6px;">Camp-wise Person Count</div>
</div>
<div class="small muted">Click bar → camp detail</div>
</div>
<div style="height:180px;margin-top:12px;"><canvas id="barChart"></canvas></div>
</div>

<div style="display:flex;gap:12px;">
<div class="card" style="flex:1;">
<div class="small muted">Referral → Attendance (program-level)</div>
<div style="font-weight:700;margin-top:6px;">Conversion Funnel</div>
<div style="height:220px;margin-top:12px;"><canvas id="funnelChart"></canvas></div>
<div class="caveat">Conversion is program-level cohort: advised (survey) → attended any camp within selected window → screened → diagnosed.</div>
</div>

<div class="card" style="width:360px;">
<div class="small muted">Referral-to-Attendance Lag</div>
<div style="font-weight:700;margin-top:6px;">Lag Distribution (days)</div>
<div style="height:200px;margin-top:12px;"><canvas id="lagChart"></canvas></div>
<div class="small muted" style="margin-top:8px;">Median / IQR shown in KPI row</div>
</div>
</div>

</div>

<!-- right: small summary -->
<div>
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;">
<div><div class="small muted">Conversion by Hub</div><div style="font-weight:700;margin-top:6px;">Hub Distribution</div></div>
<div class="small muted">Sankey-style</div>
</div>
<div id="hubBars" style="margin-top:12px;"></div>
</div>

<div class="card" style="margin-top:12px;">
<div class="small muted">Operational</div>
<div style="font-weight:700;margin-top:6px;">Summary</div>
<table style="margin-top:8px;">
<thead><tr><th>Metric</th><th>Value</th></tr></thead>
<tbody>
<tr><td class="muted">Avg persons / camp</td><td id="avgPerCamp"></td></tr>
<tr><td class="muted">Median TAT (days)</td><td id="medianTAT"></td></tr>
<tr><td class="muted">P95 TAT (days)</td><td id="p95TAT"></td></tr>
</tbody>
</table>
</div>

</div>
</div>

<div class="camp-panel" id="campPanel">
<div class="drag-header" id="dragHeader">
<span>Camp Data</span>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;">
<div>
<h4 id="cpName">Camp Name</h4>
<div class="small muted" id="cpHub">Hub • Date</div>
</div>
<div><button class="btn" id="closePanel">Close</button></div>
</div>

<div style="margin-top:10px;display:flex;gap:8px;flex-wrap:wrap;">
<div class="pill"><strong id="cpVisits">0</strong> visits</div>
<div class="pill"><strong id="cpSamples">0</strong> samples</div>
<div class="pill"><strong id="cpPos">0</strong> positives</div>
<div class="pill"><strong id="cpTAT"></strong> median TAT d</div>
</div>

<div style="margin-top:10px;">
<div style="font-size:14px; font-weight:700; color:var(--accent); margin-bottom:8px;">Disease breakdown</div>
<div id="cpBreakdown" style="margin-top:6px;"></div>

<div style="margin-top:10px;">
<div style="font-size:14px; font-weight:700; color:var(--accent); margin-bottom:8px;">Camp funnel</div>
<div style="margin-top:6px; font-size:13px; line-height:1.8;">
<div>Registered: <strong id="cpReg"></strong> <span class="small muted" id="cpRegBreakdown"></span></div>
<div>Screened: <strong id="cpScr"></strong></div>
<div>Samples: <strong id="cpSm"></strong></div>
<div>Reported: <strong id="cpRp"></strong></div>
</div>
</div>

<div class="caveat" style="margin-top:10px;">
Note: This panel shows camp-level aggregates only. No person-level roster is displayed in this prototype.
</div>
</div>

</div>

</div>

<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

<script>
// --------------------------
// Mock camp-level aggregated data
// Replace with real aggregated data in integration
const mockCamps = [
{ camp_id: 'C001', name:'Camp - Poovanchal', hub:'Manathavady', lsg:'Panchayat A', date_start:'2025-10-05', date_end:'2025-10-05', lat:11.8, lon:76.0,
visits: 142, samples: 120, positives: 9, median_tat: 7, p95_tat: 18, advised: 240, advised_converted_within_90: 102, referral_lags: [5,7,3,10,14,40,2,3,60,20]
},
{ camp_id: 'C002', name:'Camp - Kunnu', hub:'Sultan Bathery', lsg:'Ward 3', date_start:'2025-10-12', date_end:'2025-10-12', lat:11.7, lon:76.3,
visits: 86, samples: 70, positives: 6, median_tat: 6, p95_tat: 14, advised: 90, advised_converted_within_90: 40, referral_lags: [2,5,6,15,8,25,7,1]
},
{ camp_id: 'C003', name:'Camp - Thiruvampady', hub:'Kalpetta', lsg:'Ward 7', date_start:'2025-09-28', date_end:'2025-09-28', lat:11.6, lon:76.2,
visits: 210, samples: 200, positives: 18, median_tat: 5, p95_tat: 12, advised: 320, advised_converted_within_90: 160, referral_lags: [1,2,3,4,6,7,8,30,40,60,90]
},
{ camp_id: 'C004', name:'Camp - Vythiri', hub:'Manathavady', lsg:'Colony X', date_start:'2025-11-01', date_end:'2025-11-01', lat:11.9, lon:76.05,
visits: 38, samples: 30, positives: 2, median_tat: 9, p95_tat: 22, advised: 50, advised_converted_within_90: 15, referral_lags: [7,15,20,45]
},
{ camp_id: 'C005', name:'Camp - Ambalavayal', hub:'Kalpetta', lsg:'Ward 2', date_start:'2025-10-20', date_end:'2025-10-20', lat:11.58, lon:76.18,
visits: 164, samples: 150, positives: 13, median_tat: 6, p95_tat: 16, advised: 200, advised_converted_within_90: 88, referral_lags: [2,3,4,10,20,55,3,2,6,6,7]
}
];

// Utilities
function sum(arr, key){ return arr.reduce((s,i)=>s+(i[key]||0),0); }
function median(arr){
const a = [...arr].sort((x,y)=>x-y);
if(a.length===0) return null;
const mid = Math.floor(a.length/2);
return a.length%2 ? a[mid] : (a[mid-1]+a[mid])/2;
}
function percentile(a, p){
if(!a.length) return null;
const s=[...a].sort((x,y)=>x-y);
const idx = Math.min(s.length-1, Math.max(0, Math.round((p/100)*(s.length-1))));
return s[idx];
}

// Map init
const map = L.map('map', { zoomControl:true }).setView([11.75, 76.18], 11);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);

// markers
const markers = {};
function renderMarkers(){
mockCamps.forEach(c=>{
const color = c.positives / Math.max(1,c.visits) > 0.06 ? 'red' : (c.positives>0 ? 'orange' : 'green');
const size = Math.min(50, 12 + Math.sqrt(c.visits));
const html = `<div style="background:${color}; width:${size}px; height:${size}px; border-radius:50%; display:flex;align-items:center;justify-content:center; color:white; font-weight:700; box-shadow:0 6px 12px rgba(0,0,0,0.5)">${c.visits}</div>`;
const icon = L.divIcon({ html, className:'camp-pin', iconSize:[size,size], popupAnchor:[0,-size/2] });
const m = L.marker([c.lat,c.lon], { icon }).addTo(map);
m.on('click', ()=>openCampPanel(c.camp_id));
m.bindPopup(`<strong>${c.name}</strong><br>${c.hub}<br>Visits: ${c.visits}<br>Positives: ${c.positives}<br>Median TAT: ${c.median_tat} d`);
markers[c.camp_id] = m;
});
}

// Chart building
let barChart=null, funnelChart=null, lagChart=null;

function computeKPIs(windowDays){
const totalCamps = mockCamps.length;
const totalVisits = sum(mockCamps, 'visits');
const totalSamples = sum(mockCamps, 'samples');
const totalPos = sum(mockCamps, 'positives');
// conversion: sum advised and attending within window
const totalAdvised = sum(mockCamps, 'advised');
const totalConverted = sum(mockCamps, 'advised_converted_within_90'); // mock field uses 90d; in real data compute per window
// compute referral lag median
const allLags = mockCamps.flatMap(c => c.referral_lags || []);
const medLag = median(allLags);
// median/p95 TAT across samples
const allTATs = mockCamps.flatMap(c => Array(Math.max(1, c.samples)).fill(c.median_tat)); // mock: use median per camp
const p50TAT = median(allTATs);
const p95TAT = percentile(allTATs,95);

return {
totalCamps, totalVisits, totalSamples, totalPos,
conversionPct: totalAdvised? Math.round((totalConverted/totalAdvised)*10000)/100 : 0,
medLag: medLag||0, p50TAT: p50TAT||0, p95TAT:p95TAT||0,
totalAdvised, totalConverted
};
}

function renderKPIs(){
const windowDays = Number(document.getElementById('windowSelector').value);
// NOTE: mock uses 90d field; in a real backend recompute per window. Here we adapt roughly:
const data = computeKPIs(windowDays);
document.getElementById('kpiCamps').innerText = data.totalCamps;
document.getElementById('kpiVisits').innerText = data.totalVisits;
document.getElementById('kpiSamples').innerText = data.totalSamples;
document.getElementById('kpiPositives').innerText = data.totalPos;
document.getElementById('kpiConversion').innerText = (data.conversionPct || 0) + '%';
document.getElementById('kpiLag').innerText = data.medLag + ' d';
document.getElementById('medianTAT').innerText = (data.p50TAT || '');
document.getElementById('p95TAT').innerText = (data.p95TAT || '');
document.getElementById('avgPerCamp').innerText = (data.totalVisits / Math.max(1,data.totalCamps)).toFixed(1);
document.getElementById('kpiSamples').innerText = data.totalSamples;
}

function renderLeaderboard(){
const lb = document.getElementById('leaderboard');
lb.innerHTML='';
// sort by visits desc
const sorted = [...mockCamps].sort((a,b)=>b.visits-a.visits);
const table = document.createElement('table');
table.innerHTML = `<thead><tr><th>Camp</th><th class="muted">Visits</th><th class="muted">Positives</th></tr></thead>`;
const body = document.createElement('tbody');
sorted.forEach(c=>{
const tr = document.createElement('tr');
tr.innerHTML = `<td style="cursor:pointer;color:var(--accent);">${c.name}</td><td>${c.visits}</td><td>${c.positives}</td>`;
tr.querySelector('td').addEventListener('click', ()=>{ openCampPanel(c.camp_id); map.setView([c.lat,c.lon],13); });
body.appendChild(tr);
});
table.appendChild(body);
lb.appendChild(table);
}

function renderBarChart(){
const ctx = document.getElementById('barChart').getContext('2d');
const labels = mockCamps.map(c=>c.name);
const data = mockCamps.map(c=>c.visits);
if(barChart) barChart.destroy();
barChart = new Chart(ctx, {
type:'bar',
data:{ labels, datasets:[{ label:'Visits', data, backgroundColor: 'rgba(96,165,250,0.85)', barThickness: 40 }]},
options:{
responsive:true,
maintainAspectRatio:false,
onClick: (evt)=>{
const points = barChart.getElementsAtEventForMode(evt,'nearest',{intersect:true},false);
if(points.length) openCampPanel(mockCamps[points[0].index].camp_id);
},
scales:{
y:{
beginAtZero:true,
max: Math.max(...data) * 1.2,
ticks:{ color:'#94a3b8', stepSize: 50 },
grid:{ color:'rgba(255,255,255,0.05)' }
},
x:{ ticks:{ color:'#94a3b8', maxRotation: 45, minRotation: 0 }, grid:{ display:false } }
},
plugins:{ legend:{ labels:{ color:'#94a3b8' } } }
}
});
}

function renderFunnel(){
// horizontal bar chart with stacked "Attended" bar
const ctx = document.getElementById('funnelChart').getContext('2d');
// compute totals from mock
const totalAdvised = sum(mockCamps,'advised');
const totalConverted = sum(mockCamps,'advised_converted_within_90');
const totalScreened = sum(mockCamps,'visits');
const totalDiagnosed = sum(mockCamps,'positives');
// Split Attended into Survey-Referred and Walk-In (mock: 70% referred, 30% walk-in)
const attendedReferred = Math.round(totalConverted * 0.7);
const attendedWalkIn = totalConverted - attendedReferred;
const maxVal = Math.max(totalAdvised,totalConverted,totalScreened,totalDiagnosed);
if(funnelChart) funnelChart.destroy();
funnelChart = new Chart(ctx, {
type:'bar',
data:{
labels:['Advised','Attended (window)','Screened','Diagnosed'],
datasets:[
{
label: 'Survey-Referred',
data:[totalAdvised, attendedReferred, totalScreened, totalDiagnosed],
backgroundColor:'#60a5fa',
barThickness: 35
},
{
label: 'Walk-In',
data:[0, attendedWalkIn, 0, 0],
backgroundColor:'#10b981',
barThickness: 35
}
]
},
options:{
indexAxis:'y',
responsive:true,
maintainAspectRatio:false,
plugins:{
legend:{
display:true,
position:'bottom',
labels:{
color:'#94a3b8',
usePointStyle: true,
pointStyle: 'rect',
padding: 15,
filter: function(item) {
return item.text === 'Survey-Referred' || item.text === 'Walk-In';
}
}
},
tooltip: {
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.x !== null) {
label += context.parsed.x;
}
return label;
}
}
}
},
scales:{
x:{
stacked: true,
beginAtZero:true,
max: maxVal * 1.15,
ticks:{ color:'#94a3b8', stepSize: 100 },
grid:{ color:'rgba(255,255,255,0.05)' }
},
y:{
stacked: true,
ticks:{ color:'#94a3b8' },
grid:{ display:false }
}
}
}
});
}

function renderLagChart(){
const ctx = document.getElementById('lagChart').getContext('2d');
const allLags = mockCamps.flatMap(c=>c.referral_lags||[]);
// histogram bins
const max = Math.max(...allLags,10);
const bins=8; const step = Math.ceil(max/bins);
const counts = Array(bins).fill(0);
allLags.forEach(v=>{ const idx=Math.min(bins-1, Math.floor(v/step)); counts[idx]++; });
const labels = counts.map((_,i)=>`${i*step}-${(i+1)*step}d`);
const maxCount = Math.max(...counts, 1);
if(lagChart) lagChart.destroy();
lagChart = new Chart(ctx, {
type:'bar',
data:{
labels,
datasets:[{
label:'Count',
data:counts,
backgroundColor:'rgba(6,182,212,0.85)',
barThickness: 30
}]
},
options:{
responsive:true,
maintainAspectRatio:false,
plugins:{legend:{display:false}},
scales:{
y:{
beginAtZero:true,
max: maxCount * 1.2,
ticks:{ color:'#94a3b8', stepSize: Math.max(1, Math.ceil(maxCount/5)) },
grid:{ color:'rgba(255,255,255,0.05)' }
},
x:{ ticks:{ color:'#94a3b8', maxRotation: 45, minRotation: 0 }, grid:{ display:false } }
}
}
});
}

function renderHubBars(){
// aggregated conversion by hub (mock: compute sums)
const hubMap = {};
mockCamps.forEach(c=>{
if(!hubMap[c.hub]) hubMap[c.hub]={advised:0,converted:0};
hubMap[c.hub].advised += c.advised||0;
hubMap[c.hub].converted += c.advised_converted_within_90||0;
});
const container = document.getElementById('hubBars');
container.innerHTML='';
Object.keys(hubMap).forEach(h=>{
const d = hubMap[h];
const pct = d.advised? Math.round(d.converted*10000/d.advised)/100 : 0;
const row = document.createElement('div');
row.style.display='flex'; row.style.justifyContent='space-between'; row.style.alignItems='center'; row.style.marginBottom='8px';
row.innerHTML = `<div><div style="font-weight:700">${h}</div><div class="small muted">${d.advised} advised</div></div><div style="text-align:right"><div style="font-weight:800">${pct}%</div><div class="small muted">converted</div></div>`;
container.appendChild(row);
const bar = document.createElement('div'); bar.style.height='8px'; bar.style.width='100%'; bar.style.background='rgba(255,255,255,0.03)'; bar.style.borderRadius='6px'; bar.style.marginTop='6px';
const inner = document.createElement('div'); inner.style.height='8px'; inner.style.width=(pct)+'%'; inner.style.background='linear-gradient(90deg,#06b6d4,#60a5fa)'; inner.style.borderRadius='6px';
bar.appendChild(inner); container.appendChild(bar);
});
}

// Camp panel
function openCampPanel(campId){
const c = mockCamps.find(x=>x.camp_id===campId);
if(!c) return;
document.getElementById('cpName').innerText = c.name;
document.getElementById('cpHub').innerText = `${c.hub}${c.date_start}`;
document.getElementById('cpVisits').innerText = c.visits;
document.getElementById('cpSamples').innerText = c.samples;
document.getElementById('cpPos').innerText = c.positives;
document.getElementById('cpTAT').innerText = c.median_tat;
// breakdown (mock)
document.getElementById('cpBreakdown').innerHTML = `<div class="small muted">Cervical: ${Math.round(c.visits*0.4)} • Oral: ${Math.round(c.visits*0.35)} • Breast: ${Math.round(c.visits*0.25)}</div>`;
// Camp funnel - only Registered has breakdown (mock data - replace with real data)
const regTotal = Math.round(c.visits*1.05);
const regReferred = Math.round(c.advised_converted_within_90 || c.visits*0.7);
const regNew = regTotal - regReferred;
document.getElementById('cpReg').innerText = regTotal;
document.getElementById('cpRegBreakdown').innerText = `(Survey-Referred: ${regReferred}; New: ${regNew})`;
document.getElementById('cpScr').innerText = c.visits;
document.getElementById('cpSm').innerText = c.samples;
document.getElementById('cpRp').innerText = Math.round(c.samples*0.85);
document.getElementById('campPanel').style.display='block';
}
document.getElementById('closePanel').addEventListener('click', ()=>document.getElementById('campPanel').style.display='none');

// Make panel draggable
let isDragging = false;
let currentX, currentY, initialX, initialY;
const panel = document.getElementById('campPanel');
const dragHeader = document.getElementById('dragHeader');

dragHeader.addEventListener('mousedown', (e) => {
isDragging = true;
initialX = e.clientX - (parseInt(panel.style.left) || 0);
initialY = e.clientY - (parseInt(panel.style.top) || 0);
});

document.addEventListener('mousemove', (e) => {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
panel.style.left = currentX + 'px';
panel.style.top = currentY + 'px';
panel.style.right = 'auto';
}
});

document.addEventListener('mouseup', () => {
isDragging = false;
});

// initial render
renderMarkers();
renderKPIs();
renderLeaderboard();
renderBarChart();
renderFunnel();
renderLagChart();
renderHubBars();

document.getElementById('refreshBtn').addEventListener('click', ()=>{
renderKPIs();
renderBarChart();
renderFunnel();
renderLagChart();
renderHubBars();
});

// Apply filters handler
document.getElementById('applyFilters').addEventListener('click', ()=>{
const district = document.getElementById('districtFilter').value;
const hub = document.getElementById('hubFilter').value;
const dateFrom = document.getElementById('dateFrom').value;
const dateTo = document.getElementById('dateTo').value;
console.log('Filters applied:', { district, hub, dateFrom, dateTo });
// TODO: Filter mockCamps data based on selected filters
// For now, just refresh the visualizations
renderKPIs();
renderBarChart();
renderFunnel();
renderLagChart();
renderHubBars();
renderMarkers();
renderLeaderboard();
});

// small responsive
window.addEventListener('resize', ()=>{ if(barChart) barChart.resize(); if(funnelChart) funnelChart.resize(); if(lagChart) lagChart.resize(); });

</script>
</body>
</html>
    (1-1/1)