拖拽D3 TreeView后缩放跳回初始位置的解决方案咨询
解决D3 TreeView拖拽后缩放复位问题
问题根源是手动处理拖拽变换时,没有同步更新d3.zoom内部维护的变换状态,导致缩放时zoom使用初始状态重置了位置。下面是修正方案:
核心修改思路
- 移除独立的
d3.drag()绑定,直接用d3.zoom内置的平移功能,避免状态冲突 - 统一通过d3.zoom的transform对象管理所有平移、缩放状态,确保拖拽和缩放的状态同步
- 修正初始化和事件处理的transform应用方式
修改后的完整代码
JavaScript
var treeData = { "name": "Root", "children": [ { "name": "Item 1", "children": [ { "name": "Item 3" }, { "name": "Item 4" } ] }, { "name": "Item 2", "children": [ { "name": "Item 5" }, { "name": "Item 6" } ] } ] }; // Set the dimensions and margins of the diagram var margin = {top: 20, right: 10, bottom: 30, left: 50}, width = 1520 - margin.left - margin.right, height = 705 - margin.top - margin.bottom; // append the svg object to the body of the page var svgContainer = d3.select("#TreeView").append("svg") .attr("id", "D3TreeViewSvg") .attr("width", 1520) .attr("height", 705) .attr("style", "margin: auto;display: block;overflow: visible;"); // 创建包含树内容的g元素 var svg = svgContainer.append("g") .attr("class", "D3TreeViewG"); // 初始化zoom行为,设置初始变换 var zoom = d3.zoom() .scaleExtent([0.5, 2]) // 限制缩放范围(可选) .on("zoom", function() { // 直接应用zoom的transform到g元素 svg.attr("transform", d3.event.transform); }); // 给svg容器绑定zoom行为(注意是绑定到外层svg,不是内部g) svgContainer.call(zoom); // 设置初始变换:平移margin + 缩放0.99 var initialTransform = d3.zoomIdentity .translate(margin.left, margin.top) .scale(0.99); // 应用初始变换到zoom和g元素 svgContainer.call(zoom.transform, initialTransform); var rect = svg.append("rect") .attr("width", width) .attr("height", height) .style("fill", "none") .style("pointer-events", "all"); var i = 0, duration = 750, root; // declares a tree layout and assigns the size var treemap = d3.tree().size([height, width]); // Assigns parent, children, height, depth root = d3.hierarchy(treeData, function(d) { return d.children; }); root.x0 = height / 2; root.y0 = 0; update(root); // Collapse the node and all it's children function collapse(d) { if(d.children) { d._children = d.children d._children.forEach(collapse) d.children = null } } setTimeout(function(){ $('#D3TreeViewSvg').css('width', $(".D3TreeViewG")[0].getBoundingClientRect().width + 'px'); }, 1000); function update(source) { // Assigns the x and y position for the nodes var treeData = treemap(root); // Compute the new tree layout. var nodes = treeData.descendants(), links = treeData.descendants().slice(1); // Normalize for fixed-depth. nodes.forEach(function(d){ d.y = d.depth * 180}); // ****************** Nodes section *************************** var node = svg.selectAll('g.node') .data(nodes, function(d) {return d.id || (d.id = ++i); }); var nodeEnter = node.enter().append('g') .attr('class', 'node') .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; }) .on('click', click); nodeEnter.append('circle') .attr('class', 'node') .attr('r', 1e-6) .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }); nodeEnter.append('text') .attr("dy", ".35em") .attr("x", function(d) { return d.children || d._children ? -13 : 13; }) .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; }) .text(function(d) { return d.data.name; }); var nodeUpdate = nodeEnter.merge(node); nodeUpdate.transition() .duration(duration) .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; }); nodeUpdate.select('circle.node') .attr('r', 10) .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }) .attr('cursor', 'pointer'); var nodeExit = node.exit().transition() .duration(duration) .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; }) .remove(); nodeExit.select('circle') .attr('r', 1e-6); nodeExit.select('text') .style('fill-opacity', 1e-6); // ****************** links section *************************** var link = svg.selectAll('path.link') .data(links, function(d) { return d.id; }); var linkEnter = link.enter().insert('path', "g") .attr("class", "link") .attr('d', function(d){ var o = {x: source.x0, y: source.y0} return diagonal(o, o) }); var linkUpdate = linkEnter.merge(link); linkUpdate.transition() .duration(duration) .attr('d', function(d){ return diagonal(d, d.parent) }); var linkExit = link.exit().transition() .duration(duration) .attr('d', function(d) { var o = {x: source.x, y: source.y} return diagonal(o, o) }) .remove(); // Store the old positions for transition. nodes.forEach(function(d){ d.x0 = d.x; d.y0 = d.y; }); // Creates a curved (diagonal) path from parent to the child nodes function diagonal(s, d) { path = `M ${s.y} ${s.x} C ${(s.y + d.y) / 2} ${s.x}, ${(s.y + d.y) / 2} ${d.x}, ${d.y} ${d.x}` return path } // Toggle children on click. function click(d) { if (d.children) { d._children = d.children; d.children = null; } else { d.children = d._children; d._children = null; } update(d); } }
CSS
.node circle { fill: #fff; stroke: steelblue; stroke-width: 3px; } .node text { font: 12px sans-serif; } .link { fill: none; stroke: #ccc; stroke-width: 2px; /* 原代码20px过粗,调整为更合理的2px */ }
HTML
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> <script src="https://d3js.org/d3.v4.min.js"></script> <div id="TreeViewParent" style="height:705px;width:1520px;"> <div id="TreeView" style="height:100%;width:100%;"></div> </div>
关键修改说明
- 拆分svg容器和内容g元素:把zoom绑定到外层svg容器,内部g元素用来承载树内容,确保zoom变换正确作用于整个树
- 用zoom内置平移替代自定义drag:d3.zoom默认支持拖拽平移,不需要单独绑定drag行为,避免手动修改transform导致的状态不同步
- 统一管理初始变换:使用
d3.zoomIdentity创建初始变换,通过zoom.transform方法应用,保证zoom内部状态和g元素的transform一致 - 修正zoom事件处理:直接将
d3.event.transform应用到g元素,不需要手动拼接translate和scale字符串
内容的提问来源于stack exchange,提问作者sathish
相关产品推荐
相关产品推荐

