This commit is contained in:
Alex Holliday committed 2025-12-22 17:59:11 +00:00
1 parent b200157f28
commit 9f32eb6362
1 file changed
+82 -36
+82 -36
View File
@@ -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 (
<text
x={x}
y={y}
fill={strokeColor}
textAnchor={x > cx ? "start" : "end"}
dominantBaseline="central"
style={{ fontSize: 12 }}
>
{text}
</text>
);
};
const renderLegend = () => (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{segments.map((s) => (
<div
key={s.name}
style={{ display: "flex", alignItems: "center", gap: 8 }}
>
<span
style={{
display: "inline-block",
width: 14,
height: 2,
backgroundColor: strokeColor,
}}
/>
<span style={{ fontSize: 12, color: theme.palette.text.secondary }}>
{s.name}: {Math.round(Number(s.value) || 0)} ms
</span>
</div>
))}
</div>
);
return (
<BaseBox p={4} flex={1} height={"100%"}>
@@ -67,18 +110,21 @@ export const TimingsChart = ({ check }: TimingsChartProps) => {
<Pie
data={segments}
dataKey="value"
nameKey="label"
fill="transparent"
stroke={strokeColor}
strokeWidth={1}
label={({ name, value }: any) => `${name}: ${Math.round(Number(value) || 0)} ms`}
>
{segments.map((entry) => (
<Cell key={`cell-${entry.key}`} fill="transparent" />
))}
</Pie>
nameKey="name"
stroke="none"
minAngle={4}
label={renderLabel}
labelLine={{ stroke: strokeColor, strokeWidth: 1, opacity: 0.6 }}
/>
<Tooltip
formatter={(value: any) => `${Math.round(value as number)} ms`}
formatter={(value) => `${Math.round(value as number)} ms`}
/>
<Legend
layout="vertical"
verticalAlign="middle"
align="right"
content={renderLegend}
wrapperStyle={{ paddingLeft: 12 }}
/>
</PieChart>
</ResponsiveContainer>