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

D3.js nodes jump when restarting simulation to add or remove nodes

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

我有以下代码片段,它准确地表达了我的意思,我在网上看到其他示例运行良好,但未能找到我做错了什么,非常感谢您的帮助。

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>

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

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

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

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

The centering force translates nodes uniformly so that the mean position of all nodes (the center of mass if all nodes have equal weight) is at the given position ⟨x,y⟩. (docs)

因此,如果您的两个节点直接且相等地位于 below/above 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>