Узлы 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, 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>


person Franco Cuevas    schedule 23.02.2021    source источник


Ответы (2)


Это потому, что вы используете d3.forceCenter(), который не принуждает узлы к центральной точке:

Центрирующая сила перемещает узлы равномерно, так что среднее положение всех узлов (центр масс, если все узлы имеют одинаковый вес) находится в заданном положении ⟨x, y⟩. (документы)

Итак, если ваши два узла расположены прямо и одинаково ниже/выше центральной точки для 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>

person Andrew Reid    schedule 23.02.2021

Я действительно только что нашел решение проблемы.

У меня были и forceX, и forceY с параметрами по умолчанию, что означало, что была сила, толкающая узел к (0,0), изменив этот фрагмент кода, я смог это исправить:

.force('x', d3.forceX().x(width * 0.5))
.force('y', d3.forceY().y(height * 0.5));
person Franco Cuevas    schedule 23.02.2021