veza/apps/web/src/components/charts/LineChart.tsx

156 lines
3.8 KiB
TypeScript
Raw Normal View History

import { useMemo } from 'react';
import { Chart, ChartProps } from './Chart';
export interface LineChartData {
label: string;
value: number;
}
export interface LineChartProps extends Omit<ChartProps, 'children'> {
data: LineChartData[];
xAxisLabel?: string;
yAxisLabel?: string;
color?: string;
showGrid?: boolean;
showDots?: boolean;
}
/**
* Composant LineChart pour visualisation de données en ligne.
*/
export function LineChart({
data,
xAxisLabel,
yAxisLabel,
color = '#3b82f6',
showGrid = true,
showDots = true,
height = 300,
className,
...chartProps
}: LineChartProps) {
const { normalizedData, padding } = useMemo(() => {
if (data.length === 0) {
return { normalizedData: [], padding: 40 };
}
const values = data.map((d) => d.value);
const max = Math.max(...values, 0);
const min = Math.min(...values, 0);
const range = max - min || 1;
const padding = 40;
const chartHeight = height - padding * 2;
const chartWidth = 100 - (padding / 10) * 2;
const normalizedData = data.map((item, index) => {
const x = (index / (data.length - 1 || 1)) * chartWidth + padding / 10;
const y =
chartHeight - ((item.value - min) / range) * chartHeight + padding / 10;
return {
...item,
x,
y: y / 10, // Convert to percentage
};
});
return { normalizedData, padding };
}, [data, height]);
if (data.length === 0) {
return (
<Chart height={height} className={className} {...chartProps}>
<div className="flex h-full items-center justify-center text-muted-foreground">
Aucune donnée disponible
</div>
</Chart>
);
}
const pathData = normalizedData
.map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.x} ${point.y}`)
.join(' ');
const strokeWidth = 2;
const dotRadius = 4;
return (
<Chart height={height} className={className} {...chartProps}>
<svg
viewBox="0 0 100 100"
preserveAspectRatio="none"
className="h-full w-full"
>
{/* Grid lines */}
{showGrid && (
<g stroke="currentColor" strokeWidth="0.1" opacity="0.2">
{[0, 25, 50, 75, 100].map((y) => (
<line
key={y}
x1={padding / 10}
y1={y / 10}
x2={100 - padding / 10}
y2={y / 10}
/>
))}
</g>
)}
{/* Y-axis labels */}
{yAxisLabel && (
<text
x={padding / 10 - 2}
y={50}
textAnchor="middle"
dominantBaseline="middle"
transform={`rotate(-90 ${padding / 10 - 2} 50)`}
className="fill-muted-foreground text-[2px]"
fontSize="2"
>
{yAxisLabel}
</text>
)}
{/* X-axis labels */}
{xAxisLabel && (
<text
x={50}
y={100 - padding / 10 + 4}
textAnchor="middle"
className="fill-muted-foreground text-[2px]"
fontSize="2"
>
{xAxisLabel}
</text>
)}
{/* Line */}
<path
d={pathData}
fill="none"
stroke={color}
strokeWidth={strokeWidth / 10}
strokeLinecap="round"
strokeLinejoin="round"
/>
{/* Dots */}
{showDots &&
normalizedData.map((point, index) => (
<circle
key={index}
cx={point.x}
cy={point.y}
r={dotRadius / 10}
fill={color}
className="cursor-pointer transition-all hover:r-[6]"
>
<title>
{point.label}: {data[index].value}
</title>
</circle>
))}
</svg>
</Chart>
);
}