2017-05-05 91 views
3

我相信我已经遵循了新React道具上的一般更新模式。当收到新的道具时,D3会进行数据计算和渲染,以便React不必渲染每个勾号。React + D3 Force布局:新链接有未定义位置

D3适用于静态布局。但是,当我在shouldComponentUpdate(nextProps)功能接收新的节点和链路,节点缺乏以下属性:

  • 索引 - 节点阵列中的节点的索引(从零开始)。
  • x - 当前节点位置的x坐标。
  • y - 当前节点位置的y坐标。

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

我更新道具的方式是将新对象推向节点数组和链接数组。我不明白为什么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

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

+1

只要在shouldComponentUpdate中进行true/false检查并在componentWillUpdate中进行实际修改即可。这很难说,如果这能解决什么或你的问题是什么无需调试,但有错误的生命周期事件做错事可以肯定地制造搞笑的错误。你觉得你需要提供一个运行片段来获得更详细的帮助... –

+0

我做了检查,它实际上在componentWillUpdate修改。我想我发现了一个导致它的问题,这是我在馈送对象而不是在simulation.nodes和simulation.links中的数组。最初,我使用了Object.assign(...),它返回一个对象;我已经将它改成nextProps.nodes.slice()来创建新的数组。所以现在所有的节点都有位置,但是它们在爆炸中被重新渲染并且根本没有链接。我会张贴图片和片段。 – heidipercyslinky

+0

你是怎么解决这个问题的? – kurumkan

回答

0

我解决我的问题,我发现的组合错误:

  1. 节点没有链接,或者链接不与节点对应。

首先,我的d3图形和数据有不同的方式来识别节点 - 当我的链接数据指向对象时,图形寻找索引作为链接。我解决了这个不匹配问题,把它们都改成了id(即查找一个字符串)。 @thedude建议的这个link指出我是正确的。解决此问题导致新节点正确链接在一起。

  1. 旧的节点仍然分开,弹得更远,旧链接保持在第一次渲染的位置。

我怀疑这是由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; 
} 

这样做的伎俩!现在它的行为表现出我在控制台上没有错误的方式,但是我缩放,单击并拖动。

但还有改进的余地:

  • 链接是上的节点

  • 的顶部有时节点上滴答期间彼此的顶部,因此需要拖放。

1

links in forceLink默认使用对象引用来指向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 
+0

试过这个,但没有工作。也许这是我如何构建链接和节点?我构建的方式是将新节点和新链接推入阵列。 – heidipercyslinky

+0

奇怪的是,它工作时,我推开两个节点和客户端的链接,但是当我做了服务器端相同返回一个数据对象为客户端渲染,链接没有X1,Y1,X2 ,y2。 – heidipercyslinky

+0

@heidipercyslinky你能分享创建节点和链接的代码吗? – thedude