Giter VIP home page Giter VIP logo

cytoscape.js-layout-utilities's Introduction

cytoscape-layout-utilities

Description

This Cytoscape.js extension provides miscellenaous layout utilities in order to manage the placement of nodes or components without layout information.

Sometimes, your graph will have a pre-calculated layout but changes (addition and/or deletion of nodes and edges or simply changes in graph geometry) will require an incremental layout to adjust the existing layout according to these changes. However, often times, the new nodes do not have an initial position (or a good one) for incremental layout (randomize: false) to produce good results. Some utility methods in this library try to choose the best position for any node without a previous or outdated layout, so that subsequent incremental layout is a good one. 

When doing so, these methods use the following heuristics [1]:

  • If a new node without layout has a single neighbor with layout information,  the idea is to split the neighboring space around the single neighbor into quadrants and scoring how crowded each quadrant is, and placing the new node in the least crowded quadrant.

  • If a new node without layout has multiple neighbors with layout information, we chose the position of the new node without layout to be the geometric center of all its neighbors.

  • If a new node without layout has no neighbors with layout information, then we place it around the periphery of the bounding box of all nodes with up-to-date layout.

In all of the above cases, we try to choose a position which is an ideal edge length away from a neighbor, and use a random offset for the final location of the node to avoid multiple nodes ending up at the exact same location since most layout algorithms will not gracefully handle such cases. Hence these are exposed as user customizable options by the extension.

Another utility available in this library is for placing / packing components of a disconnected graph. Often times a particular layout algorithm will nicely lay out individual components of a disconnected graph but will not properly pack these components with respect to each other. Below is an example where a layout algorithm works by laying out a disconnected graph normally (left) and uses this extension to pack components after layout (right).

       

This library uses a polyomino packing based algorithm to achieve this. A polyomino is a geometric figure composed of unit squares joined at their edges. Each component is represented with a polyomino and these polyominoes are packed by a greedy algorithm described in [2].

One can provide the list of nodes and edges in each component of a disconnected graph and this library will calculate a good respective positioning for these components, returning the amount by which each graph object in a component needs to be relocated. The utility can be applied in a randomized way (randomizing component positions) or incrementally (starting from current component positions). In randomized case, the utility will take a desired aspect ratio and try to pack components so that all components will fit into a window of this aspect ratio, minimizing wasted space. It will also take polyomino grid size factor as an option, which is 1.0 by default, corresponding to the average node dimension in the graph. The lower this factor is the finer of a grid will be used. Similarly, higher values will correspond to relatively coarser grid. Notice that use of a finer grid will result in better packing of components at the cost of additional running time. If applied incrementally, the utility will not use these options, but try to achieve a good packing by maintaining user's mental map.

Recommended usage of packing utility for disconnected graphs for a layout extension is as follows. The layout should first detect components of a given graph using eles.components(). Then for each component a separate, independent layout should be calculated. The resulting graph will have a layout where components might overlap or might be very far from each other. A call to this extension with those components will return the amount of relocation needed for each component so that the resulting final layout for the disconnected graph is rather tight.

Notice that in case you make use of compound nodes, it's sufficient to pass only highest level (root) nodes in the nesting hierarchy.

Currently, some layout extensions such as fCoSE and CiSE support packing utility as an option. These layout extensions require that this extension should be registered along with the layout extension to be able to apply packing utility.

Here is a demo:

Please cite the following when you use this extension:

[1] U. Dogrusoz, A. Karacelik, I. Safarli, H. Balci, L. Dervishi, M.C. Siper, "Efficient methods and readily customizable libraries for managing complexity of large networks", PLOS ONE, 13(5):e0197238, 2018.

[2] K. Freivalds, U. Dogrusoz, and P. Kikusts, "Disconnected Graph Layout and the Polyomino Packing Approach", LNCS, vol. 2265, pp. 378-391, 2002.

Dependencies

  • Cytoscape.js ^3.2.0

Usage instructions

Download the library:

  • via npm: npm install cytoscape-layout-utilities,
  • via bower: bower install cytoscape-layout-utilities, or
  • via direct download in the repository (probably from a tag).

