Skip to content

feat(forkchoice): add color legend to fork choice viz#338

Open
mvanhorn wants to merge 1 commit intolambdaclass:mainfrom
mvanhorn:osc/335-fork-choice-legend
Open

feat(forkchoice): add color legend to fork choice viz#338
mvanhorn wants to merge 1 commit intolambdaclass:mainfrom
mvanhorn:osc/335-fork-choice-legend

Conversation

@mvanhorn
Copy link
Copy Markdown

@mvanhorn mvanhorn commented May 1, 2026

Pull Request Template

🗒️ Description / Motivation

Adds a four-swatch color legend to the right side of the info-bar on the fork choice visualization (crates/net/rpc/static/fork_choice.html).

Issue #335 lists the four colors the viz uses and asks for an in-page legend so users do not have to guess which color means what. The mapping lives in the COLORS object inside the page script (lines 178-181) and is never surfaced in the rendered UI today, so anyone looking at the viz either has to know the convention from project docs or open dev tools and read the JS source. A small swatch-and-label list in the existing top bar fixes that without changing any of the actual viz logic.

What Changed

crates/net/rpc/static/fork_choice.html:

  • Adds #legend, .legend-item, and .legend-swatch CSS rules in the existing <style> block. The legend uses margin-left: auto so it pins to the right edge of the #info-bar, and flex-wrap: wrap so it relaxes onto a second line on narrow viewports without breaking the existing Head/Justified/Finalized/Validators value cards.
  • Adds a <div id="legend" aria-label="Block color legend"> element after the Validators item inside #info-bar. Four .legend-item rows, one per role: Finalized (#4caf50), Justified (#2196f3), Safe target (#ffeb3b), Head (#ff9800). Hex values are copied from the COLORS object in the existing JS so the swatches cannot drift from the actual rendered colors. Label text uses the same casing convention (uppercase, 11px, #888) the existing info-bar labels use.

No JavaScript changes. The COLORS object stays the single source of truth for what each role renders as; the legend is hand-keyed to match.

Correctness / Behavior Guarantees

  • No JS logic touched. The viz still renders nodes through nodeColor(node, data) (line 209) using the same COLORS object that drives the new legend swatches.
  • No new network calls or polling. The legend is static markup.
  • The existing #info-bar flex container is preserved. The legend is appended after the Validators item; the existing four value cards keep their original order and alignment.
  • aria-label="Block color legend" is set on the legend container so screen readers announce the section.
  • flex-wrap: wrap on #info-bar was already present in the existing styles, so the legend wrapping onto a second line on narrow viewports does not change layout behavior elsewhere.

Tests Added / Run

fork_choice.html is a static asset served directly by the RPC layer (GET /lean/v0/fork_choice/ui). There is no compile or asset build step to run, so the existing test suite does not cover it.

I did not bring up a node + browser locally to confirm the legend renders (no devnet available in this environment). The swatch hex values are byte-equal to the .value-* CSS rules and the JS COLORS object that already exist in the file, so the displayed swatches cannot drift from the actual node colors. After the change:

$ grep -F "background: #" crates/net/rpc/static/fork_choice.html
        <div class="legend-item"><span class="legend-swatch" style="background: #4caf50;"></span>Finalized</div>
        <div class="legend-item"><span class="legend-swatch" style="background: #2196f3;"></span>Justified</div>
        <div class="legend-item"><span class="legend-swatch" style="background: #ffeb3b;"></span>Safe target</div>
        <div class="legend-item"><span class="legend-swatch" style="background: #ff9800;"></span>Head</div>

matches the four entries in the existing COLORS = { finalized, justified, safeTarget, head } declaration.

Related Issues / PRs

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 1, 2026

Greptile Summary

This PR adds a four-swatch color legend to the #info-bar in fork_choice.html, surfacing the existing COLORS object visually so users can identify node roles without reading JS source. The change is purely static markup and CSS with no logic touched.

Confidence Score: 4/5

Safe to merge; all findings are non-blocking P2 suggestions.

No logic changes, only static HTML/CSS additions. P2 findings only: missing default-color swatch, ineffective aria-label, and undocumented coupling between inline hex values and the JS COLORS object.

