D3 Force Network:拖拽节点时如何让其他节点保持固定
解决D3力导向图首次拖拽时其他节点移动的问题
问题核心原因:当力布局模拟停止后,未拖拽的节点没有设置固定位置(fx/fy),首次拖拽重启模拟时,力布局会重新计算所有节点位置,导致其他节点发生移动。
解决方案步骤
- 模拟停止时固定所有非源节点位置:在模拟迭代完成停止后,将所有目标节点的
fx和fy设置为当前的x和y值,锁定它们的位置。 - 调整拖拽逻辑:拖拽时仅修改当前节点的固定位置,直接更新视图即可,无需重启模拟触发额外力计算。
修改后的完整代码
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
相关产品推荐
相关产品推荐

