feat(website): explorable 3D graph viewer with spotlight explorer#312
Merged
Conversation
The viewer rendered a rotating hairball with a hover tooltip and nothing else: no way to find a symbol, focus a node, or scope the view. Split it into model / scene / sidebar / shell and add the explorer: - Click a node to select it: one-hop neighborhood stays lit, the rest dims, and an info panel shows the declaration, file, and per-family edge counts, with an isolate-2-hops toggle that re-lays out just that ego network. Background clicks clear; orbit drags never select. - Symbols tab: ranked name search (exact > prefix > substring > file), picking a result flies the camera to the node; chips toggle node kinds and edge families on and off. - Files tab: a directory tree of the reduced payload with per-entry symbol counts and editor-style chain collapsing; clicking an entry scopes the view to that directory or file. - `compact` prop starts the sidebar collapsed; the overview embed uses it so the entry page keeps its footprint. Pure helpers live in TtscWebsiteGraphViewerModel and are exercised against the published zod payload; the imperative three.js scene keeps selection repaints off the force simulation by reassigning color accessors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N3Cn3zmu1uUVkYJfwyPFeJ
Picking a file or directory in the Files tab removed every other node from the view, which threw away the surrounding context and re-ran the force layout on each pick. Make it a spotlight instead: the scope's nodes keep their colors, every edge touching the scope stays lit (including edges reaching out to dimmed outside nodes, so external connections stay readable), and everything else dims in place with no relayout. A node selection outranks the file spotlight; kind and edge chips remain true visibility filters, and the explicit isolate action remains the only other thing that removes nodes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N3Cn3zmu1uUVkYJfwyPFeJ
The reduce mirrors rerooted paths on the longest shared directory prefix, written for the legacy dump contract of absolute realpaths. A current ttscgraph dump emits project-relative paths, so the shared prefix came out empty and relativize collapsed every path to its basename, flattening the viewer file structure; and when a relative dump did share one directory, rerooting would strip real structure instead. Skip rerooting entirely unless the paths are absolute. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N3Cn3zmu1uUVkYJfwyPFeJ
The Symbols tab chips were visibility filters: toggling a kind removed those nodes from the view. Align them with the file picker instead: chips select what to spotlight, a file scope and a kind selection intersect (the classes in src/foo), edge-family chips restrict which touching edges light or, alone, light their own endpoints, and everything unselected dims in place. Nothing the explorer picks removes anything from the view anymore; the explicit isolate action is the only remover left. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N3Cn3zmu1uUVkYJfwyPFeJ
Excalidraw is a benchmark fixture but was never graphed for the viewer examples. Generate its payload with the current pipeline (1,495 nodes / 9,583 links reduced from 5,271 / 18,929) and add the example tab. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N3Cn3zmu1uUVkYJfwyPFeJ
The dim color sat so close to the scene background that spotlighted views read as if the other nodes had been removed. Brighten the dimmed node and link colors so a spotlight grays the rest out instead of hiding it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N3Cn3zmu1uUVkYJfwyPFeJ
Contributor
There was a problem hiding this comment.
Pull request overview
This PR upgrades the website’s 3D code graph viewer from a passive “hairball + tooltip” into an explorable UI with selection, spotlighting, and an explorer sidebar, while also fixing path rerooting so project-relative dumps preserve directory structure.
Changes:
- Adds selection + info panel + optional 2-hop isolate, plus “spotlight” semantics for files/kinds/edge families.
- Introduces a sidebar explorer (Files tree + Symbols search) and splits the viewer into model / scene / sidebar / shell components.
- Fixes reduce/reroot logic across the website, package, and benchmark mirrors to avoid flattening project-relative paths.
Reviewed changes
Copilot reviewed 8 out of 10 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| website/src/content/docs/graph/viewer.mdx | Updates viewer documentation to describe exploration, selection, isolate, and spotlighting. |
| website/src/content/docs/graph/index.mdx | Embeds the 3D viewer in compact mode for the index page. |
| website/src/components/graph/TtscWebsiteGraphViewerSidebar.tsx | Adds the explorer sidebar UI (Files tree + Symbols search + chips). |
| website/src/components/graph/TtscWebsiteGraphViewerScene.ts | Adds an imperative three.js scene wrapper with selection + highlighting + camera focus. |
| website/src/components/graph/TtscWebsiteGraphViewer3D.tsx | Rebuilds the viewer shell to integrate scene + sidebar + selection/isolate/spotlight state. |
| website/src/components/graph/TtscWebsiteGraphReduce.ts | Updates browser-side reducer reroot semantics for project-relative dumps. |
| packages/graph/src/reduce.ts | Updates library reducer reroot semantics to match the new dump contract. |
| experimental/benchmark/graph/viewer.mjs | Updates benchmark reducer reroot semantics to match the new dump contract. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+387
to
+391
| {!displayed ? ( | ||
| <div className="pointer-events-none absolute inset-0 flex items-center justify-center font-mono text-[12px] text-neutral-500"> | ||
| {busy ? "Building the graph…" : "Loading…"} | ||
| </div> | ||
| ) : null} |
Comment on lines
+116
to
+122
| const projectFiles = raw.nodes | ||
| .filter((n) => !n.external && !n.ignored) | ||
| .map((n) => n.file); | ||
| const root = | ||
| projectFiles.length > 0 && isAbsolute(projectFiles[0]!) | ||
| ? commonRoot(projectFiles) | ||
| : ""; |
Comment on lines
+138
to
+142
| const projectFiles = raw.nodes.filter((n) => !n.external).map((n) => n.file); | ||
| const root = | ||
| projectFiles.length > 0 && isAbsolute(projectFiles[0]!) | ||
| ? commonRoot(projectFiles) | ||
| : ""; |
Comment on lines
+121
to
+127
| const projectFiles = raw.nodes | ||
| .filter((n) => !n.external && !n.ignored) | ||
| .map((n) => n.file); | ||
| const root = | ||
| projectFiles.length > 0 && isAbsolute(projectFiles[0]) | ||
| ? commonRoot(projectFiles) | ||
| : ""; |
Comment on lines
42
to
+46
| function relativize(abs: string, root: string): string { | ||
| const a = posix(abs); | ||
| const r = posix(root).replace(/\/+$/, ""); | ||
| if (r && (a === r || a.startsWith(r + "/"))) | ||
| if (!r) return a; | ||
| if (a === r || a.startsWith(r + "/")) |
Comment on lines
78
to
83
| function relativize(abs: string, root: string): string { | ||
| const a = posix(abs); | ||
| const r = posix(root).replace(/\/+$/, ""); | ||
| if (r && (a === r || a.startsWith(r + "/"))) | ||
| if (!r) return a; | ||
| if (a === r || a.startsWith(r + "/")) | ||
| return a.slice(r.length).replace(/^\/+/, ""); |
Comment on lines
62
to
67
| function relativize(abs, root) { | ||
| const a = posix(abs); | ||
| const r = posix(root).replace(/\/+$/, ""); | ||
| if (r && (a === r || a.startsWith(r + "/"))) | ||
| if (!r) return a; | ||
| if (a === r || a.startsWith(r + "/")) | ||
| return a.slice(r.length).replace(/^\/+/, ""); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
The 3D viewer rendered a rotating hairball with a hover tooltip and nothing else. Make it explorable: select and inspect nodes, find symbols by name, and spotlight files, kinds, and edge families, without the view ever silently losing nodes.
Scope
compactprop keeps it collapsed on the overview embed): a directory tree of the reduced payload with symbol counts and editor-style chain collapsing, ranked symbol search with camera fly-to, and kind / edge-family chips.Test plan
tsc --noEmitpasses inwebsiteandpackages/graph.🤖 Generated with Claude Code
https://claude.ai/code/session_01N3Cn3zmu1uUVkYJfwyPFeJ