Skip to content

Graph views: flame, balloon, and hyperbolic layouts with a docked side panel - #102

Merged
JosephMaynard merged 13 commits into
masterfrom
feature/graph-alt-views
Aug 9, 2026
Merged

JosephMaynard merged 13 commits into
masterfrom
feature/graph-alt-views

Conversation

@JosephMaynard

@JosephMaynard JosephMaynard commented Aug 8, 2026 •

Copy link
Copy Markdown
Owner

Implements the graph-view overhaul requested in #91: the floating popover is replaced by a docked side panel shared across all views, and three new dependency visualizations join the classic graph, switchable from the toolbar.

The four layouts

  • Graph (classic, behaviour-preserved) — now zooms to fit the highlighted subtree on selection instead of just panning.
  • Flame — profiler-style icicle: width ∝ path-expanded subtree share, children heaviest-first, lazy draw-time layout (never materialises the multi-hundred-thousand-block expansion; sub-pixel blocks collapse into a spray strip), ancestors-pinned zoom.
  • Balloon — orbital constellation: project at centre, direct deps orbiting, sub-dependency systems fanning behind them; two-tier arc-packed orbits, sub-linear node sizing, minimum-size cartography floors, biggest-first collision-pruned labels, spatial-hash picking, screen-anchored fly-to.
  • Hyperbolic — Poincaré disk: Möbius-translation drag, cursor-anchored Euclidean magnification on top, geodesic edges, meaningful equal-hyperbolic-distance range rings, shell-staggered sibling fans.

