Search code examples
javascriptcytoscape.jswebcola

Dynamically adding nodes to Cytoscape


My company is building a graph-view editor for chatbots. We are using Cytoscape along with the cytoscape-cola extension to accomplish this. One of the issues we are facing is dynamically adding new nodes to the graph without them overlapping with existing nodes on the graph.

I have looked through previous similar questions(listed below) but to no avail:

Cytoscape - handle locked nodes

I tried the solution in there i.e. applying the layout only on the newly added nodes but they keep getting stacked in the centre of the screen.

Cytoscape - ignore locked nodes

I tried the solution mentioned in here but regardless of locking the nodes in one go i.e. cy.nodes().lock() or individually i.e. cy.nodes().forEach(node => node.lock()), the locked nodes still keep moving. Also interesting thing to note here is that when locking nodes individually, the newly added node(s) are also locked regardless of whether or not I am locking in the call above or not.

Cytoscape - dynamically add nodes without moving others

I tried this solution as well but the locked nodes still move and the complete layout is changed - sometimes altogether, sometimes just a little.

Code

This is currently what I am using to construct the graph:

const layoutConfig = {
    name: "cola",
    handleDisconnected: true,
    animate: true,
    avoidOverlap: true,
    infinite: false,
    unconstrIter: 1,
    userConstIter: 0,
    allConstIter: 1,
    ready: e => {
        e.cy.fit()
        e.cy.center()
    }
}
this.graph = Cytoscape({ ... })

this.layout = this.grapg.makeLayout(layoutConfig)

this.layout.run();

This is what I am using to add new node(s) to the graph:

const addElements = (elements: ElementSingular | ElementMultiple) => {
    this.graph.nodes().forEach(node => {
        node.lock();
    })

    this.graph.add(elements)

    this.layout = this.graph.makeLayout(layoutConfig)

    this.layout.on("layoutready", () => {
        this.nodes().forEach(node => {
            node.unlock();
        })
    })

    this.layout.run()

    this.graph.nodes().forEach(node => {
        node.unlock();
    })
}

I'd like to do one of the following:

  1. Understand what I am doing wrong if what I am trying to accomplish is possible but my code doesn't achieve it
  2. Understand why I would not be able to accomplish it i.e. the limitations that govern it

Solution

  • Edit: Is this what you were looking for? https://output.jsbin.com/hokineluwo

    Edit: I didn't saw before, but you are also unlocking the nodes right after the layout call, Cola is async, the run only kicks-off the process. Remove that code and only use the layoutstop method.

    I don't remember correctly, but i think that cola keeps moving the elements after the layoutready. From their code:

    // trigger layoutready when each node has had its position set at least once
    

    There is a layoutstop event that you could use (and cytoscape-cola uses too).

    From the docs (emphasis is mine):

    layoutready : when a layout has set initial positions for all the nodes (but perhaps not final positions)

    layoutstop : when a layout has finished running completely or otherwise stopped running

    I would try to remove that callback and inspect if it gives good results. Also, you should try to reproduce it here: http://jsbin.com/fiqugiq

    It makes it easier for others (even the authors) to play with it and see if you have found a bug or not.