Сохранение и перезагрузка силовой компоновки с использованием d3.js

Я пытаюсь найти правильный метод, чтобы иметь возможность сэкономить силовую диаграмму node позиции макета после ее установки, а затем перезагрузить этот макет и начать снова с одного и того же разрешенного состояния.

Я пытаюсь сделать это, клонировав элементы DOM, содержащие диаграмму, удалив ее, а затем перезагрузив ее.

Это я могу сделать, частично как указано ниже: -

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

Выбирает содержащий div, клонирует его и удаляет, а затем

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

Выбирает div, который используется для хранения диаграммы и перезагрузки. Перезагруженная диаграмма исправлена.

Я не знаю, как восстановить силу, чтобы позволить манипуляции продолжать. Это возможно? Я хочу сохранить заданное положение узлов.

Еще одна возможность, можно ли перезагрузить позиции node и запустить силу с низкой альфа?

<!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; });

});

Ответ 1

ИЗМЕНИТЬ: ПОЛНОЕ решение сейчас!

Кроме того, этот подход будет работать в самых разных сценариях - как для остановки, так и для перезапуска макета на одной странице, а также для сохранения и повторной загрузки макета на другой странице.

Сначала сохраните исходный график JSON в конце процесса компоновки, который вы можете прослушать для использования:

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

Сохранение теперь ценно, потому что x, y координаты (и некоторые другие вещи) были добавлены к каждому node и кромке по d3 во время макета (но продолжайте менять, пока он не остановится). Будучи JSON, график легко сериализуем, вставляем в localStorage, вытаскиваем и снова разбираем:

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

Как только вы вытащили его из хранилища, вам просто не нужен объект JSON, вы хотите вернуть этот сохраненный объект обратно в svg, и в идеале, используя устройство, уже имеющееся с d3.layout.force для простоты. И на самом деле вы можете сделать это - с небольшими изменениями.

Если вы вернете сохраненный график обратно, т.е. просто запустите

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

с сохраненным графиком, вы получите два странных поведения.

Странное поведение 1 и решение

Основываясь на хорошей документации, в том числе координаты x и y в начальном графе переопределяют случайную инициализацию процесса компоновки - но только инициализация. Таким образом, вы получите узлы, где они должны быть, но затем они выплывут в равномерно распределенный круг, поскольку макет галочки. Чтобы это не происходило, используйте:

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

перед запуском force.start().

Странное поведение 2 и решение Теперь ваши узлы и ребра будут находиться там, где вы хотите, но ваши грани будут - уменьшаться?

Нечто похожее произошло, но, к сожалению, вы не можете использовать точно такое же решение. Длины ребер были сохранены в объекте 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 })

(документация, для которой вы можете найти здесь), и, наконец, ваш график будет выглядеть так, как предполагается.

В общем, если вы убедитесь, что ваш график JSON 1) имеет координаты x, y на узлах, 2) имеет узлы, установленные в fixed=1, а 3) force имеет linkDistance, установленный перед .start(), тогда вы может запускать точно такой же процесс компоновки, как если бы вы инициализировали с нуля, и вы вернете свой сохраненный график.

Ответ 2

Итак, если я неправильно читаю, что при:

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

компоновка силы фактически инициализирует (или повторно инициализирует, если вы снова вызовете возобновление/запуск) макет с node и информацией о краю, заданной для значений, переданных в функции узлов/ребер.

Я проверил это, используя вашу диаграмму, а затем на конце макета, возобновив компоновку сил. Он не рекомпостирует позиции node/edge, так как они уже сохраняются в наборе данных который был первоначально принят. Вы также можете проверить это, добавив значения 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 будет обновлен с каждым node, содержащим новую позицию, скорость и т.д., Таким образом представляя текущее состояние компоновки силовых элементов.

Однако есть одна вещь, отсутствующая, чтобы иметь возможность фиксировать полное состояние макета, которое является его текущим значением alpha.

Сохраняя как dataset, так и alpha, любым способом, который вам нравится, позже вы сможете восстановить компоновку силы в состояние в момент, когда будут сохранены эти свойства. В зависимости от ваших потребностей вы можете использовать довольно энергозависимое хранилище, например, сохранять локальные ссылки на эти свойства или JSON.stringify(), чтобы они могли как-то их сохранить.

Для вашего собственного кода это можно сделать следующим образом:

  • Если вам нужно полностью удалить SVG из DOM, как это сделано в вашем обратном вызове, в первый тайм-аут, удобно поместить код, который добавляет 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, содержащую состояние макета. Но в соответствии с документами API для force.nodes([nodes]) значения, присутствующие на узлах, поставляемых в качестве параметра, также будут приняты при настройке совершенно нового раскладка. Затем вы можете возобновить выполнение, установив 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);
    

Посмотрите полный фрагмент демонстрации. Я сократил таймауты, чтобы подчеркнуть эффект.

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