D3.js层级树折叠/展开时会重建整个树的问题排查与解决请求
D3.js层级树折叠/展开时会重建整个树的问题排查与解决请求
我在尝试用TypeScript复现D3.js的层级树示例时遇到了一个动画异常问题:树能正常初始化渲染,但每次点击折叠/展开节点时,过渡效果看起来像是重新构建了整个树,而不是只对点击节点的子节点进行添加/移除操作。按住Alt键慢放动画的话,能明显看到从点击位置生成了新树,原来的树被“吸”进了点击的节点里,这和预期的局部过渡效果完全不符。
我先定义了TreeNodeData类型来规范节点数据结构:
export interface TreeNodeData { name: string; fill?: string; children?: TreeNodeData[]; value?: number; id?: string | number; }
以下是我的核心实现代码:
const root = d3.hierarchy(data); const dx = 40; const dy = (this.width - this.marginRight - this.marginLeft) / (1 + root.height); // 定义树布局和链接的绘制形状 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!); // 创建SVG容器、链接图层和节点图层 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; // 按住Alt键慢放过渡动画 const nodes = root.descendants().reverse(); const links = root.links(); // 计算新的树布局 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 = () => { svg.dispatch("toggle"); return () => { }; }; transition.tween("resize", tweenFactory); } // 绑定节点数据,指定key函数做数据匹配 const node = gNode.selectAll<SVGGElement, d3.HierarchyNode<TreeNodeData>>("g") .data(nodes, (d) => { if (d && d.id !== undefined) { return d.id; } // 处理d.id不存在的 fallback 情况 return String(Math.random()); // 临时用随机字符串作为key }); // 进入阶段:将新节点添加到父节点的初始位置 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"); // 更新阶段:将节点过渡到新的布局位置 const nodeUpdate = node.merge(nodeEnter).transition(transition) .attr("transform", d => `translate(${d.y},${d.x})`) .attr("fill-opacity", 1) .attr("stroke-opacity", 1); // 注:原代码中节点退出阶段的逻辑未完整编写 };
我初步怀疑问题可能出在数据绑定的key函数上?或者是节点/链接的过渡逻辑有遗漏?希望能有人帮忙排查下,为什么折叠/展开时的过渡不是只针对子节点的局部动画,而是触发了整个树的重建效果。
内容来源于stack exchange
相关产品推荐
相关产品推荐

