diff --git a/client/src/components/checks/ChartTiming.tsx b/client/src/components/checks/ChartTiming.tsx index c272d0074..3eb3b1c5c 100644 --- a/client/src/components/checks/ChartTiming.tsx +++ b/client/src/components/checks/ChartTiming.tsx @@ -1,5 +1,5 @@ import type { ICheck, CheckTimingPhases } from "@/types/check"; -import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from "recharts"; +import { PieChart, Pie, ResponsiveContainer, Tooltip, Legend } from "recharts"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import { alpha, useTheme } from "@mui/material/styles"; @@ -8,10 +8,10 @@ import { BaseBox } from "@/components/design-elements"; type TimingsChartProps = { check: ICheck | null }; export type TimingSegment = { - key: keyof CheckTimingPhases; - label: string; + name: string; value: number; - percent: number; + fill: string; + stroke: string; }; const PHASE_ORDER: (keyof CheckTimingPhases)[] = [ @@ -24,36 +24,79 @@ const PHASE_ORDER: (keyof CheckTimingPhases)[] = [ "download", ]; -const buildTimingSegments = (phases?: CheckTimingPhases): TimingSegment[] => { +const buildTimingSegments = ( + phases: CheckTimingPhases | undefined, + stroke: string +): TimingSegment[] => { if (!phases) return []; - const raw = PHASE_ORDER.map((key) => ({ - key, + return PHASE_ORDER.map((key) => ({ + name: String(key), value: Math.max(0, Number((phases as any)[key] ?? 0)), - })); - const total = raw.reduce((sum, p) => sum + p.value, 0); - if (total <= 0) { - return raw.map(({ key, value }) => ({ - key, - label: key, - value, - percent: 0, - })); - } - return raw.map(({ key, value }) => ({ - key, - label: key, - value, - percent: (value / total) * 100, + fill: "transparent", + stroke, })); }; export const TimingsChart = ({ check }: TimingsChartProps) => { - const segments = buildTimingSegments(check?.timings?.phases); - if (!segments.length) return null; if (!check) return null; const theme = useTheme(); - const strokeColor = alpha(theme.palette.success.main, 0.8); + const strokeColor = alpha(theme.palette.primary.main, 0.8); + const segments = buildTimingSegments(check?.timings?.phases, strokeColor); + if (!segments.length) return null; + const LABEL_THRESHOLD = 0.05; + + const renderLabel = ({ + cx, + cy, + midAngle, + outerRadius, + name, + value, + percent, + }: any) => { + if (!percent || percent < LABEL_THRESHOLD) return null; + const RAD = Math.PI / 180; + const r = (outerRadius || 0) + 24; + const x = cx + r * Math.cos(-midAngle * RAD); + const y = cy + r * Math.sin(-midAngle * RAD); + const text = `${name}: ${Math.round(Number(value) || 0)} ms`; + return ( + cx ? "start" : "end"} + dominantBaseline="central" + style={{ fontSize: 12 }} + > + {text} + + ); + }; + + const renderLegend = () => ( +
+ {segments.map((s) => ( +
+ + + {s.name}: {Math.round(Number(s.value) || 0)} ms + +
+ ))} +
+ ); return ( @@ -67,18 +110,21 @@ export const TimingsChart = ({ check }: TimingsChartProps) => { `${name}: ${Math.round(Number(value) || 0)} ms`} - > - {segments.map((entry) => ( - - ))} - + nameKey="name" + stroke="none" + minAngle={4} + label={renderLabel} + labelLine={{ stroke: strokeColor, strokeWidth: 1, opacity: 0.6 }} + /> `${Math.round(value as number)} ms`} + formatter={(value) => `${Math.round(value as number)} ms`} + /> +