From d9197b3ce264d968bcdc0cecc66a45822808ce04 Mon Sep 17 00:00:00 2001 From: conache Date: Fri, 1 May 2026 14:48:26 +0300 Subject: [PATCH 1/2] Completely fill the finalized block node in the forkchoice visualization --- crates/net/rpc/static/fork_choice.html | 40 ++++++++++++++++---------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/crates/net/rpc/static/fork_choice.html b/crates/net/rpc/static/fork_choice.html index 5a49d96..fb0cf29 100644 --- a/crates/net/rpc/static/fork_choice.html +++ b/crates/net/rpc/static/fork_choice.html @@ -188,6 +188,7 @@ let svg, gLinks, gNodes, gAxis; let currentData = null; + let hoveredRoot = null; function initSVG() { svg = d3.select("#chart-container") @@ -212,8 +213,17 @@ return COLORS.default; } - function weightRatio(node, validatorCount) { - if (!validatorCount) return 0; + function nodeStroke(node, data) { + const color = nodeColor(node, data); + return d3.color(color).darker(0.5).toString(); + } + + function nodeRatio(node, data) { + // Finalized blocks have full support by definition — fill completely + // rather than scaling by fork-choice weight (which is 0 at the root). + if (node.slot <= data.finalized.slot) return 1; + const validatorCount = data.validator_count; + if (!validatorCount || validatorCount === 0) return 0; return Math.max(0, Math.min(1, node.weight / validatorCount)); } @@ -309,7 +319,8 @@ x: d.x, y: d.y, _color: nodeColor(d.data, data), - _ratio: weightRatio(d.data, data.validator_count) + _stroke: nodeStroke(d.data, data), + _ratio: nodeRatio(d.data, data) })); const links = []; @@ -334,21 +345,20 @@ return { nodes: flatNodes, links, width: svgWidth, height: svgHeight, slots }; } - // Tracked so render() can refresh the tooltip on each poll without - // requiring the user to move the mouse. - let hoveredRoot = null; - - function tooltipHtml(d, total) { - const pct = total ? parseFloat(((d.weight / total) * 100).toFixed(2)) : 0; + function tooltipHtml(d, data) { + const isFinalized = d.slot <= data.finalized.slot; + const weightLine = isFinalized + ? `status: finalized` + : `weight: ${d.weight}`; return `root: ${truncateRoot(d.root)}
` + `slot: ${d.slot}
` + `proposer: ${d.proposer_index}
` + - `weight: ${d.weight}${total != null ? `/${total} (${pct}%)` : ''}`; + weightLine; } - function showTooltip(event, d) { + function showTooltip(event, d, data) { hoveredRoot = d.root; - tooltip.innerHTML = tooltipHtml(d, currentData?.validator_count); + tooltip.innerHTML = tooltipHtml(d, data); tooltip.style.opacity = 1; tooltip.style.left = (event.clientX + 14) + "px"; tooltip.style.top = (event.clientY - 14) + "px"; @@ -479,8 +489,8 @@ const nodeMerged = nodeEnter.merge(nodeGroups); nodeMerged - .on("mouseover", function (event, d) { showTooltip(event, d); }) - .on("mousemove", function (event, d) { showTooltip(event, d); }) + .on("mouseover", function (event, d) { showTooltip(event, d, data); }) + .on("mousemove", function (event, d) { showTooltip(event, d, data); }) .on("mouseout", hideTooltip); nodeMerged @@ -513,7 +523,7 @@ // Keep the tooltip live while the user holds the mouse still over a node. if (hoveredRoot) { const hovered = layout.nodes.find(n => n.root === hoveredRoot); - if (hovered) tooltip.innerHTML = tooltipHtml(hovered, data.validator_count); + if (hovered) tooltip.innerHTML = tooltipHtml(hovered, data); } // Auto-scroll to head node From ea2b5acfdb9d94c1392d0a8836d3c3d09f0a7ba4 Mon Sep 17 00:00:00 2001 From: conache Date: Fri, 1 May 2026 15:49:21 +0300 Subject: [PATCH 2/2] Cleanup changes based on initial reviews --- crates/net/rpc/static/fork_choice.html | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/net/rpc/static/fork_choice.html b/crates/net/rpc/static/fork_choice.html index fb0cf29..e11b655 100644 --- a/crates/net/rpc/static/fork_choice.html +++ b/crates/net/rpc/static/fork_choice.html @@ -347,13 +347,19 @@ function tooltipHtml(d, data) { const isFinalized = d.slot <= data.finalized.slot; - const weightLine = isFinalized - ? `status: finalized` - : `weight: ${d.weight}`; + let lastLine; + if (isFinalized) { + lastLine = `status: finalized`; + } else { + const total = data.validator_count; + const pct = total ? parseFloat(((d.weight / total) * 100).toFixed(2)) : 0; + const suffix = total != null ? `/${total} (${pct}%)` : ""; + lastLine = `weight: ${d.weight}${suffix}`; + } return `root: ${truncateRoot(d.root)}
` + `slot: ${d.slot}
` + `proposer: ${d.proposer_index}
` + - weightLine; + lastLine; } function showTooltip(event, d, data) { @@ -474,7 +480,7 @@ nodeEnter.append("circle") .attr("class", "node-outer") .attr("r", NODE_RADIUS) - .attr("stroke", d => d._color); + .attr("stroke", d => d._stroke); // Invisible hit target so hover works regardless of fill level. nodeEnter.append("circle") @@ -515,7 +521,7 @@ .transition() .delay(TRANSITION_DURATION) .duration(100) - .attr("stroke", d => d._color); + .attr("stroke", d => d._stroke); nodeMerged.select("text") .text(d => truncateRoot(d.root));