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>
175 lines
5.2 KiB
TypeScript
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>
|
|
);
|
|
}
|