@@ -7,23 +7,26 @@ import * as d3 from 'd3';
7
7
// Then update it whenever SSE data arrives
8
8
// ----------------------------------------------------------
9
9
10
- // Basic chart config
11
- const width = 200 ;
12
- const height = 100 ;
13
- const margin = 0 ;
14
- const radius = Math . min ( width , height ) / 2 - margin ;
15
-
16
- // Create an SVG and group for the pie chart
10
+ // 1) Chart dimensions
11
+ const pieDiameter = 100 ; // width & height for the *pie itself*
12
+ const labelArea = 120 ; // extra space on the right for labels
13
+ const svgWidth = pieDiameter + labelArea ;
14
+ const svgHeight = pieDiameter ;
15
+ const radius = pieDiameter / 2 ;
16
+
17
+ // 2) Create SVG & group
17
18
const svg = d3
18
19
. select ( "#pie-chart" )
19
20
. append ( "svg" )
20
- . attr ( "width" , width )
21
- . attr ( "height" , height ) ;
21
+ . attr ( "width" , svgWidth )
22
+ . attr ( "height" , svgHeight )
23
+ // allow labels to overflow if you tweak beyond labelArea
24
+ . attr ( "overflow" , "visible" ) ;
22
25
23
- // We'll place everything in a group centered in the SVG
24
26
const chartGroup = svg
25
27
. append ( "g" )
26
- . attr ( "transform" , `translate(${ width / 4 } , ${ height / 2 } )` ) ;
28
+ // center the pie in the *first* pieDiameter pixels
29
+ . attr ( "transform" , `translate(${ pieDiameter / 2 } , ${ pieDiameter / 2 } )` ) ;
27
30
28
31
// Color scale (you can change to your own palette)
29
32
const color = d3
@@ -41,138 +44,105 @@ const arc = d3
41
44
. innerRadius ( 0 )
42
45
. outerRadius ( radius ) ;
43
46
44
- // We create a group for each arc (slice + label + arrow)
45
- function keyFn ( d : d3 . PieArcDatum < { type : string ; count : number } > ) {
46
- // Use the node type as the key
47
- return d . data . type ;
48
- }
47
+ // sliceLayer holds only the 'path's
48
+ const sliceLayer = chartGroup . append ( "g" ) . attr ( "class" , "slice-layer" ) ;
49
+ // labelLayer holds all the callout lines + texts
50
+ const labelLayer = chartGroup . append ( "g" ) . attr ( "class" , "label-layer" ) ;
49
51
50
- // --- UPDATE FUNCTION (WITH TRANSITIONS) ---
51
52
export function updatePieChart ( data : { type : string ; count : number } [ ] ) {
52
- // 1) Build the pie data (slices)
53
+ // 1) Build the pie arcs
53
54
const pieData = pie ( data ) ;
54
55
55
- // 2) Sort slices ALPHABETICALLY (by their `type`) **just** for labeling
56
- // We won't reorder the slices themselves on the chart (that's governed by pieData),
57
- // but we *will* decide the label stacking order by alphabetical type.
58
- const sortedForLabels = pieData
59
- . slice ( )
60
- . sort ( ( a , b ) => a . data . type . localeCompare ( b . data . type ) ) ;
61
-
62
- // 3) Assign each slice a "stacked" y-position for its label
63
- // so that labels don't overlap
64
- const lineHeight = 18 ; // vertical spacing between stacked labels
65
- const offsetY = - ( ( sortedForLabels . length - 1 ) * lineHeight ) / 2 ;
66
-
67
- // We'll store the assigned (x, y) in a dictionary keyed by slice's "type"
68
- const labelX = radius * 1.25 ; // All labels on the right side
69
- const labelPositions : Record < string , { x : number ; y : number } > = { } ;
56
+ // 2) Augment each arc with its centroid Y
57
+ interface Aug extends d3 . PieArcDatum < { type : string ; count : number } > {
58
+ centroidY : number ;
59
+ }
60
+ const withCentroid : Aug [ ] = pieData . map ( d => {
61
+ const [ cx , cy ] = arc . centroid ( d ) ;
62
+ return { ...d , centroidY : cy } ;
63
+ } ) ;
70
64
71
- sortedForLabels . forEach ( ( slice , i ) => {
72
- labelPositions [ slice . data . type ] = {
65
+ // 3) Sort by centroidY so top‑most slices label first
66
+ const stack = withCentroid
67
+ . slice ( )
68
+ . sort ( ( a , b ) => a . centroidY - b . centroidY ) ;
69
+
70
+ // 4) Compute stacked label positions
71
+ const lineHeight = 18 ;
72
+ const offsetY = - ( ( stack . length - 1 ) * lineHeight ) / 2 ;
73
+ const labelX = radius + 20 ; // still all on the right
74
+ const labelPos : Record < string , { x : number ; y : number } > = { } ;
75
+ stack . forEach ( ( d , i ) => {
76
+ labelPos [ d . data . type ] = {
73
77
x : labelX ,
74
78
y : offsetY + i * lineHeight
75
79
} ;
76
80
} ) ;
77
81
78
- // -- Data Join for the arcs themselves --
79
- const arcGroups = chartGroup
80
- . selectAll < SVGGElement , typeof pieData [ 0 ] > ( "g.slice " )
81
- . data ( pieData , keyFn ) ;
82
+ // ---- SLICES LAYER -- --
83
+ const paths = sliceLayer
84
+ . selectAll < SVGPathElement , typeof pieData [ 0 ] > ( "path " )
85
+ . data ( pieData , d => d . data . type ) ;
82
86
83
- // EXIT
84
- arcGroups . exit ( ) . remove ( ) ;
87
+ paths . exit ( ) . remove ( ) ;
85
88
86
- // ENTER
87
- const arcGroupsEnter = arcGroups
88
- . enter ( )
89
- . append ( "g" )
90
- . attr ( "class" , "slice" )
91
- . each ( function ( d ) {
92
- // store the initial angles so we can animate from them
93
- ( this as any ) . _current = d ;
94
- } ) ;
95
-
96
- // Each group contains: <path>, <polyline>, <text>
97
- arcGroupsEnter
89
+ const pathsEnter = paths . enter ( )
98
90
. append ( "path" )
99
- . attr ( "fill" , ( d ) => color ( d . data . type ) )
100
91
. attr ( "stroke" , "#222" )
101
- . style ( "stroke-width" , "1px" ) ;
92
+ . style ( "stroke-width" , "1px" )
93
+ . attr ( "fill" , d => color ( d . data . type ) )
94
+ . each ( function ( d ) { ( this as any ) . _current = d ; } ) ;
102
95
103
- // We'll use a polyline for the kinked callout line
104
- arcGroupsEnter
105
- . append ( "polyline" )
96
+ pathsEnter . merge ( paths )
97
+ . transition ( ) . duration ( 750 )
98
+ . attrTween ( "d" , function ( d ) {
99
+ const interp = d3 . interpolate ( ( this as any ) . _current , d ) ;
100
+ ( this as any ) . _current = interp ( 0 ) ;
101
+ return t => arc ( interp ( t ) ) ! ;
102
+ } ) ;
103
+
104
+ // ---- LABELS LAYER ----
105
+ // One <g> per slice for polyline+text
106
+ const callouts = labelLayer
107
+ . selectAll < SVGGElement , Aug > ( "g.label" )
108
+ . data ( withCentroid , d => d . data . type ) ;
109
+
110
+ callouts . exit ( ) . remove ( ) ;
111
+
112
+ const enter = callouts . enter ( )
113
+ . append ( "g" )
114
+ . attr ( "class" , "label" )
115
+ . style ( "pointer-events" , "none" ) ;
116
+
117
+ enter . append ( "polyline" )
106
118
. attr ( "fill" , "none" )
107
- . attr ( "stroke" , "#fff" ) ; // white lines for black background
119
+ . attr ( "stroke" , "#fff" ) ;
108
120
109
- // Label text
110
- arcGroupsEnter
111
- . append ( "text" )
112
- . style ( "fill" , "#fff" )
121
+ enter . append ( "text" )
122
+ . style ( "alignment-baseline" , "middle" )
113
123
. style ( "text-anchor" , "start" )
114
- . style ( "alignment-baseline " , "middle " ) ;
124
+ . style ( "fill " , "#fff " ) ;
115
125
116
- // MERGE
117
- const arcGroupsUpdate = arcGroupsEnter . merge ( arcGroups ) ;
126
+ const all = enter . merge ( callouts ) ;
118
127
119
- // 4) Animate the arc <path>
120
- arcGroupsUpdate
121
- . select ( "path" )
122
- . transition ( )
123
- . duration ( 750 )
124
- . attrTween ( "d" , function ( d ) {
125
- const i = d3 . interpolate ( ( this as any ) . _current , d ) ;
126
- ( this as any ) . _current = i ( 0 ) ;
127
- return ( t ) => arc ( i ( t ) ) ! ;
128
+ // 5) Animate the lines
129
+ all . select ( "polyline" )
130
+ . transition ( ) . duration ( 750 )
131
+ . attr ( "points" , d => {
132
+ const [ cx , cy ] = arc . centroid ( d ) ;
133
+ const { x : px , y : py } = labelPos [ d . data . type ] ;
134
+ const mx = px * 0.6 ;
135
+ return `${ cx } ,${ cy } ${ mx } ,${ py } ${ px } ,${ py } ` ;
128
136
} ) ;
129
137
130
- // 5) For each slice, we figure out its final label position from
131
- // labelPositions[d.data.type]. Then we draw a polyline
132
- // from the slice’s arc centroid to that label, with one “kink.”
133
-
134
- arcGroupsUpdate
135
- . select ( "polyline" )
136
- . transition ( )
137
- . duration ( 750 )
138
- . attr ( "points" , ( function ( d ) {
139
- // Arc centroid
140
- const [ cx , cy ] = arc . centroid ( d ) ;
141
- // Stacked label position
142
- const { x : lx , y : ly } = labelPositions [ d . data . type ] ;
143
-
144
- // We'll define:
145
- // 1) from (cx, cy) horizontally to (radius+10, cy) [kink corner #1]
146
- // 2) then vertically to (radius+10, ly) [kink corner #2]
147
- // 3) then horizontally to (lx, ly)
148
-
149
- // If you prefer exactly one kink, you can do:
150
- // 1) from (cx, cy) to (radius+10, ly) [one corner]
151
- // 2) then (lx, ly)
152
- // That’s two line segments total, forming one corner.
153
-
154
- const kinkX = radius + 10 ;
155
-
156
- // Example: single-corner approach
157
- return [
158
- [ cx , cy ] ,
159
- [ kinkX , ly ] ,
160
- [ lx , ly ]
161
- ] ;
162
- } ) as any ) ;
163
-
164
- // 6) Animate the text to the stacked position, and set the label text
165
- arcGroupsUpdate
166
- . select ( "text" )
167
- . transition ( )
168
- . duration ( 750 )
169
- . attr ( "transform" , function ( d ) {
170
- const { x : lx , y : ly } = labelPositions [ d . data . type ] ;
171
- return `translate(${ lx } , ${ ly } )` ;
138
+ // 6) Animate the texts
139
+ all . select ( "text" )
140
+ . transition ( ) . duration ( 750 )
141
+ . attr ( "transform" , d => {
142
+ const p = labelPos [ d . data . type ] ;
143
+ return `translate(${ p . x } ,${ p . y } )` ;
172
144
} )
173
- . tween ( "text" , function ( d ) {
174
- return ( ) => {
175
- ( this as any ) . textContent = `${ d . data . type } (${ d . data . count } )` ;
176
- } ;
145
+ . tween ( "label" , function ( d ) {
146
+ return ( ) => { ( this as any ) . textContent = `${ d . data . type } (${ d . data . count } )` ; } ;
177
147
} ) ;
178
148
}
0 commit comments