"use client";
import type { BubbleDataPoint, Chart as ChartType, Point } from "chart.js";
import dynamic from "next/dynamic";
import { useEffect, useRef } from "react";
interface DonutChartProps {
data: {
labels: string[];
values: number[];
colors?: string[];
};
centerText?: {
title?: string;
value?: string | number;
};
}
function DonutChartSkeleton() {
return (
{[...Array(4)].map((_, i) => (
))}
);
}
function DonutChartInner({ data, centerText }: DonutChartProps) {
const ref = useRef(null);
const chartRef = useRef | null>(null);
useEffect(() => {
let isMounted = true;
async function initChart() {
if (!ref.current || !data.values.length) return;
const ctx = ref.current.getContext("2d");
if (!ctx) return;
const { default: Chart } = await import("chart.js/auto");
if (!isMounted) return;
// Default colors if not provided
const defaultColors: string[] = [
"rgba(59, 130, 246, 0.8)", // blue
"rgba(16, 185, 129, 0.8)", // green
"rgba(249, 115, 22, 0.8)", // orange
"rgba(236, 72, 153, 0.8)", // pink
"rgba(139, 92, 246, 0.8)", // purple
"rgba(107, 114, 128, 0.8)", // gray
];
const colors: string[] = data.colors || defaultColors;
// Helper to create an array of colors based on the data length
const getColors = () => {
const result: string[] = [];
for (let i = 0; i < data.values.length; i++) {
result.push(colors[i % colors.length]);
}
return result;
};
// Destroy existing chart if any
if (chartRef.current) {
chartRef.current.destroy();
}
chartRef.current = new Chart(ctx, {
type: "doughnut",
data: {
labels: data.labels,
datasets: [
{
data: data.values,
backgroundColor: getColors(),
borderWidth: 1,
hoverOffset: 5,
},
],
},
options: {
responsive: true,
maintainAspectRatio: true,
cutout: "70%",
plugins: {
legend: {
position: "right",
labels: {
boxWidth: 12,
padding: 20,
usePointStyle: true,
},
},
tooltip: {
callbacks: {
label: (context) => {
const label = context.label || "";
const value = context.formattedValue;
const total = context.chart.data.datasets[0].data.reduce(
(
a: number,
b:
| number
| Point
| [number, number]
| BubbleDataPoint
| null
) => {
if (typeof b === "number") {
return a + b;
}
return a;
},
0
) as number;
const percentage = Math.round((context.parsed * 100) / total);
return `${label}: ${value} (${percentage}%)`;
},
},
},
},
},
plugins: centerText
? [
{
id: "centerText",
beforeDraw: (chart: ChartType<"doughnut">) => {
const height = chart.height;
const ctx = chart.ctx;
ctx.restore();
const chartArea = chart.chartArea;
const chartWidth = chartArea.right - chartArea.left;
const centerX = chartArea.left + chartWidth / 2;
const centerY = height / 2;
if (centerText.title) {
ctx.font = "1rem sans-serif";
ctx.fillStyle = "#6B7280";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(centerText.title, centerX, centerY - 10);
}
if (centerText.value !== undefined) {
ctx.font = "bold 1.5rem sans-serif";
ctx.fillStyle = "#1F2937";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(
centerText.value.toString(),
centerX,
centerY + 15
);
}
ctx.save();
},
},
]
: [],
});
}
initChart();
return () => {
isMounted = false;
if (chartRef.current) {
chartRef.current.destroy();
chartRef.current = null;
}
};
}, [data, centerText]);
return ;
}
const DonutChart = dynamic(() => Promise.resolve(DonutChartInner), {
ssr: false,
loading: () => ,
});
export default DonutChart;