React + D3 Force Layout:新链接具有未定义的位置
React + D3 Force Layout: new Links have undefined positions
我相信我已经遵循了新 React Props 的一般更新模式。 D3 在接收到新的 props 时进行数据计算和渲染,因此 React 不必每次都渲染。
D3 适用于静态布局。但是当我在 shouldComponentUpdate(nextProps) 函数中收到新节点和链接时,节点缺少以下属性:
- index - nodes 数组中节点的从零开始的索引。
- x - 当前节点位置的 x 坐标。
- y - 当前节点位置的y坐标。
因此,所有新节点都有 <g tranform=translate(undefined, undefined)/>
并且聚集在左上角。
我更新道具的方式是将新对象推送到节点数组和链接数组。我不明白为什么 D3 不分配 d.x 和 d.y,就像它在 componentDidMount() 的初始设置中所做的那样。几天来我一直在为这个问题而苦苦挣扎。希望有人能帮我解决这个问题。
这里是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(...) 不是 return 数组。改为 array.slice() 。现在所有节点都被渲染了一个位置但根本没有链接。旧节点也从原始位置磨损。
Here is how it looks when new props go in and shouldComponentUpdate is triggered
我不明白为什么链接上的位置与节点不对应。
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
我解决了我的问题,我发现它是错误的组合:
- 节点没有 link,或者 link 与节点不对应。
首先,我的 d3 图和数据有不同的方式来识别节点 - 当我的 link 数据指向对象时,图寻找索引为 links。我通过将两者都更改为 id(即查找字符串)解决了这个不匹配问题。 @thedude 建议的 link 为我指出了正确的方法。解决此问题导致新节点 link 正确组合在一起。
- 旧的节点仍然分离,反弹得更远,旧的 links 留在它们最初渲染的地方。
我怀疑这是从 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;
}
成功了!现在它的行为与我打算的一样,在控制台上没有错误,但是我缩放、单击和拖动。
但还有改进的余地:
link在节点之上
有时节点在 tick 期间彼此重叠,因此需要拖放。
我相信我已经遵循了新 React Props 的一般更新模式。 D3 在接收到新的 props 时进行数据计算和渲染,因此 React 不必每次都渲染。
D3 适用于静态布局。但是当我在 shouldComponentUpdate(nextProps) 函数中收到新节点和链接时,节点缺少以下属性:
- index - nodes 数组中节点的从零开始的索引。
- x - 当前节点位置的 x 坐标。
- y - 当前节点位置的y坐标。
因此,所有新节点都有 <g tranform=translate(undefined, undefined)/>
并且聚集在左上角。
我更新道具的方式是将新对象推送到节点数组和链接数组。我不明白为什么 D3 不分配 d.x 和 d.y,就像它在 componentDidMount() 的初始设置中所做的那样。几天来我一直在为这个问题而苦苦挣扎。希望有人能帮我解决这个问题。
这里是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(...) 不是 return 数组。改为 array.slice() 。现在所有节点都被渲染了一个位置但根本没有链接。旧节点也从原始位置磨损。
Here is how it looks when new props go in and shouldComponentUpdate is triggered
我不明白为什么链接上的位置与节点不对应。
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
我解决了我的问题,我发现它是错误的组合:
- 节点没有 link,或者 link 与节点不对应。
首先,我的 d3 图和数据有不同的方式来识别节点 - 当我的 link 数据指向对象时,图寻找索引为 links。我通过将两者都更改为 id(即查找字符串)解决了这个不匹配问题。 @thedude 建议的 link 为我指出了正确的方法。解决此问题导致新节点 link 正确组合在一起。
- 旧的节点仍然分离,反弹得更远,旧的 links 留在它们最初渲染的地方。
我怀疑这是从 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;
}
成功了!现在它的行为与我打算的一样,在控制台上没有错误,但是我缩放、单击和拖动。
但还有改进的余地:
link在节点之上
有时节点在 tick 期间彼此重叠,因此需要拖放。