Search code examples
layoutgraphcytoscape.jswebcola

Cytoscape Cola Layout: How to restart the layout without any change of positions?


I'm trying to use the Cytoscape cola layout to render a graph that should apply a force directed layout while using it (so when dragging nodes around, they should act as if there is some gravity involved). Relevant libraries:

My first problem is that adding nodes to the graph via add(node) doesn't include them in the cola layout algorithm. The only way I found around that is to destroy the layout, re-initialize it and start it again. But this causes the nodes to jump in some cases.

I assumed that this was due to the fact that I completely destroyed the old layout but when setting up a minimal example, I realized that even just calling layout.stop() and layout.run() leads to nodes being repositioned.

In the following example, there is only one node. Moving the node via drag and drop, then pressing the "stop" button and then the "start" button causes the node to jump back to its initial position:

document.addEventListener('DOMContentLoaded', function(){
        
  // Register cola layout
  cytoscapeCola(cytoscape);

  var nodes = [{ data: { id: 1, name: 1 } }]
  var edges = [];

  var cy = window.cy = cytoscape({
    container: document.getElementById('cy'),

    style: [
      {
        selector: 'node[name]',
        style: {
          'content': 'data(name)'
        }
      },

      {
        selector: 'edge',
        style: {
          'curve-style': 'bezier',
          'target-arrow-shape': 'triangle'
        }
      },
    ],

    elements: {
      nodes: nodes,
      edges: edges
    }
  });

  var layout = cy.layout({
    name: 'cola',
    infinite: true,
    fit: false,
  });
  layout.run();

  document.querySelector('#start').addEventListener('click', function() {
    layout.run();
  });

  document.querySelector('#stop').addEventListener('click', function() {
    layout.stop();
  });

  document.querySelector('#add-node').addEventListener('click', function() {
    var id = Math.random();
    cy.add({ group: 'nodes', data: { id: id, name: id } });
    cy.add({ group: 'edges', data: { source: id, target: _.head(nodes).data.id } });
    layout.stop();
    layout.destroy();
    layout = cy.layout({
      name: 'cola',
      infinite: true,
      fit: false,
    });
    layout.run();
  });

});
body {
  font-family: helvetica neue, helvetica, liberation sans, arial, sans-serif;
  font-size: 14px;
}

#cy {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  right: 0;
  z-index: 999;
}

h1 {
  opacity: 0.5;
  font-size: 1em;
  font-weight: bold;
}

#buttons {
  position: absolute;
  right: 0;
  bottom: 0;
  z-index: 99999;
}
<!DOCTYPE>

<html>

  <head>
    <title>cytoscape-edgehandles.js demo for infinite layout</title>

    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">

    <script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>

    <script src="https://unpkg.com/webcola/WebCola/cola.min.js"></script> 
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/cytoscape-cola.min.js"></script>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.js"></script>
    <script src="cytoscape-edgehandles.js"></script>
    
    </head>

  <body>
    <h1>cytoscape-edgehandles demo with an infinite layout</h1>
    <div id="cy"></div>
    <div id="buttons">
      <button id="start">Start</button>
      <button id="stop">Stop</button>
      <button id="add-node">Add Node</button>
    </div>
  </body>

</html>

Is this a bug or am I doing something wrong? Does anyone know how to stop and restart the layout without the nodes changing their position?

Thanks a lot, Jesse


Solution

  • Okay actually you were very close @Stephan. The problem was that WebCola centers the nodes when calling start by default: https://github.com/tgdwyer/WebCola/blob/78a24fc0dbf0b4eb4a12386db9c09b087633267d/src/layout.ts#L504

    The cytoscape wrapper for WebCola does not currently support this option, so I forked it and added the option myself: https://github.com/deje1011/cytoscape.js-cola/commit/f357b97aba900327e12f97b1530c4df624ff9d61

    I'll open a pull request at some point.

    Now you can smoothly restart the layout like this:

    layout.stop();
    layout.destroy(); // cleanup event listeners
    layout = graph.layout({ name: 'cola', infinite: true, fit: false, centerGraph: false });
    layout.run()
    

    This way, the nodes keep their position 🎉