如何在D3力模拟中实现圆盘形状?

时间:2019-01-24 10:10:32

标签: javascript d3.js data-visualization visualization force-layout

我正在尝试从Nadieh Bremer和Shirely Wu的Bussed out中重建令人敬畏的“点流”可视化效果。

original bubbles

“气泡”的非常圆形的形状和点到达气泡的点处的流体动力学般的压缩(黑色箭头)使我特别感兴趣。

我的目的是通过.fx.fy(黑点)创建(三个)固定节点,并将所有其他节点链接到相应的固定节点。当我降低力使动画运行慢一点时,结果看起来非常混乱,气泡甚至没有在它们的中心节点周围形成。

  const simulation = d3.forceSimulation(nodes)
    .force("collide", d3.forceCollide((n, i) => i < 3 ? 0 : 7))
    .force("links", d3.forceLink(links).strength(.06))

关于力设置的任何想法都会产生更美观的结果?

我确实知道我必须随着时间的推移对小组分配进行动画处理才能获得“ tri流”效果(否则所有点都将蜂拥到目的地),但是我想从一个不错的开始模拟的稳态。

编辑

我确实检查了源代码,只是出于性能原因,它只是重放预先记录的模拟数据。

my result

3 个答案:

答案 0 :(得分:2)

Nadieh Bremer是我在D3视觉形象中的偶像,她是绝对的明星! (在OP的comment之后进行的更正:看来此数据视图是由Shirley Wu创建的……无论如何,这并没有改变我对不来梅所说的话。)

找出该页面上正在发生的事情的第一次尝试是查看source code,不幸的是,这是一项艰巨的工作。因此,剩下的选择就是尝试重现这一点。

这里的挑战不是创建圆形图案,这很容易:您只需结合forceXforceYforceCollide

const svg = d3.select("svg")
const data = d3.range(500).map(() => ({}));

const simulation = d3.forceSimulation(data)
  .force("x", d3.forceX(200))
  .force("y", d3.forceY(120))
  .force("collide", d3.forceCollide(4))
  .stop();

for (let i = 300; i--;) simulation.tick();

const circles = svg.selectAll(null)
  .data(data)
  .enter()
  .append("circle")
  .attr("r", 2)
  .style("fill", "tomato")
  .attr("cx", d => d.x)
  .attr("cy", d => d.y);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="400" height="300"></svg>

这里真正的挑战是像我here一样,将这些圆圈逐个移动到给定的模拟中,而不是同时全部移动。

所以,这是我的建议/尝试:

我们创建一个模拟,然后停止...

simulation.stop();

然后,在计时器中...

