finalize detail page except NDS and squencing
This commit is contained in:
@@ -1,36 +1,84 @@
|
||||
const standardPrograms = ['SARI', 'ILI', 'LBM'];
|
||||
const standardPrograms = ['SARI', 'ILI', 'LBM', 'AFI'];
|
||||
const programCode = (window.PROGRAM_CODE || '').trim().toUpperCase();
|
||||
let map;
|
||||
let provinceLayer;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
.then(programCode === 'AFI' ? renderAFIDashboard : renderDashboard)
|
||||
.catch(err => console.error("Dashboard API error:", err));
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function renderAFIDashboard(data) {
|
||||
const pathogenRows = (data.pathogen_distribution || [])
|
||||
.sort((a, b) => b.total - a.total);
|
||||
const colors = [
|
||||
'#2563eb', '#10b981', '#f59e0b', '#ef4444',
|
||||
'#8b5cf6', '#14b8a6', '#f97316', '#84cc16', '#e11dba', '#f6f63b'
|
||||
];
|
||||
|
||||
renderSummary(data.summary);
|
||||
renderAFITrend(data.afi_trend);
|
||||
renderProvinceHeatmap(data.province_distribution);
|
||||
renderPathogenChart(data.pathogen_distribution);
|
||||
renderDemographics(data);
|
||||
renderSentinel(data.sentinel_sites || []);
|
||||
renderSubtypeChart(data.subtype_distribution || []);
|
||||
|
||||
charts['pathogenChart'].data.datasets[0].backgroundColor = colors;
|
||||
charts['pathogenChart'].update();
|
||||
charts['ageChart'].data.datasets[0].backgroundColor = colors;
|
||||
charts['ageChart'].update();
|
||||
charts['sexChart'].data.datasets[0].backgroundColor = colors;
|
||||
charts['sexChart'].update();
|
||||
charts['sentinelChart'].data.datasets[0].backgroundColor = colors;
|
||||
charts['sentinelChart'].update();
|
||||
charts['subtypeChart'].data.datasets[0].backgroundColor = colors;
|
||||
charts['subtypeChart'].update();
|
||||
}
|
||||
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();
|
||||
}
|
||||
if (map) map.remove();
|
||||
|
||||
map = L.map('provinceMap').setView([12.7, 104.9], 7);
|
||||
|
||||
@@ -44,6 +92,28 @@ function renderProvinceHeatmap(rows) {
|
||||
.then(r => r.json())
|
||||
.then(geo => {
|
||||
|
||||
const validProvinces = new Set(
|
||||
geo.features.map(f => f.properties.ADM1_EN)
|
||||
);
|
||||
|
||||
const totals = {};
|
||||
|
||||
rows.forEach(r => {
|
||||
|
||||
// ✅ FIX: use patient_province + validSet
|
||||
const province = normalizeProvince(r.patient_province, validProvinces);
|
||||
if (!province) return;
|
||||
|
||||
// ✅ FIX: accumulate instead of overwrite
|
||||
if (!totals[province]) {
|
||||
totals[province] = { total: 0, positive: 0 };
|
||||
}
|
||||
|
||||
totals[province].total += Number(r.total);
|
||||
totals[province].positive += Number(r.positive);
|
||||
|
||||
});
|
||||
|
||||
function getColor(value) {
|
||||
if (value > 50) return "#b91c1c";
|
||||
if (value >= 10) return "#ef4444";
|
||||
@@ -65,11 +135,15 @@ function renderProvinceHeatmap(rows) {
|
||||
};
|
||||
},
|
||||
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);
|
||||
|
||||
// ✅ positivity kept
|
||||
const percent = total
|
||||
? ((positive / total) * 100).toFixed(1)
|
||||
: 0;
|
||||
|
||||
layer.bindTooltip(`
|
||||
${province}<br>
|
||||
@@ -159,6 +233,7 @@ function renderTrend(valueId, changeId, current, previous, suffix = '') {
|
||||
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);
|
||||
@@ -230,32 +305,22 @@ function renderSummary(summary) {
|
||||
function renderDashboard(data) {
|
||||
|
||||
data = data || {};
|
||||
|
||||
|
||||
let virusRows = data.virus_trend || [];
|
||||
if (!virusRows.length) {
|
||||
virusRows = [
|
||||
{ period: '', influenza: 0, covid: 0, total_samples: 0 }
|
||||
];
|
||||
}
|
||||
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'
|
||||
'#8b5cf6', '#14b8a6', '#f97316', '#84cc16', '#e11dba', '#f6f63b'
|
||||
];
|
||||
|
||||
|
||||
@@ -305,3 +370,114 @@ function renderDashboard(data) {
|
||||
charts['sentinelChart'].update();
|
||||
|
||||
}
|
||||
|
||||
|
||||
//AFI
|
||||
function renderAFITrend(rows) {
|
||||
|
||||
if (!rows.length) {
|
||||
buildStackedChart('trendChart', [], []);
|
||||
return;
|
||||
}
|
||||
|
||||
const { labels, datasets } = transformAFIData(rows);
|
||||
|
||||
buildStackedChart('trendChart', labels, datasets);
|
||||
}
|
||||
function transformAFIData(rows) {
|
||||
|
||||
const grouped = {};
|
||||
const pathogensSet = new Set();
|
||||
|
||||
rows.forEach(r => {
|
||||
|
||||
const key = `${r.year}-W${r.period}`;
|
||||
|
||||
if (!grouped[key]) {
|
||||
grouped[key] = {};
|
||||
}
|
||||
|
||||
grouped[key][r.pathogen] = r.total;
|
||||
pathogensSet.add(r.pathogen);
|
||||
});
|
||||
|
||||
const labels = Object.keys(grouped).sort((a, b) => {
|
||||
const [yA, wA] = a.split('-W').map(Number);
|
||||
const [yB, wB] = b.split('-W').map(Number);
|
||||
return yA === yB ? wA - wB : yA - yB;
|
||||
});
|
||||
|
||||
const pathogenTotals = {};
|
||||
|
||||
rows.forEach(r => {
|
||||
pathogenTotals[r.pathogen] = (pathogenTotals[r.pathogen] || 0) + r.total;
|
||||
});
|
||||
|
||||
const pathogens = Object.keys(pathogenTotals)
|
||||
.sort((a, b) => pathogenTotals[b] - pathogenTotals[a]);
|
||||
const datasets = pathogens.map(p => ({
|
||||
label: p,
|
||||
data: labels.map(l => grouped[l][p] || 0),
|
||||
backgroundColor: getColorForPathogen(p)
|
||||
}));
|
||||
|
||||
return { labels: labels.map(l => l.split('-')[1]), datasets };
|
||||
}
|
||||
function getColorForPathogen(name) {
|
||||
|
||||
const colors = {
|
||||
Dengue: '#2563eb',
|
||||
Chikungunya: '#10b981',
|
||||
Zika: '#f59e0b',
|
||||
Leptospira: '#ef4444',
|
||||
Rickettsia: '#8b5cf6',
|
||||
Salmonella: '#f97316',
|
||||
Plasmodium: '#14b8a6',
|
||||
Influenza: '#84cc16'
|
||||
};
|
||||
|
||||
|
||||
if (colors[name]) return colors[name];
|
||||
|
||||
// fallback random color (for future pathogens)
|
||||
return `hsl(${Math.floor(Math.random() * 360)}, 70%, 60%)`;
|
||||
}
|
||||
function renderPathogenChart(rows) {
|
||||
buildChart(
|
||||
'pathogenChart',
|
||||
'doughnut',
|
||||
rows.map(r => r.pathogen),
|
||||
rows.map(r => r.total)
|
||||
);
|
||||
}
|
||||
function renderSentinel(rows) {
|
||||
buildChart(
|
||||
'sentinelChart',
|
||||
'pie',
|
||||
rows.map(r => r.name),
|
||||
rows.map(r => r.total)
|
||||
);
|
||||
}
|
||||
function renderDemographics(data) {
|
||||
buildChart(
|
||||
'ageChart',
|
||||
'doughnut',
|
||||
(data.age_distribution || []).map(r => r.age_group),
|
||||
(data.age_distribution || []).map(r => r.total)
|
||||
);
|
||||
|
||||
buildChart(
|
||||
'sexChart',
|
||||
'bar',
|
||||
(data.sex_distribution || []).map(r => r.patient_sex),
|
||||
(data.sex_distribution || []).map(r => r.total)
|
||||
);
|
||||
}
|
||||
function renderSubtypeChart(rows) {
|
||||
buildChart(
|
||||
'subtypeChart',
|
||||
'bar',
|
||||
rows.map(r => r.subtype),
|
||||
rows.map(r => r.total)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user