React + D3 Force Layout:新链接有未定义的位置

时间:2017-05-05 16:33:29

标签: javascript node.js reactjs d3.js force-layout

我相信我已经遵循新React Props的一般更新模式。当收到新的道具时,D3会进行数据计算和渲染,这样React就不必渲染每个滴答。

D3适用于静态布局。但是当我在shouldComponentUpdate(nextProps)函数中收到新节点和链接时,节点缺少以下属性:

  • index - 节点数组中节点的从零开始的索引。
  • x - 当前节点位置的x坐标。
  • y - 当前节点位置的y坐标。

因此,所有新节点都有<g tranform=translate(undefined, undefined)/>,并且聚集在左上角。

我更新道具的方法是将新对象推送到nodes数组和链接数组。我不明白D3为什么不像在componentDidMount()的初始设置那样分配d.x和d.y。几天来我一直在努力解决这个问题。希望有人可以帮助我。

这是ForceLayout.jsx:

//React for structure - D3 for data calculation - D3 for rendering

import React from 'react';
import * as d3 from 'd3';

export default class ForceLayout extends React.Component{
  constructor(props){
    super(props);
  }

  componentDidMount(){ //only find the ref graph after rendering
    const nodes = this.props.nodes;
    const links = this.props.links;
    const width = this.props.width;
    const height = this.props.height;

    this.simulation = d3.forceSimulation(nodes)
      .force("link", d3.forceLink(links).distance(50))
      .force("charge", d3.forceManyBody().strength(-120))
      .force('center', d3.forceCenter(width / 2, height / 2));

    this.graph = d3.select(this.refs.graph);

    this.svg = d3.select('svg');
    this.svg.call(d3.zoom().on(
      "zoom", () => {
        this.graph.attr("transform", d3.event.transform)
      })
    );

    var node = this.graph.selectAll('.node')
      .data(nodes)
      .enter()
      .append('g')
      .attr("class", "node")
      .call(enterNode);

    var link = this.graph.selectAll('.link')
      .data(links)
      .enter()
      .call(enterLink);

    this.simulation.on('tick', () => {
      this.graph.call(updateGraph);
    });
  }

  shouldComponentUpdate(nextProps){
    //only allow d3 to re-render if the nodes and links props are different
    if(nextProps.nodes !== this.props.nodes || nextProps.links !== this.props.links){
      console.log('should only appear when updating graph');

      this.simulation.stop();
      this.graph = d3.select(this.refs.graph);

      var d3Nodes = this.graph.selectAll('.node')
        .data(nextProps.nodes);
      d3Nodes
        .enter() 
        .append('g')
        .attr("class", "node")
      .call(enterNode);
      d3Nodes.exit().remove(); //get nodes to be removed
      // d3Nodes.call(updateNode);

      var d3Links = this.graph.selectAll('.link')
        .data(nextProps.links);
      d3Links
        .enter()
        .call(enterLink);
      d3Links.exit().remove();
      // d3Links.call(updateLink);

      const newNodes = nextProps.nodes.slice(); //originally Object.assign({}, nextProps.nodes)
      const newLinks = nextProps.links.slice(); //originally Object.assign({}, nextProps.links)
      this.simulation.nodes(newNodes);
      this.simulation.force("link").links(newLinks);

      this.simulation.alpha(1).restart();

      this.simulation.on('tick', () => {
        this.graph.call(updateGraph);
      });
    }
    return false;
  }

  render(){
    return(
      <svg
        width={this.props.width}
        height={this.props.height}
        style={this.props.style}>
        <g ref='graph' />
      </svg>
    );
    }
  }

  /** d3 functions to manipulate attributes **/
  var enterNode = (selection) => {
    selection.append('circle')
      .attr('r', 10)
      .style('fill', '#888888')
      .style('stroke', '#fff')
      .style('stroke-width', 1.5);

    selection.append("text")
      .attr("x", function(d){return 20}) //
      .attr("dy", ".35em") // vertically centre text regardless of font size
      .text(function(d) { return d.word });
   };

   var enterLink = (selection) => {
      selection.append('line')
        .attr("class", "link")
        .style('stroke', '#999999')
        .style('stroke-opacity', 0.6);
   };

   var updateNode = (selection) => {
       selection.attr("transform", (d) => "translate(" + d.x + "," + d.y + ")");
    };

    var updateLink = (selection) => {
      selection.attr("x1", (d) => d.source.x)
        .attr("y1", (d) => d.source.y)
        .attr("x2", (d) => d.target.x)
        .attr("y2", (d) => d.target.y);
    };

    var updateGraph = (selection) => {
       selection.selectAll('.node')
          .call(updateNode);
       selection.selectAll('.link')
          .call(updateLink);
    };

