Skip to content

Commit bc30a8a

Browse files
committed
Better pie labelling
1 parent d2cfc0f commit bc30a8a

File tree

1 file changed

+91
-121
lines changed
  • src/Nethermind/Nethermind.Runner/scripts

1 file changed

+91
-121
lines changed

src/Nethermind/Nethermind.Runner/scripts/peerPie.ts

+91-121
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,26 @@ import * as d3 from 'd3';
77
// Then update it whenever SSE data arrives
88
// ----------------------------------------------------------
99

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
1718
const svg = d3
1819
.select("#pie-chart")
1920
.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");
2225

23-
// We'll place everything in a group centered in the SVG
2426
const chartGroup = svg
2527
.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})`);
2730

2831
// Color scale (you can change to your own palette)
2932
const color = d3
@@ -41,138 +44,105 @@ const arc = d3
4144
.innerRadius(0)
4245
.outerRadius(radius);
4346

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");
4951

50-
// --- UPDATE FUNCTION (WITH TRANSITIONS) ---
5152
export function updatePieChart(data: { type: string; count: number }[]) {
52-
// 1) Build the pie data (slices)
53+
// 1) Build the pie arcs
5354
const pieData = pie(data);
5455

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+
});
7064

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] = {
7377
x: labelX,
7478
y: offsetY + i * lineHeight
7579
};
7680
});
7781

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);
8286

83-
// EXIT
84-
arcGroups.exit().remove();
87+
paths.exit().remove();
8588

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()
9890
.append("path")
99-
.attr("fill", (d) => color(d.data.type))
10091
.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; });
10295

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")
106118
.attr("fill", "none")
107-
.attr("stroke", "#fff"); // white lines for black background
119+
.attr("stroke", "#fff");
108120

109-
// Label text
110-
arcGroupsEnter
111-
.append("text")
112-
.style("fill", "#fff")
121+
enter.append("text")
122+
.style("alignment-baseline", "middle")
113123
.style("text-anchor", "start")
114-
.style("alignment-baseline", "middle");
124+
.style("fill", "#fff");
115125

116-
// MERGE
117-
const arcGroupsUpdate = arcGroupsEnter.merge(arcGroups);
126+
const all = enter.merge(callouts);
118127

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}`;
128136
});
129137

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})`;
172144
})
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})`; };
177147
});
178148
}

0 commit comments

Comments
 (0)