const timer = d3.interval(function() {etc...

...我们将节点添加到仿真中:

const newData = data.slice(0, index++)
simulation.nodes(newData);

这是结果,单击按钮:

const radius = 2;
let index = 0;
const limit = 500;
const svg = d3.select("svg")
const data = d3.range(500).map(() => ({
  x: 80 + Math.random() * 40,
  y: 80 + Math.random() * 40
}));

let circles = svg.selectAll(null)
  .data(data);
circles = circles.enter()
  .append("circle")
  .attr("r", radius)
  .style("fill", "tomato")
  .attr("cx", d => d.x)
  .attr("cy", d => d.y)
  .style("opacity", 0)
  .merge(circles);

const simulation = d3.forceSimulation()
  .force("x", d3.forceX(500))
  .force("y", d3.forceY(100))
  .force("collide", d3.forceCollide(radius * 2))
  .stop();

function ticked() {
  circles.attr("cx", d => d.x)
    .attr("cy", d => d.y);
}

d3.select("button").on("click", function() {
  simulation.on("tick", ticked).restart();
  const timer = d3.interval(function() {
    if (index > limit) timer.stop();
    circles.filter((_, i) => i === index).style("opacity", 1)
    const newData = data.slice(0, index++)
    simulation.alpha(0.25).nodes(newData);
  }, 5)
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<button>Click</button>
<svg width="600" height="200"></svg>

这种方法存在的问题

如您所见,这里的熵太大,尤其是在中心。 Nadieh Bremer / Shirley Wu可能使用了更复杂的代码。但是现在这只是我的两分钱,让我们看看其他答案是否会以不同的方式出现。

答案 1 :(得分:2)

以Gerardo的开始为基础,

我认为,避免过度熵的要点之一是指定速度衰减-这将有助于避免超出期望的位置。太慢了,流量停止的速度不会太快而导致密度增加,节点变得混乱或过高,在太远和太短之间振荡。

许多体力在这里很有用-它可以使节点保持间隔(而不是碰撞力),节点之间的排斥力通过每个簇的定位力来抵消。下面,我使用了两个对中点和一个节点属性来确定使用哪一个。这些力量必须相当弱-强大的力量很容易导致过度矫正。

我没有使用计时器,而是使用了Simulation.find()功能,每个刻度都从一个群集中选择一个节点并切换它吸引到的中心。经过1000次滴答声后,以下模拟将停止:

var canvas = d3.select("canvas");
var width = +canvas.attr("width");
var height = +canvas.attr("height");
var context = canvas.node().getContext('2d');

// Key variables:
var nodes = [];
var strength = -0.25;         // default repulsion
var centeringStrength = 0.01; // power of centering force for two clusters
var velocityDecay = 0.15;     // velocity decay: higher value, less overshooting
var outerRadius = 250;        // new nodes within this radius
var innerRadius = 100;        // new nodes outside this radius, initial nodes within.
var startCenter = [250,250];  // new nodes/initial nodes center point
var endCenter = [710,250];	  // destination center
var n = 200;		          // number of initial nodes
var cycles = 1000;	          // number of ticks before stopping.



// Create a random node:
var random = function() {
	var angle = Math.random() * Math.PI * 2;
	var distance = Math.random() * (outerRadius - innerRadius) + innerRadius;
	var x = Math.cos(angle) * distance + startCenter[0];
	var y = Math.sin(angle) * distance + startCenter[1];

	return { 
	   x: x,
	   y: y,
	   strength: strength,
	   migrated: false
	   }
}

// Initial nodes:
for(var i = 0; i < n; i++) {
	nodes.push(random());
}
	
var simulation = d3.forceSimulation()
    .force("charge", d3.forceManyBody().strength(function(d) { return d.strength; } ))
	.force("x1",d3.forceX().x(function(d) { return d.migrated ? endCenter[0] : startCenter[0] }).strength(centeringStrength))
	.force("y1",d3.forceY().y(function(d) { return d.migrated ? endCenter[1] : startCenter[1] }).strength(centeringStrength))
	.alphaDecay(0)
	.velocityDecay(velocityDecay)
    .nodes(nodes)
    .on("tick", ticked);

var tick = 0;
	
function ticked() {
	tick++;
	
	if(tick > cycles) this.stop();
	
	nodes.push(random()); // create a node
	this.nodes(nodes);    // update the nodes.

  var migrating = this.find((Math.random() - 0.5) * 50 + startCenter[0], (Math.random() - 0.5) * 50 + startCenter[1], 10);
  if(migrating) migrating.migrated = true;
  
	
	context.clearRect(0,0,width,height);
	
	nodes.forEach(function(d) {
		context.beginPath();
		context.fillStyle = d.migrated ? "steelblue" : "orange";
		context.arc(d.x,d.y,3,0,Math.PI*2);
		context.fill();
	})
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<canvas width="960" height="500"></canvas>

这是一个block view(摘要会是整页的更好,参数是针对它的)。初始节点与后面的节点在同一环中形成(因此一开始会有一些争吵,但这很容易解决)。在每个刻度上,都会创建一个节点,并尝试将一个节点从中间附近迁移到另一侧-这样就创建了一个流(与任何随机节点相对)。

对于流体,未链接的节点可能是最好的(我一直在风模拟中使用它)-链接的节点是网或布等结构化材料的理想选择。而且,像Gerardo一样,我也是Nadieh作品的粉丝,但将来也必须关注Shirley的作品。

答案 2 :(得分:2)

借助这里的其他答案,我继续进行实验,并希望总结一下我的发现:

碟形

forceManyBody似乎比forceCollide更稳定。使用它而不会使光盘形状变形的关键是.distanceMax。不利的一面是,您的可视化不再是“无标度”的,必须手动进行调整。作为指导,在每个方向上的超调都会导致明显的伪像:

distanceMax设置得太高会使相邻的光盘变形。

distanceMax too high

distanceMax设置得太低(小于预期的光盘直径):

enter image description here

可以在Guardian可视化中看到该伪像(当红色和蓝色的点最终形成一个巨大的光盘时),因此,我很确定使用了distanceMax

节点定位

我仍然发现forceXforceY和自定义访问器函数一起使用对于复杂的动画来说太麻烦了。我决定使用“控制”节点,并进行少量调整(chargeForce.strength(-4)link.strength(.2).distance(1))就可以了。

流体感

在尝试设置时,我注意到流体的感觉(传入节点推动接受盘的边界)特别取决于simulation.velocityDecay,但是降低它会增加系统的熵。

最终结果

我的示例代码将一个“人群”分为三个,然后分为五个-在blocks上进行检查。每个接收器都由一个控制节点表示。节点被批量重新分配给新的接收器,从而可以更好地控制“流”的外观。开始挑选节点以使其更接近接收点看起来更加自然(每个动画的开头都有单个sort)。