我尝试在shouldComponentUpdate()函数中将新节点推入节点数组,而不是修改服务器中的数组。但是新节点仍然出现在左上角,位置不确定。所以我猜我的问题是在shouldComponentUpdate()。非常感谢任何帮助!!

编辑:发现Object.assign(...)没有返回数组。改为将其更改为array.slice()。现在所有节点都呈现一个位置但根本没有链接。旧节点也从原来的位置磨损。

Here is how it looks when new props go in and shouldComponentUpdate is triggered

我不明白为什么链接上的位置与节点不对应。

2 个答案:

答案 0 :(得分:1)

forceLink中的links默认对象引用使用sourcetarget

您没有展示如何构建linksnodes道具,但您可以通过调用id并设置ID访问器来指向您的节点逻辑ID来绕过它,所以假设你的节点有一个id属性,可以这样写:

.force("link", d3.forceLink(links).id(d => d.id).distance(50))

或者你可以使用节点的索引作为访问者:

.force("link", d3.forceLink(links).id(d => d.index).distance(50))

.force("link", d3.forceLink(links).id((d, i) => i).distance(50))

- 编辑 -

另一个可能有用的措施是将当前节点的属性与新节点合并,这将使它们保持位置:

const updatePositions = (newNodes = [], currentNodes = []) => {
  const positionMap = currentNodes.reduce((result, node) => {
    result[node.id] = {
      x: node.x,
      y: node.y,
    };
    return result
  }, {});
  return newNodes.map(node => ({...node, ...positionMap[node.id]}))
}

然后在你的shouldComponentUpdate中(注意,这不是代码应该存在的地方)你可以这样称呼它:

var nodes = updatePositions(newProps.nodes, this.simulation.nodes())

并使用nodes代替newNodes

请注意,此代码假定节点具有唯一的id属性。更改此选项以适合您的用例

您还应尝试在您的选择中添加key功能,以识别您的节点和链接,例如:

this.graph.selectAll('.node')
    .data(nextProps.nodes, d => d.id) // again assuming id property 

答案 1 :(得分:0)

我解决了我的问题,我发现这是一个错误的组合:

  1. 节点没有链接,或者链接与节点不对应。
  2. 首先,我的d3图和数据有不同的方法来识别节点 - 当我的链接数据指向对象时,图形查找索引作为链接。我通过将两者都改为id(即寻找一个字符串)来解决这个不匹配问题。 @thedude建议的link指出了正确的方法。解决此问题导致新节点正确链接在一起。

    1. 旧节点仍然分开,进一步弹回,旧链接停留在首次渲染的位置。
    2. 我怀疑这是由于从d3获取图形数据引起的,d3定义了x,y,vx,vy和index属性。所以我做的就是在我更新数据之前在服务器中收到currentGraph时摆脱它们。

      removeD3Extras: function(currentGraph) {
        currentGraph.nodes.forEach(function(d){
          delete d.index;
          delete d.x;
          delete d.y;
          delete d.vx;
          delete d.vy;
        });
      
        currentGraph.links.forEach(function(d){
          d.source = d.source.id;
          d.target = d.target.id;
          delete d.index;
        });
      
        return currentGraph;
      }
      

      这就是诀窍!现在它表现我打算如何在控制台上没有错误,但我缩放,单击并拖动。

      但还有改进的余地:

      • 链接位于节点顶部

      • 有时节点在节拍期间处于彼此的顶部,因此需要拖放。