crates/net/rpc/static/fork_choice.html — legend completeness and accessibility attributes.

Important Files Changed

Filename Overview
crates/net/rpc/static/fork_choice.html Adds a static color-legend bar to the fork-choice viz; CSS and HTML are well-structured, but the legend omits the default (#666) node color, the aria-label has no role, and inline hex values are not linked to the JS COLORS object.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["nodeColor(node, data)"] --> B{node.root == data.head?}
    B -- yes --> C["COLORS.head #ff9800"]
    B -- no --> D{node.root == data.safe_target?}
    D -- yes --> E["COLORS.safeTarget #ffeb3b"]
    D -- no --> F{node.root == data.justified.root?}
    F -- yes --> G["COLORS.justified #2196f3"]
    F -- no --> H{node.slot <= data.finalized.slot?}
    H -- yes --> I["COLORS.finalized #4caf50"]
    H -- no --> J["COLORS.default #666 (not in legend)"]

    subgraph Legend["New Legend"]
        L1["Finalized #4caf50"]
        L2["Justified #2196f3"]
        L3["Safe target #ffeb3b"]
        L4["Head #ff9800"]
    end
Loading
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
crates/net/rpc/static/fork_choice.html:185-190
**Legend omits the default (grey) node color**

`nodeColor()` returns `COLORS.default` (`#666`) for any block that isn't head, safe-target, justified, or finalized. Those grey nodes will appear in the viz but aren't explained by the legend, leaving users in the same "have to guess" situation the legend is meant to fix. Adding a fifth swatch for the pending/default state would make the legend complete.

```suggestion
    <div id="legend" aria-label="Block color legend" role="region">
      <div class="legend-item"><span class="legend-swatch" style="background: #4caf50;"></span>Finalized</div>
      <div class="legend-item"><span class="legend-swatch" style="background: #2196f3;"></span>Justified</div>
      <div class="legend-item"><span class="legend-swatch" style="background: #ffeb3b;"></span>Safe target</div>
      <div class="legend-item"><span class="legend-swatch" style="background: #ff9800;"></span>Head</div>
      <div class="legend-item"><span class="legend-swatch" style="background: #666;"></span>Default</div>
    </div>
```

### Issue 2 of 3
crates/net/rpc/static/fork_choice.html:185
**`aria-label` ineffective without a role on a generic `div`**

Screen readers only announce `aria-label` on elements that have an implicit or explicit ARIA role. A plain `<div>` has no role, so the label `"Block color legend"` will be silently ignored by most assistive technology. Adding `role="region"` turns this into a named landmark that screen readers will announce.

### Issue 3 of 3
crates/net/rpc/static/fork_choice.html:186-189
**Inline hex values will silently drift from `COLORS`**

The swatch colors are hardcoded as inline `style` attributes. The file already has `.value-head`, `.value-justified`, and `.value-finalized` CSS classes whose `color` values are kept in sync with the JS `COLORS` object. If `COLORS` is updated, the legend swatches won't change automatically. Consider using a CSS custom property or at minimum a comment like `/* must match COLORS in JS */` to signal the coupling.

Reviews (1): Last reviewed commit: "feat(forkchoice): add color legend to fo..." | Re-trigger Greptile

Comment on lines +185 to +190
<div id="legend" aria-label="Block color legend">
<div class="legend-item"><span class="legend-swatch" style="background: #4caf50;"></span>Finalized</div>
<div class="legend-item"><span class="legend-swatch" style="background: #2196f3;"></span>Justified</div>
<div class="legend-item"><span class="legend-swatch" style="background: #ffeb3b;"></span>Safe target</div>
<div class="legend-item"><span class="legend-swatch" style="background: #ff9800;"></span>Head</div>
</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Legend omits the default (grey) node color

nodeColor() returns COLORS.default (#666) for any block that isn't head, safe-target, justified, or finalized. Those grey nodes will appear in the viz but aren't explained by the legend, leaving users in the same "have to guess" situation the legend is meant to fix. Adding a fifth swatch for the pending/default state would make the legend complete.

Suggested change
<div id="legend" aria-label="Block color legend">
<div class="legend-item"><span class="legend-swatch" style="background: #4caf50;"></span>Finalized</div>
<div class="legend-item"><span class="legend-swatch" style="background: #2196f3;"></span>Justified</div>
<div class="legend-item"><span class="legend-swatch" style="background: #ffeb3b;"></span>Safe target</div>
<div class="legend-item"><span class="legend-swatch" style="background: #ff9800;"></span>Head</div>
</div>
<div id="legend" aria-label="Block color legend" role="region">
<div class="legend-item"><span class="legend-swatch" style="background: #4caf50;"></span>Finalized</div>
<div class="legend-item"><span class="legend-swatch" style="background: #2196f3;"></span>Justified</div>
<div class="legend-item"><span class="legend-swatch" style="background: #ffeb3b;"></span>Safe target</div>
<div class="legend-item"><span class="legend-swatch" style="background: #ff9800;"></span>Head</div>
<div class="legend-item"><span class="legend-swatch" style="background: #666;"></span>Default</div>
</div>
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/net/rpc/static/fork_choice.html
Line: 185-190

Comment:
**Legend omits the default (grey) node color**

`nodeColor()` returns `COLORS.default` (`#666`) for any block that isn't head, safe-target, justified, or finalized. Those grey nodes will appear in the viz but aren't explained by the legend, leaving users in the same "have to guess" situation the legend is meant to fix. Adding a fifth swatch for the pending/default state would make the legend complete.

```suggestion
    <div id="legend" aria-label="Block color legend" role="region">
      <div class="legend-item"><span class="legend-swatch" style="background: #4caf50;"></span>Finalized</div>
      <div class="legend-item"><span class="legend-swatch" style="background: #2196f3;"></span>Justified</div>
      <div class="legend-item"><span class="legend-swatch" style="background: #ffeb3b;"></span>Safe target</div>
      <div class="legend-item"><span class="legend-swatch" style="background: #ff9800;"></span>Head</div>
      <div class="legend-item"><span class="legend-swatch" style="background: #666;"></span>Default</div>
    </div>
```

How can I resolve this? If you propose a fix, please make it concise.

<span class="label">Validators</span>
<span class="value value-validators" id="validator-count">--</span>
</div>
<div id="legend" aria-label="Block color legend">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 aria-label ineffective without a role on a generic div

Screen readers only announce aria-label on elements that have an implicit or explicit ARIA role. A plain <div> has no role, so the label "Block color legend" will be silently ignored by most assistive technology. Adding role="region" turns this into a named landmark that screen readers will announce.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/net/rpc/static/fork_choice.html
Line: 185

Comment:
**`aria-label` ineffective without a role on a generic `div`**

Screen readers only announce `aria-label` on elements that have an implicit or explicit ARIA role. A plain `<div>` has no role, so the label `"Block color legend"` will be silently ignored by most assistive technology. Adding `role="region"` turns this into a named landmark that screen readers will announce.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +186 to +189
<div class="legend-item"><span class="legend-swatch" style="background: #4caf50;"></span>Finalized</div>
<div class="legend-item"><span class="legend-swatch" style="background: #2196f3;"></span>Justified</div>
<div class="legend-item"><span class="legend-swatch" style="background: #ffeb3b;"></span>Safe target</div>
<div class="legend-item"><span class="legend-swatch" style="background: #ff9800;"></span>Head</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Inline hex values will silently drift from COLORS

The swatch colors are hardcoded as inline style attributes. The file already has .value-head, .value-justified, and .value-finalized CSS classes whose color values are kept in sync with the JS COLORS object. If COLORS is updated, the legend swatches won't change automatically. Consider using a CSS custom property or at minimum a comment like /* must match COLORS in JS */ to signal the coupling.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/net/rpc/static/fork_choice.html
Line: 186-189

Comment:
**Inline hex values will silently drift from `COLORS`**

The swatch colors are hardcoded as inline `style` attributes. The file already has `.value-head`, `.value-justified`, and `.value-finalized` CSS classes whose `color` values are kept in sync with the JS `COLORS` object. If `COLORS` is updated, the legend swatches won't change automatically. Consider using a CSS custom property or at minimum a comment like `/* must match COLORS in JS */` to signal the coupling.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fork choice viz: add legend for each block color

1 participant