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

D3 Force Network:拖拽节点时如何让其他节点保持固定

解决D3力导向图首次拖拽时其他节点移动的问题

问题核心原因:当力布局模拟停止后,未拖拽的节点没有设置固定位置(fx/fy),首次拖拽重启模拟时,力布局会重新计算所有节点位置,导致其他节点发生移动。

解决方案步骤

  1. 模拟停止时固定所有非源节点位置:在模拟迭代完成停止后,将所有目标节点的fx和fy设置为当前的x和y值,锁定它们的位置。
  2. 调整拖拽逻辑:拖拽时仅修改当前节点的固定位置,直接更新视图即可,无需重启模拟触发额外力计算。

修改后的完整代码

const networkData = {
  "nodes": [{
      "id": 1,
      "name": "A",
      isSource: true,
      "sourceX": 100,
      "sourceY": 100
    },
    {
      "id": 2,
      "name": "B",
      isSource: false
    },
    {
      "id": 3,
      "name": "C",
      isSource: true,
      "sourceX": 150,
      "sourceY": 150
    },
    {
      "id": 4,
      "name": "D",
      isSource: false
    },
    {
      "id": 5,
      "name": "E",
      isSource: true,
      "sourceX": 200,
      "sourceY": 200
    },
    {
      "id": 6,
      "name": "F",
      isSource: false
    }
  ],
  "links": [{
      "source": 1,
      "target": 2
    },
    {
      "source": 3,
      "target": 4
    },
    {
      "source": 5,
      "target": 6
    }
  ]
}

const svg = d3.select("#my_dataviz");

const sourceNodes = networkData.nodes.filter(node => node.isSource === true);
const targetNodes = networkData.nodes.filter(node => !node.isSource);

const link = svg
  .selectAll(".link")
  .data(networkData.links)
  .enter()
  .append("line")
  .attr("class", "link")
  .attr("stroke-width", 2)
  .attr("stroke", 'grey');

const targetNodeSelection = svg
  .selectAll(".node")
  .data(targetNodes)
  .enter()
  .append("circle")
  .attr("class", "node")
  .attr("r", 10)
  .attr("fill", 'blue');

const sourceNodeSelection = svg
  .selectAll(".source-node")
  .data(sourceNodes)
  .enter()
  .append("circle")
  .attr("class", "source-node")
  .attr("r", 1)
  .attr("fill", 'white')
  .attr("cx", d => d.sourceX)
  .attr("cy", d => d.sourceY);

const maxIterations = 10;
let iterationCount = 0;

const simulation = d3.forceSimulation(networkData.nodes)
  .force("link", d3.forceLink(networkData.links).id(d => d.id))
  .force("center", d3.forceCenter(400 / 2, 400 / 2))
  .force('collision', d3.forceCollide().radius(20))
  .on("tick", function() {
    tick();
    iterationCount++;
    if (iterationCount >= maxIterations) {
      simulation.stop();
      // 固定所有目标节点的位置
      targetNodes.forEach(d => {
        d.fx = d.x;
        d.fy = d.y;
      });
    }
  });

function tick() {
  link
    .attr("x1", d => d.source.sourceX)
    .attr("y1", d => d.source.sourceY)
    .attr("x2", d => d.target.x)
    .attr("y2", d => d.target.y);

  targetNodeSelection
    .attr("cx", d => d.x)
    .attr("cy", d => d.y);

  sourceNodeSelection
    .attr("cx", d => d.sourceX)
    .attr("cy", d => d.sourceY);
}

function clamp(x, lo, hi) {
  return x < lo ? lo : x > hi ? hi : x;
}

function dragged(event, d) {
  d.fx = clamp(event.x, 0, 400);
  d.fy = clamp(event.y, 0, 400);
  // 直接更新当前节点和对应连线的位置
  targetNodeSelection.filter(node => node.id === d.id)
    .attr("cx", d.fx)
    .attr("cy", d.fy);
  link.filter(l => l.target.id === d.id)
    .attr("x2", d.fx)
    .attr("y2", d.fy);
}

const drag = d3.drag().on("drag", dragged);
targetNodeSelection.call(drag);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js" integrity="sha512-cd6CHE+XWDQ33ElJqsi0MdNte3S+bQY819f7p3NUHgwQQLXSKjE4cPZTeGNI+vaxZynk1wVU3hoHmow3m089wA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<svg id="my_dataviz" height='400' width='400'></svg>

关键修改说明

  • 在模拟停止时,遍历所有目标节点设置fx和fy,锁定初始位置,后续拖拽时力布局不会影响这些节点。
  • 拖拽函数直接更新当前节点和关联连线的位置,避免重启模拟触发不必要的力计算,确保其他节点保持固定。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 02:57:17