├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── CITATION.cff ├── LICENSE ├── README.md ├── bower.json ├── cytoscape-fcose.js ├── demo ├── demo-compound.html ├── demo-constraint-control.js ├── demo-constraint.html ├── demo.gif ├── demo.html ├── incrementalConstraints.gif └── samples │ ├── callGraph.js │ ├── callGraph_constraints.js │ ├── chalk.js │ ├── chalk_constraints.js │ ├── unix.js │ ├── unix_constraints.js │ ├── uwsn.js │ ├── uwsn_constraints.js │ ├── wsn.js │ └── wsn_constraints.js ├── package-lock.json ├── package.json ├── src ├── assign.js ├── fcose │ ├── auxiliary.js │ ├── cose.js │ ├── index.js │ └── spectral.js └── index.js ├── test └── example.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | /nbproject/* 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: "If you use this software, please cite it as below." 3 | authors: 4 | - family-names: "Balci" 5 | given-names: "Hasan" 6 | orcid: "https://orcid.org/0000-0001-8319-7758" 7 | - family-names: "Dogrusoz" 8 | given-names: "Ugur" 9 | orcid: "https://orcid.org/0000-0002-7153-0784" 10 | title: "cytoscape.js-fcose" 11 | version: 2.1.0 12 | date-released: 2021-06-25 13 | url: "https://github.com/iVis-at-Bilkent/cytoscape.js-fcose" 14 | preferred-citation: 15 | type: article 16 | authors: 17 | - family-names: "Balci" 18 | given-names: "Hasan" 19 | orcid: "https://orcid.org/0000-0001-8319-7758" 20 | - family-names: "Dogrusoz" 21 | given-names: "Ugur" 22 | orcid: "https://orcid.org/0000-0002-7153-0784" 23 | doi: "10.1109/TVCG.2021.3095303" 24 | journal: "IEEE Transactions on Visualization and Computer Graphics" 25 | title: "fCoSE: A Fast Compound Graph Layout Algorithm with Constraint Support" 26 | issue: 12 27 | volume: 28 28 | start: 4582 # First page number 29 | end: 4593 # Last page number 30 | month: 12 31 | year: 2022 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 - present, iVis-at-Bilkent. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the “Software”), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cytoscape-fcose 2 | ================================================================================ 3 | 4 | 5 | ## Description 6 | 7 | 8 | fCoSE (pron. "f-cosay", **f**ast **Co**mpound **S**pring **E**mbedder), is a faster version of our earlier compound spring embedder algorithm named [CoSE](https://github.com/cytoscape/cytoscape.js-cose-bilkent), implemented as a Cytoscape.js extension by [i-Vis Lab](http://cs.bilkent.edu.tr/~ivis/) in Bilkent University. 9 | 10 | Here are some demos: **simple**, **compound**, and **constraints**, respectively: 11 |

12 |   13 |   14 | 15 |

16 | 17 | fCoSE layout algorithm combines the speed of spectral layout with the aesthetics of force-directed layout. fCoSE runs up to 2 times as fast as CoSE while achieving similar aesthetics. 18 | 19 |

20 | 21 | Furthermore, fCoSE also supports a fairly rich set of constraint types (i.e., fixed position, vertical/horizontal alignment and relative placement). 22 | 23 |

24 | 25 | You can see constraint support in action in the following videos: [fixed node](https://youtu.be/vRZVlwntzGY), [alignment](https://youtu.be/O5rddJ7DteU), [relative placement](https://youtu.be/Xcm87bT50RA), [hybrid](https://youtu.be/KRAQHmnTvUA), [real life graphs](https://youtu.be/vTPy9G2ALcI). Constraints can also be added [incrementally](https://youtu.be/DTm2WmzwP4k) on a given layout. 26 | 27 | Please cite the following when you use this layout: 28 | 29 | H. Balci and U. Dogrusoz, "[fCoSE: A Fast Compound Graph Layout Algorithm with Constraint Support](https://doi.org/10.1109/TVCG.2021.3095303)," in IEEE Transactions on Visualization and Computer Graphics, 28(12), pp. 4582-4593, 2022. 30 | 31 | U. Dogrusoz, E. Giral, A. Cetintas, A. Civril and E. Demir, "[A Layout Algorithm For Undirected Compound Graphs](http://www.sciencedirect.com/science/article/pii/S0020025508004799)", Information Sciences, 179, pp. 980-994, 2009. 32 | 33 | ## Dependencies 34 | 35 | * Cytoscape.js ^3.2.0 36 | * cose-base ^2.0.0 37 | * cytoscape-layout-utilities.js (optional for packing disconnected components) ^1.0.0 38 | 39 | ## Documentation 40 | 41 | fCoSE supports user-defined placement constraints as well as its full support for compound graphs. These constraints may be defined for simple nodes. Supported constraint types are: 42 | 43 | * **Fixed node constraint:** The user may provide *exact* desired positions for a set of nodes called *fixed nodes*. For example, in order to position node *n1* to *(x: 100, y: 200)* and node *n2* to *(x: 200, y: -300)* as a result of the layout, ```fixedNodeConstraint``` option should be set as follows: 44 | 45 | ```js 46 | fixedNodeConstraint: [{nodeId: 'n1', position: {x: 100, y: 200}}, 47 | {nodeId: 'n2', position: {x: 200, y: -300}}], 48 | ``` 49 | 50 | * **Alignment constraint:** This constraint aims to align two or more nodes (with respect to their centers) vertically or horizontally. For example, for the vertical alignment of nodes {*n1, n2, n3*} and {*n4, n5*}, and horizontal alignment of nodes {*n2, n4*} as a result of the layout, ```alignmentConstraint``` option should be set as follows: 51 | ```js 52 | alignmentConstraint: {vertical: [['n1', 'n2', 'n3'], ['n4', 'n5']], horizontal: [['n2', 'n4']]}, 53 | ``` 54 |       ***Note:** Alignment constraints in a direction must be given in most compact form. Example: ```['n1', 'n2', 'n3']``` instead of ```['n1', 'n2'], ['n1', 'n3']```.* 55 | 56 | * **Relative placement constraint:** The user may constrain the position of a node relative to another node in either vertical or horizontal direction. For example, in order to position node *n1* to be above of node *n2* by at least 100 pixels and position node *n3* to be on the left of node *n4* by at least 75 pixels as a result of the layout, ```relativePlacementConstraint``` option should be set as follows: 57 | 58 | ```js 59 | relativePlacementConstraint: [{top: 'n1', bottom: 'n2', gap: 100}, 60 | {left: 'n3', right: 'n4', gap: 75}], 61 | ``` 62 | The `gap` property is optional. If it is omitted, average `idealEdgeLength` is used as the gap value. 63 | 64 | ## Usage instructions 65 | 66 | Download the library: 67 | * via npm: `npm install cytoscape-fcose`, 68 | * via bower: `bower install cytoscape-fcose`, or 69 | * via direct download in the repository (probably from a tag). 70 | 71 | Import the library as appropriate for your project: 72 | 73 | ES import: 74 | 75 | ```js 76 | import cytoscape from 'cytoscape'; 77 | import fcose from 'cytoscape-fcose'; 78 | 79 | cytoscape.use( fcose ); 80 | ``` 81 | 82 | CommonJS require: 83 | 84 | ```js 85 | let cytoscape = require('cytoscape'); 86 | let fcose = require('cytoscape-fcose'); 87 | 88 | cytoscape.use( fcose ); // register extension 89 | ``` 90 | 91 | AMD: 92 | 93 | ```js 94 | require(['cytoscape', 'cytoscape-fcose'], function( cytoscape, fcose ){ 95 | fcose( cytoscape ); // register extension 96 | }); 97 | ``` 98 | 99 | Plain HTML/JS has the extension registered for you automatically, because no `require()` is needed. Just add the following files: 100 | 101 | ``` 102 | 103 | 104 | 105 | ``` 106 | 107 | 108 | ## API 109 | 110 | When calling the layout, e.g. `cy.layout({ name: 'fcose', ... })`, the following options are supported: 111 | 112 | ```js 113 | var defaultOptions = { 114 | 115 | // 'draft', 'default' or 'proof' 116 | // - "draft" only applies spectral layout 117 | // - "default" improves the quality with incremental layout (fast cooling rate) 118 | // - "proof" improves the quality with incremental layout (slow cooling rate) 119 | quality: "default", 120 | // Use random node positions at beginning of layout 121 | // if this is set to false, then quality option must be "proof" 122 | randomize: true, 123 | // Whether or not to animate the layout 124 | animate: true, 125 | // Duration of animation in ms, if enabled 126 | animationDuration: 1000, 127 | // Easing of animation, if enabled 128 | animationEasing: undefined, 129 | // Fit the viewport to the repositioned nodes 130 | fit: true, 131 | // Padding around layout 132 | padding: 30, 133 | // Whether to include labels in node dimensions. Valid in "proof" quality 134 | nodeDimensionsIncludeLabels: false, 135 | // Whether or not simple nodes (non-compound nodes) are of uniform dimensions 136 | uniformNodeDimensions: false, 137 | // Whether to pack disconnected components - cytoscape-layout-utilities extension should be registered and initialized 138 | packComponents: true, 139 | // Layout step - all, transformed, enforced, cose - for debug purpose only 140 | step: "all", 141 | 142 | /* spectral layout options */ 143 | 144 | // False for random, true for greedy sampling 145 | samplingType: true, 146 | // Sample size to construct distance matrix 147 | sampleSize: 25, 148 | // Separation amount between nodes 149 | nodeSeparation: 75, 150 | // Power iteration tolerance 151 | piTol: 0.0000001, 152 | 153 | /* incremental layout options */ 154 | 155 | // Node repulsion (non overlapping) multiplier 156 | nodeRepulsion: node => 4500, 157 | // Ideal edge (non nested) length 158 | idealEdgeLength: edge => 50, 159 | // Divisor to compute edge forces 160 | edgeElasticity: edge => 0.45, 161 | // Nesting factor (multiplier) to compute ideal edge length for nested edges 162 | nestingFactor: 0.1, 163 | // Maximum number of iterations to perform - this is a suggested value and might be adjusted by the algorithm as required 164 | numIter: 2500, 165 | // For enabling tiling 166 | tile: true, 167 | // The comparison function to be used while sorting nodes during tiling operation. 168 | // Takes the ids of 2 nodes that will be compared as a parameter and the default tiling operation is performed when this option is not set. 169 | // It works similar to ``compareFunction`` parameter of ``Array.prototype.sort()`` 170 | // If node1 is less then node2 by some ordering criterion ``tilingCompareBy(nodeId1, nodeId2)`` must return a negative value 171 | // If node1 is greater then node2 by some ordering criterion ``tilingCompareBy(nodeId1, nodeId2)`` must return a positive value 172 | // If node1 is equal to node2 by some ordering criterion ``tilingCompareBy(nodeId1, nodeId2)`` must return 0 173 | tilingCompareBy: undefined, 174 | // Represents the amount of the vertical space to put between the zero degree members during the tiling operation(can also be a function) 175 | tilingPaddingVertical: 10, 176 | // Represents the amount of the horizontal space to put between the zero degree members during the tiling operation(can also be a function) 177 | tilingPaddingHorizontal: 10, 178 | // Gravity force (constant) 179 | gravity: 0.25, 180 | // Gravity range (constant) for compounds 181 | gravityRangeCompound: 1.5, 182 | // Gravity force (constant) for compounds 183 | gravityCompound: 1.0, 184 | // Gravity range (constant) 185 | gravityRange: 3.8, 186 | // Initial cooling factor for incremental layout 187 | initialEnergyOnIncremental: 0.3, 188 | 189 | /* constraint options */ 190 | 191 | // Fix desired nodes to predefined positions 192 | // [{nodeId: 'n1', position: {x: 100, y: 200}}, {...}] 193 | fixedNodeConstraint: undefined, 194 | // Align desired nodes in vertical/horizontal direction 195 | // {vertical: [['n1', 'n2'], [...]], horizontal: [['n2', 'n4'], [...]]} 196 | alignmentConstraint: undefined, 197 | // Place two nodes relatively in vertical/horizontal direction 198 | // [{top: 'n1', bottom: 'n2', gap: 100}, {left: 'n3', right: 'n4', gap: 75}, {...}] 199 | relativePlacementConstraint: undefined, 200 | 201 | /* layout event callbacks */ 202 | ready: () => {}, // on layoutready 203 | stop: () => {} // on layoutstop 204 | }; 205 | ``` 206 | To be able to use `packComponents` option, `cytoscape-layout-utilities` extension should also be registered in the application. 207 | Packing related [options](https://github.com/iVis-at-Bilkent/cytoscape.js-layout-utilities#default-options) should be set via `cytoscape-layout-utilities` extension. 208 | If they are not set, fCoSE uses default options. 209 | 210 | 211 | ## Build targets 212 | 213 | * `npm run test` : Run Mocha tests in `./test` 214 | * `npm run build` : Build `./src/**` into `cytoscape-fcose.js` 215 | * `npm run watch` : Automatically build on changes with live reloading (N.b. you must already have an HTTP server running) 216 | * `npm run dev` : Automatically build on changes with live reloading with webpack dev server 217 | * `npm run lint` : Run eslint on the source 218 | 219 | N.b. all builds use babel, so modern ES features can be used in the `src`. 220 | 221 | 222 | ## Publishing instructions 223 | 224 | This project is set up to automatically be published to npm and bower. To publish: 225 | 226 | 1. Build the extension : `npm run build:release` 227 | 1. Commit the build : `git commit -am "Build for release"` 228 | 1. Bump the version number and tag: `npm version major|minor|patch` 229 | 1. Push to origin: `git push && git push --tags` 230 | 1. Publish to npm: `npm publish .` 231 | 1. If publishing to bower for the first time, you'll need to run `bower register cytoscape-fcose https://github.com/iVis-at-Bilkent/cytoscape.js-fcose.git` 232 | 1. [Make a new release](https://github.com/iVis-at-Bilkent/cytoscape.js-fcose/releases/new) for Zenodo. 233 | 234 | ## Team 235 | 236 | * [Hasan Balcı](https://github.com/hasanbalci) and [Ugur Dogrusoz](https://github.com/ugurdogrusoz) of [i-Vis at Bilkent University](http://www.cs.bilkent.edu.tr/~ivis) 237 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cytoscape-fcose", 3 | "description": "The fCoSE layout for Cytoscape.js by Bilkent with fast compound node placement", 4 | "main": "cytoscape-fcose.js", 5 | "dependencies": { 6 | "cytoscape": "^3.2.0", 7 | "cose-base": "^1.0.0" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/iVis-at-Bilkent/cytoscape.js-fcose.git" 12 | }, 13 | "ignore": [ 14 | "**/.*", 15 | "node_modules", 16 | "bower_components", 17 | "test", 18 | "tests" 19 | ], 20 | "keywords": [ 21 | "cytoscape", 22 | "cytoscape-extension" 23 | ], 24 | "license": "MIT" 25 | } 26 | -------------------------------------------------------------------------------- /demo/demo-compound.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | cytoscape-fcose.js demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 140 | 141 | 335 | 336 | 337 | 338 |

cytoscape-fcose demo (compound)

339 |
340 |     341 | 342 | 343 |
344 | 345 | 346 | 347 | 354 | 355 | 356 | 357 | 363 | 364 | 365 | 366 | 372 | 373 | 374 | 375 | 381 | 382 | 383 | 384 | 390 | 391 | 392 | 393 | 399 | 400 | 401 | 402 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 |
Quality 348 | 353 |
Incremental 358 |
359 | 360 | 361 |
362 |
Animate 367 |
368 | 369 | 370 |
371 |
Fit 376 |
377 | 378 | 379 |
380 |
Uniform Node Dimensions 385 |
386 | 387 | 388 |
389 |
Pack Components to Window 394 |
395 | 396 | 397 |
398 |
Tile Disconnected 403 |
404 | 405 | 406 |
407 |
Node Repulsion
Ideal Edge Length
Edge Elasticity
Nesting Factor
Gravity
Gravity Range
Compound Gravity
Compound Gravity Range
Number of Iterations
Tiling Vertical Padding
Tiling Horizontal Padding
Incremental Cooling Factor
458 | 459 |
460 |
461 |
462 | 463 | 464 | 465 | 466 | -------------------------------------------------------------------------------- /demo/demo-constraint.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | cytoscape-fcose.js demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 187 | 188 | 189 |

cytoscape-fcose demo (constraint)

190 |
191 | 192 | 193 |   194 | 206 | 207 | 208 |
209 | 210 | 211 |   212 |   213 | 214 |
215 | 216 | 217 |

218 |   219 |   220 |   221 |
222 | 223 | 224 |
225 |
226 | 227 | 228 | 229 | 230 | 232 | 233 | 234 | 235 |
Constraints 231 |
236 | 237 |
238 | 239 | 240 | 241 |
242 |
243 |
244 | Fixed Node Constraint 245 |
246 |
247 |
248 |
249 |
250 |
251 | 252 | 253 | 254 | 255 |
256 |
257 | 258 |
259 |
260 |
261 | 262 |
263 | 264 | 265 |
266 |
267 |
268 | Alignment Constraint 269 |
270 |
271 |
272 |
273 | Selected Nodes Vertically 274 |
275 |
276 | 277 |
278 |
279 |
280 |
281 | Selected Nodes Horizontally 282 |
283 |
284 | 285 |
286 |
287 |
288 |
289 | Click on a node for selecting. Shift + click for extending selection. 290 |
291 |
292 |
293 | 294 |
295 | 296 | 297 |
298 |
299 |
300 | Relative Placement Constraint 301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 | 312 |
313 |
314 | 315 | 316 |
317 |
318 | 319 |
320 |
321 |
322 |
323 |
324 |
325 | 326 |
327 | 328 | 329 |
330 | Hover a constraint row to see involved nodes. 331 |
332 | 333 | 334 | 335 | 336 | 337 | 338 |
TypeNodesInfo
339 |
340 | 341 |
342 |
343 | 344 | 345 | 346 | -------------------------------------------------------------------------------- /demo/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iVis-at-Bilkent/cytoscape.js-fcose/78afcf96512a409abc903699277ad616c02dfad9/demo/demo.gif -------------------------------------------------------------------------------- /demo/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | cytoscape-fcose.js demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 140 | 141 | 327 | 328 | 329 | 330 |

cytoscape-fcose demo

