Files
nrml_dashboard/dashboard/public/js/program.js
2026-03-19 09:20:42 +07:00

308 lines
8.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}