在版本 4 中将节点动态添加到 D3 Force Layout

Posted

技术标签:

【中文标题】在版本 4 中将节点动态添加到 D3 Force Layout【英文标题】:Adding nodes dynamically to D3 Force Layout in version 4 【发布时间】:2017-01-04 14:59:51 【问题描述】:

我正在尝试实现一个简单的强制布局,其中可以动态添加和删除节点(没有链接)。我在 D3 版本 3 中成功实现了这个概念,但我无法将其转换为版本 4。添加和更新节点后,模拟冻结,传入的圆圈绘制在 svg 的左上角。有人知道为什么会这样吗?感谢您的帮助:)

我的概念是基于这个解决方案: 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');

这是我将代码翻译成 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();
    
  

【问题讨论】:

我目前遇到了类似的问题,但在画布上。我还没有解决方案,但我认为您分享的帖子中突出显示了您的问题:“第二个关键部分是节点和链接数组必须基于 force() -- 否则图表当节点被删除和添加时,模型将不同步。” 您添加了节点,但您没有定义坐标。 【参考方案1】:

请see plunkr example

我用的是canvas,但是原理是一样的:

在将它们添加到原始数组之前,您必须先提供 new 节点数组和指向 D3 核心函数的链接。

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();

然后,告诉 D3 使用新数据重新启动。

当 D3 调用您的 tick() 函数时,它已经知道您需要将什么坐标应用于 SVG 元素。

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

【讨论】:

以上是关于在版本 4 中将节点动态添加到 D3 Force Layout的主要内容,如果未能解决你的问题,请参考以下文章

d3的4.x与3.x版本的区别

d3的4.x与3.x版本的区别

d3-js 的 Force-Directed Layout 是不是支持图像作为节点?

在 D3js 力图中添加和删除节点

在 d3.js 中将节点和链接转换为分层树

在 D3 中将 css 附加到 svg