331 |
332 |   333 |   334 | 335 |
336 | 337 | 338 | 339 | 346 | 347 | 348 | 349 | 355 | 356 | 357 | 358 | 364 | 365 | 366 | 367 | 373 | 374 | 375 | 376 | 382 | 383 | 384 | 385 | 391 | 392 | 393 | 394 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 |
Quality 340 | 345 |
Incremental 350 |
351 | 352 | 353 |
354 |
Animate 359 |
360 | 361 | 362 |
363 |
Fit 368 |
369 | 370 | 371 |
372 |
Uniform Node Dimensions 377 |
378 | 379 | 380 |
381 |
Pack Components to Window 386 |
387 | 388 | 389 |
390 |
Tile Disconnected 395 |
396 | 397 | 398 |
399 |
Node Repulsion
Ideal Edge Length
Edge Elasticity
Nesting Factor
Gravity
Gravity Range
Compound Gravity
Compound Gravity Range
Number of Iterations
Tiling Vertical Padding
Tiling Horizontal Padding
Incremental Cooling Factor
450 | 451 |
452 |
453 |
454 | 455 | 456 | 457 | 458 | -------------------------------------------------------------------------------- /demo/incrementalConstraints.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iVis-at-Bilkent/cytoscape.js-fcose/78afcf96512a409abc903699277ad616c02dfad9/demo/incrementalConstraints.gif -------------------------------------------------------------------------------- /demo/samples/callGraph_constraints.js: -------------------------------------------------------------------------------- 1 | callGraph_constraints = { 2 | "alignmentConstraint": { 3 | "horizontal": [ 4 | [ 5 | "288a2f9b-2930-701e-c68e-400307fbffd4", 6 | "nwtN_a44ad134-4c47-4cf5-b244-9d9fc0804e93", 7 | "nwtN_ac02fa71-2428-47ad-8c3c-80bb5659a066" 8 | ], 9 | [ 10 | "nwtN_550a66a2-1061-45cc-9f78-14570daaff94", 11 | "82181180-e275-b004-243e-eb4383e1e72f", 12 | "359e920c-52a4-459e-2262-4ec90824f21e", 13 | "5a34dca5-646c-cae7-affe-ef4142c5ad73", 14 | "2f560d5b-bb1b-cc3f-d5b5-a69e6fab8d47", 15 | "d07ee031-2818-7c9c-5589-e8a161b9946c", 16 | "912c0f08-87fb-c746-18cb-19909a2b41c8" 17 | ], 18 | [ 19 | "1f64237f-dd15-2d5f-18f0-02a0d0338f44", 20 | "63edffef-16cf-a216-5ace-a27a4290e3d8", 21 | "d6768356-8772-a49a-c2f7-c3b9802c954b", 22 | "05b42313-71a4-e1a9-383b-6b4be641e69d" 23 | ], 24 | [ 25 | "3c161178-1fca-ec7e-3fc7-425ce940fe1a", 26 | "bab5c8a0-0f39-66d4-eaca-7f9160ef25fa", 27 | "a0d92978-e6e9-6d87-9a06-567d139d15af" 28 | ] 29 | ] 30 | }, 31 | "relativePlacementConstraint": [ 32 | { 33 | "top": "nwtN_6e27959a-ae83-4cb0-898f-9a1e6c5cafae", 34 | "bottom": "d2f78da5-fbd3-fe91-4046-3b5aea0648f6", 35 | "gap": 98.25 36 | }, 37 | { 38 | "top": "nwtN_6e27959a-ae83-4cb0-898f-9a1e6c5cafae", 39 | "bottom": "nwtN_a44ad134-4c47-4cf5-b244-9d9fc0804e93", 40 | "gap": 98.25 41 | }, 42 | { 43 | "top": "d2f78da5-fbd3-fe91-4046-3b5aea0648f6", 44 | "bottom": "288a2f9b-2930-701e-c68e-400307fbffd4", 45 | "gap": 98.25 46 | }, 47 | { 48 | "top": "d2f78da5-fbd3-fe91-4046-3b5aea0648f6", 49 | "bottom": "nwtN_ac02fa71-2428-47ad-8c3c-80bb5659a066", 50 | "gap": 98.25 51 | }, 52 | { 53 | "top": "nwtN_ac02fa71-2428-47ad-8c3c-80bb5659a066", 54 | "bottom": "912c0f08-87fb-c746-18cb-19909a2b41c8", 55 | "gap": 98.25 56 | }, 57 | { 58 | "top": "nwtN_ac02fa71-2428-47ad-8c3c-80bb5659a066", 59 | "bottom": "82181180-e275-b004-243e-eb4383e1e72f", 60 | "gap": 98.25 61 | }, 62 | { 63 | "top": "nwtN_ac02fa71-2428-47ad-8c3c-80bb5659a066", 64 | "bottom": "359e920c-52a4-459e-2262-4ec90824f21e", 65 | "gap": 98.25 66 | }, 67 | { 68 | "top": "nwtN_ac02fa71-2428-47ad-8c3c-80bb5659a066", 69 | "bottom": "5a34dca5-646c-cae7-affe-ef4142c5ad73", 70 | "gap": 98.25 71 | }, 72 | { 73 | "top": "nwtN_ac02fa71-2428-47ad-8c3c-80bb5659a066", 74 | "bottom": "2f560d5b-bb1b-cc3f-d5b5-a69e6fab8d47", 75 | "gap": 98.25 76 | }, 77 | { 78 | "top": "nwtN_a44ad134-4c47-4cf5-b244-9d9fc0804e93", 79 | "bottom": "nwtN_550a66a2-1061-45cc-9f78-14570daaff94", 80 | "gap": 98.25 81 | }, 82 | { 83 | "top": "nwtN_a44ad134-4c47-4cf5-b244-9d9fc0804e93", 84 | "bottom": "d07ee031-2818-7c9c-5589-e8a161b9946c", 85 | "gap": 98.25 86 | }, 87 | { 88 | "top": "2f560d5b-bb1b-cc3f-d5b5-a69e6fab8d47", 89 | "bottom": "1f64237f-dd15-2d5f-18f0-02a0d0338f44", 90 | "gap": 98.25 91 | }, 92 | { 93 | "top": "d07ee031-2818-7c9c-5589-e8a161b9946c", 94 | "bottom": "63edffef-16cf-a216-5ace-a27a4290e3d8", 95 | "gap": 98.25 96 | }, 97 | { 98 | "top": "d07ee031-2818-7c9c-5589-e8a161b9946c", 99 | "bottom": "d6768356-8772-a49a-c2f7-c3b9802c954b", 100 | "gap": 98.25 101 | }, 102 | { 103 | "top": "d07ee031-2818-7c9c-5589-e8a161b9946c", 104 | "bottom": "05b42313-71a4-e1a9-383b-6b4be641e69d", 105 | "gap": 98.25 106 | }, 107 | { 108 | "top": "63edffef-16cf-a216-5ace-a27a4290e3d8", 109 | "bottom": "3c161178-1fca-ec7e-3fc7-425ce940fe1a", 110 | "gap": 98.25 111 | }, 112 | { 113 | "top": "63edffef-16cf-a216-5ace-a27a4290e3d8", 114 | "bottom": "bab5c8a0-0f39-66d4-eaca-7f9160ef25fa", 115 | "gap": 98.25 116 | }, 117 | { 118 | "top": "63edffef-16cf-a216-5ace-a27a4290e3d8", 119 | "bottom": "a0d92978-e6e9-6d87-9a06-567d139d15af", 120 | "gap": 98.25 121 | } 122 | ] 123 | } -------------------------------------------------------------------------------- /demo/samples/chalk.js: -------------------------------------------------------------------------------- 1 | chalk = { 2 | "elements": { 3 | "nodes": [ 4 | { 5 | "data": { 6 | "id": "nwtN_00dcd31b-2631-4af1-980b-4493dc7d43f7", 7 | "bbox": { 8 | "x": 506.4608860015869, 9 | "y": 105.05675506591797, 10 | "w": 60.3330078125, 11 | "h": 28.25 12 | }, 13 | "originalW": 88.3330078125, 14 | "originalH": 56.25, 15 | "class": "compartment", 16 | "label": "types", 17 | "statesandinfos": [], 18 | "parent": "nwtN_80eb137f-5cd7-4584-9acc-87cc5698d255", 19 | "language": "PD", 20 | "border-width": 2.25, 21 | "border-color": "#555555", 22 | "background-color": "#f5f5f5", 23 | "background-opacity": 0.5, 24 | "background-image-opacity": "", 25 | "text-wrap": "wrap", 26 | "font-size": 11, 27 | "font-family": "Helvetica", 28 | "font-style": "normal", 29 | "font-weight": "bold", 30 | "color": "#000000", 31 | "ports": [], 32 | "background-image": "", 33 | "background-fit": "", 34 | "background-position-x": "", 35 | "background-position-y": "", 36 | "background-width": "", 37 | "background-height": "", 38 | "infoboxCalculated": true, 39 | "auxunitlayouts": {} 40 | }, 41 | "position": { 42 | "x": 506.4608860015869, 43 | "y": 105.05675506591797 44 | }, 45 | "group": "nodes", 46 | "removed": false, 47 | "selected": false, 48 | "selectable": true, 49 | "locked": false, 50 | "grabbable": true, 51 | "classes": "" 52 | }, 53 | { 54 | "data": { 55 | "id": "nwtN_80eb137f-5cd7-4584-9acc-87cc5698d255", 56 | "bbox": { 57 | "x": 503.796484757312, 58 | "y": 149.75074733690965, 59 | "w": 141.2687438947998, 60 | "h": 150.88798454198337 61 | }, 62 | "originalW": 169.2687438947998, 63 | "originalH": 178.88798454198337, 64 | "class": "compartment", 65 | "label": "chalk", 66 | "statesandinfos": [], 67 | "language": "PD", 68 | "border-width": 2.25, 69 | "border-color": "#555555", 70 | "background-color": "#f5f5f5", 71 | "background-opacity": 0.5, 72 | "background-image-opacity": "", 73 | "text-wrap": "wrap", 74 | "font-size": 11, 75 | "font-family": "Helvetica", 76 | "font-style": "normal", 77 | "font-weight": "bold", 78 | "color": "#000000", 79 | "ports": [], 80 | "background-image": "", 81 | "background-fit": "", 82 | "background-position-x": "", 83 | "background-position-y": "", 84 | "background-width": "", 85 | "background-height": "", 86 | "infoboxCalculated": true, 87 | "auxunitlayouts": {} 88 | }, 89 | "position": { 90 | "x": 503.796484757312, 91 | "y": 149.75074733690965 92 | }, 93 | "group": "nodes", 94 | "removed": false, 95 | "selected": false, 96 | "selectable": true, 97 | "locked": false, 98 | "grabbable": true, 99 | "classes": "" 100 | }, 101 | { 102 | "data": { 103 | "id": "nwtN_23452b85-7855-413f-a03d-26412ed87bfc", 104 | "bbox": { 105 | "x": 506.4608860015869, 106 | "y": 105.05675506591797, 107 | "w": 57.0830078125, 108 | "h": 25 109 | }, 110 | "class": "simple chemical", 111 | "label": "index.d.ts", 112 | "statesandinfos": [], 113 | "parent": "nwtN_00dcd31b-2631-4af1-980b-4493dc7d43f7", 114 | "language": "PD", 115 | "border-width": 1.25, 116 | "border-color": "#555555", 117 | "background-color": "#c7eae5", 118 | "background-opacity": 1, 119 | "background-image-opacity": "", 120 | "text-wrap": "wrap", 121 | "font-size": 11, 122 | "font-family": "Helvetica", 123 | "font-style": "normal", 124 | "font-weight": "normal", 125 | "color": "#000", 126 | "ports": [], 127 | "background-image": "", 128 | "background-fit": "", 129 | "background-position-x": "", 130 | "background-position-y": "", 131 | "background-width": "", 132 | "background-height": "", 133 | "infoboxCalculated": true, 134 | "auxunitlayouts": {} 135 | }, 136 | "position": { 137 | "x": 506.4608860015869, 138 | "y": 105.05675506591797 139 | }, 140 | "group": "nodes", 141 | "removed": false, 142 | "selected": false, 143 | "selectable": true, 144 | "locked": false, 145 | "grabbable": true, 146 | "classes": "" 147 | }, 148 | { 149 | "data": { 150 | "id": "nwtN_6ffc1fdf-602f-4395-89b5-666b383a5059", 151 | "bbox": { 152 | "x": 458.4355503099121, 153 | "y": 211.06973960790134, 154 | "w": 47.296875, 155 | "h": 25 156 | }, 157 | "class": "macromolecule", 158 | "label": "index.js", 159 | "statesandinfos": [], 160 | "parent": "nwtN_80eb137f-5cd7-4584-9acc-87cc5698d255", 161 | "language": "PD", 162 | "border-width": 1.25, 163 | "border-color": "#555555", 164 | "background-color": "#dfc27d", 165 | "background-opacity": 1, 166 | "background-image-opacity": "", 167 | "text-wrap": "wrap", 168 | "font-size": 11, 169 | "font-family": "Helvetica", 170 | "font-style": "normal", 171 | "font-weight": "normal", 172 | "color": "#000", 173 | "ports": [], 174 | "background-image": "", 175 | "background-fit": "", 176 | "background-position-x": "", 177 | "background-position-y": "", 178 | "background-width": "", 179 | "background-height": "", 180 | "infoboxCalculated": true, 181 | "auxunitlayouts": {} 182 | }, 183 | "position": { 184 | "x": 458.4355503099121, 185 | "y": 211.06973960790134 186 | }, 187 | "group": "nodes", 188 | "removed": false, 189 | "selected": false, 190 | "selectable": true, 191 | "locked": false, 192 | "grabbable": true, 193 | "classes": "" 194 | }, 195 | { 196 | "data": { 197 | "id": "nwtN_c152aeef-01df-49fc-9539-9e022e8d4c64", 198 | "bbox": { 199 | "x": 538.4608860015869, 200 | "y": 205.05675506591797, 201 | "w": 68.68994140625, 202 | "h": 25 203 | }, 204 | "class": "macromolecule", 205 | "label": "templates.js", 206 | "statesandinfos": [], 207 | "parent": "nwtN_80eb137f-5cd7-4584-9acc-87cc5698d255", 208 | "language": "PD", 209 | "border-width": 1.25, 210 | "border-color": "#555555", 211 | "background-color": "#dfc27d", 212 | "background-opacity": 1, 213 | "background-image-opacity": "", 214 | "text-wrap": "wrap", 215 | "font-size": 11, 216 | "font-family": "Helvetica", 217 | "font-style": "normal", 218 | "font-weight": "normal", 219 | "color": "#000", 220 | "ports": [], 221 | "background-image": "", 222 | "background-fit": "", 223 | "background-position-x": "", 224 | "background-position-y": "", 225 | "background-width": "", 226 | "background-height": "", 227 | "infoboxCalculated": true, 228 | "auxunitlayouts": {} 229 | }, 230 | "position": { 231 | "x": 538.4608860015869, 232 | "y": 205.05675506591797 233 | }, 234 | "group": "nodes", 235 | "removed": false, 236 | "selected": false, 237 | "selectable": true, 238 | "locked": false, 239 | "grabbable": true, 240 | "classes": "" 241 | }, 242 | { 243 | "data": { 244 | "id": "nwtN_c50c8570-f7bd-46c3-b7c2-66857746b854", 245 | "bbox": { 246 | "x": 457.4608860015869, 247 | "y": 305.05675506591797, 248 | "w": 50.546875, 249 | "h": 28.25 250 | }, 251 | "originalW": 78.546875, 252 | "originalH": 56.25, 253 | "class": "compartment", 254 | "label": "ansi-styles", 255 | "statesandinfos": [], 256 | "language": "PD", 257 | "border-width": 2.25, 258 | "border-color": "#555555", 259 | "background-color": "#f5f5f5", 260 | "background-opacity": 0.5, 261 | "background-image-opacity": "", 262 | "text-wrap": "wrap", 263 | "font-size": 11, 264 | "font-family": "Helvetica", 265 | "font-style": "normal", 266 | "font-weight": "bold", 267 | "color": "#000000", 268 | "ports": [], 269 | "background-image": "", 270 | "background-fit": "", 271 | "background-position-x": "", 272 | "background-position-y": "", 273 | "background-width": "", 274 | "background-height": "", 275 | "infoboxCalculated": true, 276 | "auxunitlayouts": {} 277 | }, 278 | "position": { 279 | "x": 457.4608860015869, 280 | "y": 305.05675506591797 281 | }, 282 | "group": "nodes", 283 | "removed": false, 284 | "selected": false, 285 | "selectable": true, 286 | "locked": false, 287 | "grabbable": true, 288 | "classes": "" 289 | }, 290 | { 291 | "data": { 292 | "id": "nwtN_fa99aac7-9500-4ead-82c3-90c72e9837f3", 293 | "bbox": { 294 | "x": 457.4608860015869, 295 | "y": 305.05675506591797, 296 | "w": 47.296875, 297 | "h": 25 298 | }, 299 | "class": "macromolecule", 300 | "label": "index.js", 301 | "statesandinfos": [], 302 | "parent": "nwtN_c50c8570-f7bd-46c3-b7c2-66857746b854", 303 | "language": "PD", 304 | "border-width": 1.25, 305 | "border-color": "#555555", 306 | "background-color": "#dfc27d", 307 | "background-opacity": 1, 308 | "background-image-opacity": "", 309 | "text-wrap": "wrap", 310 | "font-size": 11, 311 | "font-family": "Helvetica", 312 | "font-style": "normal", 313 | "font-weight": "normal", 314 | "color": "#000", 315 | "ports": [], 316 | "background-image": "", 317 | "background-fit": "", 318 | "background-position-x": "", 319 | "background-position-y": "", 320 | "background-width": "", 321 | "background-height": "", 322 | "infoboxCalculated": true, 323 | "auxunitlayouts": {} 324 | }, 325 | "position": { 326 | "x": 457.4608860015869, 327 | "y": 305.05675506591797 328 | }, 329 | "group": "nodes", 330 | "removed": false, 331 | "selected": false, 332 | "selectable": true, 333 | "locked": false, 334 | "grabbable": true, 335 | "classes": "" 336 | }, 337 | { 338 | "data": { 339 | "id": "nwtN_cafd8043-b744-474c-99d0-7f33deb415ac", 340 | "bbox": { 341 | "x": 451.6038619242243, 342 | "y": 380.17711757548415, 343 | "w": 50.546875, 344 | "h": 28.25 345 | }, 346 | "originalW": 78.546875, 347 | "originalH": 56.25, 348 | "class": "compartment", 349 | "label": "escape-string-regexp", 350 | "statesandinfos": [], 351 | "language": "PD", 352 | "border-width": 2.25, 353 | "border-color": "#555555", 354 | "background-color": "#f5f5f5", 355 | "background-opacity": 0.5, 356 | "background-image-opacity": "", 357 | "text-wrap": "wrap", 358 | "font-size": 11, 359 | "font-family": "Helvetica", 360 | "font-style": "normal", 361 | "font-weight": "bold", 362 | "color": "#000000", 363 | "ports": [], 364 | "background-image": "", 365 | "background-fit": "", 366 | "background-position-x": "", 367 | "background-position-y": "", 368 | "background-width": "", 369 | "background-height": "", 370 | "infoboxCalculated": true, 371 | "auxunitlayouts": {} 372 | }, 373 | "position": { 374 | "x": 451.6038619242243, 375 | "y": 380.17711757548415 376 | }, 377 | "group": "nodes", 378 | "removed": false, 379 | "selected": false, 380 | "selectable": true, 381 | "locked": false, 382 | "grabbable": true, 383 | "classes": "" 384 | }, 385 | { 386 | "data": { 387 | "id": "nwtN_6442ea6c-8a74-4948-ab60-2a2fc6b904f7", 388 | "bbox": { 389 | "x": 451.6038619242243, 390 | "y": 380.17711757548415, 391 | "w": 47.296875, 392 | "h": 25 393 | }, 394 | "class": "macromolecule", 395 | "label": "index.js", 396 | "statesandinfos": [], 397 | "parent": "nwtN_cafd8043-b744-474c-99d0-7f33deb415ac", 398 | "language": "PD", 399 | "border-width": 1.25, 400 | "border-color": "#555555", 401 | "background-color": "#dfc27d", 402 | "background-opacity": 1, 403 | "background-image-opacity": "", 404 | "text-wrap": "wrap", 405 | "font-size": 11, 406 | "font-family": "Helvetica", 407 | "font-style": "normal", 408 | "font-weight": "normal", 409 | "color": "#000", 410 | "ports": [], 411 | "background-image": "", 412 | "background-fit": "", 413 | "background-position-x": "", 414 | "background-position-y": "", 415 | "background-width": "", 416 | "background-height": "", 417 | "infoboxCalculated": true, 418 | "auxunitlayouts": {} 419 | }, 420 | "position": { 421 | "x": 451.6038619242243, 422 | "y": 380.17711757548415 423 | }, 424 | "group": "nodes", 425 | "removed": false, 426 | "selected": false, 427 | "selectable": true, 428 | "locked": false, 429 | "grabbable": true, 430 | "classes": "" 431 | }, 432 | { 433 | "data": { 434 | "id": "nwtN_e99db3a7-4249-47e7-84cf-b61676393dc8", 435 | "bbox": { 436 | "x": 467.6089290625593, 437 | "y": 476.207520405494, 438 | "w": 50.546875, 439 | "h": 28.25 440 | }, 441 | "originalW": 78.546875, 442 | "originalH": 56.25, 443 | "class": "compartment", 444 | "label": "supports-color", 445 | "statesandinfos": [], 446 | "language": "PD", 447 | "border-width": 2.25, 448 | "border-color": "#555555", 449 | "background-color": "#f5f5f5", 450 | "background-opacity": 0.5, 451 | "background-image-opacity": "", 452 | "text-wrap": "wrap", 453 | "font-size": 11, 454 | "font-family": "Helvetica", 455 | "font-style": "normal", 456 | "font-weight": "bold", 457 | "color": "#000000", 458 | "ports": [], 459 | "background-image": "", 460 | "background-fit": "", 461 | "background-position-x": "", 462 | "background-position-y": "", 463 | "background-width": "", 464 | "background-height": "", 465 | "infoboxCalculated": true, 466 | "auxunitlayouts": {} 467 | }, 468 | "position": { 469 | "x": 467.6089290625593, 470 | "y": 476.207520405494 471 | }, 472 | "group": "nodes", 473 | "removed": false, 474 | "selected": false, 475 | "selectable": true, 476 | "locked": false, 477 | "grabbable": true, 478 | "classes": "" 479 | }, 480 | { 481 | "data": { 482 | "id": "nwtN_395e3fed-fcd4-427a-909a-c01a7fc94607", 483 | "bbox": { 484 | "x": 467.6089290625593, 485 | "y": 476.207520405494, 486 | "w": 47.296875, 487 | "h": 25 488 | }, 489 | "class": "macromolecule", 490 | "label": "index.js", 491 | "statesandinfos": [], 492 | "parent": "nwtN_e99db3a7-4249-47e7-84cf-b61676393dc8", 493 | "language": "PD", 494 | "border-width": 1.25, 495 | "border-color": "#555555", 496 | "background-color": "#dfc27d", 497 | "background-opacity": 1, 498 | "background-image-opacity": "", 499 | "text-wrap": "wrap", 500 | "font-size": 11, 501 | "font-family": "Helvetica", 502 | "font-style": "normal", 503 | "font-weight": "normal", 504 | "color": "#000", 505 | "ports": [], 506 | "background-image": "", 507 | "background-fit": "", 508 | "background-position-x": "", 509 | "background-position-y": "", 510 | "background-width": "", 511 | "background-height": "", 512 | "infoboxCalculated": true, 513 | "auxunitlayouts": {} 514 | }, 515 | "position": { 516 | "x": 467.6089290625593, 517 | "y": 476.207520405494 518 | }, 519 | "group": "nodes", 520 | "removed": false, 521 | "selected": false, 522 | "selectable": true, 523 | "locked": false, 524 | "grabbable": true, 525 | "classes": "" 526 | }, 527 | { 528 | "data": { 529 | "id": "nwtN_641f76bc-059e-4885-9e58-8bc08b26765d", 530 | "bbox": { 531 | "x": 663.6710015071626, 532 | "y": 439.19580264809434, 533 | "w": 50.546875, 534 | "h": 28.25 535 | }, 536 | "originalW": 78.546875, 537 | "originalH": 56.25, 538 | "class": "compartment", 539 | "label": "has-flag", 540 | "statesandinfos": [], 541 | "language": "PD", 542 | "border-width": 2.25, 543 | "border-color": "#555555", 544 | "background-color": "#f5f5f5", 545 | "background-opacity": 0.5, 546 | "background-image-opacity": "", 547 | "text-wrap": "wrap", 548 | "font-size": 11, 549 | "font-family": "Helvetica", 550 | "font-style": "normal", 551 | "font-weight": "bold", 552 | "color": "#000000", 553 | "ports": [], 554 | "background-image": "", 555 | "background-fit": "", 556 | "background-position-x": "", 557 | "background-position-y": "", 558 | "background-width": "", 559 | "background-height": "", 560 | "infoboxCalculated": true, 561 | "auxunitlayouts": {} 562 | }, 563 | "position": { 564 | "x": 663.6710015071626, 565 | "y": 439.19580264809434 566 | }, 567 | "group": "nodes", 568 | "removed": false, 569 | "selected": false, 570 | "selectable": true, 571 | "locked": false, 572 | "grabbable": true, 573 | "classes": "" 574 | }, 575 | { 576 | "data": { 577 | "id": "nwtN_fc4a5aa8-171f-41bd-8ab3-5eee343c4f9f", 578 | "bbox": { 579 | "x": 663.6710015071626, 580 | "y": 439.19580264809434, 581 | "w": 47.296875, 582 | "h": 25 583 | }, 584 | "class": "macromolecule", 585 | "label": "index.js", 586 | "statesandinfos": [], 587 | "parent": "nwtN_641f76bc-059e-4885-9e58-8bc08b26765d", 588 | "language": "PD", 589 | "border-width": 1.25, 590 | "border-color": "#555555", 591 | "background-color": "#dfc27d", 592 | "background-opacity": 1, 593 | "background-image-opacity": "", 594 | "text-wrap": "wrap", 595 | "font-size": 11, 596 | "font-family": "Helvetica", 597 | "font-style": "normal", 598 | "font-weight": "normal", 599 | "color": "#000", 600 | "ports": [], 601 | "background-image": "", 602 | "background-fit": "", 603 | "background-position-x": "", 604 | "background-position-y": "", 605 | "background-width": "", 606 | "background-height": "", 607 | "infoboxCalculated": true, 608 | "auxunitlayouts": {} 609 | }, 610 | "position": { 611 | "x": 663.6710015071626, 612 | "y": 439.19580264809434 613 | }, 614 | "group": "nodes", 615 | "removed": false, 616 | "selected": false, 617 | "selectable": true, 618 | "locked": false, 619 | "grabbable": true, 620 | "classes": "" 621 | }, 622 | { 623 | "data": { 624 | "id": "nwtN_e4a59b7b-986f-40c1-8211-07ecd7bd4a85", 625 | "bbox": { 626 | "x": 769.0413352597577, 627 | "y": 155.046620789248, 628 | "w": 209.7825138067825, 629 | "h": 76.26330123812932 630 | }, 631 | "originalW": 237.7825138067825, 632 | "originalH": 104.26330123812932, 633 | "class": "compartment", 634 | "label": "color-convert", 635 | "statesandinfos": [], 636 | "language": "PD", 637 | "border-width": 2.25, 638 | "border-color": "#555555", 639 | "background-color": "#f5f5f5", 640 | "background-opacity": 0.5, 641 | "background-image-opacity": "", 642 | "text-wrap": "wrap", 643 | "font-size": 11, 644 | "font-family": "Helvetica", 645 | "font-style": "normal", 646 | "font-weight": "bold", 647 | "color": "#000000", 648 | "ports": [], 649 | "background-image": "", 650 | "background-fit": "", 651 | "background-position-x": "", 652 | "background-position-y": "", 653 | "background-width": "", 654 | "background-height": "", 655 | "infoboxCalculated": true, 656 | "auxunitlayouts": {} 657 | }, 658 | "position": { 659 | "x": 769.0413352597577, 660 | "y": 155.046620789248 661 | }, 662 | "group": "nodes", 663 | "removed": false, 664 | "selected": false, 665 | "selectable": true, 666 | "locked": false, 667 | "grabbable": true, 668 | "classes": "" 669 | }, 670 | { 671 | "data": { 672 | "id": "nwtN_10cd6cfc-6d2a-48b1-82e1-29d16dadd069", 673 | "bbox": { 674 | "x": 689.4235158563665, 675 | "y": 179.05327140831267, 676 | "w": 47.296875, 677 | "h": 25 678 | }, 679 | "class": "macromolecule", 680 | "label": "index.js", 681 | "statesandinfos": [], 682 | "parent": "nwtN_e4a59b7b-986f-40c1-8211-07ecd7bd4a85", 683 | "language": "PD", 684 | "border-width": 1.25, 685 | "border-color": "#555555", 686 | "background-color": "#dfc27d", 687 | "background-opacity": 1, 688 | "background-image-opacity": "", 689 | "text-wrap": "wrap", 690 | "font-size": 11, 691 | "font-family": "Helvetica", 692 | "font-style": "normal", 693 | "font-weight": "normal", 694 | "color": "#000", 695 | "ports": [], 696 | "background-image": "", 697 | "background-fit": "", 698 | "background-position-x": "", 699 | "background-position-y": "", 700 | "background-width": "", 701 | "background-height": "", 702 | "infoboxCalculated": true, 703 | "auxunitlayouts": {} 704 | }, 705 | "position": { 706 | "x": 689.4235158563665, 707 | "y": 179.05327140831267 708 | }, 709 | "group": "nodes", 710 | "removed": false, 711 | "selected": false, 712 | "selectable": true, 713 | "locked": false, 714 | "grabbable": true, 715 | "classes": "" 716 | }, 717 | { 718 | "data": { 719 | "id": "nwtN_da2a2f8c-a185-4ffd-b577-f2c5a7680a37", 720 | "bbox": { 721 | "x": 832.459935913149, 722 | "y": 174.05612167362608, 723 | "w": 79.6953125, 724 | "h": 25 725 | }, 726 | "class": "macromolecule", 727 | "label": "conversions.js", 728 | "statesandinfos": [], 729 | "parent": "nwtN_e4a59b7b-986f-40c1-8211-07ecd7bd4a85", 730 | "language": "PD", 731 | "border-width": 1.25, 732 | "border-color": "#555555", 733 | "background-color": "#dfc27d", 734 | "background-opacity": 1, 735 | "background-image-opacity": "", 736 | "text-wrap": "wrap", 737 | "font-size": 11, 738 | "font-family": "Helvetica", 739 | "font-style": "normal", 740 | "font-weight": "normal", 741 | "color": "#000", 742 | "ports": [], 743 | "background-image": "", 744 | "background-fit": "", 745 | "background-position-x": "", 746 | "background-position-y": "", 747 | "background-width": "", 748 | "background-height": "", 749 | "infoboxCalculated": true, 750 | "auxunitlayouts": {} 751 | }, 752 | "position": { 753 | "x": 832.459935913149, 754 | "y": 174.05612167362608 755 | }, 756 | "group": "nodes", 757 | "removed": false, 758 | "selected": false, 759 | "selectable": true, 760 | "locked": false, 761 | "grabbable": true, 762 | "classes": "" 763 | }, 764 | { 765 | "data": { 766 | "id": "nwtN_ec240627-f567-42b4-a9c6-4564450e9946", 767 | "bbox": { 768 | "x": 761.4273162101177, 769 | "y": 131.03997017018335, 770 | "w": 46.072265625, 771 | "h": 25 772 | }, 773 | "class": "macromolecule", 774 | "label": "route.js", 775 | "statesandinfos": [], 776 | "parent": "nwtN_e4a59b7b-986f-40c1-8211-07ecd7bd4a85", 777 | "language": "PD", 778 | "border-width": 1.25, 779 | "border-color": "#555555", 780 | "background-color": "#dfc27d", 781 | "background-opacity": 1, 782 | "background-image-opacity": "", 783 | "text-wrap": "wrap", 784 | "font-size": 11, 785 | "font-family": "Helvetica", 786 | "font-style": "normal", 787 | "font-weight": "normal", 788 | "color": "#000", 789 | "ports": [], 790 | "background-image": "", 791 | "background-fit": "", 792 | "background-position-x": "", 793 | "background-position-y": "", 794 | "background-width": "", 795 | "background-height": "", 796 | "infoboxCalculated": true, 797 | "auxunitlayouts": {} 798 | }, 799 | "position": { 800 | "x": 761.4273162101177, 801 | "y": 131.03997017018335 802 | }, 803 | "group": "nodes", 804 | "removed": false, 805 | "selected": false, 806 | "selectable": true, 807 | "locked": false, 808 | "grabbable": true, 809 | "classes": "" 810 | }, 811 | { 812 | "data": { 813 | "id": "nwtN_bc39472b-e95f-4e5c-955e-eb6fd7fe0cdf", 814 | "bbox": { 815 | "x": 914.4608860015869, 816 | "y": 275.05675506591797, 817 | "w": 50.546875, 818 | "h": 28.25 819 | }, 820 | "originalW": 78.546875, 821 | "originalH": 56.25, 822 | "class": "compartment", 823 | "label": "color-name", 824 | "statesandinfos": [], 825 | "language": "PD", 826 | "border-width": 2.25, 827 | "border-color": "#555555", 828 | "background-color": "#f5f5f5", 829 | "background-opacity": 0.5, 830 | "background-image-opacity": "", 831 | "text-wrap": "wrap", 832 | "font-size": 11, 833 | "font-family": "Helvetica", 834 | "font-style": "normal", 835 | "font-weight": "bold", 836 | "color": "#000000", 837 | "ports": [], 838 | "background-image": "", 839 | "background-fit": "", 840 | "background-position-x": "", 841 | "background-position-y": "", 842 | "background-width": "", 843 | "background-height": "", 844 | "infoboxCalculated": true, 845 | "auxunitlayouts": {} 846 | }, 847 | "position": { 848 | "x": 914.4608860015869, 849 | "y": 275.05675506591797 850 | }, 851 | "group": "nodes", 852 | "removed": false, 853 | "selected": false, 854 | "selectable": true, 855 | "locked": false, 856 | "grabbable": true, 857 | "classes": "" 858 | }, 859 | { 860 | "data": { 861 | "id": "nwtN_2f088c82-aac6-4e48-8fde-8574a3390d24", 862 | "bbox": { 863 | "x": 914.4608860015869, 864 | "y": 275.05675506591797, 865 | "w": 47.296875, 866 | "h": 25 867 | }, 868 | "class": "macromolecule", 869 | "label": "index.js", 870 | "statesandinfos": [], 871 | "parent": "nwtN_bc39472b-e95f-4e5c-955e-eb6fd7fe0cdf", 872 | "language": "PD", 873 | "border-width": 1.25, 874 | "border-color": "#555555", 875 | "background-color": "#dfc27d", 876 | "background-opacity": 1, 877 | "background-image-opacity": "", 878 | "text-wrap": "wrap", 879 | "font-size": 11, 880 | "font-family": "Helvetica", 881 | "font-style": "normal", 882 | "font-weight": "normal", 883 | "color": "#000", 884 | "ports": [], 885 | "background-image": "", 886 | "background-fit": "", 887 | "background-position-x": "", 888 | "background-position-y": "", 889 | "background-width": "", 890 | "background-height": "", 891 | "infoboxCalculated": true, 892 | "auxunitlayouts": {} 893 | }, 894 | "position": { 895 | "x": 914.4608860015869, 896 | "y": 275.05675506591797 897 | }, 898 | "group": "nodes", 899 | "removed": false, 900 | "selected": false, 901 | "selectable": true, 902 | "locked": false, 903 | "grabbable": true, 904 | "classes": "" 905 | }, 906 | { 907 | "data": { 908 | "id": "nwtN_88c73829-9d76-4b03-ab27-335fd9581531", 909 | "bbox": { 910 | "x": 638.6630841035143, 911 | "y": 555.2325394010229, 912 | "w": 30, 913 | "h": 25 914 | }, 915 | "class": "unspecified entity", 916 | "label": "os", 917 | "statesandinfos": [], 918 | "language": "PD", 919 | "border-width": 1.25, 920 | "border-color": "#555555", 921 | "background-color": "#f5f5f5", 922 | "background-opacity": 1, 923 | "background-image-opacity": "", 924 | "text-wrap": "wrap", 925 | "font-size": 11, 926 | "font-family": "Helvetica", 927 | "font-style": "normal", 928 | "font-weight": "normal", 929 | "color": "#000", 930 | "ports": [], 931 | "background-image": "", 932 | "background-fit": "", 933 | "background-position-x": "", 934 | "background-position-y": "", 935 | "background-width": "", 936 | "background-height": "", 937 | "infoboxCalculated": true, 938 | "auxunitlayouts": {} 939 | }, 940 | "position": { 941 | "x": 638.6630841035143, 942 | "y": 555.2325394010229 943 | }, 944 | "group": "nodes", 945 | "removed": false, 946 | "selected": false, 947 | "selectable": true, 948 | "locked": false, 949 | "grabbable": true, 950 | "classes": "" 951 | } 952 | ], 953 | "edges": [ 954 | { 955 | "data": { 956 | "id": "nwtE_d1ae8562-bb81-4d93-ac24-78b9def385a2", 957 | "class": "production", 958 | "bendPointPositions": [], 959 | "language": "PD", 960 | "line-color": "#555555", 961 | "width": 1.25, 962 | "cardinality": 0, 963 | "source": "nwtN_6ffc1fdf-602f-4395-89b5-666b383a5059", 964 | "target": "nwtN_c152aeef-01df-49fc-9539-9e022e8d4c64", 965 | "portsource": "nwtN_6ffc1fdf-602f-4395-89b5-666b383a5059", 966 | "porttarget": "nwtN_c152aeef-01df-49fc-9539-9e022e8d4c64" 967 | }, 968 | "position": {}, 969 | "group": "edges", 970 | "removed": false, 971 | "selected": false, 972 | "selectable": true, 973 | "locked": false, 974 | "grabbable": true, 975 | "classes": "" 976 | }, 977 | { 978 | "data": { 979 | "id": "nwtE_925bc3d3-7dac-4ef3-b0d7-875c78be8fbe", 980 | "class": "production", 981 | "bendPointPositions": [], 982 | "language": "PD", 983 | "line-color": "#555555", 984 | "width": 1.25, 985 | "cardinality": 0, 986 | "source": "nwtN_6ffc1fdf-602f-4395-89b5-666b383a5059", 987 | "target": "nwtN_fa99aac7-9500-4ead-82c3-90c72e9837f3", 988 | "portsource": "nwtN_6ffc1fdf-602f-4395-89b5-666b383a5059", 989 | "porttarget": "nwtN_fa99aac7-9500-4ead-82c3-90c72e9837f3" 990 | }, 991 | "position": {}, 992 | "group": "edges", 993 | "removed": false, 994 | "selected": false, 995 | "selectable": true, 996 | "locked": false, 997 | "grabbable": true, 998 | "classes": "" 999 | }, 1000 | { 1001 | "data": { 1002 | "id": "nwtE_d32f598b-4b96-4403-87c4-870e99ee6bc1", 1003 | "class": "production", 1004 | "bendPointPositions": [], 1005 | "language": "PD", 1006 | "line-color": "#555555", 1007 | "width": 1.25, 1008 | "cardinality": 0, 1009 | "source": "nwtN_6ffc1fdf-602f-4395-89b5-666b383a5059", 1010 | "target": "nwtN_6442ea6c-8a74-4948-ab60-2a2fc6b904f7", 1011 | "portsource": "nwtN_6ffc1fdf-602f-4395-89b5-666b383a5059", 1012 | "porttarget": "nwtN_6442ea6c-8a74-4948-ab60-2a2fc6b904f7" 1013 | }, 1014 | "position": {}, 1015 | "group": "edges", 1016 | "removed": false, 1017 | "selected": false, 1018 | "selectable": true, 1019 | "locked": false, 1020 | "grabbable": true, 1021 | "classes": "" 1022 | }, 1023 | { 1024 | "data": { 1025 | "id": "nwtE_0f92d3d4-1849-4f4c-97f0-6504eb9831ac", 1026 | "class": "production", 1027 | "bendPointPositions": [], 1028 | "language": "PD", 1029 | "line-color": "#555555", 1030 | "width": 1.25, 1031 | "cardinality": 0, 1032 | "source": "nwtN_6ffc1fdf-602f-4395-89b5-666b383a5059", 1033 | "target": "nwtN_395e3fed-fcd4-427a-909a-c01a7fc94607", 1034 | "portsource": "nwtN_6ffc1fdf-602f-4395-89b5-666b383a5059", 1035 | "porttarget": "nwtN_395e3fed-fcd4-427a-909a-c01a7fc94607" 1036 | }, 1037 | "position": {}, 1038 | "group": "edges", 1039 | "removed": false, 1040 | "selected": false, 1041 | "selectable": true, 1042 | "locked": false, 1043 | "grabbable": true, 1044 | "classes": "" 1045 | }, 1046 | { 1047 | "data": { 1048 | "id": "nwtE_2b21b38a-8443-45ea-b067-d626a731fc32", 1049 | "class": "production", 1050 | "bendPointPositions": [], 1051 | "language": "PD", 1052 | "line-color": "#555555", 1053 | "width": 1.25, 1054 | "cardinality": 0, 1055 | "source": "nwtN_fa99aac7-9500-4ead-82c3-90c72e9837f3", 1056 | "target": "nwtN_10cd6cfc-6d2a-48b1-82e1-29d16dadd069", 1057 | "portsource": "nwtN_fa99aac7-9500-4ead-82c3-90c72e9837f3", 1058 | "porttarget": "nwtN_10cd6cfc-6d2a-48b1-82e1-29d16dadd069" 1059 | }, 1060 | "position": {}, 1061 | "group": "edges", 1062 | "removed": false, 1063 | "selected": false, 1064 | "selectable": true, 1065 | "locked": false, 1066 | "grabbable": true, 1067 | "classes": "" 1068 | }, 1069 | { 1070 | "data": { 1071 | "id": "nwtE_cc83602e-928e-4d47-b60e-9ec94b93f9bd", 1072 | "class": "production", 1073 | "bendPointPositions": [], 1074 | "language": "PD", 1075 | "line-color": "#555555", 1076 | "width": 1.25, 1077 | "cardinality": 0, 1078 | "source": "nwtN_10cd6cfc-6d2a-48b1-82e1-29d16dadd069", 1079 | "target": "nwtN_ec240627-f567-42b4-a9c6-4564450e9946", 1080 | "portsource": "nwtN_10cd6cfc-6d2a-48b1-82e1-29d16dadd069", 1081 | "porttarget": "nwtN_ec240627-f567-42b4-a9c6-4564450e9946" 1082 | }, 1083 | "position": {}, 1084 | "group": "edges", 1085 | "removed": false, 1086 | "selected": false, 1087 | "selectable": true, 1088 | "locked": false, 1089 | "grabbable": true, 1090 | "classes": "" 1091 | }, 1092 | { 1093 | "data": { 1094 | "id": "nwtE_66f2d0d1-0542-450a-b6ae-f1c8bfa19e23", 1095 | "class": "production", 1096 | "bendPointPositions": [], 1097 | "language": "PD", 1098 | "line-color": "#555555", 1099 | "width": 1.25, 1100 | "cardinality": 0, 1101 | "source": "nwtN_ec240627-f567-42b4-a9c6-4564450e9946", 1102 | "target": "nwtN_da2a2f8c-a185-4ffd-b577-f2c5a7680a37", 1103 | "portsource": "nwtN_ec240627-f567-42b4-a9c6-4564450e9946", 1104 | "porttarget": "nwtN_da2a2f8c-a185-4ffd-b577-f2c5a7680a37" 1105 | }, 1106 | "position": {}, 1107 | "group": "edges", 1108 | "removed": false, 1109 | "selected": false, 1110 | "selectable": true, 1111 | "locked": false, 1112 | "grabbable": true, 1113 | "classes": "" 1114 | }, 1115 | { 1116 | "data": { 1117 | "id": "nwtE_83bd7195-5e2f-49c8-87f1-b54d74309928", 1118 | "class": "production", 1119 | "bendPointPositions": [], 1120 | "language": "PD", 1121 | "line-color": "#555555", 1122 | "width": 1.25, 1123 | "cardinality": 0, 1124 | "source": "nwtN_10cd6cfc-6d2a-48b1-82e1-29d16dadd069", 1125 | "target": "nwtN_da2a2f8c-a185-4ffd-b577-f2c5a7680a37", 1126 | "portsource": "nwtN_10cd6cfc-6d2a-48b1-82e1-29d16dadd069", 1127 | "porttarget": "nwtN_da2a2f8c-a185-4ffd-b577-f2c5a7680a37" 1128 | }, 1129 | "position": {}, 1130 | "group": "edges", 1131 | "removed": false, 1132 | "selected": false, 1133 | "selectable": true, 1134 | "locked": false, 1135 | "grabbable": true, 1136 | "classes": "" 1137 | }, 1138 | { 1139 | "data": { 1140 | "id": "nwtE_5e327bfd-b03d-440c-8825-0f03c01c7426", 1141 | "class": "production", 1142 | "bendPointPositions": [], 1143 | "language": "PD", 1144 | "line-color": "#555555", 1145 | "width": 1.25, 1146 | "cardinality": 0, 1147 | "source": "nwtN_da2a2f8c-a185-4ffd-b577-f2c5a7680a37", 1148 | "target": "nwtN_2f088c82-aac6-4e48-8fde-8574a3390d24", 1149 | "portsource": "nwtN_da2a2f8c-a185-4ffd-b577-f2c5a7680a37", 1150 | "porttarget": "nwtN_2f088c82-aac6-4e48-8fde-8574a3390d24" 1151 | }, 1152 | "position": {}, 1153 | "group": "edges", 1154 | "removed": false, 1155 | "selected": false, 1156 | "selectable": true, 1157 | "locked": false, 1158 | "grabbable": true, 1159 | "classes": "" 1160 | }, 1161 | { 1162 | "data": { 1163 | "id": "nwtE_93128cfa-29d1-4669-8496-85227b119c1f", 1164 | "class": "production", 1165 | "bendPointPositions": [], 1166 | "language": "PD", 1167 | "line-color": "#555555", 1168 | "width": 1.25, 1169 | "cardinality": 0, 1170 | "source": "nwtN_395e3fed-fcd4-427a-909a-c01a7fc94607", 1171 | "target": "nwtN_fc4a5aa8-171f-41bd-8ab3-5eee343c4f9f", 1172 | "portsource": "nwtN_395e3fed-fcd4-427a-909a-c01a7fc94607", 1173 | "porttarget": "nwtN_fc4a5aa8-171f-41bd-8ab3-5eee343c4f9f" 1174 | }, 1175 | "position": {}, 1176 | "group": "edges", 1177 | "removed": false, 1178 | "selected": false, 1179 | "selectable": true, 1180 | "locked": false, 1181 | "grabbable": true, 1182 | "classes": "" 1183 | }, 1184 | { 1185 | "data": { 1186 | "id": "nwtE_95f18b40-fe6a-482c-9ff2-5296ad1923c0", 1187 | "class": "production", 1188 | "bendPointPositions": [], 1189 | "language": "PD", 1190 | "line-color": "#a8a8a8", 1191 | "width": 1.25, 1192 | "cardinality": 0, 1193 | "source": "nwtN_395e3fed-fcd4-427a-909a-c01a7fc94607", 1194 | "target": "nwtN_88c73829-9d76-4b03-ab27-335fd9581531", 1195 | "portsource": "nwtN_395e3fed-fcd4-427a-909a-c01a7fc94607", 1196 | "porttarget": "nwtN_88c73829-9d76-4b03-ab27-335fd9581531" 1197 | }, 1198 | "position": {}, 1199 | "group": "edges", 1200 | "removed": false, 1201 | "selected": false, 1202 | "selectable": true, 1203 | "locked": false, 1204 | "grabbable": true, 1205 | "classes": "" 1206 | } 1207 | ] 1208 | }, 1209 | "style": [ 1210 | { 1211 | "selector": "node", 1212 | "style": { 1213 | "shape": "rectangle", 1214 | "text-halign": "center", 1215 | "text-valign": "center", 1216 | "background-color": "#ffffff", 1217 | "opacity": "1", 1218 | "border-color": "#555555" 1219 | } 1220 | }, 1221 | { 1222 | "selector": "node[class = 'simple chemical']", 1223 | "style": { 1224 | "shape": "ellipse", 1225 | "background-color": "#c7eae5" 1226 | } 1227 | }, 1228 | { 1229 | "selector": "node[class = 'macromolecule']", 1230 | "style": { 1231 | "shape": "roundrectangle", 1232 | "background-color": "#dfc27d" 1233 | } 1234 | }, 1235 | { 1236 | "selector": "node[class = 'unspecified entity']", 1237 | "style": { 1238 | "shape": "ellipse", 1239 | "background-color": "#f5f5f5" 1240 | } 1241 | }, 1242 | { 1243 | "selector": ":parent", 1244 | "style": { 1245 | "background-opacity": "0.333", 1246 | "text-valign": "bottom", 1247 | "shape": "barrel", 1248 | "text-margin-y": "2px", 1249 | "font-weight" : "bold", 1250 | "border-color": "#555555" 1251 | } 1252 | }, 1253 | { 1254 | "selector": "node:selected", 1255 | "style": { 1256 | "background-color": "#33ff00", 1257 | "border-color": "#22ee00" 1258 | } 1259 | }, 1260 | { 1261 | "selector": "edge", 1262 | "style": { 1263 | "curve-style": "bezier", 1264 | "width": "2px", 1265 | "line-color": "rgb(58,126,207)", 1266 | "opacity": "1", 1267 | "target-arrow-shape": "triangle", 1268 | "target-arrow-color": "rgb(58,126,207)", 1269 | } 1270 | }, 1271 | { 1272 | "selector": "edge:selected", 1273 | "style": { 1274 | "line-color": "#33ff00", 1275 | "font-size": "13px", 1276 | "text-opacity": "1", 1277 | "text-rotation": "autorotate", 1278 | "color": "#33ff00", 1279 | "font-weight": "bold", 1280 | "text-background-shape": "roundrectangle", 1281 | "text-background-opacity": "1", 1282 | "text-background-padding": "2px", 1283 | "target-arrow-color": "#33ff00", 1284 | "target-arrow-shape": "triangle", 1285 | } 1286 | } 1287 | ], 1288 | "zoomingEnabled": true, 1289 | "userZoomingEnabled": true, 1290 | "zoom": 1.7784699371350863, 1291 | "minZoom": 0.125, 1292 | "maxZoom": 16, 1293 | "panningEnabled": true, 1294 | "userPanningEnabled": true, 1295 | "pan": { 1296 | "x": -246.2736846361503, 1297 | "y": -69.57913264371261 1298 | }, 1299 | "boxSelectionEnabled": true, 1300 | "renderer": { 1301 | "name": "canvas" 1302 | }, 1303 | "wheelSensitivity": 0.1, 1304 | "motionBlur": true 1305 | } -------------------------------------------------------------------------------- /demo/samples/chalk_constraints.js: -------------------------------------------------------------------------------- 1 | chalk_constraints = { 2 | "alignmentConstraint": { 3 | "vertical": [ 4 | [ 5 | "nwtN_c152aeef-01df-49fc-9539-9e022e8d4c64", 6 | "nwtN_fa99aac7-9500-4ead-82c3-90c72e9837f3", 7 | "nwtN_6442ea6c-8a74-4948-ab60-2a2fc6b904f7", 8 | "nwtN_395e3fed-fcd4-427a-909a-c01a7fc94607" 9 | ], 10 | [ 11 | "nwtN_fc4a5aa8-171f-41bd-8ab3-5eee343c4f9f", 12 | "nwtN_10cd6cfc-6d2a-48b1-82e1-29d16dadd069", 13 | "nwtN_88c73829-9d76-4b03-ab27-335fd9581531" 14 | ], 15 | [ 16 | "nwtN_23452b85-7855-413f-a03d-26412ed87bfc", 17 | "nwtN_6ffc1fdf-602f-4395-89b5-666b383a5059" 18 | ] 19 | ] 20 | }, 21 | "relativePlacementConstraint": [ 22 | { 23 | "left": "nwtN_6ffc1fdf-602f-4395-89b5-666b383a5059", 24 | "right": "nwtN_c152aeef-01df-49fc-9539-9e022e8d4c64", 25 | "gap": 180 26 | }, 27 | { 28 | "left": "nwtN_6ffc1fdf-602f-4395-89b5-666b383a5059", 29 | "right": "nwtN_fa99aac7-9500-4ead-82c3-90c72e9837f3", 30 | "gap": 180 31 | }, 32 | { 33 | "left": "nwtN_6ffc1fdf-602f-4395-89b5-666b383a5059", 34 | "right": "nwtN_6442ea6c-8a74-4948-ab60-2a2fc6b904f7", 35 | "gap": 180 36 | }, 37 | { 38 | "left": "nwtN_6ffc1fdf-602f-4395-89b5-666b383a5059", 39 | "right": "nwtN_395e3fed-fcd4-427a-909a-c01a7fc94607", 40 | "gap": 180 41 | }, 42 | { 43 | "left": "nwtN_fa99aac7-9500-4ead-82c3-90c72e9837f3", 44 | "right": "nwtN_10cd6cfc-6d2a-48b1-82e1-29d16dadd069", 45 | "gap": 120 46 | }, 47 | { 48 | "left": "nwtN_395e3fed-fcd4-427a-909a-c01a7fc94607", 49 | "right": "nwtN_fc4a5aa8-171f-41bd-8ab3-5eee343c4f9f", 50 | "gap": 120 51 | }, 52 | { 53 | "left": "nwtN_395e3fed-fcd4-427a-909a-c01a7fc94607", 54 | "right": "nwtN_88c73829-9d76-4b03-ab27-335fd9581531", 55 | "gap": 120 56 | }, 57 | { 58 | "left": "nwtN_10cd6cfc-6d2a-48b1-82e1-29d16dadd069", 59 | "right": "nwtN_ec240627-f567-42b4-a9c6-4564450e9946", 60 | "gap": 100 61 | }, 62 | { 63 | "left": "nwtN_10cd6cfc-6d2a-48b1-82e1-29d16dadd069", 64 | "right": "nwtN_da2a2f8c-a185-4ffd-b577-f2c5a7680a37", 65 | "gap": 120 66 | }, 67 | { 68 | "left": "nwtN_ec240627-f567-42b4-a9c6-4564450e9946", 69 | "right": "nwtN_da2a2f8c-a185-4ffd-b577-f2c5a7680a37", 70 | "gap": 120 71 | }, 72 | { 73 | "left": "nwtN_da2a2f8c-a185-4ffd-b577-f2c5a7680a37", 74 | "right": "nwtN_2f088c82-aac6-4e48-8fde-8574a3390d24", 75 | "gap": 120 76 | } 77 | ] 78 | } -------------------------------------------------------------------------------- /demo/samples/unix_constraints.js: -------------------------------------------------------------------------------- 1 | unix_constraints = { 2 | "relativePlacementConstraint": [ 3 | { 4 | "top": "nwtN_4451c335-4668-4f8d-9f71-274aa2e6ab06", 5 | "bottom": "nwtN_8d99e7af-1b2f-4605-a381-ce3127b6199a", 6 | "gap": 76.25 7 | }, 8 | { 9 | "top": "nwtN_4451c335-4668-4f8d-9f71-274aa2e6ab06", 10 | "bottom": "nwtN_37220ed2-0cbe-4d1f-9cac-07dfb11d6eda", 11 | "gap": 76.25 12 | }, 13 | { 14 | "top": "nwtN_8d99e7af-1b2f-4605-a381-ce3127b6199a", 15 | "bottom": "nwtN_020b84b2-717f-4465-b716-7520e5c9c470", 16 | "gap": 76.25 17 | }, 18 | { 19 | "top": "nwtN_8d99e7af-1b2f-4605-a381-ce3127b6199a", 20 | "bottom": "nwtN_c1880331-8411-443e-9218-87e78c057b22", 21 | "gap": 76.25 22 | }, 23 | { 24 | "top": "nwtN_8d99e7af-1b2f-4605-a381-ce3127b6199a", 25 | "bottom": "nwtN_3f3b6234-e449-44bb-9ba8-059d1f843c98", 26 | "gap": 76.25 27 | }, 28 | { 29 | "top": "nwtN_8d99e7af-1b2f-4605-a381-ce3127b6199a", 30 | "bottom": "nwtN_d7bbe746-9028-4268-a642-70544788d687", 31 | "gap": 76.25 32 | }, 33 | { 34 | "top": "nwtN_8d99e7af-1b2f-4605-a381-ce3127b6199a", 35 | "bottom": "nwtN_5bb0a938-c7ee-42d8-85c8-17bdc1ff0ade", 36 | "gap": 76.25 37 | }, 38 | { 39 | "top": "nwtN_37220ed2-0cbe-4d1f-9cac-07dfb11d6eda", 40 | "bottom": "nwtN_8227939a-5dc0-433e-8c4d-987ec44a1f8f", 41 | "gap": 76.25 42 | }, 43 | { 44 | "top": "nwtN_37220ed2-0cbe-4d1f-9cac-07dfb11d6eda", 45 | "bottom": "nwtN_3f7d61be-a016-4383-9b8d-8c1db26192f2", 46 | "gap": 76.25 47 | }, 48 | { 49 | "top": "nwtN_020b84b2-717f-4465-b716-7520e5c9c470", 50 | "bottom": "nwtN_166d8364-fd55-499e-9098-0a48db6615c1", 51 | "gap": 76.25 52 | }, 53 | { 54 | "top": "nwtN_c1880331-8411-443e-9218-87e78c057b22", 55 | "bottom": "nwtN_167a075d-9d93-49d0-b5be-446efebec06c", 56 | "gap": 76.25 57 | }, 58 | { 59 | "top": "nwtN_c1880331-8411-443e-9218-87e78c057b22", 60 | "bottom": "nwtN_db35fc70-2b89-4857-acb2-9dd545e61447", 61 | "gap": 76.25 62 | }, 63 | { 64 | "top": "nwtN_c1880331-8411-443e-9218-87e78c057b22", 65 | "bottom": "nwtN_9dfde51e-a410-4e3b-a472-9061ae4fae7a", 66 | "gap": 76.25 67 | }, 68 | { 69 | "top": "nwtN_8227939a-5dc0-433e-8c4d-987ec44a1f8f", 70 | "bottom": "nwtN_9dfde51e-a410-4e3b-a472-9061ae4fae7a", 71 | "gap": 76.25 72 | }, 73 | { 74 | "top": "nwtN_3f7d61be-a016-4383-9b8d-8c1db26192f2", 75 | "bottom": "nwtN_a186f2a4-524e-4a84-95c0-66d41dbda586", 76 | "gap": 76.25 77 | }, 78 | { 79 | "top": "nwtN_3f7d61be-a016-4383-9b8d-8c1db26192f2", 80 | "bottom": "nwtN_26c73172-708d-45cc-8ba5-f29671089767", 81 | "gap": 76.25 82 | }, 83 | { 84 | "top": "nwtN_167a075d-9d93-49d0-b5be-446efebec06c", 85 | "bottom": "nwtN_a2bfb9bd-8b13-43a5-87a5-d88b0137bd78", 86 | "gap": 76.25 87 | }, 88 | { 89 | "top": "nwtN_167a075d-9d93-49d0-b5be-446efebec06c", 90 | "bottom": "nwtN_2d9c1f24-1482-46fb-a0f4-f963ba454710", 91 | "gap": 76.25 92 | }, 93 | { 94 | "top": "nwtN_167a075d-9d93-49d0-b5be-446efebec06c", 95 | "bottom": "nwtN_15fb1e74-6b08-4914-95ac-ee4ecb1b7f17", 96 | "gap": 76.25 97 | }, 98 | { 99 | "top": "nwtN_167a075d-9d93-49d0-b5be-446efebec06c", 100 | "bottom": "nwtN_485d980b-dc42-48ca-9ada-3c1b754cc2ef", 101 | "gap": 76.25 102 | }, 103 | { 104 | "top": "nwtN_167a075d-9d93-49d0-b5be-446efebec06c", 105 | "bottom": "nwtN_bce869d1-7c58-4489-81aa-e87e9418ab70", 106 | "gap": 76.25 107 | }, 108 | { 109 | "top": "nwtN_9dfde51e-a410-4e3b-a472-9061ae4fae7a", 110 | "bottom": "nwtN_db35fc70-2b89-4857-acb2-9dd545e61447", 111 | "gap": 76.25 112 | }, 113 | { 114 | "top": "nwtN_a186f2a4-524e-4a84-95c0-66d41dbda586", 115 | "bottom": "nwtN_2cf0c5aa-67eb-4394-85e3-dc84f4b1cd52", 116 | "gap": 76.25 117 | }, 118 | { 119 | "top": "nwtN_26c73172-708d-45cc-8ba5-f29671089767", 120 | "bottom": "nwtN_2b6ef682-26c7-48d7-9fd8-09fa76c7b718", 121 | "gap": 76.25 122 | }, 123 | { 124 | "top": "nwtN_a2bfb9bd-8b13-43a5-87a5-d88b0137bd78", 125 | "bottom": "nwtN_be8097dc-4a84-4d3f-ade9-e80221f86dd6", 126 | "gap": 76.25 127 | }, 128 | { 129 | "top": "nwtN_d53ea35c-4609-4051-917a-8a183e5976cf", 130 | "bottom": "nwtN_c261ca93-6060-49c1-8286-2a98900c3b9e", 131 | "gap": 76.25 132 | }, 133 | { 134 | "top": "nwtN_db35fc70-2b89-4857-acb2-9dd545e61447", 135 | "bottom": "nwtN_a68571c0-2669-4894-aa53-332555f625a2", 136 | "gap": 76.25 137 | }, 138 | { 139 | "top": "nwtN_eed8fc62-a21d-4677-85f2-f1244e3abf00", 140 | "bottom": "nwtN_2f175f07-154a-4873-98cc-bc4e4d6d13a1", 141 | "gap": 76.25 142 | }, 143 | { 144 | "top": "nwtN_eed8fc62-a21d-4677-85f2-f1244e3abf00", 145 | "bottom": "nwtN_a68571c0-2669-4894-aa53-332555f625a2", 146 | "gap": 76.25 147 | }, 148 | { 149 | "top": "nwtN_eed8fc62-a21d-4677-85f2-f1244e3abf00", 150 | "bottom": "nwtN_e55d29c7-89bc-4aa0-b599-3637e141f17f", 151 | "gap": 76.25 152 | }, 153 | { 154 | "top": "nwtN_c261ca93-6060-49c1-8286-2a98900c3b9e", 155 | "bottom": "nwtN_02a50d8b-3939-492a-ac5b-1f356cd514f6", 156 | "gap": 76.25 157 | }, 158 | { 159 | "top": "nwtN_2f175f07-154a-4873-98cc-bc4e4d6d13a1", 160 | "bottom": "nwtN_a68571c0-2669-4894-aa53-332555f625a2", 161 | "gap": 76.25 162 | }, 163 | { 164 | "top": "nwtN_166d8364-fd55-499e-9098-0a48db6615c1", 165 | "bottom": "nwtN_205680d2-dc98-4faf-baf4-21e27e8585e4", 166 | "gap": 76.25 167 | }, 168 | { 169 | "top": "nwtN_02a50d8b-3939-492a-ac5b-1f356cd514f6", 170 | "bottom": "nwtN_205680d2-dc98-4faf-baf4-21e27e8585e4", 171 | "gap": 76.25 172 | }, 173 | { 174 | "top": "nwtN_02a50d8b-3939-492a-ac5b-1f356cd514f6", 175 | "bottom": "nwtN_75318a95-e179-40fc-b6e1-e01fcd66cd8c", 176 | "gap": 76.25 177 | }, 178 | { 179 | "top": "nwtN_02a50d8b-3939-492a-ac5b-1f356cd514f6", 180 | "bottom": "nwtN_bce869d1-7c58-4489-81aa-e87e9418ab70", 181 | "gap": 76.25 182 | }, 183 | { 184 | "top": "nwtN_a68571c0-2669-4894-aa53-332555f625a2", 185 | "bottom": "nwtN_8ed84db0-02da-43c6-847c-6b33f657f52c", 186 | "gap": 76.25 187 | }, 188 | { 189 | "top": "nwtN_205680d2-dc98-4faf-baf4-21e27e8585e4", 190 | "bottom": "nwtN_be8097dc-4a84-4d3f-ade9-e80221f86dd6", 191 | "gap": 76.25 192 | }, 193 | { 194 | "top": "nwtN_205680d2-dc98-4faf-baf4-21e27e8585e4", 195 | "bottom": "nwtN_8489a816-b7af-452f-a722-04d06226341c", 196 | "gap": 76.25 197 | }, 198 | { 199 | "top": "nwtN_75318a95-e179-40fc-b6e1-e01fcd66cd8c", 200 | "bottom": "nwtN_d4bf8470-416d-4387-a563-3c9cccf55079", 201 | "gap": 76.25 202 | }, 203 | { 204 | "top": "nwtN_75318a95-e179-40fc-b6e1-e01fcd66cd8c", 205 | "bottom": "nwtN_e48d9732-8ee0-4714-8b62-8de8238ec9c2", 206 | "gap": 76.25 207 | }, 208 | { 209 | "top": "nwtN_bce869d1-7c58-4489-81aa-e87e9418ab70", 210 | "bottom": "nwtN_103a9105-ef50-47a4-b501-840a431fcbdd", 211 | "gap": 76.25 212 | }, 213 | { 214 | "top": "nwtN_8ed84db0-02da-43c6-847c-6b33f657f52c", 215 | "bottom": "nwtN_6385f491-0d09-4d53-a34c-781a861e8b42", 216 | "gap": 76.25 217 | }, 218 | { 219 | "top": "nwtN_6385f491-0d09-4d53-a34c-781a861e8b42", 220 | "bottom": "nwtN_88e20782-9221-4b12-9b62-6a6c7116a70f", 221 | "gap": 76.25 222 | }, 223 | { 224 | "top": "nwtN_167a075d-9d93-49d0-b5be-446efebec06c", 225 | "bottom": "nwtN_be8097dc-4a84-4d3f-ade9-e80221f86dd6", 226 | "gap": 76.25 227 | }, 228 | { 229 | "top": "nwtN_485d980b-dc42-48ca-9ada-3c1b754cc2ef", 230 | "bottom": "nwtN_d53ea35c-4609-4051-917a-8a183e5976cf", 231 | "gap": 76.25 232 | }, 233 | { 234 | "top": "nwtN_2cf0c5aa-67eb-4394-85e3-dc84f4b1cd52", 235 | "bottom": "nwtN_db35fc70-2b89-4857-acb2-9dd545e61447", 236 | "gap": 76.25 237 | }, 238 | { 239 | "top": "nwtN_cc15f875-f4dd-4df8-a09e-a4f2e6c8a3f5", 240 | "bottom": "nwtN_db35fc70-2b89-4857-acb2-9dd545e61447", 241 | "gap": 76.25 242 | }, 243 | { 244 | "top": "nwtN_2b6ef682-26c7-48d7-9fd8-09fa76c7b718", 245 | "bottom": "nwtN_eed8fc62-a21d-4677-85f2-f1244e3abf00", 246 | "gap": 76.25 247 | } 248 | ] 249 | } 250 | -------------------------------------------------------------------------------- /demo/samples/uwsn_constraints.js: -------------------------------------------------------------------------------- 1 | uwsn_constraints = { 2 | "fixedNodeConstraint": [ 3 | { 4 | "nodeId": "nwtN_91cff953-5f55-4626-96fa-e2f675e13e54", 5 | "position": { 6 | "x": -650, 7 | "y": -230 8 | } 9 | }, 10 | { 11 | "nodeId": "nwtN_b20cc3f4-dc9c-4031-ac79-df665d4c53cd", 12 | "position": { 13 | "x": 0, 14 | "y": -230 15 | } 16 | }, 17 | { 18 | "nodeId": "nwtN_5d6f7198-03e4-48e5-9004-919221f87e66", 19 | "position": { 20 | "x": 650, 21 | "y": -230 22 | } 23 | } 24 | ], 25 | "alignmentConstraint": { 26 | "vertical": [ 27 | [ 28 | "nwtN_91cff953-5f55-4626-96fa-e2f675e13e54", 29 | "nwtN_7e09bf9e-a4da-4618-baba-60a2b3a0b134" 30 | ], 31 | [ 32 | "nwtN_b20cc3f4-dc9c-4031-ac79-df665d4c53cd", 33 | "661498c7-ace9-bd34-0289-7c9931d3e1ba" 34 | ], 35 | [ 36 | "nwtN_5d6f7198-03e4-48e5-9004-919221f87e66", 37 | "aeee80bf-8280-f4ec-c1f3-26f3a7a0651c" 38 | ] 39 | ] 40 | }, 41 | "relativePlacementConstraint": [ 42 | { 43 | "top": "nwtN_7e09bf9e-a4da-4618-baba-60a2b3a0b134", 44 | "bottom": "nwtN_be627c29-b885-460c-bf92-8f9bb7b8c8c5", 45 | "gap": 100 46 | }, 47 | { 48 | "top": "nwtN_7e09bf9e-a4da-4618-baba-60a2b3a0b134", 49 | "bottom": "nwtN_64abed47-d1b1-474c-b944-b9ad6354c086", 50 | "gap": 100 51 | }, 52 | { 53 | "top": "661498c7-ace9-bd34-0289-7c9931d3e1ba", 54 | "bottom": "nwtN_0185dada-235a-4b51-bce7-e8bd3b8ee661", 55 | "gap": 100 56 | }, 57 | { 58 | "top": "661498c7-ace9-bd34-0289-7c9931d3e1ba", 59 | "bottom": "nwtN_273f6927-f05a-425b-aa2b-92de2be06bd9", 60 | "gap": 100 61 | }, 62 | { 63 | "top": "aeee80bf-8280-f4ec-c1f3-26f3a7a0651c", 64 | "bottom": "5c108d5c-88e4-7b97-95a8-67238c33d283", 65 | "gap": 100 66 | }, 67 | { 68 | "top": "aeee80bf-8280-f4ec-c1f3-26f3a7a0651c", 69 | "bottom": "3ea6020e-8918-8abf-a8e5-62427070f84f", 70 | "gap": 100 71 | }, 72 | { 73 | "top": "nwtN_5d6f7198-03e4-48e5-9004-919221f87e66", 74 | "bottom": "aeee80bf-8280-f4ec-c1f3-26f3a7a0651c", 75 | "gap": 250 76 | }, 77 | { 78 | "top": "nwtN_b20cc3f4-dc9c-4031-ac79-df665d4c53cd", 79 | "bottom": "661498c7-ace9-bd34-0289-7c9931d3e1ba", 80 | "gap": 250 81 | }, 82 | { 83 | "top": "nwtN_91cff953-5f55-4626-96fa-e2f675e13e54", 84 | "bottom": "nwtN_7e09bf9e-a4da-4618-baba-60a2b3a0b134", 85 | "gap": 250 86 | } 87 | ] 88 | } -------------------------------------------------------------------------------- /demo/samples/wsn_constraints.js: -------------------------------------------------------------------------------- 1 | wsn_constraints = { 2 | "fixedNodeConstraint": [ 3 | { 4 | "nodeId": "86f48f58-145d-92cc-0d33-aaf639984733", 5 | "position": { 6 | "x": 399, 7 | "y": 139 8 | } 9 | }, 10 | { 11 | "nodeId": "f91d914e-6ab7-92cf-b16d-b9a7661e0044", 12 | "position": { 13 | "x": 857, 14 | "y": 340 15 | } 16 | }, 17 | { 18 | "nodeId": "33f80abd-028e-ad1c-068b-33d5b40d9a93", 19 | "position": { 20 | "x": 1068, 21 | "y": 288 22 | } 23 | }, 24 | { 25 | "nodeId": "2b855d36-0c89-996d-9281-ea54f711cc8e", 26 | "position": { 27 | "x": 520, 28 | "y": 518 29 | } 30 | }, 31 | { 32 | "nodeId": "0ddcc98c-7b7c-ac2e-2207-081c5ffc9921", 33 | "position": { 34 | "x": 533, 35 | "y": -37 36 | } 37 | }, 38 | { 39 | "nodeId": "e53126c9-70fe-ca8a-3b85-bab123a93d70", 40 | "position": { 41 | "x": 816, 42 | "y": 280 43 | } 44 | }, 45 | { 46 | "nodeId": "4ca3044f-9b82-020b-e1cb-bbb45e4b0088", 47 | "position": { 48 | "x": 630, 49 | "y": 205 50 | } 51 | }, 52 | { 53 | "nodeId": "bc0420e3-9b6e-4c97-74c6-61278dbc18d4", 54 | "position": { 55 | "x": 507, 56 | "y": 407 57 | } 58 | }, 59 | { 60 | "nodeId": "a97f8f48-6652-7e09-8eda-2d87eae7d20b", 61 | "position": { 62 | "x": 323, 63 | "y": 330 64 | } 65 | }, 66 | { 67 | "nodeId": "889eedb3-8cb8-b906-8beb-904cd499c4f6", 68 | "position": { 69 | "x": 892, 70 | "y": -55 71 | } 72 | }, 73 | { 74 | "nodeId": "bd77a77c-4627-ad77-1c5a-2c390ba21959", 75 | "position": { 76 | "x": 1114, 77 | "y": 570 78 | } 79 | }, 80 | { 81 | "nodeId": "ffbd69bf-481f-faa8-73db-1489f3175b75", 82 | "position": { 83 | "x": 666, 84 | "y": 533 85 | } 86 | }, 87 | { 88 | "nodeId": "5fdc6150-63bb-18e8-1f03-33bb8d6b7529", 89 | "position": { 90 | "x": 726, 91 | "y": 670 92 | } 93 | }, 94 | { 95 | "nodeId": "b386ca2b-81a7-e365-4706-f955f13db03f", 96 | "position": { 97 | "x": 721, 98 | "y": -36 99 | } 100 | } 101 | ] 102 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cytoscape-fcose", 3 | "version": "2.2.0", 4 | "description": "The fCoSE layout for Cytoscape.js by Bilkent with fast compound node placement", 5 | "main": "cytoscape-fcose.js", 6 | "author": { 7 | "name": "iVis-at-Bilkent" 8 | }, 9 | "scripts": { 10 | "copyright": "update license", 11 | "lint": "eslint src", 12 | "build": "cross-env NODE_ENV=production webpack", 13 | "build:min": "cross-env NODE_ENV=production MIN=true webpack", 14 | "build:release": "run-s build copyright", 15 | "watch": "webpack --progress --watch", 16 | "dev": "webpack-dev-server --open", 17 | "test": "mocha" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/iVis-at-Bilkent/cytoscape.js-fcose.git" 22 | }, 23 | "keywords": [ 24 | "cytoscape", 25 | "cytoscape-extension" 26 | ], 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/iVis-at-Bilkent/cytoscape.js-fcose/issues" 30 | }, 31 | "homepage": "https://github.com/iVis-at-Bilkent/cytoscape.js-fcose", 32 | "devDependencies": { 33 | "babel-core": "^6.24.1", 34 | "babel-loader": "^7.1.4", 35 | "babel-preset-env": "^1.5.1", 36 | "camelcase": "^6.2.0", 37 | "chai": "4.0.2", 38 | "cpy-cli": "^3.1.1", 39 | "cross-env": "^7.0.3", 40 | "eslint": "^7.26.0", 41 | "gh-pages": "^1.0.0", 42 | "mocha": "8.4.0", 43 | "npm-run-all": "^4.1.2", 44 | "rimraf": "^3.0.2", 45 | "update": "^0.7.4", 46 | "updater-license": "^1.0.0", 47 | "webpack": "^5.37.0", 48 | "webpack-cli": "^4.7.0", 49 | "webpack-dev-server": "^3.11.2" 50 | }, 51 | "peerDependencies": { 52 | "cytoscape": "^3.2.0" 53 | }, 54 | "dependencies": { 55 | "cose-base": "^2.2.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /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.forEach( src => { 5 | Object.keys( src ).forEach( k => tgt[k] = src[k] ); 6 | } ); 7 | 8 | return tgt; 9 | }; 10 | -------------------------------------------------------------------------------- /src/fcose/auxiliary.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Auxiliary functions 3 | */ 4 | 5 | const LinkedList = require('cose-base').layoutBase.LinkedList; 6 | 7 | let auxiliary = {}; 8 | 9 | // get the top most nodes 10 | auxiliary.getTopMostNodes = function(nodes) { 11 | let nodesMap = {}; 12 | for (let i = 0; i < nodes.length; i++) { 13 | nodesMap[nodes[i].id()] = true; 14 | } 15 | let roots = nodes.filter(function (ele, i) { 16 | if(typeof ele === "number") { 17 | ele = i; 18 | } 19 | let parent = ele.parent()[0]; 20 | while(parent != null){ 21 | if(nodesMap[parent.id()]){ 22 | return false; 23 | } 24 | parent = parent.parent()[0]; 25 | } 26 | return true; 27 | }); 28 | 29 | return roots; 30 | }; 31 | 32 | // find disconnected components and create dummy nodes that connect them 33 | auxiliary.connectComponents = function(cy, eles, topMostNodes, dummyNodes){ 34 | let queue = new LinkedList(); 35 | let visited = new Set(); 36 | let visitedTopMostNodes = []; 37 | let currentNeighbor; 38 | let minDegreeNode; 39 | let minDegree; 40 | 41 | let isConnected = false; 42 | let count = 1; 43 | let nodesConnectedToDummy = []; 44 | let components = []; 45 | 46 | do{ 47 | let cmpt = cy.collection(); 48 | components.push(cmpt); 49 | 50 | let currentNode = topMostNodes[0]; 51 | let childrenOfCurrentNode = cy.collection(); 52 | childrenOfCurrentNode.merge(currentNode).merge(currentNode.descendants().intersection(eles)); 53 | visitedTopMostNodes.push(currentNode); 54 | 55 | childrenOfCurrentNode.forEach(function(node) { 56 | queue.push(node); 57 | visited.add(node); 58 | cmpt.merge(node); 59 | }); 60 | 61 | while(queue.length != 0){ 62 | currentNode = queue.shift(); 63 | 64 | // Traverse all neighbors of this node 65 | let neighborNodes = cy.collection(); 66 | currentNode.neighborhood().nodes().forEach(function(node){ 67 | if(eles.intersection(currentNode.edgesWith(node)).length > 0){ 68 | neighborNodes.merge(node); 69 | } 70 | }); 71 | 72 | for(let i = 0; i < neighborNodes.length; i++){ 73 | let neighborNode = neighborNodes[i]; 74 | currentNeighbor = topMostNodes.intersection(neighborNode.union(neighborNode.ancestors())); 75 | if(currentNeighbor != null && !visited.has(currentNeighbor[0])){ 76 | let childrenOfNeighbor = currentNeighbor.union(currentNeighbor.descendants()); 77 | 78 | childrenOfNeighbor.forEach(function(node){ 79 | queue.push(node); 80 | visited.add(node); 81 | cmpt.merge(node); 82 | if(topMostNodes.has(node)){ 83 | visitedTopMostNodes.push(node); 84 | } 85 | }); 86 | 87 | } 88 | } 89 | } 90 | 91 | cmpt.forEach(node => { 92 | eles.intersection(node.connectedEdges()).forEach(e => { // connectedEdges() usually cached 93 | if( cmpt.has(e.source()) && cmpt.has(e.target()) ){ // has() is cheap 94 | cmpt.merge(e); 95 | } 96 | }); 97 | }); 98 | 99 | if(visitedTopMostNodes.length == topMostNodes.length){ 100 | isConnected = true; 101 | } 102 | 103 | if(!isConnected || (isConnected && count > 1)){ 104 | minDegreeNode = visitedTopMostNodes[0]; 105 | minDegree = minDegreeNode.connectedEdges().length; 106 | visitedTopMostNodes.forEach(function(node){ 107 | if(node.connectedEdges().length < minDegree){ 108 | minDegree = node.connectedEdges().length; 109 | minDegreeNode = node; 110 | } 111 | }); 112 | nodesConnectedToDummy.push(minDegreeNode.id()); 113 | // TO DO: Check efficiency of this part 114 | let temp = cy.collection(); 115 | temp.merge(visitedTopMostNodes[0]); 116 | visitedTopMostNodes.forEach(function(node){ 117 | temp.merge(node); 118 | }); 119 | visitedTopMostNodes = []; 120 | topMostNodes = topMostNodes.difference(temp); 121 | count++; 122 | } 123 | 124 | } 125 | while(!isConnected); 126 | 127 | if(dummyNodes){ 128 | if(nodesConnectedToDummy.length > 0 ){ 129 | dummyNodes.set('dummy'+(dummyNodes.size+1), nodesConnectedToDummy); 130 | } 131 | } 132 | return components; 133 | }; 134 | 135 | // relocates componentResult to originalCenter if there is no fixedNodeConstraint 136 | auxiliary.relocateComponent = function(originalCenter, componentResult, options) { 137 | if (!options.fixedNodeConstraint) { 138 | let minXCoord = Number.POSITIVE_INFINITY; 139 | let maxXCoord = Number.NEGATIVE_INFINITY; 140 | let minYCoord = Number.POSITIVE_INFINITY; 141 | let maxYCoord = Number.NEGATIVE_INFINITY; 142 | if (options.quality == "draft") { 143 | // calculate current bounding box 144 | for (let [key, value] of componentResult.nodeIndexes) { 145 | let cyNode = options.cy.getElementById(key); 146 | if (cyNode) { 147 | let nodeBB = cyNode.boundingBox(); 148 | let leftX = componentResult.xCoords[value] - nodeBB.w / 2; 149 | let rightX = componentResult.xCoords[value] + nodeBB.w / 2; 150 | let topY = componentResult.yCoords[value] - nodeBB.h / 2; 151 | let bottomY = componentResult.yCoords[value] + nodeBB.h / 2; 152 | 153 | if (leftX < minXCoord) 154 | minXCoord = leftX; 155 | if (rightX > maxXCoord) 156 | maxXCoord = rightX; 157 | if (topY < minYCoord) 158 | minYCoord = topY; 159 | if (bottomY > maxYCoord) 160 | maxYCoord = bottomY; 161 | } 162 | } 163 | // find difference between current and original center 164 | let diffOnX = originalCenter.x - (maxXCoord + minXCoord) / 2; 165 | let diffOnY = originalCenter.y - (maxYCoord + minYCoord) / 2; 166 | // move component to original center 167 | componentResult.xCoords = componentResult.xCoords.map(x => x + diffOnX); 168 | componentResult.yCoords = componentResult.yCoords.map(y => y + diffOnY); 169 | } 170 | else { 171 | // calculate current bounding box 172 | Object.keys(componentResult).forEach(function (item) { 173 | let node = componentResult[item]; 174 | let leftX = node.getRect().x; 175 | let rightX = node.getRect().x + node.getRect().width; 176 | let topY = node.getRect().y; 177 | let bottomY = node.getRect().y + node.getRect().height; 178 | 179 | if (leftX < minXCoord) 180 | minXCoord = leftX; 181 | if (rightX > maxXCoord) 182 | maxXCoord = rightX; 183 | if (topY < minYCoord) 184 | minYCoord = topY; 185 | if (bottomY > maxYCoord) 186 | maxYCoord = bottomY; 187 | }); 188 | // find difference between current and original center 189 | let diffOnX = originalCenter.x - (maxXCoord + minXCoord) / 2; 190 | let diffOnY = originalCenter.y - (maxYCoord + minYCoord) / 2; 191 | // move component to original center 192 | Object.keys(componentResult).forEach(function (item) { 193 | let node = componentResult[item]; 194 | node.setCenter(node.getCenterX() + diffOnX, node.getCenterY() + diffOnY); 195 | }); 196 | } 197 | } 198 | }; 199 | 200 | auxiliary.calcBoundingBox = function(parentNode, xCoords, yCoords, nodeIndexes){ 201 | // calculate bounds 202 | let left = Number.MAX_SAFE_INTEGER; 203 | let right = Number.MIN_SAFE_INTEGER; 204 | let top = Number.MAX_SAFE_INTEGER; 205 | let bottom = Number.MIN_SAFE_INTEGER; 206 | let nodeLeft; 207 | let nodeRight; 208 | let nodeTop; 209 | let nodeBottom; 210 | 211 | let nodes = parentNode.descendants().not(":parent"); 212 | let s = nodes.length; 213 | for (let i = 0; i < s; i++) 214 | { 215 | let node = nodes[i]; 216 | 217 | nodeLeft = xCoords[nodeIndexes.get(node.id())] - node.width()/2; 218 | nodeRight = xCoords[nodeIndexes.get(node.id())] + node.width()/2; 219 | nodeTop = yCoords[nodeIndexes.get(node.id())] - node.height()/2; 220 | nodeBottom = yCoords[nodeIndexes.get(node.id())] + node.height()/2; 221 | 222 | if (left > nodeLeft) 223 | { 224 | left = nodeLeft; 225 | } 226 | 227 | if (right < nodeRight) 228 | { 229 | right = nodeRight; 230 | } 231 | 232 | if (top > nodeTop) 233 | { 234 | top = nodeTop; 235 | } 236 | 237 | if (bottom < nodeBottom) 238 | { 239 | bottom = nodeBottom; 240 | } 241 | } 242 | 243 | let boundingBox = {}; 244 | boundingBox.topLeftX = left; 245 | boundingBox.topLeftY = top; 246 | boundingBox.width = right - left; 247 | boundingBox.height = bottom - top; 248 | return boundingBox; 249 | }; 250 | 251 | // This function finds and returns parent nodes whose all children are hidden 252 | auxiliary.calcParentsWithoutChildren = function(cy, eles){ 253 | let parentsWithoutChildren = cy.collection(); 254 | eles.nodes(':parent').forEach((parent) => { 255 | let check = false; 256 | parent.children().forEach((child) => { 257 | if(child.css('display') != 'none') { 258 | check = true; 259 | } 260 | }); 261 | if(!check) { 262 | parentsWithoutChildren.merge(parent); 263 | } 264 | }); 265 | 266 | return parentsWithoutChildren; 267 | } 268 | 269 | module.exports = auxiliary; -------------------------------------------------------------------------------- /src/fcose/cose.js: -------------------------------------------------------------------------------- 1 | /** 2 | The implementation of the postprocessing part that applies CoSE layout over the spectral layout 3 | */ 4 | 5 | const aux = require('./auxiliary'); 6 | const CoSELayout = require('cose-base').CoSELayout; 7 | const CoSENode = require('cose-base').CoSENode; 8 | const PointD = require('cose-base').layoutBase.PointD; 9 | const DimensionD = require('cose-base').layoutBase.DimensionD; 10 | const LayoutConstants = require('cose-base').layoutBase.LayoutConstants; 11 | const FDLayoutConstants = require('cose-base').layoutBase.FDLayoutConstants; 12 | const CoSEConstants = require('cose-base').CoSEConstants; 13 | 14 | // main function that cose layout is processed 15 | let coseLayout = function(options, spectralResult){ 16 | 17 | let cy = options.cy; 18 | let eles = options.eles; 19 | let nodes = eles.nodes(); 20 | let edges = eles.edges(); 21 | 22 | let nodeIndexes; 23 | let xCoords; 24 | let yCoords; 25 | let idToLNode = {}; 26 | 27 | if(options.randomize){ 28 | nodeIndexes = spectralResult["nodeIndexes"]; 29 | xCoords = spectralResult["xCoords"]; 30 | yCoords = spectralResult["yCoords"]; 31 | } 32 | 33 | const isFn = fn => typeof fn === 'function'; 34 | 35 | const optFn = ( opt, ele ) => { 36 | if( isFn( opt ) ){ 37 | return opt( ele ); 38 | } else { 39 | return opt; 40 | } 41 | }; 42 | 43 | /**** Postprocessing functions ****/ 44 | 45 | let parentsWithoutChildren = aux.calcParentsWithoutChildren(cy, eles); 46 | 47 | // transfer cytoscape nodes to cose nodes 48 | let processChildrenList = function (parent, children, layout, options) { 49 | let size = children.length; 50 | for (let i = 0; i < size; i++) { 51 | let theChild = children[i]; 52 | let children_of_children = null; 53 | if(theChild.intersection(parentsWithoutChildren).length == 0) { 54 | children_of_children = theChild.children(); 55 | } 56 | let theNode; 57 | 58 | let dimensions = theChild.layoutDimensions({ 59 | nodeDimensionsIncludeLabels: options.nodeDimensionsIncludeLabels 60 | }); 61 | 62 | if (theChild.outerWidth() != null 63 | && theChild.outerHeight() != null) { 64 | if(options.randomize){ 65 | if(!theChild.isParent()){ 66 | theNode = parent.add(new CoSENode(layout.graphManager, 67 | new PointD(xCoords[nodeIndexes.get(theChild.id())] - dimensions.w / 2, yCoords[nodeIndexes.get(theChild.id())] - dimensions.h / 2), 68 | new DimensionD(parseFloat(dimensions.w), parseFloat(dimensions.h)))); 69 | } 70 | else{ 71 | let parentInfo = aux.calcBoundingBox(theChild, xCoords, yCoords, nodeIndexes); 72 | if(theChild.intersection(parentsWithoutChildren).length == 0) { 73 | theNode = parent.add(new CoSENode(layout.graphManager, 74 | new PointD(parentInfo.topLeftX, parentInfo.topLeftY), 75 | new DimensionD(parentInfo.width, parentInfo.height))); 76 | } 77 | else { // for the parentsWithoutChildren 78 | theNode = parent.add(new CoSENode(layout.graphManager, 79 | new PointD(parentInfo.topLeftX, parentInfo.topLeftY), 80 | new DimensionD(parseFloat(dimensions.w), parseFloat(dimensions.h)))); 81 | } 82 | } 83 | } 84 | else{ 85 | theNode = parent.add(new CoSENode(layout.graphManager, 86 | new PointD(theChild.position('x') - dimensions.w / 2, theChild.position('y') - dimensions.h / 2), 87 | new DimensionD(parseFloat(dimensions.w), parseFloat(dimensions.h)))); 88 | } 89 | } 90 | else { 91 | theNode = parent.add(new CoSENode(this.graphManager)); 92 | } 93 | // Attach id to the layout node and repulsion value 94 | theNode.id = theChild.data("id"); 95 | theNode.nodeRepulsion = optFn( options.nodeRepulsion, theChild ); 96 | // Attach the paddings of cy node to layout node 97 | theNode.paddingLeft = parseInt( theChild.css('padding') ); 98 | theNode.paddingTop = parseInt( theChild.css('padding') ); 99 | theNode.paddingRight = parseInt( theChild.css('padding') ); 100 | theNode.paddingBottom = parseInt( theChild.css('padding') ); 101 | 102 | //Attach the label properties to both compound and simple nodes if labels will be included in node dimensions 103 | //These properties will be used while updating bounds of compounds during iterations or tiling 104 | //and will be used for simple nodes while transferring final positions to cytoscape 105 | if(options.nodeDimensionsIncludeLabels){ 106 | theNode.labelWidth = theChild.boundingBox({ includeLabels: true, includeNodes: false, includeOverlays: false }).w; 107 | theNode.labelHeight = theChild.boundingBox({ includeLabels: true, includeNodes: false, includeOverlays: false }).h; 108 | theNode.labelPosVertical = theChild.css("text-valign"); 109 | theNode.labelPosHorizontal = theChild.css("text-halign"); 110 | } 111 | 112 | // Map the layout node 113 | idToLNode[theChild.data("id")] = theNode; 114 | 115 | if (isNaN(theNode.rect.x)) { 116 | theNode.rect.x = 0; 117 | } 118 | 119 | if (isNaN(theNode.rect.y)) { 120 | theNode.rect.y = 0; 121 | } 122 | 123 | if (children_of_children != null && children_of_children.length > 0) { 124 | let theNewGraph; 125 | theNewGraph = layout.getGraphManager().add(layout.newGraph(), theNode); 126 | processChildrenList(theNewGraph, children_of_children, layout, options); 127 | } 128 | } 129 | }; 130 | 131 | // transfer cytoscape edges to cose edges 132 | let processEdges = function(layout, gm, edges){ 133 | let idealLengthTotal = 0; 134 | let edgeCount = 0; 135 | for (let i = 0; i < edges.length; i++) { 136 | let edge = edges[i]; 137 | let sourceNode = idToLNode[edge.data("source")]; 138 | let targetNode = idToLNode[edge.data("target")]; 139 | if(sourceNode && targetNode && sourceNode !== targetNode && sourceNode.getEdgesBetween(targetNode).length == 0){ 140 | let e1 = gm.add(layout.newEdge(), sourceNode, targetNode); 141 | e1.id = edge.id(); 142 | e1.idealLength = optFn( options.idealEdgeLength, edge ); 143 | e1.edgeElasticity = optFn( options.edgeElasticity, edge ); 144 | idealLengthTotal += e1.idealLength; 145 | edgeCount++; 146 | } 147 | } 148 | // we need to update the ideal edge length constant with the avg. ideal length value after processing edges 149 | // in case there is no edge, use other options 150 | if (options.idealEdgeLength != null){ 151 | if (edgeCount > 0) 152 | CoSEConstants.DEFAULT_EDGE_LENGTH = FDLayoutConstants.DEFAULT_EDGE_LENGTH = idealLengthTotal / edgeCount; 153 | else if(!isFn(options.idealEdgeLength)) // in case there is no edge, but option gives a value to use 154 | CoSEConstants.DEFAULT_EDGE_LENGTH = FDLayoutConstants.DEFAULT_EDGE_LENGTH = options.idealEdgeLength; 155 | else // in case there is no edge and we cannot get a value from option (because it's a function) 156 | CoSEConstants.DEFAULT_EDGE_LENGTH = FDLayoutConstants.DEFAULT_EDGE_LENGTH = 50; 157 | // we need to update these constant values based on the ideal edge length constant 158 | CoSEConstants.MIN_REPULSION_DIST = FDLayoutConstants.MIN_REPULSION_DIST = FDLayoutConstants.DEFAULT_EDGE_LENGTH / 10.0; 159 | CoSEConstants.DEFAULT_RADIAL_SEPARATION = FDLayoutConstants.DEFAULT_EDGE_LENGTH; 160 | } 161 | }; 162 | 163 | // transfer cytoscape constraints to cose layout 164 | let processConstraints = function(layout, options){ 165 | // get nodes to be fixed 166 | if(options.fixedNodeConstraint){ 167 | layout.constraints["fixedNodeConstraint"] = options.fixedNodeConstraint; 168 | } 169 | // get nodes to be aligned 170 | if(options.alignmentConstraint){ 171 | layout.constraints["alignmentConstraint"] = options.alignmentConstraint; 172 | } 173 | // get nodes to be relatively placed 174 | if(options.relativePlacementConstraint){ 175 | layout.constraints["relativePlacementConstraint"] = options.relativePlacementConstraint; 176 | } 177 | }; 178 | 179 | /**** Apply postprocessing ****/ 180 | if (options.nestingFactor != null) 181 | CoSEConstants.PER_LEVEL_IDEAL_EDGE_LENGTH_FACTOR = FDLayoutConstants.PER_LEVEL_IDEAL_EDGE_LENGTH_FACTOR = options.nestingFactor; 182 | if (options.gravity != null) 183 | CoSEConstants.DEFAULT_GRAVITY_STRENGTH = FDLayoutConstants.DEFAULT_GRAVITY_STRENGTH = options.gravity; 184 | if (options.numIter != null) 185 | CoSEConstants.MAX_ITERATIONS = FDLayoutConstants.MAX_ITERATIONS = options.numIter; 186 | if (options.gravityRange != null) 187 | CoSEConstants.DEFAULT_GRAVITY_RANGE_FACTOR = FDLayoutConstants.DEFAULT_GRAVITY_RANGE_FACTOR = options.gravityRange; 188 | if(options.gravityCompound != null) 189 | CoSEConstants.DEFAULT_COMPOUND_GRAVITY_STRENGTH = FDLayoutConstants.DEFAULT_COMPOUND_GRAVITY_STRENGTH = options.gravityCompound; 190 | if(options.gravityRangeCompound != null) 191 | CoSEConstants.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR = FDLayoutConstants.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR = options.gravityRangeCompound; 192 | if (options.initialEnergyOnIncremental != null) 193 | CoSEConstants.DEFAULT_COOLING_FACTOR_INCREMENTAL = FDLayoutConstants.DEFAULT_COOLING_FACTOR_INCREMENTAL = options.initialEnergyOnIncremental; 194 | 195 | if (options.tilingCompareBy != null) 196 | CoSEConstants.TILING_COMPARE_BY = options.tilingCompareBy; 197 | 198 | if(options.quality == 'proof') 199 | LayoutConstants.QUALITY = 2; 200 | else 201 | LayoutConstants.QUALITY = 0; 202 | 203 | CoSEConstants.NODE_DIMENSIONS_INCLUDE_LABELS = FDLayoutConstants.NODE_DIMENSIONS_INCLUDE_LABELS = LayoutConstants.NODE_DIMENSIONS_INCLUDE_LABELS = options.nodeDimensionsIncludeLabels; 204 | CoSEConstants.DEFAULT_INCREMENTAL = FDLayoutConstants.DEFAULT_INCREMENTAL = LayoutConstants.DEFAULT_INCREMENTAL = 205 | !(options.randomize); 206 | CoSEConstants.ANIMATE = FDLayoutConstants.ANIMATE = LayoutConstants.ANIMATE = options.animate; 207 | CoSEConstants.TILE = options.tile; 208 | CoSEConstants.TILING_PADDING_VERTICAL = 209 | typeof options.tilingPaddingVertical === 'function' ? options.tilingPaddingVertical.call() : options.tilingPaddingVertical; 210 | CoSEConstants.TILING_PADDING_HORIZONTAL = 211 | typeof options.tilingPaddingHorizontal === 'function' ? options.tilingPaddingHorizontal.call() : options.tilingPaddingHorizontal; 212 | 213 | CoSEConstants.DEFAULT_INCREMENTAL = FDLayoutConstants.DEFAULT_INCREMENTAL = LayoutConstants.DEFAULT_INCREMENTAL = true; 214 | CoSEConstants.PURE_INCREMENTAL = !options.randomize; 215 | LayoutConstants.DEFAULT_UNIFORM_LEAF_NODE_SIZES = options.uniformNodeDimensions; 216 | 217 | // This part is for debug/demo purpose 218 | if(options.step == "transformed"){ 219 | CoSEConstants.TRANSFORM_ON_CONSTRAINT_HANDLING = true; 220 | CoSEConstants.ENFORCE_CONSTRAINTS = false; 221 | CoSEConstants.APPLY_LAYOUT = false; 222 | } 223 | if(options.step == "enforced"){ 224 | CoSEConstants.TRANSFORM_ON_CONSTRAINT_HANDLING = false; 225 | CoSEConstants.ENFORCE_CONSTRAINTS = true; 226 | CoSEConstants.APPLY_LAYOUT = false; 227 | } 228 | if(options.step == "cose"){ 229 | CoSEConstants.TRANSFORM_ON_CONSTRAINT_HANDLING = false; 230 | CoSEConstants.ENFORCE_CONSTRAINTS = false; 231 | CoSEConstants.APPLY_LAYOUT = true; 232 | } 233 | if(options.step == "all"){ 234 | if(options.randomize) 235 | CoSEConstants.TRANSFORM_ON_CONSTRAINT_HANDLING = true; 236 | else 237 | CoSEConstants.TRANSFORM_ON_CONSTRAINT_HANDLING = false; 238 | CoSEConstants.ENFORCE_CONSTRAINTS = true; 239 | CoSEConstants.APPLY_LAYOUT = true; 240 | } 241 | 242 | if(options.fixedNodeConstraint || options.alignmentConstraint || options.relativePlacementConstraint){ 243 | CoSEConstants.TREE_REDUCTION_ON_INCREMENTAL = false; 244 | } 245 | else{ 246 | CoSEConstants.TREE_REDUCTION_ON_INCREMENTAL = true; 247 | } 248 | 249 | let coseLayout = new CoSELayout(); 250 | let gm = coseLayout.newGraphManager(); 251 | 252 | processChildrenList(gm.addRoot(), aux.getTopMostNodes(nodes), coseLayout, options); 253 | processEdges(coseLayout, gm, edges); 254 | processConstraints(coseLayout, options); 255 | 256 | coseLayout.runLayout(); 257 | 258 | return idToLNode; 259 | }; 260 | 261 | module.exports = { coseLayout }; 262 | -------------------------------------------------------------------------------- /src/fcose/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | The implementation of the fcose layout algorithm 3 | */ 4 | 5 | const assign = require('../assign'); 6 | const aux = require('./auxiliary'); 7 | const { spectralLayout } = require('./spectral'); 8 | const { coseLayout } = require('./cose'); 9 | 10 | const defaults = Object.freeze({ 11 | 12 | // 'draft', 'default' or 'proof' 13 | // - 'draft' only applies spectral layout 14 | // - 'default' improves the quality with subsequent CoSE layout (fast cooling rate) 15 | // - 'proof' improves the quality with subsequent CoSE layout (slow cooling rate) 16 | quality: "default", 17 | // Use random node positions at beginning of layout 18 | // if this is set to false, then quality option must be "proof" 19 | randomize: true, 20 | // Whether or not to animate the layout 21 | animate: true, 22 | // Duration of animation in ms, if enabled 23 | animationDuration: 1000, 24 | // Easing of animation, if enabled 25 | animationEasing: undefined, 26 | // Fit the viewport to the repositioned nodes 27 | fit: true, 28 | // Padding around layout 29 | padding: 30, 30 | // Whether to include labels in node dimensions. Valid in "proof" quality 31 | nodeDimensionsIncludeLabels: false, 32 | // Whether or not simple nodes (non-compound nodes) are of uniform dimensions 33 | uniformNodeDimensions: false, 34 | // Whether to pack disconnected components - valid only if randomize: true 35 | packComponents: true, 36 | // Layout step - all, transformed, enforced, cose - for debug purpose only 37 | step: "all", 38 | 39 | /* spectral layout options */ 40 | 41 | // False for random, true for greedy 42 | samplingType: true, 43 | // Sample size to construct distance matrix 44 | sampleSize: 25, 45 | // Separation amount between nodes 46 | nodeSeparation: 75, 47 | // Power iteration tolerance 48 | piTol: 0.0000001, 49 | 50 | /* CoSE layout options */ 51 | 52 | // Node repulsion (non overlapping) multiplier 53 | nodeRepulsion: node => 4500, 54 | // Ideal edge (non nested) length 55 | idealEdgeLength: edge => 50, 56 | // Divisor to compute edge forces 57 | edgeElasticity: edge => 0.45, 58 | // Nesting factor (multiplier) to compute ideal edge length for nested edges 59 | nestingFactor: 0.1, 60 | // Gravity force (constant) 61 | gravity: 0.25, 62 | // Maximum number of iterations to perform 63 | numIter: 2500, 64 | // For enabling tiling 65 | tile: true, 66 | // The function that specifies the criteria for comparing nodes while sorting them during tiling operation. 67 | // Takes the node id as a parameter and the default tiling operation is perfomed when this option is not set. 68 | tilingCompareBy: undefined, 69 | // Represents the amount of the vertical space to put between the zero degree members during the tiling operation(can also be a function) 70 | tilingPaddingVertical: 10, 71 | // Represents the amount of the horizontal space to put between the zero degree members during the tiling operation(can also be a function) 72 | tilingPaddingHorizontal: 10, 73 | // Gravity range (constant) for compounds 74 | gravityRangeCompound: 1.5, 75 | // Gravity force (constant) for compounds 76 | gravityCompound: 1.0, 77 | // Gravity range (constant) 78 | gravityRange: 3.8, 79 | // Initial cooling factor for incremental layout 80 | initialEnergyOnIncremental: 0.3, 81 | 82 | /* constraint options */ 83 | 84 | // Fix required nodes to predefined positions 85 | // [{nodeId: 'n1', position: {x: 100, y: 200}, {...}] 86 | fixedNodeConstraint: undefined, 87 | // Align required nodes in vertical/horizontal direction 88 | // {vertical: [['n1', 'n2')], ['n3', 'n4']], horizontal: ['n2', 'n4']} 89 | alignmentConstraint: undefined, 90 | // Place two nodes relatively in vertical/horizontal direction 91 | // [{top: 'n1', bottom: 'n2', gap: 100}, {left: 'n3', right: 'n4', gap: 75}] 92 | relativePlacementConstraint: undefined, 93 | 94 | /* layout event callbacks */ 95 | ready: () => {}, // on layoutready 96 | stop: () => {} // on layoutstop 97 | }); 98 | 99 | class Layout { 100 | constructor( options ){ 101 | this.options = assign( {}, defaults, options ); 102 | } 103 | 104 | run(){ 105 | let layout = this; 106 | let options = this.options; 107 | let cy = options.cy; 108 | let eles = options.eles; 109 | 110 | let spectralResult = []; 111 | let xCoords; 112 | let yCoords; 113 | let coseResult = []; 114 | let components; 115 | let componentCenters = []; 116 | 117 | // basic validity check for constraint inputs 118 | if(options.fixedNodeConstraint && (!Array.isArray(options.fixedNodeConstraint) || options.fixedNodeConstraint.length == 0)){ 119 | options.fixedNodeConstraint = undefined; 120 | } 121 | 122 | if(options.alignmentConstraint){ 123 | if(options.alignmentConstraint.vertical && (!Array.isArray(options.alignmentConstraint.vertical) || options.alignmentConstraint.vertical.length == 0)){ 124 | options.alignmentConstraint.vertical = undefined; 125 | } 126 | if(options.alignmentConstraint.horizontal && (!Array.isArray(options.alignmentConstraint.horizontal) || options.alignmentConstraint.horizontal.length == 0)){ 127 | options.alignmentConstraint.horizontal = undefined; 128 | } 129 | 130 | } 131 | 132 | if(options.relativePlacementConstraint && (!Array.isArray(options.relativePlacementConstraint) || options.relativePlacementConstraint.length == 0)){ 133 | options.relativePlacementConstraint = undefined; 134 | } 135 | 136 | // if any constraint exists, set some options 137 | let constraintExist = options.fixedNodeConstraint || options.alignmentConstraint || options.relativePlacementConstraint; 138 | if(constraintExist){ 139 | // constraints work with these options 140 | options.tile = false; 141 | options.packComponents = false; 142 | } 143 | 144 | // decide component packing is enabled or not 145 | let layUtil; 146 | let packingEnabled = false; 147 | if(cy.layoutUtilities && options.packComponents){ 148 | layUtil = cy.layoutUtilities("get"); 149 | if(!layUtil) 150 | layUtil = cy.layoutUtilities(); 151 | packingEnabled = true; 152 | } 153 | 154 | if(eles.nodes().length > 0) { 155 | // if packing is not enabled, perform layout on the whole graph 156 | if(!packingEnabled){ 157 | // store component center 158 | let boundingBox = options.eles.boundingBox(); 159 | componentCenters.push({x: boundingBox.x1 + boundingBox.w / 2, y: boundingBox.y1 + boundingBox.h / 2}); 160 | // apply spectral layout 161 | if(options.randomize){ 162 | let result = spectralLayout(options); 163 | spectralResult.push(result); 164 | } 165 | // apply cose layout as postprocessing 166 | if(options.quality == "default" || options.quality == "proof"){ 167 | coseResult.push(coseLayout(options, spectralResult[0])); 168 | aux.relocateComponent(componentCenters[0], coseResult[0], options); // relocate center to original position 169 | } 170 | else{ 171 | aux.relocateComponent(componentCenters[0], spectralResult[0], options); // relocate center to original position 172 | } 173 | } 174 | else{ // packing is enabled 175 | let topMostNodes = aux.getTopMostNodes(options.eles.nodes()); 176 | components = aux.connectComponents(cy, options.eles, topMostNodes); 177 | // store component centers 178 | components.forEach(function(component){ 179 | let boundingBox = component.boundingBox(); 180 | componentCenters.push({x: boundingBox.x1 + boundingBox.w / 2, y: boundingBox.y1 + boundingBox.h / 2}); 181 | }); 182 | 183 | //send each component to spectral layout if randomized 184 | if(options.randomize){ 185 | components.forEach(function(component){ 186 | options.eles = component; 187 | spectralResult.push(spectralLayout(options)); 188 | }); 189 | } 190 | 191 | if(options.quality == "default" || options.quality == "proof"){ 192 | let toBeTiledNodes = cy.collection(); 193 | if(options.tile){ // behave nodes to be tiled as one component 194 | let nodeIndexes = new Map(); 195 | let xCoords = []; 196 | let yCoords = []; 197 | let count = 0; 198 | let tempSpectralResult = {nodeIndexes: nodeIndexes, xCoords: xCoords, yCoords: yCoords}; 199 | let indexesToBeDeleted = []; 200 | components.forEach(function(component, index){ 201 | if(component.edges().length == 0){ 202 | component.nodes().forEach(function(node, i){ 203 | toBeTiledNodes.merge(component.nodes()[i]); 204 | if(!node.isParent()){ 205 | tempSpectralResult.nodeIndexes.set(component.nodes()[i].id(), count++); 206 | tempSpectralResult.xCoords.push(component.nodes()[0].position().x); 207 | tempSpectralResult.yCoords.push(component.nodes()[0].position().y); 208 | } 209 | }); 210 | indexesToBeDeleted.push(index); 211 | } 212 | }); 213 | if(toBeTiledNodes.length > 1){ 214 | let boundingBox = toBeTiledNodes.boundingBox(); 215 | componentCenters.push({x: boundingBox.x1 + boundingBox.w / 2, y: boundingBox.y1 + boundingBox.h / 2}); 216 | components.push(toBeTiledNodes); 217 | spectralResult.push(tempSpectralResult); 218 | for(let i = indexesToBeDeleted.length-1; i >= 0; i--){ 219 | components.splice(indexesToBeDeleted[i], 1); 220 | spectralResult.splice(indexesToBeDeleted[i], 1); 221 | componentCenters.splice(indexesToBeDeleted[i], 1); 222 | }; 223 | } 224 | } 225 | components.forEach(function(component, index){ // send each component to cose layout 226 | options.eles = component; 227 | coseResult.push(coseLayout(options, spectralResult[index])); 228 | aux.relocateComponent(componentCenters[index], coseResult[index], options); // relocate center to original position 229 | }); 230 | } 231 | else { 232 | components.forEach(function(component, index){ 233 | aux.relocateComponent(componentCenters[index], spectralResult[index], options); // relocate center to original position 234 | }); 235 | } 236 | 237 | // packing 238 | let componentsEvaluated = new Set(); 239 | if(components.length > 1){ 240 | let subgraphs = []; 241 | let hiddenEles = eles.filter((ele) => {return ele.css('display') == 'none'}); 242 | components.forEach(function(component, index){ 243 | let nodeIndexes; 244 | if(options.quality == "draft"){ 245 | nodeIndexes = spectralResult[index].nodeIndexes; 246 | } 247 | 248 | if(component.nodes().not(hiddenEles).length > 0) { 249 | let subgraph = {}; 250 | subgraph.edges = []; 251 | subgraph.nodes = []; 252 | let nodeIndex; 253 | component.nodes().not(hiddenEles).forEach(function (node) { 254 | if(options.quality == "draft"){ 255 | if(!node.isParent()){ 256 | nodeIndex = nodeIndexes.get(node.id()); 257 | subgraph.nodes.push({x: spectralResult[index].xCoords[nodeIndex] - node.boundingbox().w/2, y: spectralResult[index].yCoords[nodeIndex] - node.boundingbox().h/2, width: node.boundingbox().w, height: node.boundingbox().h}); 258 | } 259 | else{ 260 | let parentInfo = aux.calcBoundingBox(node, spectralResult[index].xCoords, spectralResult[index].yCoords, nodeIndexes); 261 | subgraph.nodes.push({x: parentInfo.topLeftX, y: parentInfo.topLeftY, width: parentInfo.width, height: parentInfo.height}); 262 | } 263 | } 264 | else{ 265 | if(coseResult[index][node.id()]) { 266 | subgraph.nodes.push({x: coseResult[index][node.id()].getLeft(), y: coseResult[index][node.id()].getTop(), width: coseResult[index][node.id()].getWidth(), height: coseResult[index][node.id()].getHeight()}); 267 | } 268 | } 269 | }); 270 | component.edges().forEach(function (edge) { 271 | let source = edge.source(); 272 | let target = edge.target(); 273 | if(source.css("display") != "none" && target.css("display") != "none") { 274 | if(options.quality == "draft"){ 275 | let sourceNodeIndex = nodeIndexes.get(source.id()); 276 | let targetNodeIndex = nodeIndexes.get(target.id()); 277 | let sourceCenter = []; 278 | let targetCenter = []; 279 | if(source.isParent()){ 280 | let parentInfo = aux.calcBoundingBox(source, spectralResult[index].xCoords, spectralResult[index].yCoords, nodeIndexes); 281 | sourceCenter.push(parentInfo.topLeftX + parentInfo.width / 2); 282 | sourceCenter.push(parentInfo.topLeftY + parentInfo.height / 2); 283 | } 284 | else{ 285 | sourceCenter.push(spectralResult[index].xCoords[sourceNodeIndex]); 286 | sourceCenter.push(spectralResult[index].yCoords[sourceNodeIndex]); 287 | } 288 | if(target.isParent()){ 289 | let parentInfo = aux.calcBoundingBox(target, spectralResult[index].xCoords, spectralResult[index].yCoords, nodeIndexes); 290 | targetCenter.push(parentInfo.topLeftX + parentInfo.width / 2); 291 | targetCenter.push(parentInfo.topLeftY + parentInfo.height / 2); 292 | } 293 | else{ 294 | targetCenter.push(spectralResult[index].xCoords[targetNodeIndex]); 295 | targetCenter.push(spectralResult[index].yCoords[targetNodeIndex]); 296 | } 297 | subgraph.edges.push({startX: sourceCenter[0], startY: sourceCenter[1], endX: targetCenter[0], endY: targetCenter[1]}); 298 | } 299 | else{ 300 | if(coseResult[index][source.id()] && coseResult[index][target.id()]) { 301 | subgraph.edges.push({startX: coseResult[index][source.id()].getCenterX(), startY: coseResult[index][source.id()].getCenterY(), endX: coseResult[index][target.id()].getCenterX(), endY: coseResult[index][target.id()].getCenterY()}); 302 | } 303 | } 304 | } 305 | }); 306 | if(subgraph.nodes.length > 0) { 307 | subgraphs.push(subgraph); 308 | componentsEvaluated.add(index); 309 | } 310 | } 311 | }); 312 | let shiftResult = layUtil.packComponents(subgraphs, options.randomize).shifts; 313 | if(options.quality == "draft"){ 314 | spectralResult.forEach(function(result, index){ 315 | let newXCoords = result.xCoords.map(x => x + shiftResult[index].dx); 316 | let newYCoords = result.yCoords.map(y => y + shiftResult[index].dy); 317 | result.xCoords = newXCoords; 318 | result.yCoords = newYCoords; 319 | }); 320 | } 321 | else{ 322 | let count = 0; 323 | componentsEvaluated.forEach((index) => { 324 | Object.keys(coseResult[index]).forEach(function (item) { 325 | let nodeRectangle = coseResult[index][item]; 326 | nodeRectangle.setCenter(nodeRectangle.getCenterX() + shiftResult[count].dx, nodeRectangle.getCenterY() + shiftResult[count].dy); 327 | }); 328 | count++; 329 | }) 330 | } 331 | } 332 | } 333 | } 334 | 335 | // get each element's calculated position 336 | let getPositions = function(ele, i ){ 337 | if(options.quality == "default" || options.quality == "proof") { 338 | if(typeof ele === "number") { 339 | ele = i; 340 | } 341 | let pos; 342 | let node; 343 | let theId = ele.data('id'); 344 | coseResult.forEach(function(result){ 345 | if (theId in result){ 346 | pos = {x: result[theId].getRect().getCenterX(), y: result[theId].getRect().getCenterY()}; 347 | node = result[theId]; 348 | } 349 | }); 350 | if(options.nodeDimensionsIncludeLabels){ 351 | if(node.labelWidth){ 352 | if(node.labelPosHorizontal == "left"){ 353 | pos.x += node.labelWidth/2; 354 | } 355 | else if(node.labelPosHorizontal == "right"){ 356 | pos.x -= node.labelWidth/2; 357 | } 358 | } 359 | if(node.labelHeight){ 360 | if(node.labelPosVertical == "top"){ 361 | pos.y += node.labelHeight/2; 362 | } 363 | else if(node.labelPosVertical == "bottom"){ 364 | pos.y -= node.labelHeight/2; 365 | } 366 | } 367 | } 368 | if(pos == undefined) 369 | pos = {x: ele.position("x"), y: ele.position("y")}; 370 | return { 371 | x: pos.x, 372 | y: pos.y 373 | }; 374 | } 375 | else{ 376 | let pos; 377 | spectralResult.forEach(function(result){ 378 | let index = result.nodeIndexes.get(ele.id()); 379 | if(index != undefined){ 380 | pos = {x: result.xCoords[index], y: result.yCoords[index]}; 381 | } 382 | }); 383 | if(pos == undefined) 384 | pos = {x: ele.position("x"), y: ele.position("y")}; 385 | return { 386 | x: pos.x, 387 | y: pos.y 388 | }; 389 | } 390 | }; 391 | 392 | // quality = "draft" and randomize = false are contradictive so in that case positions don't change 393 | if(options.quality == "default" || options.quality == "proof" || options.randomize) { 394 | // transfer calculated positions to nodes (positions of only simple nodes are evaluated, compounds are positioned automatically) 395 | let parentsWithoutChildren = aux.calcParentsWithoutChildren(cy, eles); 396 | let hiddenEles = eles.filter((ele) => {return ele.css('display') == 'none'}); 397 | options.eles = eles.not(hiddenEles); 398 | 399 | eles.nodes().not(":parent").not(hiddenEles).layoutPositions(layout, options, getPositions); 400 | 401 | if(parentsWithoutChildren.length > 0){ 402 | parentsWithoutChildren.forEach((ele) => { 403 | ele.position(getPositions(ele)); 404 | }); 405 | } 406 | } 407 | else{ 408 | console.log("If randomize option is set to false, then quality option must be 'default' or 'proof'."); 409 | } 410 | 411 | } 412 | } 413 | 414 | module.exports = Layout; -------------------------------------------------------------------------------- /src/fcose/spectral.js: -------------------------------------------------------------------------------- 1 | /** 2 | The implementation of the spectral layout that is the first part of the fcose layout algorithm 3 | */ 4 | 5 | const aux = require('./auxiliary'); 6 | const Matrix = require('cose-base').layoutBase.Matrix; 7 | const SVD = require('cose-base').layoutBase.SVD; 8 | 9 | // main function that spectral layout is processed 10 | let spectralLayout = function(options){ 11 | 12 | let cy = options.cy; 13 | let eles = options.eles; 14 | let nodes = eles.nodes(); 15 | let parentNodes = eles.nodes(":parent"); 16 | 17 | let dummyNodes = new Map(); // map to keep dummy nodes and their neighbors 18 | let nodeIndexes = new Map(); // map to keep indexes to nodes 19 | let parentChildMap = new Map(); // mapping btw. compound and its representative node 20 | let allNodesNeighborhood = []; // array to keep neighborhood of all nodes 21 | let xCoords = []; 22 | let yCoords = []; 23 | 24 | let samplesColumn = []; // sampled vertices 25 | let minDistancesColumn = []; 26 | let C = []; // column sampling matrix 27 | let PHI = []; // intersection of column and row sampling matrices 28 | let INV = []; // inverse of PHI 29 | 30 | let firstSample; // the first sampled node 31 | let nodeSize; 32 | 33 | const infinity = 100000000; 34 | const small = 0.000000001; 35 | 36 | let piTol = options.piTol; 37 | let samplingType = options.samplingType; // false for random, true for greedy 38 | let nodeSeparation = options.nodeSeparation; 39 | let sampleSize; 40 | 41 | /**** Spectral-preprocessing functions ****/ 42 | 43 | /**** Spectral layout functions ****/ 44 | 45 | // determine which columns to be sampled 46 | let randomSampleCR = function() { 47 | let sample = 0; 48 | let count = 0; 49 | let flag = false; 50 | 51 | while(count < sampleSize){ 52 | sample = Math.floor(Math.random() * nodeSize); 53 | 54 | flag = false; 55 | for(let i = 0; i < count; i++){ 56 | if(samplesColumn[i] == sample){ 57 | flag = true; 58 | break; 59 | } 60 | } 61 | 62 | if(!flag){ 63 | samplesColumn[count] = sample; 64 | count++; 65 | } 66 | else{ 67 | continue; 68 | } 69 | } 70 | }; 71 | 72 | // takes the index of the node(pivot) to initiate BFS as a parameter 73 | let BFS = function(pivot, index, samplingMethod){ 74 | let path = []; // the front of the path 75 | let front = 0; // the back of the path 76 | let back = 0; 77 | let current = 0; 78 | let temp; 79 | let distance = []; 80 | 81 | let max_dist = 0; // the furthest node to be returned 82 | let max_ind = 1; 83 | 84 | for(let i = 0; i < nodeSize; i++){ 85 | distance[i] = infinity; 86 | } 87 | 88 | path[back] = pivot; 89 | distance[pivot] = 0; 90 | 91 | while(back >= front){ 92 | current = path[front++]; 93 | let neighbors = allNodesNeighborhood[current]; 94 | for(let i = 0; i < neighbors.length; i++){ 95 | temp = nodeIndexes.get(neighbors[i]); 96 | if(distance[temp] == infinity){ 97 | distance[temp] = distance[current] + 1; 98 | path[++back] = temp; 99 | } 100 | } 101 | C[current][index] = distance[current] * nodeSeparation; 102 | } 103 | 104 | if(samplingMethod){ 105 | for(let i = 0; i < nodeSize; i++){ 106 | if(C[i][index] < minDistancesColumn[i]) 107 | minDistancesColumn[i] = C[i][index]; 108 | } 109 | 110 | for(let i = 0; i < nodeSize; i++){ 111 | if(minDistancesColumn[i] > max_dist ){ 112 | max_dist = minDistancesColumn[i]; 113 | max_ind = i; 114 | 115 | } 116 | } 117 | } 118 | return max_ind; 119 | }; 120 | 121 | // apply BFS to all nodes or selected samples 122 | let allBFS = function(samplingMethod){ 123 | 124 | let sample; 125 | 126 | if(!samplingMethod){ 127 | randomSampleCR(); 128 | 129 | // call BFS 130 | for(let i = 0; i < sampleSize; i++){ 131 | BFS(samplesColumn[i], i, samplingMethod, false); 132 | } 133 | } 134 | else{ 135 | sample = Math.floor(Math.random() * nodeSize); 136 | firstSample = sample; 137 | 138 | for(let i = 0; i < nodeSize; i++){ 139 | minDistancesColumn[i] = infinity; 140 | } 141 | 142 | for(let i = 0; i < sampleSize; i++){ 143 | samplesColumn[i] = sample; 144 | sample = BFS(sample, i, samplingMethod); 145 | } 146 | 147 | } 148 | 149 | // form the squared distances for C 150 | for(let i = 0; i < nodeSize; i++){ 151 | for(let j = 0; j < sampleSize; j++){ 152 | C[i][j] *= C[i][j]; 153 | } 154 | } 155 | 156 | // form PHI 157 | for(let i = 0; i < sampleSize; i++){ 158 | PHI[i] = []; 159 | } 160 | 161 | for(let i = 0; i < sampleSize; i++){ 162 | for(let j = 0; j < sampleSize; j++){ 163 | PHI[i][j] = C[samplesColumn[j]][i]; 164 | } 165 | } 166 | }; 167 | 168 | // perform the SVD algorithm and apply a regularization step 169 | let sample = function(){ 170 | 171 | let SVDResult = SVD.svd(PHI); 172 | 173 | let a_q = SVDResult.S; 174 | let a_u = SVDResult.U; 175 | let a_v = SVDResult.V; 176 | 177 | let max_s = a_q[0]*a_q[0]*a_q[0]; 178 | 179 | let a_Sig = []; 180 | 181 | // regularization 182 | for(let i = 0; i < sampleSize; i++){ 183 | a_Sig[i] = []; 184 | for(let j = 0; j < sampleSize; j++){ 185 | a_Sig[i][j] = 0; 186 | if(i == j){ 187 | a_Sig[i][j] = a_q[i]/(a_q[i]*a_q[i] + max_s/(a_q[i]*a_q[i])); 188 | } 189 | } 190 | } 191 | 192 | INV = Matrix.multMat(Matrix.multMat(a_v, a_Sig), Matrix.transpose(a_u)); 193 | 194 | }; 195 | 196 | // calculate final coordinates 197 | let powerIteration = function(){ 198 | // two largest eigenvalues 199 | let theta1; 200 | let theta2; 201 | 202 | // initial guesses for eigenvectors 203 | let Y1 = []; 204 | let Y2 = []; 205 | 206 | let V1 = []; 207 | let V2 = []; 208 | 209 | for(let i = 0; i < nodeSize; i++){ 210 | Y1[i] = Math.random(); 211 | Y2[i] = Math.random(); 212 | } 213 | 214 | Y1 = Matrix.normalize(Y1); 215 | Y2 = Matrix.normalize(Y2); 216 | 217 | let count = 0; 218 | // to keep track of the improvement ratio in power iteration 219 | let current = small; 220 | let previous = small; 221 | 222 | let temp; 223 | 224 | while(true){ 225 | count++; 226 | 227 | for(let i = 0; i < nodeSize; i++){ 228 | V1[i] = Y1[i]; 229 | } 230 | 231 | Y1 = Matrix.multGamma(Matrix.multL(Matrix.multGamma(V1), C, INV)); 232 | theta1 = Matrix.dotProduct(V1, Y1); 233 | Y1 = Matrix.normalize(Y1); 234 | 235 | current = Matrix.dotProduct(V1, Y1); 236 | 237 | temp = Math.abs(current/previous); 238 | 239 | if(temp <= 1 + piTol && temp >= 1){ 240 | break; 241 | } 242 | 243 | previous = current; 244 | } 245 | 246 | for(let i = 0; i < nodeSize; i++){ 247 | V1[i] = Y1[i]; 248 | } 249 | 250 | count = 0; 251 | previous = small; 252 | while(true){ 253 | count++; 254 | 255 | for(let i = 0; i < nodeSize; i++){ 256 | V2[i] = Y2[i]; 257 | } 258 | 259 | V2 = Matrix.minusOp(V2, Matrix.multCons(V1, (Matrix.dotProduct(V1, V2)))); 260 | Y2 = Matrix.multGamma(Matrix.multL(Matrix.multGamma(V2), C, INV)); 261 | theta2 = Matrix.dotProduct(V2, Y2); 262 | Y2 = Matrix.normalize(Y2); 263 | 264 | current = Matrix.dotProduct(V2, Y2); 265 | 266 | temp = Math.abs(current/previous); 267 | 268 | if(temp <= 1 + piTol && temp >= 1){ 269 | break; 270 | } 271 | 272 | previous = current; 273 | } 274 | 275 | for(let i = 0; i < nodeSize; i++){ 276 | V2[i] = Y2[i]; 277 | } 278 | 279 | // theta1 now contains dominant eigenvalue 280 | // theta2 now contains the second-largest eigenvalue 281 | // V1 now contains theta1's eigenvector 282 | // V2 now contains theta2's eigenvector 283 | 284 | //populate the two vectors 285 | xCoords = Matrix.multCons(V1, Math.sqrt(Math.abs(theta1))); 286 | yCoords = Matrix.multCons(V2, Math.sqrt(Math.abs(theta2))); 287 | 288 | }; 289 | 290 | /**** Preparation for spectral layout (Preprocessing) ****/ 291 | 292 | // connect disconnected components (first top level, then inside of each compound node) 293 | aux.connectComponents(cy, eles, aux.getTopMostNodes(nodes), dummyNodes); 294 | 295 | parentNodes.forEach(function( ele ){ 296 | aux.connectComponents(cy, eles, aux.getTopMostNodes(ele.descendants().intersection(eles)), dummyNodes); 297 | }); 298 | 299 | // assign indexes to nodes (first real, then dummy nodes) 300 | let index = 0; 301 | for(let i = 0; i < nodes.length; i++){ 302 | if(!nodes[i].isParent()){ 303 | nodeIndexes.set(nodes[i].id(), index++); 304 | } 305 | } 306 | 307 | for (let key of dummyNodes.keys()) { 308 | nodeIndexes.set(key, index++); 309 | } 310 | 311 | // instantiate the neighborhood matrix 312 | for(let i = 0; i < nodeIndexes.size; i++){ 313 | allNodesNeighborhood[i] = []; 314 | } 315 | 316 | // form a parent-child map to keep representative node of each compound node 317 | parentNodes.forEach(function( ele ){ 318 | let children = ele.children().intersection(eles); 319 | 320 | // let random = 0; 321 | while(children.nodes(":childless").length == 0){ 322 | // random = Math.floor(Math.random() * children.nodes().length); // if all children are compound then proceed randomly 323 | children = children.nodes()[0].children().intersection(eles); 324 | } 325 | // select the representative node - we can apply different methods here 326 | // random = Math.floor(Math.random() * children.nodes(":childless").length); 327 | let index = 0; 328 | let min = children.nodes(":childless")[0].connectedEdges().length; 329 | children.nodes(":childless").forEach(function(ele2, i){ 330 | if(ele2.connectedEdges().length < min){ 331 | min = ele2.connectedEdges().length; 332 | index = i; 333 | } 334 | }); 335 | parentChildMap.set(ele.id(), children.nodes(":childless")[index].id()); 336 | }); 337 | 338 | // add neighborhood relations (first real, then dummy nodes) 339 | nodes.forEach(function( ele ){ 340 | let eleIndex; 341 | 342 | if(ele.isParent()) 343 | eleIndex = nodeIndexes.get(parentChildMap.get(ele.id())); 344 | else 345 | eleIndex = nodeIndexes.get(ele.id()); 346 | 347 | ele.neighborhood().nodes().forEach(function(node){ 348 | if(eles.intersection(ele.edgesWith(node)).length > 0){ 349 | if(node.isParent()) 350 | allNodesNeighborhood[eleIndex].push(parentChildMap.get(node.id())); 351 | else 352 | allNodesNeighborhood[eleIndex].push(node.id()); 353 | } 354 | }); 355 | }); 356 | 357 | for (let key of dummyNodes.keys()) { 358 | let eleIndex = nodeIndexes.get(key); 359 | let disconnectedId; 360 | dummyNodes.get(key).forEach(function(id){ 361 | if(cy.getElementById(id).isParent()) 362 | disconnectedId = parentChildMap.get(id); 363 | else 364 | disconnectedId = id; 365 | 366 | allNodesNeighborhood[eleIndex].push(disconnectedId); 367 | allNodesNeighborhood[nodeIndexes.get(disconnectedId)].push(key); 368 | }); 369 | } 370 | 371 | // nodeSize now only considers the size of transformed graph 372 | nodeSize = nodeIndexes.size; 373 | 374 | let spectralResult; 375 | 376 | // If number of nodes in transformed graph is 1 or 2, either SVD or powerIteration causes problem 377 | // So skip spectral and layout the graph with cose 378 | if(nodeSize > 2) { 379 | // if # of nodes in transformed graph is smaller than sample size, 380 | // then use # of nodes as sample size 381 | sampleSize = nodeSize < options.sampleSize ? nodeSize : options.sampleSize; 382 | 383 | // instantiates the partial matrices that will be used in spectral layout 384 | for(let i = 0; i < nodeSize; i++){ 385 | C[i] = []; 386 | } 387 | for(let i = 0; i < sampleSize; i++){ 388 | INV[i] = []; 389 | } 390 | 391 | /**** Apply spectral layout ****/ 392 | 393 | if(options.quality == "draft" || options.step == "all"){ 394 | allBFS(samplingType); 395 | sample(); 396 | powerIteration(); 397 | 398 | spectralResult = { nodeIndexes: nodeIndexes, xCoords: xCoords, yCoords: yCoords }; 399 | } 400 | else{ 401 | nodeIndexes.forEach(function(value, key){ 402 | xCoords.push(cy.getElementById(key).position("x")); 403 | yCoords.push(cy.getElementById(key).position("y")); 404 | }); 405 | spectralResult = { nodeIndexes: nodeIndexes, xCoords: xCoords, yCoords: yCoords }; 406 | } 407 | return spectralResult; 408 | } 409 | else { 410 | let iterator = nodeIndexes.keys(); 411 | let firstNode = cy.getElementById(iterator.next().value); 412 | let firstNodePos = firstNode.position(); 413 | let firstNodeWidth = firstNode.outerWidth(); 414 | xCoords.push(firstNodePos.x); 415 | yCoords.push(firstNodePos.y); 416 | if(nodeSize == 2){ 417 | let secondNode = cy.getElementById(iterator.next().value); 418 | let secondNodeWidth = secondNode.outerWidth(); 419 | xCoords.push(firstNodePos.x + firstNodeWidth / 2 + secondNodeWidth / 2 + options.idealEdgeLength); 420 | yCoords.push(firstNodePos.y); 421 | } 422 | 423 | spectralResult = { nodeIndexes: nodeIndexes, xCoords: xCoords, yCoords: yCoords }; 424 | return spectralResult; 425 | } 426 | }; 427 | 428 | module.exports = { spectralLayout }; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const impl = require('./fcose'); 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', 'fcose', 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 | -------------------------------------------------------------------------------- /test/example.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | 3 | describe('This', function(){ 4 | it('does that', function(){ 5 | expect( true ).to.be.true; 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /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 | globalObject: 'this' 20 | }, 21 | module: { 22 | rules: [ 23 | { test: /\.js$/, exclude: /node_modules/, use: 'babel-loader' } 24 | ] 25 | }, 26 | optimization: { 27 | minimize: MIN ? true : false, 28 | }, 29 | externals: PROD ? { 30 | 'cose-base': { 31 | commonjs2: 'cose-base', 32 | commonjs: 'cose-base', 33 | amd: 'cose-base', 34 | root: 'coseBase' 35 | } 36 | } : {} 37 | }; 38 | 39 | module.exports = config; 40 | --------------------------------------------------------------------------------