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

问题描述

我使用带有强制布局的 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,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.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,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));