"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;