Files
compliance-scanner-agent/compliance-dashboard/assets/graph-viz.js
Sharang Parnerkar b18824db25 Add graph explorer components, API handlers, and dependency updates
Adds code inspector, file tree components, graph visualization JS,
graph API handlers, sidebar navigation updates, and misc improvements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:52:49 +01:00

336 lines
9.4 KiB
JavaScript

// ═══════════════════════════════════════════════════════════════
// Graph Visualization — vis-network wrapper for Code Knowledge Graph
// Obsidian Control theme
// ═══════════════════════════════════════════════════════════════
(function () {
"use strict";
// Color palette matching Obsidian Control
const NODE_COLORS = {
function: { bg: "#00c8ff", border: "#00a0cc", font: "#060a13" },
method: { bg: "#00c8ff", border: "#00a0cc", font: "#060a13" },
struct: { bg: "#e040fb", border: "#b030d0", font: "#060a13" },
class: { bg: "#e040fb", border: "#b030d0", font: "#060a13" },
enum: { bg: "#ffb020", border: "#cc8c18", font: "#060a13" },
interface: { bg: "#7c4dff", border: "#5c38c0", font: "#ffffff" },
trait: { bg: "#7c4dff", border: "#5c38c0", font: "#ffffff" },
module: { bg: "#ff8a3d", border: "#cc6e30", font: "#060a13" },
file: { bg: "#00e676", border: "#00b85e", font: "#060a13" },
};
const EDGE_COLORS = {
calls: "#00c8ff",
imports: "#ffb020",
inherits: "#e040fb",
implements: "#7c4dff",
contains: "rgba(94, 114, 145, 0.6)",
type_ref: "rgba(94, 114, 145, 0.5)",
};
const DEFAULT_NODE_COLOR = { bg: "#5e7291", border: "#3d506b", font: "#e4eaf4" };
const DEFAULT_EDGE_COLOR = "rgba(94, 114, 145, 0.3)";
let network = null;
let nodesDataset = null;
let edgesDataset = null;
let rawNodes = [];
let rawEdges = [];
function getNodeColor(kind) {
return NODE_COLORS[kind] || DEFAULT_NODE_COLOR;
}
function getEdgeColor(kind) {
return EDGE_COLORS[kind] || DEFAULT_EDGE_COLOR;
}
// Build a vis-network node from raw graph data
function toVisNode(node, edgeCounts) {
const color = getNodeColor(node.kind);
const connections = edgeCounts[node.qualified_name] || 0;
const size = Math.max(8, Math.min(35, 8 + connections * 2));
return {
id: node.qualified_name,
label: node.name,
title: `${node.kind}: ${node.qualified_name}\nFile: ${node.file_path}:${node.start_line}`,
size: size,
color: {
background: color.bg,
border: color.border,
highlight: { background: color.bg, border: "#ffffff" },
hover: { background: color.bg, border: "#ffffff" },
},
font: {
color: color.font,
size: 10,
face: "'JetBrains Mono', monospace",
strokeWidth: 2,
strokeColor: "#060a13",
},
borderWidth: 1,
borderWidthSelected: 3,
shape: node.kind === "file" ? "diamond" : node.kind === "module" ? "square" : "dot",
// Store raw data for click handler
_raw: node,
};
}
function toVisEdge(edge) {
const color = getEdgeColor(edge.kind);
const isDim = edge.kind === "contains" || edge.kind === "type_ref";
return {
from: edge.source,
to: edge.target,
color: {
color: color,
highlight: "#ffffff",
hover: color,
},
width: isDim ? 1 : 2,
arrows: {
to: {
enabled: edge.kind !== "contains",
scaleFactor: 0.5,
},
},
smooth: {
enabled: true,
type: "continuous",
roundness: 0.3,
},
title: `${edge.kind}: ${edge.source}${edge.target}`,
_raw: edge,
};
}
/**
* Initialize or reload the graph visualization.
* Called from Rust via eval().
*/
window.__loadGraph = function (nodes, edges) {
var container = document.getElementById("graph-canvas");
if (!container) {
console.error("[graph-viz] #graph-canvas not found");
return;
}
// Count edges per node for sizing
const edgeCounts = {};
edges.forEach(function (e) {
edgeCounts[e.source] = (edgeCounts[e.source] || 0) + 1;
edgeCounts[e.target] = (edgeCounts[e.target] || 0) + 1;
});
// Deduplicate nodes by qualified_name (keep first occurrence)
var seenNodes = {};
var uniqueNodes = [];
nodes.forEach(function (n) {
if (!seenNodes[n.qualified_name]) {
seenNodes[n.qualified_name] = true;
uniqueNodes.push(n);
}
});
// Deduplicate edges by source+target+kind
var seenEdges = {};
var uniqueEdges = [];
edges.forEach(function (e) {
var key = e.source + "|" + e.target + "|" + e.kind;
if (!seenEdges[key]) {
seenEdges[key] = true;
uniqueEdges.push(e);
}
});
rawNodes = uniqueNodes;
rawEdges = uniqueEdges;
var visNodes = uniqueNodes.map(function (n) {
return toVisNode(n, edgeCounts);
});
var visEdges = uniqueEdges.map(toVisEdge);
nodesDataset = new vis.DataSet(visNodes);
edgesDataset = new vis.DataSet(visEdges);
const options = {
nodes: {
font: { color: "#e4eaf4", size: 10 },
scaling: { min: 8, max: 35 },
},
edges: {
font: { color: "#5e7291", size: 9, strokeWidth: 0 },
selectionWidth: 3,
},
physics: {
enabled: true,
solver: "forceAtlas2Based",
forceAtlas2Based: {
gravitationalConstant: -60,
centralGravity: 0.012,
springLength: 80,
springConstant: 0.06,
damping: 0.4,
avoidOverlap: 0.5,
},
stabilization: {
enabled: true,
iterations: 1000,
updateInterval: 25,
},
maxVelocity: 40,
minVelocity: 0.1,
},
interaction: {
hover: true,
tooltipDelay: 200,
hideEdgesOnDrag: false,
hideEdgesOnZoom: false,
multiselect: false,
navigationButtons: false,
keyboard: { enabled: true },
},
layout: {
improvedLayout: uniqueNodes.length < 300,
},
};
// Destroy previous instance
if (network) {
network.destroy();
}
network = new vis.Network(
container,
{ nodes: nodesDataset, edges: edgesDataset },
options
);
// Click handler — sends data to Rust
network.on("click", function (params) {
if (params.nodes.length > 0) {
const nodeId = params.nodes[0];
const visNode = nodesDataset.get(nodeId);
if (visNode && visNode._raw && window.__onNodeClick) {
window.__onNodeClick(JSON.stringify(visNode._raw));
}
}
});
// Stabilization overlay
var overlay = document.getElementById("graph-stabilization-overlay");
var progressFill = document.getElementById("graph-stab-progress-fill");
var pctLabel = document.getElementById("graph-stab-pct");
// Show the overlay
if (overlay) {
overlay.classList.remove("graph-stab-fade-out");
overlay.style.display = "flex";
}
network.on("stabilizationProgress", function (params) {
var pct = Math.round((params.iterations / params.total) * 100);
if (progressFill) {
progressFill.style.width = pct + "%";
}
if (pctLabel) {
pctLabel.textContent = pct + "% — computing layout";
}
});
network.on("stabilizationIterationsDone", function () {
if (progressFill) progressFill.style.width = "100%";
if (pctLabel) pctLabel.textContent = "Complete";
// Fade out overlay
if (overlay) {
overlay.classList.add("graph-stab-fade-out");
setTimeout(function () {
overlay.style.display = "none";
}, 900);
}
network.setOptions({ physics: { enabled: false } });
});
console.log(
"[graph-viz] Loaded " + nodes.length + " nodes, " + edges.length + " edges"
);
};
/**
* Highlight a node by qualified name. Focuses + selects it.
*/
window.__highlightNode = function (qualifiedName) {
if (!network || !nodesDataset) return;
const node = nodesDataset.get(qualifiedName);
if (!node) return;
network.selectNodes([qualifiedName]);
network.focus(qualifiedName, {
scale: 1.5,
animation: { duration: 500, easingFunction: "easeInOutQuad" },
});
// Trigger click callback too
if (node._raw && window.__onNodeClick) {
window.__onNodeClick(JSON.stringify(node._raw));
}
};
/**
* Highlight multiple nodes (e.g., search results or file nodes).
*/
window.__highlightNodes = function (qualifiedNames) {
if (!network || !nodesDataset) return;
network.selectNodes(qualifiedNames);
if (qualifiedNames.length > 0) {
network.fit({
nodes: qualifiedNames,
animation: { duration: 500, easingFunction: "easeInOutQuad" },
});
}
};
/**
* Highlight all nodes belonging to a specific file path.
*/
window.__highlightFileNodes = function (filePath) {
if (!network || !nodesDataset) return;
var matching = [];
rawNodes.forEach(function (n) {
if (n.file_path === filePath) {
matching.push(n.qualified_name);
}
});
if (matching.length > 0) {
network.selectNodes(matching);
network.fit({
nodes: matching,
animation: { duration: 500, easingFunction: "easeInOutQuad" },
});
}
};
/**
* Clear selection.
*/
window.__clearGraphSelection = function () {
if (!network) return;
network.unselectAll();
};
/**
* Fit entire graph in view.
*/
window.__fitGraph = function () {
if (!network) return;
network.fit({
animation: { duration: 400, easingFunction: "easeInOutQuad" },
});
};
})();