From ba7d1c66758e0330f3aa38caf023006b45620e42 Mon Sep 17 00:00:00 2001 From: Zachery Aaron Shores-Chmielewski Date: Fri, 6 Feb 2026 21:51:49 +0700 Subject: [PATCH] feat: inter-module code dependency DAG visualizer --- README.md | 8 +++ deps.html | 102 +++++++++++++++++++++++++++++++++++-- tools/depgraph/src/main.rs | 102 +++++++++++++++++++++++++++++++++++-- 3 files changed, 204 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index bcce6fe..78c4ddc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,14 @@ # swactor (S)mall (W)ASM-compatible (actor) library +## Useful + +View code dependency DAG + +```bash +cargo run --manifest-path tools/depgraph/Cargo.toml -- --src-dir src/ --output deps +``` + ## Quick example ```rust diff --git a/deps.html b/deps.html index f461137..9268f46 100644 --- a/deps.html +++ b/deps.html @@ -23,7 +23,7 @@ - scroll to zoom · drag to pan + scroll to zoom · drag to pan · click node to focus
Loading Graphviz…
@@ -262,8 +262,100 @@ svg.querySelectorAll('.graph > text, .cluster > text, .edge text').forEach(el => // Node text (inside record shapes): keep dark for readability on pastel fills svg.querySelectorAll('.node text').forEach(el => el.setAttribute('fill','#1a1a1a')); +// ─── Click-to-focus ──────────────────────────────────────────────────────── +// Build adjacency: for each edge, record which node titles it connects. +const edges = svg.querySelectorAll('.edge'); +const nodes = svg.querySelectorAll('.node'); +// Cluster chrome = the path + text that draw the cluster box/label (not child nodes) +const clusterChrome = []; +svg.querySelectorAll('.cluster').forEach(c => { + c.querySelectorAll(':scope > path, :scope > polygon, :scope > text').forEach(el => clusterChrome.push(el)); +}); + +// Map: node title → DOM element +const nodeByTitle = new Map(); +nodes.forEach(n => { + const t = n.querySelector('title'); + if (t) nodeByTitle.set(t.textContent.trim(), n); +}); + +// Which cluster contains which node titles +const nodeToClusterEls = new Map(); +svg.querySelectorAll('.cluster').forEach(cluster => { + const chrome = [...cluster.querySelectorAll(':scope > path, :scope > polygon, :scope > text')]; + cluster.querySelectorAll('.node title').forEach(t => { + nodeToClusterEls.set(t.textContent.trim(), chrome); + }); +}); + +// Map: node title → set of connected edge elements + set of neighbor titles +const adj = new Map(); +edges.forEach(edge => { + const t = edge.querySelector('title'); + if (!t) return; + const parts = t.textContent.trim().split('->').map(s => s.trim()); + if (parts.length !== 2) return; + const [src, dst] = parts; + if (!adj.has(src)) adj.set(src, { edges: [], neighbors: new Set() }); + if (!adj.has(dst)) adj.set(dst, { edges: [], neighbors: new Set() }); + adj.get(src).edges.push(edge); + adj.get(src).neighbors.add(dst); + adj.get(dst).edges.push(edge); + adj.get(dst).neighbors.add(src); +}); + +const DIM = 0.08; +let focused = null; + +function clearFocus() { + focused = null; + nodes.forEach(n => n.style.opacity = ''); + edges.forEach(e => e.style.opacity = ''); + clusterChrome.forEach(el => el.style.opacity = ''); +} + +function focusNode(title) { + if (focused === title) { clearFocus(); return; } + focused = title; + const info = adj.get(title) || { edges: [], neighbors: new Set() }; + const connected = new Set([title, ...info.neighbors]); + + // Dim all nodes, edges, and cluster chrome individually (not the cluster ) + nodes.forEach(n => n.style.opacity = DIM); + edges.forEach(e => e.style.opacity = DIM); + clusterChrome.forEach(el => el.style.opacity = DIM); + + // Highlight connected nodes + connected.forEach(name => { + const el = nodeByTitle.get(name); + if (el) el.style.opacity = 1; + }); + + // Highlight connected edges + info.edges.forEach(e => e.style.opacity = 1); + + // Highlight cluster chrome for clusters that contain a connected node + const seen = new Set(); + connected.forEach(name => { + const chrome = nodeToClusterEls.get(name); + if (chrome) chrome.forEach(el => { + if (!seen.has(el)) { seen.add(el); el.style.opacity = 1; } + }); + }); +} + +// Attach click handlers to nodes +nodes.forEach(node => { + node.style.cursor = 'pointer'; + node.addEventListener('click', e => { + e.stopPropagation(); + const t = node.querySelector('title'); + if (t) focusNode(t.textContent.trim()); + }); +}); + // pan & zoom -let scale = 1, tx = 0, ty = 0, dragging = false, sx = 0, sy = 0; +let scale = 1, tx = 0, ty = 0, dragging = false, didDrag = false, sx = 0, sy = 0; function applyTransform() { svg.style.transform = `translate(${tx}px,${ty}px) scale(${scale})`; svg.style.transformOrigin = '0 0'; } function resetView() { const vw = window.innerWidth, vh = window.innerHeight; @@ -276,9 +368,11 @@ function resetView() { resetView(); vp.addEventListener('wheel', e => { e.preventDefault(); const f = e.deltaY < 0 ? 1.12 : 0.89; const rect = vp.getBoundingClientRect(); const mx = e.clientX - rect.left; const my = e.clientY - rect.top; tx = mx - f * (mx - tx); ty = my - f * (my - ty); scale *= f; applyTransform(); }, { passive:false }); -vp.addEventListener('pointerdown', e => { dragging=true; sx=e.clientX-tx; sy=e.clientY-ty; vp.setPointerCapture(e.pointerId); }); -vp.addEventListener('pointermove', e => { if(!dragging) return; tx=e.clientX-sx; ty=e.clientY-sy; applyTransform(); }); +vp.addEventListener('pointerdown', e => { dragging=true; didDrag=false; sx=e.clientX-tx; sy=e.clientY-ty; vp.setPointerCapture(e.pointerId); }); +vp.addEventListener('pointermove', e => { if(!dragging) return; didDrag=true; tx=e.clientX-sx; ty=e.clientY-sy; applyTransform(); }); vp.addEventListener('pointerup', () => dragging=false); +// Click background to clear focus (only if it wasn't a drag) +vp.addEventListener('click', e => { if (!didDrag && !e.target.closest('.node')) clearFocus(); }); function zoomIn() { scale*=1.3; applyTransform(); } function zoomOut() { scale*=0.7; applyTransform(); } diff --git a/tools/depgraph/src/main.rs b/tools/depgraph/src/main.rs index 0dbe337..2662195 100644 --- a/tools/depgraph/src/main.rs +++ b/tools/depgraph/src/main.rs @@ -1096,7 +1096,7 @@ fn generate_html(dot_source: &str) -> String { - scroll to zoom · drag to pan + scroll to zoom · drag to pan · click node to focus
Loading Graphviz…
@@ -1120,8 +1120,100 @@ svg.querySelectorAll('.graph > text, .cluster > text, .edge text').forEach(el => // Node text (inside record shapes): keep dark for readability on pastel fills svg.querySelectorAll('.node text').forEach(el => el.setAttribute('fill','#1a1a1a')); +// ─── Click-to-focus ──────────────────────────────────────────────────────── +// Build adjacency: for each edge, record which node titles it connects. +const edges = svg.querySelectorAll('.edge'); +const nodes = svg.querySelectorAll('.node'); +// Cluster chrome = the path + text that draw the cluster box/label (not child nodes) +const clusterChrome = []; +svg.querySelectorAll('.cluster').forEach(c => {{ + c.querySelectorAll(':scope > path, :scope > polygon, :scope > text').forEach(el => clusterChrome.push(el)); +}}); + +// Map: node title → DOM element +const nodeByTitle = new Map(); +nodes.forEach(n => {{ + const t = n.querySelector('title'); + if (t) nodeByTitle.set(t.textContent.trim(), n); +}}); + +// Which cluster contains which node titles +const nodeToClusterEls = new Map(); +svg.querySelectorAll('.cluster').forEach(cluster => {{ + const chrome = [...cluster.querySelectorAll(':scope > path, :scope > polygon, :scope > text')]; + cluster.querySelectorAll('.node title').forEach(t => {{ + nodeToClusterEls.set(t.textContent.trim(), chrome); + }}); +}}); + +// Map: node title → set of connected edge elements + set of neighbor titles +const adj = new Map(); +edges.forEach(edge => {{ + const t = edge.querySelector('title'); + if (!t) return; + const parts = t.textContent.trim().split('->').map(s => s.trim()); + if (parts.length !== 2) return; + const [src, dst] = parts; + if (!adj.has(src)) adj.set(src, {{ edges: [], neighbors: new Set() }}); + if (!adj.has(dst)) adj.set(dst, {{ edges: [], neighbors: new Set() }}); + adj.get(src).edges.push(edge); + adj.get(src).neighbors.add(dst); + adj.get(dst).edges.push(edge); + adj.get(dst).neighbors.add(src); +}}); + +const DIM = 0.08; +let focused = null; + +function clearFocus() {{ + focused = null; + nodes.forEach(n => n.style.opacity = ''); + edges.forEach(e => e.style.opacity = ''); + clusterChrome.forEach(el => el.style.opacity = ''); +}} + +function focusNode(title) {{ + if (focused === title) {{ clearFocus(); return; }} + focused = title; + const info = adj.get(title) || {{ edges: [], neighbors: new Set() }}; + const connected = new Set([title, ...info.neighbors]); + + // Dim all nodes, edges, and cluster chrome individually (not the cluster ) + nodes.forEach(n => n.style.opacity = DIM); + edges.forEach(e => e.style.opacity = DIM); + clusterChrome.forEach(el => el.style.opacity = DIM); + + // Highlight connected nodes + connected.forEach(name => {{ + const el = nodeByTitle.get(name); + if (el) el.style.opacity = 1; + }}); + + // Highlight connected edges + info.edges.forEach(e => e.style.opacity = 1); + + // Highlight cluster chrome for clusters that contain a connected node + const seen = new Set(); + connected.forEach(name => {{ + const chrome = nodeToClusterEls.get(name); + if (chrome) chrome.forEach(el => {{ + if (!seen.has(el)) {{ seen.add(el); el.style.opacity = 1; }} + }}); + }}); +}} + +// Attach click handlers to nodes +nodes.forEach(node => {{ + node.style.cursor = 'pointer'; + node.addEventListener('click', e => {{ + e.stopPropagation(); + const t = node.querySelector('title'); + if (t) focusNode(t.textContent.trim()); + }}); +}}); + // pan & zoom -let scale = 1, tx = 0, ty = 0, dragging = false, sx = 0, sy = 0; +let scale = 1, tx = 0, ty = 0, dragging = false, didDrag = false, sx = 0, sy = 0; function applyTransform() {{ svg.style.transform = `translate(${{tx}}px,${{ty}}px) scale(${{scale}})`; svg.style.transformOrigin = '0 0'; }} function resetView() {{ const vw = window.innerWidth, vh = window.innerHeight; @@ -1134,9 +1226,11 @@ function resetView() {{ resetView(); vp.addEventListener('wheel', e => {{ e.preventDefault(); const f = e.deltaY < 0 ? 1.12 : 0.89; const rect = vp.getBoundingClientRect(); const mx = e.clientX - rect.left; const my = e.clientY - rect.top; tx = mx - f * (mx - tx); ty = my - f * (my - ty); scale *= f; applyTransform(); }}, {{ passive:false }}); -vp.addEventListener('pointerdown', e => {{ dragging=true; sx=e.clientX-tx; sy=e.clientY-ty; vp.setPointerCapture(e.pointerId); }}); -vp.addEventListener('pointermove', e => {{ if(!dragging) return; tx=e.clientX-sx; ty=e.clientY-sy; applyTransform(); }}); +vp.addEventListener('pointerdown', e => {{ dragging=true; didDrag=false; sx=e.clientX-tx; sy=e.clientY-ty; vp.setPointerCapture(e.pointerId); }}); +vp.addEventListener('pointermove', e => {{ if(!dragging) return; didDrag=true; tx=e.clientX-sx; ty=e.clientY-sy; applyTransform(); }}); vp.addEventListener('pointerup', () => dragging=false); +// Click background to clear focus (only if it wasn't a drag) +vp.addEventListener('click', e => {{ if (!didDrag && !e.target.closest('.node')) clearFocus(); }}); function zoomIn() {{ scale*=1.3; applyTransform(); }} function zoomOut() {{ scale*=0.7; applyTransform(); }}