working on detail page for sari, lil, amd lbm
This commit is contained in:
275
dashboard/public/js/overview.js
Normal file
275
dashboard/public/js/overview.js
Normal file
@@ -0,0 +1,275 @@
|
||||
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
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
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]) => {
|
||||
|
||||
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 => d.site_province_name === province);
|
||||
|
||||
const offsets = {
|
||||
1: -0.15,
|
||||
2: 0,
|
||||
3: 0.15
|
||||
};
|
||||
|
||||
rows.forEach(row => {
|
||||
|
||||
const lat = center.lat;
|
||||
const lng = center.lng + offsets[row.surveillance_id];
|
||||
|
||||
const programName =
|
||||
row.surveillance_id === 1 ? 'SARI' :
|
||||
row.surveillance_id === 2 ? 'ILI' : 'LBM';
|
||||
|
||||
const colors = {
|
||||
1: '#2563eb',
|
||||
2: '#10b981',
|
||||
3: '#9333ea'
|
||||
};
|
||||
|
||||
L.circleMarker([lat, lng], {
|
||||
|
||||
radius: 9,
|
||||
fillColor: colors[row.surveillance_id],
|
||||
color: '#fff',
|
||||
weight: 1,
|
||||
fillOpacity: 0.9
|
||||
|
||||
})
|
||||
.bindTooltip(`
|
||||
<strong>${province}</strong><br>
|
||||
${programName}<br>
|
||||
Total: ${row.total}
|
||||
`)
|
||||
.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);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user