241 lines
5.5 KiB
TypeScript
241 lines
5.5 KiB
TypeScript
|
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();
|