"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; const linkArc = (d) => { const r = Math.hypot(d.target.x - d.source.x, d.target.y - d.source.y) * 3; return ` M${d.source.x},${d.source.y} A${r},${r} 0 0,1 ${d.target.x},${d.target.y} `; }; const drag = (simulation) => { function dragstarted(event, d) { if (!event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(event, d) { d.fx = event.x; d.fy = event.y; } function dragended(event, d) { if (!event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; } return d3 .drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended); }; // const drawChart = (data) => { // // Specify the dimensions of the chart. // const width = 1600; // const height = 1200; // // // Specify the color scale. // const color = d3.scaleOrdinal(d3.schemeCategory10); // // // The force simulation mutates links and nodes, so create a copy // // so that re-evaluating this cell produces the same result. // const links = data.links.map((d) => ({ ...d })); // const nodes = data.nodes.map((d) => ({ ...d })); // // // Create a simulation with several forces. // const simulation = d3 // .forceSimulation(nodes) // .force( // "link", // d3.forceLink(links).id((d) => d.id), // ) // .force("charge", d3.forceManyBody()) // .force("x", d3.forceX()) // .force("y", d3.forceY()); // // // Create the SVG container. // const svg = d3 // .create("svg") // .attr("width", width) // .attr("height", height) // .attr("viewBox", [-width / 2, -height / 2, width, height]) // .attr("style", "max-width: 100%; height: auto;"); // // // Add a line for each link, and a circle for each node. // const link = svg // .append("g") // .attr("stroke", "#999") // .attr("stroke-opacity", 0.6) // .selectAll("line") // .data(links) // .join("line") // .attr("stroke-width", 1); // (d) => Math.sqrt(d.value)); // // const node = svg // .append("g") // .attr("stroke", "#fff") // .attr("stroke-width", 1.5) // .selectAll("circle") // .data(nodes) // .join("circle") // .attr("r", 5) // .attr("fill", (d) => color(d.group)); // // node.append("title").text((d) => d.id); // // // Add a drag behavior. // node.call( // d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended), // ); // // // Set the position attributes of links and nodes each time the simulation ticks. // simulation.on("tick", () => { // link // .attr("x1", (d) => d.source.x) // .attr("y1", (d) => d.source.y) // .attr("x2", (d) => d.target.x) // .attr("y2", (d) => d.target.y); // // node.attr("cx", (d) => d.x).attr("cy", (d) => d.y); // }); // // // Reheat the simulation when drag starts, and fix the subject position. // function dragstarted(event) { // if (!event.active) simulation.alphaTarget(0.3).restart(); // event.subject.fx = event.subject.x; // event.subject.fy = event.subject.y; // } // // // Update the subject (dragged node) position during drag. // function dragged(event) { // event.subject.fx = event.x; // event.subject.fy = event.y; // } // // // Restore the target alpha so the simulation cools after dragging ends. // // Unfix the subject position now that it’s no longer being dragged. // function dragended(event) { // if (!event.active) simulation.alphaTarget(0); // event.subject.fx = null; // event.subject.fy = null; // } // // // When this cell is re-run, stop the previous simulation. (This doesn’t // // really matter since the target alpha is zero and the simulation will // // stop naturally, but it’s a good practice.) // // invalidation.then(() => simulation.stop()); // // return svg.node(); // }; // const drawChart2 = (data) => { // // Data parsing // const nodes = Array.from( // new Set(data.flatMap((l) => [l.source, l.target])), // (id) => ({ id }), // ); // const links = data.map((d) => Object.create(d)); // // // Styles // const width = window.visualViewport.width; // const height = window.visualViewport.height - 10; // // const colors = Array.from(new Set(data.map((d) => d.traceId))); // const color = d3.scaleOrdinal(colors, d3.schemeCategory10); // // const simulation = d3 // .forceSimulation(nodes) // .force( // "link", // d3.forceLink(links).id((d) => d.id), // ) // .force("charge", d3.forceManyBody().strength(-400)) // .force("x", d3.forceX()) // .force("y", d3.forceY()); // // // Canvas settings // const svg = d3 // .create("svg") // .attr("width", width) // .attr("height", height) // .attr("viewBox", [-width / 2, -height / 2, width, height]) // .attr("style", "max-width: 100%; height: auto; font: 14px monospace;"); // // // Pre-type arrowheads, as they don't inherit styles. // svg // .append("defs") // .selectAll("marker") // .data(colors) // .join("marker") // .attr("id", (d) => `arrow-${d}`) // .attr("viewBox", "0 -5 10 10") // .attr("refX", 15) // .attr("refY", -0.5) // .attr("markerWidth", 5) // .attr("markerHeight", 5) // .attr("orient", "auto") // .append("path") // .attr("fill", color) // .attr("d", "M0,-5L10,0L0,5"); // // const link = svg // .append("g") // .attr("fill", "none") // .attr("stroke-width", 1.5) // .selectAll("path") // .data(links) // .join("path") // .attr("stroke", (d) => color(d.traceId)) // .attr( // "marker-end", // (d) => `url(${new URL(`#arrow-${d.traceId}`, location)})`, // ); // // const node = svg // .append("g") // .attr("fill", "currentColor") // .attr("stroke-linecap", "round") // .attr("stroke-linejoin", "round") // .selectAll("g") // .data(nodes) // .join("g") // .call(drag(simulation)); // // // Node "icon" // node // .append("circle") // .attr("stroke", "white") // .attr("stroke-width", 1.5) // .attr("r", 5); // // // Node text // node // .append("text") // .attr("x", 8) // .attr("y", 4) // .text((d) => (d.id.endsWith("*") ? "*" : d.id)) // .clone(true) // .lower() // .attr("fill", "black") // .attr("stroke", "white") // .attr("stroke-width", 2); // // simulation.on("tick", () => { // link.attr("d", linkArc); // // link // // .attr("x1", (d) => d.source.x) // // .attr("y1", (d) => d.source.y) // // .attr("x2", (d) => d.target.x) // // .attr("y2", (d) => d.target.y); // node.attr("transform", (d) => `translate(${d.x},${d.y})`); // }); // // // invalidation.then(() => simulation.stop()); // // return Object.assign(svg.node(), { // scales: { color }, // }); // }; const state = {}; const drawSigma = (data) => { // Create a graphology graph const graph = new graphology.MultiDirectedGraph(); const setHoveredNode = (node) => { if (node) { 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, }); } else { state.hoveredNode = undefined; state.hoveredTrace = undefined; // Refresh rendering renderer.refresh({ // We don't touch the graph data so we can skip its reindexation skipIndexation: true, }); } }; data.nodes.forEach((n) => { try { graph.addNode(n.id, n); } catch (e) { // Duplicate node found, which is correct in our data scenario. } }); data.links.forEach((l) => { graph.addEdge(l.source, l.target, l); }); // Instantiate sigma.js and render the graph const renderer = new Sigma(graph, document.getElementById("container"), { // labelThreshold: -10000, renderEdgeLabels: true, }); // Bind graph interactions: renderer.on("enterNode", ({ node }) => { console.log("enter:", node); setHoveredNode(node); }); renderer.on("leaveNode", () => { setHoveredNode(undefined); }); // 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 = Object.assign({}, 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 = Object.assign({}, data); const props = graph.getEdgeAttributes(edge); if (state.hoveredTrace && props.traceId !== state.hoveredTrace) { res.hidden = true; } return res; }); }; const getNodeID = (hop, prevId, 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))); 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, }; }; function main() { return __awaiter(this, void 0, void 0, function* () { const response = yield fetch("/trace/"); const traces = yield response.json(); console.log("Traces:", traces); const data = parseNodesAndLinks(traces); console.log("Data:", data); // const chart = drawChart2(data.links); const chart = drawSigma(data); // container.append(chart); }); } main();