├── .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 | 
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 |
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 |