使用 d3.js 保存和重新加载强制布局

Posted

技术标签:

【中文标题】使用 d3.js 保存和重新加载强制布局【英文标题】:Saving and reloading a force layout using d3.js 【发布时间】:2013-08-14 21:36:26 【问题描述】:

我正在尝试找到正确的方法,以便能够在确定后保存力图节点布局位置,然后重新加载该布局并从相同的已确定状态重新开始。

我试图通过克隆包含图表的 DOM 元素、删除它然后重新加载它来做到这一点。

我可以做到,部分如下所示:-

_clone = $('#chart').clone(true,true);
$('#chart').remove();

选择包含的 div,克隆它并删除它,然后再删除

var _target = $('#p1content');
_target.append(_clone);

选择用于保存图表的div 并重新加载它。重新加载的图表已修复。

我不知道如何重新连接力以允许操纵继续进行。这可能吗?我想保留节点的固定位置。

另一种可能性,我可以重新加载节点位置并以低 alpha 启动力吗?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>D3: Force layout</title>
    <script src="./jquery-2.0.3.min.js" type="text/javascript"></script>
    <script type="text/javascript" src="../d3.v3.js"></script>
    <style type="text/css">
        /* No style rules here yet */
    </style>
</head>
<body>
     <div data-role="content" id="p1content">
        <div id="chart"></div>
    </div>
    <script type="text/javascript">

        //Width and height
        var w = 800;
        var h = 600;

        //Original data
        var dataset = 
            nodes: [
                 name: "Adam" ,
                 name: "Bob" ,
                 name: "Carrie" ,
                 name: "Donovan" ,
                 name: "Edward" ,
                 name: "Felicity" ,
                 name: "George" ,
                 name: "Hannah" ,
                 name: "Iris" ,
                 name: "Jerry" 
            ],
            edges: [
                 source: 0, target: 1 ,
                 source: 0, target: 2 ,
                 source: 0, target: 3 ,
                 source: 0, target: 4 ,
                 source: 1, target: 5 ,
                 source: 2, target: 5 ,
                 source: 2, target: 5 ,
                 source: 3, target: 4 ,
                 source: 5, target: 8 ,
                 source: 5, target: 9 ,
                 source: 6, target: 7 ,
                 source: 7, target: 8 ,
                 source: 8, target: 9 
            ]
        ;

        //Initialize a default force layout, using the nodes and edges in dataset
        var force = d3.layout.force()
                             .nodes(dataset.nodes)
                             .links(dataset.edges)
                             .size([w, h])
                             .linkDistance([100])
                             .charge([-100])
                             .start();

        var colors = d3.scale.category10();

        //Create SVG element
        var svg = d3.select("#chart")
                    .append("svg")
                    .attr("width", w)
                    .attr("height", h);

        //Create edges as lines
        var edges = svg.selectAll("line")
            .data(dataset.edges)
            .enter()
            .append("line")
            .style("stroke", "#ccc")
            .style("stroke-width", 1);

        //Create nodes as circles
        var nodes = svg.selectAll("circle")
            .data(dataset.nodes)
            .enter()
            .append("circle")
            .attr("r", 10)
            .style("fill", function(d, i) 
                return colors(i);
            )
            .call(force.drag);

        //Every time the simulation "ticks", this will be called
        force.on("tick", function() 

            edges.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; );

            nodes.attr("cx", function(d)  return d.x; )
                 .attr("cy", function(d)  return d.y; );

        );

// After 5 secs clone and remove DOM elements
        setTimeout(function() 
                        _clone = $('#chart').clone(true,true);
                        $('#chart').remove();
        , 5000);
//After 10 secs reload DOM
        setTimeout(function() 
                        var _target = $('#p1content');
                        _target.append(_clone);

// WHAT NEEDS TO GO HERE TO RECOUPLE THE FORCE?                     

         , 10000);

    </script>
</body>
</html>

在我放的地方添加了这个 // 什么需要去这里来恢复力量? 这似乎可以拾取已恢复的现有元素并将原力重新耦合到它停止将力节点等传递到超时函数的位置

force = d3.layout.force()
    .nodes(dataset.nodes)
    .links(dataset.edges)
    .size([w, h])
    .linkDistance([100])
    .charge([-100])
    .start();

colors = d3.scale.category10();

//Create SVG element
svg = d3.select("#chart");

