There is an example of force-directed graph i've tried to draw with the help of the d3.js.
I have 3 big questions at all. And this is the code (you can run code snippet below, it might works):
function getRandomInt(max, min = 0) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function fdSortShit(g, nodeDimensions) {
const gNodes = [];
const gLinks = [];
g.children().forEach(child => {
gNodes.push({
id: child,
w: nodeDimensions[child].w,
h: nodeDimensions[child].h,
radius:
Math.sqrt(
nodeDimensions[child].w * nodeDimensions[child].w + nodeDimensions[child].h * nodeDimensions[child].h
) / 2
});
});
g.edges().forEach(edge => {
gLinks.push({ source: edge.v, target: edge.w });
});
const data = {
nodes: gNodes,
links: gLinks
};
const nodes = data.nodes;
const links = data.links;
const linkNodeRad = 5;
const linkNodes = [];
links.forEach((link, idx) => {
if (link.source != link.target) {
linkNodes.push({
id: `link-node-${idx}`,
source: nodes.filter(e => {
return e.id == link.source;
})[0],
target: nodes.filter(e => {
return e.id == link.target;
})[0],
radius: linkNodeRad
});
}
});
const width = 800;
const height = 600;
var svg = d3
.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", "-400, -300, 800, 600");
function forceSimulation(nodes, links) {
return d3
.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter())
.force(
"collision",
d3.forceCollide().radius(function(d) {
return d.radius;
})
);
}
var link = svg
.selectAll(".link")
.attr("stroke", "#fff")
.data(links)
.enter()
.append("line")
.attr("class", "link");
var node = svg
.append("g")
.selectAll("g")
.data(nodes)
.enter()
.append("g");
var circles = node
.append("circle")
.attr("class", "node")
.attr("r", node => {
return node.radius;
});
var text = node
.append("text")
.text(d => {
return d.id;
})
.attr("class", "node-caption")
.attr("x", 0)
.attr("y", 0);
var linkNode = svg
.selectAll(".link-node")
.data(linkNodes)
.enter()
.append("circle")
.attr("class", "link-node")
.attr("r", linkNodeRad);
function ticked() {
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", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
linkNode
.attr("cx", function(d) {
return (d.x = (d.source.x + d.target.x) * 0.5);
})
.attr("cy", function(d) {
return (d.y = (d.source.y + d.target.y) * 0.5);
});
}
forceSimulation(nodes.concat(linkNodes), links)
.on("tick", ticked)
.on("end", () => {
console.warn("END");
});
}
const coords = {};
const size = { min: 10, max: 30 };
const dotStr = "graph g { a--a;a--b;a--b;a--c;a--d;a--e;b--b1;c--c1;c--c2;d--d1;d--d2;d--d3;d--d4;e--e1;v--w;v--x;v--y;w--z;w--w1;x--x1;x--x2;y--y1;y--y2;y--y3;y--y4;z--z1;v--a; }";
const g = graphlibDot.read(dotStr);
g.children().forEach(child => {
const x = getRandomInt(1024 - 10, 10);
const y = getRandomInt(768 - 10, 10);
coords[child] = {
x: x,
y: y,
w: getRandomInt(size.max, size.min),
h: getRandomInt(size.max, size.min)
};
});
fdSortShit(g, coords);
svg {
background-color: lightgray;
}
circle.node {
fill: lightcoral;
}
circle.link-node {
fill: rgba(0, 0, 255, 0.2);
/* fill: transparent; */
}
line.link {
stroke: lightseagreen;
}
text.node-caption {
font: normal 10px courier new;
}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/graphlib-dot.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
The image looks like this:
The first question is: What about to avoid this intersections?
I know that I can't dodge all of edge intersections but I want to minimize them. This example is a tree-graph with no cycles. I know that there is a way to build it without edge crossings. But I don't know how to do it with this algorithm.
But still annoying intersection.
The second question is: What about NOT to simulate forces in-time (I need no animation) but just to draw final result? When I use forceSimulation.on("end", cb)
it is great, but delay between start and stop is big.. but this is graph is just a small example. I can't wait so long on a bigger once.
And the third question is.. how to apply force-derected settings? Force energy, stiffness, repulsion, damping etc.? Can't find them on d3@5
The final result my project lead wants is:
- no node overlap;
- minimize edge-edge intersections;
- minimize edge-node intersections.
I'm ready for dialog.
You apply the force settings in the initialization portion. Here is an example -
This can be used to solve one of your problems...if you set the "charge" strength to some large negative number like -150, the nodes will be strongly repelled such that they don't overlap, nor do any of their links. If you aren't looking to drag the graph at all then this should be all you need to avoid overlaps.
A side effect of a highly negative charge is that the graph settles quite quickly, and as you don't want to simulate the forces in real time after the initial display, you can call
simulation.stop()
to freeze or stop the simulation.