import { MultiDirectedGraph } from "graphology"; import Sigma from "sigma"; import { Coordinates, EdgeDisplayData, NodeDisplayData } from "sigma/types"; // Nice soft greens const colors = ["#E8F9C2", "#C0DF81", "#96C832", "#59761E", "#263409"]; interface Node { id: string; label: string; x: number; y: number; size: number; color: string; } interface Link { label: string; size: number; traceId: string; source: string; target: string; } interface Data { nodes: Node[]; links: Link[]; } interface State { hoveredTrace: string | null; hoveredNode: string | null; } const state: State = { hoveredTrace: null, hoveredNode: null, }; const drawSigma = (data: Data) => { // Create a graphology graph const graph = new MultiDirectedGraph(); // Add nodes data.nodes.forEach((n) => { try { graph.addNode(n.id, n); } catch (e) { // Duplicate node found, which is correct in our data scenario. } }); // Add links data.links.forEach((l) => { graph.addEdge(l.source, l.target, l); }); const container = document.getElementById("container") as HTMLElement; // Instantiate sigma.js and render the graph const renderer = new Sigma(graph, container, { // labelThreshold: -10000, renderEdgeLabels: true, }); // Bind graph interactions: renderer.on("enterNode", ({ node }) => { console.log("enter:", node); if (!node) { return; } const props = graph.getNodeAttributes(node); state.hoveredNode = node; state.hoveredTrace = props.traceId; // Compute the partial that we need to re-render to optimize the refresh const nodes = graph.filterNodes((n) => { const np = graph.getNodeAttributes(n); return np.traceId === state.hoveredTrace; }); const nodesIndex = new Set(nodes); const edges = graph.filterEdges((e) => graph.extremities(e).some((n) => nodesIndex.has(n)), ); // Refresh rendering renderer.refresh({ partialGraph: { nodes, edges, }, // We don't touch the graph data so we can skip its reindexation skipIndexation: true, }); }); renderer.on("leaveNode", () => { state.hoveredNode = null; state.hoveredTrace = null; // Refresh rendering renderer.refresh({ // We don't touch the graph data so we can skip its reindexation skipIndexation: true, }); }); // Render nodes accordingly to the internal state: // 1. If a node is selected, it is highlighted // 2. If there is query, all non-matching nodes are greyed // 3. If there is a hovered node, all non-neighbor nodes are greyed renderer.setSetting("nodeReducer", (node, data) => { const res = { ...data }; const props = graph.getNodeAttributes(node); if (state.hoveredTrace && props.traceId !== state.hoveredTrace) { res.label = ""; res.color = "#f6f6f6"; } return res; }); // Render edges accordingly to the internal state: // 1. If a node is hovered, the edge is hidden if it is not connected to the // node // 2. If there is a query, the edge is only visible if it connects two // suggestions renderer.setSetting("edgeReducer", (edge, data) => { const res = { ...data }; const props = graph.getEdgeAttributes(edge); if (state.hoveredTrace && props.traceId !== state.hoveredTrace) { res.hidden = true; } return res; }); }; // const getNodeID = (hop: number, prevId: string, trace) => { // if (prevId === null) { // return trace.origin; // } // if (hop.name === "*") { // return `${trace.target}-${hop.number}-*`; // //return `${trace.id}-${hop.number}-*`; // } // // return hop.ip; // }; // const parseNodesAndLinks = (traces) => { // const nodes = []; // const links = []; // // const colors = Array.from(new Set(traces.map((t) => t.id))); // // TODO: Replace this with something // const color = d3.scaleOrdinal(colors, d3.schemeCategory10); // // traces.forEach((trace) => { // let prevId = null; // let latestNumber = null; // // trace.hops.forEach((hop) => { // const id = getNodeID(hop, prevId, trace); // // // New node // nodes.push({ // id: id, // label: id.endsWith("*") ? "*" : id, // x: trace.id, // y: hop.number / 2, // traceId: trace.id, // size: 9, // labelSize: 30, // color: color(trace.id), // }); // // if (prevId) { // // New link // links.push({ // label: hop.link_latency, // source: prevId, // target: id, // traceId: trace.id, // origin: trace.origin, // size: 3, // color: color(trace.id), // }); // } // // prevId = id; // latestNumber = hop.number; // }); // // /** // * Last "destination" node, just for candy // */ // nodes.push({ // id: trace.id, // label: trace.target, // traceId: trace.id, // x: trace.id, // y: (latestNumber + 1) / 2, // size: 8, // color: "black", // }); // // links.push({ // label: "-", // size: 8, // traceId: trace.id, // source: prevId, // target: trace.id, // }); // }); // // // { id: ip, group: origin, radius: 2 } // // { source: prev.ip, target: ip, value: latency } // return { // nodes, // links, // }; // }; async function main() { const response = await fetch("/trace/"); const data = await response.json(); console.log("Traces:", data); console.log("Data:", data); drawSigma(data); } main();