最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Adding nodes dynamically to D3 Force Layout in version 4 - Stack Overflow

programmeradmin1浏览0评论

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
Add a ment  | 

1 Answer 1

Reset to default 5

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

发布评论

评论列表(0)

  1. 暂无评论