Power BI自定义可折叠树:解决点击节点整树重复渲染的方案
Power BI自定义可视化:修复可折叠树节点切换问题
问题说明
开发Power BI自定义可视化的可折叠树组件时,点击节点无法正常实现展开/收起切换。当前点击节点会触发renderTree方法重新渲染整树,且传入子节点作为新根,导致树结构被替换,切换效果异常。
现有核心代码
接口与布局定义
// Declare an interface for the links/connections interface LinkCoords { x: number; y: number; } // Declare an interface for a tree node interface TreeNode { name: string; children?: TreeNode[]; // Dynamic addition of any incoming columns from analyze by data role [key: string]: number | string | undefined | TreeNode[]; } // Create a tree layout private tree = d3 .tree() // Height and width of the separations .nodeSize([50, 300]) // Wether or not the nodes have same parent, there should be a separation of 2 units between nodes .separation(function separation(a, b) { return a.parent == b.parent ? 2 : 2; }); // Links properties private diagonal = d3 .linkHorizontal<{}, LinkCoords>() .x((d) => d.y) .y((d) => d.x);
渲染与点击逻辑
// Function that would render the tree on the canvas private renderTree(measuredData: TreeNode) { // Compute the hierarchy const root = d3.hierarchy(measuredData); // Call the tree function this.tree(root); // Node variable const node = this.treeContainer .selectAll(".node") .data(root.descendants()) .enter() .append("g") .attr("transform", function (d: any) { return "translate(" + (d.y + 20) + "," + (d.x + 307) + ")"; }) .attr("font-size", 10) .on("click", (event, d) => this.click(d.data)); // Node properties node .append("rect") .attr("width", 150) .attr("height", 50) .style("stroke", function (d) { return d.children ? "#5CCE8A" : "#D45F5F"; }) .attr("fill", "grey"); // Node text properties node .append("text") .attr("x", 70) .attr("y", 25) .attr("text-anchor", "middle") .text( (d) => `name: ${d.data.name}, value: ${d.data.V}, import: ${d.data.I}` ) .attr("fill", "white"); // Create links const link = this.treeContainer .selectAll(".link") .data(root.links()) .enter(); // Define the path source and target for the links link .append("path") .attr("class", "link") .attr("fill", "none") .attr("stroke", "white") .attr("d", (d) => { // Define the source const source: LinkCoords = { x: (d.source as any).x + 332, y: (d.source as any).y + 170, }; // Define the target const target: LinkCoords = { x: (d.target as any).x + 332, y: (d.target as any).y + 20, }; return this.diagonal({ source, target }); }) .attr("stroke-width", "1px") .attr("stroke-dasharray", "1, 0"); } // Toggle the node private click(d: any) { if (d.children) { // If the clicked node has children d._children = d.children; // Hide its children d.children = null; } else { // If the clicked node does not have children d.children = d._children; // Show its children d._children = null; } this.renderTree(d); }
解决方案
1. 保存根节点引用,避免替换树结构
在Visual类中添加全局根节点属性,始终基于原始根节点进行渲染,而非点击的子节点:
private root: d3.HierarchyNode<TreeNode> | null = null;
2. 实现D3完整的enter/update/exit更新模式
当前代码仅使用enter()添加元素,未处理更新和移除,导致重复渲染。修改renderTree方法,实现三阶段更新:
private renderTree() { if (!this.root) return; // 重新计算树布局 this.tree(this.root); // -------------------------- // 节点更新逻辑 // -------------------------- const node = this.treeContainer.selectAll(".node") .data(this.root.descendants(), d => d.data.name); // 使用节点name作为唯一标识绑定数据 // 移除不需要的节点 node.exit().remove(); // 添加新节点 const nodeEnter = node.enter() .append("g") .attr("class", "node") .attr("font-size", 10) .on("click", (event, d) => this.click(d)); // 传入D3层级节点对象 // 节点矩形 nodeEnter.append("rect") .attr("width", 150) .attr("height", 50) .attr("fill", "grey"); // 节点文本 nodeEnter.append("text") .attr("x", 70) .attr("y", 25) .attr("text-anchor", "middle") .attr("fill", "white"); // 合并enter和update,更新位置和样式 const nodeMerge = nodeEnter.merge(node); nodeMerge .attr("transform", d => `translate(${d.y + 20}, ${d.x + 307})`) .select("rect") .style("stroke", d => d.children ? "#5CCE8A" : "#D45F5F"); nodeMerge.select("text") .text(d => `name: ${d.data.name}, value: ${d.data.V}, import: ${d.data.I}`); // -------------------------- // 连线更新逻辑 // -------------------------- const link = this.treeContainer.selectAll(".link") .data(this.root.links(), d => `${d.source.data.name}-${d.target.data.name}`); // 移除不需要的连线 link.exit().remove(); // 添加新连线 const linkEnter = link.enter() .append("path") .attr("class", "link") .attr("fill", "none") .attr("stroke", "white") .attr("stroke-width", "1px") .attr("stroke-dasharray", "1, 0"); // 合并enter和update,更新路径 linkEnter.merge(link) .attr("d", d => { const source: LinkCoords = { x: d.source.x + 332, y: d.source.y + 170, }; const target: LinkCoords = { x: d.target.x + 332, y: d.target.y + 20, }; return this.diagonal({ source, target }); }); }
3. 修正点击事件逻辑,操作层级节点
修改click方法,直接操作D3的HierarchyNode对象,切换其展开状态后重新渲染整树:
private click(d: d3.HierarchyNode<TreeNode>) { // 切换子节点的显示/隐藏 if (d.children) { d._children = d.children; d.children = null; } else { d.children = d._children; d._children = null; } // 重新渲染整棵树 this.renderTree(); }
4. 初始化根节点
在数据加载或更新时,初始化全局根节点并调用渲染:
// 数据更新时调用此方法 private updateData(measuredData: TreeNode) { this.root = d3.hierarchy(measuredData); this.renderTree(); }
关键说明
- 始终基于原始根节点渲染,避免点击子节点时替换树结构
- 使用D3的enter/update/exit模式,确保只更新需要变化的元素,避免重复渲染
- 操作D3层级节点而非原始数据对象,确保布局计算正确
内容的提问来源于stack exchange,提问作者Hammad Ahmed
相关产品推荐
相关产品推荐

