├── .npmignore ├── .babelrc ├── .gitignore ├── view-utilities-extension-demo.gif ├── bower.json ├── LICENSE ├── package.json ├── webpack.config.js ├── CITATION.cff ├── src ├── undo-redo.js ├── index.js └── view-utilities.js ├── README.md ├── cytoscape-view-utilities.js ├── demo.html └── demo-undoable.html /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | /nbproject/private/ 4 | /nbproject/* -------------------------------------------------------------------------------- /view-utilities-extension-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iVis-at-Bilkent/cytoscape.js-view-utilities/HEAD/view-utilities-extension-demo.gif -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cytoscape-view-utilities", 3 | "description": "Package of view utilities for cytoscape.js", 4 | "main": "cytoscape-view-utilities.js", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/iVis-at-Bilkent/view-utilities.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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-view-utilities", 3 | "version": "6.0.0", 4 | "description": "Package of view utilities for cytoscape.js", 5 | "main": "cytoscape-view-utilities.js", 6 | "spm": { 7 | "main": "cytopscape-view-utilities.js" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "build": "cross-env NODE_ENV=production webpack", 12 | "build:dev": "cross-env NODE_ENV=development webpack" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/iVis-at-Bilkent/cytoscape.js-view-utilities.git" 17 | }, 18 | "keywords": [ 19 | "cytoscape", 20 | "cyext" 21 | ], 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/iVis-at-Bilkent/cytoscape.js-view-utilities/issues" 25 | }, 26 | "homepage": "https://github.com/iVis-at-Bilkent/cytoscape.js-view-utilities", 27 | "devDependencies": { 28 | "@babel/core": "^7.11.6", 29 | "@babel/preset-env": "^7.11.5", 30 | "babel-loader": "^8.1.0", 31 | "camelcase": "^6.0.0", 32 | "cross-env": "^7.0.2", 33 | "webpack": "^4.44.2", 34 | "webpack-cli": "^3.3.12" 35 | }, 36 | "peerDependencies": { 37 | "cytoscape": "^3.2.0" 38 | }, 39 | "dependencies": { 40 | "geometric": "^2.2.3" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pkg = require('./package.json'); 3 | const camelcase = require('camelcase'); 4 | const process = require('process'); 5 | const env = process.env; 6 | 7 | const NODE_ENV = env.NODE_ENV; 8 | const PROD = NODE_ENV === 'production'; 9 | const SRC_DIR = './src'; 10 | 11 | let config = { 12 | // unless we are in production, use inline-source-map development tool 13 | // which helps track down bugs 14 | devtool: PROD ? false : 'inline-source-map', 15 | 16 | // entry point - src/index.js 17 | entry: path.join(__dirname, SRC_DIR, 'index.js'), 18 | 19 | // webpack throws warning if not provided a default mode 20 | // use the 'build:dev' script if you want development mode with non-minified file 21 | // this mode is used in 'build' script 22 | mode: 'production', 23 | output: { 24 | path: path.join( __dirname ), 25 | filename: pkg.name + '.js', 26 | library: camelcase( pkg.name ), 27 | libraryTarget: 'umd' 28 | }, 29 | // loader 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.js$/, 34 | exclude: /node_modules/, 35 | use: 'babel-loader' 36 | } 37 | ] 38 | }, 39 | // minimize file if mode is production 40 | optimization: { 41 | minimize: PROD ? true : false 42 | }, 43 | externals: { 44 | geometric: 'geometric' 45 | } 46 | }; 47 | 48 | module.exports = config; -------------------------------------------------------------------------------- /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-view-utilities" 20 | version: 6.0.0 21 | date-released: 2021-06-16 22 | url: "https://github.com/iVis-at-Bilkent/cytoscape.js-view-utilities" 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/undo-redo.js: -------------------------------------------------------------------------------- 1 | // Registers ur actions related to highlight 2 | function highlightUR(cy, ur, viewUtilities) { 3 | function getStatus(eles) { 4 | eles = eles ? eles : cy.elements(); 5 | var classes = viewUtilities.getAllHighlightClasses(); 6 | var r = []; 7 | for (var i = 0; i < classes.length; i++) { 8 | r.push(eles.filter(`.${classes[i]}:visible`)) 9 | } 10 | var selector = classes.map(x => '.' + x).join(','); 11 | // last element of array is elements which are not highlighted by any style 12 | r.push(eles.filter(":visible").not(selector)); 13 | 14 | return r; 15 | } 16 | 17 | function generalUndo(args) { 18 | var current = args.current; 19 | var r = []; 20 | for (var i = 0; i < args.length - 1; i++) { 21 | r.push(viewUtilities.highlight(args[i], i)); 22 | } 23 | // last element is for not highlighted by any style 24 | r.push(viewUtilities.removeHighlights(args[args.length - 1])); 25 | 26 | r['current'] = current; 27 | return r; 28 | } 29 | 30 | function generalRedo(args) { 31 | var current = args.current; 32 | var r = []; 33 | for (var i = 0; i < current.length - 1; i++) { 34 | r.push(viewUtilities.highlight(current[i], i)); 35 | } 36 | // last element is for not highlighted by any style 37 | r.push(viewUtilities.removeHighlights(current[current.length - 1])); 38 | 39 | r['current'] = current; 40 | return r; 41 | } 42 | 43 | function generateDoFunc(func) { 44 | return function (args) { 45 | var res = getStatus(); 46 | if (args.firstTime) 47 | viewUtilities[func](args.eles, args.idx); 48 | else 49 | generalRedo(args); 50 | 51 | res.current = getStatus(); 52 | 53 | return res; 54 | }; 55 | } 56 | 57 | ur.action("highlightNeighbors", generateDoFunc("highlightNeighbors"), generalUndo); 58 | ur.action("highlight", generateDoFunc("highlight"), generalUndo); 59 | ur.action("removeHighlights", generateDoFunc("removeHighlights"), generalUndo); 60 | } 61 | 62 | // Registers ur actions related to hide/show 63 | function hideShowUR(cy, ur, viewUtilities) { 64 | function urShow(eles) { 65 | return viewUtilities.show(eles); 66 | } 67 | 68 | function urHide(eles) { 69 | return viewUtilities.hide(eles); 70 | } 71 | 72 | function urShowHiddenNeighbors(eles) { 73 | return viewUtilities.showHiddenNeighbors(eles); 74 | } 75 | 76 | ur.action("show", urShow, urHide); 77 | ur.action("hide", urHide, urShow); 78 | ur.action("showHiddenNeighbors",urShowHiddenNeighbors, urHide); 79 | } 80 | 81 | module.exports = function (cy, ur, viewUtilities) { 82 | highlightUR(cy, ur, viewUtilities); 83 | hideShowUR(cy, ur, viewUtilities); 84 | }; 85 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | ; 2 | (function () { 3 | 'use strict'; 4 | 5 | // registers the extension on a cytoscape lib ref 6 | var register = function (cytoscape) { 7 | 8 | if (!cytoscape) { 9 | return; 10 | } // can't register if cytoscape unspecified 11 | 12 | var options = { 13 | highlightStyles: [], 14 | selectStyles: {}, 15 | setVisibilityOnHide: false, // whether to set visibility on hide/show 16 | setDisplayOnHide: true, // whether to set display on hide/show 17 | zoomAnimationDuration: 1500, //default duration for zoom animation speed 18 | neighbor: function (ele) { // return desired neighbors of tapheld node 19 | return false; 20 | }, 21 | neighborSelectTime: 500, //ms, time to taphold to select desired neighbors 22 | lassoStyle: { lineColor: "#d67614", lineWidth: 3 }, 23 | htmlElem4marqueeZoom: '', // should be string like `#cy` or `.cy`. `#cy` means get element with the ID 'cy'. `.cy` means the element with class 'cy' 24 | marqueeZoomCursor: 'se-resize', // the cursor that should be used when marquee zoom is enabled. It can also be an image if a URL to an image is given 25 | isShowEdgesBetweenVisibleNodes: true // When showing elements, show edges if both source and target nodes become visible 26 | }; 27 | 28 | var undoRedo = require("./undo-redo"); 29 | var viewUtilities = require("./view-utilities"); 30 | 31 | cytoscape('core', 'viewUtilities', function (opts) { 32 | var cy = this; 33 | 34 | function getScratch(eleOrCy) { 35 | if (!eleOrCy.scratch("_viewUtilities")) { 36 | eleOrCy.scratch("_viewUtilities", {}); 37 | } 38 | 39 | return eleOrCy.scratch("_viewUtilities"); 40 | } 41 | 42 | // If 'get' is given as the param then return the extension instance 43 | if (opts === 'get') { 44 | return getScratch(cy).instance; 45 | } 46 | 47 | /** 48 | * Deep copy or merge objects - replacement for jQuery deep extend 49 | * Taken from http://youmightnotneedjquery.com/#deep_extend 50 | * and bug related to deep copy of Arrays is fixed. 51 | * Usage:Object.extend({}, objA, objB) 52 | */ 53 | function extendOptions(out) { 54 | out = out || {}; 55 | 56 | for (var i = 1; i < arguments.length; i++) { 57 | var obj = arguments[i]; 58 | 59 | if (!obj) 60 | continue; 61 | 62 | for (var key in obj) { 63 | if (obj.hasOwnProperty(key)) { 64 | if (Array.isArray(obj[key])) { 65 | out[key] = obj[key].slice(); 66 | } else if (typeof obj[key] === 'object') { 67 | out[key] = extendOptions(out[key], obj[key]); 68 | } else { 69 | out[key] = obj[key]; 70 | } 71 | } 72 | } 73 | } 74 | 75 | return out; 76 | }; 77 | 78 | options = extendOptions({}, options, opts); 79 | 80 | // create a view utilities instance 81 | var instance = viewUtilities(cy, options); 82 | 83 | if (cy.undoRedo) { 84 | var ur = cy.undoRedo(null, true); 85 | undoRedo(cy, ur, instance); 86 | } 87 | 88 | // set the instance on the scratch pad 89 | getScratch(cy).instance = instance; 90 | 91 | if (!getScratch(cy).initialized) { 92 | getScratch(cy).initialized = true; 93 | 94 | var shiftKeyDown = false; 95 | document.addEventListener('keydown', function (event) { 96 | if (event.key == "Shift") { 97 | shiftKeyDown = true; 98 | } 99 | }); 100 | document.addEventListener('keyup', function (event) { 101 | if (event.key == "Shift") { 102 | shiftKeyDown = false; 103 | } 104 | }); 105 | //Select the desired neighbors after taphold-and-free 106 | cy.on('taphold', 'node, edge', function (event) { 107 | var target = event.target || event.cyTarget; 108 | var tapheld = false; 109 | var neighborhood; 110 | var timeout = setTimeout(function () { 111 | if (shiftKeyDown) { 112 | cy.elements().unselect(); 113 | neighborhood = options.neighbor(target); 114 | if (neighborhood) 115 | neighborhood.select(); 116 | target.lock(); 117 | 118 | // this call is necessary to make sure 119 | // the tapheld node or edge stays selected 120 | // after releasing taphold 121 | target.unselectify(); 122 | 123 | // tracks whether the taphold event happened 124 | // necessary if we want to keep 'neighborSelectTime' 125 | // property, otherwise unnecessary 126 | tapheld = true; 127 | } 128 | }, options.neighborSelectTime - 500); 129 | 130 | // this listener prevents the original tapheld node or edge 131 | // from being unselected after releasing from taphold 132 | // together with the 'unselectify' call above 133 | // called as one time event since it's defined inside another event, 134 | // shouldn't be defined over and over with 'on' 135 | cy.one('tapend', function () { 136 | if (tapheld) { 137 | setTimeout(function () { 138 | target.selectify(); 139 | target.unlock(); 140 | tapheld = false; 141 | }, 100); 142 | } 143 | else { 144 | clearTimeout(timeout); 145 | } 146 | }); 147 | 148 | cy.one('drag', 'node', function (e) { 149 | var targetDragged = e.target || e.cyTarget; 150 | if (target == targetDragged && tapheld === false) { 151 | clearTimeout(timeout); 152 | } 153 | }) 154 | }); 155 | } 156 | 157 | // return the instance of extension 158 | return getScratch(cy).instance; 159 | }); 160 | 161 | }; 162 | 163 | if (typeof module !== 'undefined' && module.exports) { // expose as a commonjs module 164 | module.exports = register; 165 | } 166 | 167 | if (typeof define !== 'undefined' && define.amd) { // expose as an amd/requirejs module 168 | define('cytoscape-view-utilities', function () { 169 | return register; 170 | }); 171 | } 172 | 173 | if (typeof cytoscape !== 'undefined') { // expose to global cytoscape (i.e. window.cytoscape) 174 | register(cytoscape); 175 | } 176 | 177 | })(); 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cytoscape-view-utilities 2 | ================================================================================ 3 | 4 | ## Description 5 | 6 | This Cytoscape.js extension provides miscellenaous view utilities such as hide/show, highlight, marquee zoom and free form selection, distributed under [The MIT License](https://opensource.org/licenses/MIT). 7 | 8 | ![](view-utilities-extension-demo.gif) 9 | 10 | Please cite the following paper when using this extension: 11 | 12 | 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. 13 | 14 | ## Demo 15 | 16 | Click [here](https://ivis-at-bilkent.github.io/cytoscape.js-view-utilities/demo.html) (no undo) or [here](https://ivis-at-bilkent.github.io/cytoscape.js-view-utilities/demo-undoable.html) (undoable) for a demo 17 | 18 | ## API 19 | 20 | `var instance = cy.viewUtilities(options)`
21 | @param options — If not provided, default options will be used. See the below section for default options. 22 | `highlightStyles` is array of objects. The objects should follow the format `{node: ..., edge: ...}`. `selectStyles` will be used if you want to override the highlighted styles when the objects are selected. 23 | `lassoStyle` will be used to override the lasso line style.
24 | e.g 25 | ```js 26 | var options = { 27 | highlightStyles: [ 28 | { node: { 'border-color': '#0b9bcd', 'border-width': 3 }, edge: {'line-color': '#0b9bcd', 'source-arrow-color': '#0b9bcd', 'target-arrow-color': '#0b9bcd', 'width' : 3} }, 29 | { node: { 'border-color': '#04f06a', 'border-width': 3 }, edge: {'line-color': '#04f06a', 'source-arrow-color': '#04f06a', 'target-arrow-color': '#04f06a', 'width' : 3} }, 30 | ], 31 | selectStyles: { 32 | node: {'border-color': 'black', 'border-width': 3, 'background-color': 'lightgrey'}, 33 | edge: {'line-color': 'black', 'source-arrow-color': 'black', 'target-arrow-color': 'black', 'width' : 3} 34 | }, 35 | setVisibilityOnHide: false, // whether to set visibility on hide/show 36 | setDisplayOnHide: true, // whether to set display on hide/show 37 | zoomAnimationDuration: 1500, // default duration for zoom animation speed 38 | neighbor: function(ele){ 39 | return ele.closedNeighborhood(); 40 | }, 41 | neighborSelectTime: 500, 42 | lassoStyle: {lineColor: "#d67614", lineWidth: 3} // default lasso line color, dark orange, and default line width 43 | htmlElem4marqueeZoom: '', // should be string like `#cy` or `.cy`. `#cy` means get element with the ID 'cy'. `.cy` means the element with class 'cy' 44 | marqueeZoomCursor: 'se-resize', // the cursor that should be used when marquee zoom is enabled. It can also be an image if a URL to an image is given 45 | isShowEdgesBetweenVisibleNodes: true // When showing elements, show edges if both source and target nodes become visible 46 | }; 47 | var api = cy.viewUtilities(options); 48 | ``` 49 | 50 | `instance.highlight(eles, idx = 0)`
51 | @param eles — [a cytoscape.js collection](https://js.cytoscape.org/#cy.collection) (collection of elements) to be highlighted
52 | @param idx — The index of the cytoscape.js style. If you don't specify it, the first style will be used.
53 | Apply style class to the specified elements. Style class is specified with its index
54 | 55 | `instance.highlightNeighbors(eles, idx = 0)`
56 | @param eles — [a cytoscape.js collection](https://js.cytoscape.org/#cy.collection) (collection of elements) to be highlighted
57 | @param idx — The index of the cytoscape.js style. If you don't specify it, the first style will be used.
58 | Highlights elements' neighborhood (based on the color option). Similar to the highlight function, either the elements and highlighting option can both be sent in the arguments. If only the elements are sent, then the default highlight color is used. 59 | 60 | `instance.removeHighlights(eles)`
61 | @param eles — elements to remove highlights
62 | Remove highlights from eles. 63 | 64 | `instance.hide(eles)`
65 | @param eles — elements to hide
66 | Hides given eles. 67 | 68 | `instance.show(eles)`
69 | @param eles — elements to show
70 | Unhides given eles. 71 | 72 | `instance.showHiddenNeighbors(eles)`
73 | @param eles — elements to show hidden neighbors
74 | Unhides hidden neigbors of given eles. Note that compound nodes are not respected as expected. 75 | 76 | `instance.zoomToSelected(eles)`
77 | @param eles — elements to zoom
78 | Zoom to selected eles. 79 | 80 | `instance.enableMarqueeZoom(callback)`
81 | @param callback — is called at the end of the function
82 | Enables marquee zoom. It is automatically called when CTRL+Shift keys are down. 83 | 84 | `instance.disableMarqueeZoom()`
85 | Disables marquee zoom. It is automatically called when CTRL+Shift keys are up. 86 | 87 | `instance.enableLassoMode(callback)`
88 | @param callback — is called at the end of the function
89 | Enables lasso tool. 90 | 91 | `instance.disableLassoMode()`
92 | Disables lasso tool. 93 | 94 | `instance.getHighlightStyles()`
95 | Returns current `highlightStyles` which is an array of objects like below 96 | ``` 97 | [ 98 | { node: { 'border-color': '#0b9bcd', 'border-width': 3 }, edge: {'line-color': '#0b9bcd', 'source-arrow-color': '#0b9bcd', 'target-arrow-color': '#0b9bcd', 'width' : 3} }, 99 | { node: { 'border-color': '#bf0603', 'border-width': 3 }, edge: {'line-color': '#bf0603', 'source-arrow-color': '#bf0603', 'target-arrow-color': '#bf0603', 'width' : 3} }, 100 | ], 101 | ``` 102 | 103 | `instance.getAllHighlightClasses()`
104 | Returns all currently used [cytoscape.js style classes](https://js.cytoscape.org/#selectors/group-class-amp-id) 105 | 106 | `instance.changeHighlightStyle(idx, nodeStyle, edgeStyle) `
107 | @param idx — index of the style that is going to be changed
108 | @param nodeStyle — [cytoscape style](https://js.cytoscape.org/#style) for nodes
109 | @param edgeStyle — [cytoscape style](https://js.cytoscape.org/#style) for edges
110 | Changes the style specified with `idx`. 111 | 112 | `instance.addHighlightStyle(nodeStyle, edgeStyle) `
113 | @param nodeStyle — [cytoscape style](https://js.cytoscape.org/#style) for nodes
114 | @param edgeStyle — [cytoscape style](https://js.cytoscape.org/#style) for edges
115 | Adds a new style to the `highlightStyles` array. 116 | 117 | `instance.removeHighlightStyle(styleIdx): void`
118 | @param styleIdx — index of the style to delete (0 based)
119 | Removes the style from `highlightStyles` array. 120 | 121 | `instance.changeLassoStyle(styleObj) `
122 | @param styleObj — lasso line style object with lineColor and/or lineWidth properties 123 | 124 | ## Default Options 125 | ```js 126 | var options = { 127 | highlightStyles: [], 128 | selectStyles: {}, 129 | setVisibilityOnHide: false, // whether to set visibility on hide/show 130 | setDisplayOnHide: true, // whether to set display on hide/show 131 | zoomAnimationDuration: 1500, // default duration for zoom animation speed 132 | neighbor: function (ele) { // return desired neighbors of tapheld element 133 | return false; 134 | }, 135 | neighborSelectTime: 500, // ms, time to taphold to select desired neighbors in addition to the taphold event detect time by cytoscape 136 | lassoStyle: {lineColor: "#d67614", lineWidth: 3} // default lasso line color, dark orange, and default line width 137 | }; 138 | ``` 139 | 140 | ## Default Undo-Redo Actions 141 | 142 | `ur.do("highlight", args)` 143 | 144 | `ur.do("highlightNeighbors", args)` 145 | `ur.do("highlightNeighbours", args)` 146 | 147 | `ur.do("removeHighlights")` 148 | 149 | `ur.do("hide", eles)` 150 | 151 | `ur.do("show", eles)` 152 | 153 | ## Dependencies 154 | 155 | * Cytoscape.js ^3.2.0 156 | * Geometric.js ^2.2.3 157 | * cytoscape-undo-redo.js ^1.0.8 (optional) 158 | 159 | ## Usage instructions 160 | 161 | Download the library: 162 | * via npm: `npm install cytoscape-view-utilities` , 163 | * via bower: `bower install cytoscape-view-utilities` , or 164 | * via direct download in the repository (probably from a tag). 165 | 166 | `require()` the library as appropriate for your project: 167 | 168 | CommonJS: 169 | 170 | ``` js 171 | var cytoscape = require('cytoscape'); 172 | var viewUtilities = require('cytoscape-view-utilities'); 173 | 174 | viewUtilities(cytoscape); // register extension 175 | ``` 176 | 177 | AMD: 178 | 179 | ``` js 180 | require(['cytoscape', 'cytoscape-view-utilities'], function(cytoscape, view - utilities) { 181 | view - utilities(cytoscape); // register extension 182 | }); 183 | ``` 184 | 185 | Plain HTML/JS has the extension registered for you automatically, because no `require()` is needed. 186 | 187 | ## Build targets 188 | 189 | * `npm run build` : Build `./src/**` into `cytoscape-view-utilities.js` in production environment and minimize the file. 190 | * `npm run build:dev` : Build `./src/**` into `cytoscape-view-utilities.js` in development environment without minimizing the file. 191 | 192 | ## Publishing instructions 193 | 194 | This project is set up to automatically be published to npm and bower. To publish: 195 | 196 | 1. Build the extension : `npm run build` 197 | 1. Commit the build : `git commit -am "Build for release"` 198 | 1. Bump the version number and tag: `npm version major|minor|patch` 199 | 1. Push to origin: `git push && git push --tags` 200 | 1. Publish to npm: `npm publish .` 201 | 1. If publishing to bower for the first time, you'll need to run `bower register cytoscape-view-utilities https://github.com/iVis-at-Bilkent/view-utilities.git` 202 | 203 | ## Team 204 | 205 | + [Hasan Balci](https://github.com/hasanbalci), [Metin Can Siper](https://github.com/metincansiper), [Huseyin Eren Calik](https://github.com/herencalik), [Mubashira Zaman](https://github.com/MobiZaman), and [Ugur Dogrusoz](https://github.com/ugurdogrusoz) of [i-Vis at Bilkent University](http://www.cs.bilkent.edu.tr/~ivis) 206 | 207 | ## Alumni 208 | 209 | + [Selim Firat Yilmaz](https://github.com/mrsfy), [Leonard Dervishi](https://github.com/leonarddrv), [Kaan Sancak](https://github.com/kaansancak) 210 | 211 | -------------------------------------------------------------------------------- /cytoscape-view-utilities.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("geometric")):"function"==typeof define&&define.amd?define(["geometric"],t):"object"==typeof exports?exports.cytoscapeViewUtilities=t(require("geometric")):e.cytoscapeViewUtilities=t(e.geometric)}(window,(function(e){return function(e){var t={};function n(o){if(t[o])return t[o].exports;var i=t[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,n),i.l=!0,i.exports}return n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(o,i,function(t){return e[t]}.bind(null,i));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){var o;function i(e){return(i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}!function(){"use strict";var r=function(e){if(e){var t={highlightStyles:[],selectStyles:{},setVisibilityOnHide:!1,setDisplayOnHide:!0,zoomAnimationDuration:1500,neighbor:function(e){return!1},neighborSelectTime:500,lassoStyle:{lineColor:"#d67614",lineWidth:3},htmlElem4marqueeZoom:"",marqueeZoomCursor:"se-resize",isShowEdgesBetweenVisibleNodes:!0},o=n(1),r=n(2);e("core","viewUtilities",(function(e){var n=this;function l(e){return e.scratch("_viewUtilities")||e.scratch("_viewUtilities",{}),e.scratch("_viewUtilities")}if("get"===e)return l(n).instance;t=function e(t){t=t||{};for(var n=1;n0&&void 0!==arguments[0]&&arguments[0];if(t.htmlElem4marqueeZoom){var n=null;t.htmlElem4marqueeZoom.startsWith(".")&&(n=document.getElementsByClassName(t.htmlElem4marqueeZoom.substr(1))[0]),t.htmlElem4marqueeZoom.startsWith("#")&&(n=document.getElementById(t.htmlElem4marqueeZoom.substr(1))),n?e?n.style.cursor=c:(c=n.style.cursor,t.marqueeZoomCursor.includes(".")?n.style.cursor="url('".concat(t.marqueeZoomCursor,"'), pointer"):n.style.cursor=t.marqueeZoomCursor):console.log("element not found!")}}function g(){t.selectStyles.node&&e.style().selector("node:selected").css(t.selectStyles.node).update(),t.selectStyles.edge&&e.style().selector("edge:selected").css(t.selectStyles.edge).update()}function f(n){var i=o[n],r=t.highlightStyles[n].node,l=t.highlightStyles[n].edge;e.style().selector("node."+i).css(r).update(),e.style().selector("edge."+i).css(l).update()}function y(n,i){e.startBatch();for(var r=0;r1&&void 0!==arguments[1]?arguments[1]:0;return y(e,t),e},b.getHighlightStyles=function(){return t.highlightStyles},b.highlightNeighbors=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;return this.highlight(m(e),t)},b.removeHighlights=function(n){e.startBatch(),null!=n&&null!=n.length||(n=e.elements());for(var i=0;it.highlightStyles.length-1||(e.elements().removeClass(o[n]),t.highlightStyles.splice(n,1),o.splice(n,1))},b.getAllHighlightClasses=function(){for(var e=[],n=0;n=200||l>=200?50:e.width()s){var l=o;o=s,s=l}if(i>u){l=i;i=u,u=l}var a;s-o<200&&(o-=a=(200-(s-o))/2,s+=a);u-i<200&&(i-=a=(200-(u-i))/2,u+=a);if(o>e.elements().boundingBox().x2||se.elements().boundingBox().y2||u options.highlightStyles.length - 1) { 251 | return; 252 | } 253 | cy.elements().removeClass(classNames4Styles[styleIdx]); 254 | options.highlightStyles.splice(styleIdx, 1); 255 | classNames4Styles.splice(styleIdx, 1); 256 | }; 257 | 258 | instance.getAllHighlightClasses = function () { 259 | var a = []; 260 | for (var i = 0; i < options.highlightStyles.length; i++) { 261 | a.push(classNames4Styles[i]); 262 | } 263 | return classNames4Styles; 264 | }; 265 | 266 | //Zoom selected Nodes 267 | instance.zoomToSelected = function (eles) { 268 | var boundingBox = eles.boundingBox(); 269 | var diff_x = Math.abs(boundingBox.x1 - boundingBox.x2); 270 | var diff_y = Math.abs(boundingBox.y1 - boundingBox.y2); 271 | var padding; 272 | if (diff_x >= 200 || diff_y >= 200) { 273 | padding = 50; 274 | } 275 | else { 276 | padding = (cy.width() < cy.height()) ? 277 | ((200 - diff_x) / 2 * cy.width() / 200) : ((200 - diff_y) / 2 * cy.height() / 200); 278 | } 279 | 280 | cy.animate({ 281 | fit: { 282 | eles: eles, 283 | padding: padding 284 | } 285 | }, { 286 | duration: options.zoomAnimationDuration 287 | }); 288 | return eles; 289 | }; 290 | 291 | //Marquee Zoom 292 | var tabStartHandler; 293 | var tabEndHandler; 294 | 295 | instance.enableMarqueeZoom = function (callback) { 296 | setCursor(false); 297 | marqueeZoomEnabled = true; 298 | var rect_start_pos_x, rect_start_pos_y, rect_end_pos_x, rect_end_pos_y; 299 | //Make the cy unselectable 300 | cy.autounselectify(true); 301 | 302 | cy.one('tapstart', tabStartHandler = function (event) { 303 | if (shiftKeyDown == true) { 304 | rect_start_pos_x = event.position.x; 305 | rect_start_pos_y = event.position.y; 306 | rect_end_pos_x = undefined; 307 | } 308 | }); 309 | cy.one('tapend', tabEndHandler = function (event) { 310 | rect_end_pos_x = event.position.x; 311 | rect_end_pos_y = event.position.y; 312 | //check whether corners of rectangle is undefined 313 | //abort marquee zoom if one corner is undefined 314 | if (rect_start_pos_x == undefined || rect_end_pos_x == undefined) { 315 | cy.autounselectify(false); 316 | if (callback) { 317 | callback(); 318 | } 319 | return; 320 | } 321 | //Reoder rectangle positions 322 | //Top left of the rectangle (rect_start_pos_x, rect_start_pos_y) 323 | //right bottom of the rectangle (rect_end_pos_x, rect_end_pos_y) 324 | if (rect_start_pos_x > rect_end_pos_x) { 325 | var temp = rect_start_pos_x; 326 | rect_start_pos_x = rect_end_pos_x; 327 | rect_end_pos_x = temp; 328 | } 329 | if (rect_start_pos_y > rect_end_pos_y) { 330 | var temp = rect_start_pos_y; 331 | rect_start_pos_y = rect_end_pos_y; 332 | rect_end_pos_y = temp; 333 | } 334 | 335 | //Extend sides of selected rectangle to 200px if less than 100px 336 | if (rect_end_pos_x - rect_start_pos_x < 200) { 337 | var extendPx = (200 - (rect_end_pos_x - rect_start_pos_x)) / 2; 338 | rect_start_pos_x -= extendPx; 339 | rect_end_pos_x += extendPx; 340 | } 341 | if (rect_end_pos_y - rect_start_pos_y < 200) { 342 | var extendPx = (200 - (rect_end_pos_y - rect_start_pos_y)) / 2; 343 | rect_start_pos_y -= extendPx; 344 | rect_end_pos_y += extendPx; 345 | } 346 | 347 | //Check whether rectangle intersects with bounding box of the graph 348 | //if not abort marquee zoom 349 | if ((rect_start_pos_x > cy.elements().boundingBox().x2) 350 | || (rect_end_pos_x < cy.elements().boundingBox().x1) 351 | || (rect_start_pos_y > cy.elements().boundingBox().y2) 352 | || (rect_end_pos_y < cy.elements().boundingBox().y1)) { 353 | cy.autounselectify(false); 354 | if (callback) { 355 | callback(); 356 | } 357 | return; 358 | } 359 | 360 | //Calculate zoom level 361 | var zoomLevel = Math.min(cy.width() / (Math.abs(rect_end_pos_x - rect_start_pos_x)), 362 | cy.height() / Math.abs(rect_end_pos_y - rect_start_pos_y)); 363 | 364 | var diff_x = cy.width() / 2 - (cy.pan().x + zoomLevel * (rect_start_pos_x + rect_end_pos_x) / 2); 365 | var diff_y = cy.height() / 2 - (cy.pan().y + zoomLevel * (rect_start_pos_y + rect_end_pos_y) / 2); 366 | 367 | cy.animate({ 368 | panBy: { x: diff_x, y: diff_y }, 369 | zoom: zoomLevel, 370 | duration: options.zoomAnimationDuration, 371 | complete: function () { 372 | if (callback) { 373 | callback(); 374 | } 375 | cy.autounselectify(false); 376 | } 377 | }); 378 | }); 379 | }; 380 | 381 | instance.disableMarqueeZoom = function () { 382 | setCursor(true); 383 | cy.off('tapstart', tabStartHandler); 384 | cy.off('tapend', tabEndHandler); 385 | cy.autounselectify(false); 386 | marqueeZoomEnabled = false; 387 | }; 388 | //Lasso Mode 389 | var geometric = require('geometric'); 390 | 391 | instance.changeLassoStyle = function (styleObj) { 392 | if (styleObj.lineWidth) 393 | options.lassoStyle.lineWidth = styleObj.lineWidth; 394 | if (styleObj.lineColor) 395 | options.lassoStyle.lineColor = styleObj.lineColor; 396 | }; 397 | 398 | instance.enableLassoMode = function (callback) { 399 | 400 | var isClicked = false; 401 | var tempCanv = document.createElement('canvas'); 402 | tempCanv.id = 'lasso-canvas'; 403 | const container = cy.container(); 404 | container.appendChild(tempCanv); 405 | 406 | const width = container.offsetWidth; 407 | const height = container.offsetHeight; 408 | 409 | tempCanv.width = width; 410 | tempCanv.height = height; 411 | tempCanv.setAttribute("style", `z-index: 1000; position: absolute; top: 0; left: 0;`,); 412 | 413 | cy.panningEnabled(false); 414 | cy.zoomingEnabled(false); 415 | cy.autounselectify(true); 416 | var points = []; 417 | 418 | tempCanv.onclick = function (event) { 419 | 420 | if (isClicked == false) { 421 | isClicked = true; 422 | var context = tempCanv.getContext("2d"); 423 | context.strokeStyle = options.lassoStyle.lineColor; 424 | context.lineWidth = options.lassoStyle.lineWidth; 425 | context.lineJoin = "round"; 426 | cy.panningEnabled(false); 427 | cy.zoomingEnabled(false); 428 | cy.autounselectify(true); 429 | var formerX = event.offsetX; 430 | var formerY = event.offsetY; 431 | 432 | points.push([formerX, formerY]); 433 | tempCanv.onmouseleave = function (e) { 434 | isClicked = false; 435 | container.removeChild(tempCanv); 436 | tempCanv = null; 437 | cy.panningEnabled(true); 438 | cy.zoomingEnabled(true); 439 | cy.autounselectify(false); 440 | if (callback) { 441 | callback(); 442 | } 443 | }; 444 | tempCanv.onmousemove = function (e) { 445 | context.beginPath(); 446 | points.push([e.offsetX, e.offsetY]); 447 | context.moveTo(formerX, formerY); 448 | context.lineTo(e.offsetX, e.offsetY); 449 | formerX = e.offsetX; 450 | formerY = e.offsetY; 451 | context.stroke(); 452 | context.closePath(); 453 | }; 454 | } 455 | else { 456 | var eles = cy.elements(); 457 | points.push(points[0]); 458 | for (var i = 0; i < eles.length; i++) { 459 | if (eles[i].isEdge()) { 460 | 461 | var p1 = [(eles[i].sourceEndpoint().x) * cy.zoom() + cy.pan().x, (eles[i].sourceEndpoint().y) * cy.zoom() + cy.pan().y]; 462 | var p2 = [(eles[i].targetEndpoint().x) * cy.zoom() + cy.pan().x, (eles[i].targetEndpoint().y) * cy.zoom() + cy.pan().y]; 463 | 464 | if (geometric.pointInPolygon(p1, points) && geometric.pointInPolygon(p2, points)) { 465 | eles[i].select(); 466 | } 467 | 468 | } 469 | else { 470 | cy.autounselectify(false); 471 | var bb = [[eles[i].renderedBoundingBox().x1, eles[i].renderedBoundingBox().y1], 472 | [eles[i].renderedBoundingBox().x1, eles[i].renderedBoundingBox().y2], 473 | [eles[i].renderedBoundingBox().x2, eles[i].renderedBoundingBox().y2], 474 | [eles[i].renderedBoundingBox().x2, eles[i].renderedBoundingBox().y1]]; 475 | 476 | if (geometric.polygonIntersectsPolygon(bb, points) || geometric.polygonInPolygon(bb, points) 477 | || geometric.polygonInPolygon(points, bb)) { 478 | eles[i].select(); 479 | } 480 | } 481 | } 482 | isClicked = false; 483 | container.removeChild(tempCanv); 484 | tempCanv = null; 485 | 486 | cy.panningEnabled(true); 487 | cy.zoomingEnabled(true); 488 | if (callback) { 489 | callback(); 490 | } 491 | } 492 | }; 493 | }; 494 | 495 | instance.disableLassoMode = function () { 496 | var c = document.getElementById('lasso-canvas'); 497 | if (c) { 498 | c.parentElement.removeChild(c); 499 | c = null; 500 | } 501 | cy.panningEnabled(true); 502 | cy.zoomingEnabled(true); 503 | cy.autounselectify(false); 504 | } 505 | // return the instance 506 | return instance; 507 | }; 508 | 509 | module.exports = viewUtilities; 510 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | cytoscape-view-utilities.js demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 124 | 487 | 488 | 489 | 490 |

cytoscape.js-view-utilities demo

491 |
492 | Hide Selected / Show All

493 | 494 | Highlight Color: 495 |
496 | 498 | 499 | 500 | 501 |
502 |
503 | 504 | Highlight Selected

505 | Highlight Neighbors

506 | Show Hidden Neighbors of Selected

507 | Remove Selected Highlights

508 | Remove All Highlights

509 | Zoom to Selected

510 | Marquee Zoom 511 |
512 | Shift + drag to specify region; gets disabled after zoom, mouse events on canvas and other api functions calls. 513 |

514 | Lasso Tool 515 |
516 | Left click to start specifying region; left click again to complete the region. 517 |
518 | Line width (between 1 and 10) and Color: 519 | 520 |

521 | 522 | 523 |

524 | Shift + taphold to select neighbors 525 | 526 |
527 |
528 | 529 | 530 | 531 | -------------------------------------------------------------------------------- /demo-undoable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | cytoscape-view-utilities.js demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 115 | 116 | 514 | 515 | 516 | 517 |

cytoscape.js-view-utilities demo undoable

518 | 519 |
520 | Hide Selected / Show All

521 | 522 | Highlight Color: 523 |
524 | 526 | 527 | 528 | 529 |
530 |
531 | 532 | Highlight Selected

533 | Highlight Neighbors

534 | Show Hidden Neighbors of Selected

535 | Remove Selected Highlights

536 | Remove All Highlights

537 | Zoom to Selected

538 | Marquee Zoom 539 |
540 | Shift + drag to specify region; gets disabled after zoom, mouse events on canvas and other api functions calls. 541 |

542 | Lasso Tool 543 |
544 | Left click to start specifying region; left click again to complete the region. 545 |
546 | Line width (between 1 and 10) and Color: 547 | 548 |

549 | 550 | 551 |

552 | CTRL+Z to undo, CTRL+Y to redo 553 |

554 | Shift + taphold to select neighbors 555 |
556 | 557 |
558 | 559 | 560 | 561 | 562 | --------------------------------------------------------------------------------