Files
nrml_dashboard/dashboard/public/js/dashboard/charts.js

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' }
}
}
}
});
}