You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

拖拽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>

关键修改说明

  1. 拆分svg容器和内容g元素:把zoom绑定到外层svg容器,内部g元素用来承载树内容,确保zoom变换正确作用于整个树
  2. 用zoom内置平移替代自定义drag:d3.zoom默认支持拖拽平移,不需要单独绑定drag行为,避免手动修改transform导致的状态不同步
  3. 统一管理初始变换:使用d3.zoomIdentity创建初始变换,通过zoom.transform方法应用,保证zoom内部状态和g元素的transform一致
  4. 修正zoom事件处理:直接将d3.event.transform应用到g元素,不需要手动拼接translate和scale字符串

内容的提问来源于stack exchange,提问作者sathish

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 15:45:35