Import the library as appropriate for your project:

ES import:

import cytoscape from 'cytoscape';
import layoutUtilities from 'cytoscape-layout-utilities';

cytoscape.use( layoutUtilities );

CommonJS require:

let cytoscape = require('cytoscape');
let layoutUtilities = require('cytoscape-layout-utilities');

cytoscape.use( layoutUtilities ); // register extension

AMD:

require(['cytoscape', 'cytoscape-layout-utilities'], function( cytoscape, layoutUtilities ){
  layoutUtilities( cytoscape ); // register extension
});

Plain HTML/JS has the extension registered for you automatically, because no require() is needed.

API

var instance = cy.layoutUtilities(options)

Initializes the extension and sets options. This can be used to override default options.

An instance has a number of functions available:

instance.setOption(name, value)

Sets the value of the option given by the name to the given value.

instance.placeHiddenNodes(nodesWithLayout)

Places hidden neighbors of each given node. It is assumed that the given nodes have pre-calculated layout, and with this method, proper initial positions are calculated for their hidden neighbors to prepare them for a successful incremental layout.

instance.placeNewNodes(nodesWithoutLayout)

Places each given node. It is assumed that the remaining nodes in the graph already have pre-calculated layout, whereas given nodes do not. With this method, given nodes are positioned with respect to their already laid out neighbors so that a following incremental layout produce a good layout for the entire graph.

instance.packComponents(components, randomize = true)

Packs components of a disconnected graph. Packing is done in a way that it preserves the center of the bounding rectangle of components. The function parameter components has two arrays, namely nodes and edges. Each node has properties (x, y), top left corner coordinate of the node, width and height. Each edge has the properties (startX, startY), (endX, endY) representing the starting and ending points of the edge, respectively. randomize parameter (default true) determines whether packing is applied in a randomized way (randomizing component positions) or incrementally ( starting from current component positions).

The function returns an object which has the following properties:

  1. shift amount needed: an array of shift amounts (dx, dy). Each element in the corrosponding (same index) input component should be shifted by this amount.
  2. aspect ratio: the actual aspect ratio of the resulting packed components (only for randomized packing)
  3. fullness: the fullness of the resulting packed components (only for randomized packing)
  4. adjusted fullness: the adjusted (with respect to desired aspect ratio) fullness of the resulting packed components (only for randomized packing)

Here is a sample input:

[
  {//first component
    nodes:[
          {x:150,y:100,width:30,height:20},
          {x:400,y:120,width:40,height:20}
          ],
    edges:[
          {startX:165,startY:110,endX:420,endY:130}
          ]
  },
  {//second component
    nodes:[
          {x:80,y:40,width:20,height:20},
          {x:600,y:800,width:30,height:30},
          {x:200,y:200,width:30,height:20},
          ],
    edges:[
          {startX:90,startY:50,endX:215,endY:210},
          {startX:90,startY:50,endX:615,endY:815}
          ]
  }
]

resulting sample output:

{
  shifts: [
          {dx:344,dy:420},//shift amount for first component
          {dx:-80,dy:-40}//shift amount for second component
         ],
  aspectRatio: 1,
  fullness: 35.25,
  adjustedFullness: 30.40
}

Default Options

      // Placing new / hidden nodes
      idealEdgeLength: 50,
      offset: 20,
      
      // Packing options - options other than componentSpacing are only for randomized packing
      desiredAspectRatio: 1,
      polyominoGridSizeFactor: 1,
      utilityFunction: 1  // maximize adjusted Fullness   2: maximizes weighted function of fullness and aspect ratio
      componentSpacing: 80 // use to increase spacing between components in pixels. If passed undefined when randomized is false, then it is calculated automatically.

