|
| 1 | +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; |
| 2 | +import { |
| 3 | + ChartConfig, |
| 4 | + ChartContainer, |
| 5 | + ChartLegend, |
| 6 | + ChartLegendContent, |
| 7 | + ChartTooltip, |
| 8 | + ChartTooltipContent, |
| 9 | +} from '@/components/ui/chart'; |
| 10 | +import { ChartItem } from '@/types'; |
| 11 | +import { useMemo } from 'react'; |
| 12 | +import { Label, Pie, PieChart, Sector } from 'recharts'; |
| 13 | +import { PieSectorDataItem } from 'recharts/types/polar/Pie'; |
| 14 | + |
| 15 | +const PieLabel = ({ viewBox, totalEmployees }: { viewBox?: unknown; totalEmployees: number }) => { |
| 16 | + if (viewBox && typeof viewBox === 'object' && viewBox !== null && 'cx' in viewBox && 'cy' in viewBox) { |
| 17 | + const { cx, cy } = viewBox as { cx: number; cy: number }; |
| 18 | + |
| 19 | + return ( |
| 20 | + <text x={cx} y={cy} textAnchor="middle" dominantBaseline="middle"> |
| 21 | + <tspan x={cx} y={cy} className="fill-foreground text-3xl font-bold"> |
| 22 | + {totalEmployees} |
| 23 | + </tspan> |
| 24 | + |
| 25 | + <tspan x={cx} y={cy + 24} className="fill-muted-foreground"> |
| 26 | + Total |
| 27 | + </tspan> |
| 28 | + </text> |
| 29 | + ); |
| 30 | + } |
| 31 | + |
| 32 | + return null; |
| 33 | +}; |
| 34 | + |
| 35 | +const ActiveShape = ({ outerRadius = 0, ...props }: PieSectorDataItem) => ( |
| 36 | + <g> |
| 37 | + <Sector {...props} outerRadius={outerRadius + 10} /> |
| 38 | + <Sector {...props} outerRadius={outerRadius + 25} innerRadius={outerRadius + 12} /> |
| 39 | + </g> |
| 40 | +); |
| 41 | + |
| 42 | +export default function EmployeesByPositionChart({ chartData }: Readonly<{ chartData: ChartItem }>) { |
| 43 | + const normalizeKey = (s: string) => s.toLowerCase().replaceAll(/\s+/g, '_'); |
| 44 | + |
| 45 | + const transformedData = useMemo(() => { |
| 46 | + return chartData.labels.map((label, i) => ({ |
| 47 | + position: normalizeKey(label), |
| 48 | + total: chartData.data[i] ?? 0, |
| 49 | + fill: `var(--chart-${(i % 6) + 1})`, |
| 50 | + })); |
| 51 | + }, [chartData]); |
| 52 | + |
| 53 | + const chartConfig = useMemo(() => { |
| 54 | + const cfg: Record<string, { label: string; color?: string }> = { |
| 55 | + total: { label: 'Total Employees' }, |
| 56 | + }; |
| 57 | + |
| 58 | + for (const [i, label] of chartData.labels.entries()) { |
| 59 | + cfg[normalizeKey(label)] = { label, color: `var(--chart-${(i % 6) + 1})` }; |
| 60 | + } |
| 61 | + |
| 62 | + return cfg; |
| 63 | + }, [chartData]) satisfies ChartConfig; |
| 64 | + |
| 65 | + const totalEmployees = chartData.data.reduce((sum, val) => sum + val, 0); |
| 66 | + |
| 67 | + const activeIndex = useMemo( |
| 68 | + () => transformedData.findIndex((entry) => entry.position === 'manager'), |
| 69 | + [transformedData], |
| 70 | + ); |
| 71 | + |
| 72 | + return ( |
| 73 | + <Card className="flex h-full flex-col"> |
| 74 | + <CardHeader className="pb-0"> |
| 75 | + <CardTitle>Employees by Position</CardTitle> |
| 76 | + <CardDescription>Number of employees categorized by their job positions.</CardDescription> |
| 77 | + </CardHeader> |
| 78 | + |
| 79 | + <CardContent className="max-h-90 flex-1 pb-0"> |
| 80 | + <ChartContainer config={chartConfig} className="mx-auto aspect-square h-full"> |
| 81 | + <PieChart accessibilityLayer> |
| 82 | + <ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} /> |
| 83 | + <Pie |
| 84 | + data={transformedData} |
| 85 | + dataKey="total" |
| 86 | + nameKey="position" |
| 87 | + innerRadius={60} |
| 88 | + strokeWidth={5} |
| 89 | + activeIndex={activeIndex} |
| 90 | + activeShape={<ActiveShape />} |
| 91 | + > |
| 92 | + <Label content={<PieLabel totalEmployees={totalEmployees} />} /> |
| 93 | + </Pie> |
| 94 | + <ChartLegend |
| 95 | + content={<ChartLegendContent nameKey="position" />} |
| 96 | + className="-translate-y-2 flex-wrap gap-2 *:basis-1/4 *:justify-center" |
| 97 | + /> |
| 98 | + </PieChart> |
| 99 | + </ChartContainer> |
| 100 | + </CardContent> |
| 101 | + </Card> |
| 102 | + ); |
| 103 | +} |
0 commit comments