代码之家  ›  专栏  ›  技术社区  ›  liborm

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

  •  3
  • liborm  · 技术社区  · 6 年前

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

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

    有没有关于部队设置的想法,能产生更美观的效果?

    我确实理解,随着时间的推移,我将不得不对小组分配进行动画处理,以获得“涓涓流”效果(否则所有的点都会蜂拥到目的地),但我想从模拟的良好和圆滑的稳定状态开始。

    编辑

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

    my result

    3 回复  |  直到 6 年前
        1
  •  2
  •   Andrew Reid    6 年前

    从杰拉尔多的起点开始,

    我认为其中一个关键点,避免过度的熵是指定一个速度衰减-这将有助于避免超调所需的位置。速度太慢,在流停止的地方不会增加密度,速度太快,而且节点要么过于混乱,要么过冲目标,在太远和太短之间来回摆动。

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

    我不使用计时器,而是使用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 (代码片段最好是整页的,参数是为它准备的)。最初的节点和后来的节点形成在同一个环中(所以在开始时有一些冲突,但这是一个简单的修复)。在每个标记上,创建一个节点,并尝试将靠近中间的节点迁移到另一侧-这样就创建了一个流(而不是任何随机节点)。

    对于流体,未链接的节点可能是最好的(我一直在使用它进行风模拟)-链接的节点是结构材料(如网或布)的理想选择。而且,像杰拉尔多一样,我也是纳迪厄作品的粉丝,但将来也必须关注雪莉的作品。

        2
  •  2
  •   Gerardo Furtado    6 年前

    Nadieh Bremer 是我的偶像在3视觉化,她是一个绝对的明星!(手术后校正 comment :此datavis似乎是由 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 .

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

    我们创建一个模拟,我们停止…

    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>

    这种方法的问题

    如你所见,这里有太多的熵,特别是在中心。NadiehBremer/ShirleyWu可能使用了一种更为简单的代码。但现在这是我的两分钱,让我们看看其他答案是否会以不同的方式出现。

        3
  •  2
  •   liborm    6 年前

    在其他答案的帮助下,我继续进行实验,我想总结一下我的发现:

    盘形

    forceManyBody 似乎比 forceCollide . 在不扭曲盘形的情况下使用它的关键是 .distanceMax .缺点是,你的可视化不再是“无比例”的,它必须手动调整。作为一种指导,每个方向的超调会导致不同的伪影:

    设置 distanceMax 过高会使相邻的盘变形。

    distanceMax too high

    设置 远距摄影 过低(低于预期的盘直径):

    enter image description here

    这个神器可以在卫报的图像中看到(当红色和蓝色的点最终形成一个巨大的圆盘时),所以我很确定 远距摄影 使用。

    节点定位

    我仍然发现使用 forceX 具有 forceY 自定义访问器的功能对于更复杂的动画来说过于繁琐。我决定使用“控制”节点,并进行一些微调( chargeForce.strength(-4) , link.strength(.2).distance(1) 这行得通。

    流感

    在试验设置时,我注意到流体感觉(进入节点推动接收盘的边界)尤其依赖于 simulation.velocityDecay 但是如果降低得太多,系统就会增加太多的熵。

    最终结果

    我的示例代码将一个“总体”拆分为三个,然后再拆分为五个-检查一下 blocks . 每个接收器由一个控制节点表示。这些节点被批量重新分配给新的接收器,这样可以更好地控制“流”的可视性。开始选择节点以分配到更靠近水槽的位置看起来更自然(单个 sort 在每个动画的开头)。