Build targets

  • npm run test : Run Mocha tests in ./test
  • npm run build : Build ./src/** into cytoscape-layout-utilities.js
  • npm run watch : Automatically build on changes with live reloading (N.b. you must already have an HTTP server running)
  • npm run dev : Automatically build on changes with live reloading with webpack dev server
  • npm run lint : Run eslint on the source

N.b. all builds use babel, so modern ES features can be used in the src.

Publishing instructions

This project is set up to automatically be published to npm and bower. To publish:

  1. Build the extension : npm run build:release
  2. Commit the build : git commit -am "Build for release"
  3. Bump the version number and tag: npm version major|minor|patch
  4. Push to origin: git push && git push --tags
  5. Publish to npm: npm publish .
  6. If publishing to bower for the first time, you'll need to run bower register cytoscape-layout-utilities https://github.com//cytoscape.js-layout-utilities.git
  7. Make a new release for Zenodo.

Credits

Third-party libraries: Turf-js, convex-minkowski-sum licensed with MIT. d3-delaunay licensed with ISC license.

Team

Alumni

cytoscape.js-layout-utilities's People

Contributors

canbax avatar hasanbalci avatar msalihaltun avatar nasimsaleh avatar onsah avatar rumeysaozaydin avatar ugurdogrusoz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

cytoscape.js-layout-utilities's Issues

Holistic approach to placing multiple levels of hidden neighbors

Currently, the utility's place hidden neighbors method assumes only 1 level of hidden neighbors to be shown at one time. However, in some cases (e.g. Newt), one might need to show multiple levels. We need more of a holistic approach for handling this rather than showing one level at a time.

Expose packing in API

Currently the options for configuring packing is in README but nothing related to the usage of these methods.

Demo improvements

  • 1. "Desired Aspect Ratio" => "Desired aspect ratio" and so on everywhere including dropdown box content (only capitalize the first word)
  • 2. Move Result aspect ratio and Result fullness in "Pack components" section
  • 3. Make the step increment/decrement 0.1 for Polyomino grid size factor as well
  • 4. Display results only 2 digits after decimal point
  • 5. Doesn't look like Result aspect ratio is calculated (taken from?) correctly as it's almost always the same as Desired aspect ratio
  • 6. Relabel nodes consistently n1, n2, ...
  • 7. Also display "Result adjusted fullness".

Parameterization issue for packing

Hello,
I'm currently working on creating compound graphs relying on fcose, and trying to take advantage of layout utilities for packing the nodes. Reading the documentation and playing with the options, I currently didn't succeeded in removing some wasted space with packing utilities. I attached a snapshop of the graph I obtained plus the layout options I used.
I've to admit I'm a little bit lost with all the parameters and what should results from their combination.
Any suggestion on how I should parameter the layout in order to reduce the wasted space?
Thanks in advance for your support.

/// Definition of the layout for the compound graph interactive viewer
var layout1={
name: "fcose",
randomize: true,
// fisheye: true,
animate: 'end',
fit: true,
nodeDimensionsIncludeLabels: "true",
condense:true,
gravity: 1000,
gravityRangeCompound: 100,
gravityCompound: 100,
gravityRange: 3.8,
packComponents: true,
// initialEnergyOnIncremental: 0.3,
// nodeRepulsion: node => 10000,
// idealEdgeLength: edge => 10,
// edgeElasticity: edge => 0.45,
// nestingFactor: 0.1,
numIter: 50000,
quality: "default"

};
/// complementary layout with parameters to define when randomize is not used for layouting
//packing
var layout2= {
idealEdgeLength: 1,
offset: 20,
desiredAspectRatio: 10,
polyominoGridSizeFactor:0.1,
utilityFunction: 1,
componentSpacing: 1}
Capture d’écran 2022-03-11 à 05 11 29

Packing moves components to unrelated location

When the utility of packing of components is used for disconnected graphs, the components may end up in locations which are completely different than their original ones.
Instead, we should compute the center of a minimal bounding box based on input positions and adjust the output to be centered at this location as well.

Edge bend support

Currently edges are assumed to be straight lines by this extension. We should either:

  • ask the users to pass a minimally sized dummy node for each bend point in README, or
  • allow users to include bend points as part of each edge

Show all bug for hidden edges

If you hide edges in the demo and try to make them reapper by clicking "show all" it doesn't restore them. However, it restores them if you use the "show hidden neighbours" option.

Incremental packing improvement

When we have few components incremental packing is not stable as seen below. I suspect that an edge might be created between the red component and the small blue component down to the right in this case, yielding such a result:
imgpsh_fullsize_anim

Demo not passing edges correctly to packing utility

While debugging #26, I came across another bug with below graph with two components. The larger component contains 6 non-multiple edges. But when we call packing here, an inspection of the start and end coordinates of the 6 edges reveal that (n5,n7) is listed twice and (n6,n8) is missing!
Screen Shot 2021-05-12 at 14 29 23

Screen Shot 2021-05-12 at 14 32 15

      nodes: [
        { data: { id: 'n1', name: 'n1' }, position: {x: 0, y: 0} },
        { data: { id: 'n2', name: 'n2' }, position: {x: 0, y: 100} },
        { data: { id: 'n3', name: 'n3' }, position: {x: 100, y: 100} },

        { data: { id: 'n5', name: 'n5' }, position: {x: 200, y: 0} },
        { data: { id: 'n6', name: 'n6' }, position: {x: 200, y: 700} },
        { data: { id: 'n7', name: 'n7' }, position: {x: 700, y: 700} },
        { data: { id: 'n8', name: 'n8' }, position: {x: 700, y: 0} },
      ],
      edges: [
        { data: { source: "n1", target: "n2" } },
        { data: { source: 'n2', target: 'n3' } },
        { data: { source: 'n3', target: 'n1' } },

        { data: { source: "n5", target: "n6" } },
        { data: { source: 'n6', target: 'n7' } },
        { data: { source: 'n7', target: 'n5' } },
        { data: { source: "n5", target: "n8" } },
        { data: { source: "n6", target: "n8" } },
        { data: { source: "n7", target: "n8" } },
      ]

Demo improvements

  • Let's remove redo layout as it's redundant (see the Rearrange on Hide/Show checkbox)
  • Coloring nodes with hidden neighbors works only one time on hide but we lose this with the next operation (same behavior with the demo of this extension). Let's discuss.

Place new nodes not working properly

When we have new nodes in a graph and want to place them in available, empty spaces near their neighbors, we use placeNewNodes with new node list as a parameter. However, this does not seem to be working properly and placing new nodes in spaces that are already occupied even when there is available space around its single neighbor.

Show hidden neighbors not performing proper layout

  • Take a graph with a good layout
  • Mess up the layout
  • Select a single node and apply "Show hidden neighbors of selected"
  • Notice how only some nodes move and others (messed up part) stays messed up. I am guessing this is a demo bug, hopefully not a bug with the extension's hide/show facility.

Spacing option in between components

Sometimes the components are too close to each other, which might not be desirable by the user. There should be an option "componentSpacing" that is zero by default by could be a positive number, increasing the space between components. This could be an option used to extend the bounding rectangle of each component so that they are taken to be bigger than they actually are.

Infinite loop during packing

I was trying to pack two disconnected nodes and realized that sometimes code runs into infinite loop. I think it's this loop in which the cells variable becomes 0 and cannot find placement. A sample case can be found here.

Update Demo and README

We should illustrate how new nodes (neighbors of existing ones) can be initially nicely placed before an incremental layout can take place (placeNewNodes) in the Demo as well (maybe something similar to how we do it in CiSE demo when right clicked on a node).
In addition, README should be improved and all features should be properly documented.

placeNewNodes ignoring compounds, parent-child relations

When integrating new nodes into an existing graph with a layout, the user could use the placeNewNodes API. When doing so we check whether or not the new nodes have neighbors with existing layout information but we ignore the parent-child relations. For instance, if a compound node is a neighbor of an existing node, this compound's children are also indirectly neighbors with the existing node. We currently completely ignore such relations.

Incremental packing

When a graph is laid out and its components are packed, we get a nice compact layout of disconnected graphs. However, if some minor changes are made to the graph topology (e.g. adding some neighbors to one of the nodes), then the packing is not done incrementally when layout is done incrementally. If the layout is done non-incrementally, the user loses their mental map o the graph. So, let's see if we can find a way to make the current polyomino packing algorithm work incrementally.

Search for more levels before a decision

Looks like we should try more neighboring levels for a good location to place the next polyomino before we make a decision. Otherwise, this is more or less the first closest place that works. Maybe at least maximum of half of width and height (# of squares) of the bounding box of the polyomino.

Non-incremental packing fails in some cases

Non-incremental component packing fails in some cases like below. Unfortunately, I couldn't identify exactly in which cases it fails, but can generally be reproduced with a node setup similar to the example below. This bug is valid for both master and unstable branches.

Animation

How to use this utility for compound nodes

How should I use this utility for compound nodes ?
When use layout cose-bilkent with tile true, the disconnected nodes which maybe have parent node or not, will be tiled together, but sometimes the disconnected nodes maybe layout far away from the connected nodes. Is this utility effective for such case ?

Notice that in case you make use of compound nodes, it's sufficient to pass only highest level (root) nodes in the nesting hierarchy.

I read this sentence from the description of this tool. Does it mean cy.elements(":visible[^parent]").components() ?

Demo related issues

  • 1. "Shift + taphold" seems to be not working, and also it's a bit confusing term, what does it exactly mean?
  • 2. In the "pack components" section of the website, it might be a good idea to rearrange the order of the entries. And maybe creating a button to "pack components" rather than to use the title as a button would be more user-friendly.
  • 3. In the "pack components" section, it would also be good to either hide or specify "Result" fields as empty before "pack components" was applied.

packingApi.packComponents() hangs sometimes 1.0.0

Our application sometimes hangs, and I've now spent several hours tracking down where the problem is.

Finally I found out, that the problem is with packComponents() method.

If I have to nodes, sometimes the following subgraphs are generated and this results in busy loop:

[
    {
        "nodes": [
            {
                "x": 0,
                "y": 0,
                "width": 45,
                "height": 45
            }
        ],
        "edges": []
    },
    {
        "nodes": [
            {
                "x": 55,
                "y": 0,
                "width": 45,
                "height": 45
            }
        ],
        "edges": []
    }
]

Below you can see the whole code:

    let cy = cytoscape({
      container: this.$refs.cyto,
      elements: this.generateElements(), 
      style: this.generateStyle(),
      autounselectify: true,
      userPanningEnabled: false,
      userZoomingEnabled: false,
      boxSelectionEnabled: false,
      autoungrabify: true
    });

    cyInstance = cy;

    cy.layout({
      name: "cose-bilkent",
      animate: false,
      idealEdgeLength: 20,
      quality: "proof",
      randomize: false,
      nodeDimensionsIncludeLabels: true,
      nodeRepulsion: 8000,
      edgeElasticity: 0.7,
      nestingFactor: 0.1,
      numIter: 10000,
      gravity: 0.25
    }).run();

    var packingApi = cy.layoutUtilities({
      desiredAspectRatio: 1,
      polyominoGridSizeFactor: 1,
      utilityFunction: 1,
      componentSpacing: 0
    });

    var components = cy.elements(":visible").components();
    var subgraphs = [];
    components.forEach(function(component) {
      var subgraph = {};
      subgraph.nodes = [];
      subgraph.edges = [];

      component.edges().forEach(function(edge) {
        var boundingBox = edge.boundingBox();
        subgraph.edges.push({
          startX: boundingBox.x1,
          startY: boundingBox.y1,
          endX: boundingBox.x2,
          endY: boundingBox.y2
        });
      });
      component.nodes().forEach(function(node) {
        var boundingBox = node.boundingBox();
        subgraph.nodes.push({
          x: boundingBox.x1,
          y: boundingBox.y1,
          width: boundingBox.w,
          height: boundingBox.h
        });
      });

      subgraphs.push(subgraph);
    });

    var result = packingApi.packComponents(subgraphs);

Let me know if you think the problem could be caused with our generateElements() or generateStyles() methods, I can then try and tidy them up. However they are quite simple methods and I think are not part of the problem.

Pack components inconsistent spacing

With the component spacing of 30, incremental and non-incremental layout results in different actual spacing:

Non-incremental:
Screenshot 2021-02-24 162123

Incremental:
Screenshot 2021-02-24 162139

Let's make them consistent (by trimming values prior to the operations if needed). I think the default should be 80 and the non-incremental version should deduct a constant amount from this value to stay consistent.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.