├── .npmignore ├── .gitignore ├── demo ├── icon-minus.png ├── icon-plus.png ├── style.css ├── demo-compounds-collapsed.html ├── demo.html ├── demo-undoable.html └── demo-compounds.js ├── expand-collapse-extension-demo.gif ├── expand-collapse-extension-edge-demo.gif ├── SECURITY.md ├── bower.json ├── src ├── boundingBoxUtilities.js ├── debounce2.js ├── elementUtilities.js ├── undoRedoUtilities.js ├── debounce.js ├── saveLoadUtilities.js ├── cueUtilities.js ├── index.js └── expandCollapseUtilities.js ├── webpack.config.js ├── LICENSE.md ├── package.json ├── CITATION.cff ├── README.md └── cytoscape-expand-collapse.js /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /nbproject/* 3 | /.idea/ 4 | -------------------------------------------------------------------------------- /demo/icon-minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iVis-at-Bilkent/cytoscape.js-expand-collapse/HEAD/demo/icon-minus.png -------------------------------------------------------------------------------- /demo/icon-plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iVis-at-Bilkent/cytoscape.js-expand-collapse/HEAD/demo/icon-plus.png -------------------------------------------------------------------------------- /expand-collapse-extension-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iVis-at-Bilkent/cytoscape.js-expand-collapse/HEAD/expand-collapse-extension-demo.gif -------------------------------------------------------------------------------- /expand-collapse-extension-edge-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iVis-at-Bilkent/cytoscape.js-expand-collapse/HEAD/expand-collapse-extension-edge-demo.gif -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 4.1.1 | :white_check_mark: | 8 | | 4.1.0 | :x: | 9 | | 4.0.x | :x: | 10 | | < 4.0.0 | :x: | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | To report a vulnerability, either open an issue or send an email to i-Vis Research Lab ivis@cs.bilkent.edu.tr. 15 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cytoscape-expand-collapse", 3 | "description": "This extension provides an interface to expand/collapse nodes.", 4 | "main": "cytoscape-expand-collapse.js", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/iVis-at-Bilkent/cytoscape.js-expand-collapse.git" 8 | }, 9 | "ignore": [ 10 | "**/.*", 11 | "node_modules", 12 | "bower_components", 13 | "test", 14 | "tests" 15 | ], 16 | "keywords": [ 17 | "cytoscape", 18 | "cyext" 19 | ], 20 | "license": "MIT" 21 | } 22 | -------------------------------------------------------------------------------- /src/boundingBoxUtilities.js: -------------------------------------------------------------------------------- 1 | var boundingBoxUtilities = { 2 | equalBoundingBoxes: function(bb1, bb2){ 3 | return bb1.x1 == bb2.x1 && bb1.x2 == bb2.x2 && bb1.y1 == bb2.y1 && bb1.y2 == bb2.y2; 4 | }, 5 | getUnion: function(bb1, bb2){ 6 | var union = { 7 | x1: Math.min(bb1.x1, bb2.x1), 8 | x2: Math.max(bb1.x2, bb2.x2), 9 | y1: Math.min(bb1.y1, bb2.y1), 10 | y2: Math.max(bb1.y2, bb2.y2), 11 | }; 12 | 13 | union.w = union.x2 - union.x1; 14 | union.h = union.y2 - union.y1; 15 | 16 | return union; 17 | } 18 | }; 19 | 20 | module.exports = boundingBoxUtilities; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path'); 3 | const pkg = require('./package.json'); 4 | const camelcase = require('camelcase'); 5 | const process = require('process'); 6 | const env = process.env; 7 | 8 | const NODE_ENV = env.NODE_ENV; 9 | const PROD = NODE_ENV === 'production'; 10 | const SRC_DIR = "./src"; 11 | 12 | module.exports = { 13 | entry: path.join(__dirname, SRC_DIR, '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 | mode: 'production', 22 | // devtool: PROD ? false : 'inline-source-map', 23 | optimization: { 24 | minimize: PROD ? true: false, 25 | }, 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.js$/, 30 | exclude: /node_modules/, 31 | use: 'babel-loader' 32 | } 33 | ] 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/debounce2.js: -------------------------------------------------------------------------------- 1 | var debounce2 = (function () { 2 | /** 3 | * Slightly modified version of debounce. Calls fn2 at the beginning of frequent calls to fn1 4 | * @static 5 | * @category Function 6 | * @param {Function} fn1 The function to debounce. 7 | * @param {number} [wait=0] The number of milliseconds to delay. 8 | * @param {Function} fn2 The function to call the beginning of frequent calls to fn1 9 | * @returns {Function} Returns the new debounced function. 10 | */ 11 | function debounce2(fn1, wait, fn2) { 12 | let timeout; 13 | let isInit = true; 14 | return function () { 15 | const context = this, args = arguments; 16 | const later = function () { 17 | timeout = null; 18 | fn1.apply(context, args); 19 | isInit = true; 20 | }; 21 | clearTimeout(timeout); 22 | timeout = setTimeout(later, wait); 23 | if (isInit) { 24 | fn2.apply(context, args); 25 | isInit = false; 26 | } 27 | }; 28 | } 29 | return debounce2; 30 | })(); 31 | 32 | module.exports = debounce2; -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 - present, iVis@Bilkent. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cytoscape-expand-collapse", 3 | "version": "4.1.1", 4 | "description": "This extension provides an interface to expand-collapse nodes.", 5 | "main": "cytoscape-expand-collapse.js", 6 | "spm": { 7 | "main": "cytoscape-expand-collapse.js" 8 | }, 9 | "scripts": { 10 | "build": "cross-env NODE_ENV=production webpack", 11 | "build:dev": "cross-env NODE_ENV=development webpack", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/iVis-at-Bilkent/cytoscape.js-expand-collapse.git" 17 | }, 18 | "keywords": [ 19 | "cytoscape", 20 | "cyext" 21 | ], 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/iVis-at-Bilkent/cytoscape.js-expand-collapse/issues" 25 | }, 26 | "homepage": "https://github.com/iVis-at-Bilkent/cytoscape.js-expand-collapse", 27 | "devDependencies": { 28 | "@babel/core": "^7.10.3", 29 | "@babel/preset-env": "^7.10.3", 30 | "babel-loader": "^8.1.0", 31 | "camelcase": "^6.2.0", 32 | "cross-env": "^7.0.2", 33 | "jshint-stylish": "^2.0.1", 34 | "node-notifier": "^4.3.1", 35 | "run-sequence": "^1.1.4", 36 | "vinyl-buffer": "^1.0.1", 37 | "vinyl-source-stream": "^1.1.2", 38 | "webpack": "^5.36.1", 39 | "webpack-cli": "^4.6.0" 40 | }, 41 | "peerDependencies": { 42 | "cytoscape": "^3.3.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /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: "Dogrusoz" 5 | given-names: "Ugur" 6 | orcid: "https://orcid.org/0000-0002-7153-0784" 7 | - family-names: "Karacelik" 8 | given-names: "Alper" 9 | orcid: "https://orcid.org/0000-0000-0000-0000" 10 | - family-names: "Safarli" 11 | given-names: "Ilkin" 12 | - family-names: "Balci" 13 | given-names: "Hasan" 14 | orcid: "https://orcid.org/0000-0001-8319-7758" 15 | - family-names: "Dervishi" 16 | given-names: "Leonard" 17 | - family-names: "Siper" 18 | given-names: "Metin Can" 19 | title: "cytoscape-expand-collapse" 20 | version: 4.1.0 21 | date-released: 2021-06-16 22 | url: "https://github.com/iVis-at-Bilkent/cytoscape.js-expand-collapse" 23 | preferred-citation: 24 | type: article 25 | authors: 26 | - family-names: "Dogrusoz" 27 | given-names: "Ugur" 28 | orcid: "https://orcid.org/0000-0002-7153-0784" 29 | - family-names: "Karacelik" 30 | given-names: "Alper" 31 | orcid: "https://orcid.org/0000-0000-0000-0000" 32 | - family-names: "Safarli" 33 | given-names: "Ilkin" 34 | - family-names: "Balci" 35 | given-names: "Hasan" 36 | orcid: "https://orcid.org/0000-0001-8319-7758" 37 | - family-names: "Dervishi" 38 | given-names: "Leonard" 39 | - family-names: "Siper" 40 | given-names: "Metin Can" 41 | doi: "10.1371/journal.pone.0197238" 42 | journal: "PLOS ONE" 43 | month: 5 44 | start: 1 # First page number 45 | end: 18 # Last page number 46 | title: "Efficient methods and readily customizable libraries for managing complexity of large networks" 47 | issue: 5 48 | volume: 13 49 | year: 2018 50 | -------------------------------------------------------------------------------- /src/elementUtilities.js: -------------------------------------------------------------------------------- 1 | function elementUtilities(cy) { 2 | return { 3 | moveNodes: function (positionDiff, nodes, notCalcTopMostNodes) { 4 | var topMostNodes = notCalcTopMostNodes ? nodes : this.getTopMostNodes(nodes); 5 | var nonParents = topMostNodes.not(":parent"); 6 | // moving parents spoils positioning, so move only nonparents 7 | nonParents.positions(function(ele, i){ 8 | return { 9 | x: nonParents[i].position("x") + positionDiff.x, 10 | y: nonParents[i].position("y") + positionDiff.y 11 | }; 12 | }); 13 | for (var i = 0; i < topMostNodes.length; i++) { 14 | var node = topMostNodes[i]; 15 | var children = node.children(); 16 | this.moveNodes(positionDiff, children, true); 17 | } 18 | }, 19 | getTopMostNodes: function (nodes) {//*// 20 | var nodesMap = {}; 21 | for (var i = 0; i < nodes.length; i++) { 22 | nodesMap[nodes[i].id()] = true; 23 | } 24 | var roots = nodes.filter(function (ele, i) { 25 | if(typeof ele === "number") { 26 | ele = i; 27 | } 28 | 29 | var parent = ele.parent()[0]; 30 | while (parent != null) { 31 | if (nodesMap[parent.id()]) { 32 | return false; 33 | } 34 | parent = parent.parent()[0]; 35 | } 36 | return true; 37 | }); 38 | 39 | return roots; 40 | }, 41 | rearrange: function (layoutBy) { 42 | if (typeof layoutBy === "function") { 43 | layoutBy(); 44 | } else if (layoutBy != null) { 45 | var layout = cy.layout(layoutBy); 46 | if (layout && layout.run) { 47 | layout.run(); 48 | } 49 | } 50 | }, 51 | convertToRenderedPosition: function (modelPosition) { 52 | var pan = cy.pan(); 53 | var zoom = cy.zoom(); 54 | 55 | var x = modelPosition.x * zoom + pan.x; 56 | var y = modelPosition.y * zoom + pan.y; 57 | 58 | return { 59 | x: x, 60 | y: y 61 | }; 62 | } 63 | }; 64 | } 65 | 66 | module.exports = elementUtilities; 67 | -------------------------------------------------------------------------------- /demo/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: helvetica neue, helvetica, liberation sans, arial, sans-serif; 3 | font-size: 14px; 4 | } 5 | 6 | #cy { 7 | z-index: 999; 8 | } 9 | 10 | h1 { 11 | opacity: 0.5; 12 | font-size: 1em; 13 | font-weight: bold; 14 | } 15 | 16 | .wrap { 17 | width: 100%; 18 | overflow: auto; 19 | } 20 | 21 | .fleft { 22 | float: left; 23 | width: 20%; 24 | height: 95%; 25 | } 26 | 27 | .menu-container { 28 | background-color: #f8f9fa; 29 | margin: 2px; 30 | } 31 | 32 | .fright { 33 | float: right; 34 | height: 95%; 35 | width: calc(80% - 4px); 36 | /* count border wid*/ 37 | border: 2px solid #2F4154; 38 | border-radius: 6px; 39 | } 40 | 41 | .list-item { 42 | margin-left: 10px; 43 | margin-top: 10px; 44 | } 45 | 46 | button { 47 | position: relative; 48 | display: inline-block; 49 | height: 36px; 50 | padding: 0 8px; 51 | margin: 6px 2px; 52 | background-color: #ffffff; 53 | color: #3f51b5; 54 | border: 0; 55 | border-radius: 2px; 56 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12); 57 | text-decoration: none; 58 | font-size: 14px; 59 | font-weight: 500; 60 | line-height: 36px; 61 | vertical-align: middle; 62 | overflow: hidden; 63 | cursor: pointer; 64 | outline: 0; 65 | z-index: 1; 66 | -webkit-transition: all .15s ease-in; 67 | transition: all .15s ease-in; 68 | } 69 | 70 | button:hover, 71 | button:focus { 72 | box-shadow: 0 4px 2px 0 rgba(0, 0, 0, 0.14), 0 6px 1px -2px rgba(0, 0, 0, 0.2), 0 6px 5px 0 rgba(0, 0, 0, 0.12); 73 | } 74 | 75 | button:before { 76 | content: ''; 77 | position: absolute; 78 | top: 50%; 79 | left: 50%; 80 | width: 0; 81 | height: 0; 82 | -webkit-transform: translate(-50%, -50%); 83 | transform: translate(-50%, -50%); 84 | border-radius: 50%; 85 | background-color: currentColor; 86 | visibility: hidden; 87 | z-index: 2; 88 | } 89 | 90 | button:not(:active):before { 91 | -webkit-animation: rippleanim 0.4s cubic-bezier(0, 0, 0.2, 1); 92 | animation: rippleanim 0.4s cubic-bezier(0, 0, 0.2, 1); 93 | -webkit-transition: visibility .4s step-end; 94 | transition: visibility .4s step-end; 95 | } 96 | 97 | button:active:before { 98 | visibility: visible; 99 | } 100 | 101 | button:disabled { 102 | cursor: not-allowed; 103 | opacity: .7; 104 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12); 105 | } 106 | 107 | label { 108 | line-height: 36px; 109 | } 110 | 111 | input[type='checkbox'] { 112 | vertical-align: middle; 113 | } 114 | 115 | @-webkit-keyframes rippleanim { 116 | 0% { 117 | width: 0; 118 | height: 0; 119 | opacity: .5; 120 | } 121 | 122 | 100% { 123 | width: 150px; 124 | height: 150px; 125 | opacity: 0; 126 | } 127 | } 128 | 129 | @keyframes rippleanim { 130 | 0% { 131 | width: 0; 132 | height: 0; 133 | opacity: .5; 134 | } 135 | 136 | 100% { 137 | width: 150px; 138 | height: 150px; 139 | opacity: 0; 140 | } 141 | } 142 | 143 | .accordion { 144 | background-color: #eee; 145 | color: #444; 146 | cursor: pointer; 147 | width: 100%; 148 | border: none; 149 | text-align: left; 150 | outline: none; 151 | font-size: 15px; 152 | transition: 0.4s; 153 | padding: 4px; 154 | } 155 | 156 | .active, 157 | .accordion:hover { 158 | background-color: #ccc; 159 | } 160 | 161 | .accordion:after { 162 | content: '\002B'; 163 | color: #777; 164 | font-weight: bold; 165 | float: right; 166 | margin-left: 5px; 167 | } 168 | 169 | .active:after { 170 | content: "\2212"; 171 | } 172 | 173 | .panel { 174 | background-color: white; 175 | max-height: 0; 176 | overflow: hidden; 177 | transition: max-height 0.2s ease-out; 178 | } 179 | 180 | .accordion-container { 181 | padding: 1px 12px 1px 0px; 182 | } -------------------------------------------------------------------------------- /demo/demo-compounds-collapsed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cytoscape-expand-collapse.js demo 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |

cytoscape-expand-collapse demo

26 |
27 | 95 |
96 | 97 |
98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /src/undoRedoUtilities.js: -------------------------------------------------------------------------------- 1 | module.exports = function (cy, api) { 2 | if (cy.undoRedo == null) 3 | return; 4 | 5 | var ur = cy.undoRedo({}, true); 6 | 7 | function getEles(_eles) { 8 | return (typeof _eles === "string") ? cy.$(_eles) : _eles; 9 | } 10 | 11 | function getNodePositions() { 12 | var positions = {}; 13 | var nodes = cy.nodes(); 14 | 15 | for (var i = 0; i < nodes.length; i++) { 16 | var ele = nodes[i]; 17 | positions[ele.id()] = { 18 | x: ele.position("x"), 19 | y: ele.position("y") 20 | }; 21 | } 22 | 23 | return positions; 24 | } 25 | 26 | function returnToPositions(positions) { 27 | var currentPositions = {}; 28 | cy.nodes().not(":parent").positions(function (ele, i) { 29 | if(typeof ele === "number") { 30 | ele = i; 31 | } 32 | currentPositions[ele.id()] = { 33 | x: ele.position("x"), 34 | y: ele.position("y") 35 | }; 36 | var pos = positions[ele.id()]; 37 | return { 38 | x: pos.x, 39 | y: pos.y 40 | }; 41 | }); 42 | 43 | return currentPositions; 44 | } 45 | 46 | var secondTimeOpts = { 47 | layoutBy: null, 48 | animate: false, 49 | fisheye: false 50 | }; 51 | 52 | function doIt(func) { 53 | return function (args) { 54 | var result = {}; 55 | var nodes = getEles(args.nodes); 56 | if (args.firstTime) { 57 | result.oldData = getNodePositions(); 58 | result.nodes = func.indexOf("All") > 0 ? api[func](args.options) : api[func](nodes, args.options); 59 | } else { 60 | result.oldData = getNodePositions(); 61 | result.nodes = func.indexOf("All") > 0 ? api[func](secondTimeOpts) : api[func](cy.collection(nodes), secondTimeOpts); 62 | returnToPositions(args.oldData); 63 | } 64 | 65 | return result; 66 | }; 67 | } 68 | 69 | var actions = ["collapse", "collapseRecursively", "collapseAll", "expand", "expandRecursively", "expandAll"]; 70 | 71 | for (var i = 0; i < actions.length; i++) { 72 | if(i == 2) 73 | ur.action("collapseAll", doIt("collapseAll"), doIt("expandRecursively")); 74 | else if(i == 5) 75 | ur.action("expandAll", doIt("expandAll"), doIt("collapseRecursively")); 76 | else 77 | ur.action(actions[i], doIt(actions[i]), doIt(actions[(i + 3) % 6])); 78 | } 79 | 80 | function collapseEdges(args){ 81 | var options = args.options; 82 | var edges = args.edges; 83 | var result = {}; 84 | 85 | result.options = options; 86 | if(args.firstTime){ 87 | var collapseResult = api.collapseEdges(edges,options); 88 | result.edges = collapseResult.edges; 89 | result.oldEdges = collapseResult.oldEdges; 90 | result.firstTime = false; 91 | }else{ 92 | result.oldEdges = edges; 93 | result.edges = args.oldEdges; 94 | if(args.edges.length > 0 && args.oldEdges.length > 0){ 95 | cy.remove(args.edges); 96 | cy.add(args.oldEdges); 97 | } 98 | 99 | 100 | } 101 | 102 | return result; 103 | } 104 | function collapseEdgesBetweenNodes(args){ 105 | var options = args.options; 106 | var result = {}; 107 | result.options = options; 108 | if(args.firstTime){ 109 | var collapseAllResult = api.collapseEdgesBetweenNodes(args.nodes, options); 110 | result.edges = collapseAllResult.edges; 111 | result.oldEdges = collapseAllResult.oldEdges; 112 | result.firstTime = false; 113 | }else{ 114 | result.edges = args.oldEdges; 115 | result.oldEdges = args.edges; 116 | if(args.edges.length > 0 && args.oldEdges.length > 0){ 117 | cy.remove(args.edges); 118 | cy.add(args.oldEdges); 119 | } 120 | 121 | } 122 | 123 | return result; 124 | 125 | } 126 | function collapseAllEdges(args){ 127 | var options = args.options; 128 | var result = {}; 129 | result.options = options; 130 | if(args.firstTime){ 131 | var collapseAllResult = api.collapseAllEdges(options); 132 | result.edges = collapseAllResult.edges; 133 | result.oldEdges = collapseAllResult.oldEdges; 134 | result.firstTime = false; 135 | }else{ 136 | result.edges = args.oldEdges; 137 | result.oldEdges = args.edges; 138 | if(args.edges.length > 0 && args.oldEdges.length > 0){ 139 | cy.remove(args.edges); 140 | cy.add(args.oldEdges); 141 | } 142 | 143 | } 144 | 145 | return result; 146 | } 147 | function expandEdges(args){ 148 | var options = args.options; 149 | var result ={}; 150 | 151 | result.options = options; 152 | if(args.firstTime){ 153 | var expandResult = api.expandEdges(args.edges); 154 | result.edges = expandResult.edges; 155 | result.oldEdges = expandResult.oldEdges; 156 | result.firstTime = false; 157 | 158 | }else{ 159 | result.oldEdges = args.edges; 160 | result.edges = args.oldEdges; 161 | if(args.edges.length > 0 && args.oldEdges.length > 0){ 162 | cy.remove(args.edges); 163 | cy.add(args.oldEdges); 164 | } 165 | 166 | } 167 | 168 | return result; 169 | } 170 | function expandEdgesBetweenNodes(args){ 171 | var options = args.options; 172 | var result = {}; 173 | result.options = options; 174 | if(args.firstTime){ 175 | var collapseAllResult = api.expandEdgesBetweenNodes(args.nodes,options); 176 | result.edges = collapseAllResult.edges; 177 | result.oldEdges = collapseAllResult.oldEdges; 178 | result.firstTime = false; 179 | }else{ 180 | result.edges = args.oldEdges; 181 | result.oldEdges = args.edges; 182 | if(args.edges.length > 0 && args.oldEdges.length > 0){ 183 | cy.remove(args.edges); 184 | cy.add(args.oldEdges); 185 | } 186 | 187 | } 188 | 189 | return result; 190 | } 191 | function expandAllEdges(args){ 192 | var options = args.options; 193 | var result = {}; 194 | result.options = options; 195 | if(args.firstTime){ 196 | var expandResult = api.expandAllEdges(options); 197 | result.edges = expandResult.edges; 198 | result.oldEdges = expandResult.oldEdges; 199 | result.firstTime = false; 200 | }else{ 201 | result.edges = args.oldEdges; 202 | result.oldEdges = args.edges; 203 | if(args.edges.length > 0 && args.oldEdges.length > 0){ 204 | cy.remove(args.edges); 205 | cy.add(args.oldEdges); 206 | } 207 | 208 | } 209 | 210 | return result; 211 | } 212 | 213 | 214 | ur.action("collapseEdges", collapseEdges, expandEdges); 215 | ur.action("expandEdges", expandEdges, collapseEdges); 216 | 217 | ur.action("collapseEdgesBetweenNodes", collapseEdgesBetweenNodes, expandEdgesBetweenNodes); 218 | ur.action("expandEdgesBetweenNodes", expandEdgesBetweenNodes, collapseEdgesBetweenNodes); 219 | 220 | ur.action("collapseAllEdges", collapseAllEdges, expandAllEdges); 221 | ur.action("expandAllEdges", expandAllEdges, collapseAllEdges); 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | }; 230 | -------------------------------------------------------------------------------- /src/debounce.js: -------------------------------------------------------------------------------- 1 | var debounce = (function () { 2 | /** 3 | * lodash 3.1.1 (Custom Build) 4 | * Build: `lodash modern modularize exports="npm" -o ./` 5 | * Copyright 2012-2015 The Dojo Foundation 6 | * Based on Underscore.js 1.8.3 7 | * Copyright 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 8 | * Available under MIT license 9 | */ 10 | /** Used as the `TypeError` message for "Functions" methods. */ 11 | var FUNC_ERROR_TEXT = 'Expected a function'; 12 | 13 | /* Native method references for those with the same name as other `lodash` methods. */ 14 | var nativeMax = Math.max, 15 | nativeNow = Date.now; 16 | 17 | /** 18 | * Gets the number of milliseconds that have elapsed since the Unix epoch 19 | * (1 January 1970 00:00:00 UTC). 20 | * 21 | * @static 22 | * @memberOf _ 23 | * @category Date 24 | * @example 25 | * 26 | * _.defer(function(stamp) { 27 | * console.log(_.now() - stamp); 28 | * }, _.now()); 29 | * // => logs the number of milliseconds it took for the deferred function to be invoked 30 | */ 31 | var now = nativeNow || function () { 32 | return new Date().getTime(); 33 | }; 34 | 35 | /** 36 | * Creates a debounced function that delays invoking `func` until after `wait` 37 | * milliseconds have elapsed since the last time the debounced function was 38 | * invoked. The debounced function comes with a `cancel` method to cancel 39 | * delayed invocations. Provide an options object to indicate that `func` 40 | * should be invoked on the leading and/or trailing edge of the `wait` timeout. 41 | * Subsequent calls to the debounced function return the result of the last 42 | * `func` invocation. 43 | * 44 | * **Note:** If `leading` and `trailing` options are `true`, `func` is invoked 45 | * on the trailing edge of the timeout only if the the debounced function is 46 | * invoked more than once during the `wait` timeout. 47 | * 48 | * See [David Corbacho's article](http://drupalmotion.com/article/debounce-and-throttle-visual-explanation) 49 | * for details over the differences between `_.debounce` and `_.throttle`. 50 | * 51 | * @static 52 | * @memberOf _ 53 | * @category Function 54 | * @param {Function} func The function to debounce. 55 | * @param {number} [wait=0] The number of milliseconds to delay. 56 | * @param {Object} [options] The options object. 57 | * @param {boolean} [options.leading=false] Specify invoking on the leading 58 | * edge of the timeout. 59 | * @param {number} [options.maxWait] The maximum time `func` is allowed to be 60 | * delayed before it's invoked. 61 | * @param {boolean} [options.trailing=true] Specify invoking on the trailing 62 | * edge of the timeout. 63 | * @returns {Function} Returns the new debounced function. 64 | * @example 65 | * 66 | * // avoid costly calculations while the window size is in flux 67 | * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); 68 | * 69 | * // invoke `sendMail` when the click event is fired, debouncing subsequent calls 70 | * jQuery('#postbox').on('click', _.debounce(sendMail, 300, { 71 | * 'leading': true, 72 | * 'trailing': false 73 | * })); 74 | * 75 | * // ensure `batchLog` is invoked once after 1 second of debounced calls 76 | * var source = new EventSource('/stream'); 77 | * jQuery(source).on('message', _.debounce(batchLog, 250, { 78 | * 'maxWait': 1000 79 | * })); 80 | * 81 | * // cancel a debounced call 82 | * var todoChanges = _.debounce(batchLog, 1000); 83 | * Object.observe(models.todo, todoChanges); 84 | * 85 | * Object.observe(models, function(changes) { 86 | * if (_.find(changes, { 'user': 'todo', 'type': 'delete'})) { 87 | * todoChanges.cancel(); 88 | * } 89 | * }, ['delete']); 90 | * 91 | * // ...at some point `models.todo` is changed 92 | * models.todo.completed = true; 93 | * 94 | * // ...before 1 second has passed `models.todo` is deleted 95 | * // which cancels the debounced `todoChanges` call 96 | * delete models.todo; 97 | */ 98 | function debounce(func, wait, options) { 99 | var args, 100 | maxTimeoutId, 101 | result, 102 | stamp, 103 | thisArg, 104 | timeoutId, 105 | trailingCall, 106 | lastCalled = 0, 107 | maxWait = false, 108 | trailing = true; 109 | 110 | if (typeof func != 'function') { 111 | throw new TypeError(FUNC_ERROR_TEXT); 112 | } 113 | wait = wait < 0 ? 0 : (+wait || 0); 114 | if (options === true) { 115 | var leading = true; 116 | trailing = false; 117 | } else if (isObject(options)) { 118 | leading = !!options.leading; 119 | maxWait = 'maxWait' in options && nativeMax(+options.maxWait || 0, wait); 120 | trailing = 'trailing' in options ? !!options.trailing : trailing; 121 | } 122 | 123 | function cancel() { 124 | if (timeoutId) { 125 | clearTimeout(timeoutId); 126 | } 127 | if (maxTimeoutId) { 128 | clearTimeout(maxTimeoutId); 129 | } 130 | lastCalled = 0; 131 | maxTimeoutId = timeoutId = trailingCall = undefined; 132 | } 133 | 134 | function complete(isCalled, id) { 135 | if (id) { 136 | clearTimeout(id); 137 | } 138 | maxTimeoutId = timeoutId = trailingCall = undefined; 139 | if (isCalled) { 140 | lastCalled = now(); 141 | result = func.apply(thisArg, args); 142 | if (!timeoutId && !maxTimeoutId) { 143 | args = thisArg = undefined; 144 | } 145 | } 146 | } 147 | 148 | function delayed() { 149 | var remaining = wait - (now() - stamp); 150 | if (remaining <= 0 || remaining > wait) { 151 | complete(trailingCall, maxTimeoutId); 152 | } else { 153 | timeoutId = setTimeout(delayed, remaining); 154 | } 155 | } 156 | 157 | function maxDelayed() { 158 | complete(trailing, timeoutId); 159 | } 160 | 161 | function debounced() { 162 | args = arguments; 163 | stamp = now(); 164 | thisArg = this; 165 | trailingCall = trailing && (timeoutId || !leading); 166 | 167 | if (maxWait === false) { 168 | var leadingCall = leading && !timeoutId; 169 | } else { 170 | if (!maxTimeoutId && !leading) { 171 | lastCalled = stamp; 172 | } 173 | var remaining = maxWait - (stamp - lastCalled), 174 | isCalled = remaining <= 0 || remaining > maxWait; 175 | 176 | if (isCalled) { 177 | if (maxTimeoutId) { 178 | maxTimeoutId = clearTimeout(maxTimeoutId); 179 | } 180 | lastCalled = stamp; 181 | result = func.apply(thisArg, args); 182 | } 183 | else if (!maxTimeoutId) { 184 | maxTimeoutId = setTimeout(maxDelayed, remaining); 185 | } 186 | } 187 | if (isCalled && timeoutId) { 188 | timeoutId = clearTimeout(timeoutId); 189 | } 190 | else if (!timeoutId && wait !== maxWait) { 191 | timeoutId = setTimeout(delayed, wait); 192 | } 193 | if (leadingCall) { 194 | isCalled = true; 195 | result = func.apply(thisArg, args); 196 | } 197 | if (isCalled && !timeoutId && !maxTimeoutId) { 198 | args = thisArg = undefined; 199 | } 200 | return result; 201 | } 202 | 203 | debounced.cancel = cancel; 204 | return debounced; 205 | } 206 | 207 | /** 208 | * Checks if `value` is the [language type](https://es5.github.io/#x8) of `Object`. 209 | * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) 210 | * 211 | * @static 212 | * @memberOf _ 213 | * @category Lang 214 | * @param {*} value The value to check. 215 | * @returns {boolean} Returns `true` if `value` is an object, else `false`. 216 | * @example 217 | * 218 | * _.isObject({}); 219 | * // => true 220 | * 221 | * _.isObject([1, 2, 3]); 222 | * // => true 223 | * 224 | * _.isObject(1); 225 | * // => false 226 | */ 227 | function isObject(value) { 228 | // Avoid a V8 JIT bug in Chrome 19-20. 229 | // See https://code.google.com/p/v8/issues/detail?id=2291 for more details. 230 | var type = typeof value; 231 | return !!value && (type == 'object' || type == 'function'); 232 | } 233 | 234 | return debounce; 235 | 236 | })(); 237 | 238 | module.exports = debounce; -------------------------------------------------------------------------------- /src/saveLoadUtilities.js: -------------------------------------------------------------------------------- 1 | function saveLoadUtilities(cy, api) { 2 | /** converts array of JSON to a cytoscape.js collection (bottom-up recursive) 3 | * keeps information about parents, all nodes added to cytoscape, and nodes to be collapsed 4 | * @param {} jsonArr an array of objects (a JSON array) 5 | * @param {} allNodes a cytoscape.js collection 6 | * @param {} nodes2collapse a cytoscape.js collection 7 | * @param {} node2parent a JS object (simply key-value pairs) 8 | */ 9 | function json2cyCollection(jsonArr, allNodes, nodes2collapse, node2parent) { 10 | // process edges last since they depend on nodes 11 | jsonArr.sort((a) => { 12 | if (a.group === 'edges') { 13 | return 1; 14 | } 15 | return -1; 16 | }); 17 | 18 | // add compound nodes first, then add other nodes then edges 19 | let coll = cy.collection(); 20 | for (let i = 0; i < jsonArr.length; i++) { 21 | const json = jsonArr[i]; 22 | const d = json.data; 23 | if (d.parent) { 24 | node2parent[d.id] = d.parent; 25 | } 26 | const pos = { x: json.position.x, y: json.position.y }; 27 | const e = cy.add(json); 28 | if (e.isNode()) { 29 | allNodes.merge(e); 30 | } 31 | 32 | if (d.originalEnds) { 33 | // all nodes should be in the memory (in cy or not) 34 | let src = allNodes.$id(d.originalEnds.source.data.id); 35 | if (d.originalEnds.source.data.parent) { 36 | node2parent[d.originalEnds.source.data.id] = d.originalEnds.source.data.parent; 37 | } 38 | let tgt = allNodes.$id(d.originalEnds.target.data.id); 39 | if (d.originalEnds.target.data.parent) { 40 | node2parent[d.originalEnds.target.data.id] = d.originalEnds.target.data.parent; 41 | } 42 | e.data('originalEnds', { source: src, target: tgt }); 43 | } 44 | if (d.collapsedChildren) { 45 | nodes2collapse.merge(e); 46 | json2cyCollection(d.collapsedChildren, allNodes, nodes2collapse, node2parent); 47 | clearCollapseMetaData(e); 48 | } else if (d.collapsedEdges) { 49 | e.data('collapsedEdges', json2cyCollection(d.collapsedEdges, allNodes, nodes2collapse, node2parent)); 50 | // delete collapsed edges from cy 51 | cy.remove(e.data('collapsedEdges')); 52 | } 53 | e.position(pos); // adding new elements to a compound might change its position 54 | coll.merge(e); 55 | } 56 | return coll; 57 | } 58 | 59 | /** clears all the data related to collapsed node 60 | * @param {} e a cytoscape element 61 | */ 62 | function clearCollapseMetaData(e) { 63 | e.data('collapsedChildren', null); 64 | e.removeClass('cy-expand-collapse-collapsed-node'); 65 | e.data('position-before-collapse', null); 66 | e.data('size-before-collapse', null); 67 | e.data('expandcollapseRenderedStartX', null); 68 | e.data('expandcollapseRenderedStartY', null); 69 | e.data('expandcollapseRenderedCueSize', null); 70 | } 71 | 72 | /** converts cytoscape collection to JSON array.(bottom-up recursive) 73 | * @param {} elems 74 | */ 75 | function cyCollection2Json(elems) { 76 | let r = []; 77 | for (let i = 0; i < elems.length; i++) { 78 | const elem = elems[i]; 79 | let jsonObj = null; 80 | if (!elem.collapsedChildren && !elem.collapsedEdges) { 81 | jsonObj = elem.cy.json(); 82 | } 83 | else if (elem.collapsedChildren) { 84 | elem.collapsedChildren = cyCollection2Json(halfDeepCopyCollection(elem.collapsedChildren)); 85 | jsonObj = elem.cy.json(); 86 | jsonObj.data.collapsedChildren = elem.collapsedChildren; 87 | } else if (elem.collapsedEdges) { 88 | elem.collapsedEdges = cyCollection2Json(halfDeepCopyCollection(elem.collapsedEdges)); 89 | jsonObj = elem.cy.json(); 90 | jsonObj.data.collapsedEdges = elem.collapsedEdges; 91 | } 92 | if (elem.originalEnds) { 93 | const src = elem.originalEnds.source.json(); 94 | const tgt = elem.originalEnds.target.json(); 95 | if (src.data.collapsedChildren) { 96 | src.data.collapsedChildren = cyCollection2Json(halfDeepCopyCollection(src.data.collapsedChildren)); 97 | } 98 | if (tgt.data.collapsedChildren) { 99 | tgt.data.collapsedChildren = cyCollection2Json(halfDeepCopyCollection(tgt.data.collapsedChildren)); 100 | } 101 | jsonObj.data.originalEnds = { source: src, target: tgt }; 102 | } 103 | r.push(jsonObj); 104 | } 105 | return r; 106 | } 107 | 108 | /** returns { cy: any, collapsedEdges: any, collapsedChildren: any, originalEnds: any }[] 109 | * from cytoscape collection 110 | * @param {} col 111 | */ 112 | function halfDeepCopyCollection(col) { 113 | let arr = []; 114 | for (let i = 0; i < col.length; i++) { 115 | arr.push({ cy: col[i], collapsedEdges: col[i].data('collapsedEdges'), collapsedChildren: col[i].data('collapsedChildren'), originalEnds: col[i].data('originalEnds') }); 116 | } 117 | return arr; 118 | } 119 | 120 | /** saves the string as a file. 121 | * @param {} str string 122 | * @param {} fileName string 123 | */ 124 | function str2file(str, fileName) { 125 | const blob = new Blob([str], { type: 'text/plain' }); 126 | const anchor = document.createElement('a'); 127 | 128 | anchor.download = fileName; 129 | anchor.href = (window.URL).createObjectURL(blob); 130 | anchor.dataset.downloadurl = 131 | ['text/plain', anchor.download, anchor.href].join(':'); 132 | anchor.click(); 133 | } 134 | 135 | function overrideJson2Elem(elem, json) { 136 | const collapsedChildren = elem.data('collapsedChildren'); 137 | const collapsedEdges = elem.data('collapsedEdges'); 138 | const originalEnds = elem.data('originalEnds'); 139 | elem.json(json); 140 | if (collapsedChildren) { 141 | elem.data('collapsedChildren', collapsedChildren); 142 | } 143 | if (collapsedEdges) { 144 | elem.data('collapsedEdges', collapsedEdges); 145 | } 146 | if (originalEnds) { 147 | elem.data('originalEnds', originalEnds); 148 | } 149 | } 150 | 151 | return { 152 | 153 | /** Load elements from JSON formatted string representation. 154 | * For collapsed compounds, first add all collapsed nodes as normal nodes then collapse them. Then reposition them. 155 | * For collapsed edges, first add all of the edges then remove collapsed edges from cytoscape. 156 | * For original ends, restore their reference to cytoscape elements 157 | * @param {} txt string 158 | */ 159 | loadJson: function (txt) { 160 | const fileJSON = JSON.parse(txt); 161 | // original endpoints won't exist in cy. So keep a reference. 162 | const nodePositions = {}; 163 | const allNodes = cy.collection(); // some elements are stored in cy, some are deleted 164 | const nodes2collapse = cy.collection(); // some are deleted 165 | const node2parent = {}; 166 | for (const n of fileJSON.nodes) { 167 | nodePositions[n.data.id] = { x: n.position.x, y: n.position.y }; 168 | if (n.data.parent) { 169 | node2parent[n.data.id] = n.data.parent; 170 | } 171 | const node = cy.add(n); 172 | allNodes.merge(node); 173 | if (node.data('collapsedChildren')) { 174 | json2cyCollection(node.data('collapsedChildren'), allNodes, nodes2collapse, node2parent); 175 | nodes2collapse.merge(node); 176 | clearCollapseMetaData(node); 177 | } 178 | } 179 | for (const e of fileJSON.edges) { 180 | const edge = cy.add(e); 181 | if (edge.data('collapsedEdges')) { 182 | edge.data('collapsedEdges', json2cyCollection(e.data.collapsedEdges, allNodes, nodes2collapse, node2parent)); 183 | cy.remove(edge.data('collapsedEdges')); // delete collapsed edges from cy 184 | } 185 | if (edge.data('originalEnds')) { 186 | const srcId = e.data.originalEnds.source.data.id; 187 | const tgtId = e.data.originalEnds.target.data.id; 188 | e.data.originalEnds = { source: allNodes.filter('#' + srcId), target: allNodes.filter('#' + tgtId) }; 189 | } 190 | } 191 | // set parents 192 | for (let node in node2parent) { 193 | const elem = allNodes.$id(node); 194 | if (elem.length === 1) { 195 | elem.move({ parent: node2parent[node] }); 196 | } 197 | } 198 | // collapse the collapsed nodes 199 | api.collapse(nodes2collapse, { layoutBy: null, fisheye: false, animate: false }); 200 | 201 | // positions might be changed in collapse extension 202 | for (const n of fileJSON.nodes) { 203 | const node = cy.$id(n.data.id) 204 | if (node.isChildless()) { 205 | cy.$id(n.data.id).position(nodePositions[n.data.id]); 206 | } 207 | } 208 | cy.fit(); 209 | }, 210 | 211 | 212 | /** saves cytoscape elements (collection) as JSON 213 | * calls elements' json method (https://js.cytoscape.org/#ele.json) when we keep a cytoscape element in the data. 214 | * @param {} elems cytoscape collection 215 | * @param {} filename string 216 | */ 217 | saveJson: function (elems, filename) { 218 | if (!elems) { 219 | elems = cy.$(); 220 | } 221 | const nodes = halfDeepCopyCollection(elems.nodes()); 222 | const edges = halfDeepCopyCollection(elems.edges()); 223 | if (edges.length + nodes.length < 1) { 224 | return; 225 | } 226 | 227 | // according to cytoscape.js format 228 | const o = { nodes: [], edges: [] }; 229 | for (const e of edges) { 230 | if (e.collapsedEdges) { 231 | e.collapsedEdges = cyCollection2Json(halfDeepCopyCollection(e.collapsedEdges)); 232 | } 233 | if (e.originalEnds) { 234 | const src = e.originalEnds.source.json(); 235 | const tgt = e.originalEnds.target.json(); 236 | if (src.data.collapsedChildren) { 237 | // e.originalEnds.source.data.collapsedChildren will be changed 238 | src.data.collapsedChildren = cyCollection2Json(halfDeepCopyCollection(src.data.collapsedChildren)); 239 | } 240 | if (tgt.data.collapsedChildren) { 241 | tgt.data.collapsedChildren = cyCollection2Json(halfDeepCopyCollection(tgt.data.collapsedChildren)); 242 | } 243 | e.originalEnds = { source: src, target: tgt }; 244 | } 245 | const jsonObj = e.cy.json(); 246 | jsonObj.data.collapsedEdges = e.collapsedEdges; 247 | jsonObj.data.originalEnds = e.originalEnds; 248 | o.edges.push(jsonObj); 249 | } 250 | for (const n of nodes) { 251 | if (n.collapsedChildren) { 252 | n.collapsedChildren = cyCollection2Json(halfDeepCopyCollection(n.collapsedChildren)); 253 | } 254 | const jsonObj = n.cy.json(); 255 | jsonObj.data.collapsedChildren = n.collapsedChildren; 256 | o.nodes.push(jsonObj); 257 | } 258 | 259 | let stringifiedJSON = JSON.stringify(o); 260 | if (filename) { 261 | str2file(stringifiedJSON, filename); 262 | } 263 | return stringifiedJSON; 264 | } 265 | }; 266 | } 267 | 268 | module.exports = saveLoadUtilities; 269 | -------------------------------------------------------------------------------- /src/cueUtilities.js: -------------------------------------------------------------------------------- 1 | var debounce = require('./debounce'); 2 | var debounce2 = require('./debounce2'); 3 | 4 | module.exports = function (params, cy, api) { 5 | var elementUtilities; 6 | var fn = params; 7 | const CUE_POS_UPDATE_DELAY = 100; 8 | var nodeWithRenderedCue; 9 | 10 | const getData = function () { 11 | var scratch = cy.scratch('_cyExpandCollapse'); 12 | return scratch && scratch.cueUtilities; 13 | }; 14 | 15 | const setData = function (data) { 16 | var scratch = cy.scratch('_cyExpandCollapse'); 17 | if (scratch == null) { 18 | scratch = {}; 19 | } 20 | 21 | scratch.cueUtilities = data; 22 | cy.scratch('_cyExpandCollapse', scratch); 23 | }; 24 | 25 | var functions = { 26 | init: function () { 27 | var $canvas = document.createElement('canvas'); 28 | $canvas.classList.add("expand-collapse-canvas"); 29 | var $container = cy.container(); 30 | var ctx = $canvas.getContext('2d'); 31 | $container.append($canvas); 32 | 33 | elementUtilities = require('./elementUtilities')(cy); 34 | 35 | var offset = function (elt) { 36 | var rect = elt.getBoundingClientRect(); 37 | 38 | return { 39 | top: rect.top + document.documentElement.scrollTop, 40 | left: rect.left + document.documentElement.scrollLeft 41 | } 42 | } 43 | 44 | var _sizeCanvas = debounce(function () { 45 | $canvas.height = cy.container().offsetHeight; 46 | $canvas.width = cy.container().offsetWidth; 47 | $canvas.style.position = 'absolute'; 48 | $canvas.style.top = 0; 49 | $canvas.style.left = 0; 50 | $canvas.style.zIndex = options().zIndex; 51 | 52 | setTimeout(function () { 53 | var canvasBb = offset($canvas); 54 | var containerBb = offset($container); 55 | $canvas.style.top = -(canvasBb.top - containerBb.top); 56 | $canvas.style.left = -(canvasBb.left - containerBb.left); 57 | 58 | // refresh the cues on canvas resize 59 | if (cy) { 60 | clearDraws(true); 61 | } 62 | }, 0); 63 | 64 | }, 250); 65 | 66 | function sizeCanvas() { 67 | _sizeCanvas(); 68 | } 69 | 70 | sizeCanvas(); 71 | 72 | var data = {}; 73 | 74 | // if there are events field in data unbind them here 75 | // to prevent binding the same event multiple times 76 | // if (!data.hasEventFields) { 77 | // functions['unbind'].apply( $container ); 78 | // } 79 | 80 | function options() { 81 | return cy.scratch('_cyExpandCollapse').options; 82 | } 83 | 84 | function clearDraws() { 85 | var w = cy.width(); 86 | var h = cy.height(); 87 | 88 | ctx.clearRect(0, 0, w, h); 89 | nodeWithRenderedCue = null; 90 | } 91 | 92 | function drawExpandCollapseCue(node) { 93 | var children = node.children(); 94 | var collapsedChildren = node.data('collapsedChildren'); 95 | var hasChildren = children != null && children != undefined && children.length > 0; 96 | // If this is a simple node with no collapsed children return directly 97 | if (!hasChildren && !collapsedChildren) { 98 | return; 99 | } 100 | 101 | var isCollapsed = node.hasClass('cy-expand-collapse-collapsed-node'); 102 | 103 | //Draw expand-collapse rectangles 104 | var rectSize = options().expandCollapseCueSize; 105 | var lineSize = options().expandCollapseCueLineSize; 106 | 107 | var cueCenter; 108 | 109 | if (options().expandCollapseCuePosition === 'top-left') { 110 | var offset = 1; 111 | var size = cy.zoom() < 1 ? rectSize / (2 * cy.zoom()) : rectSize / 2; 112 | var nodeBorderWid = parseFloat(node.css('border-width')); 113 | var x = node.position('x') - node.width() / 2 - parseFloat(node.css('padding-left')) 114 | + nodeBorderWid + size + offset; 115 | var y = node.position('y') - node.height() / 2 - parseFloat(node.css('padding-top')) 116 | + nodeBorderWid + size + offset; 117 | 118 | cueCenter = { x: x, y: y }; 119 | } else { 120 | var option = options().expandCollapseCuePosition; 121 | cueCenter = typeof option === 'function' ? option.call(this, node) : option; 122 | } 123 | 124 | var expandcollapseCenter = elementUtilities.convertToRenderedPosition(cueCenter); 125 | 126 | // convert to rendered sizes 127 | rectSize = Math.max(rectSize, rectSize * cy.zoom()); 128 | lineSize = Math.max(lineSize, lineSize * cy.zoom()); 129 | var diff = (rectSize - lineSize) / 2; 130 | 131 | var expandcollapseCenterX = expandcollapseCenter.x; 132 | var expandcollapseCenterY = expandcollapseCenter.y; 133 | 134 | var expandcollapseStartX = expandcollapseCenterX - rectSize / 2; 135 | var expandcollapseStartY = expandcollapseCenterY - rectSize / 2; 136 | var expandcollapseRectSize = rectSize; 137 | 138 | // Draw expand/collapse cue if specified use an image else render it in the default way 139 | if (isCollapsed && options().expandCueImage) { 140 | drawImg(options().expandCueImage, expandcollapseStartX, expandcollapseStartY, rectSize, rectSize); 141 | } 142 | else if (!isCollapsed && options().collapseCueImage) { 143 | drawImg(options().collapseCueImage, expandcollapseStartX, expandcollapseStartY, rectSize, rectSize); 144 | } 145 | else { 146 | var oldFillStyle = ctx.fillStyle; 147 | var oldWidth = ctx.lineWidth; 148 | var oldStrokeStyle = ctx.strokeStyle; 149 | 150 | ctx.fillStyle = "black"; 151 | ctx.strokeStyle = "black"; 152 | 153 | ctx.ellipse(expandcollapseCenterX, expandcollapseCenterY, rectSize / 2, rectSize / 2, 0, 0, 2 * Math.PI); 154 | ctx.fill(); 155 | 156 | ctx.beginPath(); 157 | 158 | ctx.strokeStyle = "white"; 159 | ctx.lineWidth = Math.max(2.6, 2.6 * cy.zoom()); 160 | 161 | ctx.moveTo(expandcollapseStartX + diff, expandcollapseStartY + rectSize / 2); 162 | ctx.lineTo(expandcollapseStartX + lineSize + diff, expandcollapseStartY + rectSize / 2); 163 | 164 | if (isCollapsed) { 165 | ctx.moveTo(expandcollapseStartX + rectSize / 2, expandcollapseStartY + diff); 166 | ctx.lineTo(expandcollapseStartX + rectSize / 2, expandcollapseStartY + lineSize + diff); 167 | } 168 | 169 | ctx.closePath(); 170 | ctx.stroke(); 171 | 172 | ctx.strokeStyle = oldStrokeStyle; 173 | ctx.fillStyle = oldFillStyle; 174 | ctx.lineWidth = oldWidth; 175 | } 176 | 177 | node._private.data.expandcollapseRenderedStartX = expandcollapseStartX; 178 | node._private.data.expandcollapseRenderedStartY = expandcollapseStartY; 179 | node._private.data.expandcollapseRenderedCueSize = expandcollapseRectSize; 180 | 181 | nodeWithRenderedCue = node; 182 | } 183 | 184 | function drawImg(imgSrc, x, y, w, h) { 185 | var img = new Image(w, h); 186 | img.src = imgSrc; 187 | img.onload = () => { 188 | ctx.drawImage(img, x, y, w, h); 189 | }; 190 | } 191 | 192 | cy.on('resize', data.eCyResize = function () { 193 | sizeCanvas(); 194 | }); 195 | 196 | cy.on('expandcollapse.clearvisualcue', function () { 197 | if (nodeWithRenderedCue) { 198 | clearDraws(); 199 | } 200 | }); 201 | 202 | var oldMousePos = null, currMousePos = null; 203 | cy.on('mousedown', data.eMouseDown = function (e) { 204 | oldMousePos = e.renderedPosition || e.cyRenderedPosition 205 | }); 206 | 207 | cy.on('mouseup', data.eMouseUp = function (e) { 208 | currMousePos = e.renderedPosition || e.cyRenderedPosition 209 | }); 210 | 211 | cy.on('remove', 'node', data.eRemove = function (evt) { 212 | const node = evt.target; 213 | if (node == nodeWithRenderedCue) { 214 | clearDraws(); 215 | } 216 | }); 217 | 218 | var ur; 219 | cy.on('select unselect', data.eSelect = function () { 220 | if (nodeWithRenderedCue) { 221 | clearDraws(); 222 | } 223 | var selectedNodes = cy.nodes(':selected'); 224 | if (selectedNodes.length !== 1) { 225 | return; 226 | } 227 | var selectedNode = selectedNodes[0]; 228 | 229 | if (selectedNode.isParent() || selectedNode.hasClass('cy-expand-collapse-collapsed-node')) { 230 | drawExpandCollapseCue(selectedNode); 231 | } 232 | }); 233 | 234 | cy.on('tap', data.eTap = function (event) { 235 | var node = nodeWithRenderedCue; 236 | if (!node) { 237 | return; 238 | } 239 | var expandcollapseRenderedStartX = node.data('expandcollapseRenderedStartX'); 240 | var expandcollapseRenderedStartY = node.data('expandcollapseRenderedStartY'); 241 | var expandcollapseRenderedRectSize = node.data('expandcollapseRenderedCueSize'); 242 | var expandcollapseRenderedEndX = expandcollapseRenderedStartX + expandcollapseRenderedRectSize; 243 | var expandcollapseRenderedEndY = expandcollapseRenderedStartY + expandcollapseRenderedRectSize; 244 | 245 | var cyRenderedPos = event.renderedPosition || event.cyRenderedPosition; 246 | var cyRenderedPosX = cyRenderedPos.x; 247 | var cyRenderedPosY = cyRenderedPos.y; 248 | var opts = options(); 249 | var factor = (opts.expandCollapseCueSensitivity - 1) / 2; 250 | 251 | if ((Math.abs(oldMousePos.x - currMousePos.x) < 5 && Math.abs(oldMousePos.y - currMousePos.y) < 5) 252 | && cyRenderedPosX >= expandcollapseRenderedStartX - expandcollapseRenderedRectSize * factor 253 | && cyRenderedPosX <= expandcollapseRenderedEndX + expandcollapseRenderedRectSize * factor 254 | && cyRenderedPosY >= expandcollapseRenderedStartY - expandcollapseRenderedRectSize * factor 255 | && cyRenderedPosY <= expandcollapseRenderedEndY + expandcollapseRenderedRectSize * factor) { 256 | if (opts.undoable && !ur) { 257 | ur = cy.undoRedo({ defaultActions: false }); 258 | } 259 | 260 | if (api.isCollapsible(node)) { 261 | clearDraws(); 262 | if (opts.undoable) { 263 | ur.do("collapse", { 264 | nodes: node, 265 | options: opts 266 | }); 267 | } 268 | else { 269 | api.collapse(node, opts); 270 | } 271 | } 272 | else if (api.isExpandable(node)) { 273 | clearDraws(); 274 | if (opts.undoable) { 275 | ur.do("expand", { nodes: node, options: opts }); 276 | } 277 | else { 278 | api.expand(node, opts); 279 | } 280 | } 281 | if (node.selectable()) { 282 | node.unselectify(); 283 | cy.scratch('_cyExpandCollapse').selectableChanged = true; 284 | } 285 | } 286 | }); 287 | 288 | cy.on('afterUndo afterRedo', data.eUndoRedo = data.eSelect); 289 | 290 | cy.on('position', 'node', data.ePosition = debounce2(data.eSelect, CUE_POS_UPDATE_DELAY, clearDraws)); 291 | 292 | cy.on('pan zoom', data.ePosition); 293 | 294 | // write options to data 295 | data.hasEventFields = true; 296 | setData(data); 297 | }, 298 | unbind: function () { 299 | // var $container = this; 300 | var data = getData(); 301 | 302 | if (!data.hasEventFields) { 303 | console.log('events to unbind does not exist'); 304 | return; 305 | } 306 | 307 | cy.trigger('expandcollapse.clearvisualcue'); 308 | 309 | cy.off('mousedown', 'node', data.eMouseDown) 310 | .off('mouseup', 'node', data.eMouseUp) 311 | .off('remove', 'node', data.eRemove) 312 | .off('tap', 'node', data.eTap) 313 | .off('add', 'node', data.eAdd) 314 | .off('position', 'node', data.ePosition) 315 | .off('pan zoom', data.ePosition) 316 | .off('select unselect', data.eSelect) 317 | .off('free', 'node', data.eFree) 318 | .off('resize', data.eCyResize) 319 | .off('afterUndo afterRedo', data.eUndoRedo); 320 | }, 321 | rebind: function () { 322 | var data = getData(); 323 | 324 | if (!data.hasEventFields) { 325 | console.log('events to rebind does not exist'); 326 | return; 327 | } 328 | 329 | cy.on('mousedown', 'node', data.eMouseDown) 330 | .on('mouseup', 'node', data.eMouseUp) 331 | .on('remove', 'node', data.eRemove) 332 | .on('tap', 'node', data.eTap) 333 | .on('add', 'node', data.eAdd) 334 | .on('position', 'node', data.ePosition) 335 | .on('pan zoom', data.ePosition) 336 | .on('select unselect', data.eSelect) 337 | .on('free', 'node', data.eFree) 338 | .on('resize', data.eCyResize) 339 | .on('afterUndo afterRedo', data.eUndoRedo); 340 | } 341 | }; 342 | 343 | if (functions[fn]) { 344 | return functions[fn].apply(cy.container(), Array.prototype.slice.call(arguments, 1)); 345 | } else if (typeof fn == 'object' || !fn) { 346 | return functions.init.apply(cy.container(), arguments); 347 | } 348 | throw new Error('No such function `' + fn + '` for cytoscape.js-expand-collapse'); 349 | 350 | }; 351 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cytoscape-expand-collapse 2 | ================================================================================ 3 | 4 | **We are in the process of developing a new unified framework for complexity management of graphs. Thus this repositoy is no longer being maintained** 5 | 6 | ## Description 7 | 8 | This extension provides an interface to expand/collapse nodes and edges for better management of complexity of Cytoscape.js compound graphs, distributed under [The MIT License](https://opensource.org/licenses/MIT). 9 | 10 |

11 | 12 |         13 | 14 |

15 | 16 | Please cite the following paper when using this extension: 17 | 18 | U. Dogrusoz , A. Karacelik, I. Safarli, H. Balci, L. Dervishi, and M.C. Siper, "[Efficient methods and readily customizable libraries for managing complexity of large networks](https://doi.org/10.1371/journal.pone.0197238)", PLoS ONE, 13(5): e0197238, 2018. 19 | 20 | ## Demo 21 | 22 | Here are some demos: **no undo and with custom cue image**, **undoable**, and **collapsing edges and nodes**, respectively: 23 |

24 |   25 |   26 | 27 |

28 | 29 | ## API 30 | 31 | * Note that compounds are nodes. 32 | 33 | `cy.expandCollapse(options)` 34 | To initialize the extension with given options. 35 | 36 | `var api = cy.expandCollapse('get')` 37 | To get the extension instance after initialization. 38 | 39 | * Following functions get options parameter to apply during a particular event unlike the function above. 40 | 41 | `api.collapse(nodes, options)` 42 | Collapse given nodes, extend options with given param. 43 | 44 | `api.collapseRecursively(nodes, options)` 45 | Collapse given nodes recursively, extend options with given param. 46 | 47 | `api.collapseAll(options)` 48 | Collapse all nodes on graph (recursively), extend options with given param. 49 | 50 | `api.expand(nodes, options)` 51 | Expand given nodes, extend options with given param. 52 | 53 | `api.expandRecursively(nodes, options)` 54 | Expand given nodes recursively, extend options with given param. 55 | 56 | `api.expandAll(options)` 57 | Expand all nodes on graph (recursively), extend options with given param. 58 | 59 | `api.isExpandable(node)` 60 | Get whether node is expandable (or is collapsed) 61 | 62 | `api.isCollapsible(node)` 63 | Get whether node is collapsible. 64 | 65 | `api.expandableNodes(nodes)` 66 | Get expandable ones inside given nodes if nodes parameter is not specified consider all nodes 67 | 68 | `api.collapsibleNodes(nodes)` 69 | Get collapsible ones inside given nodes if nodes parameter is not specified consider all nodes 70 | 71 | `api.setOptions(options)` 72 | Resets the options to the given parameter. 73 | 74 | `api.setOption(name, value)` 75 | Sets the value of the option given by the name to the given value. 76 | 77 | `api.extendOptions(options)` 78 | Extend the current options with the given options. 79 | 80 | `api.getCollapsedChildren(node)` 81 | Get the children of the given collapsed node which are removed during collapse operation 82 | 83 | `api.getCollapsedChildrenRecursively(node)` 84 | Get collapsed children recursively including nested collapsed children. Returned value includes edges and nodes, use selector to get edges or nodes. 85 | 86 | `api.getAllCollapsedChildrenRecursively()` 87 | Get collapsed children of all collapsed nodes recursively. Returned value includes edges and nodes, use selector to get edges or nodes. 88 | 89 | `api.clearVisualCue()` 90 | Forces the visual cue to be cleared. It is to be called in extreme cases. 91 | 92 | `api.enableCue()` 93 | Enable rendering of visual cue. 94 | 95 | `api.disableCue()` 96 | Disable rendering of visual cue. 97 | 98 | `api.getParent(nodeId)` 99 | Get the parent of a node given its node id. Useful to reach parent of a node removed because of collapse operation. 100 | 101 | `api.collapseEdges(edges,options)` 102 | Collapse the given edges if all the given edges are between same two nodes and number of edges passed is at least 2. Does nothing otherwise. 103 | 104 | ` api.expandEdges(edges){ ` 105 | Expand the given collapsed edges 106 | 107 | `api.collapseEdgesBetweenNodes(nodes, options)` 108 | Collapse all edges between the set of given nodes. 109 | 110 | `api.expandEdgesBetweenNodes(nodes)` 111 | Expand all collapsed edges between the set of given nodes 112 | 113 | `api.collapseAllEdges(options)` 114 | Collapse all edges in the graph. 115 | 116 | `api.expandAllEdges()` 117 | Expand all edges in the graph. 118 | 119 | `api.loadJson(jsonStr)` 120 | Load elements from JSON string. 121 | 122 | `api.saveJson(elems, filename)` 123 | Return elements in JSON format and saves them to a file if a file name is provided via `filename` parameter. Default value for `elems` is all the elements. 124 | 125 | ## Events 126 | Notice that following events are performed for *each* node that is collapsed/expanded. Also, notice that any post-processing layout is performed *after* the event. 127 | 128 | `cy.nodes().on("expandcollapse.beforecollapse", function(event) { var node = this; ... })` Triggered before a node is collapsed 129 | 130 | `cy.nodes().on("expandcollapse.aftercollapse", function(event) { var node = this; ... })` Triggered after a node is collapsed 131 | 132 | `cy.nodes().on("expandcollapse.beforeexpand", function(event) { var node = this; ... })` Triggered before a node is expanded 133 | 134 | `cy.nodes().on("expandcollapse.afterexpand", function(event) { var node = this; ... })` Triggered after a node is expanded 135 | 136 | `cy.edges().on("expandcollapse.beforecollapseedge", function(event) { var edge = this; ... })` Triggered before an edge is collapsed 137 | 138 | `cy.edges().on("expandcollapse.aftercollapseedge", function(event) { var edge = this; ... })` Triggered after an edge is collapsed 139 | 140 | `cy.edges().on("expandcollapse.beforeexpandedge", function(event) { var edge = this; ... })` Triggered before an edge is expanded 141 | 142 | `cy.edges().on("expandcollapse.afterexpandedge", function(event) { var edge = this; ... })` Triggered after an edge is expanded 143 | 144 | All these events can also be listened as [cytoscape.js core events](https://js.cytoscape.org/#cy.on) 145 | e.g. 146 | 147 | `cy.on("expandcollapse.afterexpandedge", function(event) { var elem = event.target; ... })` 148 | 149 | ## Default Options 150 | ```javascript 151 | var options = { 152 | layoutBy: null, // to rearrange after expand/collapse. It's just layout options or whole layout function. Choose your side! 153 | // recommended usage: use cose-bilkent layout with randomize: false to preserve mental map upon expand/collapse 154 | fisheye: true, // whether to perform fisheye view after expand/collapse you can specify a function too 155 | animate: true, // whether to animate on drawing changes you can specify a function too 156 | animationDuration: 1000, // when animate is true, the duration in milliseconds of the animation 157 | ready: function () { }, // callback when expand/collapse initialized 158 | undoable: true, // and if undoRedoExtension exists, 159 | 160 | cueEnabled: true, // Whether cues are enabled 161 | expandCollapseCuePosition: 'top-left', // default cue position is top left you can specify a function per node too 162 | expandCollapseCueSize: 12, // size of expand-collapse cue 163 | expandCollapseCueLineSize: 8, // size of lines used for drawing plus-minus icons 164 | expandCueImage: undefined, // image of expand icon if undefined draw regular expand cue 165 | collapseCueImage: undefined, // image of collapse icon if undefined draw regular collapse cue 166 | expandCollapseCueSensitivity: 1, // sensitivity of expand-collapse cues 167 | edgeTypeInfo: "edgeType", // the name of the field that has the edge type, retrieved from edge.data(), can be a function, if reading the field returns undefined the collapsed edge type will be "unknown" 168 | groupEdgesOfSameTypeOnCollapse : false, // if true, the edges to be collapsed will be grouped according to their types, and the created collapsed edges will have same type as their group. if false the collapased edge will have "unknown" type. 169 | allowNestedEdgeCollapse: true, // when you want to collapse a compound edge (edge which contains other edges) and normal edge, should it collapse without expanding the compound first 170 | zIndex: 999 // z-index value of the canvas in which cue ımages are drawn 171 | }; 172 | ``` 173 | 174 | ## Default Undo/Redo Actions 175 | `ur.do("collapse", { nodes: eles, options: opts)` Equivalent of eles.collapse(opts) 176 | 177 | `ur.do("expand", { nodes: eles, options: opts)` Equivalent of eles.expand(opts) 178 | 179 | `ur.do("collapseRecursively", { nodes: eles, options: opts)` Equivalent of eles.collapseRecursively(opts) 180 | 181 | `ur.do("expandRecursively", { nodes: eles, options: opts)` Equivalent of eles.expandRecursively(opts) 182 | 183 | `ur.do("collapseAll", { options: opts)` Equivalent of cy.collapseAll(opts) 184 | 185 | `ur.do("expandAll", { options: opts })` Equivalent of cy.expandAll(opts) 186 | 187 | `ur.do("collapseEdges", { edges: eles, options: opts})` Equivalent of eles.collapseEdges(opts) 188 | 189 | `ur.do("expandEdges", { edges: eles})` Equivalent of eles.expandEdges() 190 | 191 | `ur.do("collapseEdgesBetweenNodes", { nodes: eles, options: opts})` Equivalent of eles.collapseEdgesBetweenNodes(opts) 192 | 193 | `ur.do("expandEdgesBetweenNodes", { nodes: eles})` Equivalent of eles.expandEdgesBetweenNodes() 194 | 195 | `ur.do("collapseAllEdges", {options: opts)}` Equivalent of cy.collapseAllEdges(opts) 196 | 197 | `ur.do("expandAllEdges")`Equivalent of cy.expandAllEdges() 198 | 199 | 200 | ## Elements Style 201 | 202 | * Collapsed nodes have 'cy-expand-collapse-collapsed-node' class. 203 | * Meta edges (edges from/to collapsed nodes) have 'cy-expand-collapse-meta-edge' class. 204 | * Collapsed edges have 'cy-expand-collapse-collapsed-edge' class. 205 | * Collapsed edges data have 'directionType' field which can be either: 206 | - 'unidirection' if all the edges that are collapsed into this edge have the same direction (all have same source and same target) 207 | or 208 | - 'bidirection' if the edges that are collapsed into this edge have different direction (different target and/or source) 209 | * Collapsed edges data have a field that holds the type, the field name is as defined in options but if it is not defined in options or was defined as a function it will be named 'edgeType' 210 | 211 | 212 | 213 | ## Dependencies 214 | 215 | * Cytoscape.js ^3.3.0 216 | * cytoscape-undo-redo.js(optional) ^1.0.1 217 | * cytoscape-cose-bilkent.js(optional/suggested for layout after expand/collapse) ^4.0.0 218 | 219 | 220 | ## Usage instructions 221 | 222 | Download the library: 223 | * via npm: `npm install cytoscape-expand-collapse`, 224 | * via bower: `bower install cytoscape-expand-collapse`, or 225 | * via direct download in the repository (probably from a tag). 226 | 227 | `require()` the library as appropriate for your project: 228 | 229 | CommonJS: 230 | ```js 231 | var cytoscape = require('cytoscape'); 232 | var expandCollapse = require('cytoscape-expand-collapse'); 233 | 234 | expandCollapse( cytoscape ); // register extension 235 | ``` 236 | 237 | AMD: 238 | ```js 239 | require(['cytoscape', 'cytoscape-expand-collapse'], function( cytoscape, expandCollapse ){ 240 | expandCollapse( cytoscape ); // register extension 241 | }); 242 | ``` 243 | 244 | Plain HTML/JS has the extension registered for you automatically, because no `require()` is needed. 245 | 246 | 247 | ## Build targets 248 | 249 | * `npm run build` : Build `./src/**` into `cytoscape-expand-collapse.js` in production environment and minimize the file. 250 | * `npm run build:dev` : Build `./src/**` into `cytoscape-expand-collapse.js` in development environment without minimizing the file. 251 | 252 | ## Publishing instructions 253 | 254 | This project is set up to automatically be published to npm and bower. To publish: 255 | 256 | 1. Build the extension : `npm run build` 257 | 1. Commit the build : `git commit -am "Build for release"` 258 | 1. Bump the version number and tag: `npm version major|minor|patch` 259 | 1. Push to origin: `git push && git push --tags` 260 | 1. Publish to npm: `npm publish .` 261 | 1. If publishing to bower for the first time, you'll need to run `bower register cytoscape-expand-collapse https://github.com/iVis-at-Bilkent/cytoscape.js-expand-collapse.git` 262 | 263 | 264 | ## Team 265 | 266 | * [Hasan Balci](https://github.com/hasanbalci), [Yusuf Canbaz](https://github.com/canbax), [Ugur Dogrusoz](https://github.com/ugurdogrusoz) of [i-Vis at Bilkent University](http://www.cs.bilkent.edu.tr/~ivis) and [Metin Can Siper](https://github.com/metincansiper) of the Demir Lab at [OHSU](http://www.ohsu.edu/) 267 | 268 | ## Alumni 269 | 270 | * [Alper Karacelik](https://github.com/alperkaracelik), [Ilkin Safarli](https://github.com/kinimesi), [Nasim Saleh](https://github.com/nasimsaleh), [Selim Firat Yilmaz](https://github.com/mrsfy) 271 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | // registers the extension on a cytoscape lib ref 5 | var register = function (cytoscape) { 6 | 7 | if (!cytoscape) { 8 | return; 9 | } // can't register if cytoscape unspecified 10 | 11 | var undoRedoUtilities = require('./undoRedoUtilities'); 12 | var cueUtilities = require("./cueUtilities"); 13 | var saveLoadUtils = null; 14 | 15 | function extendOptions(options, extendBy) { 16 | var tempOpts = {}; 17 | for (var key in options) 18 | tempOpts[key] = options[key]; 19 | 20 | for (var key in extendBy) 21 | if (tempOpts.hasOwnProperty(key)) 22 | tempOpts[key] = extendBy[key]; 23 | return tempOpts; 24 | } 25 | 26 | // evaluate some specific options in case of they are specified as functions to be dynamically changed 27 | function evalOptions(options) { 28 | var animate = typeof options.animate === 'function' ? options.animate.call() : options.animate; 29 | var fisheye = typeof options.fisheye === 'function' ? options.fisheye.call() : options.fisheye; 30 | 31 | options.animate = animate; 32 | options.fisheye = fisheye; 33 | } 34 | 35 | // creates and returns the API instance for the extension 36 | function createExtensionAPI(cy, expandCollapseUtilities) { 37 | var api = {}; // API to be returned 38 | // set functions 39 | 40 | function handleNewOptions(opts) { 41 | var currentOpts = getScratch(cy, 'options'); 42 | if (opts.cueEnabled && !currentOpts.cueEnabled) { 43 | api.enableCue(); 44 | } 45 | else if (!opts.cueEnabled && currentOpts.cueEnabled) { 46 | api.disableCue(); 47 | } 48 | } 49 | 50 | function isOnly1Pair(edges) { 51 | let relatedEdgesArr = []; 52 | for (let i = 0; i < edges.length; i++) { 53 | const srcId = edges[i].source().id(); 54 | const targetId = edges[i].target().id(); 55 | const obj = {}; 56 | obj[srcId] = true; 57 | obj[targetId] = true; 58 | relatedEdgesArr.push(obj); 59 | } 60 | for (let i = 0; i < relatedEdgesArr.length; i++) { 61 | for (let j = i + 1; j < relatedEdgesArr.length; j++) { 62 | const keys1 = Object.keys(relatedEdgesArr[i]); 63 | const keys2 = Object.keys(relatedEdgesArr[j]); 64 | const allKeys = new Set(keys1.concat(keys2)); 65 | if (allKeys.size != keys1.length || allKeys.size != keys2.length) { 66 | return false; 67 | } 68 | } 69 | } 70 | return true; 71 | } 72 | 73 | // set all options at once 74 | api.setOptions = function (opts) { 75 | handleNewOptions(opts); 76 | setScratch(cy, 'options', opts); 77 | }; 78 | 79 | api.extendOptions = function (opts) { 80 | var options = getScratch(cy, 'options'); 81 | var newOptions = extendOptions(options, opts); 82 | handleNewOptions(newOptions); 83 | setScratch(cy, 'options', newOptions); 84 | } 85 | 86 | // set the option whose name is given 87 | api.setOption = function (name, value) { 88 | var opts = {}; 89 | opts[name] = value; 90 | 91 | var options = getScratch(cy, 'options'); 92 | var newOptions = extendOptions(options, opts); 93 | 94 | handleNewOptions(newOptions); 95 | setScratch(cy, 'options', newOptions); 96 | }; 97 | 98 | // Collection functions 99 | 100 | // collapse given eles extend options with given param 101 | api.collapse = function (_eles, opts) { 102 | var eles = this.collapsibleNodes(_eles); 103 | var options = getScratch(cy, 'options'); 104 | var tempOptions = extendOptions(options, opts); 105 | evalOptions(tempOptions); 106 | 107 | return expandCollapseUtilities.collapseGivenNodes(eles, tempOptions); 108 | }; 109 | 110 | // collapse given eles recursively extend options with given param 111 | api.collapseRecursively = function (_eles, opts) { 112 | var eles = this.collapsibleNodes(_eles); 113 | var options = getScratch(cy, 'options'); 114 | var tempOptions = extendOptions(options, opts); 115 | evalOptions(tempOptions); 116 | 117 | return this.collapse(eles.union(eles.descendants()), tempOptions); 118 | }; 119 | 120 | // expand given eles extend options with given param 121 | api.expand = function (_eles, opts) { 122 | var eles = this.expandableNodes(_eles); 123 | var options = getScratch(cy, 'options'); 124 | var tempOptions = extendOptions(options, opts); 125 | evalOptions(tempOptions); 126 | 127 | return expandCollapseUtilities.expandGivenNodes(eles, tempOptions); 128 | }; 129 | 130 | // expand given eles recusively extend options with given param 131 | api.expandRecursively = function (_eles, opts) { 132 | var eles = this.expandableNodes(_eles); 133 | var options = getScratch(cy, 'options'); 134 | var tempOptions = extendOptions(options, opts); 135 | evalOptions(tempOptions); 136 | 137 | return expandCollapseUtilities.expandAllNodes(eles, tempOptions); 138 | }; 139 | 140 | 141 | // Core functions 142 | 143 | // collapse all collapsible nodes 144 | api.collapseAll = function (opts) { 145 | var options = getScratch(cy, 'options'); 146 | var tempOptions = extendOptions(options, opts); 147 | evalOptions(tempOptions); 148 | 149 | return this.collapseRecursively(this.collapsibleNodes(), tempOptions); 150 | }; 151 | 152 | // expand all expandable nodes 153 | api.expandAll = function (opts) { 154 | var options = getScratch(cy, 'options'); 155 | var tempOptions = extendOptions(options, opts); 156 | evalOptions(tempOptions); 157 | 158 | return this.expandRecursively(this.expandableNodes(), tempOptions); 159 | }; 160 | 161 | 162 | // Utility functions 163 | 164 | // returns if the given node is expandable 165 | api.isExpandable = function (node) { 166 | return node.hasClass('cy-expand-collapse-collapsed-node'); 167 | }; 168 | 169 | // returns if the given node is collapsible 170 | api.isCollapsible = function (node) { 171 | return !this.isExpandable(node) && node.isParent(); 172 | }; 173 | 174 | // get collapsible ones inside given nodes if nodes parameter is not specified consider all nodes 175 | api.collapsibleNodes = function (_nodes) { 176 | var self = this; 177 | var nodes = _nodes ? _nodes : cy.nodes(); 178 | return nodes.filter(function (ele, i) { 179 | if (typeof ele === "number") { 180 | ele = i; 181 | } 182 | return self.isCollapsible(ele); 183 | }); 184 | }; 185 | 186 | // get expandable ones inside given nodes if nodes parameter is not specified consider all nodes 187 | api.expandableNodes = function (_nodes) { 188 | var self = this; 189 | var nodes = _nodes ? _nodes : cy.nodes(); 190 | return nodes.filter(function (ele, i) { 191 | if (typeof ele === "number") { 192 | ele = i; 193 | } 194 | return self.isExpandable(ele); 195 | }); 196 | }; 197 | 198 | // Get the children of the given collapsed node which are removed during collapse operation 199 | api.getCollapsedChildren = function (node) { 200 | return node.data('collapsedChildren'); 201 | }; 202 | 203 | /** Get collapsed children recursively including nested collapsed children 204 | * Returned value includes edges and nodes, use selector to get edges or nodes 205 | * @param node : a collapsed node 206 | * @return all collapsed children 207 | */ 208 | api.getCollapsedChildrenRecursively = function (node) { 209 | var collapsedChildren = cy.collection(); 210 | return expandCollapseUtilities.getCollapsedChildrenRecursively(node, collapsedChildren); 211 | }; 212 | 213 | /** Get collapsed children of all collapsed nodes recursively including nested collapsed children 214 | * Returned value includes edges and nodes, use selector to get edges or nodes 215 | * @return all collapsed children 216 | */ 217 | api.getAllCollapsedChildrenRecursively = function () { 218 | var collapsedChildren = cy.collection(); 219 | var collapsedNodes = cy.nodes(".cy-expand-collapse-collapsed-node"); 220 | var j; 221 | for (j = 0; j < collapsedNodes.length; j++) { 222 | collapsedChildren = collapsedChildren.union(this.getCollapsedChildrenRecursively(collapsedNodes[j])); 223 | } 224 | return collapsedChildren; 225 | }; 226 | // This method forces the visual cue to be cleared. It is to be called in extreme cases 227 | api.clearVisualCue = function (node) { 228 | cy.trigger('expandcollapse.clearvisualcue'); 229 | }; 230 | 231 | api.disableCue = function () { 232 | var options = getScratch(cy, 'options'); 233 | if (options.cueEnabled) { 234 | cueUtilities('unbind', cy, api); 235 | options.cueEnabled = false; 236 | } 237 | }; 238 | 239 | api.enableCue = function () { 240 | var options = getScratch(cy, 'options'); 241 | if (!options.cueEnabled) { 242 | cueUtilities('rebind', cy, api); 243 | options.cueEnabled = true; 244 | } 245 | }; 246 | 247 | api.getParent = function (nodeId) { 248 | if (cy.getElementById(nodeId)[0] === undefined) { 249 | var parentData = getScratch(cy, 'parentData'); 250 | return parentData[nodeId]; 251 | } 252 | else { 253 | return cy.getElementById(nodeId).parent(); 254 | } 255 | }; 256 | 257 | api.collapseEdges = function (edges, opts) { 258 | var result = { edges: cy.collection(), oldEdges: cy.collection() }; 259 | if (edges.length < 2) return result; 260 | if (!isOnly1Pair(edges)) return result; 261 | var options = getScratch(cy, 'options'); 262 | var tempOptions = extendOptions(options, opts); 263 | return expandCollapseUtilities.collapseGivenEdges(edges, tempOptions); 264 | }; 265 | 266 | api.expandEdges = function (edges) { 267 | var result = { edges: cy.collection(), oldEdges: cy.collection() } 268 | if (edges === undefined) return result; 269 | 270 | //if(typeof edges[Symbol.iterator] === 'function'){//collection of edges is passed 271 | edges.forEach(function (edge) { 272 | var operationResult = expandCollapseUtilities.expandEdge(edge); 273 | result.edges = result.edges.add(operationResult.edges); 274 | result.oldEdges = result.oldEdges.add(operationResult.oldEdges); 275 | 276 | }); 277 | /* }else{//one edge passed 278 | var operationResult = expandCollapseUtilities.expandEdge(edges); 279 | result.edges = result.edges.add(operationResult.edges); 280 | result.oldEdges = result.oldEdges.add(operationResult.oldEdges); 281 | 282 | } */ 283 | return result; 284 | }; 285 | 286 | api.collapseEdgesBetweenNodes = function (nodes, opts) { 287 | var options = getScratch(cy, 'options'); 288 | var tempOptions = extendOptions(options, opts); 289 | function pairwise(list) { 290 | var pairs = []; 291 | list 292 | .slice(0, list.length - 1) 293 | .forEach(function (first, n) { 294 | var tail = list.slice(n + 1, list.length); 295 | tail.forEach(function (item) { 296 | pairs.push([first, item]) 297 | }); 298 | }) 299 | return pairs; 300 | } 301 | var nodesPairs = pairwise(nodes); 302 | // for self-loops 303 | nodesPairs.push(...nodes.map(x => [x, x])); 304 | var result = { edges: cy.collection(), oldEdges: cy.collection() }; 305 | nodesPairs.forEach(function (nodePair) { 306 | const id1 = nodePair[1].id(); 307 | var edges = nodePair[0].connectedEdges('[source = "' + id1 + '"],[target = "' + id1 + '"]'); 308 | // edges for self-loops 309 | if (nodePair[0].id() === id1) { 310 | edges = nodePair[0].connectedEdges('[source = "' + id1 + '"][target = "' + id1 + '"]'); 311 | } 312 | if (edges.length >= 2) { 313 | var operationResult = expandCollapseUtilities.collapseGivenEdges(edges, tempOptions) 314 | result.oldEdges = result.oldEdges.add(operationResult.oldEdges); 315 | result.edges = result.edges.add(operationResult.edges); 316 | } 317 | 318 | }.bind(this)); 319 | 320 | return result; 321 | 322 | }; 323 | 324 | api.expandEdgesBetweenNodes = function (nodes) { 325 | var edgesToExpand = cy.collection(); 326 | function pairwise(list) { 327 | var pairs = []; 328 | list 329 | .slice(0, list.length - 1) 330 | .forEach(function (first, n) { 331 | var tail = list.slice(n + 1, list.length); 332 | tail.forEach(function (item) { 333 | pairs.push([first, item]) 334 | }); 335 | }) 336 | return pairs; 337 | } 338 | var nodesPairs = pairwise(nodes); 339 | // for self-loops 340 | nodesPairs.push(...nodes.map(x => [x, x])); 341 | nodesPairs.forEach(function (nodePair) { 342 | const id1 = nodePair[1].id(); 343 | var edges = nodePair[0].connectedEdges('.cy-expand-collapse-collapsed-edge[source = "' + id1 + '"],[target = "' + id1 + '"]'); 344 | // edges for self-loops 345 | if (nodePair[0].id() === id1) { 346 | edges = nodePair[0].connectedEdges('[source = "' + id1 + '"][target = "' + id1 + '"]'); 347 | } 348 | edgesToExpand = edgesToExpand.union(edges); 349 | }.bind(this)); 350 | return this.expandEdges(edgesToExpand); 351 | }; 352 | 353 | api.collapseAllEdges = function (opts) { 354 | return this.collapseEdgesBetweenNodes(cy.edges().connectedNodes(), opts); 355 | }; 356 | 357 | api.expandAllEdges = function () { 358 | var edges = cy.edges(".cy-expand-collapse-collapsed-edge"); 359 | var result = { edges: cy.collection(), oldEdges: cy.collection() }; 360 | var operationResult = this.expandEdges(edges); 361 | result.oldEdges = result.oldEdges.add(operationResult.oldEdges); 362 | result.edges = result.edges.add(operationResult.edges); 363 | return result; 364 | }; 365 | 366 | api.loadJson = function (jsonStr) { 367 | saveLoadUtils.loadJson(jsonStr); 368 | }; 369 | 370 | api.saveJson = function (elems, filename) { 371 | return saveLoadUtils.saveJson(elems, filename); 372 | }; 373 | 374 | return api; // Return the API instance 375 | } 376 | 377 | // Get the whole scratchpad reserved for this extension (on an element or core) or get a single property of it 378 | function getScratch(cyOrEle, name) { 379 | if (cyOrEle.scratch('_cyExpandCollapse') === undefined) { 380 | cyOrEle.scratch('_cyExpandCollapse', {}); 381 | } 382 | 383 | var scratch = cyOrEle.scratch('_cyExpandCollapse'); 384 | var retVal = (name === undefined) ? scratch : scratch[name]; 385 | return retVal; 386 | } 387 | 388 | // Set a single property on scratchpad of an element or the core 389 | function setScratch(cyOrEle, name, val) { 390 | getScratch(cyOrEle)[name] = val; 391 | } 392 | 393 | // register the extension cy.expandCollapse() 394 | cytoscape("core", "expandCollapse", function (opts) { 395 | var cy = this; 396 | 397 | var options = getScratch(cy, 'options') || { 398 | layoutBy: null, // for rearrange after expand/collapse. It's just layout options or whole layout function. Choose your side! 399 | fisheye: true, // whether to perform fisheye view after expand/collapse you can specify a function too 400 | animate: true, // whether to animate on drawing changes you can specify a function too 401 | animationDuration: 1000, // when animate is true, the duration in milliseconds of the animation 402 | ready: function () { }, // callback when expand/collapse initialized 403 | undoable: true, // and if undoRedoExtension exists, 404 | 405 | cueEnabled: true, // Whether cues are enabled 406 | expandCollapseCuePosition: 'top-left', // default cue position is top left you can specify a function per node too 407 | expandCollapseCueSize: 12, // size of expand-collapse cue 408 | expandCollapseCueLineSize: 8, // size of lines used for drawing plus-minus icons 409 | expandCueImage: undefined, // image of expand icon if undefined draw regular expand cue 410 | collapseCueImage: undefined, // image of collapse icon if undefined draw regular collapse cue 411 | expandCollapseCueSensitivity: 1, // sensitivity of expand-collapse cues 412 | 413 | edgeTypeInfo: "edgeType", //the name of the field that has the edge type, retrieved from edge.data(), can be a function 414 | groupEdgesOfSameTypeOnCollapse: false, 415 | allowNestedEdgeCollapse: true, 416 | zIndex: 999 // z-index value of the canvas in which cue ımages are drawn 417 | }; 418 | 419 | // If opts is not 'get' that is it is a real options object then initilize the extension 420 | if (opts !== 'get') { 421 | options = extendOptions(options, opts); 422 | 423 | var expandCollapseUtilities = require('./expandCollapseUtilities')(cy); 424 | var api = createExtensionAPI(cy, expandCollapseUtilities); // creates and returns the API instance for the extension 425 | saveLoadUtils = require("./saveLoadUtilities")(cy, api); 426 | setScratch(cy, 'api', api); 427 | 428 | undoRedoUtilities(cy, api); 429 | 430 | cueUtilities(options, cy, api); 431 | 432 | // if the cue is not enabled unbind cue events 433 | if (!options.cueEnabled) { 434 | cueUtilities('unbind', cy, api); 435 | } 436 | 437 | if (options.ready) { 438 | options.ready(); 439 | } 440 | 441 | setScratch(cy, 'options', options); 442 | 443 | var parentData = {}; 444 | setScratch(cy, 'parentData', parentData); 445 | } 446 | 447 | return getScratch(cy, 'api'); // Expose the API to the users 448 | }); 449 | }; 450 | 451 | if (typeof module !== 'undefined' && module.exports) { // expose as a commonjs module 452 | module.exports = register; 453 | } 454 | 455 | if (typeof define !== 'undefined' && define.amd) { // expose as an amd/requirejs module 456 | define('cytoscape-expand-collapse', function () { 457 | return register; 458 | }); 459 | } 460 | 461 | if (typeof cytoscape !== 'undefined') { // expose to global cytoscape (i.e. window.cytoscape) 462 | register(cytoscape); 463 | } 464 | 465 | })(); 466 | -------------------------------------------------------------------------------- /demo/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | cytoscape-expand-collapse.js demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 109 | 110 | 111 | 112 |

cytoscape-expand-collapse demo

113 | 114 |
115 | 121 |
122 | 123 |
124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /demo/demo-undoable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | cytoscape-expand-collapse.js demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 123 | 124 | 125 | 126 |

cytoscape-expand-collapse demo

127 | 128 |
129 | 137 |
138 | 139 |
140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /cytoscape-expand-collapse.js: -------------------------------------------------------------------------------- 1 | !function(e,o){"object"==typeof exports&&"object"==typeof module?module.exports=o():"function"==typeof define&&define.amd?define([],o):"object"==typeof exports?exports.cytoscapeExpandCollapse=o():e.cytoscapeExpandCollapse=o()}(this,(()=>{return e={78:e=>{var o={equalBoundingBoxes:function(e,o){return e.x1==o.x1&&e.x2==o.x2&&e.y1==o.y1&&e.y2==o.y2},getUnion:function(e,o){var n={x1:Math.min(e.x1,o.x1),x2:Math.max(e.x2,o.x2),y1:Math.min(e.y1,o.y1),y2:Math.max(e.y2,o.y2)};return n.w=n.x2-n.x1,n.h=n.y2-n.y1,n}};e.exports=o},540:(e,o,n)=>{var t=n(630),a=n(438);e.exports=function(e,o,d){var i,s,l=e;const r=function(){var e=o.scratch("_cyExpandCollapse");return e&&e.cueUtilities};var c={init:function(){var e=document.createElement("canvas");e.classList.add("expand-collapse-canvas");var l=o.container(),r=e.getContext("2d");l.append(e),i=n(537)(o);var c=function(e){var o=e.getBoundingClientRect();return{top:o.top+document.documentElement.scrollTop,left:o.left+document.documentElement.scrollLeft}},p=t((function(){e.height=o.container().offsetHeight,e.width=o.container().offsetWidth,e.style.position="absolute",e.style.top=0,e.style.left=0,e.style.zIndex=f().zIndex,setTimeout((function(){var n=c(e),t=c(l);e.style.top=-(n.top-t.top),e.style.left=-(n.left-t.left),o&&h()}),0)}),250);function u(){p()}u();var g={};function f(){return o.scratch("_cyExpandCollapse").options}function h(){var e=o.width(),n=o.height();r.clearRect(0,0,e,n),s=null}function v(e,o,n,t,a){var d=new Image(t,a);d.src=e,d.onload=()=>{r.drawImage(d,o,n,t,a)}}o.on("resize",g.eCyResize=function(){u()}),o.on("expandcollapse.clearvisualcue",(function(){s&&h()}));var y,x=null,E=null;o.on("mousedown",g.eMouseDown=function(e){x=e.renderedPosition||e.cyRenderedPosition}),o.on("mouseup",g.eMouseUp=function(e){E=e.renderedPosition||e.cyRenderedPosition}),o.on("remove","node",g.eRemove=function(e){e.target==s&&h()}),o.on("select unselect",g.eSelect=function(){s&&h();var e=o.nodes(":selected");if(1===e.length){var n=e[0];(n.isParent()||n.hasClass("cy-expand-collapse-collapsed-node"))&&function(e){var n=e.children(),t=e.data("collapsedChildren");if(null!=n&&null!=n&&n.length>0||t){var a,d=e.hasClass("cy-expand-collapse-collapsed-node"),l=f().expandCollapseCueSize,c=f().expandCollapseCueLineSize;if("top-left"===f().expandCollapseCuePosition){var p=o.zoom()<1?l/(2*o.zoom()):l/2,u=parseFloat(e.css("border-width"));a={x:e.position("x")-e.width()/2-parseFloat(e.css("padding-left"))+u+p+1,y:e.position("y")-e.height()/2-parseFloat(e.css("padding-top"))+u+p+1}}else{var g=f().expandCollapseCuePosition;a="function"==typeof g?g.call(this,e):g}var h=i.convertToRenderedPosition(a),y=((l=Math.max(l,l*o.zoom()))-(c=Math.max(c,c*o.zoom())))/2,x=h.x,E=h.y,m=x-l/2,C=E-l/2,b=l;if(d&&f().expandCueImage)v(f().expandCueImage,m,C,l,l);else if(!d&&f().collapseCueImage)v(f().collapseCueImage,m,C,l,l);else{var w=r.fillStyle,T=r.lineWidth,N=r.strokeStyle;r.fillStyle="black",r.strokeStyle="black",r.ellipse(x,E,l/2,l/2,0,0,2*Math.PI),r.fill(),r.beginPath(),r.strokeStyle="white",r.lineWidth=Math.max(2.6,2.6*o.zoom()),r.moveTo(m+y,C+l/2),r.lineTo(m+c+y,C+l/2),d&&(r.moveTo(m+l/2,C+y),r.lineTo(m+l/2,C+c+y)),r.closePath(),r.stroke(),r.strokeStyle=N,r.fillStyle=w,r.lineWidth=T}e._private.data.expandcollapseRenderedStartX=m,e._private.data.expandcollapseRenderedStartY=C,e._private.data.expandcollapseRenderedCueSize=b,s=e}}(n)}}),o.on("tap",g.eTap=function(e){var n=s;if(n){var t=n.data("expandcollapseRenderedStartX"),a=n.data("expandcollapseRenderedStartY"),i=n.data("expandcollapseRenderedCueSize"),l=t+i,r=a+i,c=e.renderedPosition||e.cyRenderedPosition,p=c.x,u=c.y,g=f(),v=(g.expandCollapseCueSensitivity-1)/2;Math.abs(x.x-E.x)<5&&Math.abs(x.y-E.y)<5&&p>=t-i*v&&p<=l+i*v&&u>=a-i*v&&u<=r+i*v&&(g.undoable&&!y&&(y=o.undoRedo({defaultActions:!1})),d.isCollapsible(n)?(h(),g.undoable?y.do("collapse",{nodes:n,options:g}):d.collapse(n,g)):d.isExpandable(n)&&(h(),g.undoable?y.do("expand",{nodes:n,options:g}):d.expand(n,g)),n.selectable()&&(n.unselectify(),o.scratch("_cyExpandCollapse").selectableChanged=!0))}}),o.on("afterUndo afterRedo",g.eUndoRedo=g.eSelect),o.on("position","node",g.ePosition=a(g.eSelect,100,h)),o.on("pan zoom",g.ePosition),g.hasEventFields=!0,function(e){var n=o.scratch("_cyExpandCollapse");null==n&&(n={}),n.cueUtilities=e,o.scratch("_cyExpandCollapse",n)}(g)},unbind:function(){var e=r();e.hasEventFields?(o.trigger("expandcollapse.clearvisualcue"),o.off("mousedown","node",e.eMouseDown).off("mouseup","node",e.eMouseUp).off("remove","node",e.eRemove).off("tap","node",e.eTap).off("add","node",e.eAdd).off("position","node",e.ePosition).off("pan zoom",e.ePosition).off("select unselect",e.eSelect).off("free","node",e.eFree).off("resize",e.eCyResize).off("afterUndo afterRedo",e.eUndoRedo)):console.log("events to unbind does not exist")},rebind:function(){var e=r();e.hasEventFields?o.on("mousedown","node",e.eMouseDown).on("mouseup","node",e.eMouseUp).on("remove","node",e.eRemove).on("tap","node",e.eTap).on("add","node",e.eAdd).on("position","node",e.ePosition).on("pan zoom",e.ePosition).on("select unselect",e.eSelect).on("free","node",e.eFree).on("resize",e.eCyResize).on("afterUndo afterRedo",e.eUndoRedo):console.log("events to rebind does not exist")}};if(c[l])return c[l].apply(o.container(),Array.prototype.slice.call(arguments,1));if("object"==typeof l||!l)return c.init.apply(o.container(),arguments);throw new Error("No such function `"+l+"` for cytoscape.js-expand-collapse")}},630:e=>{var o,n,t=(o=Math.max,n=Date.now||function(){return(new Date).getTime()},function(e,t,a){var d,i,s,l,r,c,p,u,g,f=0,h=!1,v=!0;if("function"!=typeof e)throw new TypeError("Expected a function");if(t=t<0?0:+t||0,!0===a){var y=!0;v=!1}else g=typeof(u=a),!u||"object"!=g&&"function"!=g||(y=!!a.leading,h="maxWait"in a&&o(+a.maxWait||0,t),v="trailing"in a?!!a.trailing:v);function x(o,t){t&&clearTimeout(t),i=c=p=void 0,o&&(f=n(),s=e.apply(r,d),c||i||(d=r=void 0))}function E(){var e=t-(n()-l);e<=0||e>t?x(p,i):c=setTimeout(E,e)}function m(){x(v,c)}function C(){if(d=arguments,l=n(),r=this,p=v&&(c||!y),!1===h)var o=y&&!c;else{i||y||(f=l);var a=h-(l-f),u=a<=0||a>h;u?(i&&(i=clearTimeout(i)),f=l,s=e.apply(r,d)):i||(i=setTimeout(m,a))}return u&&c?c=clearTimeout(c):c||t===h||(c=setTimeout(E,t)),o&&(u=!0,s=e.apply(r,d)),!u||c||i||(d=r=void 0),s}return C.cancel=function(){c&&clearTimeout(c),i&&clearTimeout(i),f=0,i=c=p=void 0},C});e.exports=t},438:e=>{e.exports=function(e,o,n){let t,a=!0;return function(){const d=this,i=arguments;clearTimeout(t),t=setTimeout((function(){t=null,e.apply(d,i),a=!0}),o),a&&(n.apply(d,i),a=!1)}}},537:e=>{e.exports=function(e){return{moveNodes:function(e,o,n){var t=n?o:this.getTopMostNodes(o),a=t.not(":parent");a.positions((function(o,n){return{x:a[n].position("x")+e.x,y:a[n].position("y")+e.y}}));for(var d=0;d{var t=n(78);e.exports=function(e){var o=n(537)(e);return{animatedlyMovingNodeCount:0,expandNodeBaseFunction:function(n,t,a){if(n._private.data.collapsedChildren){var d={x:n._private.position.x-n._private.data["position-before-collapse"].x,y:n._private.position.y-n._private.data["position-before-collapse"].y};n.removeData("infoLabel"),n.removeClass("cy-expand-collapse-collapsed-node"),n.trigger("expandcollapse.beforeexpand");var i=n._private.data.collapsedChildren;i.restore();for(var s=e.scratch("_cyExpandCollapse").parentData,l=0;l0&&(this.collapseNode(e),e.removeData("collapse"))},expandTopDown:function(e,o){e.data("expand")&&null!=e._private.data.collapsedChildren&&(this.expandNode(e,o),e.removeData("expand"));for(var n=e.children(),t=0;ts?(r+=g,c-=g):(r-=g,c+=g),e._private.data["y-before-fisheye"]>l?(p+=f,u-=f):(p-=f,u+=f);for(var h=[],v=[],y=0;yC?r:c,E=l>b?p:u,isFinite(w)&&(T=Math.min(x,E/Math.abs(w))),0!==w&&(N=Math.min(E,x*Math.abs(w))),s>C&&(T*=-1),l>b&&(N*=-1),this.fishEyeViewMoveNode(m,T,N,n,o,t,a,d)}return 0==i.length&&e.same(n)&&this.expandNodeBaseFunction(n,o,a),null!=e.parent()[0]&&this.fishEyeViewExpandGivenNode(e.parent()[0],o,n,t,a,d),e},getSiblings:function(o){return null==o.parent()[0]?e.nodes(":visible").orphans().difference(o):o.siblings(":visible")},fishEyeViewMoveNode:function(o,n,t,a,d,i,s,l){var r=e.collection();o.isParent()&&(r=o.children(":visible"));var c=this;if(0==r.length){var p={x:o._private.position.x+n,y:o._private.position.y+t};d&&i?(this.animatedlyMovingNodeCount++,o.animate({position:p,complete:function(){c.animatedlyMovingNodeCount--,c.animatedlyMovingNodeCount>0||!a.hasClass("cy-expand-collapse-collapsed-node")||c.expandNodeBaseFunction(a,d,s)}},{duration:l||1e3})):o.position(p)}else for(var u=0;u0?t.add(a):t.add(n)}return t},expandEdge:function(o){o.unselect();var n={edges:e.collection(),oldEdges:e.collection()},t=o.data("collapsedEdges");return void 0!==t&&t.length>0&&(o.trigger("expandcollapse.beforeexpandedge"),n.oldEdges=n.oldEdges.add(o),e.remove(o),n.edges=e.add(t),o.trigger("expandcollapse.afterexpandedge")),n},isValidEdgesForCollapse:function(e){var o=this.getEdgesDistinctEndPoints(e);return 2==o.length&&o},getEdgesDistinctEndPoints:function(e){var o=[];return e.forEach(function(e){this.containsElement(o,e.source())||o.push(e.source()),this.containsElement(o,e.target())||o.push(e.target())}.bind(this)),o},containsElement:function(e,o){for(var n=!1,t=0;t{var t;!function(){"use strict";var a=function(e){if(e){var o=n(699),t=n(540),a=null;e("core","expandCollapse",(function(e){var r=this,c=s(r,"options")||{layoutBy:null,fisheye:!0,animate:!0,animationDuration:1e3,ready:function(){},undoable:!0,cueEnabled:!0,expandCollapseCuePosition:"top-left",expandCollapseCueSize:12,expandCollapseCueLineSize:8,expandCueImage:void 0,collapseCueImage:void 0,expandCollapseCueSensitivity:1,edgeTypeInfo:"edgeType",groupEdgesOfSameTypeOnCollapse:!1,allowNestedEdgeCollapse:!0,zIndex:999};if("get"!==e){c=d(c,e);var p=function(e,o){var n={};function r(o){var t=s(e,"options");o.cueEnabled&&!t.cueEnabled?n.enableCue():!o.cueEnabled&&t.cueEnabled&&n.disableCue()}return n.setOptions=function(o){r(o),l(e,"options",o)},n.extendOptions=function(o){var n=d(s(e,"options"),o);r(n),l(e,"options",n)},n.setOption=function(o,n){var t={};t[o]=n;var a=d(s(e,"options"),t);r(a),l(e,"options",a)},n.collapse=function(n,t){var a=this.collapsibleNodes(n),l=d(s(e,"options"),t);return i(l),o.collapseGivenNodes(a,l)},n.collapseRecursively=function(o,n){var t=this.collapsibleNodes(o),a=d(s(e,"options"),n);return i(a),this.collapse(t.union(t.descendants()),a)},n.expand=function(n,t){var a=this.expandableNodes(n),l=d(s(e,"options"),t);return i(l),o.expandGivenNodes(a,l)},n.expandRecursively=function(n,t){var a=this.expandableNodes(n),l=d(s(e,"options"),t);return i(l),o.expandAllNodes(a,l)},n.collapseAll=function(o){var n=d(s(e,"options"),o);return i(n),this.collapseRecursively(this.collapsibleNodes(),n)},n.expandAll=function(o){var n=d(s(e,"options"),o);return i(n),this.expandRecursively(this.expandableNodes(),n)},n.isExpandable=function(e){return e.hasClass("cy-expand-collapse-collapsed-node")},n.isCollapsible=function(e){return!this.isExpandable(e)&&e.isParent()},n.collapsibleNodes=function(o){var n=this;return(o||e.nodes()).filter((function(e,o){return"number"==typeof e&&(e=o),n.isCollapsible(e)}))},n.expandableNodes=function(o){var n=this;return(o||e.nodes()).filter((function(e,o){return"number"==typeof e&&(e=o),n.isExpandable(e)}))},n.getCollapsedChildren=function(e){return e.data("collapsedChildren")},n.getCollapsedChildrenRecursively=function(n){var t=e.collection();return o.getCollapsedChildrenRecursively(n,t)},n.getAllCollapsedChildrenRecursively=function(){var o,n=e.collection(),t=e.nodes(".cy-expand-collapse-collapsed-node");for(o=0;o[e,e])));var c={edges:e.collection(),oldEdges:e.collection()};return r.forEach(function(e){const n=e[1].id();var t=e[0].connectedEdges('[source = "'+n+'"],[target = "'+n+'"]');if(e[0].id()===n&&(t=e[0].connectedEdges('[source = "'+n+'"][target = "'+n+'"]')),t.length>=2){var a=o.collapseGivenEdges(t,l);c.oldEdges=c.oldEdges.add(a.oldEdges),c.edges=c.edges.add(a.edges)}}.bind(this)),c},n.expandEdgesBetweenNodes=function(o){var n,t,a=e.collection(),d=(t=[],(n=o).slice(0,n.length-1).forEach((function(e,o){n.slice(o+1,n.length).forEach((function(o){t.push([e,o])}))})),t);return d.push(...o.map((e=>[e,e]))),d.forEach(function(e){const o=e[1].id();var n=e[0].connectedEdges('.cy-expand-collapse-collapsed-edge[source = "'+o+'"],[target = "'+o+'"]');e[0].id()===o&&(n=e[0].connectedEdges('[source = "'+o+'"][target = "'+o+'"]')),a=a.union(n)}.bind(this)),this.expandEdges(a)},n.collapseAllEdges=function(o){return this.collapseEdgesBetweenNodes(e.edges().connectedNodes(),o)},n.expandAllEdges=function(){var o=e.edges(".cy-expand-collapse-collapsed-edge"),n={edges:e.collection(),oldEdges:e.collection()},t=this.expandEdges(o);return n.oldEdges=n.oldEdges.add(t.oldEdges),n.edges=n.edges.add(t.edges),n},n.loadJson=function(e){a.loadJson(e)},n.saveJson=function(e,o){return a.saveJson(e,o)},n}(r,n(272)(r));a=n(554)(r,p),l(r,"api",p),o(r,p),t(c,r,p),c.cueEnabled||t("unbind",r,p),c.ready&&c.ready(),l(r,"options",c),l(r,"parentData",{})}return s(r,"api")}))}function d(e,o){var n={};for(var t in e)n[t]=e[t];for(var t in o)n.hasOwnProperty(t)&&(n[t]=o[t]);return n}function i(e){var o="function"==typeof e.animate?e.animate.call():e.animate,n="function"==typeof e.fisheye?e.fisheye.call():e.fisheye;e.animate=o,e.fisheye=n}function s(e,o){void 0===e.scratch("_cyExpandCollapse")&&e.scratch("_cyExpandCollapse",{});var n=e.scratch("_cyExpandCollapse");return void 0===o?n:n[o]}function l(e,o,n){s(e)[o]=n}};e.exports&&(e.exports=a),void 0===(t=function(){return a}.call(o,n,o,e))||(e.exports=t),"undefined"!=typeof cytoscape&&a(cytoscape)}()},554:e=>{e.exports=function(e,o){function n(o,a,d,i){o.sort((e=>"edges"===e.group?1:-1));let s=e.collection();for(let l=0;l{e.exports=function(e,o){if(null!=e.undoRedo){for(var n=e.undoRedo({},!0),t={layoutBy:null,animate:!1,fisheye:!1},a=["collapse","collapseRecursively","collapseAll","expand","expandRecursively","expandAll"],d=0;d0?o[n](a.options):o[n](c,a.options)):(r.oldData=i(),r.nodes=n.indexOf("All")>0?o[n](t):o[n](e.collection(c),t),s=a.oldData,l={},e.nodes().not(":parent").positions((function(e,o){"number"==typeof e&&(e=o),l[e.id()]={x:e.position("x"),y:e.position("y")};var n=s[e.id()];return{x:n.x,y:n.y}}))),r}}function l(n){var t=n.options,a=n.edges,d={};if(d.options=t,n.firstTime){var i=o.collapseEdges(a,t);d.edges=i.edges,d.oldEdges=i.oldEdges,d.firstTime=!1}else d.oldEdges=a,d.edges=n.oldEdges,n.edges.length>0&&n.oldEdges.length>0&&(e.remove(n.edges),e.add(n.oldEdges));return d}function r(n){var t=n.options,a={};if(a.options=t,n.firstTime){var d=o.collapseEdgesBetweenNodes(n.nodes,t);a.edges=d.edges,a.oldEdges=d.oldEdges,a.firstTime=!1}else a.edges=n.oldEdges,a.oldEdges=n.edges,n.edges.length>0&&n.oldEdges.length>0&&(e.remove(n.edges),e.add(n.oldEdges));return a}function c(n){var t=n.options,a={};if(a.options=t,n.firstTime){var d=o.collapseAllEdges(t);a.edges=d.edges,a.oldEdges=d.oldEdges,a.firstTime=!1}else a.edges=n.oldEdges,a.oldEdges=n.edges,n.edges.length>0&&n.oldEdges.length>0&&(e.remove(n.edges),e.add(n.oldEdges));return a}function p(n){var t=n.options,a={};if(a.options=t,n.firstTime){var d=o.expandEdges(n.edges);a.edges=d.edges,a.oldEdges=d.oldEdges,a.firstTime=!1}else a.oldEdges=n.edges,a.edges=n.oldEdges,n.edges.length>0&&n.oldEdges.length>0&&(e.remove(n.edges),e.add(n.oldEdges));return a}function u(n){var t=n.options,a={};if(a.options=t,n.firstTime){var d=o.expandEdgesBetweenNodes(n.nodes,t);a.edges=d.edges,a.oldEdges=d.oldEdges,a.firstTime=!1}else a.edges=n.oldEdges,a.oldEdges=n.edges,n.edges.length>0&&n.oldEdges.length>0&&(e.remove(n.edges),e.add(n.oldEdges));return a}function g(n){var t=n.options,a={};if(a.options=t,n.firstTime){var d=o.expandAllEdges(t);a.edges=d.edges,a.oldEdges=d.oldEdges,a.firstTime=!1}else a.edges=n.oldEdges,a.oldEdges=n.edges,n.edges.length>0&&n.oldEdges.length>0&&(e.remove(n.edges),e.add(n.oldEdges));return a}}}},o={},function n(t){var a=o[t];if(void 0!==a)return a.exports;var d=o[t]={exports:{}};return e[t](d,d.exports,n),d.exports}(497);var e,o})); -------------------------------------------------------------------------------- /src/expandCollapseUtilities.js: -------------------------------------------------------------------------------- 1 | var boundingBoxUtilities = require('./boundingBoxUtilities'); 2 | 3 | // Expand collapse utilities 4 | function expandCollapseUtilities(cy) { 5 | var elementUtilities = require('./elementUtilities')(cy); 6 | return { 7 | //the number of nodes moving animatedly after expand operation 8 | animatedlyMovingNodeCount: 0, 9 | /* 10 | * A funtion basicly expanding a node, it is to be called when a node is expanded anyway. 11 | * Single parameter indicates if the node is expanded alone and if it is truthy then layoutBy parameter is considered to 12 | * perform layout after expand. 13 | */ 14 | expandNodeBaseFunction: function (node, single, layoutBy) { 15 | if (!node._private.data.collapsedChildren){ 16 | return; 17 | } 18 | 19 | //check how the position of the node is changed 20 | var positionDiff = { 21 | x: node._private.position.x - node._private.data['position-before-collapse'].x, 22 | y: node._private.position.y - node._private.data['position-before-collapse'].y 23 | }; 24 | 25 | node.removeData("infoLabel"); 26 | node.removeClass('cy-expand-collapse-collapsed-node'); 27 | 28 | node.trigger("expandcollapse.beforeexpand"); 29 | var restoredNodes = node._private.data.collapsedChildren; 30 | restoredNodes.restore(); 31 | var parentData = cy.scratch('_cyExpandCollapse').parentData; 32 | for(var i = 0; i < restoredNodes.length; i++){ 33 | delete parentData[restoredNodes[i].id()]; 34 | } 35 | cy.scratch('_cyExpandCollapse').parentData = parentData; 36 | this.repairEdges(node); 37 | node._private.data.collapsedChildren = null; 38 | 39 | elementUtilities.moveNodes(positionDiff, node.children()); 40 | node.removeData('position-before-collapse'); 41 | 42 | node.trigger("position"); // position not triggered by default when nodes are moved 43 | node.trigger("expandcollapse.afterexpand"); 44 | 45 | // If expand is called just for one node then call end operation to perform layout 46 | if (single) { 47 | this.endOperation(layoutBy, node); 48 | } 49 | }, 50 | /* 51 | * A helper function to collapse given nodes in a simple way (Without performing layout afterward) 52 | * It collapses all root nodes bottom up. 53 | */ 54 | simpleCollapseGivenNodes: function (nodes) {//*// 55 | nodes.data("collapse", true); 56 | var roots = elementUtilities.getTopMostNodes(nodes); 57 | for (var i = 0; i < roots.length; i++) { 58 | var root = roots[i]; 59 | 60 | // Collapse the nodes in bottom up order 61 | this.collapseBottomUp(root); 62 | } 63 | 64 | return nodes; 65 | }, 66 | /* 67 | * A helper function to expand given nodes in a simple way (Without performing layout afterward) 68 | * It expands all top most nodes top down. 69 | */ 70 | simpleExpandGivenNodes: function (nodes, applyFishEyeViewToEachNode) { 71 | nodes.data("expand", true); // Mark that the nodes are still to be expanded 72 | var roots = elementUtilities.getTopMostNodes(nodes); 73 | for (var i = 0; i < roots.length; i++) { 74 | var root = roots[i]; 75 | this.expandTopDown(root, applyFishEyeViewToEachNode); // For each root node expand top down 76 | } 77 | return nodes; 78 | }, 79 | /* 80 | * Expands all nodes by expanding all top most nodes top down with their descendants. 81 | */ 82 | simpleExpandAllNodes: function (nodes, applyFishEyeViewToEachNode) { 83 | if (nodes === undefined) { 84 | nodes = cy.nodes(); 85 | } 86 | var orphans; 87 | orphans = elementUtilities.getTopMostNodes(nodes); 88 | var expandStack = []; 89 | for (var i = 0; i < orphans.length; i++) { 90 | var root = orphans[i]; 91 | this.expandAllTopDown(root, expandStack, applyFishEyeViewToEachNode); 92 | } 93 | return expandStack; 94 | }, 95 | /* 96 | * The operation to be performed after expand/collapse. It rearrange nodes by layoutBy parameter. 97 | */ 98 | endOperation: function (layoutBy, nodes) { 99 | var self = this; 100 | cy.ready(function () { 101 | setTimeout(function() { 102 | elementUtilities.rearrange(layoutBy); 103 | if(cy.scratch('_cyExpandCollapse').selectableChanged){ 104 | nodes.selectify(); 105 | cy.scratch('_cyExpandCollapse').selectableChanged = false; 106 | } 107 | }, 0); 108 | 109 | }); 110 | }, 111 | /* 112 | * Calls simple expandAllNodes. Then performs end operation. 113 | */ 114 | expandAllNodes: function (nodes, options) {//*// 115 | var expandedStack = this.simpleExpandAllNodes(nodes, options.fisheye); 116 | 117 | this.endOperation(options.layoutBy, nodes); 118 | 119 | /* 120 | * return the nodes to undo the operation 121 | */ 122 | return expandedStack; 123 | }, 124 | /* 125 | * Expands the root and its collapsed descendents in top down order. 126 | */ 127 | expandAllTopDown: function (root, expandStack, applyFishEyeViewToEachNode) { 128 | if (root._private.data.collapsedChildren != null) { 129 | expandStack.push(root); 130 | this.expandNode(root, applyFishEyeViewToEachNode); 131 | } 132 | var children = root.children(); 133 | for (var i = 0; i < children.length; i++) { 134 | var node = children[i]; 135 | this.expandAllTopDown(node, expandStack, applyFishEyeViewToEachNode); 136 | } 137 | }, 138 | //Expand the given nodes perform end operation after expandation 139 | expandGivenNodes: function (nodes, options) { 140 | // If there is just one node to expand we need to animate for fisheye view, but if there are more then one node we do not 141 | if (nodes.length === 1) { 142 | 143 | var node = nodes[0]; 144 | if (node._private.data.collapsedChildren != null) { 145 | // Expand the given node the third parameter indicates that the node is simple which ensures that fisheye parameter will be considered 146 | this.expandNode(node, options.fisheye, true, options.animate, options.layoutBy, options.animationDuration); 147 | } 148 | } 149 | else { 150 | // First expand given nodes and then perform layout according to the layoutBy parameter 151 | this.simpleExpandGivenNodes(nodes, options.fisheye); 152 | this.endOperation(options.layoutBy, nodes); 153 | } 154 | 155 | /* 156 | * return the nodes to undo the operation 157 | */ 158 | return nodes; 159 | }, 160 | //collapse the given nodes then perform end operation 161 | collapseGivenNodes: function (nodes, options) { 162 | /* 163 | * In collapse operation there is no fisheye view to be applied so there is no animation to be destroyed here. We can do this 164 | * in a batch. 165 | */ 166 | cy.startBatch(); 167 | this.simpleCollapseGivenNodes(nodes/*, options*/); 168 | cy.endBatch(); 169 | 170 | nodes.trigger("position"); // position not triggered by default when collapseNode is called 171 | this.endOperation(options.layoutBy, nodes); 172 | 173 | // Update the style 174 | cy.style().update(); 175 | 176 | /* 177 | * return the nodes to undo the operation 178 | */ 179 | return nodes; 180 | }, 181 | //collapse the nodes in bottom up order starting from the root 182 | collapseBottomUp: function (root) { 183 | var children = root.children(); 184 | for (var i = 0; i < children.length; i++) { 185 | var node = children[i]; 186 | this.collapseBottomUp(node); 187 | } 188 | //If the root is a compound node to be collapsed then collapse it 189 | if (root.data("collapse") && root.children().length > 0) { 190 | this.collapseNode(root); 191 | root.removeData("collapse"); 192 | } 193 | }, 194 | //expand the nodes in top down order starting from the root 195 | expandTopDown: function (root, applyFishEyeViewToEachNode) { 196 | if (root.data("expand") && root._private.data.collapsedChildren != null) { 197 | // Expand the root and unmark its expand data to specify that it is no more to be expanded 198 | this.expandNode(root, applyFishEyeViewToEachNode); 199 | root.removeData("expand"); 200 | } 201 | // Make a recursive call for children of root 202 | var children = root.children(); 203 | for (var i = 0; i < children.length; i++) { 204 | var node = children[i]; 205 | this.expandTopDown(node); 206 | } 207 | }, 208 | // Converst the rendered position to model position according to global pan and zoom values 209 | convertToModelPosition: function (renderedPosition) { 210 | var pan = cy.pan(); 211 | var zoom = cy.zoom(); 212 | 213 | var x = (renderedPosition.x - pan.x) / zoom; 214 | var y = (renderedPosition.y - pan.y) / zoom; 215 | 216 | return { 217 | x: x, 218 | y: y 219 | }; 220 | }, 221 | /* 222 | * This method expands the given node. It considers applyFishEyeView, animate and layoutBy parameters. 223 | * It also considers single parameter which indicates if this node is expanded alone. If this parameter is truthy along with 224 | * applyFishEyeView parameter then the state of view port is to be changed to have extra space on the screen (if needed) before appliying the 225 | * fisheye view. 226 | */ 227 | expandNode: function (node, applyFishEyeView, single, animate, layoutBy, animationDuration) { 228 | var self = this; 229 | 230 | var commonExpandOperation = function (node, applyFishEyeView, single, animate, layoutBy, animationDuration) { 231 | if (applyFishEyeView) { 232 | 233 | node._private.data['width-before-fisheye'] = node._private.data['size-before-collapse'].w; 234 | node._private.data['height-before-fisheye'] = node._private.data['size-before-collapse'].h; 235 | 236 | // Fisheye view expand the node. 237 | // The first paramter indicates the node to apply fisheye view, the third parameter indicates the node 238 | // to be expanded after fisheye view is applied. 239 | self.fishEyeViewExpandGivenNode(node, single, node, animate, layoutBy, animationDuration); 240 | } 241 | 242 | // If one of these parameters is truthy it means that expandNodeBaseFunction is already to be called. 243 | // However if none of them is truthy we need to call it here. 244 | if (!single || !applyFishEyeView || !animate) { 245 | self.expandNodeBaseFunction(node, single, layoutBy); 246 | } 247 | }; 248 | 249 | if (node._private.data.collapsedChildren != null) { 250 | this.storeWidthHeight(node); 251 | var animating = false; // Variable to check if there is a current animation, if there is commonExpandOperation will be called after animation 252 | 253 | // If the node is the only node to expand and fisheye view should be applied, then change the state of viewport 254 | // to create more space on screen (If needed) 255 | if (applyFishEyeView && single) { 256 | var topLeftPosition = this.convertToModelPosition({x: 0, y: 0}); 257 | var bottomRightPosition = this.convertToModelPosition({x: cy.width(), y: cy.height()}); 258 | var padding = 80; 259 | var bb = { 260 | x1: topLeftPosition.x, 261 | x2: bottomRightPosition.x, 262 | y1: topLeftPosition.y, 263 | y2: bottomRightPosition.y 264 | }; 265 | 266 | var nodeBB = { 267 | x1: node._private.position.x - node._private.data['size-before-collapse'].w / 2 - padding, 268 | x2: node._private.position.x + node._private.data['size-before-collapse'].w / 2 + padding, 269 | y1: node._private.position.y - node._private.data['size-before-collapse'].h / 2 - padding, 270 | y2: node._private.position.y + node._private.data['size-before-collapse'].h / 2 + padding 271 | }; 272 | 273 | var unionBB = boundingBoxUtilities.getUnion(nodeBB, bb); 274 | 275 | // If these bboxes are not equal then we need to change the viewport state (by pan and zoom) 276 | if (!boundingBoxUtilities.equalBoundingBoxes(unionBB, bb)) { 277 | var viewPort = cy.getFitViewport(unionBB, 10); 278 | var self = this; 279 | animating = animate; // Signal that there is an animation now and commonExpandOperation will be called after animation 280 | // Check if we need to animate during pan and zoom 281 | if (animate) { 282 | cy.animate({ 283 | pan: viewPort.pan, 284 | zoom: viewPort.zoom, 285 | complete: function () { 286 | commonExpandOperation(node, applyFishEyeView, single, animate, layoutBy, animationDuration); 287 | } 288 | }, { 289 | duration: animationDuration || 1000 290 | }); 291 | } 292 | else { 293 | cy.zoom(viewPort.zoom); 294 | cy.pan(viewPort.pan); 295 | } 296 | } 297 | } 298 | 299 | // If animating is not true we need to call commonExpandOperation here 300 | if (!animating) { 301 | commonExpandOperation(node, applyFishEyeView, single, animate, layoutBy, animationDuration); 302 | } 303 | 304 | //return the node to undo the operation 305 | return node; 306 | } 307 | }, 308 | //collapse the given node without performing end operation 309 | collapseNode: function (node) { 310 | if (node._private.data.collapsedChildren == null) { 311 | node.data('position-before-collapse', { 312 | x: node.position().x, 313 | y: node.position().y 314 | }); 315 | 316 | node.data('size-before-collapse', { 317 | w: node.outerWidth(), 318 | h: node.outerHeight() 319 | }); 320 | 321 | var children = node.children(); 322 | 323 | children.unselect(); 324 | children.connectedEdges().unselect(); 325 | 326 | node.trigger("expandcollapse.beforecollapse"); 327 | 328 | this.barrowEdgesOfcollapsedChildren(node); 329 | this.removeChildren(node, node); 330 | node.addClass('cy-expand-collapse-collapsed-node'); 331 | 332 | node.trigger("expandcollapse.aftercollapse"); 333 | 334 | node.position(node.data('position-before-collapse')); 335 | 336 | //return the node to undo the operation 337 | return node; 338 | } 339 | }, 340 | storeWidthHeight: function (node) {//*// 341 | if (node != null) { 342 | node._private.data['x-before-fisheye'] = this.xPositionInParent(node); 343 | node._private.data['y-before-fisheye'] = this.yPositionInParent(node); 344 | node._private.data['width-before-fisheye'] = node.outerWidth(); 345 | node._private.data['height-before-fisheye'] = node.outerHeight(); 346 | 347 | if (node.parent()[0] != null) { 348 | this.storeWidthHeight(node.parent()[0]); 349 | } 350 | } 351 | 352 | }, 353 | /* 354 | * Apply fisheye view to the given node. nodeToExpand will be expanded after the operation. 355 | * The other parameter are to be passed by parameters directly in internal function calls. 356 | */ 357 | fishEyeViewExpandGivenNode: function (node, single, nodeToExpand, animate, layoutBy, animationDuration) { 358 | var siblings = this.getSiblings(node); 359 | 360 | var x_a = this.xPositionInParent(node); 361 | var y_a = this.yPositionInParent(node); 362 | 363 | var d_x_left = Math.abs((node._private.data['width-before-fisheye'] - node.outerWidth()) / 2); 364 | var d_x_right = Math.abs((node._private.data['width-before-fisheye'] - node.outerWidth()) / 2); 365 | var d_y_upper = Math.abs((node._private.data['height-before-fisheye'] - node.outerHeight()) / 2); 366 | var d_y_lower = Math.abs((node._private.data['height-before-fisheye'] - node.outerHeight()) / 2); 367 | 368 | var abs_diff_on_x = Math.abs(node._private.data['x-before-fisheye'] - x_a); 369 | var abs_diff_on_y = Math.abs(node._private.data['y-before-fisheye'] - y_a); 370 | 371 | // Center went to LEFT 372 | if (node._private.data['x-before-fisheye'] > x_a) { 373 | d_x_left = d_x_left + abs_diff_on_x; 374 | d_x_right = d_x_right - abs_diff_on_x; 375 | } 376 | // Center went to RIGHT 377 | else { 378 | d_x_left = d_x_left - abs_diff_on_x; 379 | d_x_right = d_x_right + abs_diff_on_x; 380 | } 381 | 382 | // Center went to UP 383 | if (node._private.data['y-before-fisheye'] > y_a) { 384 | d_y_upper = d_y_upper + abs_diff_on_y; 385 | d_y_lower = d_y_lower - abs_diff_on_y; 386 | } 387 | // Center went to DOWN 388 | else { 389 | d_y_upper = d_y_upper - abs_diff_on_y; 390 | d_y_lower = d_y_lower + abs_diff_on_y; 391 | } 392 | 393 | var xPosInParentSibling = []; 394 | var yPosInParentSibling = []; 395 | 396 | for (var i = 0; i < siblings.length; i++) { 397 | xPosInParentSibling.push(this.xPositionInParent(siblings[i])); 398 | yPosInParentSibling.push(this.yPositionInParent(siblings[i])); 399 | } 400 | 401 | for (var i = 0; i < siblings.length; i++) { 402 | var sibling = siblings[i]; 403 | 404 | var x_b = xPosInParentSibling[i]; 405 | var y_b = yPosInParentSibling[i]; 406 | 407 | var slope = (y_b - y_a) / (x_b - x_a); 408 | 409 | var d_x = 0; 410 | var d_y = 0; 411 | var T_x = 0; 412 | var T_y = 0; 413 | 414 | // Current sibling is on the LEFT 415 | if (x_a > x_b) { 416 | d_x = d_x_left; 417 | } 418 | // Current sibling is on the RIGHT 419 | else { 420 | d_x = d_x_right; 421 | } 422 | // Current sibling is on the UPPER side 423 | if (y_a > y_b) { 424 | d_y = d_y_upper; 425 | } 426 | // Current sibling is on the LOWER side 427 | else { 428 | d_y = d_y_lower; 429 | } 430 | 431 | if (isFinite(slope)) { 432 | T_x = Math.min(d_x, (d_y / Math.abs(slope))); 433 | } 434 | 435 | if (slope !== 0) { 436 | T_y = Math.min(d_y, (d_x * Math.abs(slope))); 437 | } 438 | 439 | if (x_a > x_b) { 440 | T_x = -1 * T_x; 441 | } 442 | 443 | if (y_a > y_b) { 444 | T_y = -1 * T_y; 445 | } 446 | 447 | // Move the sibling in the special way 448 | this.fishEyeViewMoveNode(sibling, T_x, T_y, nodeToExpand, single, animate, layoutBy, animationDuration); 449 | } 450 | 451 | // If there is no sibling call expand node base function here else it is to be called one of fishEyeViewMoveNode() calls 452 | if (siblings.length == 0 && node.same(nodeToExpand)) { 453 | this.expandNodeBaseFunction(nodeToExpand, single, layoutBy); 454 | } 455 | 456 | if (node.parent()[0] != null) { 457 | // Apply fisheye view to the parent node as well ( If exists ) 458 | this.fishEyeViewExpandGivenNode(node.parent()[0], single, nodeToExpand, animate, layoutBy, animationDuration); 459 | } 460 | 461 | return node; 462 | }, 463 | getSiblings: function (node) { 464 | var siblings; 465 | 466 | if (node.parent()[0] == null) { 467 | var orphans = cy.nodes(":visible").orphans(); 468 | siblings = orphans.difference(node); 469 | } else { 470 | siblings = node.siblings(":visible"); 471 | } 472 | 473 | return siblings; 474 | }, 475 | /* 476 | * Move node operation specialized for fish eye view expand operation 477 | * Moves the node by moving its descandents. Movement is animated if both single and animate flags are truthy. 478 | */ 479 | fishEyeViewMoveNode: function (node, T_x, T_y, nodeToExpand, single, animate, layoutBy, animationDuration) { 480 | var childrenList = cy.collection(); 481 | if(node.isParent()){ 482 | childrenList = node.children(":visible"); 483 | } 484 | var self = this; 485 | 486 | /* 487 | * If the node is simple move itself directly else move it by moving its children by a self recursive call 488 | */ 489 | if (childrenList.length == 0) { 490 | var newPosition = {x: node._private.position.x + T_x, y: node._private.position.y + T_y}; 491 | if (!single || !animate) { 492 | node.position(newPosition); // at this point, position should be updated 493 | } 494 | else { 495 | this.animatedlyMovingNodeCount++; 496 | node.animate({ 497 | position: newPosition, 498 | complete: function () { 499 | self.animatedlyMovingNodeCount--; 500 | if (self.animatedlyMovingNodeCount > 0 || !nodeToExpand.hasClass('cy-expand-collapse-collapsed-node')) { 501 | 502 | return; 503 | } 504 | 505 | // If all nodes are moved we are ready to expand so call expand node base function 506 | self.expandNodeBaseFunction(nodeToExpand, single, layoutBy); 507 | 508 | } 509 | }, { 510 | duration: animationDuration || 1000 511 | }); 512 | } 513 | } 514 | else { 515 | for (var i = 0; i < childrenList.length; i++) { 516 | this.fishEyeViewMoveNode(childrenList[i], T_x, T_y, nodeToExpand, single, animate, layoutBy, animationDuration); 517 | } 518 | } 519 | }, 520 | xPositionInParent: function (node) {//*// 521 | var parent = node.parent()[0]; 522 | var x_a = 0.0; 523 | 524 | // Given node is not a direct child of the the root graph 525 | if (parent != null) { 526 | x_a = node.relativePosition('x') + (parent.width() / 2); 527 | } 528 | // Given node is a direct child of the the root graph 529 | 530 | else { 531 | x_a = node.position('x'); 532 | } 533 | 534 | return x_a; 535 | }, 536 | yPositionInParent: function (node) {//*// 537 | var parent = node.parent()[0]; 538 | 539 | var y_a = 0.0; 540 | 541 | // Given node is not a direct child of the the root graph 542 | if (parent != null) { 543 | y_a = node.relativePosition('y') + (parent.height() / 2); 544 | } 545 | // Given node is a direct child of the the root graph 546 | 547 | else { 548 | y_a = node.position('y'); 549 | } 550 | 551 | return y_a; 552 | }, 553 | /* 554 | * for all children of the node parameter call this method 555 | * with the same root parameter, 556 | * remove the child and add the removed child to the collapsedchildren data 557 | * of the root to restore them in the case of expandation 558 | * root._private.data.collapsedChildren keeps the nodes to restore when the 559 | * root is expanded 560 | */ 561 | removeChildren: function (node, root) { 562 | var children = node.children(); 563 | for (var i = 0; i < children.length; i++) { 564 | var child = children[i]; 565 | this.removeChildren(child, root); 566 | var parentData = cy.scratch('_cyExpandCollapse').parentData; 567 | parentData[child.id()] = child.parent(); 568 | cy.scratch('_cyExpandCollapse').parentData = parentData; 569 | var removedChild = child.remove(); 570 | if (root._private.data.collapsedChildren == null) { 571 | root._private.data.collapsedChildren = removedChild; 572 | } 573 | else { 574 | root._private.data.collapsedChildren = root._private.data.collapsedChildren.union(removedChild); 575 | } 576 | } 577 | }, 578 | isMetaEdge: function(edge) { 579 | return edge.hasClass("cy-expand-collapse-meta-edge"); 580 | }, 581 | barrowEdgesOfcollapsedChildren: function(node) { 582 | var relatedNodes = node.descendants(); 583 | var edges = relatedNodes.edgesWith(cy.nodes().not(relatedNodes.union(node))); 584 | 585 | var relatedNodeMap = {}; 586 | 587 | relatedNodes.each(function(ele, i) { 588 | if(typeof ele === "number") { 589 | ele = i; 590 | } 591 | relatedNodeMap[ele.id()] = true; 592 | }); 593 | 594 | for (var i = 0; i < edges.length; i++) { 595 | var edge = edges[i]; 596 | var source = edge.source(); 597 | var target = edge.target(); 598 | 599 | if (!this.isMetaEdge(edge)) { // is original 600 | var originalEndsData = { 601 | source: source, 602 | target: target 603 | }; 604 | 605 | edge.addClass("cy-expand-collapse-meta-edge"); 606 | edge.data('originalEnds', originalEndsData); 607 | } 608 | 609 | edge.move({ 610 | target: !relatedNodeMap[target.id()] ? target.id() : node.id(), 611 | source: !relatedNodeMap[source.id()] ? source.id() : node.id() 612 | }); 613 | } 614 | }, 615 | findNewEnd: function(node) { 616 | var current = node; 617 | var parentData = cy.scratch('_cyExpandCollapse').parentData; 618 | var parent = parentData[current.id()]; 619 | 620 | while( !current.inside() ) { 621 | current = parent; 622 | parent = parentData[parent.id()]; 623 | } 624 | 625 | return current; 626 | }, 627 | repairEdges: function(node) { 628 | var connectedMetaEdges = node.connectedEdges('.cy-expand-collapse-meta-edge'); 629 | 630 | for (var i = 0; i < connectedMetaEdges.length; i++) { 631 | var edge = connectedMetaEdges[i]; 632 | var originalEnds = edge.data('originalEnds'); 633 | var currentSrcId = edge.data('source'); 634 | var currentTgtId = edge.data('target'); 635 | 636 | if ( currentSrcId === node.id() ) { 637 | edge = edge.move({ 638 | source: this.findNewEnd(originalEnds.source).id() 639 | }); 640 | } else { 641 | edge = edge.move({ 642 | target: this.findNewEnd(originalEnds.target).id() 643 | }); 644 | } 645 | 646 | if ( edge.data('source') === originalEnds.source.id() && edge.data('target') === originalEnds.target.id() ) { 647 | edge.removeClass('cy-expand-collapse-meta-edge'); 648 | edge.removeData('originalEnds'); 649 | } 650 | } 651 | }, 652 | /*node is an outer node of root 653 | if root is not it's anchestor 654 | and it is not the root itself*/ 655 | isOuterNode: function (node, root) {//*// 656 | var temp = node; 657 | while (temp != null) { 658 | if (temp == root) { 659 | return false; 660 | } 661 | temp = temp.parent()[0]; 662 | } 663 | return true; 664 | }, 665 | /** 666 | * Get all collapsed children - including nested ones 667 | * @param node : a collapsed node 668 | * @param collapsedChildren : a collection to store the result 669 | * @return : collapsed children 670 | */ 671 | getCollapsedChildrenRecursively: function(node, collapsedChildren){ 672 | var children = node.data('collapsedChildren') || []; 673 | var i; 674 | for (i=0; i < children.length; i++){ 675 | if (children[i].data('collapsedChildren')){ 676 | collapsedChildren = collapsedChildren.union(this.getCollapsedChildrenRecursively(children[i], collapsedChildren)); 677 | } 678 | collapsedChildren = collapsedChildren.union(children[i]); 679 | } 680 | return collapsedChildren; 681 | }, 682 | /* -------------------------------------- start section edge expand collapse -------------------------------------- */ 683 | collapseGivenEdges: function (edges, options) { 684 | edges.unselect(); 685 | var nodes = edges.connectedNodes(); 686 | var edgesToCollapse = {}; 687 | // group edges by type if this option is set to true 688 | if (options.groupEdgesOfSameTypeOnCollapse) { 689 | edges.forEach(function (edge) { 690 | var edgeType = "unknown"; 691 | if (options.edgeTypeInfo !== undefined) { 692 | edgeType = options.edgeTypeInfo instanceof Function ? options.edgeTypeInfo.call(edge) : edge.data()[options.edgeTypeInfo]; 693 | } 694 | if (edgesToCollapse.hasOwnProperty(edgeType)) { 695 | edgesToCollapse[edgeType].edges = edgesToCollapse[edgeType].edges.add(edge); 696 | 697 | if (edgesToCollapse[edgeType].directionType == "unidirection" && (edgesToCollapse[edgeType].source != edge.source().id() || edgesToCollapse[edgeType].target != edge.target().id())) { 698 | edgesToCollapse[edgeType].directionType = "bidirection"; 699 | } 700 | } else { 701 | var edgesX = cy.collection(); 702 | edgesX = edgesX.add(edge); 703 | edgesToCollapse[edgeType] = { edges: edgesX, directionType: "unidirection", source: edge.source().id(), target: edge.target().id() } 704 | } 705 | }); 706 | } else { 707 | edgesToCollapse["unknown"] = { edges: edges, directionType: "unidirection", source: edges[0].source().id(), target: edges[0].target().id() } 708 | for (var i = 0; i < edges.length; i++) { 709 | if (edgesToCollapse["unknown"].directionType == "unidirection" && (edgesToCollapse["unknown"].source != edges[i].source().id() || edgesToCollapse["unknown"].target != edges[i].target().id())) { 710 | edgesToCollapse["unknown"].directionType = "bidirection"; 711 | break; 712 | } 713 | } 714 | } 715 | 716 | var result = { edges: cy.collection(), oldEdges: cy.collection() } 717 | var newEdges = []; 718 | for (const edgeGroupType in edgesToCollapse) { 719 | if (edgesToCollapse[edgeGroupType].edges.length < 2) { 720 | continue; 721 | } 722 | edges.trigger('expandcollapse.beforecollapseedge'); 723 | result.oldEdges = result.oldEdges.add(edgesToCollapse[edgeGroupType].edges); 724 | var newEdge = {}; 725 | newEdge.group = "edges"; 726 | newEdge.data = {}; 727 | newEdge.data.source = edgesToCollapse[edgeGroupType].source; 728 | newEdge.data.target = edgesToCollapse[edgeGroupType].target; 729 | var id1 = nodes[0].id(); 730 | var id2 = id1; 731 | if (nodes[1]) { 732 | id2 = nodes[1].id(); 733 | } 734 | newEdge.data.id = "collapsedEdge_" + id1 + "_" + id2 + "_" + edgeGroupType + "_" + Math.floor(Math.random() * Date.now()); 735 | newEdge.data.collapsedEdges = cy.collection(); 736 | 737 | edgesToCollapse[edgeGroupType].edges.forEach(function (edge) { 738 | newEdge.data.collapsedEdges = newEdge.data.collapsedEdges.add(edge); 739 | }); 740 | 741 | newEdge.data.collapsedEdges = this.check4nestedCollapse(newEdge.data.collapsedEdges, options); 742 | 743 | var edgesTypeField = "edgeType"; 744 | if (options.edgeTypeInfo !== undefined) { 745 | edgesTypeField = options.edgeTypeInfo instanceof Function ? edgesTypeField : options.edgeTypeInfo; 746 | } 747 | newEdge.data[edgesTypeField] = edgeGroupType; 748 | 749 | newEdge.data["directionType"] = edgesToCollapse[edgeGroupType].directionType; 750 | newEdge.classes = "cy-expand-collapse-collapsed-edge"; 751 | 752 | newEdges.push(newEdge); 753 | cy.remove(edgesToCollapse[edgeGroupType].edges); 754 | edges.trigger('expandcollapse.aftercollapseedge'); 755 | } 756 | 757 | result.edges = cy.add(newEdges); 758 | return result; 759 | }, 760 | 761 | check4nestedCollapse: function(edges2collapse, options){ 762 | if (options.allowNestedEdgeCollapse) { 763 | return edges2collapse; 764 | } 765 | let r = cy.collection(); 766 | for (let i = 0; i < edges2collapse.length; i++) { 767 | let curr = edges2collapse[i]; 768 | let collapsedEdges = curr.data('collapsedEdges'); 769 | if (collapsedEdges && collapsedEdges.length > 0) { 770 | r = r.add(collapsedEdges); 771 | } else { 772 | r = r.add(curr); 773 | } 774 | } 775 | return r; 776 | }, 777 | 778 | expandEdge: function (edge) { 779 | edge.unselect(); 780 | var result = { edges: cy.collection(), oldEdges: cy.collection() } 781 | var edges = edge.data('collapsedEdges'); 782 | if (edges !== undefined && edges.length > 0) { 783 | edge.trigger('expandcollapse.beforeexpandedge'); 784 | result.oldEdges = result.oldEdges.add(edge); 785 | cy.remove(edge); 786 | result.edges = cy.add(edges); 787 | edge.trigger('expandcollapse.afterexpandedge'); 788 | } 789 | return result; 790 | }, 791 | 792 | //if the edges are only between two nodes (valid for collpasing) returns the two nodes else it returns false 793 | isValidEdgesForCollapse: function (edges) { 794 | var endPoints = this.getEdgesDistinctEndPoints(edges); 795 | if (endPoints.length != 2) { 796 | return false; 797 | } else { 798 | return endPoints; 799 | } 800 | }, 801 | 802 | //returns a list of distinct endpoints of a set of edges. 803 | getEdgesDistinctEndPoints: function (edges) { 804 | var endPoints = []; 805 | edges.forEach(function (edge) { 806 | if (!this.containsElement(endPoints, edge.source())) { 807 | endPoints.push(edge.source()); 808 | } 809 | if (!this.containsElement(endPoints, edge.target())) { 810 | endPoints.push(edge.target()); 811 | 812 | } 813 | }.bind(this)); 814 | 815 | return endPoints; 816 | }, 817 | 818 | //function to check if a list of elements contains the given element by looking at id() 819 | containsElement: function (elements, element) { 820 | var exists = false; 821 | for (var i = 0; i < elements.length; i++) { 822 | if (elements[i].id() == element.id()) { 823 | exists = true; 824 | break; 825 | } 826 | } 827 | return exists; 828 | } 829 | /* -------------------------------------- end section edge expand collapse -------------------------------------- */ 830 | } 831 | 832 | }; 833 | 834 | module.exports = expandCollapseUtilities; 835 | -------------------------------------------------------------------------------- /demo/demo-compounds.js: -------------------------------------------------------------------------------- 1 | const hardcoded_elems = [ 2 | { "data": { "id": "nwtN_50c55b8c-3489-4c4e-8bea-6a1c1162ac9c" }, "position": { "x": 577.5410894097904, "y": 612.5647477282114 }, "group": "nodes" }, 3 | { "data": { "source": "nwtN_3a5d1ad1-5bfe-48e7-99ee-0cdf3913b062", "target": "nwtN_743ee692-2363-4e76-a0c2-d6d3f717953e", "id": "nwtE_6d4afc19-88a0-4fd4-9fbf-3591cb6ba062" }, "position": {}, "group": "edges" }, 4 | { "data": { "source": "nwtN_8753a0df-286b-4f9b-a00d-bc093113bac7", "target": "nwtN_9a23093c-257f-4e74-9f74-34cdf693daec", "id": "nwtE_605f28bd-77c0-4eef-8251-c5ba9668bda7" }, "position": {}, "group": "edges" }, 5 | { "data": { "source": "nwtN_1b72ec9f-c49f-4768-85a7-16ac6ff345e3", "target": "nwtN_7813a042-3f67-44ab-9d83-ced928bedd25", "id": "nwtE_5bafa3fe-246a-477c-849c-3284c3e62578" }, "position": { "x": null, "y": null }, "group": "edges" }, 6 | { "data": { "source": "nwtN_d578fedc-d576-4c07-8406-89956b346a9d", "target": "nwtN_6fb77c5b-4321-4c3c-a941-91a951082e71", "id": "nwtE_6dda445b-530e-4b95-a3b1-e09cabc73993" }, "position": { "x": null, "y": null }, "group": "edges" }, 7 | { "data": { "source": "nwtN_ef9670aa-321a-41ba-a665-c3980f30eb2a", "target": "nwtN_9a23093c-257f-4e74-9f74-34cdf693daec", "id": "nwtE_b6195365-55fd-4e16-b03d-af46585b2618" }, "position": { "x": null, "y": null }, "group": "edges" }, 8 | { "data": { "source": "nwtN_7813a042-3f67-44ab-9d83-ced928bedd25", "target": "nwtN_477a1284-d1e7-44c6-8553-92fa8a6a553d", "id": "nwtE_6f57baf0-3722-4012-b33e-783c267645fa" }, "position": { "x": null, "y": null }, "group": "edges" }, 9 | { "data": { "source": "nwtN_9d2ac5f6-093a-4090-a750-942e7464a15f", "target": "nwtN_6fb77c5b-4321-4c3c-a941-91a951082e71", "id": "nwtE_9fac6ca3-d907-4b5a-8496-b0edbc3815ca" }, "position": {}, "group": "edges" }, 10 | { "data": { "source": "nwtN_6fb77c5b-4321-4c3c-a941-91a951082e71", "target": "nwtN_f95babe0-0c64-4076-b380-fad5605fec6e", "id": "nwtE_ac487e12-218a-45fd-b94a-f8fb51494baa" }, "position": { "x": null, "y": null }, "group": "edges" }, 11 | { "data": { "id": "nwtN_04d7dde6-171a-4179-85f5-a0cf510f55fb" }, "position": { "x": 195.56340747734816, "y": 484.3338177685355 }, "group": "nodes" }, 12 | { "data": { "source": "nwtN_9d2ac5f6-093a-4090-a750-942e7464a15f", "target": "nwtN_e79b5f83-1e09-485f-83cb-f85c9c6dae25", "id": "nwtE_24228974-e8ba-4f05-8fe8-e775d314bcff" }, "position": {}, "group": "edges" }, 13 | { "data": { "source": "nwtN_01047009-f54b-4c2a-8153-3d83c6e32eab", "target": "nwtN_6af44d07-59d1-4773-bab6-c99641e4810b", "id": "nwtE_56a86996-2c25-4071-b3a3-3000057eef90" }, "position": {}, "group": "edges" }, 14 | { "data": { "source": "nwtN_65df5546-116f-4bda-92c7-acc6549589f1", "target": "nwtN_30d6a1fb-f835-4d67-98db-dbfd8e91166e", "id": "nwtE_a690584d-974b-4a78-8169-584dc4aa2ef8" }, "position": {}, "group": "edges" }, 15 | { "data": { "source": "nwtN_fc734e6e-c7c1-446f-8ae6-a3935cbb8b29", "target": "nwtN_1f8d5d5d-f085-4317-84d4-7b8612d11367", "id": "nwtE_bca25d80-d197-41ca-871c-9c3806a802c3" }, "position": { "x": null, "y": null }, "group": "edges" }, 16 | { "data": { "id": "nwtN_6fb77c5b-4321-4c3c-a941-91a951082e71", "parent": "nwtN_717d31aa-6b70-4067-bcf2-13e0f6bd879a" }, "position": { "x": 424.9142621725959, "y": 163.663834699366 }, "group": "nodes" }, 17 | { "data": { "id": "nwtN_d578fedc-d576-4c07-8406-89956b346a9d", "parent": "nwtN_717d31aa-6b70-4067-bcf2-13e0f6bd879a" }, "position": { "x": 489.2620636399552, "y": 205.99231330748833 }, "group": "nodes" }, 18 | { "data": { "source": "nwtN_6fb77c5b-4321-4c3c-a941-91a951082e71", "target": "nwtN_1b72ec9f-c49f-4768-85a7-16ac6ff345e3", "id": "nwtE_fdd46d3d-3529-4552-bcaf-e5a43364d5eb" }, "position": { "x": null, "y": null }, "group": "edges" }, 19 | { "data": { "id": "nwtN_9d2ac5f6-093a-4090-a750-942e7464a15f" }, "position": { "x": 307.4167261049662, "y": 242.51235456419 }, "group": "nodes" }, 20 | { "data": { "id": "nwtN_f95babe0-0c64-4076-b380-fad5605fec6e", "parent": "nwtN_717d31aa-6b70-4067-bcf2-13e0f6bd879a" }, "position": { "x": 433.25389502259094, "y": 81.8501883151051 }, "group": "nodes" }, 21 | { "data": { "source": "nwtN_f95babe0-0c64-4076-b380-fad5605fec6e", "target": "nwtN_1b72ec9f-c49f-4768-85a7-16ac6ff345e3", "id": "nwtE_9298d0d5-8159-4b50-b880-48aa19738a86" }, "position": { "x": null, "y": null }, "group": "edges" }, 22 | { "data": { "source": "nwtN_6b82a0c0-db1a-4aed-8434-f56152c6bac1", "target": "nwtN_65df5546-116f-4bda-92c7-acc6549589f1", "id": "nwtE_449cf49b-88e5-44f9-9300-5a8dbd79c135" }, "position": {}, "group": "edges" }, 23 | { "data": { "id": "nwtN_30d6a1fb-f835-4d67-98db-dbfd8e91166e" }, "position": { "x": 579.7696102042084, "y": 292.2890755756693 }, "group": "nodes" }, 24 | { "data": { "source": "nwtN_65df5546-116f-4bda-92c7-acc6549589f1", "target": "nwtN_8753a0df-286b-4f9b-a00d-bc093113bac7", "id": "nwtE_15b708e3-501d-432c-941d-627df912946f" }, "position": {}, "group": "edges" }, 25 | { "data": { "id": "nwtN_ef9670aa-321a-41ba-a665-c3980f30eb2a", "parent": "nwtN_50c55b8c-3489-4c4e-8bea-6a1c1162ac9c" }, "position": { "x": 540.8474401288637, "y": 548.2864791672267 }, "group": "nodes" }, 26 | { "data": { "source": "nwtN_8de3d737-f713-404d-a181-c065f9cce74f", "target": "nwtN_50c55b8c-3489-4c4e-8bea-6a1c1162ac9c", "id": "nwtE_fd0f48e7-988f-4707-b126-b8a04dc3f64c" }, "position": {}, "group": "edges" }, 27 | { "data": { "source": "nwtN_3a5d1ad1-5bfe-48e7-99ee-0cdf3913b062", "target": "nwtN_8de3d737-f713-404d-a181-c065f9cce74f", "id": "nwtE_7049cf2c-cc2b-40ed-94b8-590e2b703c45" }, "position": {}, "group": "edges" }, 28 | { "data": { "source": "nwtN_1f8d5d5d-f085-4317-84d4-7b8612d11367", "target": "nwtN_ef9670aa-321a-41ba-a665-c3980f30eb2a", "id": "nwtE_28f94f80-370a-4819-b01e-7c14286528d6" }, "position": { "x": null, "y": null }, "group": "edges" }, 29 | { "data": { "source": "nwtN_1c510598-47d3-48a4-ba9d-fdfb915cda10", "target": "nwtN_04d7dde6-171a-4179-85f5-a0cf510f55fb", "id": "nwtE_4b91ec16-80c7-476e-ac78-40ec11628f8c" }, "position": {}, "group": "edges" }, 30 | { "data": { "source": "nwtN_1b72ec9f-c49f-4768-85a7-16ac6ff345e3", "target": "nwtN_477a1284-d1e7-44c6-8553-92fa8a6a553d", "id": "nwtE_580dc718-3a38-4131-8527-5966dc7117bd" }, "position": { "x": null, "y": null }, "group": "edges" }, 31 | { "data": { "id": "nwtN_717d31aa-6b70-4067-bcf2-13e0f6bd879a" }, "position": { "x": 491.63465589698114, "y": 136.22441840106094 }, "group": "nodes" }, 32 | { "data": { "id": "nwtN_9a23093c-257f-4e74-9f74-34cdf693daec", "parent": "nwtN_50c55b8c-3489-4c4e-8bea-6a1c1162ac9c" }, "position": { "x": 609.4769080081592, "y": 540.0632700234723 }, "group": "nodes" }, 33 | { "data": { "source": "nwtN_91e530f8-4a18-423b-ae2b-0f87ae72d824", "target": "nwtN_787d128e-8256-4207-9e34-948bd142f842", "id": "nwtE_c1260ab9-e976-4b02-a0d4-28e4e7b71956" }, "position": {}, "group": "edges" }, 34 | { "data": { "source": "nwtN_6af44d07-59d1-4773-bab6-c99641e4810b", "target": "nwtN_65df5546-116f-4bda-92c7-acc6549589f1", "id": "nwtE_1379f27c-2858-4c7c-b305-f9dbef07f992" }, "position": {}, "group": "edges" }, 35 | { "data": { "source": "nwtN_f95babe0-0c64-4076-b380-fad5605fec6e", "target": "nwtN_477a1284-d1e7-44c6-8553-92fa8a6a553d", "id": "nwtE_70a9c66e-a05d-4795-9830-b941aa0bdf8d" }, "position": { "x": null, "y": null }, "group": "edges" }, 36 | { "data": { "id": "nwtN_1b72ec9f-c49f-4768-85a7-16ac6ff345e3", "parent": "nwtN_717d31aa-6b70-4067-bcf2-13e0f6bd879a" }, "position": { "x": 491.4678555276823, "y": 133.24054767963713 }, "group": "nodes" }, 37 | { "data": { "source": "nwtN_65df5546-116f-4bda-92c7-acc6549589f1", "target": "nwtN_d578fedc-d576-4c07-8406-89956b346a9d", "id": "nwtE_3a1a451a-396c-46dd-844a-09c8c4506788" }, "position": {}, "group": "edges" }, 38 | { "data": { "id": "nwtN_1f8d5d5d-f085-4317-84d4-7b8612d11367", "parent": "nwtN_50c55b8c-3489-4c4e-8bea-6a1c1162ac9c" }, "position": { "x": 597.8765594527064, "y": 612.7761198138919 }, "group": "nodes" }, 39 | { "data": { "source": "nwtN_6b82a0c0-db1a-4aed-8434-f56152c6bac1", "target": "nwtN_6fb77c5b-4321-4c3c-a941-91a951082e71", "id": "nwtE_03b7f374-f923-4cbd-9b1c-358e6bd0a66a" }, "position": {}, "group": "edges" }, 40 | { "data": { "source": "nwtN_d578fedc-d576-4c07-8406-89956b346a9d", "target": "nwtN_1b72ec9f-c49f-4768-85a7-16ac6ff345e3", "id": "nwtE_bdaaa9a5-5464-44eb-a69a-177006535c60" }, "position": { "x": null, "y": null }, "group": "edges" }, 41 | { "data": { "id": "nwtN_7813a042-3f67-44ab-9d83-ced928bedd25", "parent": "nwtN_717d31aa-6b70-4067-bcf2-13e0f6bd879a" }, "position": { "x": 558.3550496213663, "y": 140.29772029134818 }, "group": "nodes" }, 42 | { "data": { "id": "nwtN_477a1284-d1e7-44c6-8553-92fa8a6a553d", "parent": "nwtN_717d31aa-6b70-4067-bcf2-13e0f6bd879a" }, "position": { "x": 508.5039225894028, "y": 66.45652349463356 }, "group": "nodes" }, 43 | { "data": { "source": "nwtN_9a23093c-257f-4e74-9f74-34cdf693daec", "target": "nwtN_1f8d5d5d-f085-4317-84d4-7b8612d11367", "id": "nwtE_06738526-f767-4e34-8d41-bfd8b046d48e" }, "position": { "x": null, "y": null }, "group": "edges" }, 44 | { "data": { "id": "nwtN_fc734e6e-c7c1-446f-8ae6-a3935cbb8b29", "parent": "nwtN_50c55b8c-3489-4c4e-8bea-6a1c1162ac9c" }, "position": { "x": 614.2347386907171, "y": 685.0662254329505 }, "group": "nodes" }, 45 | { "data": { "source": "nwtN_1c510598-47d3-48a4-ba9d-fdfb915cda10", "target": "nwtN_8de3d737-f713-404d-a181-c065f9cce74f", "id": "nwtE_874c0108-f1b5-4331-9580-bbc904d5ed52" }, "position": {}, "group": "edges" }, 46 | { "data": { "source": "nwtN_30d6a1fb-f835-4d67-98db-dbfd8e91166e", "target": "nwtN_7813a042-3f67-44ab-9d83-ced928bedd25", "id": "nwtE_8ecc2707-8d9f-4c5a-b79f-36028161a2de" }, "position": {}, "group": "edges" }, 47 | { "data": { "id": "nwtN_3a5d1ad1-5bfe-48e7-99ee-0cdf3913b062" }, "position": { "x": 390.8088604802138, "y": 631.143932383176 }, "group": "nodes" }, 48 | { "data": { "id": "nwtN_1c510598-47d3-48a4-ba9d-fdfb915cda10" }, "position": { "x": 385.86501672672586, "y": 549.4623389479385 }, "group": "nodes" }, 49 | { "data": { "id": "nwtN_01047009-f54b-4c2a-8153-3d83c6e32eab" }, "position": { "x": 420.38955421084455, "y": 471.15574980196067 }, "group": "nodes" }, 50 | { "data": { "id": "nwtN_e79b5f83-1e09-485f-83cb-f85c9c6dae25" }, "position": { "x": 369.7167651842458, "y": 293.0403182947785 }, "group": "nodes" }, 51 | { "data": { "id": "nwtN_2ac61ffd-0f55-4d76-ac39-f12efc1712ba" }, "position": { "x": 418.05570853622856, "y": 392.34060880148394 }, "group": "nodes" }, 52 | { "data": { "id": "nwtN_6af44d07-59d1-4773-bab6-c99641e4810b" }, "position": { "x": 488.8353737093525, "y": 424.0878886254484 }, "group": "nodes" }, 53 | { "data": { "id": "nwtN_6b82a0c0-db1a-4aed-8434-f56152c6bac1" }, "position": { "x": 438.2063143421404, "y": 315.0732399204851 }, "group": "nodes" }, 54 | { "data": { "id": "nwtN_8de3d737-f713-404d-a181-c065f9cce74f" }, "position": { "x": 449.9163565836266, "y": 594.1831978504854 }, "group": "nodes" }, 55 | { "data": { "id": "nwtN_65df5546-116f-4bda-92c7-acc6549589f1" }, "position": { "x": 511.65889587382577, "y": 346.18005665157585 }, "group": "nodes" }, 56 | { "data": { "id": "nwtN_8753a0df-286b-4f9b-a00d-bc093113bac7" }, "position": { "x": 562.2598442850485, "y": 415.8153103233126 }, "group": "nodes" }, 57 | { "data": { "source": "nwtN_9d2ac5f6-093a-4090-a750-942e7464a15f", "target": "nwtN_04d7dde6-171a-4179-85f5-a0cf510f55fb", "id": "nwtE_11040a46-0530-4375-a13d-2cdca0a98536" }, "position": {}, "group": "edges" }, 58 | { "data": { "source": "nwtN_ef9670aa-321a-41ba-a665-c3980f30eb2a", "target": "nwtN_6af44d07-59d1-4773-bab6-c99641e4810b", "id": "nwtE_b47d4380-4724-4409-a449-1d80a798f9df" }, "position": {}, "group": "edges" }, 59 | { "data": { "source": "nwtN_e79b5f83-1e09-485f-83cb-f85c9c6dae25", "target": "nwtN_6b82a0c0-db1a-4aed-8434-f56152c6bac1", "id": "nwtE_b7156db8-08d3-4a8d-9b43-db3de0701017" }, "position": {}, "group": "edges" }, 60 | { "data": { "source": "nwtN_6b82a0c0-db1a-4aed-8434-f56152c6bac1", "target": "nwtN_2ac61ffd-0f55-4d76-ac39-f12efc1712ba", "id": "nwtE_1ab89f57-598a-4805-85d6-445f44bed701" }, "position": {}, "group": "edges" }, 61 | { "data": { "source": "nwtN_2ac61ffd-0f55-4d76-ac39-f12efc1712ba", "target": "nwtN_01047009-f54b-4c2a-8153-3d83c6e32eab", "id": "nwtE_3a06bd73-a5ea-42fc-bebd-6cc9b227c4d4" }, "position": {}, "group": "edges" }, 62 | { "data": { "source": "nwtN_01047009-f54b-4c2a-8153-3d83c6e32eab", "target": "nwtN_1c510598-47d3-48a4-ba9d-fdfb915cda10", "id": "nwtE_fe9dae92-bb4d-4fbc-8b00-3275e457899b" }, "position": {}, "group": "edges" }, 63 | { "data": { "source": "nwtN_1c510598-47d3-48a4-ba9d-fdfb915cda10", "target": "nwtN_3a5d1ad1-5bfe-48e7-99ee-0cdf3913b062", "id": "nwtE_498e1c01-8c7d-4711-86d4-25119fd459b3" }, "position": {}, "group": "edges" }, 64 | { "data": { "id": "nwtN_91be4b3b-b492-4cf2-822e-c2a1de14dbfe", "parent": "nwtN_d70e8589-ab02-41e3-879f-29aed04212fa" }, "position": { "x": 131.35922693271124, "y": 605.7954433087209 }, "group": "nodes" }, 65 | { "data": { "id": "nwtN_743ee692-2363-4e76-a0c2-d6d3f717953e", "parent": "nwtN_d70e8589-ab02-41e3-879f-29aed04212fa" }, "position": { "x": 263.1586191787393, "y": 578.1716399433802 }, "group": "nodes" }, 66 | { "data": { "id": "nwtN_4d5b8b52-1f20-45f8-bc0f-3a4a1235c0f5", "parent": "nwtN_d70e8589-ab02-41e3-879f-29aed04212fa" }, "position": { "x": 195.2099595474637, "y": 563.255374790295 }, "group": "nodes" }, 67 | { "data": { "id": "nwtN_b282d9cf-0120-42bc-9036-3bd48f925d1e", "parent": "nwtN_d70e8589-ab02-41e3-879f-29aed04212fa" }, "position": { "x": 256.9500139684577, "y": 508.8943151396569 }, "group": "nodes" }, 68 | { "data": { "id": "nwtN_0bd04732-5c51-4577-87f3-3675b3294ac3", "parent": "nwtN_d70e8589-ab02-41e3-879f-29aed04212fa" }, "position": { "x": 127.96819577595704, "y": 535.5200800812347 }, "group": "nodes" }, 69 | { "data": { "id": "nwtN_d70e8589-ab02-41e3-879f-29aed04212fa", "parent": "nwtN_04d7dde6-171a-4179-85f5-a0cf510f55fb" }, "position": { "x": 195.56340747734816, "y": 557.3448792241888 }, "group": "nodes" }, 70 | { "data": { "id": "nwtN_0f5340ab-a217-423f-b5f7-0a149f3217e8", "parent": "nwtN_04d7dde6-171a-4179-85f5-a0cf510f55fb" }, "position": { "x": 253.48384362872935, "y": 375.70472845792517 }, "group": "nodes" }, 71 | { "data": { "id": "nwtN_91e530f8-4a18-423b-ae2b-0f87ae72d824", "parent": "nwtN_04d7dde6-171a-4179-85f5-a0cf510f55fb" }, "position": { "x": 187.4692319760543, "y": 341.24719222835006 }, "group": "nodes" }, 72 | { "data": { "id": "nwtN_787d128e-8256-4207-9e34-948bd142f842", "parent": "nwtN_04d7dde6-171a-4179-85f5-a0cf510f55fb" }, "position": { "x": 119.07770758703417, "y": 363.03066656565034 }, "group": "nodes" }, 73 | { "data": { "source": "nwtN_4d5b8b52-1f20-45f8-bc0f-3a4a1235c0f5", "target": "nwtN_91be4b3b-b492-4cf2-822e-c2a1de14dbfe", "id": "nwtE_720b0a71-9ad3-4821-b828-ecace971acd1" }, "position": { "x": null, "y": null }, "group": "edges" }, 74 | { "data": { "source": "nwtN_91be4b3b-b492-4cf2-822e-c2a1de14dbfe", "target": "nwtN_0bd04732-5c51-4577-87f3-3675b3294ac3", "id": "nwtE_ce9e18ee-2bb9-4abb-88f8-272b3d76a8b4" }, "position": { "x": null, "y": null }, "group": "edges" }, 75 | { "data": { "source": "nwtN_743ee692-2363-4e76-a0c2-d6d3f717953e", "target": "nwtN_4d5b8b52-1f20-45f8-bc0f-3a4a1235c0f5", "id": "nwtE_6a465aa8-1c8b-4455-95bc-e67b77f3f7d3" }, "position": { "x": null, "y": null }, "group": "edges" }, 76 | { "data": { "source": "nwtN_b282d9cf-0120-42bc-9036-3bd48f925d1e", "target": "nwtN_743ee692-2363-4e76-a0c2-d6d3f717953e", "id": "nwtE_c6b430bd-17fc-4b1f-82a9-16bc0d5dfa78" }, "position": { "x": null, "y": null }, "group": "edges" }, 77 | { "data": { "source": "nwtN_0bd04732-5c51-4577-87f3-3675b3294ac3", "target": "nwtN_4d5b8b52-1f20-45f8-bc0f-3a4a1235c0f5", "id": "nwtE_12dd751b-6a30-4bef-8511-d36869559740" }, "position": { "x": null, "y": null }, "group": "edges" }, 78 | { "data": { "source": "nwtN_4d5b8b52-1f20-45f8-bc0f-3a4a1235c0f5", "target": "nwtN_b282d9cf-0120-42bc-9036-3bd48f925d1e", "id": "nwtE_7944e4ea-bb97-484f-a6ee-0d77d7bab80f" }, "position": { "x": null, "y": null }, "group": "edges" }, 79 | { "data": { "source": "nwtN_b282d9cf-0120-42bc-9036-3bd48f925d1e", "target": "nwtN_0f5340ab-a217-423f-b5f7-0a149f3217e8", "id": "nwtE_d0a8fa82-36ac-4c28-837b-aa38b8f2cdb6" }, "position": { "x": null, "y": null }, "group": "edges" }, 80 | { "data": { "source": "nwtN_0f5340ab-a217-423f-b5f7-0a149f3217e8", "target": "nwtN_91e530f8-4a18-423b-ae2b-0f87ae72d824", "id": "nwtE_a4051f32-f6fe-451e-b153-e4de31f4808b" }, "position": {}, "group": "edges" }, 81 | { "data": { "source": "nwtN_0f5340ab-a217-423f-b5f7-0a149f3217e8", "target": "nwtN_e79b5f83-1e09-485f-83cb-f85c9c6dae25", "id": "nwtE_70a31acd-428d-47be-a981-38107a83d2e1" }, "position": {}, "group": "edges" }, 82 | { data: { id: 'e0', source: 'n0', target: 'n1', edgeType: "type1" }, group: "edges" }, 83 | { data: { id: 'e1', source: 'n0', target: 'n1', edgeType: "type1" }, group: "edges" }, 84 | { data: { id: 'e3', source: 'n1', target: 'n0', edgeType: "type1" }, group: "edges" }, 85 | { data: { id: 'e4', source: 'n2', target: 'n3', edgeType: "type2" }, group: "edges" }, 86 | { data: { id: 'e5', source: 'n3', target: 'n2', edgeType: "type2" }, group: "edges" }, 87 | { data: { id: 'e6', source: 'n0', target: 'n3', edgeType: "type2" }, group: "edges" }, 88 | { data: { id: 'e7', source: 'n1', target: 'n0', edgeType: "type2" }, group: "edges" }, 89 | { data: { id: 'e8', source: 'n1', target: 'n0', edgeType: "type2" }, group: "edges" }, 90 | { data: { id: 'e9', source: 'n1', target: 'n0', edgeType: "type2" }, group: "edges" }, 91 | { data: { id: 'e10', source: 'n3', target: 'n4', edgeType: "type3" }, group: "edges" }, 92 | { data: { id: 'e11', source: 'n3', target: 'n4', edgeType: "type3" }, group: "edges" }, 93 | { data: { id: 'e12', source: 'n4', target: 'n3', edgeType: "type3" }, group: "edges" }, 94 | { data: { id: 'e13', source: 'n5', target: 'n1', edgeType: "type3" }, group: "edges" }, 95 | { data: { id: 'e14', source: 'n1', target: 'n5', edgeType: "type3" }, group: "edges" }, 96 | { data: { id: 'e15', source: 'n2', target: 'n5', edgeType: "type1" }, group: "edges" }, 97 | { data: { id: 'e16', source: 'n2', target: 'n5', edgeType: "type1" }, group: "edges" }, 98 | { data: { id: 'e17', source: 'n2', target: 'n5', edgeType: "type1" }, group: "edges" }, 99 | { data: { id: 'e18', source: 'n2', target: 'n5', edgeType: "type1" }, group: "edges" }, 100 | { data: { id: 'e19', source: 'n2', target: 'n5', edgeType: "type1" }, group: "edges" }, 101 | { data: { id: 'e20', source: 'n2', target: 'n5', edgeType: "type1" }, group: "edges" }, 102 | { data: { id: 'e21', source: 'n2', target: 'n5', edgeType: "type1" }, group: "edges" }, 103 | { data: { id: 'e22', source: 'n2', target: 'n5', edgeType: "type1" }, group: "edges" }, 104 | { data: { id: 'e23', source: 'n2', target: 'n5', edgeType: "type1" }, group: "edges" }, 105 | { data: { id: 'e16', source: 'n2', target: 'n5', edgeType: "type1" }, group: "edges" }, 106 | { data: { id: 'e24', source: 'n2', target: 'n5', edgeType: "type1" }, group: "edges" }, 107 | { data: { id: 'e25', source: 'n2', target: 'n5', edgeType: "type1" }, group: "edges" }, 108 | { data: { id: 'e26', source: 'n2', target: 'n5', edgeType: "type1" }, group: "edges" }, 109 | { data: { id: 'e27', source: 'n2', target: 'n5', edgeType: "type1" }, group: "edges" }, 110 | { data: { id: 'e28', source: 'n2', target: 'n5', edgeType: "type1" }, group: "edges" }, 111 | { data: { id: 'e29', source: 'n2', target: 'n5', edgeType: "type1" }, group: "edges" }, 112 | { data: { id: 'e30', source: 'n3', target: 'n3', edgeType: "type3" }, group: "edges" }, 113 | { data: { id: 'e31', source: 'n3', target: 'n3', edgeType: "type3" }, group: "edges" }, 114 | { data: { id: 'e32', source: 'nwtN_0f5340ab-a217-423f-b5f7-0a149f3217e8', target: 'n0' }, "group": "edges" }, 115 | { data: { id: 'n0', name: 'n0' }, group: "nodes" }, 116 | { data: { id: 'n1', name: 'n1' }, group: "nodes" }, 117 | { data: { id: 'n2', name: 'n2' }, group: "nodes" }, 118 | { data: { id: 'n3', name: 'n3' }, group: "nodes" }, 119 | { data: { id: 'n4', name: 'n4' }, group: "nodes" }, 120 | { data: { id: 'n5', name: 'n5' }, group: "nodes" }, 121 | ]; 122 | 123 | function readTxtFile(file, cb) { 124 | const fileReader = new FileReader(); 125 | fileReader.onload = () => { 126 | try { 127 | cb(fileReader.result); 128 | } catch (error) { 129 | console.error('Given file is not suitable.', error); 130 | } 131 | }; 132 | fileReader.onerror = (error) => { 133 | console.error('File could not be read!', error); 134 | fileReader.abort(); 135 | }; 136 | fileReader.readAsText(file); 137 | } 138 | 139 | function activateAccordions() { 140 | const acc = document.getElementsByClassName("accordion"); 141 | let i; 142 | 143 | for (i = 0; i < acc.length; i++) { 144 | acc[i].addEventListener("click", function () { 145 | this.classList.toggle("active"); 146 | var panel = this.nextElementSibling; 147 | if (panel.style.maxHeight) { 148 | panel.style.maxHeight = null; 149 | } else { 150 | panel.style.maxHeight = panel.scrollHeight + "px"; 151 | } 152 | }); 153 | } 154 | } 155 | 156 | function addParentNode(idSuffix, parent = undefined) { 157 | const id = 'c' + idSuffix; 158 | const parentNode = { data: { id: id } }; 159 | cy.add(parentNode); 160 | cy.$('#' + id).move({ parent: parent }); 161 | return id; 162 | } 163 | 164 | 165 | function main() { 166 | const edgeStyles = { 167 | "type1": { "color": "#CFA79D", "arrowShape": "triangle" }, 168 | "type2": { "color": "#9DCFA7", "arrowShape": "triangle" }, 169 | "type3": { "color": "#A79DCF", "arrowShape": "triangle" }, 170 | }; 171 | 172 | function setColor4CompoundEdge(e) { 173 | const collapsedEdges = e.data('collapsedEdges'); 174 | if (doElemsMultiTypes(collapsedEdges)) { 175 | return '#b3b3b3'; 176 | } 177 | return collapsedEdges[0].style('line-color') 178 | } 179 | 180 | function setTargetArrowShape(e) { 181 | const collapsedEdges = e.data('collapsedEdges'); 182 | const shapes = {}; 183 | for (let i = 0; i < collapsedEdges.length; i++) { 184 | shapes[collapsedEdges[0].style('target-arrow-shape')] = true; 185 | } 186 | delete shapes['none']; 187 | if (Object.keys(shapes).length < 1) { 188 | if (collapsedEdges.sources().length > 1) { 189 | return collapsedEdges[0].style('source-arrow-shape'); 190 | } 191 | return 'none'; 192 | } 193 | return Object.keys(shapes)[0]; 194 | } 195 | 196 | function setSourceArrowShape(e) { 197 | const collapsedEdges = e.data('collapsedEdges'); 198 | const shapes = {}; 199 | for (let i = 0; i < collapsedEdges.length; i++) { 200 | shapes[collapsedEdges[0].style('source-arrow-shape')] = true; 201 | } 202 | delete shapes['none']; 203 | if (Object.keys(shapes).length < 1) { 204 | if (collapsedEdges.sources().length > 1) { 205 | return collapsedEdges[0].style('target-arrow-shape'); 206 | } 207 | return 'none'; 208 | } 209 | return Object.keys(shapes)[0]; 210 | } 211 | 212 | function doElemsMultiTypes(elems) { 213 | const classDict = {}; 214 | for (let i = 0; i < elems.length; i++) { 215 | classDict[elems[i].data('edgeType')] = true; 216 | } 217 | return Object.keys(classDict).length > 1; 218 | } 219 | 220 | var cy = window.cy = cytoscape({ 221 | container: document.getElementById('cy'), 222 | 223 | ready: function () { 224 | this.layout({ 225 | name: 'fcose', 226 | randomize: true, 227 | fit: true, 228 | animate: false 229 | }).run(); 230 | var api = this.expandCollapse({ 231 | layoutBy: { 232 | name: "fcose", 233 | animate: true, 234 | randomize: false, 235 | fit: true 236 | }, 237 | fisheye: true, 238 | animate: true, 239 | undoable: false 240 | }); 241 | api.collapseAll(); 242 | }, 243 | style: [ 244 | { 245 | selector: 'node', 246 | style: { 247 | 'background-color': '#ad1a66' 248 | } 249 | }, 250 | { 251 | selector: ':parent', 252 | style: { 253 | 'background-opacity': 0.333 254 | } 255 | }, 256 | 257 | { 258 | selector: "node.cy-expand-collapse-collapsed-node", 259 | style: { 260 | "background-color": "darkblue", 261 | "shape": "rectangle" 262 | } 263 | }, 264 | { 265 | selector: 'edge', 266 | style: { 267 | 'width': 3, 268 | 'line-color': '#ad1a66', 269 | 'curve-style': 'bezier' 270 | } 271 | }, 272 | { 273 | selector: ':selected', 274 | style: { 275 | 'overlay-color': "#6c757d", 276 | 'overlay-opacity': 0.3, 277 | 'background-color': "#999999" 278 | } 279 | }, 280 | { 281 | selector: 'edge[edgeType="type1"]', 282 | style: { 283 | 'width': 3, 284 | 'line-color': edgeStyles["type1"].color, 285 | 'target-arrow-shape': edgeStyles["type1"].arrowShape, 286 | 'target-arrow-color': edgeStyles["type1"].color, 287 | } 288 | }, 289 | { 290 | selector: 'edge[edgeType="type2"]', 291 | style: { 292 | 'width': 3, 293 | 'line-color': edgeStyles["type2"].color, 294 | 'target-arrow-shape': edgeStyles["type2"].arrowShape, 295 | 'target-arrow-color': edgeStyles["type2"].color, 296 | } 297 | }, 298 | { 299 | selector: 'edge[edgeType="type3"]', 300 | style: { 301 | 'width': 3, 302 | 'line-color': edgeStyles["type3"].color, 303 | 'target-arrow-shape': edgeStyles["type3"].arrowShape, 304 | 'target-arrow-color': edgeStyles["type3"].color, 305 | } 306 | }, 307 | { 308 | selector: 'edge.cy-expand-collapse-collapsed-edge', 309 | style: 310 | { 311 | "text-outline-color": "#ffffff", 312 | "text-outline-width": "2px", 313 | 'label': (e) => { 314 | return '(' + e.data('collapsedEdges').length + ')'; 315 | }, 316 | 'width': function (edge) { 317 | const n = edge.data('collapsedEdges').length; 318 | return (3 + Math.log2(n)) + 'px'; 319 | }, 320 | 'line-style': 'dashed', 321 | 'line-color': setColor4CompoundEdge.bind(this), 322 | 'target-arrow-color': setColor4CompoundEdge.bind(this), 323 | 'target-arrow-shape': setTargetArrowShape.bind(this), 324 | 'source-arrow-shape': setSourceArrowShape.bind(this), 325 | 'source-arrow-color': setColor4CompoundEdge.bind(this), 326 | } 327 | }, 328 | ], 329 | 330 | elements: hardcoded_elems 331 | }); 332 | 333 | var api = cy.expandCollapse('get'); 334 | var elements = null; 335 | var markovClusteringClickable = true; 336 | 337 | 338 | function setClusterBtn(isEnabled) { 339 | markovClusteringClickable = isEnabled; 340 | document.getElementById("apply-markov-clustering").disabled = !isEnabled; 341 | } 342 | 343 | document.getElementById("collapseRecursively").addEventListener("click", function () { 344 | api.collapseRecursively(cy.$(":selected")); 345 | }); 346 | 347 | document.getElementById("expandRecursively").addEventListener("click", function () { 348 | api.expandRecursively(cy.$(":selected")); 349 | }); 350 | 351 | document.getElementById("expandAllAndRemove").addEventListener("click", function () { 352 | api.expandAll(); 353 | elements = cy.elements().remove(); 354 | }); 355 | 356 | document.getElementById("loadInCollapsedState").addEventListener("click", function () { 357 | if (elements) { 358 | cy.add(elements); 359 | api.collapseAll(); 360 | elements = null; 361 | } 362 | else { 363 | console.warn("Remove elements first by clicking on 'Expand all and remove' button."); 364 | } 365 | }); 366 | 367 | document.getElementById("collapseAll").addEventListener("click", function () { 368 | api.collapseAll(); 369 | }); 370 | 371 | document.getElementById("expandAll").addEventListener("click", function () { 372 | api.expandAll(); 373 | }); 374 | 375 | function applyMarkovClustering() { 376 | var clusteringDepth = document.getElementById("clustering-depth-input").value; 377 | if (clusteringDepth < 1) 378 | return; 379 | var createdNodeIds = []; 380 | //cy.startBatch(); 381 | 382 | for (var i = 0; i < clusteringDepth; i++) { 383 | //get clusters for this depth level 384 | let clusters = cy.elements().markovClustering(); 385 | 386 | for (var j = 0; j < clusters.length; j++) { 387 | let parentId = "p_" + i + "_" + j; 388 | 389 | let clusterBoundingBox = clusters[j].bb(); 390 | let parentPos = {x: clusterBoundingBox.x1 + clusterBoundingBox.w / 2, y: clusterBoundingBox.y1 + clusterBoundingBox.h / 2}; 391 | 392 | var newNode = cy.add({ 393 | group: 'nodes', 394 | data: { 395 | id: parentId 396 | }, 397 | position: parentPos 398 | }); 399 | createdNodeIds.push(parentId); 400 | clusters[j].move({ 401 | parent: parentId 402 | }); 403 | } 404 | var nodesToRemove = cy.collection(); 405 | cy.nodes().forEach(function (node) { 406 | if (createdNodeIds.includes(node.id()) && node.degree() < 1 && !node.isParent()) { 407 | nodesToRemove = nodesToRemove.union(node); 408 | } 409 | }); 410 | cy.remove(nodesToRemove); 411 | 412 | api.collapseAll(); 413 | } 414 | 415 | //cy.endBatch(); 416 | setClusterBtn(false); 417 | } 418 | 419 | document.getElementById("graphml-input").addEventListener("change", function (evt) { 420 | //read graphML file 421 | let files = evt.target.files; 422 | let reader = new FileReader(); 423 | let contents; 424 | reader.readAsText(files[0]); 425 | reader.onload = function (event) { 426 | contents = event.target.result; 427 | 428 | cy.startBatch(); 429 | cy.elements().remove(); 430 | cy.graphml({ layoutBy: 'preset'}); 431 | cy.graphml(contents); 432 | cy.endBatch(); 433 | 434 | cy.makeLayout({ 435 | name: 'fcose', 436 | randomize: true, 437 | fit: true, 438 | animate: false 439 | }).run(); 440 | 441 | //to be able to open the same file again 442 | document.getElementById("graphml-input").value = ""; 443 | //avoid adding the same listener multiple times 444 | if (!markovClusteringClickable) { 445 | setClusterBtn(true); 446 | } 447 | if (document.getElementById("cluster-by-default").checked) { 448 | applyMarkovClustering(); 449 | } 450 | }; 451 | 452 | }); 453 | 454 | document.getElementById("apply-markov-clustering").addEventListener("click", applyMarkovClustering); 455 | 456 | function getEdgeOptions() { 457 | const groupEdgesOfSameTypeOnCollapse = document.getElementById('groupEdges').checked; 458 | const allowNestedEdgeCollapse = document.getElementById('allowNestedEdgeCollapse').checked; 459 | return { groupEdgesOfSameTypeOnCollapse: groupEdgesOfSameTypeOnCollapse, allowNestedEdgeCollapse: allowNestedEdgeCollapse }; 460 | } 461 | 462 | document.getElementById("collapseSelectedEdges").addEventListener("click", function () { 463 | const edges = cy.edges(":selected"); 464 | if (edges.length >= 2) { 465 | api.collapseEdges(edges, getEdgeOptions()); 466 | } 467 | }); 468 | 469 | document.getElementById("expandSelectedEdges").addEventListener("click", function () { 470 | const edges = cy.edges(":selected"); 471 | if (edges.length > 0) { 472 | api.expandEdges(edges, getEdgeOptions()); 473 | } 474 | }); 475 | 476 | document.getElementById("collapseAllEdges").addEventListener("click", function () { 477 | api.collapseAllEdges(getEdgeOptions()); 478 | }); 479 | 480 | document.getElementById("collapseEdgesBetweenNodes").addEventListener("click", function () { 481 | api.collapseEdgesBetweenNodes(cy.nodes(":selected"), getEdgeOptions()); 482 | }); 483 | 484 | document.getElementById("expandEdgesBetweenNodes").addEventListener("click", function () { 485 | api.expandEdgesBetweenNodes(cy.nodes(":selected"), getEdgeOptions()); 486 | }); 487 | 488 | document.getElementById("expandAllEdges").addEventListener("click", function () { 489 | if (cy.edges(".cy-expand-collapse-collapsed-edge").length > 0) { 490 | api.expandAllEdges(); 491 | } 492 | }); 493 | 494 | document.getElementById('saveAsJson').addEventListener('click', function () { 495 | api.saveJson(cy.$(), 'expand-collapse-output.json'); 496 | }); 497 | 498 | document.getElementById('loadFromJson').addEventListener('click', function () { 499 | const el = document.getElementById('load-from-inp'); 500 | el.value = ''; 501 | el.click(); 502 | }); 503 | 504 | document.getElementById('load-from-inp').addEventListener('change', function () { 505 | readTxtFile(this.files[0], function (txt) { 506 | cy.$().remove(); 507 | api.loadJson(txt); 508 | }) 509 | }); 510 | 511 | document.getElementById('add-compound').addEventListener('click', function () { 512 | const elems = cy.nodes(':selected'); 513 | if (elems.length < 1) { 514 | return; 515 | } 516 | const parent = elems[0].parent().id(); 517 | for (let i = 1; i < elems.length; i++) { 518 | if (parent !== elems[i].parent().id()) { 519 | return; 520 | } 521 | } 522 | const id = new Date().getTime(); 523 | addParentNode(id, parent); 524 | for (let i = 0; i < elems.length; i++) { 525 | elems[i].move({ parent: 'c' + id }); 526 | } 527 | }); 528 | 529 | document.getElementById('remove-compound').addEventListener('click', function () { 530 | const elems = cy.nodes(':selected').filter(':compound'); 531 | if (elems.length < 1) { 532 | return; 533 | } 534 | for (let i = 0; i < elems.length; i++) { 535 | // expand if collapsed 536 | if (elems[i].hasClass('cy-expand-collapse-collapsed-node')) { 537 | api.expand(elems[i], { layoutBy: null, fisheye: false, animate: false }); 538 | } 539 | const grandParent = elems[i].parent().id() ?? null; 540 | const children = elems[i].children(); 541 | children.move({ parent: grandParent }); 542 | cy.remove(elems[i]); 543 | } 544 | }); 545 | 546 | activateAccordions(); 547 | 548 | setTimeout(() => { 549 | document.getElementsByClassName('accordion')[1].click(); 550 | }, 500); 551 | } 552 | 553 | document.addEventListener('DOMContentLoaded', main); 554 | 555 | --------------------------------------------------------------------------------