I am trying to implement a simple force layout in which nodes (without links) can be dynamically added and removed. I was successful in implementing the concept in D3 version 3, but I am unable to translate it to version 4. After adding and updating nodes the simulation freezes and ining circles are drawn in the upper left corner of the svg. Does someone knows why this is the case? Thanks for any help :)
My concept is based on this solution: Adding new nodes to Force-directed layout
JSFiddle: working code in d3 v3
/* Define class */
class Planet {
constructor(selector) {
this.w = $(selector).innerWidth();
this.h = $(selector).innerHeight();
this.svg = d3.select(selector)
.append('svg')
.attr('width', this.w)
.attr('height', this.h);
this.force = d3.layout.force()
.gravity(0.05)
.charge(-100)
.size([this.w, this.h]);
this.nodes = this.force.nodes();
}
/* Methods (are called on object) */
update() {
/* Join selection to data array -> results in three new selections enter, update and exit */
const circles = this.svg.selectAll('circle')
.data(this.nodes, d => d.id); // arrow function, function(d) { return d.y;}
/* Add missing elements by calling append on enter selection */
circles.enter()
.append('circle')
.attr('r', 10)
.style('fill', 'steelblue')
.call(this.force.drag);
/* Remove surplus elements from exit selection */
circles.exit()
.remove();
this.force.on('tick', () => {
circles.attr('cx', d => d.x)
.attr('cy', d => d.y);
});
/* Restart the force layout */
this.force.start();
}
addThought(content) {
this.nodes.push({ id: content });
this.update();
}
findThoughtIndex(content) {
return this.nodes.findIndex(node => node.id === content);
}
removeThought(content) {
const index = this.findThoughtIndex(content);
if (index !== -1) {
this.nodes.splice(index, 1);
this.update();
}
}
}
/* Instantiate class planet with selector and initial data*/
const planet = new Planet('.planet');
planet.addThought('Hallo');
planet.addThought('Ballo');
planet.addThought('Yallo');
This is my intent of translating the code into v4:
/* Define class */
class Planet {
constructor(selector) {
this.w = $(selector).innerWidth();
this.h = $(selector).innerHeight();
this.svg = d3.select(selector)
.append('svg')
.attr('width', this.w)
.attr('height', this.h);
this.simulation = d3.forceSimulation()
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(this.w / 2, this.h / 2));
this.nodes = this.simulation.nodes();
}
/* Methods (are called on object) */
update() {
/* Join selection to data array -> results in three new selections enter, update and exit */
let circles = this.svg.selectAll('circle')
.data(this.nodes, d => d.id); // arrow function, function(d) { return d.y;}
/* Add missing elements by calling append on enter selection */
const circlesEnter = circles.enter()
.append('circle')
.attr('r', 10)
.style('fill', 'steelblue');
circles = circlesEnter.merge(circles);
/* Remove surplus elements from exit selection */
circles.exit()
.remove();
this.simulation.on('tick', () => {
circles.attr('cx', d => d.x)
.attr('cy', d => d.y);
});
/* Assign nodes to simulation */
this.simulation.nodes(this.nodes);
/* Restart the force layout */
this.simulation.restart();
}
addThought(content) {
this.nodes.push({ id: content });
this.update();
}
findThoughtIndex(content) {
return this.nodes.findIndex(node => node.id === content);
}
removeThought(content) {
const index = this.findThoughtIndex(content);
if (index !== -1) {
this.nodes.splice(index, 1);
this.update();
}
}
}
I am trying to implement a simple force layout in which nodes (without links) can be dynamically added and removed. I was successful in implementing the concept in D3 version 3, but I am unable to translate it to version 4. After adding and updating nodes the simulation freezes and ining circles are drawn in the upper left corner of the svg. Does someone knows why this is the case? Thanks for any help :)
My concept is based on this solution: Adding new nodes to Force-directed layout
JSFiddle: working code in d3 v3
/* Define class */
class Planet {
constructor(selector) {
this.w = $(selector).innerWidth();
this.h = $(selector).innerHeight();
this.svg = d3.select(selector)
.append('svg')
.attr('width', this.w)
.attr('height', this.h);
this.force = d3.layout.force()
.gravity(0.05)
.charge(-100)
.size([this.w, this.h]);
this.nodes = this.force.nodes();
}
/* Methods (are called on object) */
update() {
/* Join selection to data array -> results in three new selections enter, update and exit */
const circles = this.svg.selectAll('circle')
.data(this.nodes, d => d.id); // arrow function, function(d) { return d.y;}
/* Add missing elements by calling append on enter selection */
circles.enter()
.append('circle')
.attr('r', 10)
.style('fill', 'steelblue')
.call(this.force.drag);
/* Remove surplus elements from exit selection */
circles.exit()
.remove();
this.force.on('tick', () => {
circles.attr('cx', d => d.x)
.attr('cy', d => d.y);
});
/* Restart the force layout */
this.force.start();
}
addThought(content) {
this.nodes.push({ id: content });
this.update();
}
findThoughtIndex(content) {
return this.nodes.findIndex(node => node.id === content);
}
removeThought(content) {
const index = this.findThoughtIndex(content);
if (index !== -1) {
this.nodes.splice(index, 1);
this.update();
}
}
}
/* Instantiate class planet with selector and initial data*/
const planet = new Planet('.planet');
planet.addThought('Hallo');
planet.addThought('Ballo');
planet.addThought('Yallo');
This is my intent of translating the code into v4:
/* Define class */
class Planet {
constructor(selector) {
this.w = $(selector).innerWidth();
this.h = $(selector).innerHeight();
this.svg = d3.select(selector)
.append('svg')
.attr('width', this.w)
.attr('height', this.h);
this.simulation = d3.forceSimulation()
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(this.w / 2, this.h / 2));
this.nodes = this.simulation.nodes();
}
/* Methods (are called on object) */
update() {
/* Join selection to data array -> results in three new selections enter, update and exit */
let circles = this.svg.selectAll('circle')
.data(this.nodes, d => d.id); // arrow function, function(d) { return d.y;}
/* Add missing elements by calling append on enter selection */
const circlesEnter = circles.enter()
.append('circle')
.attr('r', 10)
.style('fill', 'steelblue');
circles = circlesEnter.merge(circles);
/* Remove surplus elements from exit selection */
circles.exit()
.remove();
this.simulation.on('tick', () => {
circles.attr('cx', d => d.x)
.attr('cy', d => d.y);
});
/* Assign nodes to simulation */
this.simulation.nodes(this.nodes);
/* Restart the force layout */
this.simulation.restart();
}
addThought(content) {
this.nodes.push({ id: content });
this.update();
}
findThoughtIndex(content) {
return this.nodes.findIndex(node => node.id === content);
}
removeThought(content) {
const index = this.findThoughtIndex(content);
if (index !== -1) {
this.nodes.splice(index, 1);
this.update();
}
}
}
Share
Improve this question
edited May 23, 2017 at 12:16
CommunityBot
11 silver badge
asked Aug 28, 2016 at 13:18
palermopalermo
611 silver badge4 bronze badges
1
- I'm currently stuck with a similar issue, but with the canvas. I don't have a solution yet, but I think your problem is highlighted in the post you shared: "The second critical part is that the node and link arrays must be based on the force() -- otherwise the graph and model will go out of synch as nodes are deleted and added." You adding the nodes, but you are not defining the coordinates. – Design by Adrian Commented Aug 30, 2016 at 13:08
1 Answer
Reset to default 5Please see plunkr example
I'm using canvas, but the theory is the same:
You have to give your new array of nodes and links to D3 core functions first, before adding them to the original array.
drawData: function(graph){
var countExtent = d3.extent(graph.nodes,function(d){return d.connections}),
radiusScale = d3.scalePow().exponent(2).domain(countExtent).range(this.nodes.sizeRange);
// Let D3 figure out the forces
for(var i=0,ii=graph.nodes.length;i<ii;i++) {
var node = graph.nodes[i];
node.r = radiusScale(node.connections);
node.force = this.forceScale(node);
};
// Concat new and old data
this.graph.nodes = this.graph.nodes.concat(graph.nodes);
this.graph.links = this.graph.links.concat(graph.links);
// Feed to simulation
this.simulation
.nodes(this.graph.nodes);
this.simulation.force("link")
.links(this.graph.links);
this.simulation.alpha(0.3).restart();
}
Afterwards, tell D3 to restart with the new data.
When D3 calls your tick()
function, it already knows what coordinates you need to apply to your SVG elements.
ticked: function(){
if(!this.graph) {
return false;
}
this.context.clearRect(0,0,this.width,this.height);
this.context.save();
this.context.translate(this.width / 2, this.height / 2);
this.context.beginPath();
this.graph.links.forEach((d)=>{
this.context.moveTo(d.source.x, d.source.y);
this.context.lineTo(d.target.x, d.target.y);
});
this.context.strokeStyle = this.lines.stroke.color;
this.context.lineWidth = this.lines.stroke.thickness;
this.context.stroke();
this.graph.nodes.forEach((d)=>{
this.context.beginPath();
this.context.moveTo(d.x + d.r, d.y);
this.context.arc(d.x, d.y, d.r, 0, 2 * Math.PI);
this.context.fillStyle = d.colour;
this.context.strokeStyle =this.nodes.stroke.color;
this.context.lineWidth = this.nodes.stroke.thickness;
this.context.fill();
this.context.stroke();
});
this.context.restore();
}
Plunkr example