【问题标题】:Calm down initial tick of a force layout冷静力布局的初始刻度
【发布时间】:2012-11-07 22:06:18
【问题描述】:

我刚刚开始涉足 d3,发现学习曲线相当陡峭。这个过程与我习惯的完全不同,数学大部分都超出了我的想象。

无论如何,我的项目包含一个表示系统之间集成图的力布局。这部分效果非常好,但我确实有一个主要问题,这也体现在 Michael Bostocks 网站上的力导向布局演示中: 当节点启动时,它们似乎是在画布之外渲染的。在此之后,一些严肃的物理数学正在接管,模拟引力,它将节点在相当混乱的路径上来回发送,直到它们平静下来并定居在一些随机坐标上。尽管在第一次运行演示时这些动作非常酷,但当您尝试从公司管理员的角度查看网络接口的状态并且服务器不会保持静止时,一段时间后就会变得厌烦。

我确信我已经为这个项目设置了正确的布局,因为我确实希望服务器自动布局,我确实希望将它们之间的链接可视化。然而,我对引力效应感到矛盾。

我想知道;是否可以手动设置每个节点的初始位置,以便我可以将它们靠近重心并缩短“反弹时间”?

【问题讨论】:

    标签: d3.js force-layout


    【解决方案1】:

    以上所有答案都误解了Øystein Amundsen的问题。

    稳定启动力的唯一方法是将 node.x 和 node.y 设置为适当的值。 请注意,节点是d3.js的数据,不是代表的DOM类型。

    例如,如果你加载

    nodes = [{"id": a}, {"id": b}, {"id": c}]
    

    进入

    d3.layout.force().nodes(nodes)
    

    您必须设置节点数组中所有元素的所有 .x 和 .y 它会是这样的(在coffeescript中)

    nodes = [{"id": a}, {"id": b}, {"id": c}]
    for i in [0..2]
      nodes[i].x = 500 #the initial x position of node
      nodes[i].y = 300 #the initial y position of node
    d3.layout.force().nodes(nodes).links(links)
    

    然后节点将在 force.start() 时的位置开始。 这样可以避免混乱。

    【讨论】:

    • 我认为你是对的。我为你对抗人群的勇气喝彩。 +1 票。
    • 这按预期工作。我会将其标记为正确答案,因为它实际上解决了问题而不是解决问题。谢谢! :-)
    • 如果你已经知道所有节点的初始位置,那为什么还要使用力有向图呢?
    • 我确实希望 d3 为我自动布局我的节点。我只是不希望它如此混乱。通过强制所有节点具有初始位置,它们都将被布置在画布上而不是画布之外,这反过来又使它们弹跳得更少,甚至根本不弹跳。这个项目的重点是可视化不同系统之间的网络集成。不要使用物理模拟来展示很酷的动画。
    • 我觉得值得一提的是,将很多节点的初始位置设置为相同的固定位置,排斥力会非常强,也会使布局混乱。可以通过向初始位置添加随机“噪声”来防止这种情况。该解决方案由 Lars Kotthoff 在stackoverflow.com/questions/31127949/… 中提出
    【解决方案2】:

    在内部,在“正常”使用情况下,强制布局反复异步调用其自己的tick() 方法(通过setIntervalrequestAnimationFrame),直到布局确定解决方案。此时其alpha() 值等于或接近0。

    因此,如果您想“快进”通过此解决方案过程,您可以一遍又一遍地同步调用 tick() 方法,直到布局的 alpha 达到一个值,根据您的具体要求,构成“足够接近”解决方案。像这样:

    var force = d3.layout.force(),
        safety = 0;
    while(force.alpha() > 0.05) { // You'll want to try out different, "small" values for this
        force.tick();
        if(safety++ > 500) {
          break;// Avoids infinite looping in case this solution was a bad idea
        }
    }
    
    if(safety < 500) {
      console.log('success??');
    }
    

    此代码运行后,您可以根据节点的状态绘制布局。或者,如果您通过绑定到 tick 事件(即force.on('tick', drawMyLayout))来绘制布局,您将希望在此代码运行之后进行绑定,否则您将不必要地渲染在while 循环期间同步布局数百次。

    JohnS 将这种方法归结为一个简洁的函数。在此页面的某处查看他的答案。

    【讨论】:

    • 对tick 的调用确实有效,但不太令人满意。面板只是空白了一会儿,然后图表才终于出现 - 完全静止。似乎这种技术不会“禁用”动画,但会阻止它可见。通过稍微调整最大“安全”值,应用程序会在较早的阶段显示图表,从而减少“弹性”,并且不会等到图表可见。 alpha 设置似乎被完全忽略了。如果没有其他人提出更“正确”的答案,我会接受。谢谢你。 :-)
    • 感谢您接受这个答案。我没有意识到你仍然希望看到图表中的运动——而不仅仅是让它处于最终状态。是的:降低“安全性”可以做到这一点。基本上,在同步循环中重复调用 tick() 与让它异步运行(例如每秒 30 帧)相同,但都是在瞬间完成的——直到它稳定下来,或者足够慢。我越看 d3 源,似乎这是“解决”布局的唯一方法。源代码中的注释称为Simulated Annealing
    • 我想你误解了我的意思。运动并不那么重要,但图表出现之前的时间才是。我不希望应用程序被视为“缓慢”或“无响应”。 :-)
    • 我也想找到一种方法来设置初始位置。有时看到所有节点从 外部限制 飞进来看起来很奇怪!就我而言,我想根据父位置设置初始节点位置。还在寻找...
    • @Inactivist 如果您仍在寻找初始化节点的好方法,我只需将每个节点的 x 和 y 设置为随机数。 github.com/boccobrock/mythologyTree/blob/master/index.html#L83
    【解决方案3】:

    不久前我处理过类似的事情。有几件事需要考虑。

    1) 迭代滴答正在模拟一个达到平衡的系统。因此,在系统稳定并且您拥有自动布局之前,无法避免根据需要多次调用 tick。也就是说,您不需要在每个滴答声中都更新您的可视化来让模拟工作!事实上,如果你不这样做,迭代会快得多。我的代码的相关部分是:

    var iters = 600; // You can get decent results from 300 if you are pressed for time
    var thresh = 0.001;
    if(!hasCachedLayout || optionsChanged || minorOptionsChanged) {
        force.start(); // Defaults to alpha = 0.1
        if(hasCachedLayout) {
            force.alpha(optionsChanged ? 0.1 : 0.01);
        }
        for (var i = iters; i > 0; --i) {
            force.tick();
            if(force.alpha() < thresh) {
                //console.log("Reached " + force.alpha() + " for " + data.nodes.length + " node chart after " + (iters - i) + " ticks.");
                break;
            }
        }
        force.stop();
    }
    

    它同步运行,运行后我为所有节点和链接创建 dom 元素。对于小图,这运行得非常快,但您会发现对于较大的图(100 多个节点)会有延迟——它们的计算成本要高得多。

    2) 您可以缓存/种子布局。强制布局将在初始化时均匀分布节点如果没有设置位置!因此,如果您确保您的节点设置了 x 和 y 属性,这些将被使用。特别是当我更新现有图表时,我将重新使用以前布局中的 x 和 y 位置。

    您会发现,有了良好的初始布局,您将需要 很多 次迭代才能达到稳定的配置。 (这是上面代码中 hasCachedLayout 跟踪的内容)。注意:如果您在相同的布局中重复使用相同的节点,那么您还必须确保将 px 和 py 设置为 NaN,否则您会得到奇怪的结果。

    【讨论】:

    • 谢谢。您对 d3 内部如何工作的解释很好。不过我确实有一些问题。关于第 1 点;为什么要先检查 hasCachedLayout 是否为 false 才能继续,然后在 if 块内再次检查 hasCachedLayout 是否突然发生变化?
    • 第 2 点非常合乎逻辑,而且很有魅力。每次更新我的图形数据时,我都会执行一次循环检查,以查看是否可以重用节点(具有相同的 id)。然后我继续将原始节点 x 和 y 坐标复制到我的新数据中,并且图形在稳定之前经历了轻微的跳跃。完美运行。
    • 第二个检查是针对我有一个缓存布局但一个选项改变的情况(例如重力、链接强度、节点排斥)。选项会改变布局,但是因为我从一个稳定的布局开始,所以我开始循环更接近结束 - 将 alpha 设置为 0.01 而不是默认值 (0.1) 意味着我希望在稳定之前进行更少的迭代。
    【解决方案4】:

    根据其他答案我做了这个方法:

    function forwardAlpha(layout, alpha, max) {
      alpha = alpha || 0;
      max = max || 1000;
      var i = 0;
      while(layout.alpha() > alpha && i++ < max) layout.tick();
    }
    

    【讨论】:

      【解决方案5】:

      也许force.friction(0.5),或低于默认值 0.9 的其他数字,会有帮助吗?至少它在页面加载时给人一种不那么混乱的印象。

      【讨论】:

        【解决方案6】:

        	var width = 960,
        	  height = 500;
        
        	var fill = d3.scale.category20();
        
        	var force = d3.layout.force()
        	  .size([width, height])
        	  .nodes([{}]) // initialize with a single node
        	  .linkDistance(30)
        	  .charge(-60)
        	  .on("tick", tick);
        
        	var svg = d3.select("body").append("svg")
        	  .attr("width", width)
        	  .attr("height", height)
        	  .on("mousedown", mousedown);
        
        	svg.append("rect")
        	  .attr("width", width)
        	  .attr("height", height);
        
        	var nodes = force.nodes(),
        	  links = force.links(),
        	  node = svg.selectAll(".node"),
        	  link = svg.selectAll(".link");
        
        	 // var cursor = svg.append("circle")
        	 //     .attr("r", 30)
        	 //     .attr("transform", "translate(-100,-100)")
        	 //     .attr("class", "cursor");
        
        	restart();
        
        	function mousedown() {
        	  var point = d3.mouse(this),
        	    node = {
        	      x: width / 2,
        	      y: height / 2,
        	      "number": Math.floor(Math.random() * 100)
        	    },
        	    n = nodes.push(node);
        
        	  // add links to any nearby nodes
        	  /*  nodes.forEach(function(target) {
        		    var x = target.x - node.x,
        		        y = target.y - node.y;
        		    if (Math.sqrt(x * x + y * y) < 30) {
        		      links.push({source: node, target: target});
        		    }
        		  });
        		*/
        	  restart();
        	}
        
        	function tick() {
        	  link.attr("x1", function(d) {
        	      return d.source.x;
        	    })
        	    .attr("y1", function(d) {
        	      return d.source.y;
        	    })
        	    .attr("x2", function(d) {
        	      return d.target.x;
        	    })
        	    .attr("y2", function(d) {
        	      return d.target.y;
        	    });
        
        	  node.attr("transform", function(d) {
        	    return "translate(" + d.x + "," + d.y + ")";
        	  });
        	}
        
        	function restart() {
        	  link = link.data(links);
        
        	  link.enter().insert("line", ".node")
        	    .attr("class", "link");
        
        	  node = node.data(nodes);
        
        	  // node.enter().insert("circle", ".cursor")
        	  //     .attr("class", "node")
        	  //     .attr("r", 5)
        	  //     .call(force.drag);
        
        	  var nodeEnter = node.enter().insert("svg:g", ".cursor")
        	    .attr("class", "node")
        	    .call(force.drag);
        
        	  nodeEnter.append("svg:circle")
        	    .attr("r", 5)
        
        	  nodeEnter.append("svg:text")
        	    .attr("class", "textClass")
        	    .attr("x", 14)
        	    .attr("y", ".31em")
        	    .text(function(d) {
        	      return d.number;
        	    });
        
        	  force.start();
        	}
        	rect {
        	  fill: none;
        	  pointer-events: all;
        	}
        	.node {
        	  fill: #000;
        	}
        	.cursor {
        	  fill: none;
        	  stroke: brown;
        	  pointer-events: none;
        	}
        	.link {
        	  stroke: #999;
        	}
        	.textClass {
        	  stroke: #323232;
        	  font-family: "Lucida Grande", "Droid Sans", Arial, Helvetica, sans-serif;
        	  font-weight: normal;
        	  stroke-width: .5;
        	  font-size: 14px;
        	}
        	
        &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"&gt;&lt;/script&gt;

        您可能正在寻找的示例。它在将新节点插入布局之前设置它们的 x & y 属性。所需位置是 svg 元素的中心。

        【讨论】:

          猜你喜欢
          • 2018-11-01
          • 1970-01-01
          • 2017-01-03
          • 2019-08-25
          • 2011-06-19
          • 2013-07-21
          • 1970-01-01
          • 2013-09-03
          • 1970-01-01
          相关资源
          最近更新 更多