Files
livedash-node/components/DonutChart.tsx
Kaj Kowalski 5bfd762e55 fix: comprehensive TypeScript/build fixes and modernization
- Update tsconfig to ES2024 target and bundler moduleResolution
- Add dynamic imports for chart.js and recharts (bundle optimization)
- Consolidate 17 useState into useReducer in sessions page
- Fix 18 .js extension imports across lib files
- Add type declarations for @rapideditor/country-coder
- Fix platform user types (PlatformUserRole enum)
- Fix Calendar component prop types
- Centralize next-auth type augmentation
- Add force-dynamic to all API routes (prevent build-time prerender)
- Fix Prisma JSON null handling with Prisma.DbNull
- Fix various type mismatches (SessionMessage, ImportRecord, etc.)
- Export ButtonProps from button component
- Update next-themes import path
- Replace JSX.Element with React.ReactElement
- Remove obsolete debug scripts and pnpm lockfile
- Downgrade eslint to v8 for next compatibility
2026-01-20 07:28:10 +01:00

196 lines
5.6 KiB
TypeScript

"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 (
<div className="flex items-center justify-center h-[300px]">
<div className="animate-pulse flex items-center gap-8">
<div className="w-40 h-40 rounded-full bg-gray-200 dark:bg-gray-700" />
<div className="space-y-2">
{[...Array(4)].map((_, i) => (
<div key={i} className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-gray-200 dark:bg-gray-700" />
<div className="w-20 h-3 rounded bg-gray-200 dark:bg-gray-700" />
</div>
))}
</div>
</div>
</div>
);
}
function DonutChartInner({ data, centerText }: DonutChartProps) {
const ref = useRef<HTMLCanvasElement | null>(null);
const chartRef = useRef<ChartType<"doughnut"> | 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 <canvas ref={ref} height={300} />;
}
const DonutChart = dynamic(() => Promise.resolve(DonutChartInner), {
ssr: false,
loading: () => <DonutChartSkeleton />,
});
export default DonutChart;