11import type { ICheck , CheckTimingPhases } from "@/types/check" ;
2- import { PieChart , Pie , Cell , ResponsiveContainer , Tooltip } from "recharts" ;
2+ import { PieChart , Pie , ResponsiveContainer , Tooltip , Legend } from "recharts" ;
33import Stack from "@mui/material/Stack" ;
44import Typography from "@mui/material/Typography" ;
55import { alpha , useTheme } from "@mui/material/styles" ;
@@ -8,10 +8,10 @@ import { BaseBox } from "@/components/design-elements";
88type TimingsChartProps = { check : ICheck | null } ;
99
1010export 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
1717const 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
5040export 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