Files
nrml_dashboard/dashboard/public/js/overview.js
2026-04-29 09:53:56 +07:00

1126 lines
39 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 influenzaSubtypeChart;
let covidDistributedByAgeChart;
let covidLineageFrequencyChart;
let influenzaSubtypeFrequencyChart;
let map;
/*
|--------------------------------------------------------------------------
| Load Summary Cards
|--------------------------------------------------------------------------
*/
function loadSummary() {
fetch('/api/dashboard/summary')
.then(res => res.json())
.then(data => {
let html = '';
const alerts = [];
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} 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>
<small class="text-muted">
${item.previous_total ?? 0} last week
</small>
</div>
</div>
</div>
</div>
</div>
`;
window._summaryData = data;
updateAlerts();
});
document.getElementById('summary_cards').innerHTML = html;
renderAlerts(alerts);
});
}
/*
|--------------------------------------------------------------------------
| 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',
AFI: '#fc5741',
NDS: '#d59d01',
SEQ: '#9ca3af'
};
const datasets = [];
const allowedPrograms = ['SARI', 'ILI', 'LBM', 'AFI', 'NDS'];
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' },
datalabels: {
display: false
}
},
interaction: {
mode: 'index',
intersect: false
},
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
},
title: {
display: true,
text: 'Number of Cases'
},
},
x: {
grid: {
display: false
},
title: {
display: true,
text: 'Surveillance'
},
}
}
}
});
charts['trendChart'] = trendChart;
});
}
function loadInfluenzaSubtypeDistribution(periodType, startYear, startWeek, endYear, endWeek) {
fetch(`/api/dashboard/influenza-subtype-distribution?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.subtype);
let dataset = data.map(item => item.total);
const colors = [
'rgba(177,111,243,0.94)'
];
if (influenzaSubtypeChart) influenzaSubtypeChart.destroy();
influenzaSubtypeChart = new Chart(document.getElementById('influenzaSubtypeDistribution'), {
type: 'bar', // you can change to 'pie', 'doughnut', etc.
data: {
labels: displayLabels,
datasets: [{
data: dataset,
backgroundColor: colors,
}]
},
options: {
indexAxis: 'y',
responsive: true,
plugins: {
legend: {
display: false,
position:'right'
},
datalabels: {
color: '#ffffff',
backgroundColor: 'rgba(68,76,68,0.31)',
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: 10,
clamp: 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;
});
}
function loadCovidDistributedByAgeGroup(periodType, startYear, startWeek, endYear, endWeek) {
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);
const colors = [
'#84cc16'
];
if (covidDistributedByAgeChart) covidDistributedByAgeChart.destroy();
covidDistributedByAgeChart = new Chart(document.getElementById('covidDistributedByAgeGroup'), {
type: 'bar', // you can change to 'pie', 'doughnut', etc.
data: {
labels: displayLabels,
datasets: [{
label: 'Total Covid-19 Detected',
data: dataset,
backgroundColor: colors,
}]
},
options: {
responsive: true,
plugins: {
legend: {
display: false,
position:'bottom'
},
datalabels: {
color: '#fff',
backgroundColor: 'rgba(68,76,68,0.7)',
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: 10,
clamp: 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;
});
}
function loadCovidLineageFrequency(periodType, startYear, startWeek, endYear, endWeek) {
fetch(`/api/dashboard/covid-lineage-frequency?period_type=${periodType}&start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`)
.then(res => res.json())
.then(data => {
// Extract unique weeks (X-axis)
const weeks = [...new Set(data.map(item => item.week))].sort();
// Extract unique lineages
const lineages = [...new Set(data.map(item => item.lineage))];
// Color palette
const colors = [
'#84cc16','#22c55e','#06b6d4','#3b82f6',
'#6366f1','#a855f7','#ec4899','#ef4444',
'#f97316','#eab308'
];
// Build datasets
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;
});
return {
label: lineage,
data: lineageData,
fill: true, // area fill
tension: 0.4, // smooth curve
borderColor: 'transparent', // hide the line
borderWidth: 0,
pointRadius: 0, // hide points
backgroundColor: hexToRGBA(colors[index % colors.length], 0.3),
stack: 'total'
};
});
// Destroy previous chart if exists
if (covidLineageFrequencyChart) covidLineageFrequencyChart.destroy();
const ctx = document.getElementById('covidLineageFrequency').getContext('2d');
covidLineageFrequencyChart = new Chart(ctx, {
type: 'line',
data: {
labels: weeks,
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false
},
plugins: {
legend: {
display: false // hide default legend
},
tooltip: {
mode: 'index',
intersect: false
},
datalabels: {
display: false // hide labels
}
},
scales: {
x: {
stacked: true,
title: {
display: true,
text: 'Week'
},
grid:{
display: false
}
},
y: {
stacked: true,
beginAtZero: true,
title: {
display: true,
text: 'Relative Frequency'
},
}
}
}
});
charts['covidLineageFrequency'] = covidLineageFrequencyChart;
// -------------------------
// Custom right-side scrollable legend
// -------------------------
const legendContainer = document.getElementById('legendContainer');
legendContainer.innerHTML = ''; // clear old legend
datasets.forEach((dataset, index) => {
const item = document.createElement('div');
item.style.display = 'flex';
item.style.alignItems = 'center';
item.style.marginBottom = '4px';
item.style.fontSize = '11px';
item.style.cursor = 'pointer';
item.innerHTML = `
<span style="width:15px;height:15px;background:${dataset.backgroundColor};display:inline-block;margin-right:8px;"></span>
${dataset.label}
`;
item.addEventListener('click', () => {
const meta = covidLineageFrequencyChart.getDatasetMeta(index);
// If the clicked dataset is already the only visible one, show all
const allHidden = datasets.every((d, i) => covidLineageFrequencyChart.getDatasetMeta(i).hidden || i === index);
if (!allHidden) {
// Hide all datasets
datasets.forEach((d, i) => {
covidLineageFrequencyChart.getDatasetMeta(i).hidden = true;
});
// Show only clicked
meta.hidden = false;
} else {
// Show all datasets
datasets.forEach((d, i) => {
covidLineageFrequencyChart.getDatasetMeta(i).hidden = false;
});
}
covidLineageFrequencyChart.update();
// Update legend opacity
Array.from(legendContainer.children).forEach((child, i) => {
const metaItem = covidLineageFrequencyChart.getDatasetMeta(i);
child.style.opacity = metaItem.hidden ? 0.5 : 1;
});
});
legendContainer.appendChild(item);
});
// Scrollable CSS (in case legend is long)
legendContainer.style.maxHeight = '375px';
legendContainer.style.overflowY = 'auto';
legendContainer.style.padding = '8px';
legendContainer.style.borderRadius = '0px';
});
}
function loadInfluenzaSubtypeFrequency(periodType, startYear, startWeek, endYear, endWeek) {
fetch(`/api/dashboard/influenza-relative-frequency?period_type=${periodType}&start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`)
.then(res => res.json())
.then(data => {
// Extract unique weeks (X-axis)
const weeks = [...new Set(data.map(item => item.week))].sort();
const colors = [
'#84cc16','#22c55e','#06b6d4','#3b82f6',
'#6366f1','#a855f7','#ec4899','#ef4444',
'#f97316','#eab308'
];
// Extract unique lineages
const lineages = [...new Set(data.map(item => item.lineage))];
// Build datasets
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;
});
return {
label: lineage,
data: lineageData,
fill: true, // area fill
tension: 0.4, // smooth curve
borderColor: 'transparent', // hide the line
borderWidth: 0,
pointRadius: 0, // hide points
backgroundColor: hexToRGBA(colors[index % colors.length], 0.3),
stack: 'total'
};
});
// Destroy previous chart if exists
if (influenzaSubtypeFrequencyChart) influenzaSubtypeFrequencyChart.destroy();
const ctx = document.getElementById('influenzaSubtypeFrequency').getContext('2d');
influenzaSubtypeFrequencyChart = new Chart(ctx, {
type: 'line',
data: {
labels: weeks,
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false
},
plugins: {
legend: {
display: false // hide default legend
},
tooltip: {
mode: 'index',
intersect: false
},
datalabels: {
display: false // hide labels
}
},
scales: {
x: {
stacked: true,
title: {
display: true,
text: 'Week'
},
grid:{
display: false
}
},
y: {
stacked: true,
beginAtZero: true,
title: {
display: true,
text: 'Relative Frequency'
},
}
}
}
});
const legendContainer = document.getElementById('legendContainerInfluenzaSubtypeFrequency');
legendContainer.innerHTML = ''; // clear old legend
datasets.forEach((dataset, index) => {
const item = document.createElement('div');
item.style.display = 'flex';
item.style.alignItems = 'center';
item.style.marginBottom = '4px';
item.style.fontSize = '11px';
item.style.cursor = 'pointer';
item.innerHTML = `
<span style="width:15px;height:15px;background:${dataset.backgroundColor};display:inline-block;margin-right:8px;"></span>
${dataset.label}
`;
item.addEventListener('click', () => {
const meta = influenzaSubtypeFrequencyChart.getDatasetMeta(index);
// If the clicked dataset is already the only visible one, show all
const allHidden = datasets.every((d, i) => influenzaSubtypeFrequencyChart.getDatasetMeta(i).hidden || i === index);
if (!allHidden) {
// Hide all datasets
datasets.forEach((d, i) => {
influenzaSubtypeFrequencyChart.getDatasetMeta(i).hidden = true;
});
// Show only clicked
meta.hidden = false;
} else {
// Show all datasets
datasets.forEach((d, i) => {
influenzaSubtypeFrequencyChart.getDatasetMeta(i).hidden = false;
});
}
influenzaSubtypeFrequencyChart.update();
// Update legend opacity
Array.from(legendContainer.children).forEach((child, i) => {
const metaItem = influenzaSubtypeFrequencyChart.getDatasetMeta(i);
child.style.opacity = metaItem.hidden ? 0.5 : 1;
});
});
legendContainer.appendChild(item);
});
// Scrollable CSS (in case legend is long)
legendContainer.style.maxHeight = '375px';
legendContainer.style.overflowY = 'auto';
legendContainer.style.padding = '8px';
legendContainer.style.borderRadius = '0px';
});
}
// -------------------------
// Helper to convert hex to rgba
// -------------------------
function hexToRGBA(hex, alpha) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
function updateAlerts() {
if (!window._summaryData || !window._provinceData) return;
const raw = buildAlerts(window._summaryData, window._provinceData);
const finalAlerts = processAlerts(raw);
renderAlerts(finalAlerts);
}
function generateAlerts(data) {
const alerts = [];
// -------------------------
// 1. Program-level alerts
// -------------------------
const summary = data.summary || {};
const programs = [
{ key: 'influenza_rate', label: 'Influenza' },
{ key: 'covid_rate', label: 'COVID-19' },
{ key: 'positivity_rate', label: 'Overall positivity' }
];
programs.forEach(p => {
const current = summary[p.key]?.current || 0;
const previous = summary[p.key]?.previous || 0;
const diff = previous ? ((current - previous) / previous) * 100 : 0;
if (current >= 15) {
alerts.push(`🔴 High ${p.label} (${current}%)`);
} else if (current >= 10) {
alerts.push(`🟠 Moderate ${p.label} (${current}%)`);
}
if (diff >= 10) {
alerts.push(`🟡 Increasing ${p.label} (+${diff.toFixed(1)}%)`);
}
});
// -------------------------
// 2. Province-level alerts
// -------------------------
const provinces = data.province_distribution || [];
const top = [...provinces]
.sort((a, b) => b.total - a.total)
.slice(0, 3);
top.forEach(p => {
const percent = p.total
? ((p.positive / p.total) * 100)
: 0;
if (percent >= 15) {
alerts.push(`🔴 High positivity in ${p.patient_province} (${percent.toFixed(1)}%)`);
} else if (percent >= 10) {
alerts.push(`🟠 Moderate positivity in ${p.patient_province}`);
}
if (p.total >= 50) {
alerts.push(`🟡 High case volume in ${p.patient_province} (${p.total})`);
}
});
// -------------------------
// fallback
// -------------------------
if (!alerts.length) {
alerts.push("🟢 No unusual activity detected");
}
return alerts;
}
function createAlert(type, message, priority) {
return { type, message, priority };
}
function buildAlerts(summaryData, provinceData) {
const alerts = [];
// -------------------------
// 1. Program alerts
// -------------------------
summaryData.forEach(item => {
// 🔴 High activity
if (item.current_total >= 80) {
alerts.push(createAlert(
'high',
`High ${item.code} activity (${item.current_total} cases)`,
1
));
}
// 🟠 Moderate
else if (item.current_total >= 40) {
alerts.push(createAlert(
'moderate',
`${item.code} activity elevated (${item.current_total})`,
2
));
}
// 🟡 Increasing trend
if (item.percent_change >= 10) {
alerts.push(createAlert(
'trend',
`Increasing ${item.code} (+${item.percent_change}%)`,
3
));
}
});
// -------------------------
// 2. Province alerts
// -------------------------
const top = [...provinceData]
.sort((a, b) => b.total - a.total)
.slice(0, 5);
top.forEach(p => {
const percent = p.total
? ((p.positive / p.total) * 100)
: 0;
// 🔴 High positivity
if (percent >= 15) {
alerts.push(createAlert(
'high',
`High positivity in ${p.patient_province} (${percent.toFixed(1)}%)`,
1
));
}
// 🟠 Moderate positivity
else if (percent >= 10) {
alerts.push(createAlert(
'moderate',
`Moderate positivity in ${p.patient_province}`,
2
));
}
// 🟡 High volume
if (p.total >= 50) {
alerts.push(createAlert(
'volume',
`High case volume in ${p.patient_province} (${p.total})`,
3
));
}
});
return alerts;
}
function processAlerts(alerts) {
const seen = new Set();
const unique = alerts.filter(a => {
if (seen.has(a.message)) return false;
seen.add(a.message);
return true;
});
// sort by priority
unique.sort((a, b) => a.priority - b.priority);
// limit to top 5
return unique.slice(0, 5);
}
function renderAlerts(alerts) {
const container = document.getElementById('alertsList');
if (!container) return;
if (!alerts.length) {
container.innerHTML = `
<li class="list-group-item text-success">
🟢 No unusual activity detected
</li>
`;
return;
}
const colorMap = {
high: 'text-danger',
moderate: 'text-warning',
trend: 'text-primary',
volume: 'text-secondary'
};
container.innerHTML = alerts.map(a => `
<li class="list-group-item ${colorMap[a.type] || ''}">
${a.type === 'high' ? '🔴' :
a.type === 'moderate' ? '🟠' :
a.type === 'trend' ? '🟡' :
'🔵'}
${a.message}
</li>
`).join('');
}
/*
|--------------------------------------------------------------------------
| Province Map Helpers
|--------------------------------------------------------------------------
*/
function getPositivityColor(p) {
if (p == 'A/H1N1pdm') return "#2563eb";
if (p == 'A/H3N2') return "#11de9d";
if (p == 'B/Yam') return "#9333ea";
if (p == 'B/Vic') return "#1021b9";
if (p == 'A/H9N2') return "#b91081";
if (p == 'A/H5N1') return "#ad850d";
if (p == 'A/Unsubtypable') return "#047393";
if (p == 'B/Unsubtypable') return "#890512";
return "#9ca3af";
}
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));
}
function addPositivityLegend() {
const legend = L.control({ position: "bottomleft" });
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;">Influenza Subtypes</div>
<div><span style="background: #ff0200; border: 2px solid #aa9d9d; width:12px;height:12px;display:inline-block;margin-right:6px; border-radius: 50%;"></span>A/H1N1pdm</div>
<div><span style="background: #11de9d; border: 2px solid #aa9d9d; width:12px;height:12px;display:inline-block;margin-right:6px; border-radius: 50%;"></span>A/H3N2</div>
<div><span style="background: #b91081; border: 2px solid #aa9d9d; width:12px;height:12px;display:inline-block;margin-right:6px; border-radius: 50%;"></span>A/H9N2</div>
<div><span style="background: #ad850d; border: 2px solid #aa9d9d; width:12px;height:12px;display:inline-block;margin-right:6px; border-radius: 50%;"></span>A/H5N1</div>
<div><span style="background: #047393; border: 2px solid #aa9d9d; width:12px;height:12px;display:inline-block;margin-right:6px; border-radius: 50%;"></span>A/Unsubtypable</div>
<div><span style="background: #9333ea; border: 2px solid #aa9d9d; width:12px;height:12px;display:inline-block;margin-right:6px; border-radius: 50%;"></span>B/Yam</div>
<div><span style="background: #086037; border: 2px solid #aa9d9d; width:12px;height:12px;display:inline-block;margin-right:6px; border-radius: 50%;"></span>B/Vic</div>
<div><span style="background: #890512; border: 2px solid #aa9d9d; width:12px;height:12px;display:inline-block;margin-right:6px; border-radius: 50%;"></span>B/Unsubtypable</div>
</div>
`;
return div;
};
legend.addTo(map);
}
/*
|--------------------------------------------------------------------------
| Province Map
|--------------------------------------------------------------------------
*/
function loadProvinceMap(startYear, startWeek, endYear, endWeek) {
if (map) map.remove();
map = L.map('provinceMap').setView([12.7, 104.9], 7);
addPositivityLegend();
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]) => {
window._provinceData = data;
updateAlerts();
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 => {
const name = normalizeProvince(d.patient_province, validProvinces);
return name === province;
});
if (!rows.length) return;
// 👉 group by pathogen_name
const pathogens = [...new Set(rows.map(r => r.pathogen_name))];
const spacing = 0.15; //0.12; // adjust spacing between circles
rows.forEach(row => {
// 👉 dynamic offset based on pathogen index
const index = pathogens.indexOf(row.pathogen_name);
const offset = (index - (pathogens.length - 1) / 2) * spacing;
const lat = center.lat;
const lng = center.lng + offset;
L.circleMarker([lat, lng], {
radius: getRadius(row.total),
fillColor: getColorByPathogen(row.pathogen_name), // 👈 new function
color: getPositivityColor(row.pathogen_nam),
weight: 2,
fillOpacity: 0.9
})
.bindTooltip(`
<strong>${province}</strong><br>
${row.pathogen_name}<br>
Total Detected: ${row.total}<br>
`)
.addTo(map);
});
}
}).addTo(map);
});
}
function getColorByPathogen(name) {
const colors = {
"A/H1N1pdm": "#ff0200",
"A/H3N2": "#11de9d",
"B/Yam": "#9333ea",
"B/Vic" : "#086037",
"A/H9N2": "#b91081",
"A/H5N1": "#ad850d",
"A/Unsubtypable": "#047393",
"B/Unsubtypable": "#890512"
};
return colors[name] || "#000000"; // fallback color
}
/*
|--------------------------------------------------------------------------
| Initialize Dashboard
|--------------------------------------------------------------------------
*/
document.addEventListener("DOMContentLoaded", () => {
loadSummary();
new DashboardFilter((startYear, startWeek, endYear, endWeek) => {
loadTrend('week', startYear, startWeek, endYear, endWeek);
loadInfluenzaSubtypeDistribution('week', startYear, startWeek, endYear, endWeek);
loadCovidDistributedByAgeGroup('week', startYear, startWeek, endYear, endWeek);
loadInfluenzaSubtypeFrequency('week', startYear, startWeek, endYear, endWeek);
loadCovidLineageFrequency('week', startYear, startWeek, endYear, endWeek);
loadProvinceMap(startYear, startWeek, endYear, endWeek);
const elements = document.querySelectorAll(".report-period");
elements.forEach(el => {
el.textContent = 'Week ' + startWeek + ' of '+startYear+' to ' + 'Week ' + endWeek + ' of ' + endYear
});
});
});
document.addEventListener("DOMContentLoaded", function () {
let currentSlide = 0;
const slides = document.querySelectorAll('.slide');
const nextBtn = document.querySelector('.next-btn');
const prevBtn = document.querySelector('.prev-btn');
function showSlide(index) {
slides.forEach((slide, i) => {
slide.classList.remove('active', 'prev');
if (i === index) {
slide.classList.add('active');
} else if (i === index - 1) {
slide.classList.add('prev');
}
});
}
function nextSlide() {
currentSlide = (currentSlide + 1) % slides.length;
showSlide(currentSlide);
}
function prevSlide() {
currentSlide = (currentSlide - 1 + slides.length) % slides.length;
showSlide(currentSlide);
}
// Button events
nextBtn.addEventListener('click', nextSlide);
prevBtn.addEventListener('click', prevSlide);
// Auto slide every 1 minute
let slideInterval = 2 * (60 * 1000);
setInterval(nextSlide, slideInterval);
// Init
showSlide(currentSlide);
});