d3js prevent forceSimulation from recalculating po

2020-04-14 03:13发布

问题:

I'm trying to build a force layout that will allow me to visualize the flow of objects in a system.
I want to show how many objects are on a specific state and when a state change I want to update my graph.

I've built a prototype, but I've noticed that D3.js is recalculating transform of each node even when they don't need to move:

can this be fixed? maybe there is an option to add minimum value for an update?

I've declared force layout this way:

const force = d3.forceSimulation()
  .force('link', d3.forceLink().id((d) => d.id).distance(150))
  .force('charge', d3.forceManyBody().strength(-500))
  .force('x', d3.forceX(width / 2))
  .force('y', d3.forceY(height / 2))
  .on('tick', tick);

After changing alphaTarget to alpha recalculation stopped, but I got another bug:
I've added drag functionality and it stopped working with above changes.
Here is the version with fixed recalculation but with drag problem.

回答1:

The culprit is in your restart() function:

force.alphaTarget(0.3).restart();

The way you are reheating your simulation by setting .alphaTarget(0.3) is not correct. alphaTarget is a configuration parameter which controls the way alpha decreases. Figuratively speaking, alpha—for as long as it is greater than alphaMin— is headed towards alphaTarget. The heat in the system is measured by alpha which can be thought of as dynamic data; alphaTarget, on the other hand, resembles more or less static data.

Furthermore, it is important to have alphaTarget set to a value less than alphaMin or else your simulation is going to run indefinitely because alpha, while on its way towards alphaTarget so to speak, is never going to be less than alphaMin.

Thus, if you want to reheat your system, you have to manipulate alpha instead of alphaTarget. Changing above mentioned line to the following is all it takes to get the desired effect.

force.alpha(0.3).restart();

Have a look at the following snippet, which is basically a fork of your JSFiddle, to see it in action.

document.getElementById("a").addEventListener("click", function() {
  AddNewLink(null, 1);
});
document.getElementById("b").addEventListener("click", function() {
  AddNewLink(1, 2);
});
document.getElementById("c").addEventListener("click", function() {
  AddNewLink(2, 3);
});
document.getElementById("d").addEventListener("click", function() {
  AddNewLink(1, 3);
});
document.getElementById("e").addEventListener("click", function() {
  AddNewLink(3, 4);
});
document.getElementById("f").addEventListener("click", function() {
  AddNewLink(4, 5);
});

function AddNewLink(from, to) {
  var startNode;
  var start = availableNodes.find(x => x.id === from);
  if (start !== undefined) {
    //check if this node is already added
    var foundStart = nodes.find(x => x.id == start.id);
    if (foundStart === undefined) {
      nodes.push(start);
      startNode = start;
    } else {
      foundStart.value--;
      if (foundStart.value < 0) foundStart.value = 0;
      startNode = foundStart;
    }
  }

  var endNode;
  var end = availableNodes.find(x => x.id === to);
  if (end !== undefined) {
    //check if this node is already added
    var foundEnd = nodes.find(x => x.id == end.id);
    if (foundEnd === undefined) {
      nodes.push(end);
      endNode = end;
      end.value++;
    } else {
      foundEnd.value++;
      endNode = foundEnd;
    }
  }
  //console.log(startNode, endNode);

  if (startNode !== undefined && endNode !== undefined) {
    links.push({
      source: startNode,
      target: endNode
    });
  }

  restart();
}



// set up SVG for D3
const width = 400;
const height = 400;
const colors = d3.scaleOrdinal(d3.schemeCategory10);

const svg = d3.select('svg')
  .on('contextmenu', () => {
    d3.event.preventDefault();
  })
  .attr('width', width)
  .attr('height', height);

var availableNodes = [{
  id: 1,
  name: "Start",
  value: 0,
  reflexive: false
}, {
  id: 2,
  name: "Node 1",
  value: 0,
  reflexive: false
}, {
  id: 3,
  name: "Node 2",
  value: 0,
  reflexive: false
}, {
  id: 4,
  name: "Node 3",
  value: 0,
  reflexive: false
}, {
  id: 5,
  name: "Finish",
  value: 0,
  reflexive: false
}];

