如何在D3力模拟中实现圆盘形状?
How to achieve disc shape in D3 force simulation?
我正在尝试重现 Nadieh Bremer 和 Shirely Wu Bussed out 的精彩 'dot flow' 可视化效果。
我对 'bubbles' 的非常圆的形状和点到达气泡(黑色箭头)处的类似流体动力学的压缩特别感兴趣。
我的做法是通过 .fx
和 .fy
(黑点)和 link 将所有其他节点创建(三个)固定节点到相应的固定节点。结果看起来很乱,气泡甚至没有在它们的中心节点周围形成,当我降低力使动画运行得慢一点时。
const simulation = d3.forceSimulation(nodes)
.force("collide", d3.forceCollide((n, i) => i < 3 ? 0 : 7))
.force("links", d3.forceLink(links).strength(.06))
关于力设置的任何想法都会产生更美观的结果?
我知道我必须随着时间的推移对组分配进行动画处理才能获得 'trickle' 效果(否则所有点都会蜂拥而至),但我想从一个漂亮而圆润的模拟稳定状态。
编辑
我确实查看了源代码,它只是重放预先录制的模拟数据,我猜是出于性能原因。
Nadieh Bremer is my idol in D3 visualisations, she's an absolute star! (correction after OP's : it seems that this datavis was created by Shirley Wu...无论如何,这不会改变我对布雷默的看法。
要找出该页面上发生的事情的第一次尝试是查看 source code,不幸的是,这是一项艰巨的工作。因此,剩下的选项就是尝试重现它。
这里的挑战不是创建一个圆形图案,这很简单:您只需要组合 forceX
、forceY
和 forceCollide
:
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。
所以,这是我的 suggestion/attempt:
我们创建一个模拟,我们停止...
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 可能使用了一种更复杂的代码方式。但这些是我现在的两分钱,让我们看看其他答案是否会以不同的方式出现。
以 Gerardo 的起点为基础,
我认为避免熵过大的关键点之一是指定速度衰减 - 这将有助于避免超出所需位置。太慢,你不会在流动停止的地方增加密度,太快,你的节点要么太混乱,要么超过它们的目的地,在太远和太短之间振荡。
许多体积力在这里很有用 - 它可以保持节点间隔(而不是碰撞力),节点之间的排斥力被每个集群的定位力抵消。下面我使用了两个中心点和一个节点属性来确定使用哪个。这些力必须相当弱 - 强大的力很容易导致过度修正。
我没有使用计时器,而是使用 simulation.find() 功能,每次滴答到一个集群中的一个节点 select 并切换它被吸引到哪个中心。 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 的作品。
在其他答案的帮助下,我继续试验,我想总结一下我的发现:
圆盘形状
forceManyBody
好像比forceCollide
更稳定。使用它而不扭曲圆盘形状的关键是 .distanceMax
。缺点是您的可视化不再 'scale-free' 并且必须手动调整。作为指导,每个方向的过冲都会导致不同的伪像:
设置 distanceMax
太高会使相邻的圆盘变形。
设置 distanceMax
太低(低于预期的圆盘直径):
这个神器可以在守护者观想中看到(最后红蓝点形成一个巨大的圆盘),所以我很确定distanceMax
被使用了。
节点定位
我仍然发现将 forceX
与 forceY
和自定义访问器函数一起使用对于更复杂的动画来说太麻烦了。我决定使用 'control' 个节点,并且稍作调整(chargeForce.strength(-4)
、link.strength(.2).distance(1)
)就可以正常工作。
流畅感
在尝试这些设置时,我注意到流体感(传入节点推动接受盘的边界)尤其取决于 simulation.velocityDecay
,但将其降低太多会给系统增加太多熵。
最终结果
我的示例代码将一个 'population' 分成三个,然后再分成五个 - 在 blocks 上查看。每个汇都由一个控制节点表示。节点被分批重新分配给新的接收器,这可以更好地控制 'stream' 的视觉效果。开始选择靠近汇点分配的节点看起来更自然(每个动画开始时的单个 sort
)。
我正在尝试重现 Nadieh Bremer 和 Shirely Wu Bussed out 的精彩 'dot flow' 可视化效果。
我对 'bubbles' 的非常圆的形状和点到达气泡(黑色箭头)处的类似流体动力学的压缩特别感兴趣。
我的做法是通过 .fx
和 .fy
(黑点)和 link 将所有其他节点创建(三个)固定节点到相应的固定节点。结果看起来很乱,气泡甚至没有在它们的中心节点周围形成,当我降低力使动画运行得慢一点时。
const simulation = d3.forceSimulation(nodes)
.force("collide", d3.forceCollide((n, i) => i < 3 ? 0 : 7))
.force("links", d3.forceLink(links).strength(.06))
关于力设置的任何想法都会产生更美观的结果?
我知道我必须随着时间的推移对组分配进行动画处理才能获得 'trickle' 效果(否则所有点都会蜂拥而至),但我想从一个漂亮而圆润的模拟稳定状态。
编辑
我确实查看了源代码,它只是重放预先录制的模拟数据,我猜是出于性能原因。
Nadieh Bremer is my idol in D3 visualisations, she's an absolute star! (correction after OP's
要找出该页面上发生的事情的第一次尝试是查看 source code,不幸的是,这是一项艰巨的工作。因此,剩下的选项就是尝试重现它。
这里的挑战不是创建一个圆形图案,这很简单:您只需要组合 forceX
、forceY
和 forceCollide
:
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。
所以,这是我的 suggestion/attempt:
我们创建一个模拟,我们停止...
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 可能使用了一种更复杂的代码方式。但这些是我现在的两分钱,让我们看看其他答案是否会以不同的方式出现。
以 Gerardo 的起点为基础,
我认为避免熵过大的关键点之一是指定速度衰减 - 这将有助于避免超出所需位置。太慢,你不会在流动停止的地方增加密度,太快,你的节点要么太混乱,要么超过它们的目的地,在太远和太短之间振荡。
许多体积力在这里很有用 - 它可以保持节点间隔(而不是碰撞力),节点之间的排斥力被每个集群的定位力抵消。下面我使用了两个中心点和一个节点属性来确定使用哪个。这些力必须相当弱 - 强大的力很容易导致过度修正。
我没有使用计时器,而是使用 simulation.find() 功能,每次滴答到一个集群中的一个节点 select 并切换它被吸引到哪个中心。 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 的作品。
在其他答案的帮助下,我继续试验,我想总结一下我的发现:
圆盘形状
forceManyBody
好像比forceCollide
更稳定。使用它而不扭曲圆盘形状的关键是 .distanceMax
。缺点是您的可视化不再 'scale-free' 并且必须手动调整。作为指导,每个方向的过冲都会导致不同的伪像:
设置 distanceMax
太高会使相邻的圆盘变形。
设置 distanceMax
太低(低于预期的圆盘直径):
这个神器可以在守护者观想中看到(最后红蓝点形成一个巨大的圆盘),所以我很确定distanceMax
被使用了。
节点定位
我仍然发现将 forceX
与 forceY
和自定义访问器函数一起使用对于更复杂的动画来说太麻烦了。我决定使用 'control' 个节点,并且稍作调整(chargeForce.strength(-4)
、link.strength(.2).distance(1)
)就可以正常工作。
流畅感
在尝试这些设置时,我注意到流体感(传入节点推动接受盘的边界)尤其取决于 simulation.velocityDecay
,但将其降低太多会给系统增加太多熵。
最终结果
我的示例代码将一个 'population' 分成三个,然后再分成五个 - 在 blocks 上查看。每个汇都由一个控制节点表示。节点被分批重新分配给新的接收器,这可以更好地控制 'stream' 的视觉效果。开始选择靠近汇点分配的节点看起来更自然(每个动画开始时的单个 sort
)。