traceroute_map/app/static/index.js
2024-06-01 19:43:18 +03:00

313 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const linkArc = (d) => {
const r = Math.hypot(d.target.x - d.source.x, d.target.y - d.source.y);
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 its 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 doesnt
// // really matter since the target alpha is zero and the simulation will
// // stop naturally, but its a good practice.)
// // invalidation.then(() => simulation.stop());
//
// return svg.node();
// };
const drawChart2 = (data) => {
const width = window.visualViewport.width;
const height = window.visualViewport.height - 10;
const colors = Array.from(new Set(data.map((d) => d.traceId)));
const nodes = Array.from(
new Set(data.flatMap((l) => [l.source, l.target])),
(id) => ({ id }),
);
const links = data.map((d) => Object.create(d));
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("viewBox", [-width / 2, -height / 2, width, height])
.attr("width", width)
.attr("height", height)
.attr("style", "max-width: 100%; height: auto; font: 14px monospace;");
// Per-type markers, 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", 6)
.attr("markerHeight", 6)
.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
.append("circle")
.attr("stroke", "white")
.attr("stroke-width", 1.5)
.attr("r", 4);
node
.append("text")
.attr("x", 8)
.attr("y", "0.31em")
.text((d) => (d.id.endsWith("*") ? "*" : d.id))
.clone(true)
.lower()
.attr("fill", "none")
.attr("stroke", "white")
.attr("stroke-width", 3);
simulation.on("tick", () => {
link.attr("d", linkArc);
node.attr("transform", (d) => `translate(${d.x},${d.y})`);
});
// invalidation.then(() => simulation.stop());
return Object.assign(svg.node(), { scales: { color } });
};
const getId = (hop, prevId, trace) => {
if (prevId === null) {
return trace.origin;
}
if (hop.name === "*") {
return `${trace.id}-${hop.number}-*`;
}
return hop.ip;
};
const parseNodesAndLinks = (traces) => {
const result = {
nodes: [],
links: [],
};
traces.forEach((trace) => {
let prevId = null;
trace.hops.forEach((hop) => {
const id = getId(hop, prevId, trace);
// New node
result.nodes.push({
id: id,
radius: 8,
value: hop.name || "name?",
origin: trace.origin,
});
if (prevId) {
// New link
result.links.push({
source: prevId,
target: id,
traceId: trace.id,
origin: trace.origin,
});
}
prevId = id;
});
// Last "destination" node
result.nodes.push({
id: trace.id,
radius: 8,
value: trace.target,
origin: trace.origin,
});
if (prevId) {
// New link
result.links.push({
source: prevId,
target: trace.target,
traceId: trace.id,
origin: trace.origin,
});
}
});
// { id: ip, group: origin, radius: 2 }
// { source: prev.ip, target: ip, value: latency }
return result;
};
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);
container.append(chart);
}
main();