From d76f3707c312c2cdb6656f49f0f909f239626bde Mon Sep 17 00:00:00 2001 From: dicethedev Date: Tue, 5 May 2026 00:50:52 +0100 Subject: [PATCH 1/3] Add follow head toggle to fork choice viz --- crates/net/rpc/static/fork_choice.html | 119 +++++++++++++++++++++++-- 1 file changed, 113 insertions(+), 6 deletions(-) diff --git a/crates/net/rpc/static/fork_choice.html b/crates/net/rpc/static/fork_choice.html index 5a49d966..cd808190 100644 --- a/crates/net/rpc/static/fork_choice.html +++ b/crates/net/rpc/static/fork_choice.html @@ -51,6 +51,28 @@ gap: 2px; } + #follow-head-toggle { + margin-left: auto; + padding: 7px 12px; + border: 1px solid #ff9800; + border-radius: 4px; + background: transparent; + color: #ff9800; + font-family: "Courier New", Courier, monospace; + font-size: 12px; + font-weight: bold; + cursor: pointer; + } + + #follow-head-toggle:hover { + background: rgba(255, 152, 0, 0.12); + } + + #follow-head-toggle[aria-pressed="false"] { + border-color: #666; + color: #aaa; + } + .value-head { color: #ff9800; } .value-justified { color: #2196f3; } .value-finalized { color: #4caf50; } @@ -157,6 +179,7 @@ Validators -- +
@@ -185,9 +208,15 @@ const container = document.getElementById("chart-container"); const tooltip = document.getElementById("tooltip"); const emptyMsg = document.getElementById("empty-message"); + const followHeadToggle = document.getElementById("follow-head-toggle"); let svg, gLinks, gNodes, gAxis; let currentData = null; + let followHead = true; + let currentHeadScrollTarget = null; + let lastScrollTop = 0; + let programmaticScrollTimeout = null; + let isProgrammaticScroll = false; function initSVG() { svg = d3.select("#chart-container") @@ -359,6 +388,50 @@ tooltip.style.opacity = 0; } + function updateFollowHeadToggle() { + followHeadToggle.setAttribute("aria-pressed", String(followHead)); + followHeadToggle.textContent = followHead ? "Following Head" : "Follow Head"; + } + + function markProgrammaticScroll() { + isProgrammaticScroll = true; + if (programmaticScrollTimeout) clearTimeout(programmaticScrollTimeout); + programmaticScrollTimeout = setTimeout(() => { + isProgrammaticScroll = false; + }, TRANSITION_DURATION + 150); + } + + function scrollToHead(behavior = "smooth") { + if (currentHeadScrollTarget == null) return; + markProgrammaticScroll(); + container.scrollTo({ + top: currentHeadScrollTarget, + behavior + }); + } + + function isAtHead() { + return currentHeadScrollTarget != null && + Math.abs(container.scrollTop - currentHeadScrollTarget) <= NODE_RADIUS * 3; + } + + function setFollowHead(enabled, shouldScroll = false) { + followHead = enabled; + updateFollowHeadToggle(); + + if (followHead && shouldScroll) { + scrollToHead(); + } + } + + function markUserScrollIntent() { + isProgrammaticScroll = false; + if (programmaticScrollTimeout) { + clearTimeout(programmaticScrollTimeout); + programmaticScrollTimeout = null; + } + } + function render(data) { if (!data || !data.nodes || data.nodes.length === 0) { emptyMsg.style.display = "block"; @@ -516,16 +589,22 @@ if (hovered) tooltip.innerHTML = tooltipHtml(hovered, data.validator_count); } - // Auto-scroll to head node + // Auto-scroll to head node if head tracking is enabled. const headNode = layout.nodes.find(n => n.root === data.head); if (headNode) { const containerRect = container.getBoundingClientRect(); const headY = headNode.y; - const scrollTarget = headY - containerRect.height / 2; - container.scrollTo({ - top: Math.max(0, scrollTarget), - behavior: "smooth" - }); + const maxScrollTop = Math.max(0, layout.height - containerRect.height); + currentHeadScrollTarget = Math.min( + maxScrollTop, + Math.max(0, headY - containerRect.height / 2) + ); + + if (followHead) { + scrollToHead(); + } + } else { + currentHeadScrollTarget = null; } } @@ -547,9 +626,37 @@ } initSVG(); + updateFollowHeadToggle(); fetchAndRender(); setInterval(fetchAndRender, POLL_INTERVAL); + followHeadToggle.addEventListener("click", () => { + setFollowHead(!followHead, true); + }); + + container.addEventListener("scroll", () => { + const scrollingUp = container.scrollTop < lastScrollTop - 2; + + if (!isProgrammaticScroll) { + if (followHead && scrollingUp && !isAtHead()) { + setFollowHead(false); + } else if (!followHead && isAtHead()) { + setFollowHead(true); + } + } + + lastScrollTop = container.scrollTop; + }); + + container.addEventListener("wheel", markUserScrollIntent, { passive: true }); + container.addEventListener("pointerdown", markUserScrollIntent); + container.addEventListener("touchstart", markUserScrollIntent, { passive: true }); + window.addEventListener("keydown", event => { + if (["ArrowUp", "PageUp", "Home"].includes(event.key)) { + markUserScrollIntent(); + } + }); + window.addEventListener("resize", () => { if (currentData) render(currentData); }); From 005e98e8e245d67aa53e22c78a546cc194796a9b Mon Sep 17 00:00:00 2001 From: Blessing Samuel Date: Tue, 5 May 2026 00:57:59 +0100 Subject: [PATCH 2/3] Update crates/net/rpc/static/fork_choice.html Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- crates/net/rpc/static/fork_choice.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/net/rpc/static/fork_choice.html b/crates/net/rpc/static/fork_choice.html index cd808190..377947d5 100644 --- a/crates/net/rpc/static/fork_choice.html +++ b/crates/net/rpc/static/fork_choice.html @@ -652,7 +652,7 @@ container.addEventListener("pointerdown", markUserScrollIntent); container.addEventListener("touchstart", markUserScrollIntent, { passive: true }); window.addEventListener("keydown", event => { - if (["ArrowUp", "PageUp", "Home"].includes(event.key)) { + if (["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " "].includes(event.key)) { markUserScrollIntent(); } }); From d8738ab3a90c25e371de1c586055ed6be277bafc Mon Sep 17 00:00:00 2001 From: Blessing Samuel Date: Tue, 5 May 2026 00:58:17 +0100 Subject: [PATCH 3/3] Update crates/net/rpc/static/fork_choice.html Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- crates/net/rpc/static/fork_choice.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/net/rpc/static/fork_choice.html b/crates/net/rpc/static/fork_choice.html index 377947d5..16642050 100644 --- a/crates/net/rpc/static/fork_choice.html +++ b/crates/net/rpc/static/fork_choice.html @@ -649,7 +649,7 @@ }); container.addEventListener("wheel", markUserScrollIntent, { passive: true }); - container.addEventListener("pointerdown", markUserScrollIntent); + container.addEventListener("pointerdown", markUserScrollIntent, { passive: true }); container.addEventListener("touchstart", markUserScrollIntent, { passive: true }); window.addEventListener("keydown", event => { if (["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " "].includes(event.key)) {