D3.js 重新启动模拟时节点跳转添加或删除节点

Posted

技术标签:

【中文标题】D3.js 重新启动模拟时节点跳转添加或删除节点【英文标题】:D3.js nodes jump when restarting simulation to add or remove nodes 【发布时间】:2021-05-26 00:24:52 【问题描述】:

我正在使用带有强制布局的 d3.js v6 来表示网络图。 我正在添加和删除节点,但是当我重新启动模拟时,所有节点都会跳到左上角,然后又回到原来的位置。

我有以下代码 sn-p 可以准确显示我的意思,我在网上看到了其他运行良好但无法找到我做错的示例,非常感谢任何帮助。

var dataset = 
  nodes: [
    
      id: 1
    , 
    
      id: 2
    
  ],
  links: [
    id: 1,
    source: 1,
    target: 2
  ]
;

let switchBool = false;

let svg = d3.select('svg')
           .attr('width', '100%')
           .attr('height', '100%');

const width = svg.node()
  .getBoundingClientRect().width;
const height = svg.node()
  .getBoundingClientRect().height;

console.log(`$width, $height`);

svg = svg.append('g');

svg.append('g')
  .attr('class', 'links');

svg.append('g')
  .attr('class', 'nodes');

const simulation = d3.forceSimulation();
initSimulation();

let link = svg.select('.links')
    .selectAll('line');
  
loadLinks();

let node = svg.select('.nodes')
    .selectAll('.node');
  
loadNodes();
restartSimulation();

function initSimulation() 
    simulation
  .force('link', d3.forceLink())
  .force('charge', d3.forceManyBody())
  .force('collide', d3.forceCollide())
  .force('center', d3.forceCenter())
  .force('forceX', d3.forceX())
  .force('forceY', d3.forceY());

  simulation.force('center')
    .x(width * 0.5)
    .y(height * 0.5);

  simulation.force('link')
    .id((d) => d.id)
    .distance(100)
    .iterations(1);

  simulation.force('collide')
    .radius(10);

  simulation.force('charge')
    .strength(-100);


function loadLinks() 
    link = svg.select('.links')
    .selectAll('line')
    .data(dataset.links, (d) => d.id)
    .join(
      enter => enter.append('line').attr('stroke', '#000000'),
    );


function loadNodes() 
    node = svg.select('.nodes')
    .selectAll('.node')
    .data(dataset.nodes, (d) => d.id)
    .join(
      enter => 
        const nodes = enter.append('g')
          .attr('class', 'node')
        nodes.append('circle').attr('r', 10);
        return nodes;
      ,
    );


function restartSimulation() 
  simulation.nodes(dataset.nodes);
  simulation.force('link').links(dataset.links);
  simulation.alpha(1).restart();
  simulation.on('tick', ticked);


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);

  node.attr('transform', (d) => `translate($d.x,$d.y)`);


function updateData() 
    switchBool = !switchBool;
  if (switchBool) 
    dataset.nodes.push(id: 3);
    dataset.links.push(id: 2, source: 1, target: 3);
   else 
    dataset.nodes.pop();
    dataset.links.pop();
  
  
  loadLinks();
  loadNodes();
    restartSimulation();
<script src="https://d3js.org/d3.v6.min.js"></script>
<div>
  <button onclick="updateData()">Add/Remove</button>
  <svg></svg>
</div>

【问题讨论】:

【参考方案1】:

我实际上刚刚找到了问题的解决方案。

forceXforceY 都有默认参数,这意味着有一个力将节点推向 (0,0),更改了这段代码我能够修复它:

.force('x', d3.forceX().x(width * 0.5))
.force('y', d3.forceY().y(height * 0.5));

【讨论】:

【参考方案2】:

这是因为您使用了d3.forceCenter(),它不会将节点强制到一个中心点:

中心力均匀地平移节点,使得均值 所有节点的位置(如果所有节点都相等,则为质心 weight) 在给定的位置⟨x,y⟩。 (docs)

因此,如果您的两个节点直接位于 d3.forceCenter 的中心点下方/上方,则质量是平衡的。引入一个新节点并且必须转换整个力以使质心成为中心。这个翻译就是你看到的跳跃。

移除 forceCenter 并使用 d3.forceX 和 d3.forceY 指定中心值,这会将节点推向指定的 x 和 y 值:

var dataset = 
  nodes: [
    
      id: 1
    , 
    
      id: 2
    
  ],
  links: [
    id: 1,
    source: 1,
    target: 2
  ]
;

let switchBool = false;

let svg = d3.select('svg')
           .attr('width', '100%')
           .attr('height', '100%');

const width = svg.node()
  .getBoundingClientRect().width;
const height = svg.node()
  .getBoundingClientRect().height;

console.log(`$width, $height`);

svg = svg.append('g');

svg.append('g')
  .attr('class', 'links');

svg.append('g')
  .attr('class', 'nodes');

const simulation = d3.forceSimulation();
initSimulation();

let link = svg.select('.links')
    .selectAll('line');
  
loadLinks();

let node = svg.select('.nodes')
    .selectAll('.node');
  
loadNodes();
restartSimulation();

function initSimulation() 
    simulation
  .force('link', d3.forceLink())
  .force('charge', d3.forceManyBody())
  .force('collide', d3.forceCollide())
  .force('forceX', d3.forceX().x(width/2))
  .force('forceY', d3.forceY().y(height/2));



  simulation.force('link')
    .id((d) => d.id)
    .distance(100)
    .iterations(1);

  simulation.force('collide')
    .radius(10);

  simulation.force('charge')
    .strength(-100);


function loadLinks() 
    link = svg.select('.links')
    .selectAll('line')
    .data(dataset.links, (d) => d.id)
    .join(
      enter => enter.append('line').attr('stroke', '#000000'),
    );


function loadNodes() 
    node = svg.select('.nodes')
    .selectAll('.node')
    .data(dataset.nodes, (d) => d.id)
    .join(
      enter => 
        const nodes = enter.append('g')
          .attr('class', 'node')
        nodes.append('circle').attr('r', 10);
        return nodes;
      ,
    );


function restartSimulation() 
  simulation.nodes(dataset.nodes);
  simulation.force('link').links(dataset.links);
  simulation.alpha(1).restart();
  simulation.on('tick', ticked);


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);

  node.attr('transform', (d) => `translate($d.x,$d.y)`);


function updateData() 
    switchBool = !switchBool;
  if (switchBool) 
    dataset.nodes.push(id: 3);
    dataset.links.push(id: 2, source: 1, target: 3);
   else 
    dataset.nodes.pop();
    dataset.links.pop();
  
  
  loadLinks();
  loadNodes();
    restartSimulation();
<script src="https://d3js.org/d3.v6.min.js"></script>
<div>
  <button onclick="updateData()">Add/Remove</button>
  <svg></svg>
</div>

【讨论】:

以上是关于D3.js 重新启动模拟时节点跳转添加或删除节点的主要内容,如果未能解决你的问题,请参考以下文章

D3.js 使用缩放zoom时节点无法拖动,只能整体移动的问题

如何检查没有链接的节点的d3 js力图并删除它们?

创建、重命名或删除文件夹时 ASP.NET 重新启动

使用 d3.js 保存和重新加载强制布局

Vue项目中实现描点跳转scrollIntoView-案例

Vue项目中实现描点跳转scrollIntoView-案例