add sequencing tab
This commit is contained in:
238
dashboard/public/js/sequencing.js
Normal file
238
dashboard/public/js/sequencing.js
Normal file
@@ -0,0 +1,238 @@
|
||||
let sequencingChart;
|
||||
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 || []);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
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: 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'
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
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 }
|
||||
},
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
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 + '%'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user