Files
nrml_dashboard/dashboard/public/js/overview.js

317 lines
9.3 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.
let trendChart;
let map;
/*
|--------------------------------------------------------------------------
| Load Summary Cards
|--------------------------------------------------------------------------
*/
function loadSummary() {
fetch('/api/dashboard/summary')
.then(res => res.json())
.then(data => {
let html = '';
data.forEach(item => {
let trendColor = 'text-secondary';
if (item.percent_change > 0) trendColor = 'text-danger';
if (item.percent_change < 0) trendColor = 'text-success';
html += `
<div class="col-md-2 mb-3">
<div class="card shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h6 class="fw-bold">${item.code}</h6>
<h3 class="mb-1">${item.current_total}</h3>
<small class="text-muted">Last 7 days</small>
</div>
<div class="text-end">
<div class="${trendColor} fw-bold">
${item.percent_change > 0 ? '▲' : item.percent_change < 0 ? '▼' : ''}
${Math.abs(item.percent_change)}%
</div>
<small class="text-muted">
${item.previous_total ?? 0} last week
</small>
</div>
</div>
</div>
</div>
</div>
`;
});
document.getElementById('summary_cards').innerHTML = html;
});
}
/*
|--------------------------------------------------------------------------
| Load Trend Chart
|--------------------------------------------------------------------------
*/
function loadTrend(periodType, startYear, startWeek, endYear, endWeek) {
fetch(`/api/dashboard/trend?period_type=${periodType}&start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`)
.then(res => res.json())
.then(data => {
if (trendChart) trendChart.destroy();
const labelsSet = new Set();
Object.values(data).forEach(program => {
program.forEach(row => {
labelsSet.add(`${row.year}-${row.period}`);
});
});
const labels = Array.from(labelsSet).sort((a, b) => {
const [yearA, weekA] = a.split('-').map(Number);
const [yearB, weekB] = b.split('-').map(Number);
if (yearA !== yearB) return yearA - yearB;
return weekA - weekB;
});
const colors = {
SARI: '#2563eb',
ILI: '#10b981',
LBM: '#9333ea'
};
const datasets = [];
const allowedPrograms = ['SARI', 'ILI', 'LBM'];
Object.keys(data).forEach(code => {
if (!allowedPrograms.includes(code)) return;
const values = labels.map(label => {
const found = data[code].find(row => `${row.year}-${row.period}` === label);
return found ? found.total : 0;
});
datasets.push({
label: code,
data: values,
borderColor: colors[code],
backgroundColor: colors[code],
borderWidth: 3,
pointRadius: 4,
fill: false,
tension: 0.3
});
});
const displayLabels = labels.map(l => {
const [year, week] = l.split('-');
return `W${String(week).padStart(2, '0')}`;
});
trendChart = new Chart(document.getElementById('trendChart'), {
type: 'line',
data: {
labels: displayLabels,
datasets: datasets
},
options: {
responsive: true,
plugins: {
legend: { position: 'bottom' }
},
interaction: {
mode: 'index',
intersect: false
},
scales: {
y: {
beginAtZero: true,
ticks: { stepSize: 1 }
},
x: { grid: { display: false } }
}
}
});
});
}
/*
|--------------------------------------------------------------------------
| Province Map Helpers
|--------------------------------------------------------------------------
*/
function normalizeProvince(name, validSet) {
if (!name || !validSet) return null;
const clean = str =>
str.toLowerCase().replace(/\s+/g, '');
const raw = name.trim();
const map = {
"kepville": "Kep",
"sihanoukville": "Preah Sihanouk",
"sihanoukvillecity": "Preah Sihanouk",
"krongpailin": "Pailin",
"mondulkiri": "Mondulkiri",
"odormeanchey": "Oddar Meanchey",
"tbongkhmom": "Tboung Khmum",
"tboungkhmum": "Tboung Khmum",
"rattanakiri": "Ratanak Kiri"
};
const key = clean(raw);
if (map[key] && validSet.has(map[key])) {
return map[key];
}
const match = [...validSet].find(p => clean(p) === key);
return match || null;
}
function getRadius(total) {
if (!total) return 0;
const r = Math.sqrt(total);
return Math.max(4, Math.min(r * 2, 22));
}
/*
|--------------------------------------------------------------------------
| Province Map
|--------------------------------------------------------------------------
*/
function loadProvinceMap(startYear, startWeek, endYear, endWeek) {
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'
}).addTo(map);
Promise.all([
fetch('/geo/cambodia_provinces.geojson').then(r => r.json()),
fetch(`/api/dashboard/province-circles?start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`).then(r => r.json())
])
.then(([geojson, data]) => {
const validProvinces = new Set(
geojson.features.map(f => f.properties.ADM1_EN)
);
L.geoJSON(geojson, {
style: {
fillOpacity: 0,
color: '#ccc',
weight: 1,
interactive: false
},
onEachFeature: function (feature, layer) {
const province = feature.properties.ADM1_EN;
const center = layer.getBounds().getCenter();
const rows = data.filter(d => {
if (![1, 2, 3].includes(d.surveillance_id)) return false;
const name = normalizeProvince(d.patient_province, validProvinces);
return name === province;
});
const offsets = { 1: -0.15, 2: 0, 3: 0.15 };
const colors = {
1: '#2563eb',
2: '#10b981',
3: '#9333ea'
};
rows.forEach(row => {
const percent = row.total
? ((row.positive / row.total) * 100).toFixed(1)
: 0;
const offset = offsets[row.surveillance_id] ?? 0;
const lat = center.lat;
const lng = center.lng + offset;
const programName =
row.surveillance_id === 1 ? 'SARI' :
row.surveillance_id === 2 ? 'ILI' : 'LBM';
L.circleMarker([lat, lng], {
radius: getRadius(row.total),
fillColor: colors[row.surveillance_id],
color: '#fff',
weight: 1,
fillOpacity: 0.9
})
.bindTooltip(`
<strong>${province}</strong><br>
${programName}<br>
Cases: ${row.total}<br>
Positivity: ${percent}%
`)
.addTo(map);
});
}
}).addTo(map);
});
}
/*
|--------------------------------------------------------------------------
| Initialize Dashboard
|--------------------------------------------------------------------------
*/
document.addEventListener("DOMContentLoaded", () => {
loadSummary();
new DashboardFilter((startYear, startWeek, endYear, endWeek) => {
loadTrend('week', startYear, startWeek, endYear, endWeek);
loadProvinceMap(startYear, startWeek, endYear, endWeek);
});
});