Skip to content

feat(website): explorable 3D graph viewer with spotlight explorer#312

Merged
samchon merged 6 commits into
masterfrom
feat/graph-viewer-explorer
Jul 2, 2026
Merged

feat(website): explorable 3D graph viewer with spotlight explorer#312
samchon merged 6 commits into
masterfrom
feat/graph-viewer-explorer

Conversation

@samchon

@samchon samchon commented Jul 2, 2026

Copy link
Copy Markdown
Owner

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

  • Selection: click a node to light its one-hop neighborhood and dim the rest, with an info panel (declaration, file, per-family edge counts) and an isolate-2-hops toggle that re-lays out just that ego network. Background clicks clear; orbit drags never select.
  • Explorer sidebar (Files / Symbols tabs, collapsible; compact prop 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.
  • Spotlight semantics everywhere: picking a file, kind, or edge family lights the match and dims everything else in place (file and kind intersect; edge families restrict lit edges or, alone, light their endpoints). The explicit isolate action is the only thing that removes nodes from the view.
  • Component split: model (pure helpers) / scene (imperative three.js) / sidebar / shell, mirroring the benchmark component split.
  • Excalidraw example: generated with the current pipeline (1,495 nodes / 9,583 links) and added to the example tabs.
  • Reduce fix (all three mirrors): path rerooting assumed the legacy absolute-path dump contract; with current project-relative dumps the shared prefix came out empty and every path collapsed to its basename (flat file trees for fresh dumps and uploads). Rerooting now applies only to absolute paths.

Test plan

  • tsc --noEmit passes in website and packages/graph.
  • Pure model helpers exercised against the published zod and excalidraw payloads: identity projection, file-tree leaf sums, spotlight intersection (5 types in excalidraw-app), edge-only spotlight endpoints, 2-hop isolate, search ranking.
  • Interactions verified by hand on the running app (reviewer-confirmed): chips spotlight without changing the node count, dimmed nodes stay visible after the dim-color fix.

🤖 Generated with Claude Code

https://claude.ai/code/session_01N3Cn3zmu1uUVkYJfwyPFeJ

samchon and others added 6 commits July 2, 2026 19:05
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
Copilot AI review requested due to automatic review settings July 2, 2026 11:37
@samchon samchon merged commit df04347 into master Jul 2, 2026
30 checks passed
@samchon samchon deleted the feat/graph-viewer-explorer branch July 2, 2026 11:37

Copilot AI 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.

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(/^\/+/, "");
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