d3js始终防止forceSimulation重新计算位置 [英] d3js prevent forceSimulation from recalculating position all the time

查看:148
本文介绍了d3js始终防止forceSimulation重新计算位置的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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.

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

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

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

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.

推荐答案

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

The culprit is in your restart() function:

force.alphaTarget(0.3).restart();

通过设置.alphaTarget(0.3)重新加热模拟的方式不正确. alphaTarget是控制alpha减小方式的配置参数.形象地讲,alpha—只要它大于alphaMin—朝向alphaTarget.系统中的热量通过alpha测量,可以将其视为动态数据.另一方面,alphaTarget或多或少类似于静态数据.

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.

此外,将alphaTarget设置为小于alphaMin的值非常重要,否则您的模拟将无限期地运行,因为可以说,alpha在走向alphaTarget的过程中永远不会小于alphaMin.

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.

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

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

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

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>

这篇关于d3js始终防止forceSimulation重新计算位置的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