我相信我已经遵循新React Props的一般更新模式。当收到新的道具时,D3会进行数据计算和渲染,这样React就不必渲染每个滴答。
D3适用于静态布局。但是当我在shouldComponentUpdate(nextProps)函数中收到新节点和链接时,节点缺少以下属性:
因此,所有新节点都有<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
我不明白为什么链接上的位置与节点不对应。
答案 0 :(得分:1)
forceLink
中的links默认对象引用使用source
和target
。
您没有展示如何构建links
和nodes
道具,但您可以通过调用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)
我解决了我的问题,我发现这是一个错误的组合:
首先,我的d3图和数据有不同的方法来识别节点 - 当我的链接数据指向对象时,图形查找索引作为链接。我通过将两者都改为id(即寻找一个字符串)来解决这个不匹配问题。 @thedude建议的link指出了正确的方法。解决此问题导致新节点正确链接在一起。
我怀疑这是由于从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;
}
这就是诀窍!现在它表现我打算如何在控制台上没有错误,但我缩放,单击并拖动。
但还有改进的余地:
链接位于节点顶部
有时节点在节拍期间处于彼此的顶部,因此需要拖放。