Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Adding nodes dynamically to D3 Force Layout in version 4

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 incoming 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();
    }
  }
}
like image 841
palermo Avatar asked Aug 28 '16 13:08

palermo


1 Answers

Please 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

like image 97
Design by Adrian Avatar answered Nov 04 '22 22:11

Design by Adrian