Merge branch 'master' of github.com:khantey1998/nrml-dashboard

This commit is contained in:
2026-04-29 09:11:39 +07:00
12 changed files with 540 additions and 60 deletions

View File

@@ -8,7 +8,7 @@ Chart.register({
(ds.data || []).some(v => Number(v) > 0)
);
chart.$noData = !hasData;
chart.$noData = !hasData;
if (hasData) return;
@@ -94,7 +94,7 @@ function buildStackedChart(canvasId, labels, datasets) {
bottom: 30
}
},
plugins: {
legend: {
@@ -159,9 +159,9 @@ function buildChart(id, type, labels, data) {
layout: {
padding: 30
},
indexAxis: isHorizontal ? 'y' : 'x',
indexAxis: isHorizontal ? 'y' : 'x',
plugins: {
legend: {
@@ -189,7 +189,7 @@ function buildChart(id, type, labels, data) {
},
datalabels: {
color: "#282626",
offset: 6,
offset: 6,
clip: false,
display: (ctx) => {
const chart = ctx.chart;
@@ -257,7 +257,7 @@ function buildChart(id, type, labels, data) {
arc: {
borderWidth: (ctx) => ctx.chart.$noData ? 0 : 1
}
};
}
@@ -306,7 +306,7 @@ function buildMixedTrendChart(canvasId, labels, samples, fluRate, covidRate) {
},
{
type: 'line',
label: 'COVID-19 %',
@@ -318,7 +318,7 @@ function buildMixedTrendChart(canvasId, labels, samples, fluRate, covidRate) {
pointStyle: 'line',
},
{
type: 'bar',
label: 'Total Cases',
@@ -327,7 +327,7 @@ function buildMixedTrendChart(canvasId, labels, samples, fluRate, covidRate) {
borderRadius: 2,
barPercentage: 0.8,
categoryPercentage: 0.7,
yAxisID: 'y',
yAxisID: 'y',
}
]
@@ -338,7 +338,6 @@ function buildMixedTrendChart(canvasId, labels, samples, fluRate, covidRate) {
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
@@ -351,6 +350,7 @@ function buildMixedTrendChart(canvasId, labels, samples, fluRate, covidRate) {
align: "top",
anchor: "end",
color: "#555",
display: false,
font: {
size: 10
},
@@ -387,7 +387,7 @@ function buildMixedTrendChart(canvasId, labels, samples, fluRate, covidRate) {
display: true,
text: '% Positivity'
},
}
}
}

View File

@@ -75,8 +75,8 @@ class DashboardFilter {
const year = new Date().getFullYear();
for (let y = year-20; y <= year; y++) {
for (let y = year; y >= year-20; y--) {
this.startYear.innerHTML += `<option value="${y}">${y}</option>`;
this.endYear.innerHTML += `<option value="${y}">${y}</option>`;
@@ -128,4 +128,4 @@ class DashboardFilter {
}
}
}

View File

@@ -931,13 +931,13 @@ function addPositivityLegend() {
<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: #2563eb; 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: #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: #1021b9; 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: #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>
`;
@@ -1036,10 +1036,10 @@ function loadProvinceMap(startYear, startWeek, endYear, endWeek) {
function getColorByPathogen(name) {
const colors = {
"A/H1N1pdm": "#2563eb",
"A/H1N1pdm": "#ff0200",
"A/H3N2": "#11de9d",
"B/Yam": "#9333ea",
"B/Vic" : "#1021b9",
"B/Vic" : "#086037",
"A/H9N2": "#b91081",
"A/H5N1": "#ad850d",
"A/Unsubtypable": "#047393",

View File

@@ -1,20 +1,33 @@
let sequencingChart;
let covidLineageFrequencyChart;
let influenzaSubtypeFrequencyChart;
document.addEventListener("DOMContentLoaded", () => {
const canvas = document.getElementById('sequencingChart');
if (!canvas) return;
//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 || []);
//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) {
@@ -39,7 +52,7 @@ function renderSequencingCountChart(rows) {
const found = rows.find(r => r.period === w && r.subtype === sub);
return found ? found.total : 0;
}),
backgroundColor: colors[i % colors.length]
backgroundColor: hexToRGBA(colors[i % colors.length], 0.3)// colors[i % colors.length]
}));
new Chart(ctx, {
@@ -59,6 +72,9 @@ function renderSequencingCountChart(rows) {
display: true,
position: 'bottom',
align: 'center'
},
datalabels: {
display: true
}
}
@@ -123,9 +139,13 @@ function renderSequencingTotalChart(rows) {
options: {
maintainAspectRatio: false,
plugins: {
legend: { display: false }
legend: { display: false },
datalabels: {
display: false
}
},
}
});
}
@@ -176,6 +196,8 @@ function renderSequencingChart(rows) {
}
});
console.log('aggregated', aggregated)
const cleanRows = Object.values(aggregated);
const weeks = [...new Set(cleanRows.map(r => r.period))];
@@ -235,4 +257,310 @@ function renderSequencingChart(rows) {
}
}
});
}
}
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';
});
}