/** * Standalone HTML visualization of the knowledge graph. * * Uses vis.js (vis-network) for interactive graph visualization with: * - ForceAtlas2 physics for natural clustering * - Sidebar with node details on click * - Community-based coloring * - Search with dropdown * - Legend with toggle per community * - Node/edge caps for large graphs */ import type { KnowledgeGraph } from "./graph.ts"; const VIS_JS = "https://unpkg.com/vis-network@9.1.6/dist/vis-network.min.js"; const VIS_CSS = "https://unpkg.com/vis-network@9.1.6/dist/vis-network.min.css"; const MAX_VIZ_NODES = 1500; const MAX_EDGES = 4000; export function generateHtml(kg: KnowledgeGraph, title: string = "Mind Place"): string { // Sort non-file nodes by degree, take top const nonFileNodes = [...kg.nodes.values()] .filter(n => n.type !== "file") .map(n => ({ node: n, degree: kg.adjacency.get(n.id)?.size ?? 0 })) .sort((a, b) => b.degree - a.degree); const cappedNodes = nonFileNodes.slice(0, MAX_VIZ_NODES); const cappedIds = new Set(cappedNodes.map(n => n.node.id)); // Colors from graphify's palette const COLORS = [ "#4E79A7","#F28E2B","#E15759","#76B7B2","#59A14F", "#EDC948","#B07AA1","#FF9DA7","#9C755F","#BAB0AC", "#6A3D9A","#FF7F00","#33A02C","#1F78B4","#E31A1C", "#FDBF6F","#A6CEE3","#B2DF8A","#FB9A99","#CAB2D6", "#FFFF99","#B15928","#8DD3C7","#FFFFB3","#BEBADA", ]; // Build nodes const nodes = cappedNodes.map(n => { const node = n.node; const comm = node.community ?? 0; return { id: node.id, label: node.label, color: COLORS[comm % COLORS.length], size: Math.max(5, Math.min(25, Math.log2(n.degree + 2) * 8)), font: { size: 10, color: "#ddd" }, title: `${node.label} (${node.type})
${node.sourceFile}${node.sourceLocation ? " " + node.sourceLocation : ""}
${n.degree} connections`, community: comm, sourceFile: node.sourceFile, fileType: node.type, degree: n.degree, }; }); // Build edges (only between capped nodes) const edges = []; for (const edge of kg.edges) { if (edges.length >= MAX_EDGES) break; if (!cappedIds.has(edge.source) || !cappedIds.has(edge.target)) continue; edges.push({ from: edge.source, to: edge.target, title: `${edge.relation} [${edge.confidence}]`, dashes: edge.confidence !== "EXTRACTED", width: edge.confidence === "EXTRACTED" ? 1 : 0.5, color: { color: edge.confidence === "EXTRACTED" ? "#555" : "#444", opacity: edge.confidence === "EXTRACTED" ? 0.4 : 0.25 }, }); } // Legend const legend = new Map(); for (const n of cappedNodes) { const comm = n.node.community ?? 0; const c = legend.get(comm) ?? { label: `Community ${comm}`, color: COLORS[comm % COLORS.length], count: 0 }; c.count++; if (c.label === `Community ${comm}`) c.label = n.node.label; legend.set(comm, c); } const legendArr = [...legend.entries()].sort((a, b) => b[1].count - a[1].count).slice(0, 50); const totalNodes = kg.nodes.size; const shownInfo = cappedNodes.length < nonFileNodes.length ? `Showing ${cappedNodes.length} of ${nonFileNodes.length} nodes (top by connections)` : `${cappedNodes.length} nodes`; return ` ${title} - Mind Place
`; }