mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 20:40:55 +00:00
129 lines
4.2 KiB
TypeScript
129 lines
4.2 KiB
TypeScript
import {
|
|
Cell,
|
|
Pie,
|
|
PieChart,
|
|
ResponsiveContainer,
|
|
Tooltip,
|
|
} from "recharts";
|
|
import { Box, Group, Paper, Stack, Text } from "@mantine/core";
|
|
|
|
import { overviewChartColors } from "./overview.styles";
|
|
|
|
export interface DonutChartItem {
|
|
name: string;
|
|
value: number;
|
|
}
|
|
|
|
interface OverviewDonutChartProps {
|
|
title: string;
|
|
data: DonutChartItem[];
|
|
emptyMessage?: string;
|
|
}
|
|
|
|
export function OverviewDonutChart({
|
|
title,
|
|
data,
|
|
emptyMessage = "No data available",
|
|
}: OverviewDonutChartProps) {
|
|
const filtered = data.filter((item) => item.value > 0);
|
|
const hasData = filtered.length > 0;
|
|
const total = filtered.reduce((sum, item) => sum + item.value, 0);
|
|
|
|
return (
|
|
<Paper p="md" radius="lg" withBorder h="100%">
|
|
<Stack gap="sm" h="100%">
|
|
<Text fw={600} size="sm">
|
|
{title}
|
|
</Text>
|
|
{!hasData ? (
|
|
<Text size="sm" c="dimmed" ta="center" py="lg">
|
|
{emptyMessage}
|
|
</Text>
|
|
) : (
|
|
<Group gap="md" wrap="nowrap" align="center" style={{ flex: 1 }}>
|
|
{/* Compact donut with the total in its center */}
|
|
<Box style={{ position: "relative", width: 132, height: 132, flexShrink: 0 }}>
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<PieChart>
|
|
<Pie
|
|
data={filtered}
|
|
dataKey="value"
|
|
nameKey="name"
|
|
cx="50%"
|
|
cy="50%"
|
|
innerRadius={42}
|
|
outerRadius={62}
|
|
paddingAngle={2}
|
|
stroke="none"
|
|
>
|
|
{filtered.map((entry, index) => (
|
|
<Cell
|
|
key={entry.name}
|
|
fill={
|
|
overviewChartColors.pipeline[
|
|
index % overviewChartColors.pipeline.length
|
|
]
|
|
}
|
|
/>
|
|
))}
|
|
</Pie>
|
|
<Tooltip formatter={(value) => [value, "Count"]} />
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
<Box
|
|
style={{
|
|
position: "absolute",
|
|
inset: 0,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
pointerEvents: "none",
|
|
}}
|
|
>
|
|
<Text fw={800} size="xl" lh={1} style={{ color: "#0f172a" }}>
|
|
{total}
|
|
</Text>
|
|
<Text size="10px" c="dimmed" fw={600} tt="uppercase" style={{ letterSpacing: 0.4 }}>
|
|
Total
|
|
</Text>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Legend fills the space — color, name, count and share */}
|
|
<Stack gap={6} style={{ flex: 1, minWidth: 0 }}>
|
|
{filtered.map((entry, index) => {
|
|
const color =
|
|
overviewChartColors.pipeline[index % overviewChartColors.pipeline.length];
|
|
const pct = total > 0 ? Math.round((entry.value / total) * 100) : 0;
|
|
return (
|
|
<Group key={entry.name} gap={8} wrap="nowrap" justify="space-between">
|
|
<Group gap={8} wrap="nowrap" style={{ minWidth: 0 }}>
|
|
<Box
|
|
w={9}
|
|
h={9}
|
|
style={{ borderRadius: 3, background: color, flexShrink: 0 }}
|
|
/>
|
|
<Text size="xs" c="dimmed" truncate>
|
|
{entry.name}
|
|
</Text>
|
|
</Group>
|
|
<Group gap={6} wrap="nowrap" style={{ flexShrink: 0 }}>
|
|
<Text size="xs" fw={700} c="dark.5">
|
|
{entry.value}
|
|
</Text>
|
|
<Text size="xs" c="dimmed" style={{ width: 34, textAlign: "right" }}>
|
|
{pct}%
|
|
</Text>
|
|
</Group>
|
|
</Group>
|
|
);
|
|
})}
|
|
</Stack>
|
|
</Group>
|
|
)}
|
|
</Stack>
|
|
</Paper>
|
|
);
|
|
}
|