I am trying to dynamically add nodes to a forceSimulation in d3. The code snippet below should first generate the nodes from a static object graph
and then after 5 seconds a new node should be added to graph
and the Force-directed graph in d3 should be updated with the new node. But there is an error in my implementation and instead all the nodes jump to the top-left corner. My current "solution" uses the merge
syntax as described in another similar SO question: https://stackoverflow.com/a/40028002/14923227 and here. I believe this is the right way to go about it but I am not sure how to implement it with my contrived example below.
var graph = {
"nodes": [
{
"id": 1,
"name": "A"
},
{
"id": 2,
"name": "B"
},
{
"id": 3,
"name": "C"
}
],
"links": [
{
"source": 0,
"target": 1
},
{
"source": 1,
"target": 2
},
{
"source": 0,
"target": 2
}
]
};
var svg = d3.select("svg"),
width = svg.attr("width"),
height = svg.attr("height");
var simulation = d3.forceSimulation()
.force("charge", d3.forceManyBody().strength(-400))
.force("center", d3.forceCenter(width / 2, height / 2))
.nodes(graph.nodes)
.on("tick", ticked)
.force("link", d3.forceLink(graph.links))
var link = svg.selectAll(".link")
.data(graph.links)
.join("line")
.classed("link", true)
.style("stroke", "#aaa")
var node = svg.selectAll(".node")
.data(graph.nodes);
var g = node.enter()
.append('g')
.attr('class', 'node');
g.append('circle')
.attr("r", 20)
.style("fill", "#d9d9d9");
g.append('text')
.attr('class', 'text')
.text(function(d) { return d.name });
node.exit().remove()
function ticked() {
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; });
svg.selectAll('.node')
.attr("cx", function (d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr('transform', function(d) {
return 'translate(' d.x ',' d.y ')';
})
}
// problem here: add new node after five seconds
setTimeout(() => {
graph.nodes.push({"id": 4, "name": "X"}); // new node 'X'
// this line gives error
//graph.links.push({"source": 2, "target": 3}); // connect 'X' with 'C'
// Update the nodes
node = node.data(graph.nodes);
// Enter any new nodes
nodeEnter = node.enter()
.append("g")
.attr('class', 'node');
nodeEnter.append('circle')
.attr("r", 20)
.style("fill", "#d9d9d9");
nodeEnter.append('text')
.attr('class', 'text')
.text(function(d) { return d.name });
node = nodeEnter.merge(node);
nodeEnter.exit().remove();
simulation.restart();
}, 5000)
<!DOCTYPE html>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v7.min.js"></script>
<body>
<svg width="960" height="600"></svg>
</body>
CodePudding user response:
While the enter method will create new elements in the DOM, it won't add new elements to the simulation. You'll need to specify a new nodes array to update the simulation:
simulation.nodes(graph.nodes);
Of course, since you are using a centering force this will cause the other nodes to jump away (the centering force does not pull nodes to a center, it keeps their center of gravity at the center). You'll want to do something like set the x,y properties of the new node to something sensible, I've set it to the average value of all the other nodes, which won't upset the centering force:
// sum x,y values, find mean for all nodes:
var sx = 0;
var sy = 0;
graph.nodes.forEach(function(node) { sx = node.x; sy = node.y })
var mx = sx / graph.nodes.length;
var my = sy / graph.nodes.length;
// add new node
graph.nodes.push({"id": 4, "name": "X", x: mx, y: my}); // new node 'X'
And as restart won't reheat the simulation, I've reheated it with some more alpha on restart. All together, that gives us:
var graph = {
"nodes": [
{
"id": 1,
"name": "A"
},
{
"id": 2,
"name": "B"
},
{
"id": 3,
"name": "C"
}
],
"links": [
{
"source": 0,
"target": 1
},
{
"source": 1,
"target": 2
},
{
"source": 0,
"target": 2
}
]
};
var svg = d3.select("svg"),
width = svg.attr("width"),
height = svg.attr("height");
var simulation = d3.forceSimulation()
.force("charge", d3.forceManyBody().strength(-400))
.force("center", d3.forceCenter(width / 2, height / 2))
.nodes(graph.nodes)
.on("tick", ticked)
.force("link", d3.forceLink(graph.links))
var link = svg.selectAll(".link")
.data(graph.links)
.join("line")
.classed("link", true)
.style("stroke", "#aaa")
var node = svg.selectAll(".node")
.data(graph.nodes);
var g = node.enter()
.append('g')
.attr('class', 'node');
g.append('circle')
.attr("r", 20)
.style("fill", "#d9d9d9");
g.append('text')
.attr('class', 'text')
.text(function(d) { return d.name });
node.exit().remove()
function ticked() {
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; });
svg.selectAll('.node')
.attr("cx", function (d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr('transform', function(d) {
return 'translate(' d.x ',' d.y ')';
})
}
// problem here: add new node after five seconds
setTimeout(() => {
var sx = 0;
var sy = 0;
graph.nodes.forEach(function(node) { sx = node.x; sy = node.y })
var mx = sx / graph.nodes.length;
var my = sy / graph.nodes.length;
graph.nodes.push({"id": 4, "name": "X", x: mx, y: my}); // new node 'X'
// this line gives error
//graph.links.push({"source": 2, "target": 3}); // connect 'X' with 'C'
// Update the nodes
node = node.data(graph.nodes);
simulation.nodes(graph.nodes);
// Enter any new nodes
nodeEnter = node.enter()
.append("g")
.attr('class', 'node');
nodeEnter.append('circle')
.attr("r", 20)
.style("fill", "#d9d9d9");
nodeEnter.append('text')
.attr('class', 'text')
.text(function(d) { return d.name });
node = nodeEnter.merge(node);
nodeEnter.exit().remove();
simulation.alpha(0.3).restart();
}, 5000)
<!DOCTYPE html>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v7.min.js"></script>
<body>
<svg width="400" height="200"></svg>
</body>