333 lines
8.5 KiB
JavaScript
333 lines
8.5 KiB
JavaScript
Chart.register({
|
|
id: 'noDataText',
|
|
afterDraw(chart) {
|
|
|
|
const datasets = chart.data.datasets || [];
|
|
|
|
const hasData = datasets.some(ds =>
|
|
(ds.data || []).some(v => Number(v) > 0)
|
|
);
|
|
|
|
chart.$noData = !hasData;
|
|
|
|
if (hasData) return;
|
|
|
|
const { ctx, width, height } = chart;
|
|
|
|
ctx.save();
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.font = '14px sans-serif';
|
|
ctx.fillStyle = '#9ca3af';
|
|
|
|
ctx.fillText('No data available', width / 2, height / 2);
|
|
|
|
ctx.restore();
|
|
}
|
|
});
|
|
Chart.register({
|
|
id: 'centerText',
|
|
afterDraw(chart) {
|
|
|
|
if (chart.config.type !== 'doughnut') return;
|
|
if (chart.$noData) return;
|
|
|
|
const { ctx, chartArea } = chart;
|
|
|
|
const data = chart.data.datasets[0].data;
|
|
const total = data.reduce((a, b) => a + b, 0);
|
|
|
|
if (!chartArea) return;
|
|
|
|
const centerX = (chartArea.left + chartArea.right) / 2;
|
|
const centerY = (chartArea.top + chartArea.bottom) / 2;
|
|
|
|
ctx.save();
|
|
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
|
|
ctx.font = 'bold 18px sans-serif';
|
|
ctx.fillStyle = '#111827';
|
|
ctx.fillText(total, centerX, centerY - 8);
|
|
|
|
ctx.font = '12px sans-serif';
|
|
ctx.fillStyle = '#6b7280';
|
|
ctx.fillText('Total cases', centerX, centerY + 12);
|
|
|
|
ctx.restore();
|
|
}
|
|
});
|
|
Chart.register(ChartDataLabels);
|
|
const charts = {};
|
|
|
|
|
|
function buildStackedChart(canvasId, labels, datasets) {
|
|
const ctx = document.getElementById(canvasId);
|
|
|
|
if (!ctx) return;
|
|
|
|
if (charts[canvasId]) {
|
|
charts[canvasId].destroy();
|
|
}
|
|
|
|
charts[canvasId] = new Chart(ctx, {
|
|
|
|
type: "bar",
|
|
|
|
data: {
|
|
labels: labels,
|
|
datasets: datasets,
|
|
|
|
},
|
|
|
|
plugins: [ChartDataLabels],
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
|
|
layout: {
|
|
padding: {
|
|
top: 20,
|
|
bottom: 30
|
|
}
|
|
},
|
|
|
|
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom',
|
|
align: 'center',
|
|
|
|
labels: {
|
|
padding: 20,
|
|
boxWidth: 10,
|
|
boxHeight: 10,
|
|
usePointStyle: true,
|
|
pointStyle: 'circle'
|
|
}
|
|
},
|
|
// datalabels: {
|
|
// color: "#000",
|
|
// anchor: "end",
|
|
// align: "top",
|
|
// clamp: true,
|
|
// clip: false,
|
|
// font: {
|
|
// weight: "bold",
|
|
// size: 10
|
|
// },
|
|
// formatter: function (value) {
|
|
// return value > 0 ? value : null;
|
|
// }
|
|
// }
|
|
datalabels: {
|
|
display: false
|
|
}
|
|
},
|
|
|
|
scales: {
|
|
x: {
|
|
stacked: true
|
|
},
|
|
y: {
|
|
stacked: true,
|
|
beginAtZero: true
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
function buildChart(id, type, labels, data) {
|
|
|
|
const ctx = document.getElementById(id);
|
|
if (!ctx) return;
|
|
|
|
Chart.getChart(id)?.destroy();
|
|
|
|
const hasData = data && data.some(v => Number(v) > 0);
|
|
|
|
if (!hasData) {
|
|
labels = [];
|
|
data = [];
|
|
}
|
|
const isHorizontal = id === 'sexChart';
|
|
const isAgeChart = id === 'ageChart';
|
|
const isSentinelChart = id === 'sentinelChart';
|
|
const options = {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
|
|
layout: {
|
|
padding: 30
|
|
},
|
|
|
|
|
|
indexAxis: isHorizontal ? 'y' : 'x',
|
|
plugins: {
|
|
|
|
legend: {
|
|
position: isAgeChart || isSentinelChart ? 'left' : 'bottom',
|
|
align: 'center',
|
|
display: (ctx) => {
|
|
const chart = ctx.chart;
|
|
|
|
if (!(chart.config.type === 'pie' || chart.config.type === 'doughnut')) {
|
|
return false;
|
|
}
|
|
|
|
return !chart.$noData;
|
|
},
|
|
labels: {
|
|
padding: 14,
|
|
boxWidth: 10,
|
|
boxHeight: 10,
|
|
usePointStyle: true,
|
|
pointStyle: 'circle',
|
|
font: {
|
|
size: 11
|
|
}
|
|
}
|
|
},
|
|
datalabels: {
|
|
color: "#282626",
|
|
offset: 6,
|
|
clip: false,
|
|
display: (ctx) => {
|
|
const chart = ctx.chart;
|
|
if (chart.$noData) return false;
|
|
if (chart.config.type === 'bar') return true;
|
|
return !chart.$noData;
|
|
},
|
|
anchor: (ctx) => {
|
|
const type = ctx.chart.config.type;
|
|
|
|
if (type === 'doughnut' || type === 'pie') {
|
|
return 'center';
|
|
}
|
|
|
|
return 'end';
|
|
},
|
|
|
|
align: (ctx) => {
|
|
const type = ctx.chart.config.type;
|
|
if (type === 'doughnut' || type === 'pie') {
|
|
return 'center';
|
|
}
|
|
if (type === 'bar') {
|
|
return ctx.chart.options.indexAxis === 'y' ? 'right' : 'end';
|
|
}
|
|
return 'center';
|
|
},
|
|
font: {
|
|
size: 10,
|
|
weight: '600'
|
|
},
|
|
|
|
formatter: (value, ctx) => {
|
|
if (ctx.chart.$noData) return '';
|
|
|
|
const data = ctx.chart.data.datasets[0].data;
|
|
const total = data.reduce((a, b) => a + b, 0);
|
|
|
|
if (!total) return '';
|
|
|
|
return ((value / total) * 100).toFixed(1) + '%';
|
|
}
|
|
}
|
|
}
|
|
};
|
|
if (type === 'bar') {
|
|
options.scales = {
|
|
x: {
|
|
beginAtZero: true,
|
|
grid: {
|
|
display: false
|
|
}
|
|
},
|
|
y: {
|
|
grid: {
|
|
color: '#f3f4f6'
|
|
}
|
|
}
|
|
};
|
|
}
|
|
if (type === 'doughnut') {
|
|
options.cutout = '70%';
|
|
options.maintainAspectRatio = false;
|
|
options.elements = {
|
|
arc: {
|
|
borderWidth: (ctx) => ctx.chart.$noData ? 0 : 1
|
|
}
|
|
|
|
};
|
|
}
|
|
|
|
charts[id] = new Chart(ctx, {
|
|
type: type,
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
data: data,
|
|
borderWidth: 2,
|
|
tension: 0.3,
|
|
barPercentage: 0.8,
|
|
categoryPercentage: 0.6,
|
|
maxBarThickness: 50
|
|
}]
|
|
},
|
|
options: options
|
|
});
|
|
}
|
|
function buildMixedTrendChart(canvasId, labels, samples, lines) {
|
|
|
|
const ctx = document.getElementById(canvasId);
|
|
if (!ctx) return;
|
|
|
|
if (charts[canvasId]) charts[canvasId].destroy();
|
|
|
|
const datasets = [];
|
|
|
|
lines.forEach(line => {
|
|
datasets.push({
|
|
type: 'line',
|
|
label: line.label,
|
|
data: line.data,
|
|
borderColor: line.color,
|
|
backgroundColor: line.color,
|
|
tension: 0.4,
|
|
yAxisID: 'y1',
|
|
pointStyle: 'line'
|
|
});
|
|
});
|
|
|
|
datasets.push({
|
|
type: 'bar',
|
|
label: 'Total Cases',
|
|
data: samples,
|
|
backgroundColor: '#0B8F3C',
|
|
yAxisID: 'y'
|
|
});
|
|
|
|
charts[canvasId] = new Chart(ctx, {
|
|
data: { labels, datasets },
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { position: 'bottom' },
|
|
datalabels: {
|
|
display: false
|
|
}
|
|
},
|
|
scales: {
|
|
y: { title: { display: true, text: 'Cases' } },
|
|
y1: {
|
|
position: 'right',
|
|
grid: { drawOnChartArea: false },
|
|
title: { display: true, text: '% Positivity' }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
} |