Skip to content

Commit 9f32eb6

Browse files
committed
legend
1 parent b200157 commit 9f32eb6

File tree

1 file changed

+82
-36
lines changed

1 file changed

+82
-36
lines changed

client/src/components/checks/ChartTiming.tsx

Lines changed: 82 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ICheck, CheckTimingPhases } from "@/types/check";
2-
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from "recharts";
2+
import { PieChart, Pie, ResponsiveContainer, Tooltip, Legend } from "recharts";
33
import Stack from "@mui/material/Stack";
44
import Typography from "@mui/material/Typography";
55
import { alpha, useTheme } from "@mui/material/styles";
@@ -8,10 +8,10 @@ import { BaseBox } from "@/components/design-elements";
88
type TimingsChartProps = { check: ICheck | null };
99

1010
export type TimingSegment = {
11-
key: keyof CheckTimingPhases;
12-
label: string;
11+
name: string;
1312
value: number;
14-
percent: number;
13+
fill: string;
14+
stroke: string;
1515
};
1616

1717
const PHASE_ORDER: (keyof CheckTimingPhases)[] = [
@@ -24,36 +24,79 @@ const PHASE_ORDER: (keyof CheckTimingPhases)[] = [
2424
"download",
2525
];
2626

27-
const buildTimingSegments = (phases?: CheckTimingPhases): TimingSegment[] => {
27+
const buildTimingSegments = (
28+
phases: CheckTimingPhases | undefined,
29+
stroke: string
30+
): TimingSegment[] => {
2831
if (!phases) return [];
29-
const raw = PHASE_ORDER.map((key) => ({
30-
key,
32+
return PHASE_ORDER.map((key) => ({
33+
name: String(key),
3134
value: Math.max(0, Number((phases as any)[key] ?? 0)),
32-
}));
33-
const total = raw.reduce((sum, p) => sum + p.value, 0);
34-
if (total <= 0) {
35-
return raw.map(({ key, value }) => ({
36-
key,
37-
label: key,
38-
value,
39-
percent: 0,
40-
}));
41-
}
42-
return raw.map(({ key, value }) => ({
43-
key,
44-
label: key,
45-
value,
46-
percent: (value / total) * 100,
35+
fill: "transparent",
36+
stroke,
4737
}));
4838
};
4939

5040
export const TimingsChart = ({ check }: TimingsChartProps) => {
51-
const segments = buildTimingSegments(check?.timings?.phases);
52-
if (!segments.length) return null;
5341
if (!check) return null;
5442

5543
const theme = useTheme();
56-
const strokeColor = alpha(theme.palette.success.main, 0.8);
44+
const strokeColor = alpha(theme.palette.primary.main, 0.8);
45+
const segments = buildTimingSegments(check?.timings?.phases, strokeColor);
46+
if (!segments.length) return null;
47+
const LABEL_THRESHOLD = 0.05;
48+
49+
const renderLabel = ({
50+
cx,
51+
cy,
52+
midAngle,
53+
outerRadius,
54+
name,
55+
value,
56+
percent,
57+
}: any) => {
58+
if (!percent || percent < LABEL_THRESHOLD) return null;
59+
const RAD = Math.PI / 180;
60+
const r = (outerRadius || 0) + 24;
61+
const x = cx + r * Math.cos(-midAngle * RAD);
62+
const y = cy + r * Math.sin(-midAngle * RAD);
63+
const text = `${name}: ${Math.round(Number(value) || 0)} ms`;
64+
return (
65+
<text
66+
x={x}
67+
y={y}
68+
fill={strokeColor}
69+
textAnchor={x > cx ? "start" : "end"}
70+
dominantBaseline="central"
71+
style={{ fontSize: 12 }}
72+
>
73+
{text}
74+
</text>
75+
);
76+
};
77+
78+
const renderLegend = () => (
79+
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
80+
{segments.map((s) => (
81+
<div
82+
key={s.name}
83+
style={{ display: "flex", alignItems: "center", gap: 8 }}
84+
>
85+
<span
86+
style={{
87+
display: "inline-block",
88+
width: 14,
89+
height: 2,
90+
backgroundColor: strokeColor,
91+
}}
92+
/>
93+
<span style={{ fontSize: 12, color: theme.palette.text.secondary }}>
94+
{s.name}: {Math.round(Number(s.value) || 0)} ms
95+
</span>
96+
</div>
97+
))}
98+
</div>
99+
);
57100

58101
return (
59102
<BaseBox p={4} flex={1} height={"100%"}>
@@ -67,18 +110,21 @@ export const TimingsChart = ({ check }: TimingsChartProps) => {
67110
<Pie
68111
data={segments}
69112
dataKey="value"
70-
nameKey="label"
71-
fill="transparent"
72-
stroke={strokeColor}
73-
strokeWidth={1}
74-
label={({ name, value }: any) => `${name}: ${Math.round(Number(value) || 0)} ms`}
75-
>
76-
{segments.map((entry) => (
77-
<Cell key={`cell-${entry.key}`} fill="transparent" />
78-
))}
79-
</Pie>
113+
nameKey="name"
114+
stroke="none"
115+
minAngle={4}
116+
label={renderLabel}
117+
labelLine={{ stroke: strokeColor, strokeWidth: 1, opacity: 0.6 }}
118+
/>
80119
<Tooltip
81-
formatter={(value: any) => `${Math.round(value as number)} ms`}
120+
formatter={(value) => `${Math.round(value as number)} ms`}
121+
/>
122+
<Legend
123+
layout="vertical"
124+
verticalAlign="middle"
125+
align="right"
126+
content={renderLegend}
127+
wrapperStyle={{ paddingLeft: 12 }}
82128
/>
83129
</PieChart>
84130
</ResponsiveContainer>

0 commit comments

Comments
 (0)