// set up initial nodes and links
//  - nodes are known by 'id', not by index in array.
//  - reflexive edges are indicated on the node (as a bold black circle).
//  - links are always source < target; edge directions are set by 'left' and 'right'.
let nodes = [
  availableNodes[0], availableNodes[1], availableNodes[2]
];
let links = [{
    source: nodes[0],
    target: nodes[1]
  },
  {
    source: nodes[1],
    target: nodes[2]
  }
];

// init D3 force layout
const force = d3.forceSimulation()
  .force('link', d3.forceLink().id((d) => d.id).distance(150))
  .force('charge', d3.forceManyBody().strength(-500))
  .force('x', d3.forceX(width / 2))
  .force('y', d3.forceY(height / 2))
  .on('tick', tick);

// define arrow markers for graph links
svg.append('svg:defs').append('svg:marker')
  .attr('id', 'end-arrow')
  .attr('viewBox', '0 -5 10 10')
  .attr('refX', 8)
  .attr('markerWidth', 3)
  .attr('markerHeight', 3)
  .attr('orient', 'auto')
  .append('svg:path')
  .attr('d', 'M0,-5L10,0L0,5')
  .attr('fill', '#000');


// handles to link and node element groups
let path = svg.append('svg:g').attr('id', 'lines').selectAll('path');
let circle = svg.append('svg:g').attr('id', 'circles').selectAll('g');

// update force layout (called automatically each iteration)
function tick() {
  // draw directed edges with proper padding from node centers
  path.attr('d', (d) => {
    const deltaX = d.target.x - d.source.x;
    const deltaY = d.target.y - d.source.y;
    const dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
    const normX = deltaX / dist;
    const normY = deltaY / dist;
    const sourcePadding = d.left ? 17 : 12;
    const targetPadding = d.right ? 17 : 12;
    const sourceX = d.source.x + (sourcePadding * normX);
    const sourceY = d.source.y + (sourcePadding * normY);
    const targetX = d.target.x - (targetPadding * normX);
    const targetY = d.target.y - (targetPadding * normY);

    return `M${sourceX},${sourceY}L${targetX},${targetY}`;
  });

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

// update graph (called when needed)
function restart() {
  // path (link) group
  path = path.data(links);

  // remove old links
  path.exit().remove();

  // add new links
  path = path.enter().append('svg:path')
    .attr('class', 'link')
    .style('marker-end', 'url(#end-arrow)')
    .merge(path);

  // circle (node) group
  // NB: the function arg is crucial here! nodes are known by id, not by index!
  circle = circle.data(nodes, (d) => d.id);

  // update existing nodes (reflexive & selected visual states)
  circle.selectAll('circle')
    .style('fill', (d) => colors(d.id))
    .classed('reflexive', (d) => d.reflexive);

  circle.selectAll('text.value').text((d) => d.value);

  // remove old nodes
  circle.exit().remove();

  // add new nodes
  const g = circle.enter().append('svg:g');

  g.append('svg:circle')
    .attr('class', 'node')
    .attr('r', 12)
    .style('fill', (d) => colors(d.id))
    .style('stroke', (d) => d3.rgb(colors(d.id)).darker().toString())
    .classed('reflexive', (d) => d.reflexive)

  // show node IDs
  g.append('svg:text')
    .attr('x', 0)
    .attr('y', 4)
    .attr('class', 'value')
    .text((d) => d.value);

  g.append('svg:text')
    .attr('x', 20)
    .attr('y', 4)
    .attr('class', 'name')
    .text((d) => d.name);

  circle = g.merge(circle);

  // set the graph in motion
  force
    .nodes(nodes)
    .force('link').links(links);

  force.alpha(0.3).restart();
}

restart();
svg {
  background-color: #FFF;
  cursor: default;
  user-select: none;
}

path.link {
  fill: none;
  stroke: #000;
  stroke-width: 3px;
  cursor: default;
}

path.link.selected {
  stroke-dasharray: 10, 2;
}

path.link.dragline {
  pointer-events: none;
}

path.link.hidden {
  stroke-width: 0;
}

circle.node.reflexive {
  stroke: #000 !important;
  stroke-width: 2.5px;
}

text {
  font: 12px sans-serif;
  pointer-events: none;
}

text.value {
  text-anchor: middle;
  font-weight: bold;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.9.1/d3.js"></script>
<button id='a'>1</button>
<button id='b'>1>2</button>
<button id='c'>2>3</button>
<button id='d'>1>3</button>
<button id='e'>3>4</button>
<button id='f'>4>5</button>
<svg width="400" height="400"></svg>