chart = {
const height =600;
const svg = d3.create("svg").attr("viewBox", [-width / 2, -height / 2, width, height]);
const tooltip = d3.select("body").append("div")
.attr("id", "tooltip")
.style("position", "absolute")
.style("transition", "opacity .1s")
.style("opacity", "0")
.style("visibility", "hidden")
.style("background-color", "white")
.style("padding", "8px")
.style("border", "1px solid #eee")
.style("pointer-events", "none");
const edges = graph.edges
.map(e => ({...e}))
;
const depthToY = d => d.randId * range - range/2;
function getDepth(node) {
if (isRoot(node)) { return 0; }
const incomingEdge = graph.edges.find(e => e.target === node.id);
const parent = graph.nodes.find(n => n.id === incomingEdge.source);
return getDepth(parent) + 1;
}
function isRoot(node) {
return graph.edges.every(e => e.target !== node.id);
}
const randoms = Array.from({length: graph.nodes.length}, () => Math.random());
const nodes = graph.nodes
.map(n => ({...n}))
.map((n,i) => ({
...n,
incomingEdges: graph.edges.filter(e => e.target === n.id).length,
outgoingEdges: graph.edges.filter(e => e.source === n.id).length,
isRoot: isRoot(n),
isLeaf: graph.edges.every(e => e.source !== n.id),
onlyHasOneOutput: graph.edges.filter(e => e.source === n.id).length === 1,
depth: getDepth(n),
randId: randoms[i]
}));
const maxDepth = graph.nodes.reduce((max, node) => node.depth > max ? node.depth : max, nodes[0].depth);
function setTooltip(annotations, [x,y]) {
if (!annotations) return;
tooltip.style("visibility", "visible")
.style("opacity", "1")
.html(`${Object.entries(annotations).map(([k,v])=> k + ":" + v +"\n")}`) // Dynamic content
.style("left", (x + 10) + "px")
.style("top", (y + 10) + "px");
}
const linkSelection = svg
.append("g")
.attr("stroke", "#999")
.selectAll("line")
.data(edges)
.join("g")
.call(edge)
.call(e =>
e.on("mouseover", function(e,d) {
setTooltip(d.annotations, [e.pageX, e.pageY]);
})
.on("mouseout", function() {
tooltip.style("visibility", "hidden")
})
.style("cursor", d => d.annotations ? "pointer" : "not-allowed")
)
;
const nodeSelection = svg
.append("g")
.selectAll("g")
.data(nodes)
.join("g")
.call(node)
;
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(edges).id(d => d.id))
.force("center", d3.forceCenter(0,0).strength(.05))
//.force("collide", d3.forceCollide().radius(d => (1-(d.depth / maxDepth))*collide_mult))
.force("manyBody", d3.forceManyBody().strength(-100 * collide_mult))
//.force("x", d3.forceX(-1000).strength(.001))
//.force("forceY", d3.forceY(d => 400 * d.depth - 800))
.alphaDecay(0)
;
let time = 0;
simulation.on("tick", () => {
time++;
nodeSelection
.attr("transform", d => `translate(${d.x}, ${(d.y)})`)
;
linkSelection.selectAll("line")
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y)
const prog = 1 - (time/47 % 1);
linkSelection.selectAll("circle")
.attr("r", 2)
.attr("cx", d => d.target.x + (d.source.x - d.target.x) * prog)
.attr("cy", d => d.target.y + (d.source.y - d.target.y) * prog)
.attr("fill", d => "black");
if (time === stopFrame) {
simulation.force("collide", null);
simulation.force("x", null);
simulation.force("link", null);
simulation.force("manyBody", null);
}
});
invalidation.then(() => simulation.stop());
return { simulation, svg, edges, nodes, time };
}