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 drawSigma = (data) => { // Create a graphology graph const graph = new graphology.MultiDirectedGraph(); data.nodes.forEach((n, i) => { console.log("Node:", n); try { graph.addNode(n.id, n); } catch (e) { console.log("Node add:", e); } }); data.links.forEach((l, i) => { graph.addEdge(l.source, l.target, l); }); // Instantiate sigma.js and render the graph const renderer = new Sigma(graph, document.getElementById("container"), { labelThreshold: -10000, }); }; const getNodeID = (hop, prevId, trace) => { if (prevId === null) { return trace.origin; } if (hop.name === "*") { 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, size: 9, labelSize: 30, color: color(trace.id), }); if (prevId) { // New link links.push({ label: "asd", source: prevId, target: id, traceId: trace.id, origin: trace.origin, size: 1, color: color(trace.id), }); } prevId = id; latestNumber = hop.number; }); /** * Last "destination" node, just for candy */ nodes.push({ id: trace.id, label: trace.target, x: trace.id, y: (latestNumber + 1) / 2, size: 8, color: "black", }); links.push({ 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 traces = await 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();