//Create edges as lines
edges = svg.selectAll("line")
    .data(dataset.edges);

//Create nodes as circles
nodes = svg.selectAll("circle")
    .data(dataset.nodes)
    .call(force.drag);

//Every time the simulation "ticks", this will be called
force.on("tick", function() 

    edges.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; );
    nodes.attr("cx", function(d)  return d.x; )
        .attr("cy", function(d)  return d.y; );

);

【问题讨论】:

我不认为你仅仅通过克隆 DOM 元素就能轻易摆脱... 可视化是由 d3.js 内部工作原理创建的,这意味着当你 重新加载,能够再次使用它意味着d3.js 知道它正在处理什么。就是说,如果你复制一个被 d3.jshold 的元素,d3.js 只能处理 original 而不是副本。 这意味着您可能必须序列化 d3.js 使用的关于可视化的信息,然后找到一种方法从序列化数据中再次构建它......我不认为这是听起来很简单。 是的,我认为这可能并不容易。我当然可以保存数据并重新加载,但是节点的布局不会保留,因为力图可以稳定到一个新位置。这就是查看 DOM 元素以创建快照的原因。但是如果不从头开始有效地重新启动,我无法看到如何将力重新耦合到它 我认为除了保存 DOM 元素之外,您还应该考虑保存 d3.js 实际使用的内容。我认为您应该尝试保存 d3.js 正在使用的 nodeslinks 数组,然后在重新加载脚本时,将其用作计算布局的数据,而不是调用原始数据源。如果您发布您的脚本,我可以尝试更好地解释这一点。 添加了示例脚本,谢谢 【参考方案1】:

编辑:现在是完整的解决方案!

此外,这种方法适用于各种场景——既可以停止和重新启动单个页面上的布局,也可以保存和重新加载不同页面上的布局.

首先,将原始JSON图保存在布局过程的end处,您可以使用:

force.on('tick', function()
    ...
).on('end', function()
    // Run this when the layout has finished!
);

现在保存很有价值,因为在布局期间,d3 已将 x,y 坐标(和其他一些东西)添加到每个节点和边(但要不断变化,直到停止)。由于是 JSON,图很容易序列化,粘贴在 localStorage 中,拉出并再次解析:

localStorage.setItem(JSON.stringify(graph));
...
JSON.parse(localStorage.getItem('graph'));

一旦你把它从存储中取出,你不只是想要一个 JSON 对象,你想把保存的对象转换回 svg,理想情况下,使用 d3.layout.force 已经可用的设备为简单起见。事实上,您可以做到这一点 -- 只需进行一些小改动

如果您将保存的图表直接放回原处,即运行

force
  .nodes(graph.nodes)
  .links(graph.links)
  .start();

使用保存的图表,你会得到两个奇怪的行为。

怪异行为 1 及解决方案

基于good documentation,在起始图中包含 x 和 y 坐标会覆盖布局过程的随机初始化——但只是初始化。因此,您将获得它们应该所在的节点,但是随着布局的滴答声,它们会漂浮到一个均匀分布的圆圈中。为避免这种情况发生,请使用:

  for(n in graph.nodes)
    graph.nodes[n].fixed = 1
  

之前运行force.start()

怪异行为 2 及解决方案 现在,您的节点和边都将位于您希望它们所在的位置,但您的边将 -- shrink?

发生了类似的事情,但不幸的是,您不能使用完全相同的解决方案。边长保存在 JSON 对象中,并在布局的初始化中使用,但随后布局对它们都施加了默认长度 (20),除非您首先保存边JSON 图中的长度 --

.on('end', function() 

    links = svg.selectAll(".link")[0]
    for(i in graph.links)
      graph.links[i].length = links[i].getAttribute('length')
    
    localStorage.setItem('graph', JSON.stringify(graph));

);

然后,在force.start() 之前 --

force.linkDistance(function (d)  return d.length )

(可在here 找到相关文档)最后,您的图表将看起来像它应该的那样。

总之,如果您确保您的 JSON 图 1) 在节点上具有 x,y 坐标,2) 将节点设置为 fixed=1,并且 3) force.start() 之前设置了 linkDistance,那么您只需运行完全相同的布局过程,就好像您从头开始初始化一样,您将取回已保存的图表。

【讨论】:

