Files
edr-platform/apps/edr-freight-web/backoffice/src/components/overview/OverviewDonutChart.tsx
Marshal d3a479c142 ui fix
2026-06-12 14:21:07 +00:00

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>
);
}