I'm trying to remove some nodes from a Force Layout.
The first part of the process is selecting one or more nodes. That's done with the click handler:
var nodeg = node.enter().append("g")
.attr("class", "node")
.on('click', function (n) {
if (n.dragging === true) {
return;
}
// select the clicked node
n.selected = !n.selected;
d3.select(this)
.transition()
.duration(500)
.ease('bounce')
.attr('fill', getNodeBackground(n))
.attr('transform', getNodeTransform(n));
})
.call(drag);
Note that I'm setting selected
to true.
Next, if the user presses the delete
key I remove the selected nodes:
function removeSelectedNodes() {
if (!confirm('Are you sure you want to delete the selected relationships?')) {
return;
}
var firstIndex = -1;
d3.selectAll('.node')
.each(function (n, i) {
if (n.selected !== true) {
return;
}
n.data.remove = true;
n.data.index = i;
if (firstIndex === -1) {
firstIndex = i;
}
});
var offset = 0;
_.each(_.where(scope.nodes, {data: {remove: true}}), function (n) {
var removeAt = n.index;
if (n.index > firstIndex) {
removeAt = n.index - 1 - offset;
offset++;
}
scope.nodes.splice(removeAt, 1);
scope.links.splice(removeAt - 1, 1);
});
renderGraph();
}
The entire renderGraph
function looks like this:
function renderGraph() {
force
.nodes(scope.nodes)
.links(scope.links)
.start();
var link = svg.selectAll(".link")
.data(scope.links);
link.exit().remove();
link.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(scope.nodes);
node.exit().remove();
var nodeg = node.enter().append("g")
.attr("class", "node")
.on('click', function (n) {
if (n.dragging === true) {
return;
}
// select the clicked node
n.selected = !n.selected;
d3.select(this)
.transition()
.duration(500)
.ease('bounce')
.attr('fill', getNodeBackground(n))
.attr('transform', getNodeTransform(n));
})
.call(drag);
nodeg.append("image")
.attr("xlink:href", function (d) {
return d.avatar || 'https://github.com/favicon.ico'
})
.attr("x", -56)
.attr("y", -8)
.attr("width", 64)
.attr("height", 64);
nodeg.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.attr('class', 'name')
.text(function (d) {
return d.displayName;
});
nodeg.append("text")
.attr("dx", 12)
.attr("dy", "1.35em")
.text(function (d) {
return d.relationship;
});
force.on("tick", function () {
link.attr("x1", function (d) {
return d.source.x;
})
.attr("y1", function (d) {
return d.source.y;
})
.attr("x2", function (d) {
return d.target.x;
})
.attr("y2", function (d) {
return d.target.y;
});
node.attr("transform", function (d) {
return getNodeTransform(d);
});
});
}
This is where it starts going south. If I select one node , the graph is rendered correctly after splicing the node. However, if I remove two nodes the graph ends up persisting the first removed node (by index).
Let's say the first node (by index) was 'Bob' and the second node was 'Bill'. The second node will be removed, but the first one will persist. Interestingly, another one of the nodes, the current last node (by index), will be gone instead.
NOTE: the array looks good. The nodes I wanted removed are gone, and the remaining ones are correct.
What did I do wrong here?
UPDATE: I've tried not setting nodes
and links
after removing nodes, and just calling start
:
force.start()
This didn't work.
The answer, thanks to Lars again, was to add a key function to the links and nodes:
var link = svg.selectAll(".link")
.data(scope.links, function (d) {
return d.source.data._id + '|' + d.target.data._id;
});
var node = svg.selectAll(".node")
.data(scope.nodes, function (d) {
return d.data._id;
});
Thanks Lars!
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.