-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
Hover a node to spotlight its neighborhood. Click to pin the focus, then export the current view as PNG.
+
+
+
Entrypoint
+
Service
+
Repository / DB
+
Config
+
Middleware
+
External integration
+
Utility
+
Node size = importance ยท Edge = import relationship
@@ -426,7 +494,7 @@ def _build_fragment_markup(self, container_id: str, payload_json: str, script_no
const shell = document.getElementById("{container_id}-shell");
const container = document.getElementById("{container_id}");
if (typeof vis === "undefined") {{
- container.innerHTML = '
Graph library could not be loaded in this browser.
';
+ container.innerHTML = '
Graph library could not be loaded in this browser.
';
return;
}}
@@ -436,80 +504,68 @@ def _build_fragment_markup(self, container_id: str, payload_json: str, script_no
spotlight: null,
hoveredNode: null,
draggingNode: null,
- scale: 1,
filters: {{ hideUtilities: false, hideIsolated: false, coreOnly: false, sideEffectsOnly: false, minImportance: 0 }},
}};
- let physicsTimer = null;
- let physicsFrame = null;
- let driftFrame = null;
- let previousPositions = null;
- let stableFrames = 0;
- let physicsActive = false;
- let physicsStartedAt = 0;
- let driftStartedAt = 0;
- const VELOCITY_THRESHOLD = 0.02;
- const STABLE_FRAMES_REQUIRED = 60;
- const MAX_PHYSICS_DURATION_MS = 5000;
- const DRAG_RESTABILIZE_DELAY_MS = 120;
- const CANVAS_PADDING = 100;
- const DRIFT_AMPLITUDE = 1.2;
- const DRIFT_PERIOD_MS = 8000;
+ let searchTimer = null;
+ let tooltipFrame = null;
+ let filterFrame = null;
+ let ambientFrame = null;
+ let ambientTimestamp = performance.now();
+ let lastAmbientPaint = 0;
+ let collisionFrame = null;
+ let collisionTargets = null;
+ let collisionView = null;
+ let dragOrigin = null;
+ let graphInViewport = true;
+ const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
+ const COLLISION_PADDING = 18;
+ const MAX_COLLISION_NODES = 16;
+ const MAX_COLLISION_DISPLACEMENT = 260;
+ const COLLISION_DURATION_MS = 180;
const descriptionEl = shell.querySelector("[data-description]");
const statsEl = shell.querySelector("[data-stats]");
const tooltipEl = shell.querySelector("[data-tooltip]");
const nodeSet = new vis.DataSet([]);
const edgeSet = new vis.DataSet([]);
+ const layoutCache = new Map();
const visibleState = {{
nodes: [],
edges: [],
nodeMap: new Map(),
- basePositions: new Map(),
}};
const network = new vis.Network(container, {{ nodes: nodeSet, edges: edgeSet }}, {{
- layout: {{ improvedLayout: true, randomSeed: 17 }},
+ layout: {{ improvedLayout: false, randomSeed: 17 }},
interaction: {{
hover: true,
hoverConnectedEdges: false,
- navigationButtons: true,
+ navigationButtons: false,
keyboard: true,
- tooltipDelay: 120,
+ tooltipDelay: 80,
zoomView: true,
dragView: true,
- hideEdgesOnDrag: false
- }},
- physics: {{
- enabled: true,
- solver: "barnesHut",
- stabilization: {{ enabled: true, iterations: 420, fit: true }},
- barnesHut: {{
- gravitationalConstant: -1800,
- centralGravity: 0.015,
- springLength: 260,
- springConstant: 0.018,
- damping: 0.92,
- avoidOverlap: 1.55
- }},
- minVelocity: 0.02,
- maxVelocity: 18,
- timestep: 0.45,
- adaptiveTimestep: true
+ dragNodes: true,
+ hideEdgesOnDrag: false,
+ selectConnectedEdges: false
}},
+ physics: {{ enabled: false }},
nodes: {{
- borderWidth: 1.8,
- borderWidthSelected: 3.2,
+ borderWidth: 1.5,
+ borderWidthSelected: 2,
labelHighlightBold: false,
- font: {{ face: "Segoe UI", size: 13, color: "#dbe7f5", strokeWidth: 4, strokeColor: "rgba(17,19,23,0.92)" }}
+ chosen: false,
+ font: {{ face: "Segoe UI", size: 13, color: "#d8dce3", strokeWidth: 2, strokeColor: "#101318" }}
}},
edges: {{
- arrows: {{ to: {{ enabled: true, scaleFactor: 0.45 }} }},
- smooth: {{ type: "dynamic", roundness: 0.24 }},
+ arrows: {{ to: {{ enabled: true, scaleFactor: 0.38 }} }},
+ smooth: {{ enabled: true, type: "curvedCW", roundness: 0.08 }},
selectionWidth: 0,
- hoverWidth: 0
+ hoverWidth: 0,
+ chosen: false
}}
}});
function rgba(hex, alpha) {{
- const clean = (hex || "#94a3b8").replace("#", "");
+ const clean = (hex || "#68717e").replace("#", "");
const normalized = clean.length === 3 ? clean.split("").map((p) => p + p).join("") : clean;
const n = parseInt(normalized, 16);
return `rgba(${{(n >> 16) & 255}}, ${{(n >> 8) & 255}}, ${{n & 255}}, ${{alpha}})`;
@@ -520,7 +576,7 @@ def _build_fragment_markup(self, container_id: str, payload_json: str, script_no
}}
function activeFocusId() {{
- return state.hoveredNode || state.spotlight;
+ return state.spotlight;
}}
function currentPositions(nodeIds) {{
@@ -552,29 +608,64 @@ def _build_fragment_markup(self, container_id: str, payload_json: str, script_no
}}
clusters.get(key).push(node);
}});
- const orderedClusters = Array.from(clusters.entries()).sort((left, right) => left[0].localeCompare(right[0]));
+ const orderedClusters = Array.from(clusters.entries())
+ .map(([name, members]) => [name, members.slice().sort((left, right) => right.importance - left.importance || left.id.localeCompare(right.id))])
+ .sort((left, right) => right[1].length - left[1].length || left[0].localeCompare(right[0]));
const positions = new Map();
- const clusterCount = Math.max(orderedClusters.length, 1);
- const clusterRadius = clusterCount === 1 ? 0 : 320 + Math.min(200, clusterCount * 14);
- orderedClusters.forEach(([clusterName, members], clusterIndex) => {{
- const baseHash = clusterName.split("").reduce((acc, ch) => acc + ch.charCodeAt(0), 0);
- const baseAngle = clusterCount === 1 ? 0 : ((clusterIndex / clusterCount) * Math.PI * 2) + ((baseHash % 17) * 0.02);
- const centerX = Math.cos(baseAngle) * clusterRadius;
- const centerY = Math.sin(baseAngle) * clusterRadius * 0.74;
- const orderedMembers = members.slice().sort((left, right) => right.importance - left.importance || left.id.localeCompare(right.id));
- orderedMembers.forEach((node, memberIndex) => {{
- const ringIndex = Math.floor(memberIndex / 6);
- const localCount = Math.min(6, orderedMembers.length - ringIndex * 6);
- const localIndex = memberIndex % 6;
- const localAngle = localCount <= 1 ? 0 : (localIndex / localCount) * Math.PI * 2;
- const localRadius = 42 + ringIndex * 46 + Math.min(14, Math.log1p(Math.max(node.importance, 1)) * 4);
- const centerBias = node.isEntrypoint || node.isCore ? 0.72 : 1;
- positions.set(node.id, {{
- x: centerX + Math.cos(localAngle + baseAngle * 0.2) * localRadius * centerBias,
- y: centerY + Math.sin(localAngle + baseAngle * 0.2) * localRadius * centerBias,
- }});
+
+ function clusterExtent(memberCount) {{
+ if (memberCount <= 1) return 0;
+ let remaining = memberCount - 1;
+ let ring = 0;
+ while (remaining > 0) {{
+ ring += 1;
+ remaining -= 8 + ((ring - 1) * 6);
+ }}
+ return ring * 140;
+ }}
+
+ function placeCluster(clusterName, members, centerX, centerY) {{
+ if (!members.length) return;
+ positions.set(members[0].id, {{ x: centerX, y: centerY }});
+ let memberIndex = 1;
+ let ring = 1;
+ const phase = (clusterName.split("").reduce((acc, ch) => acc + ch.charCodeAt(0), 0) % 24) * (Math.PI / 96);
+ while (memberIndex < members.length) {{
+ const capacity = 8 + ((ring - 1) * 6);
+ const ringCount = Math.min(capacity, members.length - memberIndex);
+ const radius = ring * 140;
+ for (let ringIndex = 0; ringIndex < ringCount; ringIndex += 1) {{
+ const node = members[memberIndex + ringIndex];
+ const angle = phase + (ringIndex / ringCount) * Math.PI * 2;
+ positions.set(node.id, {{
+ x: centerX + Math.cos(angle) * radius,
+ y: centerY + Math.sin(angle) * radius,
+ }});
+ }}
+ memberIndex += ringCount;
+ ring += 1;
+ }}
+ }}
+
+ if (!orderedClusters.length) return positions;
+ const totalNodes = nodes.length;
+ const centralCluster = orderedClusters[0];
+ const useCentralCluster = orderedClusters.length === 1
+ || (centralCluster[1].length >= 3 && centralCluster[1].length >= Math.ceil(totalNodes * 0.36));
+ const outerClusters = useCentralCluster ? orderedClusters.slice(1) : orderedClusters;
+ const centralExtent = useCentralCluster ? clusterExtent(centralCluster[1].length) : 0;
+ if (useCentralCluster) placeCluster(centralCluster[0], centralCluster[1], 0, 0);
+ if (outerClusters.length) {{
+ const outerExtent = Math.max(...outerClusters.map(([, members]) => clusterExtent(members.length)));
+ const minimumOrbit = totalNodes <= 5 ? 180 : 260;
+ const orbitRadius = centralExtent + outerExtent + Math.max(minimumOrbit, outerClusters.length * 60);
+ outerClusters.forEach(([clusterName, members], clusterIndex) => {{
+ const angle = (clusterIndex / outerClusters.length) * Math.PI * 2;
+ const centerX = Math.cos(angle) * orbitRadius;
+ const centerY = Math.sin(angle) * orbitRadius * 0.55;
+ placeCluster(clusterName, members, centerX, centerY);
}});
- }});
+ }}
return positions;
}}
@@ -623,10 +714,20 @@ def _build_fragment_markup(self, container_id: str, payload_json: str, script_no
tooltipEl.hidden = false;
tooltipEl.setAttribute("aria-hidden", "false");
placeTooltip(pointer);
- window.requestAnimationFrame(() => tooltipEl.classList.add("is-visible"));
+ if (tooltipFrame) window.cancelAnimationFrame(tooltipFrame);
+ tooltipFrame = window.requestAnimationFrame(() => {{
+ tooltipFrame = null;
+ if (state.hoveredNode === nodeId && !tooltipEl.hidden) {{
+ tooltipEl.classList.add("is-visible");
+ }}
+ }});
}}
function hideTooltip() {{
+ if (tooltipFrame) {{
+ window.cancelAnimationFrame(tooltipFrame);
+ tooltipFrame = null;
+ }}
tooltipEl.classList.remove("is-visible");
tooltipEl.setAttribute("aria-hidden", "true");
window.setTimeout(() => {{
@@ -643,15 +744,391 @@ def _build_fragment_markup(self, container_id: str, payload_json: str, script_no
visibleState.nodeMap = new Map(nodes.map((node) => [node.id, node]));
}}
- function resolveBasePositions(nodes, livePositions, seededPositions) {{
- const nextBasePositions = new Map(visibleState.basePositions);
- nodes.forEach((node) => {{
- if (nextBasePositions.has(node.id)) {{
+ function positionsForView(viewName, view) {{
+ if (!layoutCache.has(viewName)) {{
+ layoutCache.set(viewName, seedPositions(view.nodes));
+ }}
+ return layoutCache.get(viewName);
+ }}
+
+ function syncPositionSignature() {{
+ const positions = currentPositions(nodeSet.getIds());
+ let hash = 2166136261;
+ Object.keys(positions).sort().forEach((id) => {{
+ const position = positions[id];
+ const token = `${{id}}:${{Math.round(position.x * 10)}}:${{Math.round(position.y * 10)}}`;
+ for (let index = 0; index < token.length; index += 1) {{
+ hash ^= token.charCodeAt(index);
+ hash = Math.imul(hash, 16777619);
+ }}
+ }});
+ shell.dataset.positionSignature = `${{Object.keys(positions).length}}:${{hash >>> 0}}`;
+ }}
+
+ function hashText(value) {{
+ let hash = 2166136261;
+ const text = String(value);
+ for (let index = 0; index < text.length; index += 1) {{
+ hash ^= text.charCodeAt(index);
+ hash = Math.imul(hash, 16777619);
+ }}
+ return hash >>> 0;
+ }}
+
+ function curveDetails(edge) {{
+ const hash = hashText(edge.id);
+ return {{
+ direction: hash % 2 === 0 ? 1 : -1,
+ roundness: 0.065 + ((hash % 4) * 0.012),
+ }};
+ }}
+
+ function nodePosition(nodeId) {{
+ const bodyNode = network.body && network.body.nodes ? network.body.nodes[nodeId] : null;
+ if (!bodyNode || !Number.isFinite(bodyNode.x) || !Number.isFinite(bodyNode.y)) return null;
+ return {{ x: bodyNode.x, y: bodyNode.y }};
+ }}
+
+ function drawNodeBreathing(context) {{
+ if (reducedMotion.matches || visibleState.nodes.length > 180) return;
+ const scale = Math.max(network.getScale(), 0.2);
+ context.save();
+ context.lineWidth = 1 / scale;
+ visibleState.nodes.forEach((node) => {{
+ const position = nodePosition(node.id);
+ if (!position) return;
+ const phase = (hashText(node.id) % 628) / 100;
+ const pulse = (Math.sin((ambientTimestamp / 1250) + phase) + 1) / 2;
+ const radius = node.size * (1.035 + pulse * 0.045);
+ context.beginPath();
+ context.arc(position.x, position.y, radius, 0, Math.PI * 2);
+ context.strokeStyle = rgba(node.borderColor, 0.16 + pulse * 0.1);
+ context.stroke();
+ }});
+ context.restore();
+ }}
+
+ function drawEdgeFlow(context) {{
+ if (reducedMotion.matches || visibleState.edges.length > 360) return;
+ const focusId = activeFocusId();
+ const scale = Math.max(network.getScale(), 0.2);
+ let drawn = 0;
+ const maxAmbientEdges = focusId ? 120 : 64;
+ context.save();
+ visibleState.edges.forEach((edge) => {{
+ const focused = focusId && (edge.from === focusId || edge.to === focusId);
+ if (focusId && !focused) return;
+ if (drawn >= maxAmbientEdges) return;
+ const phase = (hashText(edge.id) % 1000) / 1000;
+ const progress = ((ambientTimestamp / 2600) + phase) % 1;
+ const flowAlpha = Math.pow(Math.sin(Math.PI * progress), 1.4);
+ const t = 0.08 + progress * 0.84;
+ const bodyEdge = network.body && network.body.edges ? network.body.edges[edge.id] : null;
+ const edgePath = bodyEdge && bodyEdge.edgeType;
+ let point = null;
+ if (edgePath && typeof edgePath.getPoint === "function") {{
+ try {{
+ point = edgePath.getPoint(t);
+ }} catch (error) {{
+ point = null;
+ }}
+ }}
+ if (!point || !Number.isFinite(point.x) || !Number.isFinite(point.y)) {{
+ const source = nodePosition(edge.from);
+ const target = nodePosition(edge.to);
+ if (!source || !target) return;
+ const dx = target.x - source.x;
+ const dy = target.y - source.y;
+ const distance = Math.max(Math.hypot(dx, dy), 1);
+ const curve = curveDetails(edge);
+ const bend = Math.min(96, distance * curve.roundness) * curve.direction;
+ const controlX = (source.x + target.x) / 2 - (dy / distance) * bend;
+ const controlY = (source.y + target.y) / 2 + (dx / distance) * bend;
+ const inverse = 1 - t;
+ point = {{
+ x: (inverse * inverse * source.x) + (2 * inverse * t * controlX) + (t * t * target.x),
+ y: (inverse * inverse * source.y) + (2 * inverse * t * controlY) + (t * t * target.y),
+ }};
+ }}
+ const baseColor = focused ? (edge.from === focusId ? "#5b8cff" : "#56b6b2") : edge.color;
+ context.beginPath();
+ context.arc(point.x, point.y, (focused ? 2.1 : 1.35) / scale, 0, Math.PI * 2);
+ context.fillStyle = rgba(baseColor, (focused ? 0.9 : 0.52) * flowAlpha);
+ context.fill();
+ drawn += 1;
+ }});
+ context.restore();
+ }}
+
+ function startAmbientAnimation() {{
+ if (
+ ambientFrame
+ || document.hidden
+ || reducedMotion.matches
+ || !graphInViewport
+ || state.draggingNode
+ || collisionTargets
+ || visibleState.nodes.length > 180
+ || visibleState.edges.length > 360
+ ) return;
+ const animate = (timestamp) => {{
+ if (document.hidden || reducedMotion.matches || !graphInViewport || state.draggingNode || collisionTargets) {{
+ ambientFrame = null;
return;
}}
- nextBasePositions.set(node.id, livePositions[node.id] || seededPositions.get(node.id) || {{ x: 0, y: 0 }});
+ if (visibleState.nodes.length > 180 || visibleState.edges.length > 360) {{
+ ambientFrame = null;
+ return;
+ }}
+ const interval = visibleState.nodes.length > 100 || visibleState.edges.length > 220 ? 66 : 33;
+ if (timestamp - lastAmbientPaint >= interval) {{
+ ambientTimestamp = timestamp;
+ lastAmbientPaint = timestamp;
+ network.redraw();
+ }}
+ ambientFrame = window.requestAnimationFrame(animate);
+ }};
+ ambientFrame = window.requestAnimationFrame(animate);
+ }}
+
+ function setNodePosition(nodeId, position) {{
+ const bodyNode = network.body && network.body.nodes ? network.body.nodes[nodeId] : null;
+ if (!bodyNode) return;
+ bodyNode.x = position.x;
+ bodyNode.y = position.y;
+ }}
+
+ function persistLayoutPositions(positionMap, viewName = state.view) {{
+ const view = payload.views[viewName] || visibleView();
+ const cachedPositions = positionsForView(viewName, view);
+ const updates = [];
+ positionMap.forEach((position, nodeId) => {{
+ cachedPositions.set(nodeId, {{ x: position.x, y: position.y }});
+ if (viewName === state.view && nodeSet.get(nodeId)) {{
+ updates.push({{ id: nodeId, x: position.x, y: position.y }});
+ }}
+ }});
+ if (updates.length) nodeSet.update(updates);
+ }}
+
+ function localCollisionTargets(draggedNodeId, fallbackPosition) {{
+ const view = visibleView();
+ const nodeMap = new Map(view.nodes.map((node) => [node.id, node]));
+ const cachedPositions = positionsForView(state.view, view);
+ const livePositions = currentPositions(nodeSet.getIds());
+ const targets = new Map();
+ view.nodes.forEach((node) => {{
+ const position = livePositions[node.id] || cachedPositions.get(node.id) || {{ x: 0, y: 0 }};
+ targets.set(node.id, {{ x: position.x, y: position.y }});
+ }});
+ const originals = new Map(Array.from(targets, ([nodeId, position]) => [nodeId, {{ x: position.x, y: position.y }}]));
+ const nodeIds = Array.from(targets.keys()).sort();
+ const maximumRadius = Math.max(1, ...view.nodes.map((node) => Number(node.size) || 1));
+ const cellSize = Math.max(96, maximumRadius * 2 + COLLISION_PADDING);
+ const cells = new Map();
+ const affected = new Set([draggedNodeId]);
+ const queue = [draggedNodeId];
+ const pending = new Set(queue);
+ let overflow = false;
+ let processed = 0;
+
+ function cellCoordinates(position) {{
+ return {{ x: Math.floor(position.x / cellSize), y: Math.floor(position.y / cellSize) }};
+ }}
+
+ function cellKey(position) {{
+ const coordinates = cellCoordinates(position);
+ return `${{coordinates.x}}:${{coordinates.y}}`;
+ }}
+
+ function addToCell(nodeId, position) {{
+ const key = cellKey(position);
+ if (!cells.has(key)) cells.set(key, new Set());
+ cells.get(key).add(nodeId);
+ }}
+
+ function removeFromCell(nodeId, position) {{
+ const key = cellKey(position);
+ const bucket = cells.get(key);
+ if (!bucket) return;
+ bucket.delete(nodeId);
+ if (!bucket.size) cells.delete(key);
+ }}
+
+ function nearbyNodeIds(position) {{
+ const coordinates = cellCoordinates(position);
+ const nearby = new Set();
+ for (let offsetX = -1; offsetX <= 1; offsetX += 1) {{
+ for (let offsetY = -1; offsetY <= 1; offsetY += 1) {{
+ const bucket = cells.get(`${{coordinates.x + offsetX}}:${{coordinates.y + offsetY}}`);
+ if (bucket) bucket.forEach((nodeId) => nearby.add(nodeId));
+ }}
+ }}
+ return Array.from(nearby).sort();
+ }}
+
+ function moveTarget(nodeId, position) {{
+ const previous = targets.get(nodeId);
+ const original = originals.get(nodeId);
+ if (!previous || !original || nodeId === draggedNodeId) return false;
+ if (!affected.has(nodeId) && affected.size >= MAX_COLLISION_NODES) return false;
+ if (Math.hypot(position.x - original.x, position.y - original.y) > MAX_COLLISION_DISPLACEMENT) return false;
+ removeFromCell(nodeId, previous);
+ targets.set(nodeId, position);
+ addToCell(nodeId, position);
+ affected.add(nodeId);
+ if (!pending.has(nodeId)) {{
+ queue.push(nodeId);
+ pending.add(nodeId);
+ }}
+ return true;
+ }}
+
+ nodeIds.forEach((nodeId) => addToCell(nodeId, targets.get(nodeId)));
+ while (queue.length && processed < MAX_COLLISION_NODES * 8) {{
+ const currentId = queue.shift();
+ pending.delete(currentId);
+ processed += 1;
+ const current = targets.get(currentId);
+ const currentNode = nodeMap.get(currentId);
+ if (!current || !currentNode) continue;
+ const candidates = nearbyNodeIds(current);
+ for (const otherId of candidates) {{
+ if (otherId === currentId) continue;
+ const other = targets.get(otherId);
+ const otherNode = nodeMap.get(otherId);
+ if (!other || !otherNode) continue;
+ const anchorId = otherId === draggedNodeId ? otherId : currentId;
+ const moverId = otherId === draggedNodeId ? currentId : otherId;
+ if (moverId === draggedNodeId) continue;
+ const anchor = targets.get(anchorId);
+ const mover = targets.get(moverId);
+ const anchorNode = nodeMap.get(anchorId);
+ const moverNode = nodeMap.get(moverId);
+ if (!anchor || !mover || !anchorNode || !moverNode) continue;
+ let dx = mover.x - anchor.x;
+ let dy = mover.y - anchor.y;
+ let distance = Math.hypot(dx, dy);
+ const minimumDistance = anchorNode.size + moverNode.size + COLLISION_PADDING;
+ if (distance >= minimumDistance) continue;
+ if (distance < 0.001) {{
+ const angle = (hashText(`${{anchorId}}|${{moverId}}`) % 628) / 100;
+ dx = Math.cos(angle);
+ dy = Math.sin(angle);
+ distance = 1;
+ }}
+ const overlap = minimumDistance - distance + 0.5;
+ const nextPosition = {{
+ x: mover.x + (dx / distance) * overlap,
+ y: mover.y + (dy / distance) * overlap,
+ }};
+ if (!moveTarget(moverId, nextPosition)) overflow = true;
+ }}
+ }}
+ if (queue.length) overflow = true;
+
+ affected.forEach((nodeId) => {{
+ if (overflow) return;
+ const position = targets.get(nodeId);
+ const node = nodeMap.get(nodeId);
+ if (!position || !node) return;
+ for (const otherId of nearbyNodeIds(position)) {{
+ if (otherId === nodeId) continue;
+ const other = targets.get(otherId);
+ const otherNode = nodeMap.get(otherId);
+ if (!other || !otherNode) continue;
+ if (Math.hypot(other.x - position.x, other.y - position.y) < node.size + otherNode.size + COLLISION_PADDING - 0.5) {{
+ overflow = true;
+ break;
+ }}
+ }}
+ }});
+
+ if (overflow) {{
+ const draggedNode = nodeMap.get(draggedNodeId);
+ const droppedPosition = originals.get(draggedNodeId);
+ if (fallbackPosition) {{
+ return new Map([[draggedNodeId, fallbackPosition]]);
+ }}
+ const otherIds = nodeIds.filter((nodeId) => nodeId !== draggedNodeId);
+ const isAvailable = (candidate) => otherIds.every((nodeId) => {{
+ const other = originals.get(nodeId);
+ const otherNode = nodeMap.get(nodeId);
+ return !other || !otherNode || Math.hypot(other.x - candidate.x, other.y - candidate.y) >= draggedNode.size + otherNode.size + COLLISION_PADDING;
+ }});
+ if (draggedNode && droppedPosition) {{
+ const ringStep = Math.max(28, draggedNode.size * 0.8);
+ const phase = (hashText(draggedNodeId) % 628) / 100;
+ for (let ring = 1; ring <= 16; ring += 1) {{
+ const samples = 12 + ring * 4;
+ for (let sample = 0; sample < samples; sample += 1) {{
+ const angle = phase + (sample / samples) * Math.PI * 2;
+ const candidate = {{
+ x: droppedPosition.x + Math.cos(angle) * ring * ringStep,
+ y: droppedPosition.y + Math.sin(angle) * ring * ringStep,
+ }};
+ if (isAvailable(candidate)) return new Map([[draggedNodeId, candidate]]);
+ }}
+ }}
+ }}
+ return new Map([[draggedNodeId, droppedPosition || {{ x: 0, y: 0 }}]]);
+ }}
+
+ const changedTargets = new Map();
+ affected.forEach((nodeId) => {{
+ const target = targets.get(nodeId);
+ if (target) changedTargets.set(nodeId, target);
}});
- return nextBasePositions;
+ return changedTargets;
+ }}
+
+ function finishCollisionResolution() {{
+ if (!collisionTargets || !collisionView) return;
+ if (collisionFrame) window.cancelAnimationFrame(collisionFrame);
+ if (collisionView === state.view) {{
+ collisionTargets.forEach((position, nodeId) => setNodePosition(nodeId, position));
+ }}
+ persistLayoutPositions(collisionTargets, collisionView);
+ collisionFrame = null;
+ collisionTargets = null;
+ collisionView = null;
+ shell.dataset.collisionState = "idle";
+ network.redraw();
+ syncPositionSignature();
+ startAmbientAnimation();
+ }}
+
+ function animateCollisionResolution(targets) {{
+ if (!targets.size) return;
+ const starts = currentPositions(Array.from(targets.keys()));
+ const startedAt = performance.now();
+ collisionTargets = targets;
+ collisionView = state.view;
+ shell.dataset.collisionState = "settling";
+
+ if (reducedMotion.matches) {{
+ finishCollisionResolution();
+ return;
+ }}
+
+ const step = (timestamp) => {{
+ const progress = Math.min(1, (timestamp - startedAt) / COLLISION_DURATION_MS);
+ const eased = 1 - Math.pow(1 - progress, 3);
+ targets.forEach((target, nodeId) => {{
+ const start = starts[nodeId] || target;
+ setNodePosition(nodeId, {{
+ x: start.x + (target.x - start.x) * eased,
+ y: start.y + (target.y - start.y) * eased,
+ }});
+ }});
+ network.redraw();
+ if (progress < 1) {{
+ collisionFrame = window.requestAnimationFrame(step);
+ }} else {{
+ finishCollisionResolution();
+ }}
+ }};
+ collisionFrame = window.requestAnimationFrame(step);
}}
function neighbors(nodeId, edges) {{
@@ -698,231 +1175,85 @@ def _build_fragment_markup(self, container_id: str, payload_json: str, script_no
return {{ nodes, edges }};
}}
- function persistBasePositions(nodeIds = nodeSet.getIds()) {{
- if (typeof network.storePositions === "function") {{
- try {{
- network.storePositions();
- }} catch (error) {{
- // Ignore storePositions failures and fall back to reading live coordinates.
- }}
- }}
- const positions = currentPositions(nodeIds);
- const nextBasePositions = new Map(visibleState.basePositions);
- Object.entries(positions).forEach(([nodeId, position]) => {{
- nextBasePositions.set(nodeId, position);
- }});
- visibleState.basePositions = nextBasePositions;
- driftStartedAt = performance.now();
- }}
-
- function applyVisualPositions(anchors) {{
- if (!anchors || !anchors.size) {{
- return;
- }}
- let changed = false;
- anchors.forEach((position, nodeId) => {{
- const bodyNode = network.body?.nodes?.[nodeId];
- if (!bodyNode) {{
- return;
- }}
- bodyNode.x = position.x;
- bodyNode.y = position.y;
- if (bodyNode.options) {{
- bodyNode.options.x = position.x;
- bodyNode.options.y = position.y;
- }}
- changed = true;
- }});
- if (changed) {{
- network.redraw();
- }}
- }}
-
- function stopDrift(resetToAnchors = false) {{
- if (driftFrame) {{
- window.cancelAnimationFrame(driftFrame);
- driftFrame = null;
- }}
- if (resetToAnchors && visibleState.basePositions.size) {{
- applyVisualPositions(visibleState.basePositions);
- }}
- }}
-
- function startDrift() {{
- stopDrift(true);
- if (!visibleState.basePositions.size) {{
- persistBasePositions();
- }}
- if (!visibleState.basePositions.size) {{
- return;
- }}
- const anchors = new Map(visibleState.basePositions);
- const driftStep = (timestamp) => {{
- if (physicsActive || state.draggingNode) {{
- driftFrame = window.requestAnimationFrame(driftStep);
- return;
- }}
- const updates = [];
- anchors.forEach((anchor, nodeId) => {{
- if (!nodeSet.get(nodeId)) {{
- return;
- }}
- const phaseSeed = nodeId.split("").reduce((acc, ch) => acc + ch.charCodeAt(0), 0);
- const phase = phaseSeed * 0.07;
- const elapsed = (timestamp - driftStartedAt) / DRIFT_PERIOD_MS;
- const importance = visibleState.nodeMap.get(nodeId)?.importance || 1;
- const amplitude = DRIFT_AMPLITUDE + Math.min(0.75, Math.log1p(importance) * 0.16);
- const dx = Math.sin((elapsed * Math.PI * 2) + phase) * amplitude;
- const dy = Math.cos((elapsed * Math.PI * 2 * 0.92) + phase) * amplitude * 0.72;
- updates.push({{ id: nodeId, x: anchor.x + dx, y: anchor.y + dy }});
- }});
- if (updates.length) {{
- applyVisualPositions(new Map(updates.map((item) => [item.id, {{ x: item.x, y: item.y }}])));
- }}
- driftFrame = window.requestAnimationFrame(driftStep);
- }};
- driftFrame = window.requestAnimationFrame(driftStep);
- }}
-
- function stopPhysics() {{
- physicsActive = false;
- previousPositions = null;
- stableFrames = 0;
- if (physicsTimer) {{
- window.clearTimeout(physicsTimer);
- physicsTimer = null;
- }}
- if (physicsFrame) {{
- window.cancelAnimationFrame(physicsFrame);
- physicsFrame = null;
- }}
- network.stopSimulation();
- network.setOptions({{ physics: {{ enabled: false }} }});
- persistBasePositions();
- shell.classList.add("is-settled");
- startDrift();
- }}
-
- function monitorPhysics(nodeIds) {{
- if (!physicsActive) {{
- return;
- }}
- const positions = currentPositions(nodeIds);
- const ids = Object.keys(positions);
- if (previousPositions) {{
- let totalMovement = 0;
- let samples = 0;
- ids.forEach((id) => {{
- const current = positions[id];
- const previous = previousPositions[id];
- if (!current || !previous) {{
- return;
- }}
- totalMovement += Math.hypot(current.x - previous.x, current.y - previous.y);
- samples += 1;
- }});
- const averageVelocity = samples ? totalMovement / samples : 0;
- stableFrames = averageVelocity < VELOCITY_THRESHOLD ? stableFrames + 1 : 0;
- if (stableFrames >= STABLE_FRAMES_REQUIRED || performance.now() - physicsStartedAt > MAX_PHYSICS_DURATION_MS) {{
- stopPhysics();
- return;
- }}
+ function applyInteractiveStyles(selectSearchMatch = false) {{
+ if (selectSearchMatch && state.search.trim()) {{
+ const query = state.search.trim().toLowerCase();
+ const firstMatch = visibleState.nodes.find((node) =>
+ [node.id, node.label, node.cluster, node.role].join(" ").toLowerCase().includes(query)
+ );
+ state.spotlight = firstMatch ? firstMatch.id : null;
}}
- previousPositions = positions;
- physicsFrame = window.requestAnimationFrame(() => monitorPhysics(nodeIds));
- }}
-
- function beginPhysics(nodeIds) {{
- shell.classList.remove("is-settled");
- stopDrift(true);
- if (physicsTimer) {{
- window.clearTimeout(physicsTimer);
- physicsTimer = null;
- }}
- if (physicsFrame) {{
- window.cancelAnimationFrame(physicsFrame);
- physicsFrame = null;
- }}
- physicsActive = true;
- physicsStartedAt = performance.now();
- previousPositions = null;
- stableFrames = 0;
- network.setOptions({{ physics: {{ enabled: true }} }});
- network.startSimulation();
- physicsTimer = window.setTimeout(() => monitorPhysics(nodeIds), 80);
- }}
-
- function applyInteractiveStyles(focusSearch = false) {{
const focusId = activeFocusId();
const spotlightNeighbors = focusId ? neighbors(focusId, visibleState.edges) : {{ incoming: new Set(), outgoing: new Set() }};
nodeSet.update(visibleState.nodes.map((node) => {{
const highlighted = !focusId || node.id === focusId || spotlightNeighbors.incoming.has(node.id) || spotlightNeighbors.outgoing.has(node.id);
- const showLabel = node.showLabel
- || state.scale >= 1.34
- || node.id === focusId
- || ((spotlightNeighbors.incoming.has(node.id) || spotlightNeighbors.outgoing.has(node.id)) && state.scale >= 1.08);
+ const showLabel = node.showLabel || node.id === focusId;
+ const baseBorder = node.isEntrypoint ? node.borderColor : rgba(node.borderColor, 0.86);
return {{
id: node.id,
label: showLabel ? node.fullLabel : "",
title: "",
color: {{
- background: highlighted ? node.backgroundColor : rgba(node.backgroundColor, 0.1),
- border: highlighted ? node.borderColor : rgba(node.borderColor, 0.14),
- highlight: {{ background: node.backgroundColor, border: node.borderColor }},
- hover: {{ background: node.backgroundColor, border: node.borderColor }}
+ background: highlighted ? node.backgroundColor : rgba(node.backgroundColor, 0.14),
+ border: node.id === focusId ? "#9bb5ff" : (highlighted ? baseBorder : rgba(node.borderColor, 0.18)),
+ highlight: {{ background: node.backgroundColor, border: "#9bb5ff" }},
+ hover: {{ background: node.backgroundColor, border: baseBorder }}
}},
font: {{
face: "Segoe UI",
size: showLabel ? 13 : 1,
- color: highlighted ? "#edf4ff" : "rgba(154,168,188,0.24)",
- strokeWidth: showLabel ? 4 : 0,
- strokeColor: "rgba(17,19,23,0.94)"
+ color: highlighted ? "#d8dce3" : "rgba(141,150,165,0.28)",
+ strokeWidth: showLabel ? 2 : 0,
+ strokeColor: "#101318"
}},
- borderWidth: node.id === focusId ? 3.2 : (node.isEntrypoint ? 2.5 : 1.6),
- shadow: highlighted ? {{ enabled: true, color: rgba(node.borderColor, node.id === focusId ? 0.42 : 0.24), size: node.id === focusId ? 28 : 18, x: 0, y: 0 }} : {{ enabled: false, size: 0, x: 0, y: 0 }},
+ borderWidth: node.isEntrypoint ? 2 : 1.5,
+ shadow: false,
}};
}}));
edgeSet.update(visibleState.edges.map((edge) => {{
- const baseColor = edge.dashes && edge.color === "#94a3b8" ? "#f59e0b" : edge.color;
- const baseAlpha = edge.width >= 4 ? 0.6 : (edge.width >= 2.6 ? 0.32 : 0.15);
+ const baseColor = edge.dashes && edge.color === "#68717e" ? "#d0a04d" : edge.color;
+ const baseAlpha = edge.width >= 4 ? 0.52 : (edge.width >= 2.6 ? 0.28 : 0.14);
let color = rgba(baseColor, baseAlpha);
- let width = edge.width;
const isOutgoingFocus = focusId && edge.from === focusId;
const isIncomingFocus = focusId && edge.to === focusId;
- const isFocusedEdge = Boolean(isOutgoingFocus || isIncomingFocus);
if (focusId) {{
- if (isOutgoingFocus) {{ color = rgba("#60a5fa", 0.98); width = Math.max(width + 1.8, 3.8); }}
- else if (isIncomingFocus) {{ color = rgba("#67e8f9", 0.98); width = Math.max(width + 1.8, 3.8); }}
- else {{ color = rgba(baseColor, 0.06); width = Math.max(0.8, width * 0.6); }}
+ if (isOutgoingFocus) color = rgba("#5b8cff", 0.92);
+ else if (isIncomingFocus) color = rgba("#56b6b2", 0.92);
+ else color = rgba(baseColor, 0.05);
}}
return {{
id: edge.id,
title: "",
color: {{ color, highlight: color, hover: color }},
- width,
- shadow: isFocusedEdge ? {{ enabled: true, color: rgba("#38bdf8", 0.34), size: 18, x: 0, y: 0 }} : {{ enabled: false, size: 0, x: 0, y: 0 }},
+ width: edge.width,
+ shadow: false,
}};
}}));
descriptionEl.textContent = focusId
- ? `${{visibleView().description}} Spotlight mode highlights direct dependencies.`
+ ? `${{visibleView().description}} Direct dependencies are highlighted.`
: visibleView().description;
statsEl.textContent = `${{visibleState.nodes.length}} nodes - ${{visibleState.edges.length}} edges`;
- shell.querySelectorAll(".ecb-mode[data-view]").forEach((button) => button.classList.toggle("is-active", button.dataset.view === state.view));
- if (focusSearch && state.search.trim()) {{
- const query = state.search.trim().toLowerCase();
- const firstMatch = visibleState.nodes.find((node) => [node.id, node.label, node.cluster, node.role].join(" ").toLowerCase().includes(query));
- if (firstMatch) {{
- network.selectNodes([firstMatch.id]);
- network.focus(firstMatch.id, {{ scale: Math.max(state.scale, 1.18), animation: {{ duration: 280, easingFunction: "easeInOutQuad" }} }});
- }}
- }} else if (focusId) {{
+ shell.querySelectorAll(".ecb-mode[data-view]").forEach((button) => {{
+ const active = button.dataset.view === state.view;
+ button.classList.toggle("is-active", active);
+ button.setAttribute("aria-selected", String(active));
+ }});
+ if (focusId) {{
network.selectNodes([focusId]);
}} else {{
network.unselectAll();
}}
}}
- function render(focusSearch = false, restartLayout = false) {{
+ function replaceItems(dataSet, items) {{
+ const nextIds = new Set(items.map((item) => item.id));
+ const removedIds = dataSet.getIds().filter((id) => !nextIds.has(id));
+ if (removedIds.length) dataSet.remove(removedIds);
+ if (items.length) dataSet.update(items);
+ }}
+
+ function render(selectSearchMatch = false, fitView = false) {{
+ if (collisionTargets) finishCollisionResolution();
const view = visibleView();
const filtered = filterView(view);
if (state.spotlight && !filtered.nodes.some((node) => node.id === state.spotlight)) {{
@@ -931,17 +1262,10 @@ def _build_fragment_markup(self, container_id: str, payload_json: str, script_no
if (state.hoveredNode && !filtered.nodes.some((node) => node.id === state.hoveredNode)) {{
state.hoveredNode = null;
}}
- const nodeIds = filtered.nodes.map((node) => node.id);
- const livePositions = currentPositions(nodeIds);
- const seededPositions = seedPositions(filtered.nodes);
updateVisibleState(filtered.nodes, filtered.edges);
- stopDrift(true);
- const basePositions = resolveBasePositions(filtered.nodes, livePositions, seededPositions);
- visibleState.basePositions = basePositions;
- nodeSet.clear();
- edgeSet.clear();
- nodeSet.add(filtered.nodes.map((node) => {{
- const position = basePositions.get(node.id) || {{ x: 0, y: 0 }};
+ const positions = positionsForView(state.view, view);
+ replaceItems(nodeSet, filtered.nodes.map((node) => {{
+ const position = positions.get(node.id) || {{ x: 0, y: 0 }};
return {{
id: node.id,
label: "",
@@ -950,6 +1274,7 @@ def _build_fragment_markup(self, container_id: str, payload_json: str, script_no
path: node.path,
x: position.x,
y: position.y,
+ fixed: {{ x: false, y: false }},
size: node.size,
color: {{
background: node.backgroundColor,
@@ -960,18 +1285,19 @@ def _build_fragment_markup(self, container_id: str, payload_json: str, script_no
font: {{
face: "Segoe UI",
size: 1,
- color: "#edf4ff",
+ color: "#d8dce3",
strokeWidth: 0,
- strokeColor: "rgba(17,19,23,0.94)"
+ strokeColor: "#101318"
}},
- borderWidth: node.isEntrypoint ? 2.5 : 1.6,
- mass: Math.max(1, node.size / 8),
- shadow: {{ enabled: true, color: rgba(node.borderColor, 0.18), size: 14, x: 0, y: 0 }},
+ borderWidth: node.isEntrypoint ? 2 : 1.5,
+ chosen: false,
+ shadow: false,
}};
}}));
- edgeSet.add(filtered.edges.map((edge) => {{
- const baseColor = edge.dashes && edge.color === "#94a3b8" ? "#f59e0b" : edge.color;
- const baseAlpha = edge.width >= 4 ? 0.6 : (edge.width >= 2.6 ? 0.32 : 0.15);
+ replaceItems(edgeSet, filtered.edges.map((edge) => {{
+ const baseColor = edge.dashes && edge.color === "#68717e" ? "#d0a04d" : edge.color;
+ const baseAlpha = edge.width >= 4 ? 0.52 : (edge.width >= 2.6 ? 0.28 : 0.14);
+ const curve = curveDetails(edge);
return {{
id: edge.id,
from: edge.from,
@@ -981,23 +1307,21 @@ def _build_fragment_markup(self, container_id: str, payload_json: str, script_no
width: edge.width,
dashes: edge.dashes,
color: {{ color: rgba(baseColor, baseAlpha), highlight: rgba(baseColor, baseAlpha), hover: rgba(baseColor, baseAlpha) }},
- smooth: {{ type: "dynamic", roundness: 0.24 }},
- shadow: {{ enabled: false, size: 0, x: 0, y: 0 }},
+ smooth: {{
+ enabled: true,
+ type: curve.direction > 0 ? "curvedCW" : "curvedCCW",
+ roundness: curve.roundness,
+ }},
+ chosen: false,
+ shadow: false,
}};
}}));
- applyInteractiveStyles(focusSearch);
- if (restartLayout) {{
- beginPhysics(nodeIds);
- window.setTimeout(() => {{
- network.fit({{ animation: {{ duration: 260, easingFunction: "easeInOutQuad" }} }});
- }}, 120);
- window.setTimeout(() => {{
- const nextScale = Math.max(0.72, network.getScale() * (1 - CANVAS_PADDING / 1250));
- network.moveTo({{ scale: nextScale, animation: {{ duration: 260, easingFunction: "easeInOutQuad" }} }});
- }}, 420);
- }} else if (!physicsActive) {{
- startDrift();
+ applyInteractiveStyles(selectSearchMatch);
+ if (fitView && filtered.nodes.length) {{
+ network.fit({{ nodes: filtered.nodes.map((node) => node.id), animation: false }});
}}
+ syncPositionSignature();
+ startAmbientAnimation();
}}
shell.querySelectorAll(".ecb-mode[data-view]").forEach((button) => button.addEventListener("click", () => {{
@@ -1005,14 +1329,15 @@ def _build_fragment_markup(self, container_id: str, payload_json: str, script_no
state.spotlight = null;
state.hoveredNode = null;
hideTooltip();
- render(true, true);
+ render(false, true);
}}));
shell.querySelector(".ecb-search").addEventListener("input", (event) => {{
state.search = event.target.value;
state.spotlight = null;
state.hoveredNode = null;
hideTooltip();
- render(true, false);
+ if (searchTimer) window.clearTimeout(searchTimer);
+ searchTimer = window.setTimeout(() => render(true, false), 90);
}});
shell.querySelectorAll("input[data-filter]").forEach((input) => input.addEventListener("input", () => {{
state.filters.hideUtilities = shell.querySelector('input[data-filter="hide-utilities"]').checked;
@@ -1023,7 +1348,11 @@ def _build_fragment_markup(self, container_id: str, payload_json: str, script_no
state.spotlight = null;
state.hoveredNode = null;
hideTooltip();
- render(true, false);
+ if (filterFrame) window.cancelAnimationFrame(filterFrame);
+ filterFrame = window.requestAnimationFrame(() => {{
+ filterFrame = null;
+ render(false, false);
+ }});
}}));
shell.querySelector('[data-export="png"]').addEventListener("click", () => {{
const canvas = container.querySelector("canvas");
@@ -1041,16 +1370,17 @@ def _build_fragment_markup(self, container_id: str, payload_json: str, script_no
hideTooltip();
}}
applyInteractiveStyles(false);
+ syncPositionSignature();
}});
network.on("hoverNode", (params) => {{
state.hoveredNode = params.node;
+ container.style.cursor = "grab";
showTooltip(params.node, params.pointer.DOM);
- applyInteractiveStyles(false);
}});
network.on("blurNode", () => {{
state.hoveredNode = null;
+ if (!state.draggingNode) container.style.cursor = "default";
hideTooltip();
- applyInteractiveStyles(false);
}});
network.on("mousemove", (params) => {{
if (state.hoveredNode && params.pointer && params.pointer.DOM) {{
@@ -1058,28 +1388,70 @@ def _build_fragment_markup(self, container_id: str, payload_json: str, script_no
}}
}});
network.on("dragStart", (params) => {{
+ if (!params.nodes.length) return;
+ if (collisionTargets) finishCollisionResolution();
+ if (ambientFrame) {{
+ window.cancelAnimationFrame(ambientFrame);
+ ambientFrame = null;
+ }}
+ state.draggingNode = params.nodes[0];
+ dragOrigin = currentPositions([state.draggingNode])[state.draggingNode] || null;
state.hoveredNode = null;
- state.draggingNode = params.nodes.length ? params.nodes[0] : null;
+ container.style.cursor = "grabbing";
hideTooltip();
- beginPhysics(nodeSet.getIds());
}});
- network.on("dragEnd", () => {{
- const draggedNodeId = state.draggingNode;
- if (draggedNodeId) {{
- persistBasePositions([draggedNodeId]);
- }}
+ network.on("dragEnd", (params) => {{
+ const draggedNodeId = state.draggingNode || params.nodes[0];
state.draggingNode = null;
- window.setTimeout(() => {{
- if (!physicsActive) {{
- beginPhysics(nodeSet.getIds());
- }}
- }}, DRAG_RESTABILIZE_DELAY_MS);
+ container.style.cursor = "default";
+ if (!draggedNodeId || !dragOrigin) {{
+ dragOrigin = null;
+ startAmbientAnimation();
+ return;
+ }}
+ const droppedPosition = currentPositions([draggedNodeId])[draggedNodeId];
+ const movementThreshold = 5 / Math.max(network.getScale(), 0.2);
+ const movedDistance = droppedPosition
+ ? Math.hypot(droppedPosition.x - dragOrigin.x, droppedPosition.y - dragOrigin.y)
+ : 0;
+ if (!droppedPosition || movedDistance < movementThreshold) {{
+ const original = new Map([[draggedNodeId, dragOrigin]]);
+ original.forEach((position, nodeId) => setNodePosition(nodeId, position));
+ persistLayoutPositions(original);
+ network.redraw();
+ syncPositionSignature();
+ startAmbientAnimation();
+ dragOrigin = null;
+ return;
+ }}
+ const fallbackPosition = dragOrigin;
+ dragOrigin = null;
+ animateCollisionResolution(localCollisionTargets(draggedNodeId, fallbackPosition));
}});
- network.on("zoom", (params) => {{
- state.scale = params.scale;
- applyInteractiveStyles(false);
+ network.on("beforeDrawing", (context) => drawNodeBreathing(context));
+ network.on("afterDrawing", (context) => drawEdgeFlow(context));
+ if (typeof IntersectionObserver !== "undefined") {{
+ const visibilityObserver = new IntersectionObserver((entries) => {{
+ graphInViewport = entries.some((entry) => entry.isIntersecting);
+ if (graphInViewport) {{
+ startAmbientAnimation();
+ }} else if (ambientFrame) {{
+ window.cancelAnimationFrame(ambientFrame);
+ ambientFrame = null;
+ }}
+ }}, {{ threshold: 0.01 }});
+ visibilityObserver.observe(shell);
+ }}
+ document.addEventListener("visibilitychange", () => {{
+ if (document.hidden && ambientFrame) {{
+ window.cancelAnimationFrame(ambientFrame);
+ ambientFrame = null;
+ }} else if (!document.hidden) {{
+ startAmbientAnimation();
+ }}
}});
render(false, true);
+ startAmbientAnimation();
}})();
"""
@@ -1245,7 +1617,7 @@ def _architecture_view(
"from": source,
"to": target,
"width": min(6.0, 1.6 + data["count"] * 0.7),
- "color": "#dc2626" if data["side_effect"] else "#94a3b8",
+ "color": "#b95d66" if data["side_effect"] else "#68717e",
"dashes": bool(data["cycle"]),
"title": f"Import relationships: {data['count']}",
}
@@ -1362,7 +1734,7 @@ def _file_view_edges(
"from": source,
"to": target,
"width": min(5.4, 1.2 + log1p(max(int(file_nodes[target]["importance"]), 1)) * 0.8),
- "color": "#dc2626" if file_nodes[source]["isSideEffect"] or file_nodes[target]["isSideEffect"] else "#94a3b8",
+ "color": "#b95d66" if file_nodes[source]["isSideEffect"] or file_nodes[target]["isSideEffect"] else "#68717e",
"dashes": (source, target) in cycle_edges,
"title": "Import relationship",
}
@@ -1439,7 +1811,7 @@ def _group_role(self, roles: Counter[str], has_entrypoint: bool, has_side_effect
def _node_colors(self, role: str, side_effects: list[str]) -> tuple[str, str]:
if side_effects and role not in {"entrypoint", "repository", "config", "middleware"}:
- return "#f97316", "#c2410c"
+ return "#bf7b55", "#d1906b"
return self.ROLE_COLORS.get(role, self.ROLE_COLORS["unknown"])
def _risk_level(
diff --git a/pyproject.toml b/pyproject.toml
index 05b817b..e588c80 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "explain-codebase"
-version = "0.2.0"
+version = "0.2.1"
description = "Map repository architecture, dependencies, entry points, and change-risk signals from the command line."
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.10"
diff --git a/tests/test_security_regressions.py b/tests/test_security_regressions.py
index 858dacf..18c2b74 100644
--- a/tests/test_security_regressions.py
+++ b/tests/test_security_regressions.py
@@ -285,3 +285,22 @@ def test_graph_document_pins_external_script_and_sets_csp() -> None:
assert 'referrerpolicy="no-referrer"' in document
assert 'http-equiv="Content-Security-Policy"' in document
assert "script-src 'nonce-" in document
+ assert 'data-layout-mode="static"' in document
+ assert 'data-motion-mode="ambient"' in document
+ assert "physics: { enabled: false }" in document
+ assert "layout: { improvedLayout: false, randomSeed: 17 }" in document
+ assert "dragNodes: true" in document
+ assert "fixed: { x: false, y: false }" in document
+ assert 'type: "curvedCW"' in document
+ assert "drawNodeBreathing" in document
+ assert "drawEdgeFlow" in document
+ assert "localCollisionTargets" in document
+ assert "animateCollisionResolution" in document
+ assert 'type: "dynamic"' not in document
+ assert "startDrift" not in document
+ assert "beginPhysics" not in document
+ assert "startSimulation" not in document
+ assert "network.focus" not in document
+ assert "gradient" not in document
+ assert "box-shadow" not in document
+ assert "backdrop-filter" not in document