96 |
97 |
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/src/undoRedoUtilities.js:
--------------------------------------------------------------------------------
1 | module.exports = function (cy, api) {
2 | if (cy.undoRedo == null)
3 | return;
4 |
5 | var ur = cy.undoRedo({}, true);
6 |
7 | function getEles(_eles) {
8 | return (typeof _eles === "string") ? cy.$(_eles) : _eles;
9 | }
10 |
11 | function getNodePositions() {
12 | var positions = {};
13 | var nodes = cy.nodes();
14 |
15 | for (var i = 0; i < nodes.length; i++) {
16 | var ele = nodes[i];
17 | positions[ele.id()] = {
18 | x: ele.position("x"),
19 | y: ele.position("y")
20 | };
21 | }
22 |
23 | return positions;
24 | }
25 |
26 | function returnToPositions(positions) {
27 | var currentPositions = {};
28 | cy.nodes().not(":parent").positions(function (ele, i) {
29 | if(typeof ele === "number") {
30 | ele = i;
31 | }
32 | currentPositions[ele.id()] = {
33 | x: ele.position("x"),
34 | y: ele.position("y")
35 | };
36 | var pos = positions[ele.id()];
37 | return {
38 | x: pos.x,
39 | y: pos.y
40 | };
41 | });
42 |
43 | return currentPositions;
44 | }
45 |
46 | var secondTimeOpts = {
47 | layoutBy: null,
48 | animate: false,
49 | fisheye: false
50 | };
51 |
52 | function doIt(func) {
53 | return function (args) {
54 | var result = {};
55 | var nodes = getEles(args.nodes);
56 | if (args.firstTime) {
57 | result.oldData = getNodePositions();
58 | result.nodes = func.indexOf("All") > 0 ? api[func](args.options) : api[func](nodes, args.options);
59 | } else {
60 | result.oldData = getNodePositions();
61 | result.nodes = func.indexOf("All") > 0 ? api[func](secondTimeOpts) : api[func](cy.collection(nodes), secondTimeOpts);
62 | returnToPositions(args.oldData);
63 | }
64 |
65 | return result;
66 | };
67 | }
68 |
69 | var actions = ["collapse", "collapseRecursively", "collapseAll", "expand", "expandRecursively", "expandAll"];
70 |
71 | for (var i = 0; i < actions.length; i++) {
72 | if(i == 2)
73 | ur.action("collapseAll", doIt("collapseAll"), doIt("expandRecursively"));
74 | else if(i == 5)
75 | ur.action("expandAll", doIt("expandAll"), doIt("collapseRecursively"));
76 | else
77 | ur.action(actions[i], doIt(actions[i]), doIt(actions[(i + 3) % 6]));
78 | }
79 |
80 | function collapseEdges(args){
81 | var options = args.options;
82 | var edges = args.edges;
83 | var result = {};
84 |
85 | result.options = options;
86 | if(args.firstTime){
87 | var collapseResult = api.collapseEdges(edges,options);
88 | result.edges = collapseResult.edges;
89 | result.oldEdges = collapseResult.oldEdges;
90 | result.firstTime = false;
91 | }else{
92 | result.oldEdges = edges;
93 | result.edges = args.oldEdges;
94 | if(args.edges.length > 0 && args.oldEdges.length > 0){
95 | cy.remove(args.edges);
96 | cy.add(args.oldEdges);
97 | }
98 |
99 |
100 | }
101 |
102 | return result;
103 | }
104 | function collapseEdgesBetweenNodes(args){
105 | var options = args.options;
106 | var result = {};
107 | result.options = options;
108 | if(args.firstTime){
109 | var collapseAllResult = api.collapseEdgesBetweenNodes(args.nodes, options);
110 | result.edges = collapseAllResult.edges;
111 | result.oldEdges = collapseAllResult.oldEdges;
112 | result.firstTime = false;
113 | }else{
114 | result.edges = args.oldEdges;
115 | result.oldEdges = args.edges;
116 | if(args.edges.length > 0 && args.oldEdges.length > 0){
117 | cy.remove(args.edges);
118 | cy.add(args.oldEdges);
119 | }
120 |
121 | }
122 |
123 | return result;
124 |
125 | }
126 | function collapseAllEdges(args){
127 | var options = args.options;
128 | var result = {};
129 | result.options = options;
130 | if(args.firstTime){
131 | var collapseAllResult = api.collapseAllEdges(options);
132 | result.edges = collapseAllResult.edges;
133 | result.oldEdges = collapseAllResult.oldEdges;
134 | result.firstTime = false;
135 | }else{
136 | result.edges = args.oldEdges;
137 | result.oldEdges = args.edges;
138 | if(args.edges.length > 0 && args.oldEdges.length > 0){
139 | cy.remove(args.edges);
140 | cy.add(args.oldEdges);
141 | }
142 |
143 | }
144 |
145 | return result;
146 | }
147 | function expandEdges(args){
148 | var options = args.options;
149 | var result ={};
150 |
151 | result.options = options;
152 | if(args.firstTime){
153 | var expandResult = api.expandEdges(args.edges);
154 | result.edges = expandResult.edges;
155 | result.oldEdges = expandResult.oldEdges;
156 | result.firstTime = false;
157 |
158 | }else{
159 | result.oldEdges = args.edges;
160 | result.edges = args.oldEdges;
161 | if(args.edges.length > 0 && args.oldEdges.length > 0){
162 | cy.remove(args.edges);
163 | cy.add(args.oldEdges);
164 | }
165 |
166 | }
167 |
168 | return result;
169 | }
170 | function expandEdgesBetweenNodes(args){
171 | var options = args.options;
172 | var result = {};
173 | result.options = options;
174 | if(args.firstTime){
175 | var collapseAllResult = api.expandEdgesBetweenNodes(args.nodes,options);
176 | result.edges = collapseAllResult.edges;
177 | result.oldEdges = collapseAllResult.oldEdges;
178 | result.firstTime = false;
179 | }else{
180 | result.edges = args.oldEdges;
181 | result.oldEdges = args.edges;
182 | if(args.edges.length > 0 && args.oldEdges.length > 0){
183 | cy.remove(args.edges);
184 | cy.add(args.oldEdges);
185 | }
186 |
187 | }
188 |
189 | return result;
190 | }
191 | function expandAllEdges(args){
192 | var options = args.options;
193 | var result = {};
194 | result.options = options;
195 | if(args.firstTime){
196 | var expandResult = api.expandAllEdges(options);
197 | result.edges = expandResult.edges;
198 | result.oldEdges = expandResult.oldEdges;
199 | result.firstTime = false;
200 | }else{
201 | result.edges = args.oldEdges;
202 | result.oldEdges = args.edges;
203 | if(args.edges.length > 0 && args.oldEdges.length > 0){
204 | cy.remove(args.edges);
205 | cy.add(args.oldEdges);
206 | }
207 |
208 | }
209 |
210 | return result;
211 | }
212 |
213 |
214 | ur.action("collapseEdges", collapseEdges, expandEdges);
215 | ur.action("expandEdges", expandEdges, collapseEdges);
216 |
217 | ur.action("collapseEdgesBetweenNodes", collapseEdgesBetweenNodes, expandEdgesBetweenNodes);
218 | ur.action("expandEdgesBetweenNodes", expandEdgesBetweenNodes, collapseEdgesBetweenNodes);
219 |
220 | ur.action("collapseAllEdges", collapseAllEdges, expandAllEdges);
221 | ur.action("expandAllEdges", expandAllEdges, collapseAllEdges);
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 | };
230 |
--------------------------------------------------------------------------------
/src/debounce.js:
--------------------------------------------------------------------------------
1 | var debounce = (function () {
2 | /**
3 | * lodash 3.1.1 (Custom Build)
4 | * Build: `lodash modern modularize exports="npm" -o ./`
5 | * Copyright 2012-2015 The Dojo Foundation
6 | * Based on Underscore.js 1.8.3
7 | * Copyright 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
8 | * Available under MIT license
9 | */
10 | /** Used as the `TypeError` message for "Functions" methods. */
11 | var FUNC_ERROR_TEXT = 'Expected a function';
12 |
13 | /* Native method references for those with the same name as other `lodash` methods. */
14 | var nativeMax = Math.max,
15 | nativeNow = Date.now;
16 |
17 | /**
18 | * Gets the number of milliseconds that have elapsed since the Unix epoch
19 | * (1 January 1970 00:00:00 UTC).
20 | *
21 | * @static
22 | * @memberOf _
23 | * @category Date
24 | * @example
25 | *
26 | * _.defer(function(stamp) {
27 | * console.log(_.now() - stamp);
28 | * }, _.now());
29 | * // => logs the number of milliseconds it took for the deferred function to be invoked
30 | */
31 | var now = nativeNow || function () {
32 | return new Date().getTime();
33 | };
34 |
35 | /**
36 | * Creates a debounced function that delays invoking `func` until after `wait`
37 | * milliseconds have elapsed since the last time the debounced function was
38 | * invoked. The debounced function comes with a `cancel` method to cancel
39 | * delayed invocations. Provide an options object to indicate that `func`
40 | * should be invoked on the leading and/or trailing edge of the `wait` timeout.
41 | * Subsequent calls to the debounced function return the result of the last
42 | * `func` invocation.
43 | *
44 | * **Note:** If `leading` and `trailing` options are `true`, `func` is invoked
45 | * on the trailing edge of the timeout only if the the debounced function is
46 | * invoked more than once during the `wait` timeout.
47 | *
48 | * See [David Corbacho's article](http://drupalmotion.com/article/debounce-and-throttle-visual-explanation)
49 | * for details over the differences between `_.debounce` and `_.throttle`.
50 | *
51 | * @static
52 | * @memberOf _
53 | * @category Function
54 | * @param {Function} func The function to debounce.
55 | * @param {number} [wait=0] The number of milliseconds to delay.
56 | * @param {Object} [options] The options object.
57 | * @param {boolean} [options.leading=false] Specify invoking on the leading
58 | * edge of the timeout.
59 | * @param {number} [options.maxWait] The maximum time `func` is allowed to be
60 | * delayed before it's invoked.
61 | * @param {boolean} [options.trailing=true] Specify invoking on the trailing
62 | * edge of the timeout.
63 | * @returns {Function} Returns the new debounced function.
64 | * @example
65 | *
66 | * // avoid costly calculations while the window size is in flux
67 | * jQuery(window).on('resize', _.debounce(calculateLayout, 150));
68 | *
69 | * // invoke `sendMail` when the click event is fired, debouncing subsequent calls
70 | * jQuery('#postbox').on('click', _.debounce(sendMail, 300, {
71 | * 'leading': true,
72 | * 'trailing': false
73 | * }));
74 | *
75 | * // ensure `batchLog` is invoked once after 1 second of debounced calls
76 | * var source = new EventSource('/stream');
77 | * jQuery(source).on('message', _.debounce(batchLog, 250, {
78 | * 'maxWait': 1000
79 | * }));
80 | *
81 | * // cancel a debounced call
82 | * var todoChanges = _.debounce(batchLog, 1000);
83 | * Object.observe(models.todo, todoChanges);
84 | *
85 | * Object.observe(models, function(changes) {
86 | * if (_.find(changes, { 'user': 'todo', 'type': 'delete'})) {
87 | * todoChanges.cancel();
88 | * }
89 | * }, ['delete']);
90 | *
91 | * // ...at some point `models.todo` is changed
92 | * models.todo.completed = true;
93 | *
94 | * // ...before 1 second has passed `models.todo` is deleted
95 | * // which cancels the debounced `todoChanges` call
96 | * delete models.todo;
97 | */
98 | function debounce(func, wait, options) {
99 | var args,
100 | maxTimeoutId,
101 | result,
102 | stamp,
103 | thisArg,
104 | timeoutId,
105 | trailingCall,
106 | lastCalled = 0,
107 | maxWait = false,
108 | trailing = true;
109 |
110 | if (typeof func != 'function') {
111 | throw new TypeError(FUNC_ERROR_TEXT);
112 | }
113 | wait = wait < 0 ? 0 : (+wait || 0);
114 | if (options === true) {
115 | var leading = true;
116 | trailing = false;
117 | } else if (isObject(options)) {
118 | leading = !!options.leading;
119 | maxWait = 'maxWait' in options && nativeMax(+options.maxWait || 0, wait);
120 | trailing = 'trailing' in options ? !!options.trailing : trailing;
121 | }
122 |
123 | function cancel() {
124 | if (timeoutId) {
125 | clearTimeout(timeoutId);
126 | }
127 | if (maxTimeoutId) {
128 | clearTimeout(maxTimeoutId);
129 | }
130 | lastCalled = 0;
131 | maxTimeoutId = timeoutId = trailingCall = undefined;
132 | }
133 |
134 | function complete(isCalled, id) {
135 | if (id) {
136 | clearTimeout(id);
137 | }
138 | maxTimeoutId = timeoutId = trailingCall = undefined;
139 | if (isCalled) {
140 | lastCalled = now();
141 | result = func.apply(thisArg, args);
142 | if (!timeoutId && !maxTimeoutId) {
143 | args = thisArg = undefined;
144 | }
145 | }
146 | }
147 |
148 | function delayed() {
149 | var remaining = wait - (now() - stamp);
150 | if (remaining <= 0 || remaining > wait) {
151 | complete(trailingCall, maxTimeoutId);
152 | } else {
153 | timeoutId = setTimeout(delayed, remaining);
154 | }
155 | }
156 |
157 | function maxDelayed() {
158 | complete(trailing, timeoutId);
159 | }
160 |
161 | function debounced() {
162 | args = arguments;
163 | stamp = now();
164 | thisArg = this;
165 | trailingCall = trailing && (timeoutId || !leading);
166 |
167 | if (maxWait === false) {
168 | var leadingCall = leading && !timeoutId;
169 | } else {
170 | if (!maxTimeoutId && !leading) {
171 | lastCalled = stamp;
172 | }
173 | var remaining = maxWait - (stamp - lastCalled),
174 | isCalled = remaining <= 0 || remaining > maxWait;
175 |
176 | if (isCalled) {
177 | if (maxTimeoutId) {
178 | maxTimeoutId = clearTimeout(maxTimeoutId);
179 | }
180 | lastCalled = stamp;
181 | result = func.apply(thisArg, args);
182 | }
183 | else if (!maxTimeoutId) {
184 | maxTimeoutId = setTimeout(maxDelayed, remaining);
185 | }
186 | }
187 | if (isCalled && timeoutId) {
188 | timeoutId = clearTimeout(timeoutId);
189 | }
190 | else if (!timeoutId && wait !== maxWait) {
191 | timeoutId = setTimeout(delayed, wait);
192 | }
193 | if (leadingCall) {
194 | isCalled = true;
195 | result = func.apply(thisArg, args);
196 | }
197 | if (isCalled && !timeoutId && !maxTimeoutId) {
198 | args = thisArg = undefined;
199 | }
200 | return result;
201 | }
202 |
203 | debounced.cancel = cancel;
204 | return debounced;
205 | }
206 |
207 | /**
208 | * Checks if `value` is the [language type](https://es5.github.io/#x8) of `Object`.
209 | * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
210 | *
211 | * @static
212 | * @memberOf _
213 | * @category Lang
214 | * @param {*} value The value to check.
215 | * @returns {boolean} Returns `true` if `value` is an object, else `false`.
216 | * @example
217 | *
218 | * _.isObject({});
219 | * // => true
220 | *
221 | * _.isObject([1, 2, 3]);
222 | * // => true
223 | *
224 | * _.isObject(1);
225 | * // => false
226 | */
227 | function isObject(value) {
228 | // Avoid a V8 JIT bug in Chrome 19-20.
229 | // See https://code.google.com/p/v8/issues/detail?id=2291 for more details.
230 | var type = typeof value;
231 | return !!value && (type == 'object' || type == 'function');
232 | }
233 |
234 | return debounce;
235 |
236 | })();
237 |
238 | module.exports = debounce;
--------------------------------------------------------------------------------
/src/saveLoadUtilities.js:
--------------------------------------------------------------------------------
1 | function saveLoadUtilities(cy, api) {
2 | /** converts array of JSON to a cytoscape.js collection (bottom-up recursive)
3 | * keeps information about parents, all nodes added to cytoscape, and nodes to be collapsed
4 | * @param {} jsonArr an array of objects (a JSON array)
5 | * @param {} allNodes a cytoscape.js collection
6 | * @param {} nodes2collapse a cytoscape.js collection
7 | * @param {} node2parent a JS object (simply key-value pairs)
8 | */
9 | function json2cyCollection(jsonArr, allNodes, nodes2collapse, node2parent) {
10 | // process edges last since they depend on nodes
11 | jsonArr.sort((a) => {
12 | if (a.group === 'edges') {
13 | return 1;
14 | }
15 | return -1;
16 | });
17 |
18 | // add compound nodes first, then add other nodes then edges
19 | let coll = cy.collection();
20 | for (let i = 0; i < jsonArr.length; i++) {
21 | const json = jsonArr[i];
22 | const d = json.data;
23 | if (d.parent) {
24 | node2parent[d.id] = d.parent;
25 | }
26 | const pos = { x: json.position.x, y: json.position.y };
27 | const e = cy.add(json);
28 | if (e.isNode()) {
29 | allNodes.merge(e);
30 | }
31 |
32 | if (d.originalEnds) {
33 | // all nodes should be in the memory (in cy or not)
34 | let src = allNodes.$id(d.originalEnds.source.data.id);
35 | if (d.originalEnds.source.data.parent) {
36 | node2parent[d.originalEnds.source.data.id] = d.originalEnds.source.data.parent;
37 | }
38 | let tgt = allNodes.$id(d.originalEnds.target.data.id);
39 | if (d.originalEnds.target.data.parent) {
40 | node2parent[d.originalEnds.target.data.id] = d.originalEnds.target.data.parent;
41 | }
42 | e.data('originalEnds', { source: src, target: tgt });
43 | }
44 | if (d.collapsedChildren) {
45 | nodes2collapse.merge(e);
46 | json2cyCollection(d.collapsedChildren, allNodes, nodes2collapse, node2parent);
47 | clearCollapseMetaData(e);
48 | } else if (d.collapsedEdges) {
49 | e.data('collapsedEdges', json2cyCollection(d.collapsedEdges, allNodes, nodes2collapse, node2parent));
50 | // delete collapsed edges from cy
51 | cy.remove(e.data('collapsedEdges'));
52 | }
53 | e.position(pos); // adding new elements to a compound might change its position
54 | coll.merge(e);
55 | }
56 | return coll;
57 | }
58 |
59 | /** clears all the data related to collapsed node
60 | * @param {} e a cytoscape element
61 | */
62 | function clearCollapseMetaData(e) {
63 | e.data('collapsedChildren', null);
64 | e.removeClass('cy-expand-collapse-collapsed-node');
65 | e.data('position-before-collapse', null);
66 | e.data('size-before-collapse', null);
67 | e.data('expandcollapseRenderedStartX', null);
68 | e.data('expandcollapseRenderedStartY', null);
69 | e.data('expandcollapseRenderedCueSize', null);
70 | }
71 |
72 | /** converts cytoscape collection to JSON array.(bottom-up recursive)
73 | * @param {} elems
74 | */
75 | function cyCollection2Json(elems) {
76 | let r = [];
77 | for (let i = 0; i < elems.length; i++) {
78 | const elem = elems[i];
79 | let jsonObj = null;
80 | if (!elem.collapsedChildren && !elem.collapsedEdges) {
81 | jsonObj = elem.cy.json();
82 | }
83 | else if (elem.collapsedChildren) {
84 | elem.collapsedChildren = cyCollection2Json(halfDeepCopyCollection(elem.collapsedChildren));
85 | jsonObj = elem.cy.json();
86 | jsonObj.data.collapsedChildren = elem.collapsedChildren;
87 | } else if (elem.collapsedEdges) {
88 | elem.collapsedEdges = cyCollection2Json(halfDeepCopyCollection(elem.collapsedEdges));
89 | jsonObj = elem.cy.json();
90 | jsonObj.data.collapsedEdges = elem.collapsedEdges;
91 | }
92 | if (elem.originalEnds) {
93 | const src = elem.originalEnds.source.json();
94 | const tgt = elem.originalEnds.target.json();
95 | if (src.data.collapsedChildren) {
96 | src.data.collapsedChildren = cyCollection2Json(halfDeepCopyCollection(src.data.collapsedChildren));
97 | }
98 | if (tgt.data.collapsedChildren) {
99 | tgt.data.collapsedChildren = cyCollection2Json(halfDeepCopyCollection(tgt.data.collapsedChildren));
100 | }
101 | jsonObj.data.originalEnds = { source: src, target: tgt };
102 | }
103 | r.push(jsonObj);
104 | }
105 | return r;
106 | }
107 |
108 | /** returns { cy: any, collapsedEdges: any, collapsedChildren: any, originalEnds: any }[]
109 | * from cytoscape collection
110 | * @param {} col
111 | */
112 | function halfDeepCopyCollection(col) {
113 | let arr = [];
114 | for (let i = 0; i < col.length; i++) {
115 | arr.push({ cy: col[i], collapsedEdges: col[i].data('collapsedEdges'), collapsedChildren: col[i].data('collapsedChildren'), originalEnds: col[i].data('originalEnds') });
116 | }
117 | return arr;
118 | }
119 |
120 | /** saves the string as a file.
121 | * @param {} str string
122 | * @param {} fileName string
123 | */
124 | function str2file(str, fileName) {
125 | const blob = new Blob([str], { type: 'text/plain' });
126 | const anchor = document.createElement('a');
127 |
128 | anchor.download = fileName;
129 | anchor.href = (window.URL).createObjectURL(blob);
130 | anchor.dataset.downloadurl =
131 | ['text/plain', anchor.download, anchor.href].join(':');
132 | anchor.click();
133 | }
134 |
135 | function overrideJson2Elem(elem, json) {
136 | const collapsedChildren = elem.data('collapsedChildren');
137 | const collapsedEdges = elem.data('collapsedEdges');
138 | const originalEnds = elem.data('originalEnds');
139 | elem.json(json);
140 | if (collapsedChildren) {
141 | elem.data('collapsedChildren', collapsedChildren);
142 | }
143 | if (collapsedEdges) {
144 | elem.data('collapsedEdges', collapsedEdges);
145 | }
146 | if (originalEnds) {
147 | elem.data('originalEnds', originalEnds);
148 | }
149 | }
150 |
151 | return {
152 |
153 | /** Load elements from JSON formatted string representation.
154 | * For collapsed compounds, first add all collapsed nodes as normal nodes then collapse them. Then reposition them.
155 | * For collapsed edges, first add all of the edges then remove collapsed edges from cytoscape.
156 | * For original ends, restore their reference to cytoscape elements
157 | * @param {} txt string
158 | */
159 | loadJson: function (txt) {
160 | const fileJSON = JSON.parse(txt);
161 | // original endpoints won't exist in cy. So keep a reference.
162 | const nodePositions = {};
163 | const allNodes = cy.collection(); // some elements are stored in cy, some are deleted
164 | const nodes2collapse = cy.collection(); // some are deleted
165 | const node2parent = {};
166 | for (const n of fileJSON.nodes) {
167 | nodePositions[n.data.id] = { x: n.position.x, y: n.position.y };
168 | if (n.data.parent) {
169 | node2parent[n.data.id] = n.data.parent;
170 | }
171 | const node = cy.add(n);
172 | allNodes.merge(node);
173 | if (node.data('collapsedChildren')) {
174 | json2cyCollection(node.data('collapsedChildren'), allNodes, nodes2collapse, node2parent);
175 | nodes2collapse.merge(node);
176 | clearCollapseMetaData(node);
177 | }
178 | }
179 | for (const e of fileJSON.edges) {
180 | const edge = cy.add(e);
181 | if (edge.data('collapsedEdges')) {
182 | edge.data('collapsedEdges', json2cyCollection(e.data.collapsedEdges, allNodes, nodes2collapse, node2parent));
183 | cy.remove(edge.data('collapsedEdges')); // delete collapsed edges from cy
184 | }
185 | if (edge.data('originalEnds')) {
186 | const srcId = e.data.originalEnds.source.data.id;
187 | const tgtId = e.data.originalEnds.target.data.id;
188 | e.data.originalEnds = { source: allNodes.filter('#' + srcId), target: allNodes.filter('#' + tgtId) };
189 | }
190 | }
191 | // set parents
192 | for (let node in node2parent) {
193 | const elem = allNodes.$id(node);
194 | if (elem.length === 1) {
195 | elem.move({ parent: node2parent[node] });
196 | }
197 | }
198 | // collapse the collapsed nodes
199 | api.collapse(nodes2collapse, { layoutBy: null, fisheye: false, animate: false });
200 |
201 | // positions might be changed in collapse extension
202 | for (const n of fileJSON.nodes) {
203 | const node = cy.$id(n.data.id)
204 | if (node.isChildless()) {
205 | cy.$id(n.data.id).position(nodePositions[n.data.id]);
206 | }
207 | }
208 | cy.fit();
209 | },
210 |
211 |
212 | /** saves cytoscape elements (collection) as JSON
213 | * calls elements' json method (https://js.cytoscape.org/#ele.json) when we keep a cytoscape element in the data.
214 | * @param {} elems cytoscape collection
215 | * @param {} filename string
216 | */
217 | saveJson: function (elems, filename) {
218 | if (!elems) {
219 | elems = cy.$();
220 | }
221 | const nodes = halfDeepCopyCollection(elems.nodes());
222 | const edges = halfDeepCopyCollection(elems.edges());
223 | if (edges.length + nodes.length < 1) {
224 | return;
225 | }
226 |
227 | // according to cytoscape.js format
228 | const o = { nodes: [], edges: [] };
229 | for (const e of edges) {
230 | if (e.collapsedEdges) {
231 | e.collapsedEdges = cyCollection2Json(halfDeepCopyCollection(e.collapsedEdges));
232 | }
233 | if (e.originalEnds) {
234 | const src = e.originalEnds.source.json();
235 | const tgt = e.originalEnds.target.json();
236 | if (src.data.collapsedChildren) {
237 | // e.originalEnds.source.data.collapsedChildren will be changed
238 | src.data.collapsedChildren = cyCollection2Json(halfDeepCopyCollection(src.data.collapsedChildren));
239 | }
240 | if (tgt.data.collapsedChildren) {
241 | tgt.data.collapsedChildren = cyCollection2Json(halfDeepCopyCollection(tgt.data.collapsedChildren));
242 | }
243 | e.originalEnds = { source: src, target: tgt };
244 | }
245 | const jsonObj = e.cy.json();
246 | jsonObj.data.collapsedEdges = e.collapsedEdges;
247 | jsonObj.data.originalEnds = e.originalEnds;
248 | o.edges.push(jsonObj);
249 | }
250 | for (const n of nodes) {
251 | if (n.collapsedChildren) {
252 | n.collapsedChildren = cyCollection2Json(halfDeepCopyCollection(n.collapsedChildren));
253 | }
254 | const jsonObj = n.cy.json();
255 | jsonObj.data.collapsedChildren = n.collapsedChildren;
256 | o.nodes.push(jsonObj);
257 | }
258 |
259 | let stringifiedJSON = JSON.stringify(o);
260 | if (filename) {
261 | str2file(stringifiedJSON, filename);
262 | }
263 | return stringifiedJSON;
264 | }
265 | };
266 | }
267 |
268 | module.exports = saveLoadUtilities;
269 |
--------------------------------------------------------------------------------
/src/cueUtilities.js:
--------------------------------------------------------------------------------
1 | var debounce = require('./debounce');
2 | var debounce2 = require('./debounce2');
3 |
4 | module.exports = function (params, cy, api) {
5 | var elementUtilities;
6 | var fn = params;
7 | const CUE_POS_UPDATE_DELAY = 100;
8 | var nodeWithRenderedCue;
9 |
10 | const getData = function () {
11 | var scratch = cy.scratch('_cyExpandCollapse');
12 | return scratch && scratch.cueUtilities;
13 | };
14 |
15 | const setData = function (data) {
16 | var scratch = cy.scratch('_cyExpandCollapse');
17 | if (scratch == null) {
18 | scratch = {};
19 | }
20 |
21 | scratch.cueUtilities = data;
22 | cy.scratch('_cyExpandCollapse', scratch);
23 | };
24 |
25 | var functions = {
26 | init: function () {
27 | var $canvas = document.createElement('canvas');
28 | $canvas.classList.add("expand-collapse-canvas");
29 | var $container = cy.container();
30 | var ctx = $canvas.getContext('2d');
31 | $container.append($canvas);
32 |
33 | elementUtilities = require('./elementUtilities')(cy);
34 |
35 | var offset = function (elt) {
36 | var rect = elt.getBoundingClientRect();
37 |
38 | return {
39 | top: rect.top + document.documentElement.scrollTop,
40 | left: rect.left + document.documentElement.scrollLeft
41 | }
42 | }
43 |
44 | var _sizeCanvas = debounce(function () {
45 | $canvas.height = cy.container().offsetHeight;
46 | $canvas.width = cy.container().offsetWidth;
47 | $canvas.style.position = 'absolute';
48 | $canvas.style.top = 0;
49 | $canvas.style.left = 0;
50 | $canvas.style.zIndex = options().zIndex;
51 |
52 | setTimeout(function () {
53 | var canvasBb = offset($canvas);
54 | var containerBb = offset($container);
55 | $canvas.style.top = -(canvasBb.top - containerBb.top);
56 | $canvas.style.left = -(canvasBb.left - containerBb.left);
57 |
58 | // refresh the cues on canvas resize
59 | if (cy) {
60 | clearDraws(true);
61 | }
62 | }, 0);
63 |
64 | }, 250);
65 |
66 | function sizeCanvas() {
67 | _sizeCanvas();
68 | }
69 |
70 | sizeCanvas();
71 |
72 | var data = {};
73 |
74 | // if there are events field in data unbind them here
75 | // to prevent binding the same event multiple times
76 | // if (!data.hasEventFields) {
77 | // functions['unbind'].apply( $container );
78 | // }
79 |
80 | function options() {
81 | return cy.scratch('_cyExpandCollapse').options;
82 | }
83 |
84 | function clearDraws() {
85 | var w = cy.width();
86 | var h = cy.height();
87 |
88 | ctx.clearRect(0, 0, w, h);
89 | nodeWithRenderedCue = null;
90 | }
91 |
92 | function drawExpandCollapseCue(node) {
93 | var children = node.children();
94 | var collapsedChildren = node.data('collapsedChildren');
95 | var hasChildren = children != null && children != undefined && children.length > 0;
96 | // If this is a simple node with no collapsed children return directly
97 | if (!hasChildren && !collapsedChildren) {
98 | return;
99 | }
100 |
101 | var isCollapsed = node.hasClass('cy-expand-collapse-collapsed-node');
102 |
103 | //Draw expand-collapse rectangles
104 | var rectSize = options().expandCollapseCueSize;
105 | var lineSize = options().expandCollapseCueLineSize;
106 |
107 | var cueCenter;
108 |
109 | if (options().expandCollapseCuePosition === 'top-left') {
110 | var offset = 1;
111 | var size = cy.zoom() < 1 ? rectSize / (2 * cy.zoom()) : rectSize / 2;
112 | var nodeBorderWid = parseFloat(node.css('border-width'));
113 | var x = node.position('x') - node.width() / 2 - parseFloat(node.css('padding-left'))
114 | + nodeBorderWid + size + offset;
115 | var y = node.position('y') - node.height() / 2 - parseFloat(node.css('padding-top'))
116 | + nodeBorderWid + size + offset;
117 |
118 | cueCenter = { x: x, y: y };
119 | } else {
120 | var option = options().expandCollapseCuePosition;
121 | cueCenter = typeof option === 'function' ? option.call(this, node) : option;
122 | }
123 |
124 | var expandcollapseCenter = elementUtilities.convertToRenderedPosition(cueCenter);
125 |
126 | // convert to rendered sizes
127 | rectSize = Math.max(rectSize, rectSize * cy.zoom());
128 | lineSize = Math.max(lineSize, lineSize * cy.zoom());
129 | var diff = (rectSize - lineSize) / 2;
130 |
131 | var expandcollapseCenterX = expandcollapseCenter.x;
132 | var expandcollapseCenterY = expandcollapseCenter.y;
133 |
134 | var expandcollapseStartX = expandcollapseCenterX - rectSize / 2;
135 | var expandcollapseStartY = expandcollapseCenterY - rectSize / 2;
136 | var expandcollapseRectSize = rectSize;
137 |
138 | // Draw expand/collapse cue if specified use an image else render it in the default way
139 | if (isCollapsed && options().expandCueImage) {
140 | drawImg(options().expandCueImage, expandcollapseStartX, expandcollapseStartY, rectSize, rectSize);
141 | }
142 | else if (!isCollapsed && options().collapseCueImage) {
143 | drawImg(options().collapseCueImage, expandcollapseStartX, expandcollapseStartY, rectSize, rectSize);
144 | }
145 | else {
146 | var oldFillStyle = ctx.fillStyle;
147 | var oldWidth = ctx.lineWidth;
148 | var oldStrokeStyle = ctx.strokeStyle;
149 |
150 | ctx.fillStyle = "black";
151 | ctx.strokeStyle = "black";
152 |
153 | ctx.ellipse(expandcollapseCenterX, expandcollapseCenterY, rectSize / 2, rectSize / 2, 0, 0, 2 * Math.PI);
154 | ctx.fill();
155 |
156 | ctx.beginPath();
157 |
158 | ctx.strokeStyle = "white";
159 | ctx.lineWidth = Math.max(2.6, 2.6 * cy.zoom());
160 |
161 | ctx.moveTo(expandcollapseStartX + diff, expandcollapseStartY + rectSize / 2);
162 | ctx.lineTo(expandcollapseStartX + lineSize + diff, expandcollapseStartY + rectSize / 2);
163 |
164 | if (isCollapsed) {
165 | ctx.moveTo(expandcollapseStartX + rectSize / 2, expandcollapseStartY + diff);
166 | ctx.lineTo(expandcollapseStartX + rectSize / 2, expandcollapseStartY + lineSize + diff);
167 | }
168 |
169 | ctx.closePath();
170 | ctx.stroke();
171 |
172 | ctx.strokeStyle = oldStrokeStyle;
173 | ctx.fillStyle = oldFillStyle;
174 | ctx.lineWidth = oldWidth;
175 | }
176 |
177 | node._private.data.expandcollapseRenderedStartX = expandcollapseStartX;
178 | node._private.data.expandcollapseRenderedStartY = expandcollapseStartY;
179 | node._private.data.expandcollapseRenderedCueSize = expandcollapseRectSize;
180 |
181 | nodeWithRenderedCue = node;
182 | }
183 |
184 | function drawImg(imgSrc, x, y, w, h) {
185 | var img = new Image(w, h);
186 | img.src = imgSrc;
187 | img.onload = () => {
188 | ctx.drawImage(img, x, y, w, h);
189 | };
190 | }
191 |
192 | cy.on('resize', data.eCyResize = function () {
193 | sizeCanvas();
194 | });
195 |
196 | cy.on('expandcollapse.clearvisualcue', function () {
197 | if (nodeWithRenderedCue) {
198 | clearDraws();
199 | }
200 | });
201 |
202 | var oldMousePos = null, currMousePos = null;
203 | cy.on('mousedown', data.eMouseDown = function (e) {
204 | oldMousePos = e.renderedPosition || e.cyRenderedPosition
205 | });
206 |
207 | cy.on('mouseup', data.eMouseUp = function (e) {
208 | currMousePos = e.renderedPosition || e.cyRenderedPosition
209 | });
210 |
211 | cy.on('remove', 'node', data.eRemove = function (evt) {
212 | const node = evt.target;
213 | if (node == nodeWithRenderedCue) {
214 | clearDraws();
215 | }
216 | });
217 |
218 | var ur;
219 | cy.on('select unselect', data.eSelect = function () {
220 | if (nodeWithRenderedCue) {
221 | clearDraws();
222 | }
223 | var selectedNodes = cy.nodes(':selected');
224 | if (selectedNodes.length !== 1) {
225 | return;
226 | }
227 | var selectedNode = selectedNodes[0];
228 |
229 | if (selectedNode.isParent() || selectedNode.hasClass('cy-expand-collapse-collapsed-node')) {
230 | drawExpandCollapseCue(selectedNode);
231 | }
232 | });
233 |
234 | cy.on('tap', data.eTap = function (event) {
235 | var node = nodeWithRenderedCue;
236 | if (!node) {
237 | return;
238 | }
239 | var expandcollapseRenderedStartX = node.data('expandcollapseRenderedStartX');
240 | var expandcollapseRenderedStartY = node.data('expandcollapseRenderedStartY');
241 | var expandcollapseRenderedRectSize = node.data('expandcollapseRenderedCueSize');
242 | var expandcollapseRenderedEndX = expandcollapseRenderedStartX + expandcollapseRenderedRectSize;
243 | var expandcollapseRenderedEndY = expandcollapseRenderedStartY + expandcollapseRenderedRectSize;
244 |
245 | var cyRenderedPos = event.renderedPosition || event.cyRenderedPosition;
246 | var cyRenderedPosX = cyRenderedPos.x;
247 | var cyRenderedPosY = cyRenderedPos.y;
248 | var opts = options();
249 | var factor = (opts.expandCollapseCueSensitivity - 1) / 2;
250 |
251 | if ((Math.abs(oldMousePos.x - currMousePos.x) < 5 && Math.abs(oldMousePos.y - currMousePos.y) < 5)
252 | && cyRenderedPosX >= expandcollapseRenderedStartX - expandcollapseRenderedRectSize * factor
253 | && cyRenderedPosX <= expandcollapseRenderedEndX + expandcollapseRenderedRectSize * factor
254 | && cyRenderedPosY >= expandcollapseRenderedStartY - expandcollapseRenderedRectSize * factor
255 | && cyRenderedPosY <= expandcollapseRenderedEndY + expandcollapseRenderedRectSize * factor) {
256 | if (opts.undoable && !ur) {
257 | ur = cy.undoRedo({ defaultActions: false });
258 | }
259 |
260 | if (api.isCollapsible(node)) {
261 | clearDraws();
262 | if (opts.undoable) {
263 | ur.do("collapse", {
264 | nodes: node,
265 | options: opts
266 | });
267 | }
268 | else {
269 | api.collapse(node, opts);
270 | }
271 | }
272 | else if (api.isExpandable(node)) {
273 | clearDraws();
274 | if (opts.undoable) {
275 | ur.do("expand", { nodes: node, options: opts });
276 | }
277 | else {
278 | api.expand(node, opts);
279 | }
280 | }
281 | if (node.selectable()) {
282 | node.unselectify();
283 | cy.scratch('_cyExpandCollapse').selectableChanged = true;
284 | }
285 | }
286 | });
287 |
288 | cy.on('afterUndo afterRedo', data.eUndoRedo = data.eSelect);
289 |
290 | cy.on('position', 'node', data.ePosition = debounce2(data.eSelect, CUE_POS_UPDATE_DELAY, clearDraws));
291 |
292 | cy.on('pan zoom', data.ePosition);
293 |
294 | // write options to data
295 | data.hasEventFields = true;
296 | setData(data);
297 | },
298 | unbind: function () {
299 | // var $container = this;
300 | var data = getData();
301 |
302 | if (!data.hasEventFields) {
303 | console.log('events to unbind does not exist');
304 | return;
305 | }
306 |
307 | cy.trigger('expandcollapse.clearvisualcue');
308 |
309 | cy.off('mousedown', 'node', data.eMouseDown)
310 | .off('mouseup', 'node', data.eMouseUp)
311 | .off('remove', 'node', data.eRemove)
312 | .off('tap', 'node', data.eTap)
313 | .off('add', 'node', data.eAdd)
314 | .off('position', 'node', data.ePosition)
315 | .off('pan zoom', data.ePosition)
316 | .off('select unselect', data.eSelect)
317 | .off('free', 'node', data.eFree)
318 | .off('resize', data.eCyResize)
319 | .off('afterUndo afterRedo', data.eUndoRedo);
320 | },
321 | rebind: function () {
322 | var data = getData();
323 |
324 | if (!data.hasEventFields) {
325 | console.log('events to rebind does not exist');
326 | return;
327 | }
328 |
329 | cy.on('mousedown', 'node', data.eMouseDown)
330 | .on('mouseup', 'node', data.eMouseUp)
331 | .on('remove', 'node', data.eRemove)
332 | .on('tap', 'node', data.eTap)
333 | .on('add', 'node', data.eAdd)
334 | .on('position', 'node', data.ePosition)
335 | .on('pan zoom', data.ePosition)
336 | .on('select unselect', data.eSelect)
337 | .on('free', 'node', data.eFree)
338 | .on('resize', data.eCyResize)
339 | .on('afterUndo afterRedo', data.eUndoRedo);
340 | }
341 | };
342 |
343 | if (functions[fn]) {
344 | return functions[fn].apply(cy.container(), Array.prototype.slice.call(arguments, 1));
345 | } else if (typeof fn == 'object' || !fn) {
346 | return functions.init.apply(cy.container(), arguments);
347 | }
348 | throw new Error('No such function `' + fn + '` for cytoscape.js-expand-collapse');
349 |
350 | };
351 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | cytoscape-expand-collapse
2 | ================================================================================
3 |
4 | **We are in the process of developing a new unified framework for complexity management of graphs. Thus this repositoy is no longer being maintained**
5 |
6 | ## Description
7 |
8 | This extension provides an interface to expand/collapse nodes and edges for better management of complexity of Cytoscape.js compound graphs, distributed under [The MIT License](https://opensource.org/licenses/MIT).
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Please cite the following paper when using this extension:
17 |
18 | U. Dogrusoz , A. Karacelik, I. Safarli, H. Balci, L. Dervishi, and M.C. Siper, "[Efficient methods and readily customizable libraries for managing complexity of large networks](https://doi.org/10.1371/journal.pone.0197238)", PLoS ONE, 13(5): e0197238, 2018.
19 |
20 | ## Demo
21 |
22 | Here are some demos: **no undo and with custom cue image**, **undoable**, and **collapsing edges and nodes**, respectively:
23 |
24 |
25 |
26 |
27 |
28 |
29 | ## API
30 |
31 | * Note that compounds are nodes.
32 |
33 | `cy.expandCollapse(options)`
34 | To initialize the extension with given options.
35 |
36 | `var api = cy.expandCollapse('get')`
37 | To get the extension instance after initialization.
38 |
39 | * Following functions get options parameter to apply during a particular event unlike the function above.
40 |
41 | `api.collapse(nodes, options)`
42 | Collapse given nodes, extend options with given param.
43 |
44 | `api.collapseRecursively(nodes, options)`
45 | Collapse given nodes recursively, extend options with given param.
46 |
47 | `api.collapseAll(options)`
48 | Collapse all nodes on graph (recursively), extend options with given param.
49 |
50 | `api.expand(nodes, options)`
51 | Expand given nodes, extend options with given param.
52 |
53 | `api.expandRecursively(nodes, options)`
54 | Expand given nodes recursively, extend options with given param.
55 |
56 | `api.expandAll(options)`
57 | Expand all nodes on graph (recursively), extend options with given param.
58 |
59 | `api.isExpandable(node)`
60 | Get whether node is expandable (or is collapsed)
61 |
62 | `api.isCollapsible(node)`
63 | Get whether node is collapsible.
64 |
65 | `api.expandableNodes(nodes)`
66 | Get expandable ones inside given nodes if nodes parameter is not specified consider all nodes
67 |
68 | `api.collapsibleNodes(nodes)`
69 | Get collapsible ones inside given nodes if nodes parameter is not specified consider all nodes
70 |
71 | `api.setOptions(options)`
72 | Resets the options to the given parameter.
73 |
74 | `api.setOption(name, value)`
75 | Sets the value of the option given by the name to the given value.
76 |
77 | `api.extendOptions(options)`
78 | Extend the current options with the given options.
79 |
80 | `api.getCollapsedChildren(node)`
81 | Get the children of the given collapsed node which are removed during collapse operation
82 |
83 | `api.getCollapsedChildrenRecursively(node)`
84 | Get collapsed children recursively including nested collapsed children. Returned value includes edges and nodes, use selector to get edges or nodes.
85 |
86 | `api.getAllCollapsedChildrenRecursively()`
87 | Get collapsed children of all collapsed nodes recursively. Returned value includes edges and nodes, use selector to get edges or nodes.
88 |
89 | `api.clearVisualCue()`
90 | Forces the visual cue to be cleared. It is to be called in extreme cases.
91 |
92 | `api.enableCue()`
93 | Enable rendering of visual cue.
94 |
95 | `api.disableCue()`
96 | Disable rendering of visual cue.
97 |
98 | `api.getParent(nodeId)`
99 | Get the parent of a node given its node id. Useful to reach parent of a node removed because of collapse operation.
100 |
101 | `api.collapseEdges(edges,options)`
102 | Collapse the given edges if all the given edges are between same two nodes and number of edges passed is at least 2. Does nothing otherwise.
103 |
104 | ` api.expandEdges(edges){ `
105 | Expand the given collapsed edges
106 |
107 | `api.collapseEdgesBetweenNodes(nodes, options)`
108 | Collapse all edges between the set of given nodes.
109 |
110 | `api.expandEdgesBetweenNodes(nodes)`
111 | Expand all collapsed edges between the set of given nodes
112 |
113 | `api.collapseAllEdges(options)`
114 | Collapse all edges in the graph.
115 |
116 | `api.expandAllEdges()`
117 | Expand all edges in the graph.
118 |
119 | `api.loadJson(jsonStr)`
120 | Load elements from JSON string.
121 |
122 | `api.saveJson(elems, filename)`
123 | Return elements in JSON format and saves them to a file if a file name is provided via `filename` parameter. Default value for `elems` is all the elements.
124 |
125 | ## Events
126 | Notice that following events are performed for *each* node that is collapsed/expanded. Also, notice that any post-processing layout is performed *after* the event.
127 |
128 | `cy.nodes().on("expandcollapse.beforecollapse", function(event) { var node = this; ... })` Triggered before a node is collapsed
129 |
130 | `cy.nodes().on("expandcollapse.aftercollapse", function(event) { var node = this; ... })` Triggered after a node is collapsed
131 |
132 | `cy.nodes().on("expandcollapse.beforeexpand", function(event) { var node = this; ... })` Triggered before a node is expanded
133 |
134 | `cy.nodes().on("expandcollapse.afterexpand", function(event) { var node = this; ... })` Triggered after a node is expanded
135 |
136 | `cy.edges().on("expandcollapse.beforecollapseedge", function(event) { var edge = this; ... })` Triggered before an edge is collapsed
137 |
138 | `cy.edges().on("expandcollapse.aftercollapseedge", function(event) { var edge = this; ... })` Triggered after an edge is collapsed
139 |
140 | `cy.edges().on("expandcollapse.beforeexpandedge", function(event) { var edge = this; ... })` Triggered before an edge is expanded
141 |
142 | `cy.edges().on("expandcollapse.afterexpandedge", function(event) { var edge = this; ... })` Triggered after an edge is expanded
143 |
144 | All these events can also be listened as [cytoscape.js core events](https://js.cytoscape.org/#cy.on)
145 | e.g.
146 |
147 | `cy.on("expandcollapse.afterexpandedge", function(event) { var elem = event.target; ... })`
148 |
149 | ## Default Options
150 | ```javascript
151 | var options = {
152 | layoutBy: null, // to rearrange after expand/collapse. It's just layout options or whole layout function. Choose your side!
153 | // recommended usage: use cose-bilkent layout with randomize: false to preserve mental map upon expand/collapse
154 | fisheye: true, // whether to perform fisheye view after expand/collapse you can specify a function too
155 | animate: true, // whether to animate on drawing changes you can specify a function too
156 | animationDuration: 1000, // when animate is true, the duration in milliseconds of the animation
157 | ready: function () { }, // callback when expand/collapse initialized
158 | undoable: true, // and if undoRedoExtension exists,
159 |
160 | cueEnabled: true, // Whether cues are enabled
161 | expandCollapseCuePosition: 'top-left', // default cue position is top left you can specify a function per node too
162 | expandCollapseCueSize: 12, // size of expand-collapse cue
163 | expandCollapseCueLineSize: 8, // size of lines used for drawing plus-minus icons
164 | expandCueImage: undefined, // image of expand icon if undefined draw regular expand cue
165 | collapseCueImage: undefined, // image of collapse icon if undefined draw regular collapse cue
166 | expandCollapseCueSensitivity: 1, // sensitivity of expand-collapse cues
167 | edgeTypeInfo: "edgeType", // the name of the field that has the edge type, retrieved from edge.data(), can be a function, if reading the field returns undefined the collapsed edge type will be "unknown"
168 | groupEdgesOfSameTypeOnCollapse : false, // if true, the edges to be collapsed will be grouped according to their types, and the created collapsed edges will have same type as their group. if false the collapased edge will have "unknown" type.
169 | allowNestedEdgeCollapse: true, // when you want to collapse a compound edge (edge which contains other edges) and normal edge, should it collapse without expanding the compound first
170 | zIndex: 999 // z-index value of the canvas in which cue ımages are drawn
171 | };
172 | ```
173 |
174 | ## Default Undo/Redo Actions
175 | `ur.do("collapse", { nodes: eles, options: opts)` Equivalent of eles.collapse(opts)
176 |
177 | `ur.do("expand", { nodes: eles, options: opts)` Equivalent of eles.expand(opts)
178 |
179 | `ur.do("collapseRecursively", { nodes: eles, options: opts)` Equivalent of eles.collapseRecursively(opts)
180 |
181 | `ur.do("expandRecursively", { nodes: eles, options: opts)` Equivalent of eles.expandRecursively(opts)
182 |
183 | `ur.do("collapseAll", { options: opts)` Equivalent of cy.collapseAll(opts)
184 |
185 | `ur.do("expandAll", { options: opts })` Equivalent of cy.expandAll(opts)
186 |
187 | `ur.do("collapseEdges", { edges: eles, options: opts})` Equivalent of eles.collapseEdges(opts)
188 |
189 | `ur.do("expandEdges", { edges: eles})` Equivalent of eles.expandEdges()
190 |
191 | `ur.do("collapseEdgesBetweenNodes", { nodes: eles, options: opts})` Equivalent of eles.collapseEdgesBetweenNodes(opts)
192 |
193 | `ur.do("expandEdgesBetweenNodes", { nodes: eles})` Equivalent of eles.expandEdgesBetweenNodes()
194 |
195 | `ur.do("collapseAllEdges", {options: opts)}` Equivalent of cy.collapseAllEdges(opts)
196 |
197 | `ur.do("expandAllEdges")`Equivalent of cy.expandAllEdges()
198 |
199 |
200 | ## Elements Style
201 |
202 | * Collapsed nodes have 'cy-expand-collapse-collapsed-node' class.
203 | * Meta edges (edges from/to collapsed nodes) have 'cy-expand-collapse-meta-edge' class.
204 | * Collapsed edges have 'cy-expand-collapse-collapsed-edge' class.
205 | * Collapsed edges data have 'directionType' field which can be either:
206 | - 'unidirection' if all the edges that are collapsed into this edge have the same direction (all have same source and same target)
207 | or
208 | - 'bidirection' if the edges that are collapsed into this edge have different direction (different target and/or source)
209 | * Collapsed edges data have a field that holds the type, the field name is as defined in options but if it is not defined in options or was defined as a function it will be named 'edgeType'
210 |
211 |
212 |
213 | ## Dependencies
214 |
215 | * Cytoscape.js ^3.3.0
216 | * cytoscape-undo-redo.js(optional) ^1.0.1
217 | * cytoscape-cose-bilkent.js(optional/suggested for layout after expand/collapse) ^4.0.0
218 |
219 |
220 | ## Usage instructions
221 |
222 | Download the library:
223 | * via npm: `npm install cytoscape-expand-collapse`,
224 | * via bower: `bower install cytoscape-expand-collapse`, or
225 | * via direct download in the repository (probably from a tag).
226 |
227 | `require()` the library as appropriate for your project:
228 |
229 | CommonJS:
230 | ```js
231 | var cytoscape = require('cytoscape');
232 | var expandCollapse = require('cytoscape-expand-collapse');
233 |
234 | expandCollapse( cytoscape ); // register extension
235 | ```
236 |
237 | AMD:
238 | ```js
239 | require(['cytoscape', 'cytoscape-expand-collapse'], function( cytoscape, expandCollapse ){
240 | expandCollapse( cytoscape ); // register extension
241 | });
242 | ```
243 |
244 | Plain HTML/JS has the extension registered for you automatically, because no `require()` is needed.
245 |
246 |
247 | ## Build targets
248 |
249 | * `npm run build` : Build `./src/**` into `cytoscape-expand-collapse.js` in production environment and minimize the file.
250 | * `npm run build:dev` : Build `./src/**` into `cytoscape-expand-collapse.js` in development environment without minimizing the file.
251 |
252 | ## Publishing instructions
253 |
254 | This project is set up to automatically be published to npm and bower. To publish:
255 |
256 | 1. Build the extension : `npm run build`
257 | 1. Commit the build : `git commit -am "Build for release"`
258 | 1. Bump the version number and tag: `npm version major|minor|patch`
259 | 1. Push to origin: `git push && git push --tags`
260 | 1. Publish to npm: `npm publish .`
261 | 1. If publishing to bower for the first time, you'll need to run `bower register cytoscape-expand-collapse https://github.com/iVis-at-Bilkent/cytoscape.js-expand-collapse.git`
262 |
263 |
264 | ## Team
265 |
266 | * [Hasan Balci](https://github.com/hasanbalci), [Yusuf Canbaz](https://github.com/canbax), [Ugur Dogrusoz](https://github.com/ugurdogrusoz) of [i-Vis at Bilkent University](http://www.cs.bilkent.edu.tr/~ivis) and [Metin Can Siper](https://github.com/metincansiper) of the Demir Lab at [OHSU](http://www.ohsu.edu/)
267 |
268 | ## Alumni
269 |
270 | * [Alper Karacelik](https://github.com/alperkaracelik), [Ilkin Safarli](https://github.com/kinimesi), [Nasim Saleh](https://github.com/nasimsaleh), [Selim Firat Yilmaz](https://github.com/mrsfy)
271 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | 'use strict';
3 |
4 | // registers the extension on a cytoscape lib ref
5 | var register = function (cytoscape) {
6 |
7 | if (!cytoscape) {
8 | return;
9 | } // can't register if cytoscape unspecified
10 |
11 | var undoRedoUtilities = require('./undoRedoUtilities');
12 | var cueUtilities = require("./cueUtilities");
13 | var saveLoadUtils = null;
14 |
15 | function extendOptions(options, extendBy) {
16 | var tempOpts = {};
17 | for (var key in options)
18 | tempOpts[key] = options[key];
19 |
20 | for (var key in extendBy)
21 | if (tempOpts.hasOwnProperty(key))
22 | tempOpts[key] = extendBy[key];
23 | return tempOpts;
24 | }
25 |
26 | // evaluate some specific options in case of they are specified as functions to be dynamically changed
27 | function evalOptions(options) {
28 | var animate = typeof options.animate === 'function' ? options.animate.call() : options.animate;
29 | var fisheye = typeof options.fisheye === 'function' ? options.fisheye.call() : options.fisheye;
30 |
31 | options.animate = animate;
32 | options.fisheye = fisheye;
33 | }
34 |
35 | // creates and returns the API instance for the extension
36 | function createExtensionAPI(cy, expandCollapseUtilities) {
37 | var api = {}; // API to be returned
38 | // set functions
39 |
40 | function handleNewOptions(opts) {
41 | var currentOpts = getScratch(cy, 'options');
42 | if (opts.cueEnabled && !currentOpts.cueEnabled) {
43 | api.enableCue();
44 | }
45 | else if (!opts.cueEnabled && currentOpts.cueEnabled) {
46 | api.disableCue();
47 | }
48 | }
49 |
50 | function isOnly1Pair(edges) {
51 | let relatedEdgesArr = [];
52 | for (let i = 0; i < edges.length; i++) {
53 | const srcId = edges[i].source().id();
54 | const targetId = edges[i].target().id();
55 | const obj = {};
56 | obj[srcId] = true;
57 | obj[targetId] = true;
58 | relatedEdgesArr.push(obj);
59 | }
60 | for (let i = 0; i < relatedEdgesArr.length; i++) {
61 | for (let j = i + 1; j < relatedEdgesArr.length; j++) {
62 | const keys1 = Object.keys(relatedEdgesArr[i]);
63 | const keys2 = Object.keys(relatedEdgesArr[j]);
64 | const allKeys = new Set(keys1.concat(keys2));
65 | if (allKeys.size != keys1.length || allKeys.size != keys2.length) {
66 | return false;
67 | }
68 | }
69 | }
70 | return true;
71 | }
72 |
73 | // set all options at once
74 | api.setOptions = function (opts) {
75 | handleNewOptions(opts);
76 | setScratch(cy, 'options', opts);
77 | };
78 |
79 | api.extendOptions = function (opts) {
80 | var options = getScratch(cy, 'options');
81 | var newOptions = extendOptions(options, opts);
82 | handleNewOptions(newOptions);
83 | setScratch(cy, 'options', newOptions);
84 | }
85 |
86 | // set the option whose name is given
87 | api.setOption = function (name, value) {
88 | var opts = {};
89 | opts[name] = value;
90 |
91 | var options = getScratch(cy, 'options');
92 | var newOptions = extendOptions(options, opts);
93 |
94 | handleNewOptions(newOptions);
95 | setScratch(cy, 'options', newOptions);
96 | };
97 |
98 | // Collection functions
99 |
100 | // collapse given eles extend options with given param
101 | api.collapse = function (_eles, opts) {
102 | var eles = this.collapsibleNodes(_eles);
103 | var options = getScratch(cy, 'options');
104 | var tempOptions = extendOptions(options, opts);
105 | evalOptions(tempOptions);
106 |
107 | return expandCollapseUtilities.collapseGivenNodes(eles, tempOptions);
108 | };
109 |
110 | // collapse given eles recursively extend options with given param
111 | api.collapseRecursively = function (_eles, opts) {
112 | var eles = this.collapsibleNodes(_eles);
113 | var options = getScratch(cy, 'options');
114 | var tempOptions = extendOptions(options, opts);
115 | evalOptions(tempOptions);
116 |
117 | return this.collapse(eles.union(eles.descendants()), tempOptions);
118 | };
119 |
120 | // expand given eles extend options with given param
121 | api.expand = function (_eles, opts) {
122 | var eles = this.expandableNodes(_eles);
123 | var options = getScratch(cy, 'options');
124 | var tempOptions = extendOptions(options, opts);
125 | evalOptions(tempOptions);
126 |
127 | return expandCollapseUtilities.expandGivenNodes(eles, tempOptions);
128 | };
129 |
130 | // expand given eles recusively extend options with given param
131 | api.expandRecursively = function (_eles, opts) {
132 | var eles = this.expandableNodes(_eles);
133 | var options = getScratch(cy, 'options');
134 | var tempOptions = extendOptions(options, opts);
135 | evalOptions(tempOptions);
136 |
137 | return expandCollapseUtilities.expandAllNodes(eles, tempOptions);
138 | };
139 |
140 |
141 | // Core functions
142 |
143 | // collapse all collapsible nodes
144 | api.collapseAll = function (opts) {
145 | var options = getScratch(cy, 'options');
146 | var tempOptions = extendOptions(options, opts);
147 | evalOptions(tempOptions);
148 |
149 | return this.collapseRecursively(this.collapsibleNodes(), tempOptions);
150 | };
151 |
152 | // expand all expandable nodes
153 | api.expandAll = function (opts) {
154 | var options = getScratch(cy, 'options');
155 | var tempOptions = extendOptions(options, opts);
156 | evalOptions(tempOptions);
157 |
158 | return this.expandRecursively(this.expandableNodes(), tempOptions);
159 | };
160 |
161 |
162 | // Utility functions
163 |
164 | // returns if the given node is expandable
165 | api.isExpandable = function (node) {
166 | return node.hasClass('cy-expand-collapse-collapsed-node');
167 | };
168 |
169 | // returns if the given node is collapsible
170 | api.isCollapsible = function (node) {
171 | return !this.isExpandable(node) && node.isParent();
172 | };
173 |
174 | // get collapsible ones inside given nodes if nodes parameter is not specified consider all nodes
175 | api.collapsibleNodes = function (_nodes) {
176 | var self = this;
177 | var nodes = _nodes ? _nodes : cy.nodes();
178 | return nodes.filter(function (ele, i) {
179 | if (typeof ele === "number") {
180 | ele = i;
181 | }
182 | return self.isCollapsible(ele);
183 | });
184 | };
185 |
186 | // get expandable ones inside given nodes if nodes parameter is not specified consider all nodes
187 | api.expandableNodes = function (_nodes) {
188 | var self = this;
189 | var nodes = _nodes ? _nodes : cy.nodes();
190 | return nodes.filter(function (ele, i) {
191 | if (typeof ele === "number") {
192 | ele = i;
193 | }
194 | return self.isExpandable(ele);
195 | });
196 | };
197 |
198 | // Get the children of the given collapsed node which are removed during collapse operation
199 | api.getCollapsedChildren = function (node) {
200 | return node.data('collapsedChildren');
201 | };
202 |
203 | /** Get collapsed children recursively including nested collapsed children
204 | * Returned value includes edges and nodes, use selector to get edges or nodes
205 | * @param node : a collapsed node
206 | * @return all collapsed children
207 | */
208 | api.getCollapsedChildrenRecursively = function (node) {
209 | var collapsedChildren = cy.collection();
210 | return expandCollapseUtilities.getCollapsedChildrenRecursively(node, collapsedChildren);
211 | };
212 |
213 | /** Get collapsed children of all collapsed nodes recursively including nested collapsed children
214 | * Returned value includes edges and nodes, use selector to get edges or nodes
215 | * @return all collapsed children
216 | */
217 | api.getAllCollapsedChildrenRecursively = function () {
218 | var collapsedChildren = cy.collection();
219 | var collapsedNodes = cy.nodes(".cy-expand-collapse-collapsed-node");
220 | var j;
221 | for (j = 0; j < collapsedNodes.length; j++) {
222 | collapsedChildren = collapsedChildren.union(this.getCollapsedChildrenRecursively(collapsedNodes[j]));
223 | }
224 | return collapsedChildren;
225 | };
226 | // This method forces the visual cue to be cleared. It is to be called in extreme cases
227 | api.clearVisualCue = function (node) {
228 | cy.trigger('expandcollapse.clearvisualcue');
229 | };
230 |
231 | api.disableCue = function () {
232 | var options = getScratch(cy, 'options');
233 | if (options.cueEnabled) {
234 | cueUtilities('unbind', cy, api);
235 | options.cueEnabled = false;
236 | }
237 | };
238 |
239 | api.enableCue = function () {
240 | var options = getScratch(cy, 'options');
241 | if (!options.cueEnabled) {
242 | cueUtilities('rebind', cy, api);
243 | options.cueEnabled = true;
244 | }
245 | };
246 |
247 | api.getParent = function (nodeId) {
248 | if (cy.getElementById(nodeId)[0] === undefined) {
249 | var parentData = getScratch(cy, 'parentData');
250 | return parentData[nodeId];
251 | }
252 | else {
253 | return cy.getElementById(nodeId).parent();
254 | }
255 | };
256 |
257 | api.collapseEdges = function (edges, opts) {
258 | var result = { edges: cy.collection(), oldEdges: cy.collection() };
259 | if (edges.length < 2) return result;
260 | if (!isOnly1Pair(edges)) return result;
261 | var options = getScratch(cy, 'options');
262 | var tempOptions = extendOptions(options, opts);
263 | return expandCollapseUtilities.collapseGivenEdges(edges, tempOptions);
264 | };
265 |
266 | api.expandEdges = function (edges) {
267 | var result = { edges: cy.collection(), oldEdges: cy.collection() }
268 | if (edges === undefined) return result;
269 |
270 | //if(typeof edges[Symbol.iterator] === 'function'){//collection of edges is passed
271 | edges.forEach(function (edge) {
272 | var operationResult = expandCollapseUtilities.expandEdge(edge);
273 | result.edges = result.edges.add(operationResult.edges);
274 | result.oldEdges = result.oldEdges.add(operationResult.oldEdges);
275 |
276 | });
277 | /* }else{//one edge passed
278 | var operationResult = expandCollapseUtilities.expandEdge(edges);
279 | result.edges = result.edges.add(operationResult.edges);
280 | result.oldEdges = result.oldEdges.add(operationResult.oldEdges);
281 |
282 | } */
283 | return result;
284 | };
285 |
286 | api.collapseEdgesBetweenNodes = function (nodes, opts) {
287 | var options = getScratch(cy, 'options');
288 | var tempOptions = extendOptions(options, opts);
289 | function pairwise(list) {
290 | var pairs = [];
291 | list
292 | .slice(0, list.length - 1)
293 | .forEach(function (first, n) {
294 | var tail = list.slice(n + 1, list.length);
295 | tail.forEach(function (item) {
296 | pairs.push([first, item])
297 | });
298 | })
299 | return pairs;
300 | }
301 | var nodesPairs = pairwise(nodes);
302 | // for self-loops
303 | nodesPairs.push(...nodes.map(x => [x, x]));
304 | var result = { edges: cy.collection(), oldEdges: cy.collection() };
305 | nodesPairs.forEach(function (nodePair) {
306 | const id1 = nodePair[1].id();
307 | var edges = nodePair[0].connectedEdges('[source = "' + id1 + '"],[target = "' + id1 + '"]');
308 | // edges for self-loops
309 | if (nodePair[0].id() === id1) {
310 | edges = nodePair[0].connectedEdges('[source = "' + id1 + '"][target = "' + id1 + '"]');
311 | }
312 | if (edges.length >= 2) {
313 | var operationResult = expandCollapseUtilities.collapseGivenEdges(edges, tempOptions)
314 | result.oldEdges = result.oldEdges.add(operationResult.oldEdges);
315 | result.edges = result.edges.add(operationResult.edges);
316 | }
317 |
318 | }.bind(this));
319 |
320 | return result;
321 |
322 | };
323 |
324 | api.expandEdgesBetweenNodes = function (nodes) {
325 | var edgesToExpand = cy.collection();
326 | function pairwise(list) {
327 | var pairs = [];
328 | list
329 | .slice(0, list.length - 1)
330 | .forEach(function (first, n) {
331 | var tail = list.slice(n + 1, list.length);
332 | tail.forEach(function (item) {
333 | pairs.push([first, item])
334 | });
335 | })
336 | return pairs;
337 | }
338 | var nodesPairs = pairwise(nodes);
339 | // for self-loops
340 | nodesPairs.push(...nodes.map(x => [x, x]));
341 | nodesPairs.forEach(function (nodePair) {
342 | const id1 = nodePair[1].id();
343 | var edges = nodePair[0].connectedEdges('.cy-expand-collapse-collapsed-edge[source = "' + id1 + '"],[target = "' + id1 + '"]');
344 | // edges for self-loops
345 | if (nodePair[0].id() === id1) {
346 | edges = nodePair[0].connectedEdges('[source = "' + id1 + '"][target = "' + id1 + '"]');
347 | }
348 | edgesToExpand = edgesToExpand.union(edges);
349 | }.bind(this));
350 | return this.expandEdges(edgesToExpand);
351 | };
352 |
353 | api.collapseAllEdges = function (opts) {
354 | return this.collapseEdgesBetweenNodes(cy.edges().connectedNodes(), opts);
355 | };
356 |
357 | api.expandAllEdges = function () {
358 | var edges = cy.edges(".cy-expand-collapse-collapsed-edge");
359 | var result = { edges: cy.collection(), oldEdges: cy.collection() };
360 | var operationResult = this.expandEdges(edges);
361 | result.oldEdges = result.oldEdges.add(operationResult.oldEdges);
362 | result.edges = result.edges.add(operationResult.edges);
363 | return result;
364 | };
365 |
366 | api.loadJson = function (jsonStr) {
367 | saveLoadUtils.loadJson(jsonStr);
368 | };
369 |
370 | api.saveJson = function (elems, filename) {
371 | return saveLoadUtils.saveJson(elems, filename);
372 | };
373 |
374 | return api; // Return the API instance
375 | }
376 |
377 | // Get the whole scratchpad reserved for this extension (on an element or core) or get a single property of it
378 | function getScratch(cyOrEle, name) {
379 | if (cyOrEle.scratch('_cyExpandCollapse') === undefined) {
380 | cyOrEle.scratch('_cyExpandCollapse', {});
381 | }
382 |
383 | var scratch = cyOrEle.scratch('_cyExpandCollapse');
384 | var retVal = (name === undefined) ? scratch : scratch[name];
385 | return retVal;
386 | }
387 |
388 | // Set a single property on scratchpad of an element or the core
389 | function setScratch(cyOrEle, name, val) {
390 | getScratch(cyOrEle)[name] = val;
391 | }
392 |
393 | // register the extension cy.expandCollapse()
394 | cytoscape("core", "expandCollapse", function (opts) {
395 | var cy = this;
396 |
397 | var options = getScratch(cy, 'options') || {
398 | layoutBy: null, // for rearrange after expand/collapse. It's just layout options or whole layout function. Choose your side!
399 | fisheye: true, // whether to perform fisheye view after expand/collapse you can specify a function too
400 | animate: true, // whether to animate on drawing changes you can specify a function too
401 | animationDuration: 1000, // when animate is true, the duration in milliseconds of the animation
402 | ready: function () { }, // callback when expand/collapse initialized
403 | undoable: true, // and if undoRedoExtension exists,
404 |
405 | cueEnabled: true, // Whether cues are enabled
406 | expandCollapseCuePosition: 'top-left', // default cue position is top left you can specify a function per node too
407 | expandCollapseCueSize: 12, // size of expand-collapse cue
408 | expandCollapseCueLineSize: 8, // size of lines used for drawing plus-minus icons
409 | expandCueImage: undefined, // image of expand icon if undefined draw regular expand cue
410 | collapseCueImage: undefined, // image of collapse icon if undefined draw regular collapse cue
411 | expandCollapseCueSensitivity: 1, // sensitivity of expand-collapse cues
412 |
413 | edgeTypeInfo: "edgeType", //the name of the field that has the edge type, retrieved from edge.data(), can be a function
414 | groupEdgesOfSameTypeOnCollapse: false,
415 | allowNestedEdgeCollapse: true,
416 | zIndex: 999 // z-index value of the canvas in which cue ımages are drawn
417 | };
418 |
419 | // If opts is not 'get' that is it is a real options object then initilize the extension
420 | if (opts !== 'get') {
421 | options = extendOptions(options, opts);
422 |
423 | var expandCollapseUtilities = require('./expandCollapseUtilities')(cy);
424 | var api = createExtensionAPI(cy, expandCollapseUtilities); // creates and returns the API instance for the extension
425 | saveLoadUtils = require("./saveLoadUtilities")(cy, api);
426 | setScratch(cy, 'api', api);
427 |
428 | undoRedoUtilities(cy, api);
429 |
430 | cueUtilities(options, cy, api);
431 |
432 | // if the cue is not enabled unbind cue events
433 | if (!options.cueEnabled) {
434 | cueUtilities('unbind', cy, api);
435 | }
436 |
437 | if (options.ready) {
438 | options.ready();
439 | }
440 |
441 | setScratch(cy, 'options', options);
442 |
443 | var parentData = {};
444 | setScratch(cy, 'parentData', parentData);
445 | }
446 |
447 | return getScratch(cy, 'api'); // Expose the API to the users
448 | });
449 | };
450 |
451 | if (typeof module !== 'undefined' && module.exports) { // expose as a commonjs module
452 | module.exports = register;
453 | }
454 |
455 | if (typeof define !== 'undefined' && define.amd) { // expose as an amd/requirejs module
456 | define('cytoscape-expand-collapse', function () {
457 | return register;
458 | });
459 | }
460 |
461 | if (typeof cytoscape !== 'undefined') { // expose to global cytoscape (i.e. window.cytoscape)
462 | register(cytoscape);
463 | }
464 |
465 | })();
466 |
--------------------------------------------------------------------------------
/demo/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | cytoscape-expand-collapse.js demo
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
109 |
110 |
111 |
112 |