I have a D3 tree graph which loads data from an API when the nodes are clicked. I am noticing that the order in which the nodes are clicked is impacting the display of data in the tree. The data is correct but the order of nodes changes (const nodes = treeData.descendants()) based on the order of clicks.
The code I have
node_clicked(d) {
d.data.isCollapsed = !d.data.isCollapsed;
this.service.linkedConcepts(d.data.id)
.then(data => {
if (data) {
const items = data as any[];
this.appendData(d.data.id, this.treeData, items);
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
this.root = d3.hierarchy(this.treeData, d => d.children);
this.update(d);
}
})
.catch(err => {
this.removeFromDataBeingRetrieved(d.data.id);
console.error(err);
});
}
appendData(parentid, node, items) {
if (node.id === parentid) {
items.forEach(item => {
node.children.push({
name: item.Label,
id: item.ConceptID,
isCollapsed: true,
isTopConcept: false,
count: item.LinkedConceptsCount,
children: []
});
});
node.isCollapsed = false;
} else if (node.children) {
node.children.forEach(item => this.appendData(parentid, item, items));
}
}
update(source) {
const treeData = this.treemap(this.root);
// Compute the new tree layout.
const nodes = treeData.descendants(),
links = treeData.descendants().slice(1);
// Normalize for fixed-depth.
nodes.forEach(d => d.y = d.depth * 180);
// ****************** Nodes section ***************************
let i = 0;
// Update the nodes...
const node = this.svg.selectAll('g.node')
.data(nodes, function(d) {return d.id || (d.id = ++i); });
// Enter any new modes at the parent's previous position.
const nodeEnter = node.enter().append('g')
.attr('class', 'node')
.attr('transform', d => 'translate(' + source.y0 + ',' + source.x0 + ')')
.on('click', d => this.node_clicked(d));
// Add Circle for the nodes
nodeEnter.append('circle')
.attr('class', 'node')
.attr('r', 1e-6)
.style('stroke', function(d) {
return d.data.count > 0 ? '#fff' : 'steelblue'; })
.style('fill', function(d) {
return d.isCollapsed && d.data.count > 0 ? 'lightsteelblue' : '#fff';
})
.append('title')
.text(d => d.data.count + ' linked articles');
// Add labels for the nodes
nodeEnter.append('text')
.attr('dy', '.35em')
.attr('x', function(d) {
return d.children || d._children ? -13 : 13;
})
.attr('text-anchor', function(d) {
return (d.data.isTopConcept === true) ? 'end' : 'start';
})
.text(function(d) { return d.data.name; })
.call(d => this.wrap(d));
// UPDATE
const nodeUpdate = nodeEnter.merge(node);
// Transition to the proper position for the node
nodeUpdate.transition()
.duration(this.duration)
.attr('transform', function(d) {
return 'translate(' + d.y + ',' + d.x + ')';
});
// Update the node attributes and style
nodeUpdate.select('circle.node')
.attr('r', 10)
.style('fill', function(d) {
if (d.data.count === 0) {
return '#fff';
} else if ( d.data.isCollapsed === true ) {
return 'lightsteelblue';
} else {
return '#fff';
}
})
.style('stroke-width', '3px')
.style('stroke', function(d) {
return d._children > 0 ? '#fff' : 'lightsteelblue'; })
.attr('cursor', 'pointer');
// Remove any exiting nodes
const nodeExit = node.exit().transition()
.duration(this.duration)
.attr('transform', function(d) {
return 'translate(' + source.y + ',' + source.x + ')';
})
.remove();
// On exit reduce the node circles size to 0
nodeExit.select('circle')
.attr('r', 1e-6);
// On exit reduce the opacity of text labels
nodeExit.select('text')
.style('fill-opacity', 1e-6);
// ****************** links section ***************************
// Update the links...
const link = this.svg.selectAll('path.link')
.data(links, d => d['id']);
// Enter any new links at the parent's previous position.
const linkEnter = link.enter().insert('path', 'g')
.attr('class', 'link')
.style('fill', 'none')
.style('stroke', '#ccc')
.style('stroke-width', '3px')
.attr('d', d => this.diagonal({x: source.x, y: source.y},
{x: source.x, y: source.y}));
// UPDATE
const linkUpdate = linkEnter.merge(link);
// Transition back to the parent element position
linkUpdate.transition()
.duration(this.duration)
.attr('d', d => this.diagonal(d, d.parent) );
// Remove any exiting links
const linkExit = link.exit().transition()
.duration(this.duration)
.attr('d', d => this.diagonal({x: source.x, y: source.y},
{x: source.x, y: source.y}))
.remove();
// Store the old positions for transition.
nodes.forEach(function(d){
d.x0 = d.x;
d.y0 = d.y;
});
}
An illustration of the problem: When I expand level 2 nodes before expanding level 3 nodes, I see this which is correct data -
When I expand level 3 nodes before level 2, I see this which is incorrect data. The data displayed in level 4 is repeated in level 3 (highlighted) as well -
Do I have to manually manage the order of treeData.descendants() to handle this issue? Is there any other inbuilt way?
This line of code was causing the issue
const node = this.svg.selectAll('g.node')
.data(nodes, function(d) {return d.id || (d.id = ++i); });
I changed it to
const node = this.svg.selectAll('g.node')
.data(nodes, function(d) {return d.id || (d.id = d.data.id); });
and it works perfectly now :)