I tried to recreate the example in the page while using Typescript. The tree is created, however whenever I click to collapse/expand it the transition seems to recreate the entire tree instead of only adding / removing the nodes from the "clicked" parent.
Using the trick of holding alt while clicking I can see that a new tree is created "from" where I am clicking and the "current" tree is then removed/sucked into the node that I am clicking on.
I wasn't able to quickly think on a better way to provide the code for the example, so here is all of it. TreeNodeData being just a type I've created for the exercise:
export interface TreeNodeData {
name: string;
fill?: string;
children?: TreeNodeData[];
value?: number;
id?: string | number;
}
Code here:
const root = d3.hierarchy(data);
const dx = 40;
const dy = (this.width - this.marginRight - this.marginLeft) / (1 + root.height);
// Define the tree layout and the shape for links.
const tree = d3.tree().nodeSize([dx, dy]);
const diagonal: d3.Link<any, d3.HierarchyLink<TreeNodeData>, d3.HierarchyLink<TreeNodeData>> = d3.linkHorizontal<d3.HierarchyLink<TreeNodeData>, d3.HierarchyLink<TreeNodeData>>()
.x(d => d.y!)
.y(d => d.x!);
// Create the SVG container, a layer for the links and a layer for the nodes.
const svg = d3.create("svg")
.attr("width", this.width)
.attr("height", this.height)
.attr("viewBox", [-this.marginLeft, -this.marginTop, this.width, dx])
.attr("style", "max-width: 100%; height: auto; font: 20px sans-serif; user-select: none;");
const gLink = svg.append("g")
.attr("fill", "none")
.attr("stroke", "#555")
.attr("stroke-opacity", 0.4)
.attr("stroke-width", 1.5);
const gNode = svg.append("g")
.attr("cursor", "pointer")
.attr("pointer-events", "all");
const update = (event: KeyboardEvent | MouseEvent | null, source: d3.HierarchyNode<TreeNodeData>) => {
const duration = event?.altKey ? 2500 : 250; // hold the alt key to slow down the transition
const nodes = root.descendants().reverse();
const links = root.links();
// Compute the new tree layout.
tree(root);
let left = root;
let right = root;
root.eachBefore(node => {
if(node.x !== undefined){
if(left.x !== undefined){
if (node.x < left.x) left = node;
}
if(right.x !== undefined){
if (node.x > right.x) right = node;
}
}
});
const height = right.x! - left.x! + this.marginTop + this.marginBottom;
const transition = svg.transition()
.duration(duration)
.attr("height", height)
.attr("viewBox", [-this.marginLeft, left.x! - this.marginTop, this.width, height].join(" "));
if (window.ResizeObserver) {
transition.tween("resize", null);
} else {
const tweenFactory = () => {
// This is the "factory" function.
svg.dispatch("toggle"); // Perform your action here.
// The factory MUST return an "interpolator" function.
return () => {
};
};
transition.tween("resize", tweenFactory);
}
const node = gNode.selectAll<SVGGElement, d3.HierarchyNode<TreeNodeData>>("g")
.data(nodes, (d) => {
if (d && d.id !== undefined) {
return d.id;
}
// You might need to handle cases where d.id is missing based on your data structure.
// Returning a fallback value or throwing an error might be appropriate.
return String(Math.random()); // Example fallback: a unique string
});
// Enter any new nodes at the parent's previous position.
const nodeEnter = node.enter().append("g")
.attr("transform", d => `translate(${source.y0},${source.x0})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0)
.on("click", (event, d: d3.HierarchyNode<TreeNodeData>) => {
d.children = d.children ? undefined : d._children;
update(event, d);
});
nodeEnter.append("circle")
.attr("r", 2.5)
.attr("fill", d => d._children ? "#555" : "#999")
.attr("stroke-width", 10);
nodeEnter.append("text")
.attr("dy", "0.31em")
.attr("x", (d : d3.HierarchyNode<TreeNodeData>) => d._children ? -6 : 6)
.attr("text-anchor", d => d._children ? "end" : "start")
.text((d : d3.HierarchyNode<TreeNodeData>) => d.data.name)
.attr("stroke-linejoin", "round")
.attr("stroke-width", 3)
.attr("stroke", "white")
.attr("paint-order", "stroke");
// Transition nodes to their new position.
const nodeUpdate = node.merge(nodeEnter).transition(transition)
.attr("transform", d => `translate(${d.y},${d.x})`)
.attr("fill-opacity", 1)
.attr("stroke-opacity", 1);
// Transition exiting nodes to the parent's new position.
const nodeExit = node.exit().transition(transition).remove()
.attr("transform", d => `translate(${source.y},${source.x})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0);
// Update the links…
const link = gLink.selectAll("path")
.data(links, (d : d3.HierarchyLink<TreeNodeData>) => d.target.id!); // non-null assertion do caralho
// Enter any new links at the parent's previous position.
const linkEnter = link.enter().append("path")
.attr("d", d => {
return diagonal({ source: source, target: source });
});
// Transition links to their new position.
link.merge(linkEnter).transition(transition)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition(transition).remove()
.attr("d", d => {
return diagonal({ source: source, target: source })});
// Stash the old positions for transition.
root.eachBefore(d => {
d.x0 = d.x;
d.y0 = d.y;
});
}
// Do the first update to the initial configuration of the tree — where a number of nodes
// are open (arbitrarily selected as the root, plus nodes with 7 letters).
root.x0 = dy / 2;
root.y0 = 0;
root.descendants().forEach((d : d3.HierarchyNode<TreeNodeData>, i) => {
d._children = d.children;
if (d.depth && d.data.name.length !== 7) d.children = undefined;
});
update(null, root);
document.querySelector("body")?.appendChild(svg.node()!);
Here is the problem in your code:
const node = gNode.selectAll<SVGGElement, d3.HierarchyNode<TreeNodeData>>("g")
.data(nodes, (d) => {
if (d && d.id !== undefined) {
return d.id;
}
// You might need to handle cases where d.id is missing based on your data structure.
// Returning a fallback value or throwing an error might be appropriate.
return String(Math.random()); // Example fallback: a unique string
});
Since id
is optional and may be missing, it's probably randomised on each call and thus nodes have no persistent identification
Change your code to:
.data(nodes, d => d.id)
and make sure id
is mandatory