vue-cytoscape icon indicating copy to clipboard operation
vue-cytoscape copied to clipboard

Layout not setting after loading data

Open TJBrunson opened this issue 4 years ago • 14 comments

I am attempting to load data through axios and pass it into a child component that sets up Cytoscape. I have configured the style and display as directed in the documentation, but I get all of my nodes stacked on top of each other.

How can I either, reset the layout after the nodes are added, or have it set automatically when nodes are added?

For reference, here is the relevant part of my template:

<v-card-text>
            <cytoscape ref="cy" :config="cyConfig" :afterCreated="afterCreated">
              <cy-element v-for="def in fiData"=  :key="`${def.data.id}`"  :definition="def" />
            </cytoscape>
 </v-card-text>

my afterCreated method is:

afterCreated(cy) {
      cy.layout({ name: "cose" }).run();
      cy.fit();
      alert("layout changed");
    }

and hee is my cyConfig located in my data property:

    cyConfig: {
      style: [
        {
          selector: "node",
          style: {
            "width": "9",
            "height": "9",
            "label": "data(name)",
            "font-size": "6px",
            "shape": "ellipse",
            "background-color": "#00CC00",
            "border-color": "#00CC00",
            "background-opacity": ".4"
          }
        },
        {
          selector: "node:selected",
          style: {
            "border-width": "6px",
            "border-color": "#AAD8FF",
            "border-opacity": "0.5",
            "text-outline-color": "#0bb50b"
          }
        },
        {
          selector: "edge",
          style: {
            "curve-style": "bezier",
            "line-color": "#bbb",
            "width": "1",
            "overlay-padding": "20px"
          }
        },
        {
          selector: "edge:selected",
          style: {
            "line-color": "#FF0000",
            "width": "2"
          }
        },
      ],
      layout: {
        name: "cose",
      }
    }

TJBrunson avatar May 05 '20 18:05 TJBrunson

Ok, so, it seems like the issue is that this sets the layout before all of the elements are added. If I store the instance of cytoscape and reset the layout from a button press, it corrects the issue.

How can I get the layout to auto update after elements are added?

TJBrunson avatar May 05 '20 22:05 TJBrunson

Hi @TJBrunson,

How can I get the layout to auto update after elements are added?

After you add an element you can call layout() again. Check the addNode method in the example. Something like:

this.$refs.cy.instance.layout({ name: "cose" }).run()

Let me know if that works for you.

rcarcasses avatar May 06 '20 07:05 rcarcasses

I have solved it by adding a button and clicking it after I ensure the cy-element for loop has ended. Is there a way I can tell programmatically when the last node I add is added so I can run it then?

I tried running it from the :afterCreated directive on the cytoscape instance, but it causes an error. I think it is because no nodes are created at that point.

TJBrunson avatar May 06 '20 15:05 TJBrunson

I have ran a pretty thorough debug and the issue seems to be either:

  1. The nodes have not loaded during the :afterCreated Directive.
  2. There is an issue between lines 15340 and 15344 of vue-cytoscape.common.js causing no nodes to be selected into var eles.

I think probably the issue has to do with point 1 because I can use a button to set the layout after render.

TJBrunson avatar May 06 '20 15:05 TJBrunson

ok. This is how I fixed it.

I removed the cy-element v-for loop to add all of the elements and added this code:

afterCreated(cy) {
      this.cy = cy;
      this.addInitialNodes();
    },
    addInitialNodes() {
      this.cy.add(this.fiData);
      this.cy.layout({ name: "cose" }).run();
    },

Basically, if you try to set the layout from the afterCreated method, no nodes have been added yet. So instead of adding nodes through the cy-elements tag with a loop, I add them through the cy instance and then set the layout after that method call returns.

TJBrunson avatar May 06 '20 15:05 TJBrunson

There has to be a more elegant way to do this? I tried your solution @TJBrunson and it only works for "cose". "random" or "grid" layouts for example don't work. I wonder if this is to do some async-stuff going on there?

yeus avatar Aug 15 '20 23:08 yeus

Anyone find a solution for this yet? It seems a pity to waste the potential of using <cy-element> components when we can't apply a layout after they have initialized.

I've successfully set the layout both using a secondary "layout" button and by explicitly calling layout().run() in the afterCreated() when I manually add data, but it seems like a serious issue that layouts don't gel with the <cy-element> component.

ghost avatar Aug 26 '20 13:08 ghost

I have a solution, I think. I'm new to frontend stuff so please correct me if my understanding or terminology is wrong. I've been having this same issue of nodes getting stuck in the upper left corner but rendering correctly after I click a "layout" button.

I think the problem with using the cy-elements API is that calling layout.run() gets called before the DOM is aware that something has updated. This causes either some nodes or no nodes at all to be positioned correctly for the layout and they're all stacked up on each other in the upper left corner.

What needs to happen is that the layout needs to be called after all of the cyelements have been added. Here is what cyelements typically looks like:

 <cy-element
      v-for="def in cyElements"
      :key="`${def.data.id}`"
      :definition="def"
      v-on:click="showData($event, def)"
    />

