我使用react和Force布局制作了组织层次结构图。 Org对象有一个用户,并定义用户与其他人在工作中的关系 - 比如老板,同事,下属。可以在组织地图中动态添加新人,并使用新信息重新呈现地图。
但是,重新渲染后,地图会错误地显示链接和关系文本。即使与节点关联的数据正确,即使节点上的名称也会被错误分配。通过调试,我发现链接,节点和链接标签对象 - 都是正确的。但进入和退出似乎有点时髦,可能是问题的根源。
我有jsfiddle来模拟错误。
jsfiddle最初呈现带有四个节点的组织地图。 Joe是用户,他有一个老板John,同事Shelley和下属Maria。
我创建了一个按钮来模拟动态添加新人。单击该按钮将添加(数据被硬编码用于错误模拟)Kelly作为Maria的同事并重新渲染地图。您会注意到渲染后,所有链接和标签都不正确。但是,当我在调试模式下查看与节点相关的数据时,它是正确的。
我花了很多时间试图解决这个问题,但似乎无法抓住这个错误。
jsfiddle写的是反应。如果你不熟悉反应,请忽略反应代码,只关注d3代码。
jsfiddle代码粘贴在这里:
使用Javascript:
const ForceMap = React.createClass({
propTypes: {
data: React.PropTypes.object,
width: React.PropTypes.number,
height: React.PropTypes.number
},
componentDidMount(){
let {width,height} = this.props;
this.forceLayout = d3.layout.force()
.linkDistance(100)
.charge(-400)
.gravity(.02)
.size([width, height])
this.svg = d3.select("#graph")
.append("svg")
.attr({id:'#org-map',width:width,height:height,backgroundColor:'white'})
let container = this.svg.append("g").attr('class','container');
let rect = container.append("rect")
.attr({width:width,height:height})
.style({fill:"white","pointer-events":"all"})
this.org = this.props.data;
this.org.x = width / 2;
this.org.y = height / 2;
this.org.fixed = true;
console.log('Initial Org:',this.org);
this.d3render(this.org);
}, //componentDidMount
d3render(org) {
let container = d3.selectAll('g.container')
let nodes = this.flatten(org);
let links = d3.layout.tree().links(nodes);
let force = this.forceLayout.on("tick", tick);
force.nodes(nodes) // Restart the force layout.
.links(links)
.start();
debugger;
// Links line that connects two org members together
let link = container.selectAll(".link").data(links);
link.exit().remove()
link.enter().append("line")
.attr('class',"link")
.attr('id', (d)=> d.source.name + '-' +d.target.name)
console.log('link:',link);
//Relationship label for every link
let linkLabel = container.selectAll(".linklabelholder").data(links);
linkLabel.exit().remove();
linkLabel.enter()
.append("g")
.attr("class", "linklabelholder")
.attr('id', (d) => `linklabel-${d.source.name}-${d.target.name}`)
.append("text")
.attr({dx:1, dy: ".35em", "text-anchor": "middle"})
.text((d) => d.target.relation)
.style("font-size",12);
console.log('link Labels: ',linkLabel);
// Update nodes. Each node represents one person
let node = container.selectAll(".node").data(nodes);
node.exit().remove();
let nodeEnter = node.enter()
.append("g")
.attr("class", "node")
.attr('id', (d) => `node-${d.name}`)
nodeEnter.append('circle')
.attr('r',25)
.attr('id',(d) => d.name)
.style('fill', 'steelblue')
nodeEnter.append("text")
.attr("dy", ".35em")
.text((d) => d.name)
.attr('id', (d) => d.name)
.style("font-size",12);
console.log('Nodes: ',node);
function tick() {
node.attr("cx", function(d) { return d.x = Math.max(25, Math.min(475, d.x)); })
.attr("cy", function(d) { return d.y = Math.max(25, Math.min(275, d.y)); });
link.attr("x1", (d) => d.source.x)
.attr("y1", (d) => d.source.y)
.attr("x2", (d) => d.target.x)
.attr("y2", (d) => d.target.y)
linkLabel.attr("transform", (d) => `translate(${(d.source.x+d.target.x)/2},${(d.source.y+d.target.y)/2})`);
node.attr("transform", (d) => `translate(${d.x},${d.y})`)
} //tick
}, //d3 render
addNewPerson() {
let newPerson = {_id: "5",name: 'Kelly' ,relation:'coworker'};
let addTo = {_id:"4", name: "Maria"};
add(this.org);
console.log('RE-RENDER AFTER ADDING NEW PERSON');
console.log('Org after addding new person: ', this.org);
this.d3render(this.org);
function add(node) {
if (node.children) node.children.forEach(add);
if (node._id === addTo._id) {
if (!node.children) node.children = [];
node.children.push(newPerson);
}
}
},
flatten(org) {
var nodes = [], i = 0;
recurse(org);
return nodes;
function recurse(node) {
if (node.children) node.children.forEach(recurse);
if (!node.id) node.id = ++i;
nodes.push(node);
}
}, //flatten
render() {
return (
<div>
<div id="graph"></div>
<button className='btnClass' onClick={this.addNewPerson} type="submit">Add new person
</button>
</div>
);
},
});
var user = {
name: 'Joe',
_id: "1",
children:[
{_id:"2", name: "John", relation:"boss"},
{ _id:"3", name: "Shelley", relation:"coworker"},
{_id:"4", name: "Maria", relation:"subordinate"}
]
}
ReactDOM.render(
<ForceMap
data={user}
width={500}
height={300}
/>,
document.getElementById('container')
);
答案 0 :(得分:0)
您需要使用键函数来使除简单数据元素绑定之外的任何其他操作正常工作,否则它们只是根据索引替换/覆盖彼此: https://github.com/mbostock/d3/wiki/Selections#data
在这种情况下,这些将起作用:
let link = container.selectAll(".link").data(links, function(d) { return d.source_id+"-"+d.target._id+"-"+d.target.relation; });
...
let linkLabel = container.selectAll(".linklabelholder").data(links, function(d) {
return d.source._id+"-"+d.target._id+"-"+d.target.relation;
});
...
let node = container.selectAll(".node").data(nodes, function(d) { return d._id; });
https://jsfiddle.net/aqu1h7zr/3/
至于为什么新链接会覆盖节点,因为元素是按照它们在dom中遇到的顺序绘制的。您的新链接将添加到旧节点之后的dom中,以便将它们绘制在顶部。为了解决此问题,请添加节点和链接以分离&#39;元素(我还没有在更新的小提琴中完成此操作),因此首先绘制所有链接
e.g。不
<old links>
<old nodes>
<new links>
<new nodes>
但
<g>
<old links>
<new links>
</g>
<g>
<old nodes>
<new nodes>
</g>