Mike Bostock has an example regarding updating a force layout. The example is based on v3 — how can the same functionality be replicated in v4?
Here's my (pitiful) attempt.
I've read the changes to selections in the v4 Changelog, but the merge
call is still confusing. In particular, it's not clear to me how the data join interacts with the simulation nodes()
and links()
call.
var width = 300,
height = 200;
var color = d3.scaleOrdinal(d3.schemeCategory20);
var nodes = [],
links = [];
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
var svg = d3.select("svg");
var linkLayer = svg.append('g').attr('id','link-layer');
var nodeLayer = svg.append('g').attr('id','node-layer');
// 1. Add three nodes and three links.
setTimeout(function() {
var a = {id: "a"}, b = {id: "b"}, c = {id: "c"};
nodes.push(a, b, c);
links.push({source: a, target: b}, {source: a, target: c}, {source: b, target: c});
start();
}, 0);
// 2. Remove node B and associated links.
setTimeout(function() {
nodes.splice(1, 1); // remove b
links.shift(); // remove a-b
links.pop(); // remove b-c
start();
}, 2000);
// Add node B back.
setTimeout(function() {
var a = nodes[0], b = {id: "b"}, c = nodes[1];
nodes.push(b);
links.push({source: a, target: b}, {source: b, target: c});
start();
}, 4000);
function start() {
var link = linkLayer.selectAll(".link")
.data(links, function(d) { return d.source.id + "-" + d.target.id; });
link.enter().append("line")
.attr("class", "link");
link.exit().remove();
var node = nodeLayer.selectAll(".node")
.data(nodes, function(d) { return d.id;});
node.enter().append("circle")
.attr("class", function(d) { return "node " + d.id; })
.attr("r", 8);
node.exit().remove();
simulation
.nodes(nodes)
.on("tick", tick);
simulation.force("link")
.links(links);
}
function tick() {
nodeLayer.selectAll('.node').attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
linkLayer.selectAll('.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; });
}
.link {
stroke: #000;
stroke-width: 1.5px;
}
.node {
fill: #000;
stroke: #fff;
stroke-width: 1.5px;
}
.node.a { fill: #1f77b4; }
.node.b { fill: #ff7f0e; }
.node.c { fill: #2ca02c; }
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="300px" height="200px"></svg>
So you don't actually need a d3-selection-merge to make your example work. The reason is that your node positions and links are being updated by the simulation. So you will want to add the nodes and links on start, but any updating to the positions will happen at the end of the start method when the simulation is started.
One major flaw with your original code was that you called svg.selectAll('.node') & svg.selectAll('.link') at the initial stages of the script. When you do this, there aren't any nodes or links bound to the svg, so you get a d3-selection with empty DOM elements. This is fine when you want to add elements with enter().append() - however when removing elements this will not work. Using that same, stale d3-selection to remove elements with exit().remove() does not work b/c there aren't any DOM elements in the d3-selection to remove. You need to call svg.selectAll() each time to get the DOM elements that are currently in the svg.
I also made a few minor changes in your code. You usually want the links to always show below the nodes. As you're adding elements to an SVG, the most recently added nodes are placed at the top. However if you add groups before hand (as I did with linkLayer & nodeLayer in the code), any newly added links will appear below any items in the nodeLayer group.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With