您的回答使我找到了正确的解决方案。 fixed=1 解决了问题。您知道如何在画布上存储图形位置和缩放级别吗? @Wexoni 我的图表总是从同一个地方开始,以同样的缩放比例,然后我在它上面使用“.transform”。但到目前为止,我每次都必须重新转换它。转换字符串可以像图表一样保存。 感谢您的回复。我设法找到了解决方案。 ***.com/questions/35085513/…【参考方案2】:

所以,除非我看错了:

https://github.com/mbostock/d3/wiki/Force-Layout#wiki-nodes

强制布局实际上会初始化(或者如果您再次调用 resume/start,则重新初始化)布局,其中节点和边缘信息在传递给节点/边函数的值上指定。

我使用您的图表对此进行了测试,然后在布局结束时恢复了强制布局。它不会重新计算节点/边缘位置,因为它们已经保存在数据集上 最初传入的。您还可以通过将 x/y 值添加到初始数据来进行测试。

http://jsfiddle.net/jugglebird/Brb29/1/

force.on("end", function() 
    // At this point dataset.nodes will include layout information
    console.log("resuming");  
    force.resume(); // equivalent to force.alpha(.1);
);

【讨论】:

【参考方案3】:

请务必牢记,力布局将其结果存储在数据本身中。这样,当在刻度处理函数中调整数据绑定到的可视节点和边缘时,它们就可以访问。

在考虑所有力和约束进行计算时,力布局会将结果存储到提供给force.nodes() 的节点数组中包含的节点中。在每个刻度结束时,当所有计算完成时,您的dataset.nodes 数组将更新为包含新位置、速度等的每个节点,从而代表力布局的当前状态。

然而,要捕获布局的完整状态,还缺少一件事,即其当前值alpha

同时保存datasetalpha,通过任何您喜欢的方式,您稍后将能够将强制布局恢复到捕获这些属性时的状态。根据您的需要,您可能会使用相当不稳定的存储,例如保留对这些属性的本地引用,或者 JSON.stringify() 它们甚至能够以某种方式持久化它们。

对于您自己的代码,可以按如下方式完成:

    如果您需要像在第一次超时的回调中所做的那样从 DOM 中完全删除 SVG,则可以方便地将附加 SVG 以及节点和边的代码放入函数中,因为您需要调用两次。

    function initChart() 
      svg = d3.select("#chart")
                  .append("svg")
                  .attr("width", w)
                  .attr("height", h);
    
      //Create edges as lines
      edges = svg.selectAll("line")
          .data(dataset.edges)
          .enter()
          .append("line")
          .style("stroke", "#ccc")
          .style("stroke-width", 1);
    
      //Create nodes as circles
      nodes = svg.selectAll("circle")
          .data(dataset.nodes)
          .enter()
          .append("circle")
          .attr("r", 10)
          .style("fill", function(d, i) 
              return colors(i);
          )
          .call(force.drag);
    
    
    initChart();              // Append the SVG with nodes and edges.
    

    但是,如果仅将其设置为 display:none 就足够了,事情会变得更容易,因为您可以保持所有引用不变。

    要完全保存布局的状态,您需要存储alpha 的当前值。之后,您调用force.stop() 实际上立即停止强制布局。请记住,您的 dataset 已经设置了最新值。

    var alpha;                // This will save alpha when stopped.
    
    // Stop and remove after 1 second.
    setTimeout(function() 
      alpha = force.alpha();  // Save alpha.
      force.stop();           // Stop the force.
      svg.remove();           // Dump the SVG.
    , 1000);
    

    您可以随时将强制布局恢复到保存状态。在您的示例中,force 引用的强制布局没有被破坏,因此它仍然具有对包含布局状态的 dataset 的引用。但是根据force.nodes([nodes]) 的 API 文档,作为参数提供的节点上的值也将在设置全新布局时被采用。然后,您可以通过将force.alpha(alpha) 设置为保存的值来恢复其执行。请注意,在重新启动强制布局之前,SVG 会通过再次调用 initChart() 来重建。

    // Restore to paused state and restart.
    setTimeout(function() 
      initChart();            // Append the SVG with nodes and edges.
      force.alpha(alpha);     // Restart the force with alpha.
    , 3000);
    

