Skip to content

feat: add interactive visualization command (fallow viz)#33

Open
BartWaardenburg wants to merge 8 commits into
mainfrom
feat/interactive-codebase-visualization
Open

feat: add interactive visualization command (fallow viz)#33
BartWaardenburg wants to merge 8 commits into
mainfrom
feat/interactive-codebase-visualization

Conversation

@BartWaardenburg

Copy link
Copy Markdown
Collaborator

Summary

  • Add fallow viz subcommand that generates a self-contained HTML file with treemap and graph views
  • Treemap: nested Canvas treemap with squarify algorithm, color-coded by status, click-to-drill-down, search, dark mode
  • Graph: d3-force layout with directory/Louvain clustering, filter buttons, hover highlights, pan/zoom
  • Output formats: HTML (default, opens browser), DOT, Mermaid
  • Frontend built with Rolldown (Oxc ecosystem), vanilla TypeScript + d3-force/zoom + graphology-louvain

Rebased from #18.

Test plan

  • cargo run -- viz generates HTML and opens in browser
  • Treemap drill-down and breadcrumb navigation work
  • Graph view clustering toggle and filters work
  • DOT and Mermaid output formats generate valid output
  • Dark mode toggle works in both views

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

🌿 Fallow

⚠️ 2 dead code · ✅ Duplication · ✅ Complexity

Dead code (2 issues)
Category Count
Unused dependencies 2
Metric Value
Dead exports 44.4%
Avg complexity 1

Tip

Run fallow fix --dry-run to preview auto-fixes. Add /** @public */ above exports to preserve them.
See inline review comments for per-finding details.

@github-actions github-actions Bot left a comment

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.

Fallow Allocations

Details
Benchmark suite Current: 2d7c7dc Previous: 93867ff Ratio
Total Bytes Allocated 3036870 bytes 3031062 bytes 1.00
Total Allocations 11957 allocations 11945 allocations 1.00
Peak Memory 422082 bytes 420734 bytes 1.00
Peak Allocations 3878 allocations 5168 allocations 0.75

This comment was automatically generated by workflow using github-action-benchmark.

@github-actions github-actions Bot left a comment

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.

Fallow Binary Size

Details
Benchmark suite Current: 2d7c7dc Previous: 93867ff Ratio
Binary Size (fallow) 122352176 bytes 120895224 bytes 1.01

This comment was automatically generated by workflow using github-action-benchmark.

@github-actions github-actions Bot left a comment

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.

Fallow Benchmarks

Details
Benchmark suite Current: 2d7c7dc Previous: 93867ff Ratio
parse_single_file 39342 ns/iter (± 197) 45718 ns/iter (± 1433) 0.86
full_pipeline_10_files 959723 ns/iter (± 13443) 1134386 ns/iter (± 13178) 0.85
full_pipeline_100_files 1956124 ns/iter (± 20472) 2508647 ns/iter (± 60862) 0.78
full_pipeline_1000_files 11430520 ns/iter (± 155832) 15023901 ns/iter (± 247282) 0.76
resolve_re_export_chains 95989 ns/iter (± 2867) 100273 ns/iter (± 375) 0.96
cache_round_trip 2786 ns/iter (± 14) 1973 ns/iter (± 18) 1.41
dupe_detect_2x500_identical 161257 ns/iter (± 9645) 205327 ns/iter (± 5285) 0.79
dupe_detect_2x2000_identical 732251 ns/iter (± 6462) 914681 ns/iter (± 8636) 0.80
dupe_detect_10x500_identical 1513312 ns/iter (± 14922) 1709302 ns/iter (± 35970) 0.89
dupe_detect_50x200_diverse 462024 ns/iter (± 16295) 530777 ns/iter (± 12876) 0.87
dupe_detect_100x200_mixed 3443195 ns/iter (± 44195) 4235683 ns/iter (± 38499) 0.81
dupe_detect_2x5000_identical 2011548 ns/iter (± 5157) 2536583 ns/iter (± 6925) 0.79

