如何在D3力导向图中添加复合节点并参与布局与连边
D3力导向图添加复合矩形节点并参与布局的实现方案
已创建D3力导向布局图,同时制作了一款矩形带文字的复合节点。希望实现该复合节点与现有圆形节点建立边连接,且能参与力导向布局。推测需要将nodes与labelNodes合并后传入.forceSimulation(nodes),但不清楚具体操作方法。
核心实现步骤
- 合并节点数据:将复合节点数据合并到主节点数组,确保每个节点
id唯一无冲突 - 更新力模拟:把合并后的节点数组传入力模拟并重启,让新节点参与布局计算
- 同步位置更新:在
ticked函数中通过transform调整复合节点的位置,保证中心对齐布局坐标 - 绑定拖拽交互:给复合节点添加拖拽事件,使其和圆形节点具备相同交互能力
- 添加连接边:在边数据中加入指向复合节点的连接,让力布局计算节点间的关联关系
修改后的完整代码
<script src="https://d3js.org/d3.v7.min.js" charset="utf-8"></script> <svg ref="chart" id="chart" class="graph"></svg>
.graph { width: 1000px; height: 400px; }
// // 创建力导向图基础结构 // const width = 1000 const height = 400 const node_data = Array.from({ length: 5 }, () => ({ group: Math.floor(Math.random() * 3), })) // 扩展边数据,加入指向复合节点的连接 const edge_data = [ ...Array.from({ length: 10 }, () => ({ source: Math.floor(Math.random() * 5), target: Math.floor(Math.random() * 5), value: Math.floor(Math.random() * 10) + 1, })), {source: 0, target: 5, value: 5}, {source: 1, target: 5, value: 5} ] const links = edge_data.map((d) => ({ ...d })) const nodes = node_data.map((d, index) => ({ id: index, ...d })) const color = d3.scaleOrdinal(d3.schemeCategory10) // // 合并复合节点数据到主节点数组 // const labelNodes = [ { id: 5, // 使用不冲突的ID name: 'A title', type: 'composite' // 标记节点类型,用于区分渲染 }, ] nodes.push(...labelNodes) const svg = d3.select('#chart') const simulation = d3 .forceSimulation(nodes) .force( 'link', d3 .forceLink(links) .id((d) => d.id) .distance((d) => 100) ) .force('charge', d3.forceManyBody()) .force('center', d3.forceCenter(width / 2, height / 2)) .on('tick', ticked) const link = svg .append('g') .attr('stroke', '#999') .attr('stroke-opacity', 0.6) .selectAll() .data(links) .join('line') .attr('stroke-width', (d) => Math.sqrt(d.value)) // 渲染圆形节点(过滤非复合节点) const circleNode = svg .append('g') .attr('stroke', '#fff') .attr('stroke-width', 1.5) .selectAll() .data(nodes.filter(d => d.type !== 'composite')) .join('circle') .attr('r', 16) .attr('fill', (d) => color(d.group)) circleNode.append('title').text((d) => `hello ${d.id}`) // // 渲染复合节点(过滤复合节点) // const compositeGroup = svg .append('g') .attr('id', 'composite') .selectAll('g') .data(nodes.filter(d => d.type === 'composite'), (d) => d.id) const g = compositeGroup.enter().append('g') g.append('rect') .attr('class', 'node') .attr('rx', '15') .attr('width', 200) .attr('height', 25) .attr('fill', '#dceed3') g.append('text') .attr('class', 'name') .attr('dominant-baseline', 'middle') .attr('font-size', '10px') .attr('x', 13) .attr('y', 13) .text((d) => d.name) const compositeNode = g.merge(compositeGroup) // 给复合节点绑定拖拽事件 compositeNode.call(d3.drag().on('start', dragstarted).on('drag', dragged).on('end', dragended)) function ticked() { 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) // 更新圆形节点位置 circleNode.attr('cx', (d) => d.x).attr('cy', (d) => d.y) // 更新复合节点位置:将布局中心坐标转换为g元素的transform,偏移矩形左上角实现中心对齐 compositeNode.attr('transform', d => { return `translate(${d.x - 100}, ${d.y - 12.5})` }) } function dragstarted(event) { if (!event.active) simulation.alphaTarget(0.3).restart() event.subject.fx = event.subject.x event.subject.fy = event.subject.y } function dragged(event) { event.subject.fx = event.x event.subject.fy = event.y } function dragended(event) { if (!event.active) simulation.alphaTarget(0) event.subject.fx = null event.subject.fy = null }
关键改动说明
- 节点数据合并:给复合节点设置唯一ID(5)并标记类型,避免和原有节点冲突,直接合并到主节点数组
- 边数据扩展:新增连接到复合节点的边,让力布局能计算节点间的引力关系
- 分类型渲染:通过
filter分别处理圆形节点和复合节点的渲染逻辑,避免渲染混淆 - 位置对齐调整:在
ticked函数中通过transform偏移复合节点的g元素,保证节点中心与布局坐标对齐 - 交互同步:给复合节点绑定和圆形节点一致的拖拽事件,保持交互体验统一
内容的提问来源于stack exchange,提问作者Eric G
相关产品推荐
相关产品推荐

