I need to make a "sticky force layout" with drag-able features, and tried to replicate the example of Mike Bostock at this link. Because the author wrote that program in D3 v3, I must "upgrade" his code to D3 v4. This means both d3-force and d3-drag statement must be changed accordingly. Though the logic flow in original example is quite easy to understand, I still cannot manage to make my own version (see the code below). Issue: After a certain amount of time (2 - 3 seconds), the nodes are dragged away but the links are not updated.
My initial thoughts were focused on drag-function (.call(drag)), but later I found that the "tick" function does not run anymore after the the amount of time mentioned above (I got it via putting a count variable inside tick function, then console.log it). Up to this point, my mind is empty, cannot explore further.
Where is the problem?
var width = 960,
height = 500;
var simulation = d3.forceSimulation()
.force("charge", d3.forceManyBody().strength(-100))
d3.forceX(width)
d3.forceY(height)
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var drag = d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
var link = svg.selectAll(".link")
var node = svg.selectAll(".node")
d3.json("graph.json", function (error, graph) {
if (error) throw error;
simulation.nodes(graph.nodes)
.force("link", d3.forceLink(graph.links))
.on("tick", tick);
link = link.data(graph.links)
.enter().append("line")
.attr("class", "link");
node = node.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 12)
.on("dblclick", dblclick)
.call(drag)
});
var count = 0;
function tick() {
count++;
console.log(count);
link.attr("x1", function (d) { return d.source.x; })
.attr("y1", function (d) { return d.source.y; })
.attr("x2", function (d) { return d.target.x; })
.attr("y2", function (d) { return d.target.y; });
node.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; })
}
function dblclick(d) {
d3.select(this).classed("fixed", d.fixed = false);
// console.log("Is clicking");
}
function dragstarted(d) {
d3.select(this).classed("fixed", d.fixed = true);
console.log("Is dragging");
}
function dragged(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}
function dragended(d) {
d3.select(this).classed("active", false);
}
There are two things that are causing problems in this force directed graph:
d.fixed
to fix nodes1: fixing nodes
While you don't expressly note this, in the 2-3 seconds the graph updates link data, the nodes still move after dragging.
This is because, as opposed to d3v3, you need to fix both the x and y values of a coordinate manually using d.fx
and d.fy
; d.fixed
no longer fixes your nodes. For your code this would look something like:
function dragended(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
and
function dblclick(d) {
d.fx = null;
d.fy = null;
}
2: setting alpha decay
This is causing your graph to freeze up. After the graph has stabilized due to alpha decay, the tick function is no longer called. At this point, your drags happen without any update to the links, resulting in severed connections between node and link.
In d3v3 alpha decay is undefined by default, and d3 likely falls back on a either not computing alpha decay or using zero as the alpha decay factor (either results in the same outcome). In d3v4, alpha decay is set at a non-zero value:
If decay is specified, sets the alpha decay rate to the specified number in the range [0,1] and returns this simulation. If decay is not specified, returns the current alpha decay rate, which defaults to 0.0228… = 1 - pow(0.001, 1 / 300) where 0.001 is the default minimum alpha.
The alpha decay rate determines how quickly the current alpha interpolates towards the desired target alpha; since the default target alpha is zero, by default this controls how quickly the simulation cools. Higher decay rates cause the simulation to stabilize more quickly, but risk getting stuck in a local minimum; lower values cause the simulation to take longer to run, but typically converge on a better layout. To have the simulation run forever at the current alpha, set the decay rate to zero; alternatively, set a target alpha greater than the minimum alpha.
So, you could just use:
var simulation = d3.forceSimulation()
.force("charge", d3.forceManyBody().strength(-100))
.alphaDecay(0);
(or alternatively, set the alphaTarget to an appropriate value as noted in the documentation quote above).
Altogether, this looks like this.