用特定的目标终点运用D3js力导向图模拟

5

我正在尝试呈现一个d3js力模拟,但我希望确保我的节点不传递错误信息。

使用以下代码显示节点,但由于力布局的动态性质,它有时会将一些节点推出其适当的x坐标位置。

 inOrder(){
  this.simulation
    .force("x", d3.forceX(d => this.xScale(d.value)))
    .force("y",  d3.forceY(this.height / 2))
    .alpha(1).restart();
},

这里有一个极端的例子:数字应该从左到右排列。 Out of order nodes 我试图使用fx属性锁定节点的位置:
inOrder(){
  this.releases.forEach(x => {
    x.fx = this.xScale(x.value)
  })

  this.simulation
    .force("x", d3.forceX(d => this.xScale(d.value)))
    .force("y",  d3.forceY(this.height / 2))
    .alpha(1).restart();
},

Nodes in order

这段代码可以保留x轴位置,但是当调用inOrder方法时,节点会瞬间跳到它们最终的x轴位置。这破坏了力学仿真的流畅和动态性。

有没有办法同时做到两者兼备?也许可以使用.on("end", () => {}).on("tick", () => {})事件处理器吗?

Mike Bostock (https://stackoverflow.com/users/365814/mbostock) 和 Shan Carter 创建了一些作品,启发了我在这里所尝试的:

点击 Changes 和 Department totals 选项卡之间进行切换 https://archive.nytimes.com/www.nytimes.com/interactive/2012/02/13/us/politics/2013-budget-proposal-graphic.html?hp

点击 The Overall Picture 和 View By Industry 选项卡之间进行切换 https://archive.nytimes.com/www.nytimes.com/interactive/2013/05/25/sunday-review/corporate-taxes.html

1个回答

2
我可能漏掉了一些东西,但调整x轴定位力(和y轴)的强度可以帮助确保您的排序完成得正确。forceX或forceY的默认强度为0.1,其实现方式如下:
一个值为0.1表示节点应该在每次应用时从其当前x位置移动到目标x位置的十分之一。更高的值会更快地将节点移动到目标位置,但往往会牺牲其他力量或约束条件。不推荐使用超出范围[0,1]的值。 (来源:docs)
因此,我们可以增加forceX的强度,并减少forceY以允许节点在x轴上更自由地移动 - 使节点更容易跳过彼此 - 减少碰撞强度也可能有所帮助。
我没有给下面的圆圈打标签(而是按顺序着色),但我会检查它们是否有序(在模拟结束时记录到控制台),以下代码片段仅修改x和y力(而不是碰撞力)。

enter image description here

var height = 300;
var width = 500;

var data = d3.range(30).map(function(d,i) {
  return { size: Math.random()+1, index: i}
});

    
var svg = d3.select("body")
  .append("svg")
  .attr("width",width)
  .attr("height",height);
  
var x = d3.scaleLinear()
  .domain([1,30])
  .range([50,width-50]);
  
var color = d3.scaleLinear()
  .domain([0,29])
  .range(["#ccc","#000"])

var simulation = d3.forceSimulation()
    .force("x", d3.forceX(d => x(d.index)).strength(0.20))
    .force("y",  d3.forceY(height / 2).strength(0.05))
    .force("collide", d3.forceCollide().radius(d=> d.size*10))
    .alpha(1).restart();
    
var circles = svg.selectAll(null)
  .data(data)
  .enter()
  .append("circle")
  .attr("r", function(d) { return d.size * 10; })
  .attr("fill", function(d,i) { return color(i); })
    
simulation.nodes(data)
  .on("tick",tick)
  .on("end",verify);
  

function tick() {
  circles
  .attr("cx", function(d) { return d.x; })
  .attr("cy", function(d) { return d.y; })
}

function verify() {
  var n = 0;
  for(var i = 0; i < data.length - 1; i++) {
     if(data[i].x > data[i+1].x) n++;
  }
  console.log(n + " out of place");
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

这段代码在一个500x300的区域内放置了30个圆形,没有什么问题:我已经测试过几次,没有一个圆形错位。但是在这里放置100个圆形会出现问题:在如此狭窄的区域内,圆形将无法改变位置:可能需要进一步修改力的设置,但更大的绘图区域可能更合适(而不是小的代码片段视图)。
另一个选择是在模拟成熟过程中修改力:开始时x方向力强,碰撞力小,然后逐渐增加碰撞力,以使随后的碰撞最小化。这里有一个 示例 ,演示了如何在tick函数中修改力 - 尽管此示例是针对链接长度而非x轴上的位置,但适应应该不会太困难。
另一个可能性是保持alpha值高,直到所有圆圈在x轴上正确排序,然后开始降温,同样需要在tick函数中进行。

你给了我很多东西可以尝试。调整力X的强度似乎有所帮助......但是它太"快"了......这是一种非常刺耳的体验。 - RoboKozo
@Robodude,是的,forceX和forceY的微小变化可能会极大地改变结果——如果您注意到绘图大小以及圆圈数量(和平均半径),我可能能够展示更好的示例。 - Andrew Reid
你对在一个tick内操纵力量/碰撞的想法似乎非常有趣。我正在测试它们,也许我能够实现它。 - RoboKozo
目前我正在进行这个操作,感觉还不错。 .on("tick", () => { const increasingWithTime = 1 - this.simulation.alpha(); this.collider.strength(increasingWithTime); forceX.strength(increasingWithTime); }) - RoboKozo
这可能会让我更接近目标,但我的x轴将映射到日期,它们非常重要。我不知道是否可以让模拟器帮我“足够接近”。 - RoboKozo
我认为如果在alpha达到一定点后的tick内设置fx值就足够了。感谢您的建议。 - RoboKozo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接