如何在D3.js力导向模拟中实现节点间的吸引效果?
D3.js 实现选中节点间的自定义吸引力
要实现选中父节点与子节点后,子节点受父节点吸引且其余节点重新排列,核心是利用D3力导向模拟的自定义力函数,结合模拟状态重置来实现。以下是具体修改方案:
关键修改点
- 存储选中节点的数据对象而非仅ID,方便直接获取节点坐标
- 添加自定义吸引力,在模拟的每一步对选中的子节点施加朝向父节点的力
- 选中节点后重置模拟的
alpha值,强制触发重新布局
修改后的完整代码
// parent: red, children: blue let svg, width = 800, height = 400, radius, nodes, x, y, simulation; // 改为存储节点数据对象,而非ID let parentNode = null, childNode = null; let data = [ {id: 0, size: 0.5 }, {id: 1, size: 0.25 }, {id: 2, size: 0.125 }, {id: 3, size: 0.75 }, {id: 4, size: 0.8 }, {id: 5, size: 0.4 }, {id: 6, size: 0.25 }, {id: 7, size: 0.5 } ]; const tick = () => { nodes.attr("cx", d_ => d_.x ).attr("cy", d_ => d_.y ); // 自定义吸引力逻辑:仅当父节点和子节点都选中时生效 if (parentNode && childNode) { // 计算子节点朝向父节点的向量 const dx = parentNode.x - childNode.x; const dy = parentNode.y - childNode.y; // 施加吸引力(可调整strength值控制力度) const attractionStrength = 0.05; childNode.vx += dx * attractionStrength; childNode.vy += dy * attractionStrength; } } svg = d3.select("body").append("svg").attr("viewBox", `0 0 ${width} ${height}`); let background = svg.append("rect").attr("width", width).attr("height", height).attr("fill", "#444444"); radius = d3.scaleLinear().domain([0.0, 1.0]).range([32, 64]); x = d3.scaleLinear().domain([0, data.length]).range([64, width - 64]); y = d3.scaleLinear().domain([0, data.length]).range([64, height - 64]); simulation = d3.forceSimulation() .force("x", d3.forceX(d_ => x(d_.id)).strength(0.1)) .force("y", d3.forceY(height / 2).strength(0.05)) .force("collide", d3.forceCollide().radius(d => radius(Number(d.size)) + 10)) .alpha(1).restart(); nodes = svg.selectAll(null) .data(data) .enter() .append("circle") .attr("r", d_ => radius(d_.size)) .attr("fill", "#FFFFFF") .on("mouseover", (event_, d_) => { if (d_ !== parentNode && d_ !== childNode) { d3.select(event_.currentTarget).attr("fill", "#888888"); } }) .on("mouseout", (event_, d_) => { if (d_ !== parentNode && d_ !== childNode) { d3.select(event_.currentTarget).attr("fill", "#FFFFFF"); } }) .on("click", (event_, d_) => { if (parentNode === null) { // 选中父节点 parentNode = d_; d3.select(event_.currentTarget).attr("fill", "#FF0000"); } else if (d_ !== parentNode) { // 选中子节点 childNode = d_; d3.select(event_.currentTarget).attr("fill", "#0000FF"); // 重置模拟alpha,强制重新布局 simulation.alpha(0.8).restart(); } }); simulation.nodes(data).on("tick", tick);
<script src="https://d3js.org/d3.v7.min.js"></script>
代码解释
- 选中节点存储:将原有的ID存储改为直接存储节点数据对象,避免额外的节点查找开销,同时简化坐标获取逻辑。
- 自定义吸引力:在
tick回调中,当父节点和子节点都被选中时,计算两者间的位移向量,给子节点的速度属性(vx/vy)添加分量,实现子节点朝向父节点的移动效果,可通过调整attractionStrength控制吸引力大小。 - 模拟重置:选中子节点后调用
simulation.alpha(0.8).restart(),提高模拟的活跃度,让所有节点重新进入动态布局状态,实现其余节点的重新排列。 - 交互逻辑优化:鼠标悬停/移出的判断条件改为直接对比节点对象,避免ID重复带来的潜在问题。
内容的提问来源于stack exchange,提问作者toowren
相关产品推荐
相关产品推荐

