308 lines
8.7 KiB
JavaScript
308 lines
8.7 KiB
JavaScript
const standardPrograms = ['SARI', 'ILI', 'LBM'];
|
||
const programCode = (window.PROGRAM_CODE || '').trim().toUpperCase();
|
||
let map;
|
||
let provinceLayer;
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
|
||
|
||
if (!standardPrograms.includes(programCode)) return;
|
||
new DashboardFilter((startYear, startWeek, endYear, endWeek) => {
|
||
|
||
fetch(`/api/dashboard/program?surveillance_id=${window.SURVEILLANCE_ID}&start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`)
|
||
.then(res => res.json())
|
||
.then(renderDashboard)
|
||
.catch(err => console.error("Dashboard API error:", err));
|
||
|
||
});
|
||
|
||
});
|
||
|
||
|
||
function renderProvinceHeatmap(rows) {
|
||
|
||
const totals = {};
|
||
rows.forEach(r => {
|
||
totals[r.site_province_name] = {
|
||
total: Number(r.total),
|
||
positive: Number(r.positive)
|
||
};
|
||
});
|
||
|
||
if (map) {
|
||
map.remove();
|
||
}
|
||
|
||
map = L.map('provinceMap').setView([12.7, 104.9], 7);
|
||
|
||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||
attribution: '© OpenStreetMap contributors'
|
||
}).addTo(map);
|
||
|
||
addProvinceLegend();
|
||
|
||
fetch('/geo/cambodia_provinces.geojson')
|
||
.then(r => r.json())
|
||
.then(geo => {
|
||
|
||
function getColor(value) {
|
||
if (value > 50) return "#b91c1c";
|
||
if (value >= 10) return "#ef4444";
|
||
if (value > 0) return "#fecaca";
|
||
return "#f3f4f600";
|
||
}
|
||
|
||
provinceLayer = L.geoJSON(geo, {
|
||
style: feature => {
|
||
|
||
const province = feature.properties.ADM1_EN;
|
||
const value = totals[province]?.total || 0;
|
||
|
||
return {
|
||
color: "#444",
|
||
weight: 1,
|
||
fillColor: getColor(value),
|
||
fillOpacity: 0.7
|
||
};
|
||
},
|
||
onEachFeature: (feature, layer) => {
|
||
const province = feature.properties.ADM1_EN;
|
||
const total = totals[province]?.total || 0;
|
||
const positive = totals[province]?.positive || 0;
|
||
const percent = total ? ((positive / total) * 100).toFixed(1) : 0;
|
||
console.log(province, total, positive, percent);
|
||
|
||
layer.bindTooltip(`
|
||
${province}<br>
|
||
Total: ${total}<br>
|
||
Positivity: ${percent}%
|
||
`);
|
||
}
|
||
}).addTo(map);
|
||
|
||
});
|
||
}
|
||
function addProvinceLegend() {
|
||
|
||
const legend = L.control({ position: "bottomright" });
|
||
|
||
legend.onAdd = function () {
|
||
|
||
const div = L.DomUtil.create("div", "map-legend");
|
||
|
||
div.innerHTML = `
|
||
<div style="background:white;padding:10px 12px;border-radius:6px;
|
||
box-shadow:0 2px 6px rgba(0,0,0,0.2);font-size:12px;">
|
||
<div style="font-weight:600;margin-bottom:6px;">Cases</div>
|
||
|
||
<div style="display:flex;align-items:center;margin-bottom:4px;">
|
||
<span style="width:12px;height:12px;background:#b91c1c;
|
||
display:inline-block;margin-right:6px;"></span>
|
||
> 50
|
||
</div>
|
||
|
||
<div style="display:flex;align-items:center;margin-bottom:4px;">
|
||
<span style="width:12px;height:12px;background:#ef4444;
|
||
display:inline-block;margin-right:6px;"></span>
|
||
10 – 50
|
||
</div>
|
||
|
||
<div style="display:flex;align-items:center;margin-bottom:4px;">
|
||
<span style="width:12px;height:12px;background:#fecaca;
|
||
display:inline-block;margin-right:6px;"></span>
|
||
1 – 9
|
||
</div>
|
||
|
||
<div style="display:flex;align-items:center;">
|
||
<span style="width:12px;height:12px;background:#f3f4f6;
|
||
display:inline-block;margin-right:6px;"></span>
|
||
0
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
return div;
|
||
};
|
||
|
||
legend.addTo(map);
|
||
}
|
||
function renderTrend(valueId, changeId, current, previous, suffix = '') {
|
||
|
||
const valueEl = document.getElementById(valueId);
|
||
const changeEl = document.getElementById(changeId);
|
||
|
||
if (!valueEl || !changeEl) return;
|
||
|
||
valueEl.textContent = current + suffix;
|
||
|
||
if (!previous) {
|
||
changeEl.innerHTML = "— No previous data";
|
||
changeEl.className = "text-muted";
|
||
return;
|
||
}
|
||
|
||
const diff = current - previous;
|
||
const percent = ((diff / previous) * 100).toFixed(1);
|
||
|
||
if (diff > 0) {
|
||
changeEl.innerHTML = `↑ +${percent}% from previous week`;
|
||
changeEl.className = "text-success";
|
||
}
|
||
else if (diff < 0) {
|
||
changeEl.innerHTML = `↓ ${percent}% from previous week`;
|
||
changeEl.className = "text-danger";
|
||
}
|
||
else {
|
||
changeEl.innerHTML = "— No significant change";
|
||
changeEl.className = "text-muted";
|
||
}
|
||
}
|
||
function renderProgramTrend(rows) {
|
||
|
||
rows = rows || [];
|
||
const labels = rows.map(r => `W${r.period}`);
|
||
const samples = rows.map(r => r.total_samples || 0);
|
||
const fluRate = rows.map(r => r.influenza_rate || 0);
|
||
const covidRate = rows.map(r => r.covid_rate || 0);
|
||
|
||
buildMixedTrendChart(
|
||
'trendChart',
|
||
labels,
|
||
samples,
|
||
fluRate,
|
||
covidRate
|
||
);
|
||
}
|
||
function renderSummary(summary) {
|
||
|
||
summary = summary || {};
|
||
|
||
const cases = summary.cases || {};
|
||
const hospital = summary.hospital_rate || {};
|
||
const icu = summary.icu_rate || {};
|
||
const positivity = summary.positivity_rate || {};
|
||
|
||
renderTrend(
|
||
"totalCases",
|
||
"casesChange",
|
||
cases.current || 0,
|
||
cases.previous || 0
|
||
);
|
||
renderTrend(
|
||
"influenzaRate",
|
||
"influenzaChange",
|
||
summary.influenza_rate.current,
|
||
summary.influenza_rate.previous,
|
||
"%"
|
||
);
|
||
|
||
renderTrend(
|
||
"covidRate",
|
||
"covidChange",
|
||
summary.covid_rate.current,
|
||
summary.covid_rate.previous,
|
||
"%"
|
||
);
|
||
|
||
renderTrend(
|
||
"hospitalRate",
|
||
"hospitalChange",
|
||
hospital.current || 0,
|
||
hospital.previous || 0,
|
||
"%"
|
||
);
|
||
|
||
renderTrend(
|
||
"icuRate",
|
||
"icuChange",
|
||
icu.current || 0,
|
||
icu.previous || 0,
|
||
"%"
|
||
);
|
||
|
||
renderTrend(
|
||
"positivityRate",
|
||
"positivityChange",
|
||
positivity.current || 0,
|
||
positivity.previous || 0,
|
||
"%"
|
||
);
|
||
}
|
||
function renderDashboard(data) {
|
||
|
||
data = data || {};
|
||
|
||
|
||
renderProgramTrend(data.trend || []);
|
||
renderSummary(data.summary || {});
|
||
renderProvinceHeatmap(data.province_distribution || []);
|
||
// buildStackedChart(
|
||
// "pathogenChart",
|
||
// labels,
|
||
// [
|
||
// {
|
||
// label: "Influenza",
|
||
// data: influenza,
|
||
// backgroundColor: "#2E7D32"
|
||
// },
|
||
// {
|
||
// label: "SARS-CoV-2",
|
||
// data: covid,
|
||
// backgroundColor: "#A5D6A7"
|
||
// }
|
||
// ]
|
||
// );
|
||
const pathogenRows = (data.pathogen_distribution || [])
|
||
.sort((a, b) => b.total - a.total);
|
||
const colors = [
|
||
'#2563eb', '#10b981', '#f59e0b', '#ef4444',
|
||
'#8b5cf6', '#14b8a6', '#f97316', '#84cc16'
|
||
];
|
||
|
||
|
||
|
||
buildChart(
|
||
'pathogenChart',
|
||
'doughnut',
|
||
pathogenRows.map(r => r.pathogen),
|
||
pathogenRows.map(r => r.total)
|
||
);
|
||
charts['pathogenChart'].data.datasets[0].backgroundColor = colors;
|
||
charts['pathogenChart'].update();
|
||
|
||
buildChart(
|
||
'ageChart',
|
||
'doughnut',
|
||
(data.age_distribution || []).map(r => r.age_group),
|
||
(data.age_distribution || []).map(r => r.total)
|
||
);
|
||
charts['ageChart'].data.datasets[0].backgroundColor = colors;
|
||
charts['ageChart'].update();
|
||
|
||
buildChart(
|
||
'sexChart',
|
||
'bar',
|
||
(data.sex_distribution || []).map(r => r.patient_sex),
|
||
(data.sex_distribution || []).map(r => r.total)
|
||
);
|
||
charts['sexChart'].data.datasets[0].backgroundColor = colors;
|
||
charts['sexChart'].update();
|
||
|
||
buildChart(
|
||
'subtypeChart',
|
||
'bar',
|
||
(data.subtype_distribution || []).map(r => r.subtype),
|
||
(data.subtype_distribution || []).map(r => r.total)
|
||
);
|
||
charts['subtypeChart'].data.datasets[0].backgroundColor = colors;
|
||
charts['subtypeChart'].update();
|
||
buildChart(
|
||
'sentinelChart',
|
||
'pie',
|
||
(data.sentinel_sites || []).map(r => r.name),
|
||
(data.sentinel_sites || []).map(r => r.total)
|
||
);
|
||
charts['sentinelChart'].data.datasets[0].backgroundColor = colors;
|
||
charts['sentinelChart'].update();
|
||
|
||
}
|