Skip to content

fix(webapp): the reads × freshness chart's busiest dot fits inside its own frame (BEA-60) - #111

Merged
ssowonny merged 1 commit into
mainfrom
bea-60-reads-freshness-chart-fixes
Aug 3, 2026
Merged

fix(webapp): the reads × freshness chart's busiest dot fits inside its own frame (BEA-60)#111
ssowonny merged 1 commit into
mainfrom
bea-60-reads-freshness-chart-fixes

Conversation

@ssowonny

@ssowonny ssowonny commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

TL;DR

  • The busiest file on the READS × FRESHNESS chart was drawn half outside the plot frame — the most important dot on the chart was the clipped one.
  • No dot said which file it was, so the "these docs are hot but stale" story needed hovering dot by dot. The six busiest hot+stale files now carry their basename.
  • The "dot size = agent share of reads" caption was printed on top of the "hot + stale" label; it moved into the panel heading.
  • Scales, thresholds, quadrant labels and the danger rectangle are untouched — only the pixel box the log scale maps into changed.
  • Bonus find, fixed here because it made the above untestable: every file in the e2e seed was hours old, so the danger quadrant this panel exists for was empty in every test that had ever run against it.

Before / after

Same seeded wiki project, same data, both shots — only the rendering differs.

before (main) after
before after

On main the top dot renders at cy = 20, r = 5.9M.t is 20, so 5.9px of it is outside the frame. After: cy = 27, r = 5.9.

The heading row on a phone, since the caption now lives there (no horizontal overflow — scrollWidth is exactly the 390px viewport):

mobile

What changed

1. Radius headroom (Insights.tsx, Scatter). Y(maxReads) landed at exactly M.t while the dot radius runs up to 7px, so the hottest file always straddled the top border. The plotted box is now inset on both axes by the largest radius the size formula can produce:

const dotR = (share) => 3 + 4 * share;   // was inline at the circle
const rMax = dotR(1);                    // 7 today — derived, not hardcoded
const X = (d) => M.l + rMax + lx(d) * (W - M.l - M.r - 2 * rMax);
const Y = (r) => H - M.b - rMax - ly(r) * (H - M.t - M.b - 2 * rMax);

Derived rather than hardcoded so a future radius change can't quietly reintroduce the clipping. The thresholds, the danger rect and the dots all read X/Y and move together by a few px; the axis and frame lines use M directly and stay put.

2. Names on the danger dots. The six busiest hot+stale files get their basename beside their dot: to the right, flipped to the left when the text would leave the frame, stacked (down, or up if down would run out of frame) when two would print on one baseline. Placement is placeLabels() in lib/heat.ts — pure, so it is unit-tested on node; Insights.tsx itself can't be, which is why the rest of that arithmetic already lives there. Labels are pointer-events: none, so every dot keeps its <title> and its click.

3. Caption out of the plot. dot size = agent share of reads was a <text> at y = M.t + 28, right-anchored, 14px under the right-anchored hot + stale label. It is now a <span className="in-cap"> in the panel's <h3>, capped at the chart's own 760px max-width so it lands over the plot's right edge rather than the column's.

Deviations from the reviewed plan

Two, both forced by the real code:

  • The plan's e2e assertion couldn't hold as written (".in-pt-label count is between 1 and 6 on the seeded wiki project"). Every file in seedE2E is at most 72h old and STALE_DAYS is 30, so the seed has zero hot+stale points — the danger quadrant has been empty in every test run against this panel. Rather than weaken the assertion, the seed gained three hot-but-months-old files under archive/. Blast radius checked: no spec asserts an exact root-listing or project-history count (home.spec.ts uses >= 6), and all 109 e2e tests pass.
  • Label placement lives in lib/heat.ts, not inline in Scatter(). Same logic the plan specified, moved so it can be unit-tested — the plan's own sketch was pseudo-code for a loop with three failure modes (flip, stack, clamp) worth pinning down.

Architecture changes

architecture/webapp-frontend.md: lib gained one exported member line — heat.ts placeLabels LABEL_MAX — and the components --> lib edge label gained placeLabels. No new types, no new seams, nothing removed.

