156 lines
3.8 KiB
TypeScript
156 lines
3.8 KiB
TypeScript
|
|
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>
|
||
|
|
);
|
||
|
|
}
|