Files
livedash-node/components/DonutChart.tsx
Kaj Kowalski 93fbb44eec feat: comprehensive Biome linting fixes and code quality improvements
Major code quality overhaul addressing 58% of all linting issues:

• Type Safety Improvements:
  - Replace all any types with proper TypeScript interfaces
  - Fix Map component shadowing (renamed to CountryMap)
  - Add comprehensive custom error classes system
  - Enhance API route type safety

• Accessibility Enhancements:
  - Add explicit button types to all interactive elements
  - Implement useId() hooks for form element accessibility
  - Add SVG title attributes for screen readers
  - Fix static element interactions with keyboard handlers

• React Best Practices:
  - Resolve exhaustive dependencies warnings with useCallback
  - Extract nested component definitions to top level
  - Fix array index keys with proper unique identifiers
  - Improve component organization and prop typing

• Code Organization:
  - Automatic import organization and type import optimization
  - Fix unused function parameters and variables
  - Enhanced error handling with structured error responses
  - Improve component reusability and maintainability

Results: 248 → 104 total issues (58% reduction)
- Fixed all critical type safety and security issues
- Enhanced accessibility compliance significantly
- Improved code maintainability and performance
2025-06-29 07:35:45 +02:00

156 lines
4.8 KiB
TypeScript

"use client";
import Chart, { type BubbleDataPoint, type Point } from "chart.js/auto";
import { useEffect, useRef } from "react";
interface DonutChartProps {
data: {
labels: string[];
values: number[];
colors?: string[];
};
centerText?: {
title?: string;
value?: string | number;
};
}
export default function DonutChart({ data, centerText }: DonutChartProps) {
const ref = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
if (!ref.current || !data.values.length) return;
const ctx = ref.current.getContext("2d");
if (!ctx) 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;
};
const chart = 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;
}
// Handle other types like Point, [number, number], BubbleDataPoint if necessary
// For now, we'll assume they don't contribute to the sum or are handled elsewhere
return a;
},
0
) as number;
const percentage = Math.round((context.parsed * 100) / total);
return `${label}: ${value} (${percentage}%)`;
},
},
},
},
},
plugins: centerText
? [
{
id: "centerText",
beforeDraw: (chart: Chart<"doughnut">) => {
const height = chart.height;
const ctx = chart.ctx;
ctx.restore();
// Calculate the actual chart area width (excluding legend)
// Legend is positioned on the right, so we adjust the center X coordinate
const chartArea = chart.chartArea;
const chartWidth = chartArea.right - chartArea.left;
// Get the center of just the chart area (not including the legend)
const centerX = chartArea.left + chartWidth / 2;
const centerY = height / 2;
// Title text
if (centerText.title) {
ctx.font = "1rem sans-serif"; // Consistent font
ctx.fillStyle = "#6B7280"; // Tailwind gray-500
ctx.textAlign = "center";
ctx.textBaseline = "middle"; // Align vertically
ctx.fillText(centerText.title, centerX, centerY - 10); // Adjust Y offset
}
// Value text
if (centerText.value !== undefined) {
ctx.font = "bold 1.5rem sans-serif"; // Consistent font, larger
ctx.fillStyle = "#1F2937"; // Tailwind gray-800
ctx.textAlign = "center";
ctx.textBaseline = "middle"; // Align vertically
ctx.fillText(
centerText.value.toString(),
centerX,
centerY + 15
); // Adjust Y offset
}
ctx.save();
},
},
]
: [],
});
return () => chart.destroy();
}, [data, centerText]);
return <canvas ref={ref} height={300} />;
}