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

D3.js v7力导向图如何新增节点与链接并实现平滑重渲染

D3.js v7 力导向图动态新增节点与边实现方案

直接向nodes和links数组追加数据的核心思路是正确的,功能不生效是因为缺少了数据更新后同步DOM、更新力模拟配置、重启模拟三个关键步骤。

具体修改逻辑

  1. 追加完新节点和边数据后,将新数据同步给力模拟实例
  2. 重新执行D3的data-join逻辑,渲染新增的节点和边DOM元素
  3. 给新增的节点绑定拖拽事件、补充圆形、文本等子元素
  4. 重置力模拟的热度系数,重启模拟实现平滑过渡

完整可运行代码

<script src="https://d3js.org/d3.v7.min.js"></script>
<div class="container"></div>
.my-button {
  position: absolute;
  top: 0;
  left: 0;
}
const graph = {
  "nodes": [
    { "id": "A", "group": 1 },
    { "id": "B", "group": 2 },
    { "id": "C", "group": 2 },
    { "id": "D", "group": 3 },
    { "id": "E", "group": 3 }
  ],
  "links": [
    { "source": "A", "target": "B", "value": 1, type: 'default' },
    { "source": "A", "target": "C", "value": 2, type: 'default' },
    { "source": "B", "target": "D", "value": 2, type: 'default' },
    { "source": "A", "target": "E", "value": 4, type: 'default' },
    { "source": "A", "target": "D", "value": 2, type: 'default' }
  ]
}
const links = graph.links.map(d => Object.create(d));
const nodes = graph.nodes.map(d => Object.create(d));

const simulation = d3
  .forceSimulation(nodes)
  .force(
    'link',
    d3.forceLink(links).id(d => d.id),
  )
  .force('charge', d3.forceManyBody().strength(-300))
  .force('x', d3.forceX())
  .force('y', d3.forceY())
  .force(
    'collide',
    d3.forceCollide(d => 65),
  )
  .force('center', d3.forceCenter(500, 500));

const drag = (simulation) => {
  function dragstarted(event, d) {
    if (!event.active) simulation.alphaTarget(0.3).restart();
    d.fx = d.x;
    d.fy = d.y;
  }

  function dragged(event, d) {
    d.fx = event.x;
    d.fy = event.y;
  }

  function dragended(event, d) {
    if (!event.active) simulation.alphaTarget(0);
    d.fx = null;
    d.fy = null;
  }

  return d3.drag().on('start', dragstarted).on('drag', dragged).on('end', dragended);
};

const svg = d3
  .select('.container')
  .append('svg')
  .attr('preserveAspectRatio', 'xMinYMin meet')
  .attr('viewBox', '0 0 1000 1000')
  .classed('svg-content', true)
  .call(d3.zoom().on('zoom', e => svg.attr('transform', e.transform)))
  .append('g');

// 生成通用箭头
svg
  .append('defs')
  .append('marker')
  .attr('id', 'arrow-default')
  .attr('viewBox', '0 -5 10 10')
  .attr('refX', 38)
  .attr('refY', 0)
  .attr('markerWidth', 6)
  .attr('markerHeight', 6)
  .attr('orient', 'auto')
  .append('path')
  .attr('fill', '#262626')
  .attr('d', 'M0,-5L10,0L0,5');

let link = svg
  .append('g')
  .attr('fill', 'none')
  .attr('stroke-width', 1.5)
  .selectAll('path')
  .data(links)
  .join('path')
  .attr('stroke', 'green')
  .attr('marker-end', d => `url(#arrow-default)`);

let node = svg
  .append('g')
  .attr('fill', 'currentColor')
  .attr('stroke-linecap', 'round')
  .attr('stroke-linejoin', 'round')
  .selectAll('g')
  .data(nodes)
  .join('g')
  .call(drag(simulation));

node
  .append('circle')
  .attr('stroke', 'white')
  .attr('stroke-width', 1.5)
  .attr('r', 25)
  .attr('fill', d => '#6baed6');

node
  .append('text')
  .attr('x', 34)
  .attr('y', '0.31em')
  .text(d => d.id)
  .clone(true)
  .lower()
  .attr('fill', 'none')
  .attr('stroke', 'white')
  .attr('stroke-width', 3);

node.on('dblclick', (e, d) => console.log(nodes[d.index]));

const clickHandler = a => {
  // 避免重复添加节点
  if (nodes.find(item => item.id === 'F')) return
  nodes.push({
    id: 'F',
    group: 1
  });
  nodes.push({
    id: 'G',
    group: 1
  });
  links.push({
    source: 'A',
    target: 'F',
    value: 2,
    type: 'default'
  });
  links.push({
    source: 'F',
    target: 'G',
    value: 2,
    type: 'default'
  });

  // 同步新数据到力模拟
  simulation.nodes(nodes)
  simulation.force('link').links(links)

  // 更新边DOM
  link = link.data(links).join('path')
    .attr('stroke', 'green')
    .attr('marker-end', d => `url(#arrow-default)`);

  // 更新节点DOM
  node = node.data(nodes).join(
    enter => {
      const newNode = enter.append('g').call(drag(simulation))
      newNode.append('circle')
        .attr('stroke', 'white')
        .attr('stroke-width', 1.5)
        .attr('r', 25)
        .attr('fill', '#6baed6');
      newNode.append('text')
        .attr('x', 34)
        .attr('y', '0.31em')
        .text(d => d.id)
        .clone(true)
        .lower()
        .attr('fill', 'none')
        .attr('stroke', 'white')
        .attr('stroke-width', 3);
      newNode.on('dblclick', (e, d) => console.log(nodes[d.index]));
      return newNode
    }
  )

  // 重启力模拟实现平滑动画
  simulation.alpha(0.3).restart()
};

simulation.on('tick', () => {
  link.attr('d', d => `M${d.source.x},${d.source.y}A0,0 0 0,1 ${d.target.x},${d.target.y}`);
  node.attr('transform', d => `translate(${d.x},${d.y})`);
});

d3.select('.container').append('button').attr('class', 'my-button').text('add data').on('click', clickHandler);

const color = (types) => d3.scaleOrdinal(types, d3.schemeCategory10);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.24 12:24:04