Shared UX (the #91 asks)

  • Side panel dossier on selection: version, kind, licence, vulnerabilities, subtree size, "appears in N places" (as coloured icon chips), plus clickable depends-on / required-by chips that refocus the current view, and Open in List.
  • Search with incremental results that flies/zooms/focuses in whichever layout is active.
  • Status line showing the hover path trail along the bottom — nothing ever covers the visualization.
  • Per-mode colour key: lineage hues (one per direct dependency's subtree) in flame/balloon, direct/sub in hyperbolic, red = vulnerable everywhere, dev deps dimmed.
  • Workspace scoping matches the classic view (including its parentless-roots fallback), reset affordances per view, theme-aware rendering, and full listener/rAF cleanup on mode switches.

Also on this branch

  • The scan-evidence banner is now a dismissible fixed toast (and names the responsible flag, e.g. --offline, in each skipped-collector warning).
  • Classic pan/zoom controls restyled; overlay opacity raised since Chromium's backdrop-filter cannot blur composited canvas layers.
  • README documents the views; screenshots refreshed plus two new ones (flame, balloon).

Review

Beyond the usual suite (189 unit tests, dual typecheck, in-browser verification on a ~3,600-package monorepo), the branch went through a 36-agent review workflow: 7 dimension-focused reviewers (derivation math, each view, integration/lifecycle, DOM/CSS/security, CLI/docs) with every finding adversarially verified against the code. 27 confirmed findings were fixed in the final commit — including an "appears in N places" undercount, a NaN-corruption path via stale search results, animation-lifecycle races, and drag-vs-click misclassification. 2 findings were refuted.

Design provenance: ported from research prototypes (spiral-flame and fisheye-lens variants were evaluated and rejected; force-directed layouts deliberately avoided). Hyperbolic re-rooting-on-focus is tracked as follow-up #101.

Closes #91.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added Flame, Balloon, and Hyperbolic dependency visualization modes alongside the classic graph.
    • Added graph mode switching, package search, selection details, hover paths, and workspace-aware views.
    • Added interactive zooming, dragging, focus transitions, reset controls, and responsive layouts.
    • Added vulnerability and lineage-based visualization styling.
    • Added an offline CLI option that skips network-dependent collection tasks.
  • UI Improvements

    • Added a floating details panel with search results, package facts, and dossier information.
    • Updated scan notifications with dismissible status banners.
    • Updated documentation and screenshots for the new graph layouts.

JosephMaynard and others added 11 commits August 8, 2026 11:55
Implements the three alternative dependency visualizations from
docs/VIZ-VIEWS-HANDOFF.md (issue #91), switchable from a segmented control
in the graph toolbar, and moves selection details from the floating popover
into a docked side panel shared by all four modes.

- vizModel.ts: shared derivation layer per workspace — path-expanded
  subtree sizes, heaviest-first child ordering, BFS spanning tree,
  distinct-path counts, lineage hues — all cycle-guarded.
- flameView.ts: icicle plot with lazy draw-time layout (sub-pixel blocks
  pruned into a spray strip), ancestors-pinned zoom, lineage hue per root
  subtree, red override for vulnerable packages.
- balloonView.ts: constellation balloon tree with the tuned constants from
  the prototypes (SHRINK 0.52, FAN 3.6, sub-linear node sizing, two-tier
  root orbits, minimum hub screen sizes, biggest-first label budget,
  spatial-hash picking, LOD while interacting).
- hyperbolicView.ts: Poincaré disk with Möbius-translation drag, animated
  click focus, geodesic edges, meaningful range rings; every transform is
  applied to the virtual hub as well (the documented hub-pinning bug).
- graphModes.ts: mode switcher with reset-on-switch, shared docked dossier
  (version, kind, licence, vulnerabilities, subtree size, appears-in-N,
  clickable depends-on / required-by chips, open-in-list), shared search
  with fly-to/focus per mode, and a bottom status line showing the hover
  path trail instead of a tooltip covering the graph.
- Classic graph view now reports selection through a new onSelect option
  and its floating popover is retired in favour of the side panel; its
  canvas, controls, and behaviour are otherwise untouched.
- Alternative views mount their own canvas and clean up fully on switch
  (AbortController-scoped listeners, cancelled animation frames) — no
  leaked handlers across mode changes.
- Theme-aware: colours resolve from the report's CSS variables at draw
  time; dev-only deps render dimmer; vulnerable packages red in all views.

Verified in-browser on the repo's own scan and on a ~3,600-package
monorepo scan (cal.diy, @calcom/web workspace: 98k-block flame renders in
~16 ms via lazy layout; all modes responsive, console clean, single
canvas across rapid mode cycling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
It no longer occupies layout space above the report: it floats bottom-right
over the content with a close button. The header stat chips still convey
missing evidence in place after dismissal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Skipped-collector warnings now say why: '(scan ran with --offline)' or
'(scan ran with --no-maintenance)', flowing through to the report toast.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e dossier

- The side panel now floats over the canvas with the toolbar's translucent
  blur treatment instead of occupying layout space. Every mode's fit-to-view
  subtracts the covered width (shared --graph-panel-space token, read by the
  classic graph view too, so content centres in the uncovered region);
  small screens switch to a bottom-sheet panel with no inset.
- Balloon: the minor-root orbit is arc-packed instead of fixed-radius, so
  many light roots spread on a readable ring rather than piling into an
  overlapping donut; at idle every node keeps a >=0.85 px dot so leaf
  systems are visible in the fitted overview (interaction LOD unchanged).
- Dossier header facts are now skimmable coloured chips with icons:
  version, kind (dot-coloured to match the graph key), licence,
  vulnerability status (green check / red warning), subtree size, and
  appears-in-N.
- The toolbar key now explains each mode's colours: lineage-hue legend for
  flame/balloon, direct/sub for hyperbolic, plus the vulnerable marker —
  the hues were meaningful but undocumented.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Alt-view canvases now span the whole shell underneath the translucent
  toolbar and status line (no seam band); views inset their content via a
  shared insetTop instead.
- Balloon: minor-orbit roots get angular shares proportional to their
  enclosing radius (even-by-count spacing let differently-sized minors
  overlap into a rope); labels are collision-pruned biggest-first instead
  of drawn into each other; the depth-0 minimum screen size adapts to root
  count so hundreds of direct deps stop forcing ring overlap at fit zoom,
  while roots stay label-eligible at the reduced floor.
- Overlay backgrounds carry ~0.92 opacity: Chromium's backdrop-filter does
  not blur composited canvas layers, so translucency over the graph was
  unreadable; blur remains for surfaces where it works.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, zoom-to-selection

- .graph-overlay-top gets an explicit z-index so alt-view canvases can no
  longer paint over the toolbar or side panel.
- buildVizModel now mirrors the classic view's fallback: a workspace with no
  direct dependencies (hoisting-only monorepo roots) promotes up to 40
  parentless packages, so Workspace root shows the same tree in every mode.
- Classic pan/zoom controls restyled into a compact cluster.
- Balloon and hyperbolic get a floating "Reset view" button (flame resets
  via its pinned ancestors / double-click).
- Balloon's picking grid now indexes every body, so deep leaf nodes are
  selectable once zoomed in; clicking a body flies to it (never zooming
  out), matching search/chip behaviour.
- Classic graph focus now fits the highlighted subtree's bounding box into
  the uncovered viewport instead of just panning to the node.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…disk

- Balloon: fly-to now animates the target's SCREEN position on a straight
  path to centre while scale eases. Interpolating pan and zoom
  independently could send the target off screen mid-flight and back,
  which read as a jarring detour.
- Hyperbolic: mouse-wheel zoom magnifies the disk Euclideanly (1x-12x),
  anchored at the cursor, composing with the Möbius drag. Node radii,
  geodesics, and range rings all scale, so crushed near-rim clusters
  become readable; double-click and Reset view also clear magnification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- focusOn animates the Euclidean magnification offsets back to zero
  alongside the Mobius translation, so a focused node lands at the
  viewport centre regardless of prior zoom/pan.
- The classic controls and Reset button now sit 12px from the side panel,
  matching the 12px gap below the toolbar.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d labels

- Sibling fans larger than six stagger across three radial shells, tripling
  angular breathing room for equal-weight leaf children that previously
  crushed onto a single arc.
- On-screen node radius is capped at 22px: magnification now separates
  nodes instead of inflating them into each other.
- Labels get the same biggest-first collision pruning as the balloon view,
  so focus fans show a readable subset instead of overprinted text
  (hovered/selected labels always win).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- README: the four switchable layouts, shared side panel/search/status
  line, lineage colour semantics, and workspace scoping get their own
  section; feature bullet and image captions updated.
- Screenshots refreshed from a current full scan, plus two new ones for
  the flame and balloon views (05 shows a red vulnerability ring in orbit).
- docs/VIZ-VIEWS-HANDOFF.md removed from the repo: it was a working
  handoff, not user documentation. It is preserved with the prototypes in
  the code-radar research directory, and its one open follow-up
  (hyperbolic re-rooting on focus) is now tracked as issue #101.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Derivation (vizModel):
- occ now counts a root's top-level appearance PLUS paths through its
  dependents, matching the flame view's per-path rendering ("appears in N
  places" was systematically 1 for direct deps that are also transitive).
- Root slugs are filtered against the dependencies map BEFORE the
  parentless fallback check, and the fallback ignores self-dependencies -
  both restoring parity with the classic view.

Hyperbolic:
- Focus animations are cancelled/retargeted by new interactions instead of
  clobbering drags or silently dropping navigation for 420ms.
- Drag detection uses a cumulative screen-space threshold (slow drags no
  longer register as clicks); pointerleave clears hover state; pick()
  skips invisible nodes so empty-space double-click works near the rim;
  focusIndex bounds-checks its index (stale out-of-range indices could
  NaN-corrupt every position); focus/hover labels are exempt from the
  70-label cap they were meant to be forced into.

Balloon:
- flyTo cancels the previous animation; resize() refits only when
  dimensions change (theme flips no longer wipe pan/zoom); fitView seeds
  bounds with the central body; wheel min-zoom tracks the fitted scale;
  spatial-hash pick reach derives from the largest body; cumulative drag
  threshold; devicePixelRatio re-read on resize; placement recursion skips
  subtrees that stay sub-pixel even at maximum zoom.

Flame: hover indices invalidated on relayout; double-click no longer
double-focuses first; dead path cap removed; dpr re-read on resize.

Integration: workspace changes clear stale search results (whose closures
held indices into the discarded model - the NaN-corruption vector).

UI/docs: mode switcher is now an aria-pressed button group instead of an
incomplete ARIA tablist; the scan-status toast dodges the docked panel in
graph mode; README no longer claims the hover trail for the classic view.

Review: 7 dimension reviewers + 29 adversarial verifiers (36 agents);
2 findings refuted, duplicates merged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 8, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d7381659-04be-4212-bc19-f9039b68cdf7

📥 Commits

Reviewing files that changed from the base of the PR and between 949317d and debc58a.

📒 Files selected for processing (1)
  • src/reportTemplateSync.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/reportTemplateSync.test.ts

📝 Walkthrough

Walkthrough

The report UI adds Flame, Balloon, and Hyperbolic layouts beside the classic graph. Shared models, search, selection dossiers, workspace handling, responsive controls, and canvas interactions support all layouts. The CLI adds explicit offline collector state and warnings.

Changes

Graph layouts and shared navigation

Layer / File(s) Summary
Shared visualization model
report-ui/vizModel.ts, report-ui/graphView.ts
Adds dependency indexing, spanning-tree metrics, lineage colors, theme resolution, lifecycle contracts, and exported graph data types.
Alternative canvas layouts
report-ui/flameView.ts, report-ui/balloonView.ts, report-ui/hyperbolicView.ts
Adds three canvas layouts with rendering, hit testing, focus, zoom, selection, resizing, and cleanup.
Graph mode orchestration
report-ui/graphModes.ts, report-ui/graphView.ts
Adds mode switching, model caching, search, package dossiers, focus routing, workspace refresh, panel-aware fitting, and selection callbacks.
Report UI controls and presentation
report-ui/index.html, report-ui/main.ts, report-ui/style.css, src/report.ts, README.md, src/reportTemplateSync.test.ts
Adds layout controls, alternate hosts, status elements, side-panel markup and styling, responsive behavior, scan-warning dismissal, documentation, and template synchronization validation.

Offline collector state

Layer / File(s) Summary
Offline option and warnings
src/cli.ts
Parses --offline, disables registry-backed collectors, and reports explicit skip reasons.

Estimated code review effort: 5 (Critical) | ~100 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Main
  participant GraphModes
  participant AlternativeView
  participant Dossier
  User->>Main: select graph layout or package
  Main->>GraphModes: route mode or search action
  GraphModes->>AlternativeView: mount or focus layout
  AlternativeView->>GraphModes: report hover or selection
  GraphModes->>Dossier: render package details
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR adds the sidebar, search, alternative layouts, and focused navigation, but it does not show depth or dependency-type filtering requested in issue #91. Add filters for dependency depth and dependency type, or document why these requested capabilities are excluded from this implementation.
Out of Scope Changes check ⚠️ Warning The offline CLI changes and dismissible scan-status toast are unrelated to the graph usability objectives in issue #91. Move the offline CLI and scan-status toast changes to separate pull requests, or link issues that define those requirements.
Docstring Coverage ⚠️ Warning Docstring coverage is 40.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the three new layouts and the docked side panel, which are the primary changes.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/graph-alt-views

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 8

🧹 Nitpick comments (8)
report-ui/style.css (3)

1320-1341: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Merge the duplicated .graph-controls and .graph-canvas-shell rule blocks.

.graph-controls sets right: 12px at Line 1326 and then a second block at Line 1377 overrides it with right: max(var(--graph-panel-space, 0px), 12px). .graph-canvas-shell is likewise declared at Line 1121 and again at Line 1337 only to add --graph-panel-space. Source order makes the result correct, but a reader editing the first block will not see the later override.

Move --graph-panel-space into the Line 1121 block and keep one right declaration for .graph-controls.

Also applies to: 1376-1378

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@report-ui/style.css` around lines 1320 - 1341, Merge the duplicate
`.graph-controls` and `.graph-canvas-shell` declarations: move
`--graph-panel-space` into the existing `.graph-canvas-shell` block, remove the
later duplicate block, and retain a single `.graph-controls` `right` declaration
using the intended panel-aware value.

379-389: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive the graph-mode offset from a shared custom property.

Line 381 hard-codes right: 340px. The panel geometry is defined separately: --graph-panel-space: 324px at Line 1340, and the panel itself is width: 300px at right: 12px (Lines 1347-1349). The toast cannot read --graph-panel-space because that property is declared on .graph-canvas-shell and the toast is a body-level fixed element.

Declare --graph-panel-space on :root and override it inside the media query, then use right: calc(var(--graph-panel-space) + 16px) here. One value then drives the panel, the fit insets, and the toast offset.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@report-ui/style.css` around lines 379 - 389, Move the shared
--graph-panel-space declaration to :root so the body-level .graph-mode
.scan-status-banner can access it, and override that property in the relevant
media query for responsive layouts. Replace the hard-coded right offset in
.graph-mode .scan-status-banner with calc(var(--graph-panel-space) + 16px),
preserving the existing mobile override behavior as appropriate.

1676-1679: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The popover is hidden by CSS while its logic still runs every frame.

display: none !important suppresses the popover, but report-ui/graphView.ts still calls updatePopoverPosition at the end of every renderGraph (Line 2595). That reads getBoundingClientRect on a display: none element each frame and computes clamped coordinates that are never shown.

Note that the popover path is load-bearing: showPopover fires options.onSelect, which drives the docked dossier. Do not delete it. Instead, add an early return in updatePopoverPosition when the popover is not rendered, and record why the element is retained.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@report-ui/style.css` around lines 1676 - 1679, Keep the popover behavior and
showPopover flow intact, but update updatePopoverPosition in graphView.ts to
return immediately when the popover is not rendered or is hidden, before calling
getBoundingClientRect or calculating coordinates. Add a concise comment
documenting that the hidden element is retained because showPopover triggers
options.onSelect for the docked dossier.
report-ui/vizModel.ts (1)

239-260: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Cache the resolved theme; resolveVizTheme runs on the render hot path.

resolveVizTheme performs seven getComputedStyle(...).getPropertyValue reads per call. graphModes.ts passes resolveVizTheme directly as the theme callback. flameView.fillFor calls cb.theme() once per drawn bar, so a single frame with thousands of blocks triggers thousands of style resolutions. Cache the resolved value and invalidate it when data-theme changes.

♻️ Proposed memoisation
+let themeCache: { key: string; value: VizTheme } | null = null;
+
 export function resolveVizTheme(): VizTheme {
   const isDark =
     document.documentElement.getAttribute("data-theme") !== "light";
+  const key = isDark ? "dark" : "light";
+  if (themeCache && themeCache.key === key) return themeCache.value;
-  return {
+  const value: VizTheme = {
     isDark,
@@
     panelText: cssVar("--text-primary", isDark ? "`#e6eef8`" : "`#0f172a`"),
   };
+  themeCache = { key, value };
+  return value;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@report-ui/vizModel.ts` around lines 239 - 260, Cache the resolved VizTheme
used by resolveVizTheme so repeated calls reuse one computed value instead of
rereading CSS variables. Invalidate the cache whenever the document root’s
data-theme value changes, ensuring subsequent calls recompute the correct light
or dark theme while preserving the existing fallback values.
report-ui/main.ts (2)

3692-3694: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid the second adaptDataset call.

initGraphView already calls adaptDataset(options.report, options.knownDepKeys, options.resolveDepKey) internally at report-ui/graphView.ts Line 946. Line 3693 runs the same adaptation again over the whole dependency set. adaptDataset iterates every dependency record several times and rebuilds the full dependency map, so the report pays that cost twice when the graph view opens.

Compute the dataset once before initGraphView and pass the same object to both, or expose the adapted dataset on GraphViewHandle.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@report-ui/main.ts` around lines 3692 - 3694, Eliminate the duplicate
adaptDataset call between initGraphModes and initGraphView: compute the adapted
dataset once and reuse the same object for both consumers. Update the
surrounding initialization flow in initGraphView and the graphModes setup so the
existing dataset behavior is preserved without rebuilding the dependency map.

2373-2381: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Confirm the dismiss button keeps a visible focus indicator.

all: unset in style.css at Line 392 removes the default focus outline. The :focus-visible rule at Lines 408-412 restores only color and background. Verify that the resulting contrast change is perceivable in both themes, or add an explicit outline.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@report-ui/main.ts` around lines 2373 - 2381, Update the scan status dismiss
button created in the dismiss handler to retain a clearly visible keyboard-focus
indicator despite the global all: unset styling. Ensure its :focus-visible
styling in the scan-status-dismiss CSS provides sufficient perceivable contrast
in both themes, adding an explicit outline if necessary while preserving the
existing color and background behavior.
report-ui/graphModes.ts (1)

377-396: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Precompute lowercase package names for the search.

The handler runs on every input event. Line 384 calls toLowerCase() on each package name during the scan, so each keystroke allocates one string per scanned package. A query with no match scans the whole index. Build the lowercase names once when the model is built, then reuse them.

Also consider adding aria-live="polite" to the results list so assistive technology announces the result count.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@report-ui/graphModes.ts` around lines 377 - 396, Precompute and store
lowercase package names when ensureModel builds the model, then update the
search handler to compare q against those cached values instead of calling
toLowerCase() for each scanned ref. Preserve the existing match limit and result
rendering, and mark options.searchResults with aria-live="polite" so result
updates are announced.
src/report.ts (1)

392-404: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep the graph markup synchronized.

report-ui/index.html and src/report.ts contain duplicate graph markup. The build does not synchronize these HTML blocks. A future ID change can prevent initGraphModes from initializing in the generated report while the development harness still works. Extract a shared template or add a test that compares their graph IDs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/report.ts` around lines 392 - 404, Synchronize the duplicate graph markup
used by report-ui/index.html and src/report.ts by extracting a shared template,
or add a test that compares all graph-related IDs in both blocks. Ensure
initGraphModes receives the same required elements in generated reports and the
development harness.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@report-ui/balloonView.ts`:
- Around line 460-473: The pointer gesture cleanup is missing cancellation
handling in both views. In report-ui/balloonView.ts lines 460-473, add a canvas
pointercancel listener using { signal } that resets dragging and movedInDrag and
removes the dragging class; in report-ui/hyperbolicView.ts lines 428-441, add
the same cleanup and also clear last so stale coordinates cannot affect the next
gesture.
- Around line 556-561: Update focusIndex to handle packages absent from
firstIdxOf instead of returning silently: either preserve a pickable entry for
pruned packages during place, or resolve and flyTo the nearest placed ancestor.
Ensure search results still move the balloon view for pruned packages.
- Around line 65-118: Update place to enforce a hard maximum on materialised
placements before creating or expanding a node, using a shared placement-budget
constant and the existing px length as the count. When the budget is exhausted,
retain the current placement as a leaf and stop recursing into its children,
preserving already-created arrays and spatial-grid behavior while defining the
truncated view through the resulting leaf node.

In `@report-ui/graphModes.ts`:
- Around line 354-365: Update the workspaceSelect change handler to call
renderDossierEmpty() unconditionally after clearing the discarded model, rather
than only inside the mode !== "graph" branch; keep the existing destroyActive,
mountActive, and statusHint behavior scoped to alternative modes.

In `@report-ui/graphView.ts`:
- Around line 1946-1979: Update the JSDoc for focusViewportOn to describe
fitting the focus layout’s bounding box within the available viewport and
directly assigning the calculated zoom, removing references to the obsolete
1.35× zoom comparison and easing behavior.

In `@report-ui/hyperbolicView.ts`:
- Around line 221-272: In draw, precompute every node’s radius once per frame
into a typed radii array using nodeRadius, then use radii[id] for orderDraw
sorting, node rendering, and both forced and sized label filters. Ensure the
cached radii remains in scope through the label loop, including replacing its
nodeRadius(id) usage, without changing the existing ordering or filtering
behavior.
- Around line 486-501: Update the dpr declaration and resize function in
hyperbolicView so resize refreshes dpr from the current window.devicePixelRatio
before calculating canvas dimensions. Preserve the existing sizing and drawing
logic while ensuring canvas.width and canvas.height use the refreshed ratio.

In `@report-ui/style.css`:
- Around line 1414-1431: Restore a clear keyboard focus indicator by adding an
explicit outline declaration to the :focus-visible rules for
.graph-search-results li button, .graph-dossier-chip, .graph-dossier-open, and
.graph-mode-btn, while preserving their existing focus color, background, and
border styling.

---

Nitpick comments:
In `@report-ui/graphModes.ts`:
- Around line 377-396: Precompute and store lowercase package names when
ensureModel builds the model, then update the search handler to compare q
against those cached values instead of calling toLowerCase() for each scanned
ref. Preserve the existing match limit and result rendering, and mark
options.searchResults with aria-live="polite" so result updates are announced.

In `@report-ui/main.ts`:
- Around line 3692-3694: Eliminate the duplicate adaptDataset call between
initGraphModes and initGraphView: compute the adapted dataset once and reuse the
same object for both consumers. Update the surrounding initialization flow in
initGraphView and the graphModes setup so the existing dataset behavior is
preserved without rebuilding the dependency map.
- Around line 2373-2381: Update the scan status dismiss button created in the
dismiss handler to retain a clearly visible keyboard-focus indicator despite the
global all: unset styling. Ensure its :focus-visible styling in the
scan-status-dismiss CSS provides sufficient perceivable contrast in both themes,
adding an explicit outline if necessary while preserving the existing color and
background behavior.

In `@report-ui/style.css`:
- Around line 1320-1341: Merge the duplicate `.graph-controls` and
`.graph-canvas-shell` declarations: move `--graph-panel-space` into the existing
`.graph-canvas-shell` block, remove the later duplicate block, and retain a
single `.graph-controls` `right` declaration using the intended panel-aware
value.
- Around line 379-389: Move the shared --graph-panel-space declaration to :root
so the body-level .graph-mode .scan-status-banner can access it, and override
that property in the relevant media query for responsive layouts. Replace the
hard-coded right offset in .graph-mode .scan-status-banner with
calc(var(--graph-panel-space) + 16px), preserving the existing mobile override
behavior as appropriate.
- Around line 1676-1679: Keep the popover behavior and showPopover flow intact,
but update updatePopoverPosition in graphView.ts to return immediately when the
popover is not rendered or is hidden, before calling getBoundingClientRect or
calculating coordinates. Add a concise comment documenting that the hidden
element is retained because showPopover triggers options.onSelect for the docked
dossier.

In `@report-ui/vizModel.ts`:
- Around line 239-260: Cache the resolved VizTheme used by resolveVizTheme so
repeated calls reuse one computed value instead of rereading CSS variables.
Invalidate the cache whenever the document root’s data-theme value changes,
ensuring subsequent calls recompute the correct light or dark theme while
preserving the existing fallback values.

In `@src/report.ts`:
- Around line 392-404: Synchronize the duplicate graph markup used by
report-ui/index.html and src/report.ts by extracting a shared template, or add a
test that compares all graph-related IDs in both blocks. Ensure initGraphModes
receives the same required elements in generated reports and the development
harness.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 62f9b5be-b8c2-4054-9b38-3b958d1083dd

📥 Commits

Reviewing files that changed from the base of the PR and between 2da233f and 85f6230.

⛔ Files ignored due to path filters (10)
  • dist/cli.js is excluded by !**/dist/**
  • dist/report-assets.js is excluded by !**/dist/**
  • dist/report.js is excluded by !**/dist/**
  • docs/screenshot-01.jpg is excluded by !**/*.jpg
  • docs/screenshot-02.jpg is excluded by !**/*.jpg
  • docs/screenshot-03.jpg is excluded by !**/*.jpg
  • docs/screenshot-04.jpg is excluded by !**/*.jpg
  • docs/screenshot-05.jpg is excluded by !**/*.jpg
  • report-ui/dist/report.css is excluded by !**/dist/**
  • report-ui/dist/report.iife.js is excluded by !**/dist/**
📒 Files selected for processing (13)
  • README.md
  • report-ui/balloonView.ts
  • report-ui/flameView.ts
  • report-ui/graphModes.ts
  • report-ui/graphView.ts
  • report-ui/hyperbolicView.ts
  • report-ui/index.html
  • report-ui/main.ts
  • report-ui/style.css
  • report-ui/vizModel.ts
  • src/cli.ts
  • src/report-assets.ts
  • src/report.ts

Comment thread report-ui/balloonView.ts
Comment thread report-ui/balloonView.ts
Comment thread report-ui/balloonView.ts
Comment thread report-ui/graphModes.ts
Comment thread report-ui/graphView.ts
Comment thread report-ui/hyperbolicView.ts Outdated
Comment thread report-ui/hyperbolicView.ts
Comment thread report-ui/style.css
Valid findings fixed:
- pointercancel now resets drag state in balloon and hyperbolic views
  (interrupted gestures could leave dragging stuck on).
- hyperbolicView refreshes devicePixelRatio on resize (the balloon/flame
  fix from the agent review had missed this view) and caches per-frame
  node radii used across sorting, rendering, and label filtering.
- Balloon placement gets a hard 250k budget (subtrees beyond it become
  leaves) on top of the geometric cutoff, and focusIndex falls back to the
  nearest placed spanning-tree ancestor so search still navigates to
  pruned packages.
- Workspace changes clear the dossier in every mode, not just alt modes.
- Focus-visible outlines restored on all unset-styled interactive elements
  (search results, chips, open-in-list, mode buttons, toast dismiss).
- --graph-panel-space moved to :root so the fixed scan-status toast derives
  its graph-mode offset from the same token as everything else.
- Search caches lowercased names per model and announces results via
  aria-live; theme token resolution is cached per data-theme value (it was
  re-reading CSS variables per flame bar per frame).
- updatePopoverPosition skips layout work for the CSS-hidden popover, with
  a comment on why the element is retained; stale focus JSDoc corrected.
- New test pins the graph-section ids of report-ui/index.html and the
  src/report.ts template together so the duplicated markup cannot drift.

Skipped: deduplicating the adaptDataset call between initGraphView and
initGraphModes - it is a one-time O(dependencies) adaptation (~ms at init)
and removing it would widen initGraphView's API for no user-visible gain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/reportTemplateSync.test.ts`:
- Around line 13-14: Update the section extraction logic in the synchronization
test to fail immediately when source.indexOf('</section>', start) returns -1,
rather than passing undefined to source.slice. Preserve the existing slicing
behavior when the closing tag is present so malformed graph sections cannot
produce a false-positive test.
- Around line 15-19: Update the ID extraction helper in reportTemplateSync tests
to retain the raw IDs while separately validating uniqueness for each template.
Add an assertion that the number of extracted IDs equals the number of unique
IDs before sorting or comparing templates, so duplicate DOM IDs cause the test
to fail.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 56fb3295-d81d-401d-b16a-174d793bd1cd

📥 Commits

Reviewing files that changed from the base of the PR and between 85f6230 and 949317d.

⛔ Files ignored due to path filters (3)
  • dist/report-assets.js is excluded by !**/dist/**
  • report-ui/dist/report.css is excluded by !**/dist/**
  • report-ui/dist/report.iife.js is excluded by !**/dist/**
📒 Files selected for processing (8)
  • report-ui/balloonView.ts
  • report-ui/graphModes.ts
  • report-ui/graphView.ts
  • report-ui/hyperbolicView.ts
  • report-ui/style.css
  • report-ui/vizModel.ts
  • src/report-assets.ts
  • src/reportTemplateSync.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • report-ui/graphModes.ts
  • report-ui/hyperbolicView.ts
  • report-ui/graphView.ts
  • report-ui/style.css
  • report-ui/balloonView.ts
  • report-ui/vizModel.ts

Comment thread src/reportTemplateSync.test.ts Outdated
Comment thread src/reportTemplateSync.test.ts Outdated
…icate ids

An unclosed graph section previously sliced to end-of-file instead of
failing, and the Set-based id collection would have masked duplicate DOM
ids within one template.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

[feature] Graph view UX improvements

1 participant