✅ added · ❌ removed (strikethrough) · unmarked = unchanged

flowchart LR
    components["<div style='text-align:left'><b>components</b><br/>Insights (Scatter, Treemap, HotPath)<br/>FileView FolderListing FileTree<br/>HistoryView DiffView Palette ...</div>"]
    lib["<div style='text-align:left'><b>lib</b><br/>+diff.ts splitLines lcsDiff diffText<br/>+runs.ts groupRuns runFileCount<br/>+heat.ts heatFor heatTotal heatText heatLevel hotPathSplit<br/>+heat.ts ageRange isFlatRange ageSpanLabel (treemap scale)<br/>+heat.ts orphanPaths (reads whose file left the tree)<br/><span style='background:#22c55e55;padding:0 4px;border-radius:3px'>✅ +heat.ts placeLabels LABEL_MAX (scatter danger-dot labels)</span><br/>+sniff.ts sniffBytes BlobText MAX_BYTES<br/>+utils.ts</div>"]
    Note["placeLabels is pure, so the danger-dot<br/>placement is unit-tested on node —<br/>Insights.tsx itself cannot be (JSX)"]
    components -- "diffText groupRuns hotPathSplit <span style='background:#22c55e55;padding:0 5px;border-radius:3px'>✅ placeLabels</span>" --> lib
    lib -.- Note
    classDef added fill:#22c55e22,stroke:#22c55e,stroke-width:2px
    classDef noteBox fill:#88888822,stroke:#888888,stroke-dasharray:2 2
    class Note noteBox
    linkStyle 0 stroke:#22c55e,stroke-width:2px
Loading

Pre-existing, not fixed here: architecture/webapp-frontend.md's mermaid block does not parse at all on main — the note for components line contains {name, template}, and the literal braces break the classDiagram grammar, so GitHub renders that file as an error box. The other three diagrams parse fine. Out of scope for this issue; flagged separately.

Verification

go build ./..., go vet ./... clean
go test ./... all packages pass
npm test (frontend unit) 48 pass, incl. 4 new placeLabels cases
npm run e2e 109 pass, incl. the new dashboard spec
frontend/check-dist.sh internal/webapp/static is fresh
UI evaluation driven in a real browser at 1280px and 390px; before/after above

Rebased onto origin/main after BEA-57 (#110) landed mid-build; the internal/webapp/static conflict was resolved by re-running npm run build, not by picking a side.

Build session

cd $(git worktree list | grep nifty-shtern-360afa | awk '{print $1}') && claude --resume 960cd163-6221-4aff-a5df-f3974524794f

(only works on the machine this ran on)

…s own frame (BEA-60)

Three defects on one panel, all of them stopping it from delivering its one
insight — "these docs are hot but stale, go fix them":

- The hottest file plotted at exactly y = M.t with a radius up to 7px, so the
  most important point on the chart straddled the top border. The plotted box
  is now inset on both axes by the largest radius the size formula can
  produce, derived from that formula rather than hardcoded, so a future radius
  change can't quietly reintroduce the clipping. Thresholds, the danger rect
  and the dots all read X/Y and shift together; the axis lines use M and stay.
- No dot said which file it was — identity lived only in the hover tooltip.
  The six busiest hot+stale files now carry their basename beside their dot,
  flipped to the dot's other side rather than leaving the frame and stacked
  when two would print on one baseline. Placement is a pure function in
  lib/heat.ts so it can be unit-tested; every dot keeps its title and click.
- "dot size = agent share of reads" was drawn inside the <svg> directly under
  the right-anchored "hot + stale" label, 14px apart. It moved to the panel
  heading row, capped at the chart's own max-width.

The e2e seed grew three hot-but-months-old files: every seeded file was hours
old, so the danger quadrant — the whole reason this panel exists — was empty
in every test that had ever run against it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ssowonny
ssowonny requested a review from thefron August 3, 2026 02:27
@ssowonny
ssowonny merged commit 7861b75 into main Aug 3, 2026
2 checks passed
@ssowonny
ssowonny deleted the bea-60-reads-freshness-chart-fixes branch August 3, 2026 04:32
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.

1 participant