veza/apps/web/src/components/charts/PieChart.tsx
senke fe63c7188e refactor: Phase 3 — Semantic color + hex + z-index migration
Phase 3b: Replace hardcoded hex colors with SUMI palette values
- #66FCF1 (neon cyan) → #7c9dd6 (sumi-accent) across all files
- #3b82f6 (blue-500) → #7c9dd6 in chart components
- #36E5D1 → #7a9e6c (sage), #E4B314 → #c9a84c (gold)
- #E63946 → #d4634a (vermillion)
- Update ThemeSwitcher, AppearanceSettings, SwaggerUI, chart components

Phase 3c: Normalize z-index to SUMI scale
- z-[100] (modals) → z-[400] (--sumi-z-modal)
- z-[110] (player expanded, search) → z-[500] (--sumi-z-popover)
- z-[200] (image viewer) → z-[500]
- z-[35] (navbar overlay) → z-[300] (--sumi-z-overlay)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 01:54:47 +01:00

175 lines
5.2 KiB
TypeScript

import { useMemo } from 'react';
import { Chart, ChartProps } from './Chart';
export interface PieChartData {
label: string;
value: number;
color?: string;
}
export interface PieChartProps extends Omit<ChartProps, 'children'> {
data: PieChartData[];
showLegend?: boolean;
showLabels?: boolean;
innerRadius?: number;
colors?: string[];
}
const DEFAULT_COLORS = [
'#7c9dd6', // indigo
'#d4634a', // vermillion
'#7a9e6c', // sage
'#c9a84c', // gold
'#a8a4a0', // text-secondary
'#e0a0b8', // sakura
'#3eaa5e', // terminal-green
'#c840a0', // graffiti-magenta
];
/**
* Composant PieChart pour visualisation de données en camembert.
*/
export function PieChart({
data,
showLegend = true,
showLabels = false,
innerRadius = 0,
colors = DEFAULT_COLORS,
height = 300,
className,
...chartProps
}: PieChartProps) {
const { segments } = useMemo(() => {
if (data.length === 0) {
return {
segments: [],
};
}
const total = data.reduce((sum, item) => sum + Math.max(0, item.value), 0);
const centerX = 50;
const centerY = 50;
const radius = 40;
const innerR = (innerRadius / 100) * radius;
let currentAngle = -90;
const segments = data.map((item, index) => {
const value = Math.max(0, item.value);
const percentage = total > 0 ? value / total : 0;
const angle = percentage * 360;
const startAngle = currentAngle;
const endAngle = currentAngle + angle;
const startAngleRad = (startAngle * Math.PI) / 180;
const endAngleRad = (endAngle * Math.PI) / 180;
const x1 = centerX + radius * Math.cos(startAngleRad);
const y1 = centerY + radius * Math.sin(startAngleRad);
const x2 = centerX + radius * Math.cos(endAngleRad);
const y2 = centerY + radius * Math.sin(endAngleRad);
const x1Inner = centerX + innerR * Math.cos(startAngleRad);
const y1Inner = centerY + innerR * Math.sin(startAngleRad);
const x2Inner = centerX + innerR * Math.cos(endAngleRad);
const y2Inner = centerY + innerR * Math.sin(endAngleRad);
const largeArcFlag = angle > 180 ? 1 : 0;
const path =
innerR > 0
? `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2} L ${x2Inner} ${y2Inner} A ${innerR} ${innerR} 0 ${largeArcFlag} 0 ${x1Inner} ${y1Inner} Z`
: `M ${centerX} ${centerY} L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2} Z`;
const labelAngle = (startAngle + endAngle) / 2;
const labelAngleRad = (labelAngle * Math.PI) / 180;
const labelRadius = radius * 0.7;
const labelX = centerX + labelRadius * Math.cos(labelAngleRad);
const labelY = centerY + labelRadius * Math.sin(labelAngleRad);
currentAngle += angle;
return {
...item,
path,
color: item.color || colors[index % colors.length],
percentage,
labelX,
labelY,
labelAngle,
};
});
return { segments };
}, [data, innerRadius, colors]);
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>
);
}
return (
<Chart height={height} className={className} {...chartProps}>
<div className="flex h-full flex-col items-center justify-center">
<svg
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid meet"
className="h-full w-full"
>
{/* Segments */}
{segments.map((segment, index) => (
<g key={index}>
<path
d={segment.path}
fill={segment.color}
className="cursor-pointer transition-opacity hover:opacity-80"
stroke="white"
strokeWidth="0.5"
>
<title>
{segment.label}: {segment.value} (
{Math.round(segment.percentage * 100)}%)
</title>
</path>
{showLabels && segment.percentage > 0.05 && (
<text
x={segment.labelX}
y={segment.labelY}
textAnchor="middle"
dominantBaseline="middle"
className="fill-white text-[2px] font-semibold"
fontSize="2"
>
{Math.round(segment.percentage * 100)}%
</text>
)}
</g>
))}
</svg>
{/* Legend */}
{showLegend && (
<div className="mt-4 flex flex-wrap justify-center gap-4">
{segments.map((segment, index) => (
<div key={index} className="flex items-center gap-2 text-sm">
<div
className="h-3 w-3 rounded-full"
style={{ backgroundColor: segment.color }}
/>
<span className="text-muted-foreground">{segment.label}</span>
<span className="text-foreground font-medium">
({segment.value})
</span>
</div>
))}
</div>
)}
</div>
</Chart>
);
}