├── .babelrc ├── .eslintignore ├── .eslintrc ├── .github └── stale.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── LICENSE ├── README.md ├── bower.json ├── cytoscape-automove.js ├── demo-multiple-mean.html ├── demo.html ├── package-lock.json ├── package.json ├── pages ├── cytoscape-automove.js ├── demo-multiple-mean.html └── demo.html ├── src ├── assign.js ├── automove.js ├── defaults.js └── index.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "node": true, 6 | "amd": true, 7 | "es6": true 8 | }, 9 | "extends": "eslint:recommended", 10 | "rules": { 11 | "semi": "error" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 30 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 30 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | # Label to use when marking an issue as stale 9 | staleLabel: stale 10 | # Comment to post when marking an issue as stale. Set to `false` to disable 11 | markComment: > 12 | This issue has been automatically marked as stale, because it has not had 13 | activity within the past 30 days. It will be closed if no further activity 14 | occurs within the next 30 days. If a feature request is important to you, 15 | please consider making a pull request. Thank you for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | index.html 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cytoscape/cytoscape.js-automove/e77bcff384d0ce3fe7b77a084b4540295a47a4f8/.npmrc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | 3 | Copyright (c) 2016-2019, 2022, The Cytoscape Consortium. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the “Software”), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cytoscape-automove 2 | ================================================================================ 3 | 4 | 5 | ## Description 6 | 7 | An extension for Cytoscape.js that automatically updates node positions based on specified rules ([demo](https://cytoscape.github.io/cytoscape.js-automove)) 8 | 9 | Common usecases: 10 | 11 | * Making one node move in step with another node 12 | * Constraining a node within a boundary 13 | * Using a node to represent an n-ary interaction 14 | 15 | 16 | ## Dependencies 17 | 18 | * Cytoscape.js ^3.2.0 19 | 20 | 21 | ## Usage instructions 22 | 23 | Download the library: 24 | * via npm: `npm install cytoscape-automove`, 25 | * via bower: `bower install cytoscape-automove`, or 26 | * via direct download in the repository (probably from a tag). 27 | 28 | Import the library as appropriate for your project: 29 | 30 | ES import: 31 | 32 | ```js 33 | import cytoscape from 'cytoscape'; 34 | import automove from 'cytoscape-automove'; 35 | 36 | cytoscape.use( automove ); 37 | ``` 38 | 39 | CommonJS require: 40 | 41 | ```js 42 | let cytoscape = require('cytoscape'); 43 | let automove = require('cytoscape-automove'); 44 | 45 | cytoscape.use( automove ); // register extension 46 | ``` 47 | 48 | AMD: 49 | 50 | ```js 51 | require(['cytoscape', 'cytoscape-automove'], function( cytoscape, automove ){ 52 | automove( cytoscape ); // register extension 53 | }); 54 | ``` 55 | 56 | Plain HTML/JS has the extension registered for you automatically, because no `require()` is needed. 57 | 58 | 59 | ## API 60 | 61 | Each time `cy.automove()` is called, the specified rules are added to the core instance: 62 | 63 | ```js 64 | let defaults = { 65 | // specify nodes that should be automoved with one of 66 | // - a function that returns true for matching nodes 67 | // - a selector that matches the nodes 68 | // - a collection of nodes (very good for performance) 69 | nodesMatching: function( node ){ return false; }, 70 | 71 | // specify how a node's position should be updated with one of 72 | // - function( node ){ return { x: 1, y: 2 }; } => put the node where the function returns 73 | // - { x1, y1, x2, y2 } => constrain the node position within the bounding box (in model co-ordinates) 74 | // - { x1, y1, x2, y2, type: 'inside' } => constrain the node position within the bounding box (in model co-ordinates) 75 | // - { x1, y1, x2, y2, type: 'outside' } => constrain the node position outside the bounding box (in model co-ordinates) 76 | // - 'mean' => put the node in the average position of its neighbourhood 77 | // - 'viewport' => keeps the node body within the viewport 78 | // - 'drag' => matching nodes are effectively dragged along 79 | reposition: 'mean', 80 | 81 | // specify when the repositioning should occur by specifying a function that 82 | // calls update() when reposition updates should occur 83 | // - function( update ){ /* ... */ update(); } => a manual function for updating 84 | // - 'matching' => automatically update on position events for nodesMatching 85 | // - set efficiently and automatically for 86 | // - reposition: 'mean' 87 | // - reposition: { x1, y1, x2, y2 } 88 | // - reposition: 'viewport' 89 | // - reposition: 'drag' 90 | // - default/undefined => on a position event for any node (not as efficient...) 91 | when: undefined, 92 | 93 | 94 | 95 | // 96 | // customisation options for non-function `reposition` values 97 | // 98 | 99 | // `reposition: 'mean'` 100 | 101 | // specify nodes that should be ignored in the mean calculation 102 | // - a function that returns true for nodes to be ignored 103 | // - a selector that matches the nodes to be ignored 104 | // - a collection of nodes to be ignored (very good for performance) 105 | meanIgnores: function( node ){ return false; }, 106 | 107 | // specify whether moving a particular `nodesMatching` node causes repositioning 108 | // - true : the mid node can't be independently moved/dragged 109 | // - false : the mid node can be independently moved/dragged (useful if you want the mid node to use `reposition: 'drag' in another rule with its neighbourhood`) 110 | meanOnSelfPosition: function( node ){ return true; }, 111 | 112 | // `reposition: 'drag'` 113 | 114 | // specify nodes that when dragged cause the matched nodes to move along (i.e. the master nodes) 115 | // - a function that returns true for nodes to be listened to for drag events 116 | // - a selector that matches the nodes to be listened to for drag events 117 | // - a collection of nodes to be listened to for drag events (very good for performance) 118 | dragWith: function( node ){ return false; } 119 | }; 120 | 121 | let options = defaults; 122 | 123 | let rule = cy.automove( options ); 124 | ``` 125 | 126 | A rule has a number of functions available: 127 | 128 | ```js 129 | rule.apply(); // manually apply a rule 130 | 131 | rule.enabled(); // get whether rule is enabled 132 | 133 | rule.toggle(); // toggle whether the rule is enabled 134 | 135 | rule.disable(); // temporarily disable the rule 136 | 137 | rule.enable(); // re-enable the rule 138 | 139 | rule.destroy(); // remove and clean up just this rule 140 | ``` 141 | 142 | You can also remove all the rules you previously specified: 143 | 144 | ```js 145 | cy.automove('destroy'); 146 | ``` 147 | 148 | ## Events 149 | 150 | - `automove` : Emitted on a node when its position is changed by a rule 151 | - `node.on('automove', function( event, rule ){})` 152 | 153 | 154 | ## Build targets 155 | 156 | * `npm run test` : Run Mocha tests in `./test` 157 | * `npm run build` : Build `./src/**` into `cytoscape-automove.js` 158 | * `npm run watch` : Automatically build on changes with live reloading (N.b. you must already have an HTTP server running) 159 | * `npm run dev` : Automatically build on changes with live reloading with webpack dev server 160 | * `npm run lint` : Run eslint on the source 161 | 162 | N.b. all builds use babel, so modern ES features can be used in the `src`. 163 | 164 | 165 | ## Publishing instructions 166 | 167 | This project is set up to automatically be published to npm and bower. To publish: 168 | 169 | 1. Build the extension : `npm run build:release` 170 | 1. Commit the build : `git commit -am "Build for release"` 171 | 1. Bump the version number and tag: `npm version major|minor|patch` 172 | 1. Push to origin: `git push && git push --tags` 173 | 1. Publish to npm: `npm publish .` 174 | 1. If publishing to bower for the first time, you'll need to run `bower register cytoscape-automove https://github.com/cytoscape/cytoscape.js-automove.git` 175 | 1. [Make a new release](https://github.com/cytoscape/cytoscape.js-automove/releases/new) for Zenodo. 176 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cytoscape-automove", 3 | "description": "An extension for Cytoscape.js that automatically updates node positions based on specified rules", 4 | "main": "cytoscape-automove.js", 5 | "dependencies": { 6 | "cytoscape": "^3.0.0" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/cytoscape/cytoscape.js-automove.git" 11 | }, 12 | "ignore": [ 13 | "**/.*", 14 | "node_modules", 15 | "bower_components", 16 | "test", 17 | "tests" 18 | ], 19 | "keywords": [ 20 | "cytoscape", 21 | "cytoscape-extension" 22 | ], 23 | "license": "MIT" 24 | } 25 | -------------------------------------------------------------------------------- /cytoscape-automove.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(); 4 | else if(typeof define === 'function' && define.amd) 5 | define([], factory); 6 | else if(typeof exports === 'object') 7 | exports["cytoscapeAutomove"] = factory(); 8 | else 9 | root["cytoscapeAutomove"] = factory(); 10 | })(this, function() { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | /******/ 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | /******/ 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) { 20 | /******/ return installedModules[moduleId].exports; 21 | /******/ } 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ i: moduleId, 25 | /******/ l: false, 26 | /******/ exports: {} 27 | /******/ }; 28 | /******/ 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | /******/ 32 | /******/ // Flag the module as loaded 33 | /******/ module.l = true; 34 | /******/ 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | /******/ 39 | /******/ 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | /******/ 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | /******/ 46 | /******/ // identity function for calling harmony imports with the correct context 47 | /******/ __webpack_require__.i = function(value) { return value; }; 48 | /******/ 49 | /******/ // define getter function for harmony exports 50 | /******/ __webpack_require__.d = function(exports, name, getter) { 51 | /******/ if(!__webpack_require__.o(exports, name)) { 52 | /******/ Object.defineProperty(exports, name, { 53 | /******/ configurable: false, 54 | /******/ enumerable: true, 55 | /******/ get: getter 56 | /******/ }); 57 | /******/ } 58 | /******/ }; 59 | /******/ 60 | /******/ // getDefaultExport function for compatibility with non-harmony modules 61 | /******/ __webpack_require__.n = function(module) { 62 | /******/ var getter = module && module.__esModule ? 63 | /******/ function getDefault() { return module['default']; } : 64 | /******/ function getModuleExports() { return module; }; 65 | /******/ __webpack_require__.d(getter, 'a', getter); 66 | /******/ return getter; 67 | /******/ }; 68 | /******/ 69 | /******/ // Object.prototype.hasOwnProperty.call 70 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 71 | /******/ 72 | /******/ // __webpack_public_path__ 73 | /******/ __webpack_require__.p = ""; 74 | /******/ 75 | /******/ // Load entry module and return exports 76 | /******/ return __webpack_require__(__webpack_require__.s = 3); 77 | /******/ }) 78 | /************************************************************************/ 79 | /******/ ([ 80 | /* 0 */ 81 | /***/ (function(module, exports, __webpack_require__) { 82 | 83 | "use strict"; 84 | 85 | 86 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; 87 | 88 | var defaults = __webpack_require__(2); 89 | 90 | var typeofStr = _typeof(''); 91 | var typeofObj = _typeof({}); 92 | var typeofFn = _typeof(function () {}); 93 | 94 | var isObject = function isObject(x) { 95 | return (typeof x === 'undefined' ? 'undefined' : _typeof(x)) === typeofObj; 96 | }; 97 | var isString = function isString(x) { 98 | return (typeof x === 'undefined' ? 'undefined' : _typeof(x)) === typeofStr; 99 | }; 100 | var isFunction = function isFunction(x) { 101 | return (typeof x === 'undefined' ? 'undefined' : _typeof(x)) === typeofFn; 102 | }; 103 | var isCollection = function isCollection(x) { 104 | return isObject(x) && isFunction(x.collection); 105 | }; 106 | 107 | // Object.assign() polyfill 108 | var assign = __webpack_require__(1); 109 | 110 | var eleExists = function eleExists(ele) { 111 | return ele != null && !ele.removed(); 112 | }; 113 | 114 | var _elesHasEle = function elesHasEle(eles, ele) { 115 | if (eles.has != undefined) { 116 | // 3.x 117 | _elesHasEle = function elesHasEle(eles, ele) { 118 | return eles.has(ele); 119 | }; 120 | } else { 121 | // 2.x 122 | _elesHasEle = function elesHasEle(eles, ele) { 123 | return eles.intersection(ele).length > 0; 124 | }; 125 | } 126 | 127 | return _elesHasEle(eles, ele); 128 | }; 129 | 130 | var getEleMatchesSpecFn = function getEleMatchesSpecFn(spec) { 131 | if (isString(spec)) { 132 | return function (ele) { 133 | return ele.is(spec); 134 | }; 135 | } else if (isFunction(spec)) { 136 | return spec; 137 | } else if (isCollection(spec)) { 138 | return function (ele) { 139 | return _elesHasEle(spec, ele); 140 | }; 141 | } else { 142 | throw new Error('Can not create match function for spec', spec); 143 | } 144 | }; 145 | 146 | var bind = function bind(cy, events, selector, fn) { 147 | var b = { cy: cy, events: events, selector: selector || 'node', fn: fn }; 148 | 149 | cy.on(b.events, b.selector, b.fn); 150 | 151 | return b; 152 | }; 153 | 154 | var bindOnRule = function bindOnRule(rule, cy, events, selector, fn) { 155 | var b = bind(cy, events, selector, fn); 156 | var bindings = rule.bindings = rule.bindings || []; 157 | 158 | bindings.push(b); 159 | }; 160 | 161 | var unbindAllOnRule = function unbindAllOnRule(rule) { 162 | var unbind = function unbind(b) { 163 | b.cy.off(b.events, b.selector, b.fn); 164 | }; 165 | 166 | rule.bindings.forEach(unbind); 167 | 168 | rule.bindings = []; 169 | }; 170 | 171 | var getRepositioner = function getRepositioner(rule, cy) { 172 | var r = rule.reposition; 173 | 174 | if (r === 'mean') { 175 | return meanNeighborhoodPosition(getEleMatchesSpecFn(rule.meanIgnores)); 176 | } else if (r === 'viewport') { 177 | return viewportPosition(cy); 178 | } else if (r === 'drag') { 179 | return dragAlong(rule); 180 | } else if (isObject(r)) { 181 | if (r.type == undefined || r.type == "inside") { 182 | return boxPosition(r); 183 | } else if (r.type == "outside") { 184 | return outsideBoxPosition(r); 185 | } 186 | } else { 187 | return r; 188 | } 189 | }; 190 | 191 | var dragAlong = function dragAlong(rule) { 192 | return function (node) { 193 | var pos = node.position(); 194 | var delta = rule.delta; 195 | 196 | if (rule.delta != null && !node.same(rule.grabbedNode) && !node.grabbed()) { 197 | return { 198 | x: pos.x + delta.x, 199 | y: pos.y + delta.y 200 | }; 201 | } 202 | }; 203 | }; 204 | 205 | var meanNeighborhoodPosition = function meanNeighborhoodPosition(ignore) { 206 | return function (node) { 207 | var nhood = node.neighborhood(); 208 | var avgPos = { x: 0, y: 0 }; 209 | var nhoodSize = 0; 210 | 211 | for (var i = 0; i < nhood.length; i++) { 212 | var nhoodEle = nhood[i]; 213 | 214 | if (nhoodEle.isNode() && !ignore(nhoodEle)) { 215 | var pos = nhoodEle.position(); 216 | 217 | avgPos.x += pos.x; 218 | avgPos.y += pos.y; 219 | 220 | nhoodSize++; 221 | } 222 | } 223 | 224 | // the position should remain unchanged if we would stack the nodes on top of each other 225 | if (nhoodSize < 2) { 226 | return undefined; 227 | } 228 | 229 | avgPos.x /= nhoodSize; 230 | avgPos.y /= nhoodSize; 231 | 232 | return avgPos; 233 | }; 234 | }; 235 | 236 | var constrain = function constrain(val, min, max) { 237 | return val < min ? min : val > max ? max : val; 238 | }; 239 | 240 | var constrainInBox = function constrainInBox(node, bb) { 241 | var pos = node.position(); 242 | 243 | return { 244 | x: constrain(pos.x, bb.x1, bb.x2), 245 | y: constrain(pos.y, bb.y1, bb.y2) 246 | }; 247 | }; 248 | 249 | var boxPosition = function boxPosition(bb) { 250 | return function (node) { 251 | return constrainInBox(node, bb); 252 | }; 253 | }; 254 | 255 | var constrainOutsideBox = function constrainOutsideBox(node, bb) { 256 | var pos = node.position(); 257 | var x = pos.x, 258 | y = pos.y; 259 | var x1 = bb.x1, 260 | y1 = bb.y1, 261 | x2 = bb.x2, 262 | y2 = bb.y2; 263 | 264 | var inX = x1 <= x && x <= x2; 265 | var inY = y1 <= y && y <= y2; 266 | var abs = Math.abs; 267 | 268 | if (inX && inY) { 269 | // inside 270 | var dx1 = abs(x1 - x); 271 | var dx2 = abs(x2 - x); 272 | var dy1 = abs(y1 - y); 273 | var dy2 = abs(y2 - y); 274 | var min = Math.min(dx1, dx2, dy1, dy2); // which side of box is closest? 275 | 276 | // get position outside, by closest side of box 277 | if (min === dx1) { 278 | return { x: x1, y: y }; 279 | } else if (min === dx2) { 280 | return { x: x2, y: y }; 281 | } else if (min === dy1) { 282 | return { x: x, y: y1 }; 283 | } else { 284 | // min === dy2 285 | return { x: x, y: y2 }; 286 | } 287 | } else { 288 | // outside already 289 | return { x: x, y: y }; 290 | } 291 | }; 292 | 293 | var outsideBoxPosition = function outsideBoxPosition(bb) { 294 | return function (node) { 295 | return constrainOutsideBox(node, bb); 296 | }; 297 | }; 298 | 299 | var viewportPosition = function viewportPosition(cy) { 300 | return function (node) { 301 | var extent = cy.extent(); 302 | var w = node.outerWidth(); 303 | var h = node.outerHeight(); 304 | var bb = { 305 | x1: extent.x1 + w / 2, 306 | x2: extent.x2 - w / 2, 307 | y1: extent.y1 + h / 2, 308 | y2: extent.y2 - h / 2 309 | }; 310 | 311 | return constrainInBox(node, bb); 312 | }; 313 | }; 314 | 315 | var meanListener = function meanListener(rule) { 316 | return function (update, cy) { 317 | var matches = function matches(ele) { 318 | // must meet ele set and be connected to more than (1 edge + 1 node) 319 | return rule.matches(ele) && ele.neighborhood().length > 2 && !ele.grabbed(); 320 | }; 321 | 322 | bindOnRule(rule, cy, 'position', 'node', function () { 323 | var movedNode = this; 324 | 325 | if (movedNode.neighborhood().some(matches) || rule.meanOnSelfPosition(movedNode) && matches(movedNode)) { 326 | update(cy, [rule]); 327 | } 328 | }); 329 | 330 | bindOnRule(rule, cy, 'add remove', 'edge', function () { 331 | var edge = this; 332 | var src = cy.getElementById(edge.data('source')); 333 | var tgt = cy.getElementById(edge.data('target')); 334 | 335 | if ([src, tgt].some(matches)) { 336 | update(cy, [rule]); 337 | } 338 | }); 339 | }; 340 | }; 341 | 342 | var dragListener = function dragListener(rule) { 343 | return function (update, cy) { 344 | bindOnRule(rule, cy, 'grab', 'node', function () { 345 | var node = this; 346 | 347 | if (rule.dragWithMatches(node)) { 348 | var p = node.position(); 349 | 350 | rule.grabbedNode = node; 351 | rule.p1 = { x: p.x, y: p.y }; 352 | rule.delta = { x: 0, y: 0 }; 353 | } 354 | }); 355 | 356 | bindOnRule(rule, cy, 'drag', 'node', function () { 357 | var node = this; 358 | 359 | if (node.same(rule.grabbedNode)) { 360 | var d = rule.delta; 361 | var p1 = rule.p1; 362 | var p = node.position(); 363 | var p2 = { x: p.x, y: p.y }; 364 | 365 | d.x = p2.x - p1.x; 366 | d.y = p2.y - p1.y; 367 | 368 | rule.p1 = p2; 369 | 370 | update(cy, [rule]); 371 | } 372 | }); 373 | 374 | bindOnRule(rule, cy, 'free', 'node', function () { 375 | rule.grabbedNode = null; 376 | rule.delta = null; 377 | rule.p1 = null; 378 | }); 379 | }; 380 | }; 381 | 382 | var matchingNodesListener = function matchingNodesListener(rule) { 383 | return function (update, cy) { 384 | bindOnRule(rule, cy, 'position', 'node', function () { 385 | var movedNode = this; 386 | 387 | if (rule.matches(movedNode)) { 388 | update(cy, [rule]); 389 | } 390 | }); 391 | }; 392 | }; 393 | 394 | var getListener = function getListener(cy, rule) { 395 | if (rule.reposition === 'mean') { 396 | return meanListener(rule); 397 | } else if (rule.reposition === 'drag') { 398 | return dragListener(rule); 399 | } else if (isObject(rule.reposition) || rule.when === 'matching' || rule.reposition === 'viewport') { 400 | return matchingNodesListener(rule); 401 | } else { 402 | return rule.when; 403 | } 404 | }; 405 | 406 | var addRule = function addRule(cy, scratch, options) { 407 | var rule = assign({}, defaults, options); 408 | 409 | rule.getNewPos = getRepositioner(rule, cy); 410 | rule.listener = getListener(cy, rule); 411 | 412 | var nodesAreCollection = isCollection(rule.nodesMatching); 413 | 414 | if (nodesAreCollection) { 415 | rule.nodes = rule.nodesMatching.slice(); 416 | 417 | rule.matches = function (ele) { 418 | return eleExists(ele) && _elesHasEle(rule.nodes, ele); 419 | }; 420 | } else { 421 | var matches = getEleMatchesSpecFn(rule.nodesMatching); 422 | 423 | rule.matches = function (ele) { 424 | return eleExists(ele) && matches(ele); 425 | }; 426 | } 427 | 428 | if (rule.dragWith != null) { 429 | rule.dragWithMatches = getEleMatchesSpecFn(rule.dragWith); 430 | } 431 | 432 | rule.listener(function () { 433 | update(cy, [rule]); 434 | }, cy); 435 | 436 | rule.enabled = true; 437 | 438 | scratch.rules.push(rule); 439 | 440 | return rule; 441 | }; 442 | 443 | var bindForNodeList = function bindForNodeList(cy, scratch) { 444 | scratch.onAddNode = function (evt) { 445 | var target = evt.target; 446 | 447 | scratch.nodes.merge(target); 448 | }; 449 | 450 | scratch.onRmNode = function (evt) { 451 | var target = evt.target; 452 | 453 | scratch.nodes.unmerge(target); 454 | }; 455 | 456 | cy.on('add', 'node', scratch.onAddNode); 457 | cy.on('remove', 'node', scratch.onRmNode); 458 | }; 459 | 460 | var unbindForNodeList = function unbindForNodeList(cy, scratch) { 461 | cy.removeListener('add', 'node', scratch.onAddNode); 462 | cy.removeListener('remove', 'node', scratch.onRmNode); 463 | }; 464 | 465 | var update = function update(cy, rules) { 466 | var scratch = cy.scratch().automove; 467 | 468 | rules = rules != null ? rules : scratch.rules; 469 | 470 | cy.batch(function () { 471 | // batch for performance 472 | for (var i = 0; i < rules.length; i++) { 473 | var rule = rules[i]; 474 | 475 | if (rule.destroyed || !rule.enabled) { 476 | break; 477 | } // ignore destroyed rules b/c user may use custom when() 478 | 479 | var nodes = rule.nodes || scratch.nodes; 480 | 481 | for (var j = nodes.length - 1; j >= 0; j--) { 482 | var node = nodes[j]; 483 | 484 | if (node.removed()) { 485 | // remove from list for perf 486 | nodes.unmerge(node); 487 | continue; 488 | } 489 | 490 | if (!rule.matches(node)) { 491 | continue; 492 | } 493 | 494 | var pos = node.position(); 495 | var newPos = rule.getNewPos(node); 496 | var newPosIsDiff = newPos != null && (pos.x !== newPos.x || pos.y !== newPos.y); 497 | 498 | if (newPosIsDiff) { 499 | // only update on diff for perf 500 | node.position(newPos); 501 | 502 | node.trigger('automove', [rule]); 503 | } 504 | } 505 | } 506 | }); 507 | }; 508 | 509 | var automove = function automove(options) { 510 | var cy = this; 511 | 512 | var scratch = cy.scratch().automove = cy.scratch().automove || { 513 | rules: [] 514 | }; 515 | 516 | if (scratch.rules.length === 0) { 517 | scratch.nodes = cy.nodes().slice(); 518 | 519 | bindForNodeList(cy, scratch); 520 | } 521 | 522 | if (options === 'destroy') { 523 | scratch.rules.forEach(function (r) { 524 | unbindAllOnRule(r); 525 | 526 | r.destroyed = true; 527 | }); 528 | 529 | scratch.rules.splice(0, scratch.rules.length); 530 | 531 | unbindForNodeList(cy, scratch); 532 | 533 | return; 534 | } 535 | 536 | var rule = addRule(cy, scratch, options); 537 | 538 | update(cy, [rule]); // do an initial update to make sure the start state is correct 539 | 540 | return { 541 | apply: function apply() { 542 | update(cy, [rule]); 543 | }, 544 | 545 | disable: function disable() { 546 | this.toggle(false); 547 | }, 548 | 549 | enable: function enable() { 550 | this.toggle(true); 551 | }, 552 | 553 | enabled: function enabled() { 554 | return rule.enabled; 555 | }, 556 | 557 | toggle: function toggle(on) { 558 | rule.enabled = on !== undefined ? on : !rule.enabled; 559 | 560 | if (rule.enabled) { 561 | update(cy, [rule]); 562 | } 563 | }, 564 | 565 | destroy: function destroy() { 566 | var rules = scratch.rules; 567 | 568 | unbindAllOnRule(rule); 569 | 570 | rule.destroyed = true; 571 | 572 | rules.splice(rules.indexOf(rule), 1); 573 | 574 | if (rules.length === 0) { 575 | unbindForNodeList(cy, scratch); 576 | } 577 | 578 | return this; 579 | } 580 | }; 581 | }; 582 | 583 | module.exports = automove; 584 | 585 | /***/ }), 586 | /* 1 */ 587 | /***/ (function(module, exports, __webpack_require__) { 588 | 589 | "use strict"; 590 | 591 | 592 | // Simple, internal Object.assign() polyfill for options objects etc. 593 | 594 | module.exports = Object.assign != null ? Object.assign.bind(Object) : function (tgt) { 595 | for (var _len = arguments.length, srcs = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 596 | srcs[_key - 1] = arguments[_key]; 597 | } 598 | 599 | srcs.forEach(function (src) { 600 | Object.keys(src).forEach(function (k) { 601 | return tgt[k] = src[k]; 602 | }); 603 | }); 604 | 605 | return tgt; 606 | }; 607 | 608 | /***/ }), 609 | /* 2 */ 610 | /***/ (function(module, exports, __webpack_require__) { 611 | 612 | "use strict"; 613 | 614 | 615 | /* eslint-disable no-unused-vars */ 616 | var defaults = { 617 | // specify nodes that should be automoved with one of 618 | // - a function that returns true for matching nodes 619 | // - a selector that matches the nodes 620 | // - a collection of nodes (very good for performance) 621 | nodesMatching: function nodesMatching(node) { 622 | return false; 623 | }, 624 | 625 | // specify how a node's position should be updated with one of 626 | // - function( node ){ return { x: 1, y: 2 }; } => put the node where the function returns 627 | // - { x1, y1, x2, y2 } => constrain the node position within the bounding box (in model co-ordinates) 628 | // - { x1, y1, x2, y2, type: 'inside' } => constrain the node position within the bounding box (in model co-ordinates) 629 | // - { x1, y1, x2, y2, type: 'outside' } => constrain the node position outside the bounding box (in model co-ordinates) 630 | // - 'mean' => put the node in the average position of its neighbourhood 631 | // - 'viewport' => keeps the node body within the viewport 632 | // - 'drag' => matching nodes are effectively dragged along 633 | reposition: 'mean', 634 | 635 | // specify when the repositioning should occur by specifying a function that 636 | // calls update() when reposition updates should occur 637 | // - function( update ){ /* ... */ update(); } => a manual function for updating 638 | // - 'matching' => automatically update on position events for nodesMatching 639 | // - set efficiently and automatically for 640 | // - reposition: 'mean' 641 | // - reposition: { x1, y1, x2, y2 } 642 | // - reposition: 'viewport' 643 | // - reposition: 'drag' 644 | // - default/undefined => on a position event for any node (not as efficient...) 645 | when: undefined, 646 | 647 | // 648 | // customisation options for non-function `reposition` values 649 | // 650 | 651 | // `reposition: 'mean'` 652 | 653 | // specify nodes that should be ignored in the mean calculation 654 | // - a function that returns true for nodes to be ignored 655 | // - a selector that matches the nodes to be ignored 656 | // - a collection of nodes to be ignored (very good for performance) 657 | meanIgnores: function meanIgnores(node) { 658 | return false; 659 | }, 660 | 661 | // specify whether moving a particular `nodesMatching` node causes repositioning 662 | // - true : the mid node can't be independently moved/dragged 663 | // - false : the mid node can be independently moved/dragged (useful if you want the mid node to use `reposition: 'drag' in another rule with its neighbourhood`) 664 | meanOnSelfPosition: function meanOnSelfPosition(node) { 665 | return true; 666 | }, 667 | 668 | // `reposition: 'drag'` 669 | 670 | // specify nodes that when dragged cause the matched nodes to move along (i.e. the master nodes) 671 | // - a function that returns true for nodes to be listened to for drag events 672 | // - a selector that matches the nodes to be listened to for drag events 673 | // - a collection of nodes to be listened to for drag events (very good for performance) 674 | dragWith: function dragWith(node) { 675 | return false; 676 | } 677 | }; 678 | 679 | /* eslint-enable */ 680 | 681 | module.exports = defaults; 682 | 683 | /***/ }), 684 | /* 3 */ 685 | /***/ (function(module, exports, __webpack_require__) { 686 | 687 | "use strict"; 688 | 689 | 690 | var automove = __webpack_require__(0); 691 | 692 | // registers the extension on a cytoscape lib ref 693 | var register = function register(cytoscape) { 694 | if (!cytoscape) { 695 | return; 696 | } // can't register if cytoscape unspecified 697 | 698 | cytoscape('core', 'automove', automove); // register with cytoscape.js 699 | }; 700 | 701 | if (typeof cytoscape !== 'undefined') { 702 | // expose to global cytoscape (i.e. window.cytoscape) 703 | register(cytoscape); 704 | } 705 | 706 | module.exports = register; 707 | 708 | /***/ }) 709 | /******/ ]); 710 | }); -------------------------------------------------------------------------------- /demo-multiple-mean.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |