This document describes the current state of
index.html(~7600 lines, 290KB single file). It is structured so that each##heading is a cluster, each- \identifier`is a **node**, and backtick cross-references within descriptions are **edges** between nodes.###` subsections are sub-clusters: they are nodes in the parent cluster AND clusters of their children.
history.csv (runtime recording) or fall back to source HTML for AST analysis. Also fetch a codemap markdown file for cluster definitions and importance scores.parseHistoryCSV or analyzeCode (acorn AST walk) produce an analysisResult with functions, globals, edges, and node types.clusterFunctions assigns each node to a cluster via codemap sections, CSV cluster names, or union-find on shared state.nameClusters generates cluster labels from keywords; computeAffinities scores each node’s fractional membership across clusters.buildGraph creates node objects (with radius, color, affinities, minZoom) and link objects (with type, weight). In hypergraph mode, structural/value nodes are created on-the-fly from edge endpoints.computeLayout runs a multi-phase pipeline: intra-cluster force simulation, deep-node positioning along edges, cluster packing, optional warm restart.renderGraph does D3 data joins to create SVG elements across 6 layer groups (hulls, meta-links, links, nodes, labels, cluster-labels). Gradient edge defs, arrowheads, hulls via d3.polygonHull.applySemanticZoom adjusts visibility, opacity, font size, edge layer state, and meta-edge display based on zoom level currentK.StreamPlacement), end phase (settle with GraphPhysics).parseCSVLine: RFC 4180 CSV parser with quote handling (line 6698)parseHistoryCSV: converts CSV rows to {nodes, edges} with cluster detection (line 6718)buildAnalysisFromCSV: creates {functions, globals, nodeTypes, precomputedEdges} for downstream pipeline (line 6792)extractJS: locate inline <script> tag and extract code with line offset (line 863)analyzeCode: acorn AST parse, walk declarations, extract functions/globals/calls/reads/writes (line 874)parseCodemap: parse markdown into sections with node definitions and importance scores (line 821)clusterFunctions: assigns nodes to clusters via codemap > CSV fallback > union-find on shared state (line 1052)nameClusters: picks cluster names from keyword frequency in member function names (line 1119)computeAffinities: scores {cluster: weight} per node based on shared state, calls, and keyword overlap (line 1163)tokenizeName: splits camelCase/snake_case into tokens for keyword matching (line 1044)buildGraph: master function creating nodes[] and links[] from analysis+clusters+affinities (line 1756)
minZoom=10 (invisible until deep zoom)adjacency, fullAdjacency, nodeDegree, edgeWeights mapsEDGE_LAYERS: array of {id, color, dash, directed} – 8+ types: calls, calledBy, uses, writesTo, shared, sharedWrites, importance, memberOf (line ~1233)ensureEdgeLayer: auto-registers unknown edge types discovered during streaming (line 1395)EDGE_TYPE_MULT in GraphPhysics: force multipliers per edge type (calls=3.0, shared=1.5, etc.) (line 2530)pullLayerState: Map<layerId, opacity> – the single source of truth for edge layer visibility (line ~1418)edgeOpacity: computes per-edge opacity from deep-fade, layer opacity, node degree density, importance, and proximity boost at high zoom (line 1705)computeHiddenNodes: nodes hidden when ALL incident edges are in disabled layers (line 1443)applyNodeHiding: sets display:none on hidden nodes (line 1476)ZOOM_BREAKPOINTS: 4 presets at k=[0.3, 1.0, 4.0, 10.0] defining target layer opacities per zoom level (line 258)getZoomBreakpointOpacity: interpolates between bracketing breakpoints (line 283)applySemanticZoom auto-adjusts pullLayerState to match zoom breakpoint targets (line 3112)computeLayout: orchestrator (line 1984)
layoutClusterInternal: D3 force sim within each cluster (180 iterations) (line 2065)positionDeepNodes: places parameters/arguments along caller-callee edge vectors (line 2175)packClusters: force-based packing of cluster bounding circles (120 iterations) (line 2286)warmRestart: quick D3 sim for final settling (line 2372)tick computes: mass growth, grid-accelerated repulsion, edge attraction (type-weighted), cluster drift, center gravity, damping, hard collision resolutionHULL_EVERY_N_FRAMES (line 2547)start(), stop(), endStream(cb), isRunning(), placeNewNode(), rebuildCentroids()repositionIfOrphaned: re-places node when it gains its first edgesavedPositions: Map<id, {x,y}> – preserved across rebuilds (line 251)stickyNodes: Set<id> – nodes user has dragged (line 252)lockedNodes: Set<id> – explicitly pinned nodes (line 253)initialLayoutPositions: immutable T0 positions from last layout computation (line 314)gHulls: cluster boundary polygons + textPath boundary labelsgMetaLinks: aggregated inter-cluster bezier curves with gradientsgLinks: individual edge lines + arrowheadsgNodes: node circles + secondary affinity ringsgLabels: node text labelsgClusterLabels: floating cluster name text + user cluster labelsinitSVG: creates SVG, 6 layer groups, d3 zoom behavior (scale 0.1-12x), initial transform centered at 0.8x (line 3066)This function conflates optical scaling with navigation-level changes:
setupNodeCircle: creates circle + rings + all event handlers (click, contextmenu, mouseenter, force touch, pressure) (line 3395)renderIncremental: D3 data join for streaming updates (line 3505)renderGraph: full rebuild with position reuse, layout, element creation (line 3587)renderPositions: transforms all elements to current node x,y positions, updates gradient endpoints, arrowheads (line 4697)renderHulls: d3.polygonHull + expandHull (20px padding) + textPath labels for each cluster (line 3859)expandHull: pushes hull points outward from centroid (line 3097)renderUserClusters: dashed hulls/circles for user-defined clusters (line 3923)computeMetaLinks: aggregates inter-cluster edges, creates gradient IDs, computes bezier curves (line 4579)clusterCentroid: weighted center of visible cluster members (line 4615)renderMetaLinks: draws bezier curves with gradients (line 4632)updateMetaLinkPositions: repositions after layout changes (line 4682)computeClusterLabelPlacement: iterative positioning with spatial-grid collision avoidance (label-label and label-node) (line 4380)applyClusterLabelPlacements: sets {dx, dy, angle} on DOM elements (line 4541)renderClusterLabels: repositions floating text elements (line 4567)clusterLabelDrag: D3 drag behavior for cluster labels + invisible follower movement (line 4030)clusterImportance: Map<cid, scaleFactor> – modulates node/edge opacity per cluster (line ~4028)reduceClusterImportance: Ctrl+click scales down (line 4247)growClusterImportance: double-click scales up (line 4255)applyAllClusterImportanceVisuals: applies opacities to all nodes/edges (line 4163)selectNode: sets selectedNode, opens info panel with affinity bars and struct bars (line 4852)selectedNodes: Set<id> for multi-select via Shift+click (line 335)startAttractor: force-press or shift-hold begins pulling neighbors toward held node (line 4925)attractorLoop: RAF loop with ramping strength (0-1 over 200ms), BFS depth by hold duration (line 4936)clickPull: sustained importance-weighted pull of a single node (line 5037)startXForces: hold X to apply weak repulsion (single node or global relaxation) (line 5206)dismissNodeToT0: Shift+X+click animates node back to T0 position (line 5694)startZTimeTravel: hold Z to step backward through arrangement history (line 5281)zTravelStepDuration)startTrace: BFS traversal from selected nodes through visible graph (line 5442)traceLoop: RAF stepping BFS frontier every traceEdgeDuration ms (line 5541)startNeighborGather: Space+click pulls 1-depth neighbors toward anchor (line 5723)startClusterGather: Space+drag cluster label gathers all cluster members (line 5945)startIntraClusterGather: Shift+Space+label for within-cluster-only gather (line 6102)startSpacePull: Space+Shift+click pulls selected nodes toward target (line 6165)The d3 zoom transform is a pure optical scaling (line 3081-3094). But applySemanticZoom (line 3108) bolts navigation semantics onto it, making zoom level change WHAT you see, not just HOW you see it.
| Behavior | Optical (transform) | Navigation (content change) |
|---|---|---|
| Pan/scroll | YES | no |
| Scale factor | YES | no |
| Zoom breakpoints change layer visibility | no | YES |
| Label opacity thresholds | no | YES (info hiding) |
| Deep node fade-in (k>10) | no | YES (new content) |
| Meta-edge show/hide (k<1.5) | no | YES |
| Floating label fade-out (k>2.5) | no | YES |
| TextPath label fade-in (k=3-4) | no | YES |
| Hull opacity scaling | partial | partial |
| Edge proximity boost (k>=3) | partial | partial |
The problem: zoom is the ONLY way to navigate the hypergraph depth. There is no concept of “enter this cluster” or “go up a level” independent of the optical zoom. The zoom breakpoints (Overview at 0.3, Systems at 1.0, Structural at 4.0, Deep at 10.0) are de facto navigation levels masquerading as optical comfort settings.
What hypergraph navigation should be: A discrete operation – “expand this cluster into its members” or “collapse these nodes into their cluster” – that changes the graph topology, not the camera. Zoom could then be purely optical. The breakpoints would become navigation-level transitions triggered by zoom OR by explicit user action (click to enter, back to exit).
Where zoom is tightly coupled to functionality (all in applySemanticZoom, line 3108):
pullLayerState (edge visibility)(k - minZoom) / 2 – hard-coupled to zoom level15+ active keys with complex modifier combinations:
connectStreamSSE: progressive graph building from history.csv stream (line 6874)
StreamPlacementGraphPhysics settleconnectGraphSSE: bulk graph update events triggering loadAndAnalyze (line 6824)connectFocusSSE: step-by-step debug focus events (line 7384)CinematicStream: queues rows and animates zoom-tour through new visible nodes (line 7230)spatialMemory: {arrangements: [], spatialEdges: {}, clickLog: []} persisted to localStorage (line 580)pushArrangement: saves current node positions, cluster importance, and focal nodes (line 628)navigateArrangement: step through history with position interpolation (line 669)enterComparisonMode: side-by-side when oscillation detected (3+ back-and-forth navigations) (line 702)trackClickCoOccurrence / trackDragProximity: record affinity signals (lines 744, 763)HULL_EVERY_N_FRAMES defined inside GraphPhysics IIFE (line 2547) but renderPositions() (line 4697) calls renderHulls() unconditionally on every frame outside physics. The throttling only applies during physics ticks.traceRAF = -1 sentinel (line ~5532): fragile pattern. If any code path forgets the !== -1 check when calling cancelAnimationFrame, the animation leaks silently.setTimeout callbacks (line ~5504) for flash wave visualization are never cancelled on Escape/stopTrace. Pending timeouts fire and apply trace visuals to a cleared graph.labelPlacementTimer (line 3189) set inside applySemanticZoom can fire AFTER rebuild() replaces the graph, computing placement for nodes that no longer exist.attractorLoop (line 4936) has no maximum duration. After 1500ms maxHop locks at 3, but the loop runs indefinitely until user release.startClusterGather returns early due to existing RAF, the anchor CID from the previous gather persists.renderIncremental (line ~3568) vs layer slider toggle (line ~1577). Can diverge.These are places where the same concept is implemented in two or more divergent ways:
1. Cluster Centroid – two implementations
GraphPhysics.rebuildCentroids() (line 2552): iterates currentNodes, skips invisible, computes {sx, sy, count} per clusterNameclusterCentroid(cid) (line 4615): filters currentNodes by cluster ID, computes weighted average2. Edge Opacity – computed in two places
edgeOpacity() function (line 1705): comprehensive computation with deep-fade, layer state, density, importance, proximity boostapplySemanticZoom() inline (line 3217): calls edgeOpacity but also does its own display toggling and trace-edge filtering3. Node Visibility – 4+ mechanisms
_hiddenNodes set from computeHiddenNodes() (line 1443): based on edge layer stateapplySemanticZoom deep-node opacity (line 3160): based on zoom level and minZoomapplySemanticZoom pointer-events disable (line 3164): when deepOp < 0.14. renderHulls() + renderClusterLabels() – bundled in 6+ call sites
5. Highlight/reset – 3 implementations
clearHighlight(): resets opacity, calls applySemanticZoom + applyNodeHidingclearHighlight()clearTraceVisuals() which also resets via applySemanticZoom6. Position saving – 3 separate triggers
renderGraph() line ~3593: saves positions before layoutnodeDrag.on('end'): saves dragged node positionanimateToPositions completion: saves all final positionsstopXForces / stopZTimeTravel: saves moved nodes7. Soft collision – similar math in 4 places
softCollide() (line 432): general-purpose collision check/pushclickPull() (line ~5071): inline collision with slightly different parametersattractorLoop() (line ~4960): inline push-away for non-neighbors8. Cluster member filtering – repeated pattern
currentNodes.filter(n => n.cluster === cid) appears in 10+ placesclusterMemberCache (line 513) exists for this purpose but is only used in label placementsavedPositions vs stickyNodes vs lockedNodes
savedPositions (Map): stores x,y coordinates, survives rebuildsstickyNodes (Set): boolean flag for “user moved this”, survives rebuildslockedNodes (Set): boolean flag for “user pinned this”stickyNodes.clear() and savedPositions.clear() but lockedNodes is NOT clearedlockedNodes but not stickyNodes, or have saved positions but not be stickyselectedNode vs selectedNodes
selectedNode (single): for info panel, set by selectNode()selectedNodes (Set): for multi-select, set by Shift+clickselectedNode that’s not in selectedNodes, or multi-selected nodes with no selectedNode.13+ independent requestAnimationFrame chains, each calling renderPositions():
GraphPhysics.tick / springLoop / attractorLoop / clickPullStepxLoop / zLoop / traceLoopneighborGatherLoop / clusterGatherLoop / intraClusterGatherLoop / spacePullLoopanimateToPositions / per-node xDismissRAFsProblems:
renderPositions() in the same frame = redundant DOM writesisUserInteracting() (line ~401) is a flat OR guard but doesn’t prevent all conflictscluster property (integer ID). Clusters are IMPLICIT – they exist only as a property value.clusterImportance Map is the closest thing to “cluster as entity.”userClusterSections) are a parallel concept with different storage and rendering.At zoom level N, a cluster should appear as a single node. Zoom into it, and it expands to show its member nodes, which may themselves be clusters at a deeper level. This is the fundamental operation that is missing.
The current code has the pieces:
minZoom on deep nodes (structural nodes invisible until k=10)But these pieces are not unified into a single recursive data structure. Currently:
A cluster node should be a node that contains other nodes. When collapsed, it acts as a single node in the graph (with meta-edges as its connections). When expanded, its children become visible and its hull becomes the boundary. The expand/collapse operation should be independent of zoom – zoom changes the camera, expand/collapse changes the topology.
The hypergraph data model (nodes, edges, containment, projections) should be cleanly separated from:
Currently all of these are tangled in applySemanticZoom (which does rendering + navigation + edge visibility), renderGraph (which does data construction + layout + SVG creation), and the various gather/attractor loops (which do physics + interaction + rendering).
| File | Responsibility | Key Functions |
|---|---|---|
core/types.js |
Node, Edge, Cluster, HyperEdge type definitions | (type comments/JSDoc) |
core/state.js |
All 40+ global state variables, centralized | exported state object |
core/animation.js |
Central RAF scheduler, replaces 13 independent loops | registerLoop, tick, single renderPositions per frame |
data/parse.js |
CSV + AST + codemap parsing | parseCSVLine, parseHistoryCSV, analyzeCode, parseCodemap |
data/cluster.js |
Clustering, naming, affinity scoring | clusterFunctions, nameClusters, computeAffinities |
data/graph-builder.js |
Node/link creation from analysis | buildGraph |
edges/layers.js |
Edge layer config, pull-layer state, UI sliders | EDGE_LAYERS, pullLayerState, initPullLayerUI, edgeOpacity |
edges/visibility.js |
Unified visibility: hidden nodes + deep fade + search + trace | isNodeVisible(node) – single query |
layout/physics.js |
GraphPhysics module extracted as class | force tick, spatial grid, settle detection |
layout/placement.js |
StreamPlacement, initial layout, cluster packing | computeLayout, placeNewNode |
layout/positions.js |
Position persistence: savedPositions + sticky + locked (UNIFIED) | savePosition, isSticky, isLocked |
render/svg.js |
SVG init, layer groups, zoom behavior | initSVG |
render/nodes.js |
Node circle creation, data joins | setupNodeCircle, renderIncremental |
render/edges.js |
Edge lines, gradients, arrowheads | edge rendering from renderGraph |
render/hulls.js |
Hull computation, expansion, textPath labels | renderHulls, expandHull |
render/meta-edges.js |
Meta-link computation, bezier curves, centroids | computeMetaLinks, clusterCentroid (UNIFIED) |
render/labels.js |
Node labels, cluster labels, label placement | computeClusterLabelPlacement, renderClusterLabels |
render/positions.js |
renderPositions – transform all elements |
single render call per frame |
navigation/semantic-zoom.js |
Zoom-to-navigation-level mapping, breakpoints | applySemanticZoom (STRIPPED of rendering) |
navigation/expand-collapse.js |
Future: cluster expand/collapse topology changes | expandCluster, collapseCluster |
interact/select.js |
Selection, info panel, multi-select | selectNode, selectedNodes |
interact/attractor.js |
Force press, attractor loop | startAttractor, attractorLoop |
interact/forces.js |
X-relax, click-pull, dismiss-to-T0 | startXForces, clickPull, dismissNodeToT0 |
interact/trace.js |
T-trace BFS, flash, hold, mode switching | startTrace, traceLoop |
interact/gather.js |
All gather variants: neighbor, cluster, intra, space-pull | startNeighborGather, startClusterGather, etc. |
interact/keyboard.js |
Keydown/keyup dispatch | delegates to interaction modules |
spatial/memory.js |
Arrangement history, decay, comparison mode | pushArrangement, navigateArrangement |
spatial/time-travel.js |
Z-key arrangement stepping | startZTimeTravel |
stream/sse.js |
SSE connections, stream phases | connectStreamSSE, connectGraphSSE |
stream/cinematic.js |
CinematicStream zoom-tour | CinematicStream |
main.js |
Init pipeline, rebuild, polling |
loadAndAnalyze, initDataPipeline |
Central animation scheduler (core/animation.js): All interaction loops register their per-frame update function. Scheduler calls all active updaters, then renderPositions() exactly ONCE. Eliminates redundant DOM writes and conflicting forces.
Unified visibility (edges/visibility.js): Single isNodeVisible(node) function that combines hidden-nodes, deep-fade, search, and trace state. All rendering checks this one function.
Unified position persistence (layout/positions.js): Merge savedPositions, stickyNodes, and lockedNodes into one PositionState per node: {x, y, sticky: bool, locked: bool}. Single source of truth.
Unified cluster centroid (render/meta-edges.js): One clusterCentroid(cid) function used by both physics and meta-edge rendering. No more divergent implementations.
Navigation separated from rendering (navigation/semantic-zoom.js): Computes the navigation level from zoom, but doesn’t touch the DOM. Returns a NavigationState that the rendering modules consume.