├── .babelrc ├── .eslintignore ├── .eslintrc ├── .github └── stale.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── bower.json ├── cytoscape-cola.js ├── demo-compound.html ├── demo-constraints.html ├── demo-non-animated.html ├── demo.html ├── package-lock.json ├── package.json ├── pages ├── cytoscape-cola.js ├── demo-compound.html ├── demo-constraints.html ├── demo-non-animated.html ├── demo.html └── index.html ├── src ├── assign.js ├── cola.js ├── defaults.js ├── index.js └── raf.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "node": true, 6 | "amd": true, 7 | "es6": true 8 | }, 9 | "extends": "eslint:recommended", 10 | "rules": { 11 | "semi": "error" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 14 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | # Label to use when marking an issue as stale 9 | staleLabel: stale 10 | # Comment to post when marking an issue as stale. Set to `false` to disable 11 | markComment: > 12 | This issue has been automatically marked as stale, because it has not had 13 | activity within the past 30 days. It will be closed if no further activity 14 | occurs within the next 30 days. If a feature request is important to you, 15 | please consider making a pull request. Thank you for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | 3 | Copyright (c) 2016-2018, 2020-2021, The Cytoscape Consortium. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the “Software”), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cytoscape-cola 2 | ================================================================================ 3 | 4 | [![DOI](https://zenodo.org/badge/42205998.svg)](https://zenodo.org/badge/latestdoi/42205998) 5 | 6 | 7 | ## Description 8 | 9 | The Cola.js physics simulation layout for Cytoscape.js ([demo](https://cytoscape.github.io/cytoscape.js-cola), [non-animated demo](https://cytoscape.github.io/cytoscape.js-cola/demo-non-animated.html), [compound demo](https://cytoscape.github.io/cytoscape.js-cola/demo-compound.html), [constraint demo](https://cytoscape.github.io/cytoscape.js-cola/demo-constraints.html)) 10 | 11 | 12 | The `cola` layout uses a [force-directed](http://en.wikipedia.org/wiki/Force-directed_graph_drawing) physics simulation with several sophisticated constraints, written by [Tim Dwyer](http://www.csse.monash.edu.au/~tdwyer/). For more information about Cola and its parameters, refer to [its documentation](http://marvl.infotech.monash.edu/webcola/). 13 | 14 | It supports noncompound and compound graphs well. 15 | 16 | ## Dependencies 17 | 18 | * Cytoscape.js ^3.2.0 19 | * Cola.js ^3.1.2 20 | 21 | 22 | ## Usage instructions 23 | 24 | Download the library: 25 | * via npm: `npm install cytoscape-cola`, 26 | * via bower: `bower install cytoscape-cola`, or 27 | * via direct download in the repository (probably from a tag). 28 | 29 | Import the library as appropriate for your project: 30 | 31 | ES import: 32 | 33 | ```js 34 | import cytoscape from 'cytoscape'; 35 | import cola from 'cytoscape-cola'; 36 | 37 | cytoscape.use( cola ); 38 | ``` 39 | 40 | CommonJS require: 41 | 42 | ```js 43 | let cytoscape = require('cytoscape'); 44 | let cola = require('cytoscape-cola'); 45 | 46 | cytoscape.use( cola ); // register extension 47 | ``` 48 | 49 | AMD: 50 | 51 | ```js 52 | require(['cytoscape', 'cytoscape-cola'], function( cytoscape, cola ){ 53 | cola( cytoscape ); // register extension 54 | }); 55 | ``` 56 | 57 | Plain HTML/JS has the extension registered for you automatically, because no `require()` is needed. 58 | 59 | 60 | 61 | ## API 62 | 63 | Call the layout, e.g. `cy.layout({ name: 'cola', ... })`, with options: 64 | 65 | ```js 66 | // default layout options 67 | var defaults = { 68 | animate: true, // whether to show the layout as it's running 69 | refresh: 1, // number of ticks per frame; higher is faster but more jerky 70 | maxSimulationTime: 4000, // max length in ms to run the layout 71 | ungrabifyWhileSimulating: false, // so you can't drag nodes during layout 72 | fit: true, // on every layout reposition of nodes, fit the viewport 73 | padding: 30, // padding around the simulation 74 | boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h } 75 | nodeDimensionsIncludeLabels: false, // whether labels should be included in determining the space used by a node 76 | 77 | // layout event callbacks 78 | ready: function(){}, // on layoutready 79 | stop: function(){}, // on layoutstop 80 | 81 | // positioning options 82 | randomize: false, // use random node positions at beginning of layout 83 | avoidOverlap: true, // if true, prevents overlap of node bounding boxes 84 | handleDisconnected: true, // if true, avoids disconnected components from overlapping 85 | convergenceThreshold: 0.01, // when the alpha value (system energy) falls below this value, the layout stops 86 | nodeSpacing: function( node ){ return 10; }, // extra spacing around nodes 87 | flow: undefined, // use DAG/tree flow layout if specified, e.g. { axis: 'y', minSeparation: 30 } 88 | alignment: undefined, // relative alignment constraints on nodes, e.g. {vertical: [[{node: node1, offset: 0}, {node: node2, offset: 5}]], horizontal: [[{node: node3}, {node: node4}], [{node: node5}, {node: node6}]]} 89 | gapInequalities: undefined, // list of inequality constraints for the gap between the nodes, e.g. [{"axis":"y", "left":node1, "right":node2, "gap":25}] 90 | centerGraph: true, // adjusts the node positions initially to center the graph (pass false if you want to start the layout from the current position) 91 | 92 | // different methods of specifying edge length 93 | // each can be a constant numerical value or a function like `function( edge ){ return 2; }` 94 | edgeLength: undefined, // sets edge length directly in simulation 95 | edgeSymDiffLength: undefined, // symmetric diff edge length in simulation 96 | edgeJaccardLength: undefined, // jaccard edge length in simulation 97 | 98 | // iterations of cola algorithm; uses default values on undefined 99 | unconstrIter: undefined, // unconstrained initial layout iterations 100 | userConstIter: undefined, // initial layout iterations with user-specified constraints 101 | allConstIter: undefined, // initial layout iterations with all constraints including non-overlap 102 | }; 103 | ``` 104 | 105 | 106 | ## Notes 107 | 108 | - The `alignment` option isn't as flexible as the raw Cola option. Here, only integers can be used to specify relative positioning, so it's a bit limited. If you'd like to see a more sophisticated implementation, please send a pull request. 109 | 110 | 111 | 112 | ## Build targets 113 | 114 | * `npm run test` : Run Mocha tests in `./test` 115 | * `npm run build` : Build `./src/**` into `cytoscape-cola.js` 116 | * `npm run watch` : Automatically build on changes with live reloading (N.b. you must already have an HTTP server running) 117 | * `npm run dev` : Automatically build on changes with live reloading with webpack dev server 118 | * `npm run lint` : Run eslint on the source 119 | 120 | N.b. all builds use babel, so modern ES features can be used in the `src`. 121 | 122 | 123 | ## Publishing instructions 124 | 125 | This project is set up to automatically be published to npm and bower. To publish: 126 | 127 | 1. Build the extension : `npm run build:release` 128 | 1. Commit the build : `git commit -am "Build for release"` 129 | 1. Bump the version number and tag: `npm version major|minor|patch` 130 | 1. Push to origin: `git push && git push --tags` 131 | 1. Publish to npm: `npm publish .` 132 | 1. If publishing to bower for the first time, you'll need to run `bower register cytoscape-cola https://github.com/cytoscape/cytoscape.js-cola.git` 133 | 1. [Make a new release](https://github.com/cytoscape/cytoscape.js-cola/releases/new) for Zenodo. 134 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cytoscape-cola", 3 | "description": "The Cola.js physics simulation layout for Cytoscape.js", 4 | "main": "cytoscape-cola.js", 5 | "dependencies": { 6 | "cytoscape": "^3.2.0" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/cytoscape/cytoscape.js-cola.git" 11 | }, 12 | "ignore": [ 13 | "**/.*", 14 | "node_modules", 15 | "bower_components", 16 | "test", 17 | "tests" 18 | ], 19 | "keywords": [ 20 | "cytoscape", 21 | "cytoscape-extension" 22 | ], 23 | "license": "MIT" 24 | } 25 | -------------------------------------------------------------------------------- /cytoscape-cola.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(require("webcola")); 4 | else if(typeof define === 'function' && define.amd) 5 | define(["webcola"], factory); 6 | else if(typeof exports === 'object') 7 | exports["cytoscapeCola"] = factory(require("webcola")); 8 | else 9 | root["cytoscapeCola"] = factory(root["webcola"]); 10 | })(this, function(__WEBPACK_EXTERNAL_MODULE_5__) { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | /******/ 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | /******/ 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) { 20 | /******/ return installedModules[moduleId].exports; 21 | /******/ } 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ i: moduleId, 25 | /******/ l: false, 26 | /******/ exports: {} 27 | /******/ }; 28 | /******/ 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | /******/ 32 | /******/ // Flag the module as loaded 33 | /******/ module.l = true; 34 | /******/ 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | /******/ 39 | /******/ 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | /******/ 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | /******/ 46 | /******/ // identity function for calling harmony imports with the correct context 47 | /******/ __webpack_require__.i = function(value) { return value; }; 48 | /******/ 49 | /******/ // define getter function for harmony exports 50 | /******/ __webpack_require__.d = function(exports, name, getter) { 51 | /******/ if(!__webpack_require__.o(exports, name)) { 52 | /******/ Object.defineProperty(exports, name, { 53 | /******/ configurable: false, 54 | /******/ enumerable: true, 55 | /******/ get: getter 56 | /******/ }); 57 | /******/ } 58 | /******/ }; 59 | /******/ 60 | /******/ // getDefaultExport function for compatibility with non-harmony modules 61 | /******/ __webpack_require__.n = function(module) { 62 | /******/ var getter = module && module.__esModule ? 63 | /******/ function getDefault() { return module['default']; } : 64 | /******/ function getModuleExports() { return module; }; 65 | /******/ __webpack_require__.d(getter, 'a', getter); 66 | /******/ return getter; 67 | /******/ }; 68 | /******/ 69 | /******/ // Object.prototype.hasOwnProperty.call 70 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 71 | /******/ 72 | /******/ // __webpack_public_path__ 73 | /******/ __webpack_require__.p = ""; 74 | /******/ 75 | /******/ // Load entry module and return exports 76 | /******/ return __webpack_require__(__webpack_require__.s = 3); 77 | /******/ }) 78 | /************************************************************************/ 79 | /******/ ([ 80 | /* 0 */ 81 | /***/ (function(module, exports, __webpack_require__) { 82 | 83 | "use strict"; 84 | 85 | 86 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 87 | 88 | var assign = __webpack_require__(1); 89 | var defaults = __webpack_require__(2); 90 | var cola = __webpack_require__(5) || (typeof window !== 'undefined' ? window.cola : null); 91 | var raf = __webpack_require__(4); 92 | var isString = function isString(o) { 93 | return (typeof o === 'undefined' ? 'undefined' : _typeof(o)) === _typeof(''); 94 | }; 95 | var isNumber = function isNumber(o) { 96 | return (typeof o === 'undefined' ? 'undefined' : _typeof(o)) === _typeof(0); 97 | }; 98 | var isObject = function isObject(o) { 99 | return o != null && (typeof o === 'undefined' ? 'undefined' : _typeof(o)) === _typeof({}); 100 | }; 101 | var isFunction = function isFunction(o) { 102 | return o != null && (typeof o === 'undefined' ? 'undefined' : _typeof(o)) === _typeof(function () {}); 103 | }; 104 | var nop = function nop() {}; 105 | 106 | var getOptVal = function getOptVal(val, ele) { 107 | if (isFunction(val)) { 108 | var fn = val; 109 | return fn.apply(ele, [ele]); 110 | } else { 111 | return val; 112 | } 113 | }; 114 | 115 | // constructor 116 | // options : object containing layout options 117 | function ColaLayout(options) { 118 | this.options = assign({}, defaults, options); 119 | } 120 | 121 | // runs the layout 122 | ColaLayout.prototype.run = function () { 123 | var layout = this; 124 | var options = this.options; 125 | 126 | layout.manuallyStopped = false; 127 | 128 | var cy = options.cy; // cy is automatically populated for us in the constructor 129 | var eles = options.eles; 130 | var nodes = eles.nodes(); 131 | var edges = eles.edges(); 132 | var ready = false; 133 | 134 | var isParent = function isParent(ele) { 135 | return ele.isParent(); 136 | }; 137 | 138 | var parentNodes = nodes.filter(isParent); 139 | 140 | var nonparentNodes = nodes.subtract(parentNodes); 141 | 142 | var bb = options.boundingBox || { x1: 0, y1: 0, w: cy.width(), h: cy.height() }; 143 | if (bb.x2 === undefined) { 144 | bb.x2 = bb.x1 + bb.w; 145 | } 146 | if (bb.w === undefined) { 147 | bb.w = bb.x2 - bb.x1; 148 | } 149 | if (bb.y2 === undefined) { 150 | bb.y2 = bb.y1 + bb.h; 151 | } 152 | if (bb.h === undefined) { 153 | bb.h = bb.y2 - bb.y1; 154 | } 155 | 156 | var updateNodePositions = function updateNodePositions() { 157 | for (var i = 0; i < nodes.length; i++) { 158 | var node = nodes[i]; 159 | var dimensions = node.layoutDimensions(options); 160 | var scratch = node.scratch('cola'); 161 | 162 | // update node dims 163 | if (!scratch.updatedDims) { 164 | var padding = getOptVal(options.nodeSpacing, node); 165 | 166 | scratch.width = dimensions.w + 2 * padding; 167 | scratch.height = dimensions.h + 2 * padding; 168 | } 169 | } 170 | 171 | nodes.positions(function (node) { 172 | var scratch = node.scratch().cola; 173 | var retPos = void 0; 174 | 175 | if (!node.grabbed() && nonparentNodes.contains(node)) { 176 | retPos = { 177 | x: bb.x1 + scratch.x, 178 | y: bb.y1 + scratch.y 179 | }; 180 | 181 | if (!isNumber(retPos.x) || !isNumber(retPos.y)) { 182 | retPos = undefined; 183 | } 184 | } 185 | 186 | return retPos; 187 | }); 188 | 189 | nodes.updateCompoundBounds(); // because the way this layout sets positions is buggy for some reason; ref #878 190 | 191 | if (!ready) { 192 | onReady(); 193 | ready = true; 194 | } 195 | 196 | if (options.fit) { 197 | cy.fit(options.padding); 198 | } 199 | }; 200 | 201 | var onDone = function onDone() { 202 | if (options.ungrabifyWhileSimulating) { 203 | grabbableNodes.grabify(); 204 | } 205 | 206 | cy.off('destroy', destroyHandler); 207 | 208 | nodes.off('grab free position', grabHandler); 209 | nodes.off('lock unlock', lockHandler); 210 | 211 | // trigger layoutstop when the layout stops (e.g. finishes) 212 | layout.one('layoutstop', options.stop); 213 | layout.trigger({ type: 'layoutstop', layout: layout }); 214 | }; 215 | 216 | var onReady = function onReady() { 217 | // trigger layoutready when each node has had its position set at least once 218 | layout.one('layoutready', options.ready); 219 | layout.trigger({ type: 'layoutready', layout: layout }); 220 | }; 221 | 222 | var ticksPerFrame = options.refresh; 223 | 224 | if (options.refresh < 0) { 225 | ticksPerFrame = 1; 226 | } else { 227 | ticksPerFrame = Math.max(1, ticksPerFrame); // at least 1 228 | } 229 | 230 | var adaptor = layout.adaptor = cola.adaptor({ 231 | trigger: function trigger(e) { 232 | // on sim event 233 | var TICK = cola.EventType ? cola.EventType.tick : null; 234 | var END = cola.EventType ? cola.EventType.end : null; 235 | 236 | switch (e.type) { 237 | case 'tick': 238 | case TICK: 239 | if (options.animate) { 240 | updateNodePositions(); 241 | } 242 | break; 243 | 244 | case 'end': 245 | case END: 246 | updateNodePositions(); 247 | if (!options.infinite) { 248 | onDone(); 249 | } 250 | break; 251 | } 252 | }, 253 | 254 | kick: function kick() { 255 | // kick off the simulation 256 | //let skip = 0; 257 | 258 | var firstTick = true; 259 | 260 | var inftick = function inftick() { 261 | if (layout.manuallyStopped) { 262 | onDone(); 263 | 264 | return true; 265 | } 266 | 267 | var ret = adaptor.tick(); 268 | 269 | if (!options.infinite && !firstTick) { 270 | adaptor.convergenceThreshold(options.convergenceThreshold); 271 | } 272 | 273 | firstTick = false; 274 | 275 | if (ret && options.infinite) { 276 | // resume layout if done 277 | adaptor.resume(); // resume => new kick 278 | } 279 | 280 | return ret; // allow regular finish b/c of new kick 281 | }; 282 | 283 | var multitick = function multitick() { 284 | // multiple ticks in a row 285 | var ret = void 0; 286 | 287 | for (var i = 0; i < ticksPerFrame && !ret; i++) { 288 | ret = ret || inftick(); // pick up true ret vals => sim done 289 | } 290 | 291 | return ret; 292 | }; 293 | 294 | if (options.animate) { 295 | var frame = function frame() { 296 | if (multitick()) { 297 | return; 298 | } 299 | 300 | raf(frame); 301 | }; 302 | 303 | raf(frame); 304 | } else { 305 | while (!inftick()) { 306 | // keep going... 307 | } 308 | } 309 | }, 310 | 311 | on: nop, // dummy; not needed 312 | 313 | drag: nop // not needed for our case 314 | }); 315 | layout.adaptor = adaptor; 316 | 317 | // if set no grabbing during layout 318 | var grabbableNodes = nodes.filter(':grabbable'); 319 | if (options.ungrabifyWhileSimulating) { 320 | grabbableNodes.ungrabify(); 321 | } 322 | 323 | var destroyHandler = void 0; 324 | cy.one('destroy', destroyHandler = function destroyHandler() { 325 | layout.stop(); 326 | }); 327 | 328 | // handle node dragging 329 | var grabHandler = void 0; 330 | nodes.on('grab free position', grabHandler = function grabHandler(e) { 331 | var node = this; 332 | var scrCola = node.scratch().cola; 333 | var pos = node.position(); 334 | var nodeIsTarget = e.cyTarget === node || e.target === node; 335 | 336 | if (!nodeIsTarget) { 337 | return; 338 | } 339 | 340 | switch (e.type) { 341 | case 'grab': 342 | adaptor.dragstart(scrCola); 343 | break; 344 | case 'free': 345 | adaptor.dragend(scrCola); 346 | break; 347 | case 'position': 348 | // only update when different (i.e. manual .position() call or drag) so we don't loop needlessly 349 | if (scrCola.px !== pos.x - bb.x1 || scrCola.py !== pos.y - bb.y1) { 350 | scrCola.px = pos.x - bb.x1; 351 | scrCola.py = pos.y - bb.y1; 352 | } 353 | break; 354 | } 355 | }); 356 | 357 | var lockHandler = void 0; 358 | nodes.on('lock unlock', lockHandler = function lockHandler() { 359 | var node = this; 360 | var scrCola = node.scratch().cola; 361 | 362 | scrCola.fixed = node.locked(); 363 | 364 | if (node.locked()) { 365 | adaptor.dragstart(scrCola); 366 | } else { 367 | adaptor.dragend(scrCola); 368 | } 369 | }); 370 | 371 | // add nodes to cola 372 | adaptor.nodes(nonparentNodes.map(function (node, i) { 373 | var padding = getOptVal(options.nodeSpacing, node); 374 | var pos = node.position(); 375 | var dimensions = node.layoutDimensions(options); 376 | 377 | var struct = node.scratch().cola = { 378 | x: options.randomize && !node.locked() || pos.x === undefined ? Math.round(Math.random() * bb.w) : pos.x, 379 | y: options.randomize && !node.locked() || pos.y === undefined ? Math.round(Math.random() * bb.h) : pos.y, 380 | width: dimensions.w + 2 * padding, 381 | height: dimensions.h + 2 * padding, 382 | index: i, 383 | fixed: node.locked() 384 | }; 385 | 386 | return struct; 387 | })); 388 | 389 | // the constraints to be added on nodes 390 | var constraints = []; 391 | 392 | if (options.alignment) { 393 | // then set alignment constraints 394 | 395 | if (options.alignment.vertical) { 396 | var verticalAlignments = options.alignment.vertical; 397 | verticalAlignments.forEach(function (alignment) { 398 | var offsetsX = []; 399 | alignment.forEach(function (nodeData) { 400 | var node = nodeData.node; 401 | var scrCola = node.scratch().cola; 402 | var index = scrCola.index; 403 | offsetsX.push({ 404 | node: index, 405 | offset: nodeData.offset ? nodeData.offset : 0 406 | }); 407 | }); 408 | constraints.push({ 409 | type: 'alignment', 410 | axis: 'x', 411 | offsets: offsetsX 412 | }); 413 | }); 414 | } 415 | 416 | if (options.alignment.horizontal) { 417 | var horizontalAlignments = options.alignment.horizontal; 418 | horizontalAlignments.forEach(function (alignment) { 419 | var offsetsY = []; 420 | alignment.forEach(function (nodeData) { 421 | var node = nodeData.node; 422 | var scrCola = node.scratch().cola; 423 | var index = scrCola.index; 424 | offsetsY.push({ 425 | node: index, 426 | offset: nodeData.offset ? nodeData.offset : 0 427 | }); 428 | }); 429 | constraints.push({ 430 | type: 'alignment', 431 | axis: 'y', 432 | offsets: offsetsY 433 | }); 434 | }); 435 | } 436 | } 437 | 438 | // if gapInequalities variable is set add each inequality constraint to list of constraints 439 | if (options.gapInequalities) { 440 | options.gapInequalities.forEach(function (inequality) { 441 | 442 | // for the constraints to be passed to cola layout adaptor use indices of nodes, 443 | // not the nodes themselves 444 | var leftIndex = inequality.left.scratch().cola.index; 445 | var rightIndex = inequality.right.scratch().cola.index; 446 | 447 | constraints.push({ 448 | axis: inequality.axis, 449 | left: leftIndex, 450 | right: rightIndex, 451 | gap: inequality.gap, 452 | equality: inequality.equality 453 | }); 454 | }); 455 | } 456 | 457 | // add constraints if any 458 | if (constraints.length > 0) { 459 | adaptor.constraints(constraints); 460 | } 461 | 462 | // add compound nodes to cola 463 | adaptor.groups(parentNodes.map(function (node, i) { 464 | // add basic group incl leaf nodes 465 | var optPadding = getOptVal(options.nodeSpacing, node); 466 | var getPadding = function getPadding(d) { 467 | return parseFloat(node.style('padding-' + d)); 468 | }; 469 | 470 | var pleft = getPadding('left') + optPadding; 471 | var pright = getPadding('right') + optPadding; 472 | var ptop = getPadding('top') + optPadding; 473 | var pbottom = getPadding('bottom') + optPadding; 474 | 475 | node.scratch().cola = { 476 | index: i, 477 | 478 | padding: Math.max(pleft, pright, ptop, pbottom), 479 | 480 | // leaves should only contain direct descendants (children), 481 | // not the leaves of nested compound nodes or any nodes that are compounds themselves 482 | leaves: node.children().intersection(nonparentNodes).map(function (child) { 483 | return child[0].scratch().cola.index; 484 | }), 485 | 486 | fixed: node.locked() 487 | }; 488 | 489 | return node; 490 | }).map(function (node) { 491 | // add subgroups 492 | node.scratch().cola.groups = node.children().intersection(parentNodes).map(function (child) { 493 | return child.scratch().cola.index; 494 | }); 495 | 496 | return node.scratch().cola; 497 | })); 498 | 499 | // get the edge length setting mechanism 500 | var length = void 0; 501 | var lengthFnName = void 0; 502 | if (options.edgeLength != null) { 503 | length = options.edgeLength; 504 | lengthFnName = 'linkDistance'; 505 | } else if (options.edgeSymDiffLength != null) { 506 | length = options.edgeSymDiffLength; 507 | lengthFnName = 'symmetricDiffLinkLengths'; 508 | } else if (options.edgeJaccardLength != null) { 509 | length = options.edgeJaccardLength; 510 | lengthFnName = 'jaccardLinkLengths'; 511 | } else { 512 | length = 100; 513 | lengthFnName = 'linkDistance'; 514 | } 515 | 516 | var lengthGetter = function lengthGetter(link) { 517 | return link.calcLength; 518 | }; 519 | 520 | // add the edges to cola 521 | adaptor.links(edges.stdFilter(function (edge) { 522 | return nonparentNodes.contains(edge.source()) && nonparentNodes.contains(edge.target()); 523 | }).map(function (edge) { 524 | var c = edge.scratch().cola = { 525 | source: edge.source()[0].scratch().cola.index, 526 | target: edge.target()[0].scratch().cola.index 527 | }; 528 | 529 | if (length != null) { 530 | c.calcLength = getOptVal(length, edge); 531 | } 532 | 533 | return c; 534 | })); 535 | 536 | adaptor.size([bb.w, bb.h]); 537 | 538 | if (length != null) { 539 | adaptor[lengthFnName](lengthGetter); 540 | } 541 | 542 | // set the flow of cola 543 | if (options.flow) { 544 | var flow = void 0; 545 | var defAxis = 'y'; 546 | var defMinSep = 50; 547 | 548 | if (isString(options.flow)) { 549 | flow = { 550 | axis: options.flow, 551 | minSeparation: defMinSep 552 | }; 553 | } else if (isNumber(options.flow)) { 554 | flow = { 555 | axis: defAxis, 556 | minSeparation: options.flow 557 | }; 558 | } else if (isObject(options.flow)) { 559 | flow = options.flow; 560 | 561 | flow.axis = flow.axis || defAxis; 562 | flow.minSeparation = flow.minSeparation != null ? flow.minSeparation : defMinSep; 563 | } else { 564 | // e.g. options.flow: true 565 | flow = { 566 | axis: defAxis, 567 | minSeparation: defMinSep 568 | }; 569 | } 570 | 571 | adaptor.flowLayout(flow.axis, flow.minSeparation); 572 | } 573 | 574 | layout.trigger({ type: 'layoutstart', layout: layout }); 575 | 576 | adaptor.avoidOverlaps(options.avoidOverlap).handleDisconnected(options.handleDisconnected).start(options.unconstrIter, options.userConstIter, options.allConstIter, undefined, // gridSnapIterations = 0 577 | undefined, // keepRunning = true 578 | options.centerGraph); 579 | 580 | if (!options.infinite) { 581 | setTimeout(function () { 582 | if (!layout.manuallyStopped) { 583 | adaptor.stop(); 584 | } 585 | }, options.maxSimulationTime); 586 | } 587 | 588 | return this; // chaining 589 | }; 590 | 591 | // called on continuous layouts to stop them before they finish 592 | ColaLayout.prototype.stop = function () { 593 | if (this.adaptor) { 594 | this.manuallyStopped = true; 595 | this.adaptor.stop(); 596 | } 597 | 598 | return this; // chaining 599 | }; 600 | 601 | module.exports = ColaLayout; 602 | 603 | /***/ }), 604 | /* 1 */ 605 | /***/ (function(module, exports, __webpack_require__) { 606 | 607 | "use strict"; 608 | 609 | 610 | // Simple, internal Object.assign() polyfill for options objects etc. 611 | 612 | module.exports = Object.assign != null ? Object.assign.bind(Object) : function (tgt) { 613 | for (var _len = arguments.length, srcs = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 614 | srcs[_key - 1] = arguments[_key]; 615 | } 616 | 617 | srcs.filter(function (src) { 618 | return src != null; 619 | }).forEach(function (src) { 620 | Object.keys(src).forEach(function (k) { 621 | return tgt[k] = src[k]; 622 | }); 623 | }); 624 | 625 | return tgt; 626 | }; 627 | 628 | /***/ }), 629 | /* 2 */ 630 | /***/ (function(module, exports, __webpack_require__) { 631 | 632 | "use strict"; 633 | 634 | 635 | // default layout options 636 | var defaults = { 637 | animate: true, // whether to show the layout as it's running 638 | refresh: 1, // number of ticks per frame; higher is faster but more jerky 639 | maxSimulationTime: 4000, // max length in ms to run the layout 640 | ungrabifyWhileSimulating: false, // so you can't drag nodes during layout 641 | fit: true, // on every layout reposition of nodes, fit the viewport 642 | padding: 30, // padding around the simulation 643 | boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h } 644 | nodeDimensionsIncludeLabels: false, // whether labels should be included in determining the space used by a node 645 | 646 | // layout event callbacks 647 | ready: function ready() {}, // on layoutready 648 | stop: function stop() {}, // on layoutstop 649 | 650 | // positioning options 651 | randomize: false, // use random node positions at beginning of layout 652 | avoidOverlap: true, // if true, prevents overlap of node bounding boxes 653 | handleDisconnected: true, // if true, avoids disconnected components from overlapping 654 | convergenceThreshold: 0.01, // when the alpha value (system energy) falls below this value, the layout stops 655 | nodeSpacing: function nodeSpacing(node) { 656 | return 10; 657 | }, // extra spacing around nodes 658 | flow: undefined, // use DAG/tree flow layout if specified, e.g. { axis: 'y', minSeparation: 30 } 659 | alignment: undefined, // relative alignment constraints on nodes, e.g. function( node ){ return { x: 0, y: 1 } } 660 | gapInequalities: undefined, // list of inequality constraints for the gap between the nodes, e.g. [{"axis":"y", "left":node1, "right":node2, "gap":25}] 661 | centerGraph: true, // adjusts the node positions initially to center the graph (pass false if you want to start the layout from the current position) 662 | 663 | 664 | // different methods of specifying edge length 665 | // each can be a constant numerical value or a function like `function( edge ){ return 2; }` 666 | edgeLength: undefined, // sets edge length directly in simulation 667 | edgeSymDiffLength: undefined, // symmetric diff edge length in simulation 668 | edgeJaccardLength: undefined, // jaccard edge length in simulation 669 | 670 | // iterations of cola algorithm; uses default values on undefined 671 | unconstrIter: undefined, // unconstrained initial layout iterations 672 | userConstIter: undefined, // initial layout iterations with user-specified constraints 673 | allConstIter: undefined, // initial layout iterations with all constraints including non-overlap 674 | 675 | // infinite layout options 676 | infinite: false // overrides all other options for a forces-all-the-time mode 677 | }; 678 | 679 | module.exports = defaults; 680 | 681 | /***/ }), 682 | /* 3 */ 683 | /***/ (function(module, exports, __webpack_require__) { 684 | 685 | "use strict"; 686 | 687 | 688 | var impl = __webpack_require__(0); 689 | 690 | // registers the extension on a cytoscape lib ref 691 | var register = function register(cytoscape) { 692 | if (!cytoscape) { 693 | return; 694 | } // can't register if cytoscape unspecified 695 | 696 | cytoscape('layout', 'cola', impl); // register with cytoscape.js 697 | }; 698 | 699 | if (typeof cytoscape !== 'undefined') { 700 | // expose to global cytoscape (i.e. window.cytoscape) 701 | register(cytoscape); 702 | } 703 | 704 | module.exports = register; 705 | 706 | /***/ }), 707 | /* 4 */ 708 | /***/ (function(module, exports, __webpack_require__) { 709 | 710 | "use strict"; 711 | 712 | 713 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 714 | 715 | var raf = void 0; 716 | 717 | if ((typeof window === "undefined" ? "undefined" : _typeof(window)) !== ( true ? "undefined" : _typeof(undefined))) { 718 | raf = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || function (fn) { 719 | return setTimeout(fn, 16); 720 | }; 721 | } else { 722 | // if not available, all you get is immediate calls 723 | raf = function raf(cb) { 724 | cb(); 725 | }; 726 | } 727 | 728 | module.exports = raf; 729 | 730 | /***/ }), 731 | /* 5 */ 732 | /***/ (function(module, exports) { 733 | 734 | module.exports = __WEBPACK_EXTERNAL_MODULE_5__; 735 | 736 | /***/ }) 737 | /******/ ]); 738 | }); -------------------------------------------------------------------------------- /demo-compound.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | cytoscape-cola.js demo (compound) 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 38 | 39 | 165 | 166 | 167 | 168 |

cytoscape-cola demo (compound)

169 | 170 |
171 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /demo-constraints.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | cytoscape-cola.js demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 38 | 39 | 164 | 165 | 166 | 167 |

cytoscape-cola demo

168 |

n3 is fixed to x: 100, y: 100

169 |

n1 - n2 vertical alignment on left side

170 |

n3 - n4 vertical alignment on center

171 |

n5 - n6 vertical alignment on right side

172 |

n1 - n3 - n5 horizontal alignment

173 |

n1.x + 100 = n3.x

174 |

n3.x + 100 = n5.x

175 | 176 |
177 | 178 | 179 | 180 | 181 | -------------------------------------------------------------------------------- /demo-non-animated.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | cytoscape-cola.js demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 38 | 39 | 711 | 712 | 713 | 714 |

cytoscape-cola demo

715 | 716 |
717 | 718 | 719 | 720 | 721 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | cytoscape-cola.js demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 38 | 39 | 709 | 710 | 711 | 712 |

cytoscape-cola demo

713 | 714 |
715 | 716 | 717 | 718 | 719 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cytoscape-cola", 3 | "version": "2.5.1", 4 | "description": "The Cola.js physics simulation layout for Cytoscape.js", 5 | "main": "cytoscape-cola.js", 6 | "author": { 7 | "name": "Max Franz", 8 | "email": "maxkfranz@gmail.com" 9 | }, 10 | "scripts": { 11 | "postpublish": "run-s gh-pages", 12 | "gh-pages": "gh-pages -d pages", 13 | "copyright": "update license", 14 | "lint": "eslint src", 15 | "build": "cross-env NODE_ENV=production webpack", 16 | "build:min": "cross-env NODE_ENV=production MIN=true webpack", 17 | "build:release": "run-s build copyright", 18 | "watch": "webpack --progress --watch", 19 | "dev": "webpack-dev-server --open", 20 | "test": "mocha" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/cytoscape/cytoscape.js-cola.git" 25 | }, 26 | "keywords": [ 27 | "cytoscape", 28 | "cytoscape-extension" 29 | ], 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/cytoscape/cytoscape.js-cola/issues" 33 | }, 34 | "homepage": "https://github.com/cytoscape/cytoscape.js-cola", 35 | "devDependencies": { 36 | "babel-core": "^6.24.1", 37 | "babel-loader": "^7.0.0", 38 | "babel-preset-env": "^1.5.1", 39 | "camelcase": "^4.1.0", 40 | "chai": "4.0.2", 41 | "cpy-cli": "^1.0.1", 42 | "cross-env": "^5.2.1", 43 | "eslint": "^3.9.1", 44 | "gh-pages": "^1.0.0", 45 | "mocha": "3.4.2", 46 | "npm-run-all": "^4.1.5", 47 | "rimraf": "^2.7.1", 48 | "update": "^0.7.4", 49 | "updater-license": "^1.0.0", 50 | "webpack": "^2.6.1", 51 | "webpack-dev-server": "^2.11.5" 52 | }, 53 | "peerDependencies": { 54 | "cytoscape": "^3.2.0" 55 | }, 56 | "dependencies": { 57 | "webcola": "^3.4.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pages/cytoscape-cola.js: -------------------------------------------------------------------------------- 1 | ../cytoscape-cola.js -------------------------------------------------------------------------------- /pages/demo-compound.html: -------------------------------------------------------------------------------- 1 | ../demo-compound.html -------------------------------------------------------------------------------- /pages/demo-constraints.html: -------------------------------------------------------------------------------- 1 | ../demo-constraints.html -------------------------------------------------------------------------------- /pages/demo-non-animated.html: -------------------------------------------------------------------------------- 1 | ../demo-non-animated.html -------------------------------------------------------------------------------- /pages/demo.html: -------------------------------------------------------------------------------- 1 | ../demo.html -------------------------------------------------------------------------------- /pages/index.html: -------------------------------------------------------------------------------- 1 | ../demo.html -------------------------------------------------------------------------------- /src/assign.js: -------------------------------------------------------------------------------- 1 | // Simple, internal Object.assign() polyfill for options objects etc. 2 | 3 | module.exports = Object.assign != null ? Object.assign.bind( Object ) : function( tgt, ...srcs ){ 4 | srcs.filter(src => src != null).forEach( src => { 5 | Object.keys( src ).forEach( k => tgt[k] = src[k] ); 6 | } ); 7 | 8 | return tgt; 9 | }; 10 | -------------------------------------------------------------------------------- /src/cola.js: -------------------------------------------------------------------------------- 1 | const assign = require('./assign'); 2 | const defaults = require('./defaults'); 3 | const cola = require('webcola') || ( typeof window !== 'undefined' ? window.cola : null ); 4 | const raf = require('./raf'); 5 | const isString = function(o){ return typeof o === typeof ''; }; 6 | const isNumber = function(o){ return typeof o === typeof 0; }; 7 | const isObject = function(o){ return o != null && typeof o === typeof {}; }; 8 | const isFunction = function(o){ return o != null && typeof o === typeof function(){}; }; 9 | const nop = function(){}; 10 | 11 | const getOptVal = function( val, ele ){ 12 | if( isFunction(val) ){ 13 | let fn = val; 14 | return fn.apply( ele, [ ele ] ); 15 | } else { 16 | return val; 17 | } 18 | }; 19 | 20 | // constructor 21 | // options : object containing layout options 22 | function ColaLayout( options ){ 23 | this.options = assign( {}, defaults, options ); 24 | } 25 | 26 | // runs the layout 27 | ColaLayout.prototype.run = function(){ 28 | let layout = this; 29 | let options = this.options; 30 | 31 | layout.manuallyStopped = false; 32 | 33 | let cy = options.cy; // cy is automatically populated for us in the constructor 34 | let eles = options.eles; 35 | let nodes = eles.nodes(); 36 | let edges = eles.edges(); 37 | let ready = false; 38 | 39 | let isParent = ele => ele.isParent(); 40 | 41 | let parentNodes = nodes.filter(isParent); 42 | 43 | let nonparentNodes = nodes.subtract(parentNodes); 44 | 45 | let bb = options.boundingBox || { x1: 0, y1: 0, w: cy.width(), h: cy.height() }; 46 | if( bb.x2 === undefined ){ bb.x2 = bb.x1 + bb.w; } 47 | if( bb.w === undefined ){ bb.w = bb.x2 - bb.x1; } 48 | if( bb.y2 === undefined ){ bb.y2 = bb.y1 + bb.h; } 49 | if( bb.h === undefined ){ bb.h = bb.y2 - bb.y1; } 50 | 51 | let updateNodePositions = function(){ 52 | for( let i = 0; i < nodes.length; i++ ){ 53 | let node = nodes[i]; 54 | let dimensions = node.layoutDimensions( options ); 55 | let scratch = node.scratch('cola'); 56 | 57 | // update node dims 58 | if( !scratch.updatedDims ){ 59 | let padding = getOptVal( options.nodeSpacing, node ); 60 | 61 | scratch.width = dimensions.w + 2*padding; 62 | scratch.height = dimensions.h + 2*padding; 63 | } 64 | } 65 | 66 | nodes.positions(function(node){ 67 | let scratch = node.scratch().cola; 68 | let retPos; 69 | 70 | if( !node.grabbed() && nonparentNodes.contains(node) ){ 71 | retPos = { 72 | x: bb.x1 + scratch.x, 73 | y: bb.y1 + scratch.y 74 | }; 75 | 76 | if( !isNumber(retPos.x) || !isNumber(retPos.y) ){ 77 | retPos = undefined; 78 | } 79 | } 80 | 81 | return retPos; 82 | }); 83 | 84 | nodes.updateCompoundBounds(); // because the way this layout sets positions is buggy for some reason; ref #878 85 | 86 | if( !ready ){ 87 | onReady(); 88 | ready = true; 89 | } 90 | 91 | if( options.fit ){ 92 | cy.fit( options.padding ); 93 | } 94 | }; 95 | 96 | let onDone = function(){ 97 | if( options.ungrabifyWhileSimulating ){ 98 | grabbableNodes.grabify(); 99 | } 100 | 101 | cy.off('destroy', destroyHandler); 102 | 103 | nodes.off('grab free position', grabHandler); 104 | nodes.off('lock unlock', lockHandler); 105 | 106 | // trigger layoutstop when the layout stops (e.g. finishes) 107 | layout.one('layoutstop', options.stop); 108 | layout.trigger({ type: 'layoutstop', layout: layout }); 109 | }; 110 | 111 | let onReady = function(){ 112 | // trigger layoutready when each node has had its position set at least once 113 | layout.one('layoutready', options.ready); 114 | layout.trigger({ type: 'layoutready', layout: layout }); 115 | }; 116 | 117 | let ticksPerFrame = options.refresh; 118 | 119 | if( options.refresh < 0 ){ 120 | ticksPerFrame = 1; 121 | } else { 122 | ticksPerFrame = Math.max( 1, ticksPerFrame ); // at least 1 123 | } 124 | 125 | let adaptor = layout.adaptor = cola.adaptor({ 126 | trigger: function( e ){ // on sim event 127 | let TICK = cola.EventType ? cola.EventType.tick : null; 128 | let END = cola.EventType ? cola.EventType.end : null; 129 | 130 | switch( e.type ){ 131 | case 'tick': 132 | case TICK: 133 | if( options.animate ){ 134 | updateNodePositions(); 135 | } 136 | break; 137 | 138 | case 'end': 139 | case END: 140 | updateNodePositions(); 141 | if( !options.infinite ){ onDone(); } 142 | break; 143 | } 144 | }, 145 | 146 | kick: function(){ // kick off the simulation 147 | //let skip = 0; 148 | 149 | let firstTick = true; 150 | 151 | let inftick = function(){ 152 | if( layout.manuallyStopped ){ 153 | onDone(); 154 | 155 | return true; 156 | } 157 | 158 | let ret = adaptor.tick(); 159 | 160 | if( !options.infinite && !firstTick ){ 161 | adaptor.convergenceThreshold(options.convergenceThreshold); 162 | } 163 | 164 | firstTick = false; 165 | 166 | if( ret && options.infinite ){ // resume layout if done 167 | adaptor.resume(); // resume => new kick 168 | } 169 | 170 | return ret; // allow regular finish b/c of new kick 171 | }; 172 | 173 | let multitick = function(){ // multiple ticks in a row 174 | let ret; 175 | 176 | for( let i = 0; i < ticksPerFrame && !ret; i++ ){ 177 | ret = ret || inftick(); // pick up true ret vals => sim done 178 | } 179 | 180 | return ret; 181 | }; 182 | 183 | if( options.animate ){ 184 | let frame = function(){ 185 | if( multitick() ){ return; } 186 | 187 | raf( frame ); 188 | }; 189 | 190 | raf( frame ); 191 | } else { 192 | while( !inftick() ){ 193 | // keep going... 194 | } 195 | } 196 | }, 197 | 198 | on: nop, // dummy; not needed 199 | 200 | drag: nop // not needed for our case 201 | }); 202 | layout.adaptor = adaptor; 203 | 204 | // if set no grabbing during layout 205 | let grabbableNodes = nodes.filter(':grabbable'); 206 | if( options.ungrabifyWhileSimulating ){ 207 | grabbableNodes.ungrabify(); 208 | } 209 | 210 | let destroyHandler; 211 | cy.one('destroy', destroyHandler = function(){ 212 | layout.stop(); 213 | }); 214 | 215 | // handle node dragging 216 | let grabHandler; 217 | nodes.on('grab free position', grabHandler = function(e){ 218 | let node = this; 219 | let scrCola = node.scratch().cola; 220 | let pos = node.position(); 221 | let nodeIsTarget = e.cyTarget === node || e.target === node; 222 | 223 | if( !nodeIsTarget ){ return; } 224 | 225 | switch( e.type ){ 226 | case 'grab': 227 | adaptor.dragstart( scrCola ); 228 | break; 229 | case 'free': 230 | adaptor.dragend( scrCola ); 231 | break; 232 | case 'position': 233 | // only update when different (i.e. manual .position() call or drag) so we don't loop needlessly 234 | if( scrCola.px !== pos.x - bb.x1 || scrCola.py !== pos.y - bb.y1 ){ 235 | scrCola.px = pos.x - bb.x1; 236 | scrCola.py = pos.y - bb.y1; 237 | } 238 | break; 239 | } 240 | 241 | }); 242 | 243 | let lockHandler; 244 | nodes.on('lock unlock', lockHandler = function(){ 245 | let node = this; 246 | let scrCola = node.scratch().cola; 247 | 248 | scrCola.fixed = node.locked(); 249 | 250 | if( node.locked() ){ 251 | adaptor.dragstart( scrCola ); 252 | } else { 253 | adaptor.dragend( scrCola ); 254 | } 255 | }); 256 | 257 | // add nodes to cola 258 | adaptor.nodes( nonparentNodes.map(function( node, i ){ 259 | let padding = getOptVal( options.nodeSpacing, node ); 260 | let pos = node.position(); 261 | let dimensions = node.layoutDimensions( options ); 262 | 263 | let struct = node.scratch().cola = { 264 | x: (options.randomize && !node.locked()) || pos.x === undefined ? Math.round( Math.random() * bb.w ) : pos.x, 265 | y: (options.randomize && !node.locked()) || pos.y === undefined ? Math.round( Math.random() * bb.h ) : pos.y, 266 | width: dimensions.w + 2*padding, 267 | height: dimensions.h + 2*padding, 268 | index: i, 269 | fixed: node.locked() 270 | }; 271 | 272 | return struct; 273 | }) ); 274 | 275 | // the constraints to be added on nodes 276 | let constraints = []; 277 | 278 | if( options.alignment ){ // then set alignment constraints 279 | 280 | if(options.alignment.vertical) { 281 | let verticalAlignments = options.alignment.vertical; 282 | verticalAlignments.forEach(function(alignment){ 283 | let offsetsX = []; 284 | alignment.forEach(function(nodeData){ 285 | let node = nodeData.node; 286 | let scrCola = node.scratch().cola; 287 | let index = scrCola.index; 288 | offsetsX.push({ 289 | node: index, 290 | offset: nodeData.offset ? nodeData.offset : 0 291 | }); 292 | }); 293 | constraints.push({ 294 | type: 'alignment', 295 | axis: 'x', 296 | offsets: offsetsX 297 | }); 298 | }); 299 | } 300 | 301 | if(options.alignment.horizontal) { 302 | let horizontalAlignments = options.alignment.horizontal; 303 | horizontalAlignments.forEach(function(alignment){ 304 | let offsetsY = []; 305 | alignment.forEach(function(nodeData){ 306 | let node = nodeData.node; 307 | let scrCola = node.scratch().cola; 308 | let index = scrCola.index; 309 | offsetsY.push({ 310 | node: index, 311 | offset: nodeData.offset ? nodeData.offset : 0 312 | }); 313 | }); 314 | constraints.push({ 315 | type: 'alignment', 316 | axis: 'y', 317 | offsets: offsetsY 318 | }); 319 | }); 320 | } 321 | 322 | } 323 | 324 | // if gapInequalities variable is set add each inequality constraint to list of constraints 325 | if ( options.gapInequalities ) { 326 | options.gapInequalities.forEach( inequality => { 327 | 328 | // for the constraints to be passed to cola layout adaptor use indices of nodes, 329 | // not the nodes themselves 330 | let leftIndex = inequality.left.scratch().cola.index; 331 | let rightIndex = inequality.right.scratch().cola.index; 332 | 333 | constraints.push({ 334 | axis: inequality.axis, 335 | left: leftIndex, 336 | right: rightIndex, 337 | gap: inequality.gap, 338 | equality: inequality.equality 339 | }); 340 | 341 | } ); 342 | } 343 | 344 | // add constraints if any 345 | if ( constraints.length > 0 ) { 346 | adaptor.constraints( constraints ); 347 | } 348 | 349 | // add compound nodes to cola 350 | adaptor.groups( parentNodes.map(function( node, i ){ // add basic group incl leaf nodes 351 | let optPadding = getOptVal( options.nodeSpacing, node ); 352 | let getPadding = function(d){ 353 | return parseFloat( node.style('padding-'+d) ); 354 | }; 355 | 356 | let pleft = getPadding('left') + optPadding; 357 | let pright = getPadding('right') + optPadding; 358 | let ptop = getPadding('top') + optPadding; 359 | let pbottom = getPadding('bottom') + optPadding; 360 | 361 | node.scratch().cola = { 362 | index: i, 363 | 364 | padding: Math.max( pleft, pright, ptop, pbottom ), 365 | 366 | // leaves should only contain direct descendants (children), 367 | // not the leaves of nested compound nodes or any nodes that are compounds themselves 368 | leaves: node.children() 369 | .intersection(nonparentNodes) 370 | .map(function( child ){ 371 | return child[0].scratch().cola.index; 372 | }), 373 | 374 | fixed: node.locked() 375 | }; 376 | 377 | return node; 378 | }).map(function( node ){ // add subgroups 379 | node.scratch().cola.groups = node.children() 380 | .intersection(parentNodes) 381 | .map(function( child ){ 382 | return child.scratch().cola.index; 383 | }); 384 | 385 | return node.scratch().cola; 386 | }) ); 387 | 388 | // get the edge length setting mechanism 389 | let length; 390 | let lengthFnName; 391 | if( options.edgeLength != null ){ 392 | length = options.edgeLength; 393 | lengthFnName = 'linkDistance'; 394 | } else if( options.edgeSymDiffLength != null ){ 395 | length = options.edgeSymDiffLength; 396 | lengthFnName = 'symmetricDiffLinkLengths'; 397 | } else if( options.edgeJaccardLength != null ){ 398 | length = options.edgeJaccardLength; 399 | lengthFnName = 'jaccardLinkLengths'; 400 | } else { 401 | length = 100; 402 | lengthFnName = 'linkDistance'; 403 | } 404 | 405 | let lengthGetter = function( link ){ 406 | return link.calcLength; 407 | }; 408 | 409 | // add the edges to cola 410 | adaptor.links( edges.stdFilter(function( edge ){ 411 | return nonparentNodes.contains(edge.source()) && nonparentNodes.contains(edge.target()); 412 | }).map(function( edge ){ 413 | let c = edge.scratch().cola = { 414 | source: edge.source()[0].scratch().cola.index, 415 | target: edge.target()[0].scratch().cola.index 416 | }; 417 | 418 | if( length != null ){ 419 | c.calcLength = getOptVal( length, edge ); 420 | } 421 | 422 | return c; 423 | }) ); 424 | 425 | adaptor.size([ bb.w, bb.h ]); 426 | 427 | if( length != null ){ 428 | adaptor[ lengthFnName ]( lengthGetter ); 429 | } 430 | 431 | // set the flow of cola 432 | if( options.flow ){ 433 | let flow; 434 | let defAxis = 'y'; 435 | let defMinSep = 50; 436 | 437 | if( isString(options.flow) ){ 438 | flow = { 439 | axis: options.flow, 440 | minSeparation: defMinSep 441 | }; 442 | } else if( isNumber(options.flow) ){ 443 | flow = { 444 | axis: defAxis, 445 | minSeparation: options.flow 446 | }; 447 | } else if( isObject(options.flow) ){ 448 | flow = options.flow; 449 | 450 | flow.axis = flow.axis || defAxis; 451 | flow.minSeparation = flow.minSeparation != null ? flow.minSeparation : defMinSep; 452 | } else { // e.g. options.flow: true 453 | flow = { 454 | axis: defAxis, 455 | minSeparation: defMinSep 456 | }; 457 | } 458 | 459 | adaptor.flowLayout( flow.axis , flow.minSeparation ); 460 | } 461 | 462 | layout.trigger({ type: 'layoutstart', layout: layout }); 463 | 464 | adaptor 465 | .avoidOverlaps( options.avoidOverlap ) 466 | .handleDisconnected( options.handleDisconnected ) 467 | .start( 468 | options.unconstrIter, 469 | options.userConstIter, 470 | options.allConstIter, 471 | undefined, // gridSnapIterations = 0 472 | undefined, // keepRunning = true 473 | options.centerGraph 474 | ) 475 | ; 476 | 477 | if( !options.infinite ){ 478 | setTimeout(function(){ 479 | if( !layout.manuallyStopped ){ 480 | adaptor.stop(); 481 | } 482 | }, options.maxSimulationTime); 483 | } 484 | 485 | return this; // chaining 486 | }; 487 | 488 | // called on continuous layouts to stop them before they finish 489 | ColaLayout.prototype.stop = function(){ 490 | if( this.adaptor ){ 491 | this.manuallyStopped = true; 492 | this.adaptor.stop(); 493 | } 494 | 495 | return this; // chaining 496 | }; 497 | 498 | module.exports = ColaLayout; 499 | -------------------------------------------------------------------------------- /src/defaults.js: -------------------------------------------------------------------------------- 1 | // default layout options 2 | let defaults = { 3 | animate: true, // whether to show the layout as it's running 4 | refresh: 1, // number of ticks per frame; higher is faster but more jerky 5 | maxSimulationTime: 4000, // max length in ms to run the layout 6 | ungrabifyWhileSimulating: false, // so you can't drag nodes during layout 7 | fit: true, // on every layout reposition of nodes, fit the viewport 8 | padding: 30, // padding around the simulation 9 | boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h } 10 | nodeDimensionsIncludeLabels: false, // whether labels should be included in determining the space used by a node 11 | 12 | // layout event callbacks 13 | ready: function(){}, // on layoutready 14 | stop: function(){}, // on layoutstop 15 | 16 | // positioning options 17 | randomize: false, // use random node positions at beginning of layout 18 | avoidOverlap: true, // if true, prevents overlap of node bounding boxes 19 | handleDisconnected: true, // if true, avoids disconnected components from overlapping 20 | convergenceThreshold: 0.01, // when the alpha value (system energy) falls below this value, the layout stops 21 | nodeSpacing: function( node ){ return 10; }, // extra spacing around nodes 22 | flow: undefined, // use DAG/tree flow layout if specified, e.g. { axis: 'y', minSeparation: 30 } 23 | alignment: undefined, // relative alignment constraints on nodes, e.g. function( node ){ return { x: 0, y: 1 } } 24 | gapInequalities: undefined, // list of inequality constraints for the gap between the nodes, e.g. [{"axis":"y", "left":node1, "right":node2, "gap":25}] 25 | centerGraph: true, // adjusts the node positions initially to center the graph (pass false if you want to start the layout from the current position) 26 | 27 | 28 | // different methods of specifying edge length 29 | // each can be a constant numerical value or a function like `function( edge ){ return 2; }` 30 | edgeLength: undefined, // sets edge length directly in simulation 31 | edgeSymDiffLength: undefined, // symmetric diff edge length in simulation 32 | edgeJaccardLength: undefined, // jaccard edge length in simulation 33 | 34 | // iterations of cola algorithm; uses default values on undefined 35 | unconstrIter: undefined, // unconstrained initial layout iterations 36 | userConstIter: undefined, // initial layout iterations with user-specified constraints 37 | allConstIter: undefined, // initial layout iterations with all constraints including non-overlap 38 | 39 | // infinite layout options 40 | infinite: false // overrides all other options for a forces-all-the-time mode 41 | }; 42 | 43 | module.exports = defaults; 44 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const impl = require('./cola'); 2 | 3 | // registers the extension on a cytoscape lib ref 4 | let register = function( cytoscape ){ 5 | if( !cytoscape ){ return; } // can't register if cytoscape unspecified 6 | 7 | cytoscape( 'layout', 'cola', impl ); // register with cytoscape.js 8 | }; 9 | 10 | if( typeof cytoscape !== 'undefined' ){ // expose to global cytoscape (i.e. window.cytoscape) 11 | register( cytoscape ); 12 | } 13 | 14 | module.exports = register; 15 | -------------------------------------------------------------------------------- /src/raf.js: -------------------------------------------------------------------------------- 1 | let raf; 2 | 3 | if( typeof window !== typeof undefined ){ 4 | raf = ( window.requestAnimationFrame || 5 | window.webkitRequestAnimationFrame || 6 | window.mozRequestAnimationFrame || 7 | window.msRequestAnimationFrame || 8 | (fn => setTimeout(fn, 16)) 9 | ); 10 | } else { // if not available, all you get is immediate calls 11 | raf = function( cb ){ 12 | cb(); 13 | }; 14 | } 15 | 16 | module.exports = raf; 17 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pkg = require('./package.json'); 3 | const camelcase = require('camelcase'); 4 | const process = require('process'); 5 | const webpack = require('webpack'); 6 | const env = process.env; 7 | const NODE_ENV = env.NODE_ENV; 8 | const MIN = env.MIN; 9 | const PROD = NODE_ENV === 'production'; 10 | 11 | let config = { 12 | devtool: PROD ? false : 'inline-source-map', 13 | entry: './src/index.js', 14 | output: { 15 | path: path.join( __dirname ), 16 | filename: pkg.name + '.js', 17 | library: camelcase( pkg.name ), 18 | libraryTarget: 'umd' 19 | }, 20 | module: { 21 | rules: [ 22 | { test: /\.js$/, exclude: /node_modules/, use: 'babel-loader' } 23 | ] 24 | }, 25 | externals: PROD ? Object.keys( pkg.dependencies || {} ) : [], 26 | plugins: MIN ? [ 27 | new webpack.optimize.UglifyJsPlugin({ 28 | compress: { 29 | warnings: false, 30 | drop_console: false, 31 | } 32 | }) 33 | ] : [] 34 | }; 35 | 36 | module.exports = config; 37 | --------------------------------------------------------------------------------