This comment was automatically generated by workflow using github-action-benchmark.

@github-actions github-actions Bot left a comment

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.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark 'Fallow Benchmarks'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.10.

Benchmark suite Current: 2d7c7dc Previous: 93867ff Ratio
cache_round_trip 2786 ns/iter (± 14) 1973 ns/iter (± 18) 1.41

This comment was automatically generated by workflow using github-action-benchmark.

@BartWaardenburg BartWaardenburg force-pushed the feat/interactive-codebase-visualization branch from 2d7c7dc to 69497ae Compare June 14, 2026 08:16
Wire Command::Viz + dispatch + telemetry workflow + --help group; re-add
AnalysisOutput.project field; switch viz.rs off deprecated analyze_with_trace
to analyze_retaining_modules; fix *Finding-wrapper + is_entry_point() API drift.
… help-groups test

Rebuild viz-frontend bundle with the corrected user-facing strings.
…li-crate, project tree

Document the viz command + viz-frontend build, add the [Unreleased] changelog
entry, and unit-test the dot/mermaid emitters and html_escape.
… whitespace

Address /fallow-review SHIP-WITH-NOTES: viz now honors FALLOW_PRODUCTION and
config production (not just the flag) via dispatch.production_for, matching
list/watch; add README section; trim trailing blank line in summary.ts.
The node-drag handlers (graphDragStart/Drag/End) were fully implemented but
never connected. Wire them through interactions.ts (mousedown/mousemove/mouseup)
with a d3-zoom .filter() so a press on a node drags it while a press on empty
space still pans. graphDrag now also sets node.x/y for immediate cursor tracking.
Verified via agent-browser: a node follows the cursor, the rest of the view holds.
Delete the genuinely-dead getGraphFilter/getClusterMode getters and the
zoomToCluster no-op stub, so fallow's dead-code check on viz-frontend is clean.
Rebuild the bundle.
Make the generated visualization usable with assistive tech and the
keyboard. The canvas gets role="img" with a descriptive aria-label and
tabindex so screen-reader users land on it and hear what it is; the view
tabs, graph filters, cluster toggle, and dark-mode button expose
aria-pressed state synced to their active class; the search input and
legend get labels and list semantics.

Motion respects the OS preference: the drag reheat of the d3-force
simulation is skipped under prefers-reduced-motion (the node still tracks
the cursor, its neighbors just do not spring), and a reduced-motion media
block neutralizes CSS transitions. Buttons and the canvas get a
focus-visible outline so keyboard focus is visible.

Frontend-only; regenerates the embedded viz.js/viz.css assets.
@BartWaardenburg BartWaardenburg force-pushed the feat/interactive-codebase-visualization branch from a20c72b to 3e6c412 Compare June 14, 2026 09:14

@riker-wamf riker-wamf left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Second-reviewer review — feat: add interactive visualization command (fallow viz) (@ 3e6c412e, 33 files, +4296/-14)

Reviewed statically (no Rust toolchain in my environment, so I relied on CI + reading; the viz:: unit tests are present and pass logically). The Rust integration is clean and idiomatic; the treemap path is solid. My concerns are concentrated in the graph view's interaction layer, where a chunk of advertised behaviour is wired to write-only state that nothing renders — which is exactly the class of dead code this tool exists to catch.


🔴 Requesting changes — graph interactions are advertised but not implemented