查看完整的 sn-p 以进行演示。我缩短了超时以强调效果。

        //Width and height
        var w = 800;
        var h = 600;

        //Original data
        var dataset = 
            nodes: [
                 name: "Adam" ,
                 name: "Bob" ,
                 name: "Carrie" ,
                 name: "Donovan" ,
                 name: "Edward" ,
                 name: "Felicity" ,
                 name: "George" ,
                 name: "Hannah" ,
                 name: "Iris" ,
                 name: "Jerry" 
            ],
            edges: [
                 source: 0, target: 1 ,
                 source: 0, target: 2 ,
                 source: 0, target: 3 ,
                 source: 0, target: 4 ,
                 source: 1, target: 5 ,
                 source: 2, target: 5 ,
                 source: 2, target: 5 ,
                 source: 3, target: 4 ,
                 source: 5, target: 8 ,
                 source: 5, target: 9 ,
                 source: 6, target: 7 ,
                 source: 7, target: 8 ,
                 source: 8, target: 9 
            ]
        ;

        //Initialize a default force layout, using the nodes and edges in dataset
        var force = d3.layout.force()
          .nodes(dataset.nodes)
          .links(dataset.edges)
          .size([w, h])
          .linkDistance([100])
          .charge([-100])
          .start()
          .on("tick", function() 
            edges.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; );
 
            nodes.attr("cx", function(d)  return d.x; )
                 .attr("cy", function(d)  return d.y; );
          );

        var colors = d3.scale.category10();

        //Create SVG element
        var svg,
            edges,
            nodes;
            
        function initChart() 
          svg = d3.select("#chart")
                      .append("svg")
                      .attr("width", w)
                      .attr("height", h);
  
          //Create edges as lines
          edges = svg.selectAll("line")
              .data(dataset.edges)
              .enter()
              .append("line")
              .style("stroke", "#ccc")
              .style("stroke-width", 1);
  
          //Create nodes as circles
          nodes = svg.selectAll("circle")
              .data(dataset.nodes)
              .enter()
              .append("circle")
              .attr("r", 10)
              .style("fill", function(d, i) 
                  return colors(i);
              )
              .call(force.drag);
        
        
        initChart();              // Append the SVG with nodes and edges.

        var alpha;                // This will save alpha when stopped.

        // Stop and remove after 1 second.
        setTimeout(function() 
          alpha = force.alpha();  // Save alpha.
          force.stop();           // Stop the force.
          svg.remove();           // Dump the SVG.
        , 1000);
        
        // Restore to paused state and restart.
        setTimeout(function() 
          initChart();            // Append the SVG with nodes and edges.
          force.alpha(alpha);     // Restart the force with alpha.
        , 3000);
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8" />
  <title>D3: Force layout</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
  <style type="text/css">
    /* No style rules here yet */
  </style>
</head>

<body>
  <div data-role="content" id="p1content">
    <div id="chart"></div>
  </div>
</body>

</html>

【讨论】:

知道我发现的缩小边缘是怎么回事吗? 您是否尝试运行嵌入在我的答案中的代码 sn-p?你真的能在它的输出中看到同样的错误吗?在坚持我提出的方法时,我认为这不再是一个问题。 这基本上是我已经拥有的代码,我在初始化保存的布局时将 alpha 设置为零 - 边缘仍然缩小。而且,最令人不安的是,在初始化中传递边长度似乎也不起作用。 它对我有用,没有收缩的边缘。请看看这个JSFiddle,告诉我那里是否也能看到效果。 小提琴看起来不错(谢谢你)——无论如何,它有助于隔离问题,这似乎是由调用 force.start 再次驱动的,使用旧数据。 (您的案例只是启动、停止和重新启动一个强制对象。)由于我从另一个页面引入保存的数据,我无法使用原始对象重新启动,我认为 providing 'length' 会覆盖它的初始化,但显然不是?我认为这是现在的问题,但无法阻止它发生。

以上是关于使用 d3.js 保存和重新加载强制布局的主要内容,如果未能解决你的问题,请参考以下文章

D3.js 重新启动模拟时节点跳转添加或删除节点

UIWebView 完成加载后,如何强制重新布局 UITableViewCell?

如何使 D3.js 中的强制布局图响应屏幕/浏览器大小

D3.js中的弹簧力强制定向布局?

如何调整 UIImageView 的大小并强制重新布局父 UITableViewCell

在 Blazor 服务器中使用 NavigationManager NavigateTo 强制加载不会重新加载整个索引