d3js始终防止forceSimulation重新计算位置

时间:2019-04-17 15:03:30

标签: javascript d3.js

我正在尝试构建一种力的布局,使我可以可视化系统中对象的流动。
我想显示在特定状态下有多少个对象,以及当状态更改时我想更新我的图。

我已经built a prototype,但是我注意到D3.js正在重新计算每个节点的变换,即使它们不需要移动:

enter image description here

可以解决吗?也许有一个选项可以为更新添加最小值?

我以这种方式声明了强制布局:

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

alphaTarget更改为alpha后,重新计算停止了,但是又出现了另一个错误:
我添加了拖动功能,但它停止了上述更改。
Here是具有固定重新计算但存在拖动问题的版本。

1 个答案:

答案 0 :(得分:3)

罪魁祸首在您的restart()函数中:

force.alphaTarget(0.3).restart();

通过设置.alphaTarget(0.3)重新加热模拟的方式不正确。 alphaTarget是一个配置参数,用于控制alpha减小的方式。形象地说,只要alpha大于alphaMin,就朝着alphaTarget前进。系统中的热量由alpha测量,可以认为是动态数据。另一方面,alphaTarget类似于或多或少的静态数据。

此外,将alphaTarget设置为小于alphaMin的值非常重要,否则您的模拟将无限期地运行,因为alpha正在朝{{1 }}可以说永远不会小于alphaTarget

因此,如果要重新加热系统,则必须操纵alphaMin而不是alpha。将上面提到的行更改为以下内容即可获得所需的效果。

alphaTarget

看看下面的代码片段,它实际上是您的JSFiddle的一个分支,以了解它的实际作用。

force.alpha(0.3).restart();
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;
}