AppState carries graphFocusNode, graphFocusDepth, graphPan, graphZoom (state.ts:17‑20). They are written in several places but never read by renderGraph (it draws from gvs.transform, the d3-zoom-owned GraphViewState, not from these fields). Concretely:

  1. Keys 13 ("graph focus depth") do nothing. interactions.ts:219‑228 set state.graphFocusDepth and call renderGraph, but renderGraph (graph.ts) never references graphFocusDepth. Yet this is advertised in the canvas aria-label ("use the number keys 1-3 to set graph focus depth", main.ts:163‑166) and in the README/CHANGELOG ("keyboard navigation", "drill-down"). The accessibility text describes behaviour that does not exist.
  2. Escape "reset zoom/pan" is broken. interactions.ts:203‑207 sets state.graphZoom = 1; state.graphPan = {0,0} and re-renders — but the real transform lives in gvs.transform and is untouched, so Escape does not actually reset the graph. To reset you need to drive the d3-zoom behaviour's transform (e.g. select(canvas).call(zoomBehavior.transform, ...)).
  3. Clicking a graph node does nothing. handleGraphClick (graph.ts:695) is an empty stub (_state, _hitIdx are _-prefixed to silence noUnusedParameters), and graphGoBack (graph.ts:697) always returns false. So the graph "drill-down" the README promises isn't there, and the dblclick "clear focus" (interactions.ts:184‑189) mutates graphFocusNode, which again nothing reads.

Note this slips through both guards: tsc --noEmit with noUnusedLocals won't flag write-only interface fields, and fallow itself detects unused exports/deps, not unused struct fields — so dogfooding stays green while the dead state remains.

Ask: either implement focus-depth / click-to-focus / working Escape-reset, or remove the dead AppState fields + no-op handlers and correct the aria-label and the README/CHANGELOG so they only claim what ships (treemap drill-down + search + filters + dark mode + the working Escape/Backspace on the treemap). Right now a screen-reader user is told to press keys that do nothing.


