567 lines
19 KiB
JavaScript
567 lines
19 KiB
JavaScript
let sequencingChart;
|
|
let covidLineageFrequencyChart;
|
|
let influenzaSubtypeFrequencyChart;
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
//const canvas = document.getElementById('sequencingChart');
|
|
//if (!canvas) return;
|
|
new DashboardFilter((startYear, startWeek, endYear, endWeek) => {
|
|
|
|
fetch(`/api/dashboard/sequencing?surveillance_id=${window.SURVEILLANCE_ID}&start_year=${startYear}&start_week=${startWeek}&end_year=${endYear}&end_week=${endWeek}`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
//renderSequencingChart(data.trend || []);
|
|
renderSequencingCountChart(data.trend || []);
|
|
renderSequencingPieChart(data.distribution || []);
|
|
renderSequencingTotalChart(data.trend || []);
|
|
});
|
|
|
|
loadCovidLineageFrequency('week', startYear, startWeek, endYear, endWeek)
|
|
loadInfluenzaSubtypeFrequency('week', startYear, startWeek, endYear, endWeek)
|
|
|
|
const elements = document.querySelectorAll(".report-period");
|
|
elements.forEach(el => {
|
|
el.textContent = 'Week ' + startWeek + ' of '+startYear+' to ' + 'Week ' + endWeek + ' of ' + endYear
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
function renderSequencingCountChart(rows) {
|
|
|
|
const ctx = document.getElementById('sequencingCountChart');
|
|
|
|
Chart.getChart('sequencingCountChart')?.destroy();
|
|
|
|
rows = processTopSubtypes(rows);
|
|
|
|
const weeks = [...new Set(rows.map(r => r.period))];
|
|
const subtypes = [...new Set(rows.map(r => r.subtype))];
|
|
|
|
const colors = [
|
|
'#2563eb', '#10b981', '#f59e0b', '#ef4444',
|
|
'#8b5cf6', '#14b8a6', '#f97316', '#84cc16'
|
|
];
|
|
|
|
const datasets = subtypes.map((sub, i) => ({
|
|
label: sub,
|
|
data: weeks.map(w => {
|
|
const found = rows.find(r => r.period === w && r.subtype === sub);
|
|
return found ? found.total : 0;
|
|
}),
|
|
backgroundColor: hexToRGBA(colors[i % colors.length], 0.3)// colors[i % colors.length]
|
|
}));
|
|
|
|
new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: weeks.map(w => `W${w}`),
|
|
datasets
|
|
},
|
|
options: {
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
x: { stacked: true },
|
|
y: { stacked: true }
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: 'bottom',
|
|
align: 'center'
|
|
},
|
|
datalabels: {
|
|
display: true
|
|
}
|
|
}
|
|
|
|
}
|
|
});
|
|
}
|
|
function renderSequencingPieChart(rows) {
|
|
|
|
const ctx = document.getElementById('sequencingPieChart');
|
|
|
|
Chart.getChart('sequencingPieChart')?.destroy();
|
|
|
|
const top = rows.slice(0, 8);
|
|
|
|
const labels = top.map(r => r.subtype);
|
|
const values = top.map(r => r.total);
|
|
|
|
new Chart(ctx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels,
|
|
datasets: [{
|
|
data: values
|
|
}]
|
|
},
|
|
options: {
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom'
|
|
},
|
|
}
|
|
|
|
}
|
|
});
|
|
}
|
|
function renderSequencingTotalChart(rows) {
|
|
|
|
const ctx = document.getElementById('sequencingTotalChart');
|
|
|
|
Chart.getChart('sequencingTotalChart')?.destroy();
|
|
|
|
const totals = {};
|
|
|
|
rows.forEach(r => {
|
|
totals[r.period] = (totals[r.period] || 0) + Number(r.total);
|
|
});
|
|
|
|
const weeks = Object.keys(totals);
|
|
const values = Object.values(totals);
|
|
|
|
new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: weeks.map(w => `W${w}`),
|
|
datasets: [{
|
|
label: 'Total Samples',
|
|
data: values,
|
|
backgroundColor: '#0B8F3C'
|
|
}]
|
|
},
|
|
options: {
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { display: false },
|
|
datalabels: {
|
|
display: false
|
|
}
|
|
},
|
|
|
|
|
|
}
|
|
});
|
|
}
|
|
function processTopSubtypes(rows) {
|
|
|
|
const totals = {};
|
|
|
|
rows.forEach(r => {
|
|
totals[r.subtype] = (totals[r.subtype] || 0) + Number(r.total);
|
|
});
|
|
|
|
const sorted = Object.entries(totals)
|
|
.sort((a, b) => b[1] - a[1]);
|
|
|
|
const top = sorted.slice(0, 8).map(([k]) => k);
|
|
|
|
return rows.map(r => {
|
|
if (!top.includes(r.subtype)) {
|
|
return { ...r, subtype: 'Others' };
|
|
}
|
|
return r;
|
|
});
|
|
}
|
|
function renderSequencingChart(rows) {
|
|
|
|
rows = processTopSubtypes(rows);
|
|
|
|
const ctx = document.getElementById('sequencingChart');
|
|
|
|
if (sequencingChart) {
|
|
sequencingChart.destroy();
|
|
}
|
|
|
|
const colors = [
|
|
'#2563eb', '#10b981', '#f59e0b', '#ef4444',
|
|
'#8b5cf6', '#14b8a6', '#f97316', '#84cc16',
|
|
'#6b7280'
|
|
];
|
|
|
|
const aggregated = {};
|
|
|
|
rows.forEach(r => {
|
|
const key = `${r.period}_${r.subtype}`;
|
|
if (!aggregated[key]) {
|
|
aggregated[key] = { ...r };
|
|
} else {
|
|
aggregated[key].total += Number(r.total);
|
|
}
|
|
});
|
|
|
|
console.log('aggregated', aggregated)
|
|
|
|
const cleanRows = Object.values(aggregated);
|
|
|
|
const weeks = [...new Set(cleanRows.map(r => r.period))];
|
|
const subtypes = [...new Set(cleanRows.map(r => r.subtype))]
|
|
.sort((a, b) => {
|
|
const sum = s => cleanRows
|
|
.filter(r => r.subtype === s)
|
|
.reduce((t, r) => t + r.total, 0);
|
|
return sum(b) - sum(a);
|
|
});
|
|
|
|
const datasets = subtypes.map((sub, i) => {
|
|
return {
|
|
label: sub,
|
|
data: weeks.map(w => {
|
|
const weekRows = cleanRows.filter(r => r.period === w);
|
|
const total = weekRows.reduce((s, r) => s + Number(r.total), 0);
|
|
|
|
const found = weekRows.find(r => r.subtype === sub);
|
|
return total ? ((found?.total || 0) / total) * 100 : 0;
|
|
}),
|
|
fill: true,
|
|
tension: 0.3,
|
|
backgroundColor: colors[i % colors.length],
|
|
borderColor: colors[i % colors.length]
|
|
};
|
|
});
|
|
|
|
sequencingChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: weeks.map(w => `W${w}`),
|
|
datasets: datasets
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: 'bottom',
|
|
align: 'center'
|
|
},
|
|
datalabels: {
|
|
display: false
|
|
}
|
|
},
|
|
scales: {
|
|
x: { stacked: true },
|
|
y: {
|
|
stacked: true,
|
|
max: 100,
|
|
ticks: {
|
|
callback: v => v + '%'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
|
|
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 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'
|
|
},
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// -------------------------
|
|
// 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-sequencing?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*2) % colors.length], 0.8),
|
|
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';
|
|
});
|
|
}
|