Skip to content

Commit

Permalink
#38 support to save and load as JSON
Browse files Browse the repository at this point in the history
  • Loading branch information
canbax committed Dec 21, 2020
1 parent b807438 commit cc94823
Show file tree
Hide file tree
Showing 3 changed files with 430 additions and 4 deletions.
220 changes: 217 additions & 3 deletions cytoscape-expand-collapse.js

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

var undoRedoUtilities = require('./undoRedoUtilities');
var cueUtilities = require("./cueUtilities");
var saveLoadUtils = null;

function extendOptions(options, extendBy) {
var tempOpts = {};
Expand Down Expand Up @@ -362,7 +363,13 @@
return result;
};

api.loadJson = function (jsonStr) {
saveLoadUtils.loadJson(jsonStr);
};

api.saveJson = function (elems, filename) {
saveLoadUtils.saveJson(elems, filename);
};

return api; // Return the API instance
}
Expand Down Expand Up @@ -415,7 +422,7 @@

var expandCollapseUtilities = require('./expandCollapseUtilities')(cy);
var api = createExtensionAPI(cy, expandCollapseUtilities); // creates and returns the API instance for the extension

saveLoadUtils = require("./saveLoadUtilities")(cy, api);
setScratch(cy, 'api', api);

undoRedoUtilities(cy, api);
Expand Down
205 changes: 205 additions & 0 deletions src/saveLoadUtilities.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
function saveLoadUtilities(cy, api) {
function json2cyCollection(jsonArr, allNodes, nodes2collapse, node2parent) {
// process edges last since they depend on nodes
jsonArr.sort((a) => {
if (a.group === 'edges') {
return 1;
}
return -1;
});

// add compound nodes first, then add other nodes then edges
let coll = cy.collection();
for (let i = 0; i < jsonArr.length; i++) {
const json = jsonArr[i];
const d = json.data;
if (d.parent) {
node2parent[d.id] = d.parent;
}
const pos = { x: json.position.x, y: json.position.y };
const e = cy.add(json);
if (e.isNode()) {
allNodes.merge(e);
}

if (d.originalEnds) {
// all nodes should be in the memory (in cy or not)
let src = allNodes.$id(d.originalEnds.source.data.id);
if (d.originalEnds.source.data.parent) {
node2parent[d.originalEnds.source.data.id] = d.originalEnds.source.data.parent;
}
let tgt = allNodes.$id(d.originalEnds.target.data.id);
if (d.originalEnds.target.data.parent) {
node2parent[d.originalEnds.target.data.id] = d.originalEnds.target.data.parent;
}
e.data('originalEnds', { source: src, target: tgt });
}
if (d.collapsedChildren) {
nodes2collapse.merge(e);
json2cyCollection(d.collapsedChildren, allNodes, nodes2collapse, node2parent);
clearCollapseMetaData(e);
} else if (d.collapsedEdges) {
e.data('collapsedEdges', json2cyCollection(d.collapsedEdges, allNodes, nodes2collapse, node2parent));
// delete collapsed edges from cy
cy.remove(e.data('collapsedEdges'));
}
e.position(pos); // adding new elements to a compound might change its position
coll.merge(e);
}
return coll;
}

function clearCollapseMetaData(e) {
e.data('collapsedChildren', null);
e.removeClass('cy-expand-collapse-collapsed-node');
e.data('position-before-collapse', null);
e.data('size-before-collapse', null);
e.data('expandcollapseRenderedStartX', null);
e.data('expandcollapseRenderedStartY', null);
e.data('expandcollapseRenderedCueSize', null);
}

function cyCollection2Json(elems) {
let r = [];
for (let i = 0; i < elems.length; i++) {
const elem = elems[i];
let jsonObj = null;
if (!elem.collapsedChildren && !elem.collapsedEdges) {
jsonObj = elem.cy.json();
}
else if (elem.collapsedChildren) {
elem.collapsedChildren = cyCollection2Json(halfDeepCopyCollection(elem.collapsedChildren));
jsonObj = elem.cy.json();
jsonObj.data.collapsedChildren = elem.collapsedChildren;
} else if (elem.collapsedEdges) {
elem.collapsedEdges = cyCollection2Json(halfDeepCopyCollection(elem.collapsedEdges));
jsonObj = elem.cy.json();
jsonObj.data.collapsedEdges = elem.collapsedEdges;
}
if (elem.originalEnds) {
jsonObj.data.originalEnds = { source: elem.originalEnds.source.json(), target: elem.originalEnds.target.json() };
}
r.push(jsonObj);
}
return r;
}

// { cy: any, collapsedEdges: any, collapsedChildren: any, originalEnds: any }[]
function halfDeepCopyCollection(col) {
let arr = [];
for (let i = 0; i < col.length; i++) {
arr.push({ cy: col[i], collapsedEdges: col[i].data('collapsedEdges'), collapsedChildren: col[i].data('collapsedChildren'), originalEnds: col[i].data('originalEnds') });
}
return arr;
}

/** saves the string as a file.
* @param {} str string
* @param {} fileName string
*/
function str2file(str, fileName) {
const blob = new Blob([str], { type: 'text/plain' });
const anchor = document.createElement('a');

anchor.download = fileName;
anchor.href = (window.URL).createObjectURL(blob);
anchor.dataset.downloadurl =
['text/plain', anchor.download, anchor.href].join(':');
anchor.click();
}

return {
loadJson: function (txt) {
const fileJSON = JSON.parse(txt);
// original endpoints won't exist in cy. So keep a reference.
const nodePositions = {};
const allNodes = cy.collection(); // some elements are stored in cy, some are deleted
const nodes2collapse = cy.collection(); // some are deleted
const node2parent = {};
for (const n of fileJSON.nodes) {
nodePositions[n.data.id] = { x: n.position.x, y: n.position.y };
if (n.data.parent) {
node2parent[n.data.id] = n.data.parent;
}
const node = cy.add(n);
allNodes.merge(node);
if (node.data('collapsedChildren')) {
json2cyCollection(node.data('collapsedChildren'), allNodes, nodes2collapse, node2parent);
nodes2collapse.merge(node);
clearCollapseMetaData(node);
}
}
for (const e of fileJSON.edges) {
const edge = cy.add(e);
if (edge.data('collapsedEdges')) {
edge.data('collapsedEdges', json2cyCollection(e.data.collapsedEdges, allNodes, nodes2collapse, node2parent));
cy.remove(edge.data('collapsedEdges')); // delete collapsed edges from cy
}
if (edge.data('originalEnds')) {
const srcId = e.data.originalEnds.source.data.id;
const tgtId = e.data.originalEnds.target.data.id;
e.data.originalEnds = { source: allNodes.filter('#' + srcId), target: allNodes.filter('#' + tgtId) };
}
}
// set parents
for (let node in node2parent) {
const elem = allNodes.$id(node);
if (elem.length === 1) {
elem.move({ parent: node2parent[node] });
}
}
// collapse the collapsed nodes
api.collapse(nodes2collapse, { layoutBy: null, fisheye: false, animate: false });

// positions might be changed in collapse extension
for (const n of fileJSON.nodes) {
const node = cy.$id(n.data.id)
if (node.isChildless()) {
cy.$id(n.data.id).position(nodePositions[n.data.id]);
}
}
cy.fit();
},

saveJson: function (elems, filename) {
if (!elems) {
elems = cy.$();
}
const nodes = halfDeepCopyCollection(elems.nodes());
const edges = halfDeepCopyCollection(elems.edges());
if (edges.length + nodes.length < 1) {
return;
}

// according to cytoscape.js format
const o = { nodes: [], edges: [] };
for (const e of edges) {
if (e.collapsedEdges) {
e.collapsedEdges = cyCollection2Json(halfDeepCopyCollection(e.collapsedEdges));
}
if (e.originalEnds) {
e.originalEnds = { source: e.originalEnds.source.json(), target: e.originalEnds.target.json() };
}
const jsonObj = e.cy.json();
jsonObj.data.collapsedEdges = e.collapsedEdges;
jsonObj.data.originalEnds = e.originalEnds;
o.edges.push(jsonObj);
}
for (const n of nodes) {
if (n.collapsedChildren) {
n.collapsedChildren = cyCollection2Json(halfDeepCopyCollection(n.collapsedChildren));
}
const jsonObj = n.cy.json();
jsonObj.data.collapsedChildren = n.collapsedChildren;
o.nodes.push(jsonObj);
}

if (!filename) {
filename = 'expand-collapse-output.json';
}
str2file(JSON.stringify(o), filename);
}
};
}

module.exports = saveLoadUtilities;

0 comments on commit cc94823

Please sign in to comment.