🟡 Non-blocking

  1. Committed build artifacts can silently drift. crates/cli/viz-assets/viz.js (179 KB minified) and viz.css are the rolldown/cp outputs of viz-frontend/, embedded via include_str!. Nothing in CI rebuilds or verifies them, so a contributor who edits viz-frontend/src/* and forgets npm run build ships a stale bundle with no signal. The cli-crate.md note on how to rebuild is good; consider also a CI step (npm ci && npm run build && git diff --exit-code crates/cli/viz-assets/) to enforce sync.
  2. Theme duplicated as a literal. main.ts:34‑55 inlines the entire light ColorTheme instead of getTheme(false) from colors.ts, because state isn't created until line 169. Side effect: the summary bar's status-value colours are frozen to the light palette and don't switch in dark mode. Create state first, then pass state.theme to createSummaryBar.
  3. HTML output is non-deterministic. Graph initial positions use Math.random() (graph.ts:299‑300), so each fallow viz run yields a different layout while DOT/Mermaid/JSON are deterministic. A seeded PRNG would make the HTML reproducible (and snapshot-testable).
  4. Binary size +1.46 MB (120.9 → 122.4 MB per the size benchmark) from the embedded assets + open crate + serde monomorphization. Fine for a self-contained design, just flagging that the size-alert is real, not a warm-cache false positive — a viz cargo feature could gate it if size-sensitive.
  5. tooltip.ts:79names.map((n) => n) is a no-op; just names.join(", ").
  6. A11y: breadcrumb segments are clickable <span>s (interactions.ts), not keyboard-focusable like the view/filter <button>s. Minor inconsistency.
  7. viz-frontend/package.json declares d3-transition / @types/d3-transition, which are not imported anywhere in src/ (only a transitive dep of d3-zoom). Self-suppressed by .fallowrc.json turning off unused-dev-dependencies, but it's a genuine unused direct dep — worth dropping.
  8. Tests/verification: Rust unit tests cover DOT/Mermaid/escape only; there's no assertion that write_html embeds the JSON safely (the </<\/ guard is untested) and no frontend tests at all. The test-plan checkboxes are all unchecked. A smoke test that actually opens the generated HTML would have caught the graph-interaction issues above.

Verdict: structurally sound and a nice feature; the Rust side and treemap are good. Blocking only on #1#3 — the graph view's keyboard/click model is non-functional yet advertised, including in accessibility text. Please address the feedback and reply to each point directly, then ask coworkers to re-review.

@riker-wamf riker-wamf left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Second WAAF reviewer — feat: add interactive visualization command (fallow viz) (@ 3e6c412e, reviewed against the thermonuclear maintainability bar)

I reviewed independently from a fresh checkout at this exact head: rebuilt the frontend from source (npm ci && npm run build) and confirmed the committed viz-assets/viz.js + viz.css reproduce byte-for-byte (git diff --stat empty), and tsc --noEmit is clean. To avoid duplication I'll keep this focused on what @riker-wamf's review does not already cover.

Concur with the blocker (independently verified, not re-litigating)

I verified @riker-wamf's #1#3: graphFocusNode / graphFocusDepth / graphPan / graphZoom (state.ts:17‑20) are written-onlygrep across src/ shows the only references outside state.ts are the writes in interactions.ts:186,204‑205,220‑226; nothing in graph.ts ever reads them, and renderGraph draws solely from the d3-zoom-owned gvs.transform. The canvas aria-label (main.ts:162‑165) does tell screen-reader users to "use the number keys 1‑3 to set graph focus depth" — behaviour that does not exist. Under the thermonuclear bar this is a correctly-identified blocker; I support CHANGES_REQUESTED on those points and won't restate the fix.

Additional high-confidence findings (additive)

🟡 Synchronous force-layout has no scale ceiling. Initial layout runs for (let i=0;i<300;i++) sim.tick() (graph.ts:326) plus for (let i=0;i<200;i++) groupSim.tick() (graph.ts:268), fully synchronous on the main thread, with no node cap anywhere. The prefersReducedMotion guard (graph.ts:57,710) only skips drag-reheat, not this initial 300-tick block. Each is O(n+e), so a large monorepo's graph blocks the UI for a noticeable beat on open. Worth a node cap (or "graph too large — showing treemap") and/or offloading ticks. This is the one performance issue the size/alloc CI benches won't surface, since they don't exercise the emitted HTML.

🟡 renderGraph is a full O(n+e) redraw on every hover move. Hover sets gvs.hoveredNode and re-runs the whole node+link draw (graph.ts:420,466‑536). Fine for small graphs; compounds the scalability concern above on large ones.

🟡 Empty-graph produces a NaN transform. When there are zero file nodes the bbox loop never executes, so minX/minY stay Infinity and maxX/maxY stay -Infinity (graph.ts:351‑358); bboxW/bboxH and therefore fitScale/fitX/fitY (graph.ts:361‑365) become NaN, feeding a NaN initial zoom transform. A fileNodes.length === 0 early-return (or identity fallback) avoids it.

🟢 window.__FALLOW_DATA__ is consumed unvalidated (main.ts:18). Self-injected by the Rust writer so not an attack surface, but a missing/shape-mismatched payload throws an opaque error rather than a friendly message — a small guard would help when the HTML is opened standalone.

Verification notes for other reviewers (to pre-empt false positives)

Two things that look like bugs but are correct, so nobody blocks on them:

  • Treemap retina scaling is not cumulative. renderTreemap unconditionally reassigns canvas.width every render (treemap.ts:246), and assigning canvas.width resets the 2D context transform to identity (HTML spec) — so ctx.scale(dpr,dpr) applies once from identity, not compounding. (The graph path reassigns width only conditionally, which is exactly why it correctly uses absolute ctx.setTransform(...) instead.)
  • Cluster-toggle does not leak d3-zoom listeners. Re-select(canvas).call(zoom) replaces listeners under the fixed .zoom namespace rather than stacking them.
  • XSS posture is clean: no innerHTML anywhere in src/; all DOM is built via the el() helper / textContent / replaceChildren, and the Rust writer escapes </<\/ plus HTML-escapes the title before embedding JSON.

Verdict: structurally sound, additive, read-only feature with a clean Rust integration and a reproducible build. Aligned with @riker-wamf — blocking only on the advertised-but-dead graph interaction state (#1#3); the scalability/empty-graph items above are the highest-value additions. Please reply per-point and ask coworkers to re-review once addressed.

(Posted on behalf of Data data@wearemobilefirst.com, WAAF — second of two required reviewers.)

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.

2 participants