如何在D3.js图模拟中限制链接长度在300-600之间?
如何在D3.js力导向图中限制链接长度在300-600之间?
要实现链接长度的范围限制,你可以通过两种方式实现:模拟tick后修正节点位置,或者自定义链接力函数,以下是具体方案:
方法一:Tick事件中修正位置(简单直观)
保留原有的力模拟配置,在每次模拟迭代(tick)后,检查所有链接的实际长度,超出范围时调整节点位置(跳过被拖拽锁定的节点):
const simulation = d3 .forceSimulation(nodes) .force("charge", d3.forceManyBody().strength(-300)) .force( "link", d3 .forceLink(links) .id((d) => d.id) .distance(450), // 设置中间值作为初始导向 ) .force( "center", d3.forceCenter( container.node().clientWidth / 2 - 75, container.node().clientHeight / 2 - 75, ), ); // 监听tick事件,修正链接长度 simulation.on("tick", () => { const minDist = 300; const maxDist = 600; links.forEach(link => { const dx = link.target.x - link.source.x; const dy = link.target.y - link.source.y; const currentDist = Math.sqrt(dx * dx + dy * dy); // 仅当长度超出范围时调整 if (currentDist < minDist || currentDist > maxDist) { const targetDist = currentDist < minDist ? minDist : maxDist; const scale = targetDist / currentDist; const midX = (link.source.x + link.target.x) / 2; const midY = (link.source.y + link.target.y) / 2; const angle = Math.atan2(dy, dx); const halfTarget = targetDist / 2; // 仅调整未被拖拽锁定的节点 if (!link.source.fx && !link.source.fy) { link.source.x = midX - halfTarget * Math.cos(angle); link.source.y = midY - halfTarget * Math.sin(angle); } if (!link.target.fx && !link.target.fy) { link.target.x = midX + halfTarget * Math.cos(angle); link.target.y = midY + halfTarget * Math.sin(angle); } } }); // 更新DOM元素位置 node.attr("cx", d => d.x).attr("cy", d => d.y); link.attr("x1", d => d.source.x) .attr("y1", d => d.source.y) .attr("x2", d => d.target.x) .attr("y2", d => d.target.y); });
原理:每次模拟计算后,强制将超出长度范围的链接拉回区间内,同时尊重节点的拖拽锁定状态(fx/fy)。
方法二:自定义链接力函数(原生力模拟逻辑)
替换默认的d3.forceLink,自定义一个力函数,根据链接当前距离动态施加吸引力/排斥力,让链接自然保持在目标范围内:
// 自定义链接力函数 function boundedLinkForce(links) { let nodes; const minDist = 300; const maxDist = 600; const strength = 0.1; // 力的强度,可根据需求调整 function force(alpha) { links.forEach(link => { const source = link.source; const target = link.target; const dx = target.x - source.x; const dy = target.y - source.y; const currentDist = Math.sqrt(dx * dx + dy * dy); let forceDelta = 0; if (currentDist < minDist) { // 距离过近,施加排斥力 forceDelta = (minDist - currentDist) * alpha * strength; } else if (currentDist > maxDist) { // 距离过远,施加吸引力 forceDelta = (maxDist - currentDist) * alpha * strength; } if (forceDelta !== 0) { const fx = (dx / currentDist) * forceDelta; const fy = (dy / currentDist) * forceDelta; // 仅对未锁定的节点施加力 if (!source.fx) source.vx += fx; if (!source.fy) source.vy += fy; if (!target.fx) target.vx -= fx; if (!target.fy) target.vy -= fy; } }); } // 初始化方法,D3力模拟会自动调用 force.initialize = function(_nodes) { nodes = _nodes; }; return force; } // 初始化模拟,使用自定义链接力 const simulation = d3 .forceSimulation(nodes) .force("charge", d3.forceManyBody().strength(-300)) .force("link", boundedLinkForce(links)) // 替换默认link力 .force( "center", d3.forceCenter( container.node().clientWidth / 2 - 75, container.node().clientHeight / 2 - 75, ), );
原理:在每一次模拟迭代中,根据链接长度与范围的差值,给节点施加相应的力,让链接自然收敛到300-600的区间内,更符合D3力导向的原生逻辑。
拖拽兼容说明
两种方案都加入了对fx/fy的判断,确保当节点被拖拽锁定时,不会被自动调整位置/施加力,完美兼容D3的拖拽功能(比如d3.drag().on("drag", (event, d) => { d.fx = event.x; d.fy = event.y; }))。
内容的提问来源于stack exchange,提问作者dmbibishkin
相关产品推荐
相关产品推荐