The proper way to run layout().run() is to use Vue's nextTick() which tells the code to run after the next DOM cycle updates. Not sure if there might be race conditions when there's a lot going on the screen but here's what I have for my code to update layout:

  watch: {
    cyElements: async function (val) {
      const cy = await cyPromise;
      this.$nextTick(() => {
        cy.layout(this.layoutMode).run();
        cy.fit(null, 200);
      });
  },

In my case, cyElements is something I'm pulling from Vuex after a call to a neo4j backend and layoutMode is a set of layouts I have defined elsewhere but the important part is that the elements don't get the layout until all of the cyElements have been updated to cytoscape! I no longer get nodes stacking up in the corner anymore even after dynamically changing the list of elements.

I would make a PR but not only am I not sure if one is needed, but I wouldn't know what to change in the first place. Definitely could use the change in documentation though if this ends up working for others.

daddycocoaman avatar Aug 31 '20 21:08 daddycocoaman

I have a solution, I think. I'm new to frontend stuff so please correct me if my understanding or terminology is wrong. I've been having this same issue of nodes getting stuck in the upper left corner but rendering correctly after I click a "layout" button.

I think the problem with using the cy-elements API is that calling layout.run() gets called before the DOM is aware that something has updated. This causes either some nodes or no nodes at all to be positioned correctly for the layout and they're all stacked up on each other in the upper left corner.

What needs to happen is that the layout needs to be called after all of the cyelements have been added. Here is what cyelements typically looks like:

 <cy-element
      v-for="def in cyElements"
      :key="`${def.data.id}`"
      :definition="def"
      v-on:click="showData($event, def)"
    />

The proper way to run layout().run() is to use Vue's nextTick() which tells the code to run after the next DOM cycle updates. Not sure if there might be race conditions when there's a lot going on the screen but here's what I have for my code to update layout:

  watch: {
    cyElements: async function (val) {
      const cy = await cyPromise;
      this.$nextTick(() => {
        cy.layout(this.layoutMode).run();
        cy.fit(null, 200);
      });
  },

In my case, cyElements is something I'm pulling from Vuex after a call to a neo4j backend and layoutMode is a set of layouts I have defined elsewhere but the important part is that the elements don't get the layout until all of the cyElements have been updated to cytoscape! I no longer get nodes stacking up in the corner anymore even after dynamically changing the list of elements.

I would make a PR but not only am I not sure if one is needed, but I wouldn't know what to change in the first place. Definitely could use the change in documentation though if this ends up working for others.

This workaround worked as well for me

soipo avatar Jan 08 '21 19:01 soipo

I can't get this to work either, and @daddycocoaman's solution doesn't seem to do the trick either (maybe I did it wrong).

<cytoscape ref="cy"
         :config="config"
         v-on:mousedown="mouseDown"
         v-on:cxttapstart="updateNode"
         :afterCreated="afterCreated" :preConfig="preConfig">
<cy-element
    v-for="def in elements"
    :key="`${def.data.id}`"
    :definition="def"
    :sync="true"
    />
  </cytoscape>

And the elements and config defined as reactive data:

  data () {
    return {
      elements: [],

      config: {
        layout: {
          name: 'dagre' // tried others too
           },
       style: [
       ....

This by itself does not make a layout change, even when forced like this:

 preConfig: function (cytoscape) {
   cytoscape.use(dagre)
 },
 afterCreated: function(cy) {
   console.log(cy)
   this.$refs.cy.instance.layout(this.config.layout).run()  //tried with cy too
   this.$refs.cy.instance.fit(100)
 }

manishpatelUK avatar Apr 01 '21 14:04 manishpatelUK

@manishpatelUK Hi, the most important thing was the wrap any changes or methods called from the cy instance in a nextTick() block.

daddycocoaman avatar Apr 01 '21 15:04 daddycocoaman

Thanks for the quick response @daddycocoaman . I did as below, but still no joy unfortunately.

 afterCreated: function(cy) {
   console.log(cy)
   this.$nextTick(() => {
     cy.layout(this.config.layout).run();
     cy.fit(null, 200);
   });
 }

manishpatelUK avatar Apr 01 '21 16:04 manishpatelUK

For the afterCreated workaround, the logic surrounding the cy Promise is a little rough. There is no use of async/await or then syntax, which is quite unfortunate, because it is a little hard to follow. I am pretty sure the cy Promise is just not being resolved correctly on line 46 before running afterCreated on 47. The duality between instance and cy is confusing as well. This seems to work,

async function afterCreated(cy) {
  await cy;
  cy.layout({ name: "grid" }).run();
}

@rcarcasses if my company gets behind this then maybe I can help maintain it, but I am unsure at the moment.

aentwist avatar Jun 15 '22 19:06 aentwist

Hey guys, sorry for being off but currently I have little time to maintain this. @aentwist I'm happy to hear you/your company are considering to opt in as a maintainer, you are very welcome!

rcarcasses avatar Jun 16 '22 10:06 rcarcasses