This commit is contained in:
2026-07-01 14:48:45 +07:00
parent d201b53442
commit 5d2c055912
12 changed files with 412 additions and 497 deletions

View File

@@ -1,7 +1,5 @@
import { COLORS, SUBTYPE_COLORS, SURVEILLANCE_COLORS } from "./globals.js";
import * as Charts from "./dashboard/globals.js";
let trendChart;
let influenzaSubtypeChart;
let covidDistributedByAgeChart;
let covidLineageFrequencyChart;
let influenzaSubtypeFrequencyChart;
@@ -31,38 +29,46 @@ function loadSummary() {
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="col-md-2 mb-3">
<div class="card shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between">
<div class="d-flex justify-content-between">
<div>
<h6 class="fw-bold">${item.code} Cases</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>
<h6 class="fw-bold">
${item.code === 'H5N1'
? 'A/H5N1 Cases'
: `${item.code} Cases`}
</h6>
<h3 class="mb-1">${item.current_total}</h3>
<small class="text-muted">Last 7 days</small>
</div>
<small class="text-muted">
${item.previous_total ?? 0} last week
</small>
<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>
</div>
`;
`;
window._summaryData = data;
updateAlerts();
@@ -89,58 +95,15 @@ function loadTrend(periodType, startYear, startWeek, endYear, endWeek) {
.then(data => {
if (trendChart) trendChart.destroy();
const totalYears = endYear - startYear;
const useYearlyView = totalYears >= 5;
// 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;
// });
let labels = [];
if (useYearlyView) {
labels = [...new Set(
Object.values(data)
.flat()
.map(row => row.year)
)].sort((a, b) => a - b);
} else {
const labelsSet = new Set();
Object.values(data).forEach(program => {
program.forEach(row => {
labelsSet.add(`${row.year}-${row.period}`);
});
});
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 {
yearlyView: useYearlyView,
labels
} = Charts.buildPeriodLabels(
Object.values(data),
startYear,
endYear
);
const datasets = [];
@@ -151,17 +114,14 @@ function loadTrend(periodType, startYear, startWeek, endYear, endWeek) {
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;
// });
const values = labels.map(label => {
if (useYearlyView) {
return data[code]
.filter(row => row.year == label)
.reduce((sum, row) => sum + row.total, 0);
.filter(row => String(row.year) === String(label))
.reduce((sum, row) => sum + Number(row.total || 0), 0);
}
@@ -176,8 +136,8 @@ function loadTrend(periodType, startYear, startWeek, endYear, endWeek) {
datasets.push({
label: code,
data: values,
borderColor: SURVEILLANCE_COLORS[code],
backgroundColor: SURVEILLANCE_COLORS[code],
borderColor: Charts.SURVEILLANCE_COLORS[code],
backgroundColor: Charts.SURVEILLANCE_COLORS[code],
borderWidth: 3,
pointRadius: 4,
maxBarThickness: 50,
@@ -201,6 +161,7 @@ function loadTrend(periodType, startYear, startWeek, endYear, endWeek) {
return `${year}-W${String(week).padStart(2, '0')}`;
});
trendChart = new Chart(document.getElementById('trendChart'), {
type: 'line',
@@ -244,7 +205,7 @@ function loadTrend(periodType, startYear, startWeek, endYear, endWeek) {
}
}
});
charts['trendChart'] = trendChart;
Charts.charts['trendChart'] = trendChart;
});
}
@@ -254,88 +215,19 @@ function loadInfluenzaSubtypeDistribution(periodType, startYear, startWeek, endY
.then(res => res.json())
.then(data => {
let displayLabels = data.map(item => item.subtype);
let dataset = data.map(item => item.total);
const displayData = data.map(item => ({
...item,
subtype: item.subtype === "B/VIC" ? "B/Vic" : item.subtype
}));
// const colors = displayLabels.map(
// label => SUBTYPE_COLORS[label] || '#9ca3af'
// );
if (influenzaSubtypeChart) influenzaSubtypeChart.destroy();
influenzaSubtypeChart = new Chart(document.getElementById('influenzaSubtypeDistribution'), {
type: 'bar',
data: {
labels: displayLabels,
datasets: [{
data: dataset,
backgroundColor: displayLabels.map(
label => SUBTYPE_COLORS[label] || '#9ca3af'
),
}]
},
options: {
layout: {
padding: {
top: 20,
right: 30,
bottom: 20
}
},
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
position: 'right'
},
datalabels: {
color: '#000000',
borderRadius: 6,
z: 1000,
padding: {
top: 6,
bottom: 6,
},
font: {
weight: 'bold',
size: 12
},
formatter: (value) => value,
anchor: 'end',
align: 'end',
offset: 4,
clamp: true,
clip: false
}
},
scales: {
x: {
stacked: true,
beginAtZero: true,
title: {
display: true,
text: 'Number of Positive Influenza Subtypes'
}
},
y: {
stacked: true,
beginAtZero: true,
title: {
display: true,
text: 'Influenza Subtypes'
},
grid: {
display: false
}
}
}
},
plugins: [ChartDataLabels]
});
charts['influenzaSubtypeDistribution'] = influenzaSubtypeChart;
Charts.buildDistributionChart(
'influenzaSubtypeDistribution',
'bar',
displayData,
'subtype',
'total',
label => Charts.SUBTYPE_COLORS[label] || '#9ca3af'
);
});
}
@@ -344,83 +236,7 @@ function loadCovidDistributedByAgeGroup(periodType, startYear, startWeek, endYea
fetch(`/api/dashboard/covid-distributed-by-age-group?period_type=${periodType}&start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`)
.then(res => res.json())
.then(data => {
let displayLabels = data.map(item => item.age_group);
let dataset = data.map(item => item.total);
if (covidDistributedByAgeChart) covidDistributedByAgeChart.destroy();
covidDistributedByAgeChart = new Chart(document.getElementById('covidDistributedByAgeGroup'), {
type: 'bar',
data: {
labels: displayLabels,
datasets: [{
label: 'Total Covid-19 Detected',
data: dataset,
backgroundColor: COLORS,
maxBarThickness: 50
}]
},
options: {
layout: {
padding: {
top: 50,
bottom: 10,
}
},
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
position: 'bottom'
},
datalabels: {
color: '#000',
borderRadius: 6,
z: 1000,
padding: {
top: 6,
bottom: 6,
left: 10,
right: 10
},
font: {
weight: 'bold',
size: 12
},
formatter: (value) => value,
anchor: 'end',
align: 'end',
offset: 4,
clamp: true,
clip: false
}
},
scales: {
x: {
stacked: true,
beginAtZero: true,
title: {
display: true,
text: 'Patient Age Group'
},
grid: {
display: false
}
},
y: {
stacked: true,
beginAtZero: true,
title: {
display: true,
text: 'Number of Positive SARS-CoV-2'
}
}
}
}
});
charts['covidDistributedByAgeGroup'] = covidDistributedByAgeChart;
Charts.buildDistributionChart('covidDistributedByAgeGroup', 'bar', data, 'age_group');
});
}
@@ -430,36 +246,23 @@ function loadCovidLineageFrequency(periodType, startYear, startWeek, endYear, en
.then(res => res.json())
.then(data => {
// const weeks = [...new Set(data.map(item => item.week))].sort();
const totalYears = endYear - startYear;
const useYearlyView = totalYears >= 5;
const {
yearlyView: useYearlyView,
periods
} = Charts.buildPeriods(
data,
'week',
startYear,
endYear
);
let periods;
if (useYearlyView) {
periods = [...new Set(
data.map(item => item.week.split('-')[0])
)].sort((a, b) => a - b);
} else {
periods = [...new Set(
data.map(item => item.week)
)].sort();
}
const lineages = [...new Set(data.map(item => item.lineage))];
const datasets = lineages.map((lineage, index) => {
// const lineageData = weeks.map(week => {
// const found = data.find(
// item => item.week === week && item.lineage === lineage
// );
// return found ? found.total : 0;
// });
const lineageData = periods.map(period => {
@@ -492,7 +295,7 @@ function loadCovidLineageFrequency(periodType, startYear, startWeek, endYear, en
// borderColor: 'transparent',
borderWidth: 0,
pointRadius: 0,
backgroundColor: hexToRGBA(colors[index % colors.length], 0.3),
backgroundColor: hexToRGBA(Charts.COLORS[index % Charts.COLORS.length], 0.3),
stack: 'total'
};
});
@@ -545,11 +348,8 @@ function loadCovidLineageFrequency(periodType, startYear, startWeek, endYear, en
}
}
});
charts['covidLineageFrequency'] = covidLineageFrequencyChart;
Charts.charts['covidLineageFrequency'] = covidLineageFrequencyChart;
// -------------------------
// Custom right-side scrollable legend
// -------------------------
const legendContainer = document.getElementById('legendContainer');
legendContainer.innerHTML = '';
datasets.forEach((dataset, index) => {
@@ -602,31 +402,22 @@ function loadInfluenzaSubtypeFrequency(periodType, startYear, startWeek, endYear
.then(res => res.json())
.then(data => {
const totalYears = endYear - startYear;
const useYearlyView = totalYears >= 5;
let periods;
if (useYearlyView) {
periods = [...new Set(
data.map(item => item.week.split('-')[0])
)].sort((a, b) => a - b);
} else {
periods = [...new Set(
data.map(item => item.week)
)].sort();
}
const {
yearlyView: useYearlyView,
periods
} = Charts.buildPeriods(
data,
'week',
startYear,
endYear
);
const lineages = [...new Set(
data.map(item => item.lineage)
data.map(item => item.lineage === 'B/VIC' ? 'B/Vic' : item.lineage)
)];
const lineageColors = lineages.map(
label => SUBTYPE_COLORS[label] || '#9ca3af'
label => Charts.SUBTYPE_COLORS[label] || '#9ca3af'
);
const datasets = lineages.map((lineage, index) => {
@@ -747,7 +538,7 @@ function loadInfluenzaSubtypeFrequency(periodType, startYear, startWeek, endYear
});
charts['influenzaSubtypeFrequency'] =
Charts.charts['influenzaSubtypeFrequency'] =
influenzaSubtypeFrequencyChart;
/*
@@ -857,9 +648,9 @@ function hexToRGBA(hex, alpha) {
}
function updateAlerts() {
if (!window._summaryData || !window._provinceData) return;
if (!window._summaryData || !window.latestProvinceData) return;
const raw = buildAlerts(window._summaryData, window._provinceData);
const raw = buildAlerts(window._summaryData, window.latestProvinceData);
const finalAlerts = processAlerts(raw);
renderAlerts(finalAlerts);
@@ -1088,6 +879,7 @@ function normalizeProvince(name, validSet) {
return match || null;
}
window.normalizeProvince = normalizeProvince;
function getRadius(total) {
if (!total) return 0;
const r = Math.sqrt(total);
@@ -1095,10 +887,10 @@ function getRadius(total) {
}
function getPositivityColor(subtype) {
return SUBTYPE_COLORS[subtype] || "#9ca3af";
return Charts.SUBTYPE_COLORS[subtype] || "#9ca3af";
}
const getColorByPathogen = name =>
SUBTYPE_COLORS[name] || '#9ca3af';
Charts.SUBTYPE_COLORS[name] || '#9ca3af';
function addPositivityLegend() {
@@ -1157,7 +949,7 @@ function addPositivityLegend() {
>
<span style="
background:${SUBTYPE_COLORS[item]};
background:${Charts.SUBTYPE_COLORS[item]};
width:10px;
height:10px;
display:inline-block;
@@ -1237,7 +1029,7 @@ function loadProvinceMap(
map = L.map('provinceMap')
.setView([12.7, 104.9], 7);
window.map = map;
L.tileLayer(
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
@@ -1258,7 +1050,7 @@ function loadProvinceMap(
])
.then(([geojson, data]) => {
window._provinceData = data;
window.latestProvinceData = data;
updateAlerts();
@@ -1268,7 +1060,7 @@ function loadProvinceMap(
)
);
Object.keys(SUBTYPE_COLORS)
Object.keys(Charts.SUBTYPE_COLORS)
.forEach(subtype => {
subtypeLayers[subtype] =
@@ -1331,7 +1123,7 @@ function loadProvinceMap(
">
<span>
${r.pathogen_name}
${r.pathogen_name === 'B/VIC' ? 'B/Vic' : r.pathogen_name}
</span>
<span style="font-weight:600">
@@ -1411,17 +1203,17 @@ function loadProvinceMap(
{
radius: getRadius(row.total),
fillColor: getColorByPathogen(
row.pathogen_name
row.pathogen_name === 'B/VIC' ? 'B/Vic' : row.pathogen_name
),
fillOpacity: 0.85,
stroke: false
}
);
if (subtypeLayers[row.pathogen_name]) {
if (subtypeLayers[row.pathogen_name === 'B/VIC' ? 'B/Vic' : row.pathogen_name]) {
marker.addTo(
subtypeLayers[
row.pathogen_name
row.pathogen_name === 'B/VIC' ? 'B/Vic' : row.pathogen_name
]
);