diff --git a/README.md b/README.md index f0fd15c..e96049a 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ A. Civril, M. Magdon-Ismail, and E. Bocek-Rivele, "[SSDE: Fast Graph Drawing Usi * Cytoscape.js ^3.2.0 * numeric.js ^1.2.6 * cose-base ^1.0.0 + * cytoscape-layout-utilities.js (optional for packing disconnected components) ^1.0.0 ## Usage instructions @@ -56,7 +57,14 @@ require(['cytoscape', 'cytoscape-fcose'], function( cytoscape, fcose ){ }); ``` -Plain HTML/JS has the extension registered for you automatically, because no `require()` is needed. +Plain HTML/JS has the extension registered for you automatically, because no `require()` is needed. Just add the following files: + +``` + + + + +``` ## API @@ -86,6 +94,8 @@ var defaultOptions = { padding: 10, // whether to include labels in node dimensions. Valid in "proof" quality nodeDimensionsIncludeLabels: false, + // whether to pack disconnected components - valid only if randomize: true + packComponents: true, /* spectral layout options */ @@ -113,7 +123,7 @@ var defaultOptions = { // Maximum number of iterations to perform numIter: 2500, // For enabling tiling - tile: false, + tile: true, // Represents the amount of the vertical space to put between the zero degree members during the tiling operation(can also be a function) tilingPaddingVertical: 10, // Represents the amount of the horizontal space to put between the zero degree members during the tiling operation(can also be a function) diff --git a/cytoscape-fcose.js b/cytoscape-fcose.js index d6cde6b..b6a6673 100644 --- a/cytoscape-fcose.js +++ b/cytoscape-fcose.js @@ -89,6 +89,313 @@ module.exports = __WEBPACK_EXTERNAL_MODULE_0__; "use strict"; +/* + * Auxiliary functions + */ + +var LinkedList = __webpack_require__(0).layoutBase.LinkedList; + +var auxiliary = {}; + +auxiliary.multMat = function (array1, array2) { + var result = []; + + for (var i = 0; i < array1.length; i++) { + result[i] = []; + for (var j = 0; j < array2[0].length; j++) { + result[i][j] = 0; + for (var k = 0; k < array1[0].length; k++) { + result[i][j] += array1[i][k] * array2[k][j]; + } + } + } + return result; +}; + +auxiliary.multGamma = function (array) { + var result = []; + var sum = 0; + + for (var i = 0; i < array.length; i++) { + sum += array[i]; + } + + sum *= -1 / array.length; + + for (var _i = 0; _i < array.length; _i++) { + result[_i] = sum + array[_i]; + } + return result; +}; + +auxiliary.multL = function (array, C, INV) { + var result = []; + var temp1 = []; + var temp2 = []; + + // multiply by C^T + for (var i = 0; i < C[0].length; i++) { + var sum = 0; + for (var j = 0; j < C.length; j++) { + sum += -0.5 * C[j][i] * array[j]; + } + temp1[i] = sum; + } + // multiply the result by INV + for (var _i2 = 0; _i2 < INV.length; _i2++) { + var _sum = 0; + for (var _j = 0; _j < INV.length; _j++) { + _sum += INV[_i2][_j] * temp1[_j]; + } + temp2[_i2] = _sum; + } + // multiply the result by C + for (var _i3 = 0; _i3 < C.length; _i3++) { + var _sum2 = 0; + for (var _j2 = 0; _j2 < C[0].length; _j2++) { + _sum2 += C[_i3][_j2] * temp2[_j2]; + } + result[_i3] = _sum2; + } + + return result; +}; + +auxiliary.multCons = function (array, constant) { + var result = []; + + for (var i = 0; i < array.length; i++) { + result[i] = array[i] * constant; + } + + return result; +}; + +// assumes arrays have same size +auxiliary.minusOp = function (array1, array2) { + var result = []; + + for (var i = 0; i < array1.length; i++) { + result[i] = array1[i] - array2[i]; + } + + return result; +}; + +// assumes arrays have same size +auxiliary.dotProduct = function (array1, array2) { + var product = 0; + + for (var i = 0; i < array1.length; i++) { + product += array1[i] * array2[i]; + } + + return product; +}; + +auxiliary.mag = function (array) { + return Math.sqrt(this.dotProduct(array, array)); +}; + +auxiliary.normalize = function (array) { + var result = []; + var magnitude = this.mag(array); + + for (var i = 0; i < array.length; i++) { + result[i] = array[i] / magnitude; + } + + return result; +}; + +// get the top most nodes +auxiliary.getTopMostNodes = function (nodes) { + var nodesMap = {}; + for (var i = 0; i < nodes.length; i++) { + nodesMap[nodes[i].id()] = true; + } + var roots = nodes.filter(function (ele, i) { + if (typeof ele === "number") { + ele = i; + } + var parent = ele.parent()[0]; + while (parent != null) { + if (nodesMap[parent.id()]) { + return false; + } + parent = parent.parent()[0]; + } + return true; + }); + + return roots; +}; + +// find disconnected components and create dummy nodes that connect them +auxiliary.connectComponents = function (cy, eles, topMostNodes, dummyNodes) { + var queue = new LinkedList(); + var visited = new Set(); + var visitedTopMostNodes = []; + var currentNeighbor = void 0; + var minDegreeNode = void 0; + var minDegree = void 0; + + var isConnected = false; + var count = 1; + var nodesConnectedToDummy = []; + var components = []; + + var _loop = function _loop() { + var cmpt = cy.collection(); + components.push(cmpt); + + var currentNode = topMostNodes[0]; + var childrenOfCurrentNode = cy.collection(); + childrenOfCurrentNode.merge(currentNode).merge(currentNode.descendants()); + visitedTopMostNodes.push(currentNode); + + childrenOfCurrentNode.forEach(function (node) { + queue.push(node); + visited.add(node); + cmpt.merge(node); + }); + + var _loop2 = function _loop2() { + currentNode = queue.shift(); + + // Traverse all neighbors of this node + var neighborNodes = cy.collection(); + currentNode.neighborhood().nodes().forEach(function (node) { + if (eles.contains(currentNode.edgesWith(node))) { + neighborNodes.merge(node); + } + }); + + for (var i = 0; i < neighborNodes.length; i++) { + var neighborNode = neighborNodes[i]; + currentNeighbor = topMostNodes.intersection(neighborNode.union(neighborNode.ancestors())); + if (currentNeighbor != null && !visited.has(currentNeighbor[0])) { + var childrenOfNeighbor = currentNeighbor.union(currentNeighbor.descendants()); + + childrenOfNeighbor.forEach(function (node) { + queue.push(node); + visited.add(node); + cmpt.merge(node); + if (topMostNodes.has(node)) { + visitedTopMostNodes.push(node); + } + }); + } + } + }; + + while (queue.length != 0) { + _loop2(); + } + + cmpt.forEach(function (node) { + node.connectedEdges().forEach(function (e) { + // connectedEdges() usually cached + if (cmpt.has(e.source()) && cmpt.has(e.target())) { + // has() is cheap + cmpt.merge(e); // forEach() only considers nodes -- sets N at call time + } + }); + }); + + if (visitedTopMostNodes.length == topMostNodes.length) { + isConnected = true; + } + + if (!isConnected || isConnected && count > 1) { + minDegreeNode = visitedTopMostNodes[0]; + minDegree = minDegreeNode.connectedEdges().length; + visitedTopMostNodes.forEach(function (node) { + if (node.connectedEdges().length < minDegree) { + minDegree = node.connectedEdges().length; + minDegreeNode = node; + } + }); + nodesConnectedToDummy.push(minDegreeNode.id()); + // TO DO: Check efficiency of this part + var temp = cy.collection(); + temp.merge(visitedTopMostNodes[0]); + visitedTopMostNodes.forEach(function (node) { + temp.merge(node); + }); + visitedTopMostNodes = []; + topMostNodes = topMostNodes.difference(temp); + count++; + } + }; + + do { + _loop(); + } while (!isConnected); + + if (dummyNodes) { + if (nodesConnectedToDummy.length > 0) { + dummyNodes.set('dummy' + (dummyNodes.size + 1), nodesConnectedToDummy); + } + } + return components; +}; + +auxiliary.calcBoundingBox = function (parentNode, xCoords, yCoords, nodeIndexes) { + // calculate bounds + var left = Number.MAX_VALUE; + var right = Number.MIN_VALUE; + var top = Number.MAX_VALUE; + var bottom = Number.MIN_VALUE; + var nodeLeft = void 0; + var nodeRight = void 0; + var nodeTop = void 0; + var nodeBottom = void 0; + + var nodes = parentNode.descendants().not(":parent"); + var s = nodes.length; + for (var i = 0; i < s; i++) { + var node = nodes[i]; + + nodeLeft = xCoords[nodeIndexes.get(node.id())] - node.width() / 2; + nodeRight = xCoords[nodeIndexes.get(node.id())] + node.width() / 2; + nodeTop = yCoords[nodeIndexes.get(node.id())] - node.height() / 2; + nodeBottom = yCoords[nodeIndexes.get(node.id())] + node.height() / 2; + + if (left > nodeLeft) { + left = nodeLeft; + } + + if (right < nodeRight) { + right = nodeRight; + } + + if (top > nodeTop) { + top = nodeTop; + } + + if (bottom < nodeBottom) { + bottom = nodeBottom; + } + } + + var boundingBox = {}; + boundingBox.topLeftX = left; + boundingBox.topLeftY = top; + boundingBox.width = right - left; + boundingBox.height = bottom - top; + return boundingBox; +}; + +module.exports = auxiliary; + +/***/ }), +/* 2 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } @@ -97,7 +404,8 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons The implementation of the fcose layout algorithm */ -var assign = __webpack_require__(2); +var assign = __webpack_require__(3); +var aux = __webpack_require__(1); var _require = __webpack_require__(5), spectralLayout = _require.spectralLayout; @@ -127,6 +435,8 @@ var defaults = Object.freeze({ padding: 10, // whether to include labels in node dimensions. Valid in "proof" quality nodeDimensionsIncludeLabels: false, + // whether to pack disconnected components - valid only if randomize: true + packComponents: true, /* spectral layout options */ @@ -154,7 +464,7 @@ var defaults = Object.freeze({ // Maximum number of iterations to perform numIter: 2500, // For enabling tiling - tile: false, + tile: true, // Represents the amount of the vertical space to put between the zero degree members during the tiling operation(can also be a function) tilingPaddingVertical: 10, // Represents the amount of the horizontal space to put between the zero degree members during the tiling operation(can also be a function) @@ -185,44 +495,201 @@ var Layout = function () { value: function run() { var layout = this; var options = this.options; + var cy = options.cy; var eles = options.eles; - var spectralResult = void 0; + var spectralResult = []; var xCoords = void 0; var yCoords = void 0; - var coseResult = void 0; - - // If number of nodes is 1 or 2, either SVD or powerIteration causes problem - // So direct the graph to cose layout - if (options.randomize && eles.nodes().length > 2) { - // Apply spectral layout - spectralResult = spectralLayout(options); - xCoords = spectralResult["xCoords"]; - yCoords = spectralResult["yCoords"]; + var coseResult = []; + var components = void 0; + + var layUtil = void 0; + var packingEnabled = false; + if (cy.layoutUtilities && options.packComponents && options.randomize) { + layUtil = cy.layoutUtilities("get"); + if (!layUtil) layUtil = cy.layoutUtilities(); + packingEnabled = true; + } + + if (options.eles.length == 0) return; + + if (options.eles.length != options.cy.elements().length) { + var prevNodes = eles.nodes(); + eles = eles.union(eles.descendants()); + + eles.forEach(function (ele) { + if (ele.isNode()) { + var connectedEdges = ele.connectedEdges(); + connectedEdges.forEach(function (edge) { + if (eles.contains(edge.source()) && eles.contains(edge.target()) && !prevNodes.contains(edge.source().union(edge.target()))) { + eles = eles.union(edge); + } + }); + } + }); + + options.eles = eles; + } + + if (packingEnabled) { + var topMostNodes = aux.getTopMostNodes(options.eles.nodes()); + components = aux.connectComponents(cy, options.eles, topMostNodes); + } + + if (options.randomize) { + if (packingEnabled) { + components.forEach(function (component) { + options.eles = component; + spectralResult.push(spectralLayout(options)); + }); + } else { + // Apply spectral layout + spectralResult.push(spectralLayout(options)); + if (spectralResult[0]) { + xCoords = spectralResult[0]["xCoords"]; + yCoords = spectralResult[0]["yCoords"]; + } + } + } + + if (options.quality == "default" || options.quality == "proof" || spectralResult.includes(false)) { + if (packingEnabled) { + if (options.quality == "draft" && spectralResult.includes(false)) { + spectralResult.forEach(function (value, index) { + if (!value) { + options.eles = components[index]; + var tempResult = coseLayout(options, spectralResult[index]); + var nodeIndexes = new Map(); + var _xCoords = []; + var _yCoords = []; + var count = 0; + Object.keys(tempResult).forEach(function (item) { + nodeIndexes.set(item, count++); + _xCoords.push(tempResult[item].getCenterX()); + _yCoords.push(tempResult[item].getCenterY()); + }); + spectralResult[index] = { nodeIndexes: nodeIndexes, xCoords: _xCoords, yCoords: _yCoords }; + } + }); + } else { + var toBeTiledNodes = cy.collection(); + if (options.tile) { + var nodeIndexes = new Map(); + var _xCoords2 = []; + var _yCoords2 = []; + var count = 0; + var tempSpectralResult = { nodeIndexes: nodeIndexes, xCoords: _xCoords2, yCoords: _yCoords2 }; + var indexesToBeDeleted = []; + components.forEach(function (component, index) { + if (component.edges().length == 0) { + component.nodes().forEach(function (node, i) { + toBeTiledNodes.merge(component.nodes()[i]); + if (!node.isParent()) { + tempSpectralResult.nodeIndexes.set(component.nodes()[i].id(), count++); + tempSpectralResult.xCoords.push(component.nodes()[0].position().x); + tempSpectralResult.yCoords.push(component.nodes()[0].position().y); + } + }); + indexesToBeDeleted.push(index); + } + }); + if (toBeTiledNodes.length > 1) { + components.push(toBeTiledNodes); + for (var i = indexesToBeDeleted.length - 1; i >= 0; i--) { + components.splice(indexesToBeDeleted[i], 1); + spectralResult.splice(indexesToBeDeleted[i], 1); + }; + spectralResult.push(tempSpectralResult); + } + } + components.forEach(function (component, index) { + options.eles = component; + coseResult.push(coseLayout(options, spectralResult[index])); + }); + } + } else { + // Apply cose layout as postprocessing + coseResult.push(coseLayout(options, spectralResult[0])); + } } - if (options.quality == "default" || options.quality == "proof" || eles.nodes().length <= 2) { - // Apply cose layout as postprocessing - coseResult = coseLayout(options, spectralResult); + if (packingEnabled) { + var subgraphs = []; + components.forEach(function (component, index) { + var nodeIndexes = void 0; + if (options.quality == "draft") { + nodeIndexes = spectralResult[index].nodeIndexes; + } + var subgraph = {}; + subgraph.nodes = []; + subgraph.edges = []; + var nodeIndex = void 0; + component.nodes().forEach(function (node) { + if (options.quality == "draft") { + if (!node.isParent()) { + nodeIndex = nodeIndexes.get(node.id()); + subgraph.nodes.push({ x: spectralResult[index].xCoords[nodeIndex] - node.bb().w / 2, y: spectralResult[index].yCoords[nodeIndex] - node.bb().h / 2, width: node.bb().w, height: node.bb().h }); + } else { + var parentInfo = aux.calcBoundingBox(node, spectralResult[index].xCoords, spectralResult[index].yCoords, nodeIndexes); + subgraph.nodes.push({ x: parentInfo.topLeftX, y: parentInfo.topLeftY, width: parentInfo.width, height: parentInfo.height }); + } + } else { + subgraph.nodes.push({ x: coseResult[index][node.id()].getLeft(), y: coseResult[index][node.id()].getTop(), width: coseResult[index][node.id()].getWidth(), height: coseResult[index][node.id()].getHeight() }); + } + }); + subgraphs.push(subgraph); + }); + var shiftResult = layUtil.packComponents(subgraphs).shifts; + if (options.quality == "draft") { + spectralResult.forEach(function (result, index) { + var newXCoords = result.xCoords.map(function (x) { + return x + shiftResult[index].dx; + }); + var newYCoords = result.yCoords.map(function (y) { + return y + shiftResult[index].dy; + }); + result.xCoords = newXCoords; + result.yCoords = newYCoords; + }); + } else { + coseResult.forEach(function (result, index) { + Object.keys(result).forEach(function (item) { + var nodeRectangle = result[item]; + nodeRectangle.setCenter(nodeRectangle.getCenterX() + shiftResult[index].dx, nodeRectangle.getCenterY() + shiftResult[index].dy); + }); + }); + } } // get each element's calculated position var getPositions = function getPositions(ele, i) { - if (options.quality == "default" || options.quality == "proof") { + if (options.quality == "default" || options.quality == "proof" || options.quality == "proof" && !packingEnabled && spectralResult.includes(false)) { if (typeof ele === "number") { ele = i; } + var pos = void 0; var theId = ele.data('id'); - var lNode = coseResult[theId]; - + coseResult.forEach(function (result) { + if (theId in result) { + pos = { x: result[theId].getRect().getCenterX(), y: result[theId].getRect().getCenterY() }; + } + }); return { - x: lNode.getRect().getCenterX(), - y: lNode.getRect().getCenterY() + x: pos.x, + y: pos.y }; } else { + var _pos = void 0; + spectralResult.forEach(function (result) { + var index = result.nodeIndexes.get(ele.id()); + if (index != undefined) { + _pos = { x: result.xCoords[index], y: result.yCoords[index] }; + }; + }); return { - x: xCoords[i], - y: yCoords[i] + x: _pos.x, + y: _pos.y }; } }; @@ -230,6 +697,7 @@ var Layout = function () { // quality = "draft" and randomize = false are contradictive so in that case positions don't change if (options.quality == "default" || options.quality == "proof" || options.randomize) { // transfer calculated positions to nodes (positions of only simple nodes are evaluated, compounds are positioned automatically) + options.eles = eles; eles.nodes().not(":parent").layoutPositions(layout, options, getPositions); } else { console.log("If randomize option is set to false, then quality option must be 'default' or 'proof'."); @@ -243,7 +711,7 @@ var Layout = function () { module.exports = Layout; /***/ }), -/* 2 */ +/* 3 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -265,132 +733,6 @@ module.exports = Object.assign != null ? Object.assign.bind(Object) : function ( return tgt; }; -/***/ }), -/* 3 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -/* - * Auxiliary functions used in spectral layout (especially in power iteration) - */ - -var auxiliary = {}; - -auxiliary.multMat = function (array1, array2) { - var result = []; - - for (var i = 0; i < array1.length; i++) { - result[i] = []; - for (var j = 0; j < array2[0].length; j++) { - result[i][j] = 0; - for (var k = 0; k < array1[0].length; k++) { - result[i][j] += array1[i][k] * array2[k][j]; - } - } - } - return result; -}; - -auxiliary.multGamma = function (array) { - var result = []; - var sum = 0; - - for (var i = 0; i < array.length; i++) { - sum += array[i]; - } - - sum *= -1 / array.length; - - for (var _i = 0; _i < array.length; _i++) { - result[_i] = sum + array[_i]; - } - return result; -}; - -auxiliary.multL = function (array, C, INV) { - var result = []; - var temp1 = []; - var temp2 = []; - - // multiply by C^T - for (var i = 0; i < C[0].length; i++) { - var sum = 0; - for (var j = 0; j < C.length; j++) { - sum += -0.5 * C[j][i] * array[j]; - } - temp1[i] = sum; - } - // multiply the result by INV - for (var _i2 = 0; _i2 < INV.length; _i2++) { - var _sum = 0; - for (var _j = 0; _j < INV.length; _j++) { - _sum += INV[_i2][_j] * temp1[_j]; - } - temp2[_i2] = _sum; - } - // multiply the result by C - for (var _i3 = 0; _i3 < C.length; _i3++) { - var _sum2 = 0; - for (var _j2 = 0; _j2 < C[0].length; _j2++) { - _sum2 += C[_i3][_j2] * temp2[_j2]; - } - result[_i3] = _sum2; - } - - return result; -}; - -auxiliary.multCons = function (array, constant) { - var result = []; - - for (var i = 0; i < array.length; i++) { - result[i] = array[i] * constant; - } - - return result; -}; - -// assumes arrays have same size -auxiliary.minusOp = function (array1, array2) { - var result = []; - - for (var i = 0; i < array1.length; i++) { - result[i] = array1[i] - array2[i]; - } - - return result; -}; - -// assumes arrays have same size -auxiliary.dotProduct = function (array1, array2) { - var product = 0; - - for (var i = 0; i < array1.length; i++) { - product += array1[i] * array2[i]; - } - - return product; -}; - -auxiliary.mag = function (array) { - return Math.sqrt(this.dotProduct(array, array)); -}; - -auxiliary.normalize = function (array) { - var result = []; - var magnitude = this.mag(array); - - for (var i = 0; i < array.length; i++) { - result[i] = array[i] / magnitude; - } - - return result; -}; - -module.exports = auxiliary; - /***/ }), /* 4 */ /***/ (function(module, exports, __webpack_require__) { @@ -402,6 +744,7 @@ module.exports = auxiliary; The implementation of the postprocessing part that applies CoSE layout over the spectral layout */ +var aux = __webpack_require__(1); var CoSELayout = __webpack_require__(0).CoSELayout; var CoSENode = __webpack_require__(0).CoSENode; var PointD = __webpack_require__(0).layoutBase.PointD; @@ -422,7 +765,7 @@ var coseLayout = function coseLayout(options, spectralResult) { var yCoords = void 0; var idToLNode = {}; - if (options.randomize && nodes.length > 2) { + if (options.randomize && spectralResult) { nodeIndexes = spectralResult["nodeIndexes"]; xCoords = spectralResult["xCoords"]; yCoords = spectralResult["yCoords"]; @@ -430,29 +773,6 @@ var coseLayout = function coseLayout(options, spectralResult) { /**** Postprocessing functions ****/ - // get the top most nodes - var getTopMostNodes = function getTopMostNodes(nodes) { - var nodesMap = {}; - for (var i = 0; i < nodes.length; i++) { - nodesMap[nodes[i].id()] = true; - } - var roots = nodes.filter(function (ele, i) { - if (typeof ele === "number") { - ele = i; - } - var parent = ele.parent()[0]; - while (parent != null) { - if (nodesMap[parent.id()]) { - return false; - } - parent = parent.parent()[0]; - } - return true; - }); - - return roots; - }; - // transfer cytoscape nodes to cose nodes var processChildrenList = function processChildrenList(parent, children, layout, options) { var size = children.length; @@ -466,11 +786,11 @@ var coseLayout = function coseLayout(options, spectralResult) { }); if (theChild.outerWidth() != null && theChild.outerHeight() != null) { - if (options.randomize && nodes.length > 2) { + if (options.randomize && spectralResult) { if (!theChild.isParent()) { theNode = parent.add(new CoSENode(layout.graphManager, new PointD(xCoords[nodeIndexes.get(theChild.id())] - dimensions.w / 2, yCoords[nodeIndexes.get(theChild.id())] - dimensions.h / 2), new DimensionD(parseFloat(dimensions.w), parseFloat(dimensions.h)))); } else { - var parentInfo = calcBoundingBox(theChild); + var parentInfo = aux.calcBoundingBox(theChild, xCoords, yCoords, nodeIndexes); theNode = parent.add(new CoSENode(layout.graphManager, new PointD(parentInfo.topLeftX, parentInfo.topLeftY), new DimensionD(parentInfo.width, parentInfo.height))); } } else { @@ -516,51 +836,6 @@ var coseLayout = function coseLayout(options, spectralResult) { processChildrenList(theNewGraph, children_of_children, layout, options); } } - function calcBoundingBox(parentNode) { - // calculate bounds - var left = Number.MAX_VALUE; - var right = Number.MIN_VALUE; - var top = Number.MAX_VALUE; - var bottom = Number.MIN_VALUE; - var nodeLeft = void 0; - var nodeRight = void 0; - var nodeTop = void 0; - var nodeBottom = void 0; - - var nodes = parentNode.descendants().not(":parent"); - var s = nodes.length; - for (var _i = 0; _i < s; _i++) { - var node = nodes[_i]; - - nodeLeft = xCoords[nodeIndexes.get(node.id())] - node.width() / 2; - nodeRight = xCoords[nodeIndexes.get(node.id())] + node.width() / 2; - nodeTop = yCoords[nodeIndexes.get(node.id())] - node.height() / 2; - nodeBottom = yCoords[nodeIndexes.get(node.id())] + node.height() / 2; - - if (left > nodeLeft) { - left = nodeLeft; - } - - if (right < nodeRight) { - right = nodeRight; - } - - if (top > nodeTop) { - top = nodeTop; - } - - if (bottom < nodeBottom) { - bottom = nodeBottom; - } - } - - var boundingBox = {}; - boundingBox.topLeftX = left; - boundingBox.topLeftY = top; - boundingBox.width = right - left; - boundingBox.height = top - bottom; - return boundingBox; - } }; // transfer cytoscape edges to cose edges @@ -603,7 +878,7 @@ var coseLayout = function coseLayout(options, spectralResult) { var coseLayout = new CoSELayout(); var gm = coseLayout.newGraphManager(); - processChildrenList(gm.addRoot(), getTopMostNodes(nodes), coseLayout, options); + processChildrenList(gm.addRoot(), aux.getTopMostNodes(nodes), coseLayout, options); processEdges(coseLayout, gm, edges); @@ -625,9 +900,8 @@ module.exports = { coseLayout: coseLayout }; The implementation of the spectral layout that is the first part of the fcose layout algorithm */ -var aux = __webpack_require__(3); +var aux = __webpack_require__(1); var numeric = __webpack_require__(7); -var LinkedList = __webpack_require__(0).layoutBase.LinkedList; // main function that spectral layout is processed var spectralLayout = function spectralLayout(options) { @@ -635,6 +909,7 @@ var spectralLayout = function spectralLayout(options) { var cy = options.cy; var eles = options.eles; var nodes = eles.nodes(); + var parentNodes = eles.nodes(":parent"); var dummyNodes = new Map(); // map to keep dummy nodes and their neighbors var nodeIndexes = new Map(); // map to keep indexes to nodes @@ -662,106 +937,6 @@ var spectralLayout = function spectralLayout(options) { /**** Spectral-preprocessing functions ****/ - // get the top most nodes - var getTopMostNodes = function getTopMostNodes(nodes) { - var nodesMap = {}; - for (var i = 0; i < nodes.length; i++) { - nodesMap[nodes[i].id()] = true; - } - var roots = nodes.filter(function (ele, i) { - if (typeof ele === "number") { - ele = i; - } - var parent = ele.parent()[0]; - while (parent != null) { - if (nodesMap[parent.id()]) { - return false; - } - parent = parent.parent()[0]; - } - return true; - }); - - return roots; - }; - - // find disconnected components and create dummy nodes that connect them - var connectComponents = function connectComponents(topMostNodes) { - var queue = new LinkedList(); - var visited = new Set(); - var visitedTopMostNodes = []; - var currentNeighbor = void 0; - var minDegreeNode = void 0; - var minDegree = void 0; - - var isConnected = false; - var count = 1; - var nodesConnectedToDummy = []; - - do { - var currentNode = topMostNodes[0]; - var childrenOfCurrentNode = currentNode.union(currentNode.descendants()); - visitedTopMostNodes.push(currentNode); - - childrenOfCurrentNode.forEach(function (node) { - queue.push(node); - visited.add(node); - }); - - while (queue.length != 0) { - currentNode = queue.shift(); - - // Traverse all neighbors of this node - var neighborNodes = currentNode.neighborhood().nodes(); - for (var i = 0; i < neighborNodes.length; i++) { - var neighborNode = neighborNodes[i]; - currentNeighbor = topMostNodes.intersection(neighborNode.union(neighborNode.ancestors())); - if (currentNeighbor != null && !visited.has(currentNeighbor[0])) { - var childrenOfNeighbor = currentNeighbor.union(currentNeighbor.descendants()); - - childrenOfNeighbor.forEach(function (node) { - queue.push(node); - visited.add(node); - if (topMostNodes.has(node)) { - visitedTopMostNodes.push(node); - } - }); - } - } - } - - if (visitedTopMostNodes.length == topMostNodes.length) { - isConnected = true; - } - - if (!isConnected || isConnected && count > 1) { - (function () { - minDegreeNode = visitedTopMostNodes[0]; - minDegree = minDegreeNode.connectedEdges().length; - visitedTopMostNodes.forEach(function (node) { - if (node.connectedEdges().length < minDegree) { - minDegree = node.connectedEdges().length; - minDegreeNode = node; - } - }); - nodesConnectedToDummy.push(minDegreeNode.id()); - // TO DO: Check efficiency of this part - var temp = visitedTopMostNodes[0]; - visitedTopMostNodes.forEach(function (node) { - temp = temp.union(node); - }); - visitedTopMostNodes = []; - topMostNodes = topMostNodes.difference(temp); - count++; - })(); - } - } while (!isConnected); - - if (nodesConnectedToDummy.length > 0) { - dummyNodes.set('dummy' + (dummyNodes.size + 1), nodesConnectedToDummy); - } - }; - /**** Spectral layout functions ****/ // determine which columns to be sampled @@ -1005,10 +1180,10 @@ var spectralLayout = function spectralLayout(options) { /**** Preparation for spectral layout (Preprocessing) ****/ // connect disconnected components (first top level, then inside of each compound node) - connectComponents(getTopMostNodes(nodes)); + aux.connectComponents(cy, eles, aux.getTopMostNodes(nodes), dummyNodes); - cy.nodes(":parent").forEach(function (ele) { - connectComponents(getTopMostNodes(ele.descendants())); + parentNodes.forEach(function (ele) { + aux.connectComponents(cy, eles, aux.getTopMostNodes(ele.descendants()), dummyNodes); }); // assign indexes to nodes (first real, then dummy nodes) @@ -1051,7 +1226,7 @@ var spectralLayout = function spectralLayout(options) { } // form a parent-child map to keep representative node of each compound node - cy.nodes(":parent").forEach(function (ele) { + parentNodes.forEach(function (ele) { var children = ele.children(); // let random = 0; @@ -1073,13 +1248,15 @@ var spectralLayout = function spectralLayout(options) { }); // add neighborhood relations (first real, then dummy nodes) - cy.nodes().forEach(function (ele) { + nodes.forEach(function (ele) { var eleIndex = void 0; if (ele.isParent()) eleIndex = nodeIndexes.get(parentChildMap.get(ele.id()));else eleIndex = nodeIndexes.get(ele.id()); ele.neighborhood().nodes().forEach(function (node) { - if (node.isParent()) allNodesNeighborhood[eleIndex].push(parentChildMap.get(node.id()));else allNodesNeighborhood[eleIndex].push(node.id()); + if (eles.contains(ele.edgesWith(node))) { + if (node.isParent()) allNodesNeighborhood[eleIndex].push(parentChildMap.get(node.id()));else allNodesNeighborhood[eleIndex].push(node.id()); + } }); }); @@ -1122,26 +1299,36 @@ var spectralLayout = function spectralLayout(options) { } nodeSize = nodeIndexes.size; - // if # of nodes in transformed graph is smaller than sample size, - // then use # of nodes as sample size - sampleSize = nodeSize < options.sampleSize ? nodeSize : options.sampleSize; - // instantiates the partial matrices that will be used in spectral layout - for (var _i14 = 0; _i14 < nodeSize; _i14++) { - C[_i14] = []; - } - for (var _i15 = 0; _i15 < sampleSize; _i15++) { - INV[_i15] = []; - } + var spectralResult = void 0; + + // If number of nodes in transformed graph is 1 or 2, either SVD or powerIteration causes problem + // So skip spectral and layout the graph with cose + if (nodeSize > 2) { + // if # of nodes in transformed graph is smaller than sample size, + // then use # of nodes as sample size + sampleSize = nodeSize < options.sampleSize ? nodeSize : options.sampleSize; + + // instantiates the partial matrices that will be used in spectral layout + for (var _i14 = 0; _i14 < nodeSize; _i14++) { + C[_i14] = []; + } + for (var _i15 = 0; _i15 < sampleSize; _i15++) { + INV[_i15] = []; + } - /**** Apply spectral layout ****/ + /**** Apply spectral layout ****/ - allBFS(samplingType); - sample(); - powerIteration(); + allBFS(samplingType); + sample(); + powerIteration(); - var spectralResult = { nodeIndexes: nodeIndexes, xCoords: xCoords, yCoords: yCoords }; - return spectralResult; + spectralResult = { nodeIndexes: nodeIndexes, xCoords: xCoords, yCoords: yCoords }; + return spectralResult; + } else { + spectralResult = false; + return spectralResult; + } }; module.exports = { spectralLayout: spectralLayout }; @@ -1153,7 +1340,7 @@ module.exports = { spectralLayout: spectralLayout }; "use strict"; -var impl = __webpack_require__(1); +var impl = __webpack_require__(2); // registers the extension on a cytoscape lib ref var register = function register(cytoscape) { diff --git a/demo-compound.html b/demo-compound.html index e847297..1d390a6 100644 --- a/demo-compound.html +++ b/demo-compound.html @@ -11,6 +11,7 @@ + @@ -50,7 +51,7 @@ node.css("width", size); node.css("height", size); }); - this.layout({name: 'fcose', animationEasing: 'linear'}).run(); + this.layout({name: 'fcose', animationEasing: 'linear', packComponents: document.getElementById("packing").checked}).run(); }, // demo your layout @@ -79,7 +80,22 @@ style: { 'line-color': '#2B65EC' } - } + }, + + { + selector: 'node:selected', + style: { + 'background-color': '#F08080', + 'border-color': 'red' + } + }, + + { + selector: 'edge:selected', + style: { + 'line-color': '#F08080' + } + } ], elements: [{ group:'nodes', data:{ id: 'n0'}}, @@ -124,6 +140,9 @@ { group:'nodes', data:{ id: 'n41', parent: 'n42'}}, { group:'nodes', data:{ id: 'n42', parent: 'n43'}}, { group:'nodes', data:{ id: 'n43'}}, + { group:'nodes', data:{ id: 'n44'}}, + { group:'nodes', data:{ id: 'n45'}}, + { group:'nodes', data:{ id: 'n46'}}, { group:'edges', data:{ id: 'e0', source: 'n0', target: 'n1'} }, { group:'edges', data:{ id: 'e1', source: 'n1', target: 'n2'} }, { group:'edges', data:{ id: 'e2', source: 'n2', target: 'n3'} }, @@ -160,7 +179,10 @@ { group:'edges', data:{ id: 'e37', source: 'n33', target: 'n34'} }, { group:'edges', data:{ id: 'e38', source: 'n32', target: 'n35'} }, { group:'edges', data:{ id: 'e39', source: 'n32', target: 'n36'} }, - { group:'edges', data:{ id: 'e40', source: 'n16', target: 'n40'} } + { group:'edges', data:{ id: 'e40', source: 'n16', target: 'n40'} }, + { group:'edges', data:{ id: 'e41', source: 'n44', target: 'n45'} }, + { group:'edges', data:{ id: 'e42', source: 'n44', target: 'n46'} }, + { group:'edges', data:{ id: 'e43', source: 'n45', target: 'n46'} } ] }); @@ -181,6 +203,7 @@ quality: 'default', randomize: true, animationEasing: 'ease-out', + packComponents: document.getElementById("packing").checked }); layout.run(); @@ -194,8 +217,8 @@

cytoscape-fcose demo

    - - +     +
diff --git a/demo.html b/demo.html index 098abbe..f1548ef 100644 --- a/demo.html +++ b/demo.html @@ -73,7 +73,22 @@ 'width': 3, 'line-color': '#2B65EC' } - } + }, + + { + selector: 'node:selected', + style: { + 'background-color': '#F08080', + 'border-color': 'red' + } + }, + + { + selector: 'edge:selected', + style: { + 'line-color': '#F08080' + } + } ], elements: [{"data":{"id":"glyph9","sbgnbbox":{"x":1452.639173965406,"y":609.3619416544145,"w":"120.0","h":"60.0"},"sbgnclass":"macromolecule","sbgnlabel":"hexokinase","sbgnstatesandinfos":[],"ports":[]},"position":{"x":1452.639173965406,"y":609.3619416544145},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph0","sbgnbbox":{"x":1351.3490293961959,"y":518.9529901384763,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"glucose","sbgnstatesandinfos":[],"ports":[]},"position":{"x":1351.3490293961959,"y":518.9529901384763},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph6","sbgnbbox":{"x":1358.2854747390154,"y":707.9866590968695,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"ATP","sbgnstatesandinfos":[],"sbgnclonemarker":true,"ports":[]},"position":{"x":1358.2854747390154,"y":707.9866590968695},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph8","sbgnbbox":{"x":1322.9939787691299,"y":614.6878118623499,"w":"20.0","h":"20.0"},"sbgnclass":"process","sbgnstatesandinfos":[],"ports":[]},"position":{"x":1322.9939787691299,"y":614.6878118623499},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph7","sbgnbbox":{"x":1239.4852011317887,"y":543.2369849876238,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"ADP","sbgnstatesandinfos":[],"sbgnclonemarker":true,"ports":[]},"position":{"x":1239.4852011317887,"y":543.2369849876238},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph12","sbgnbbox":{"x":841.6855140740067,"y":765.0152660242113,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"ADP","sbgnstatesandinfos":[],"sbgnclonemarker":true,"ports":[]},"position":{"x":841.6855140740067,"y":765.0152660242113},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph13","sbgnbbox":{"x":1019.5908382748769,"y":841.6087025848726,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"ATP","sbgnstatesandinfos":[],"sbgnclonemarker":true,"ports":[]},"position":{"x":1019.5908382748769,"y":841.6087025848726},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph1","sbgnbbox":{"x":1231.2768042260652,"y":673.2683218469676,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"glucose 6P","sbgnstatesandinfos":[],"ports":[]},"position":{"x":1231.2768042260652,"y":673.2683218469676},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph2","sbgnbbox":{"x":1039.8995038336504,"y":730.180116446269,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"fructose 6P","sbgnstatesandinfos":[],"ports":[]},"position":{"x":1039.8995038336504,"y":730.180116446269},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph15","sbgnbbox":{"x":569.5498472077387,"y":506.89980858075364,"w":"120.0","h":"60.0"},"sbgnclass":"macromolecule","sbgnlabel":"triose-P isomerase","sbgnstatesandinfos":[],"ports":[]},"position":{"x":569.5498472077387,"y":506.89980858075364},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph3","sbgnbbox":{"x":903.0347368937041,"y":654.3308627056822,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"fructose 1,6P","sbgnstatesandinfos":[],"ports":[]},"position":{"x":903.0347368937041,"y":654.3308627056822},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph17","sbgnbbox":{"x":1195.6310733031135,"y":820.9504141631944,"w":"120.0","h":"60.0"},"sbgnclass":"macromolecule","sbgnlabel":"glucose-6P isomerase","sbgnstatesandinfos":[],"ports":[]},"position":{"x":1195.6310733031135,"y":820.9504141631944},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph10","sbgnbbox":{"x":1141.2404374322139,"y":732.3190922346248,"w":"20.0","h":"20.0"},"sbgnclass":"process","sbgnstatesandinfos":[],"ports":[]},"position":{"x":1141.2404374322139,"y":732.3190922346248},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph19","sbgnbbox":{"x":893.1427762830865,"y":856.2695126662625,"w":"120.0","h":"60.0"},"sbgnclass":"macromolecule","sbgnlabel":"phospho fructokinase","sbgnstatesandinfos":[],"ports":[]},"position":{"x":893.1427762830865,"y":856.2695126662625},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph11","sbgnbbox":{"x":939.3335184518824,"y":758.3699048922733,"w":"20.0","h":"20.0"},"sbgnclass":"process","sbgnstatesandinfos":[],"ports":[]},"position":{"x":939.3335184518824,"y":758.3699048922733},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph18","sbgnbbox":{"x":770.4114528170364,"y":659.2220219290564,"w":"120.0","h":"60.0"},"sbgnclass":"macromolecule","sbgnlabel":"adolase","sbgnstatesandinfos":[],"ports":[]},"position":{"x":770.4114528170364,"y":659.2220219290564},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph16","sbgnbbox":{"x":818.0111009023315,"y":564.8072603606723,"w":"20.0","h":"20.0"},"sbgnclass":"process","sbgnstatesandinfos":[],"ports":[]},"position":{"x":818.0111009023315,"y":564.8072603606723},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph22","sbgnbbox":{"x":651.1292498357636,"y":314.1387423188818,"w":"120.0","h":"60.0"},"sbgnclass":"macromolecule","sbgnlabel":"GAPDH","sbgnstatesandinfos":[],"ports":[]},"position":{"x":651.1292498357636,"y":314.1387423188818},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph4","sbgnbbox":{"x":792.0076145303351,"y":454.0225025614517,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"GA-3P","sbgnstatesandinfos":[],"ports":[]},"position":{"x":792.0076145303351,"y":454.0225025614517},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph23","sbgnbbox":{"x":704.0937009722281,"y":398.0421081673902,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"Pi","sbgnstatesandinfos":[],"ports":[]},"position":{"x":704.0937009722281,"y":398.0421081673902},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph24","sbgnbbox":{"x":809.2974819306742,"y":231.7141323534711,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"NAD","sbgnstatesandinfos":[],"ports":[]},"position":{"x":809.2974819306742,"y":231.7141323534711},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph25","sbgnbbox":{"x":890.826951363933,"y":299.74915938409947,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"H+","sbgnstatesandinfos":[],"ports":[]},"position":{"x":890.826951363933,"y":299.74915938409947},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph20","sbgnbbox":{"x":786.2625869125006,"y":331.67766378118495,"w":"20.0","h":"20.0"},"sbgnclass":"process","sbgnstatesandinfos":[],"ports":[]},"position":{"x":786.2625869125006,"y":331.67766378118495},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph26","sbgnbbox":{"x":879.2981049664311,"y":389.27232563593486,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"NADH","sbgnstatesandinfos":[],"ports":[]},"position":{"x":879.2981049664311,"y":389.27232563593486},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph35","sbgnbbox":{"x":627.088268638501,"y":40.089848876876886,"w":"120.0","h":"60.0"},"sbgnclass":"macromolecule","sbgnlabel":"PGK1","sbgnstatesandinfos":[],"ports":[]},"position":{"x":627.088268638501,"y":40.089848876876886},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph36","sbgnbbox":{"x":329.6761506918384,"y":187.20503497360494,"w":"120.0","h":"60.0"},"sbgnclass":"macromolecule","sbgnlabel":"PG mutase","sbgnstatesandinfos":[],"ports":[]},"position":{"x":329.6761506918384,"y":187.20503497360494},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph37","sbgnbbox":{"x":155.12947729633356,"y":379.5263531900425,"w":"120.0","h":"60.0"},"sbgnclass":"macromolecule","sbgnlabel":"enolase","sbgnstatesandinfos":[],"ports":[]},"position":{"x":155.12947729633356,"y":379.5263531900425},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph38","sbgnbbox":{"x":70.13952165372024,"y":581.2691021233562,"w":"120.0","h":"60.0"},"sbgnclass":"macromolecule","sbgnlabel":"pyruvate kinase","sbgnstatesandinfos":[],"ports":[]},"position":{"x":70.13952165372024,"y":581.2691021233562},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph21","sbgnbbox":{"x":713.4639263718316,"y":229.06355211274115,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"1,3 BPG","sbgnstatesandinfos":[],"ports":[]},"position":{"x":713.4639263718316,"y":229.06355211274115},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph42","sbgnbbox":{"x":523.848994074475,"y":108.47701882803744,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"ADP","sbgnstatesandinfos":[],"sbgnclonemarker":true,"ports":[]},"position":{"x":523.848994074475,"y":108.47701882803744},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph41","sbgnbbox":{"x":718.966532806447,"y":116.46683749236911,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"ATP","sbgnstatesandinfos":[],"sbgnclonemarker":true,"ports":[]},"position":{"x":718.966532806447,"y":116.46683749236911},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph31","sbgnbbox":{"x":621.3138039842713,"y":145.7168752444793,"w":"20.0","h":"20.0"},"sbgnclass":"process","sbgnstatesandinfos":[],"ports":[]},"position":{"x":621.3138039842713,"y":145.7168752444793},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph27","sbgnbbox":{"x":525.2099120385327,"y":210.92542274858295,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"3 PG","sbgnstatesandinfos":[],"ports":[]},"position":{"x":525.2099120385327,"y":210.92542274858295},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph32","sbgnbbox":{"x":426.3492127437995,"y":257.85665030680025,"w":"20.0","h":"20.0"},"sbgnclass":"process","sbgnstatesandinfos":[],"ports":[]},"position":{"x":426.3492127437995,"y":257.85665030680025},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph28","sbgnbbox":{"x":346.30926488002945,"y":344.4562152937847,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"2 PG","sbgnstatesandinfos":[],"ports":[]},"position":{"x":346.30926488002945,"y":344.4562152937847},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph43","sbgnbbox":{"x":363.54724181648487,"y":486.5705174517715,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"H2O","sbgnstatesandinfos":[],"ports":[]},"position":{"x":363.54724181648487,"y":486.5705174517715},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph33","sbgnbbox":{"x":276.2797233955059,"y":435.0423711483709,"w":"20.0","h":"20.0"},"sbgnclass":"process","sbgnstatesandinfos":[],"ports":[]},"position":{"x":269.87972487503066,"y":430.2423722580144},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph29","sbgnbbox":{"x":227.86139816113416,"y":531.824141876398,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"PEP","sbgnstatesandinfos":[],"ports":[]},"position":{"x":227.86139816113416,"y":531.824141876398},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph39","sbgnbbox":{"x":104.77693104995387,"y":691.8382969303054,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"ADP","sbgnstatesandinfos":[],"sbgnclonemarker":true,"ports":[]},"position":{"x":104.77693104995387,"y":691.8382969303054},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph40","sbgnbbox":{"x":292.039416141131,"y":643.4009391289965,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"ATP","sbgnstatesandinfos":[],"sbgnclonemarker":true,"ports":[]},"position":{"x":292.039416141131,"y":643.4009391289965},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph34","sbgnbbox":{"x":193.8304385062596,"y":632.9540034207419,"w":"20.0","h":"20.0"},"sbgnclass":"process","sbgnstatesandinfos":[],"ports":[]},"position":{"x":193.8304385062596,"y":632.9540034207419},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph30","sbgnbbox":{"x":205.4745704273754,"y":733.5181650652648,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"pyruvate","sbgnstatesandinfos":[],"ports":[]},"position":{"x":205.4745704273754,"y":733.5181650652648},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph14","sbgnbbox":{"x":695.1248473196924,"y":482.8828321494848,"w":"20.0","h":"20.0"},"sbgnclass":"process","sbgnstatesandinfos":[],"ports":[]},"position":{"x":695.1248473196924,"y":482.8828321494848},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"glyph5","sbgnbbox":{"x":721.6687687330186,"y":570.3868893775194,"w":"60.0","h":"60.0"},"sbgnclass":"simple chemical","sbgnlabel":"DHA-P","sbgnstatesandinfos":[],"ports":[]},"position":{"x":721.6687687330186,"y":570.3868893775194},"group":"nodes","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e22","sbgnclass":"catalysis","sbgncardinality":0,"source":"glyph9","target":"glyph8","portsource":"glyph9","porttarget":"glyph8"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e23","sbgnclass":"consumption","sbgncardinality":0,"source":"glyph0","target":"glyph8","portsource":"glyph0","porttarget":"glyph8"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e24","sbgnclass":"production","sbgncardinality":0,"source":"glyph8","target":"glyph1","portsource":"glyph8","porttarget":"glyph1"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e25","sbgnclass":"consumption","sbgncardinality":0,"source":"glyph6","target":"glyph8","portsource":"glyph6","porttarget":"glyph8"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e26","sbgnclass":"production","sbgncardinality":0,"source":"glyph8","target":"glyph7","portsource":"glyph8","porttarget":"glyph7"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e27","sbgnclass":"production","sbgncardinality":0,"source":"glyph11","target":"glyph12","portsource":"glyph11","porttarget":"glyph12"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e28","sbgnclass":"consumption","sbgncardinality":0,"source":"glyph13","target":"glyph11","portsource":"glyph13","porttarget":"glyph11"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e29","sbgnclass":"consumption","sbgncardinality":0,"source":"glyph1","target":"glyph10","portsource":"glyph1","porttarget":"glyph10"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e30","sbgnclass":"production","sbgncardinality":0,"source":"glyph10","target":"glyph2","portsource":"glyph10","porttarget":"glyph2"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e31","sbgnclass":"consumption","sbgncardinality":0,"source":"glyph2","target":"glyph11","portsource":"glyph2","porttarget":"glyph11"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e32","sbgnclass":"production","sbgncardinality":0,"source":"glyph11","target":"glyph3","portsource":"glyph11","porttarget":"glyph3"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e33","sbgnclass":"production","sbgncardinality":0,"source":"glyph14","target":"glyph4","portsource":"glyph14","porttarget":"glyph4"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e34","sbgnclass":"catalysis","sbgncardinality":0,"source":"glyph15","target":"glyph14","portsource":"glyph15","porttarget":"glyph14"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e35","sbgnclass":"consumption","sbgncardinality":0,"source":"glyph3","target":"glyph16","portsource":"glyph3","porttarget":"glyph16"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e36","sbgnclass":"production","sbgncardinality":0,"source":"glyph16","target":"glyph5","portsource":"glyph16","porttarget":"glyph5"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e37","sbgnclass":"production","sbgncardinality":0,"source":"glyph16","target":"glyph4","portsource":"glyph16","porttarget":"glyph4"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e38","sbgnclass":"catalysis","sbgncardinality":0,"source":"glyph17","target":"glyph10","portsource":"glyph17","porttarget":"glyph10"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e39","sbgnclass":"catalysis","sbgncardinality":0,"source":"glyph19","target":"glyph11","portsource":"glyph19","porttarget":"glyph11"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e40","sbgnclass":"catalysis","sbgncardinality":0,"source":"glyph18","target":"glyph16","portsource":"glyph18","porttarget":"glyph16"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e41","sbgnclass":"catalysis","sbgncardinality":0,"source":"glyph22","target":"glyph20","portsource":"glyph22","porttarget":"glyph20"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e42","sbgnclass":"consumption","sbgncardinality":0,"source":"glyph4","target":"glyph20","portsource":"glyph4","porttarget":"glyph20"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e43","sbgnclass":"production","sbgncardinality":0,"source":"glyph20","target":"glyph21","portsource":"glyph20","porttarget":"glyph21"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e44","sbgnclass":"consumption","sbgncardinality":0,"source":"glyph23","target":"glyph20","portsource":"glyph23","porttarget":"glyph20"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e45","sbgnclass":"consumption","sbgncardinality":0,"source":"glyph24","target":"glyph20","portsource":"glyph24","porttarget":"glyph20"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e46","sbgnclass":"production","sbgncardinality":0,"source":"glyph20","target":"glyph25","portsource":"glyph20","porttarget":"glyph25"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e47","sbgnclass":"production","sbgncardinality":0,"source":"glyph20","target":"glyph26","portsource":"glyph20","porttarget":"glyph26"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e48","sbgnclass":"catalysis","sbgncardinality":0,"source":"glyph35","target":"glyph31","portsource":"glyph35","porttarget":"glyph31"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e49","sbgnclass":"catalysis","sbgncardinality":0,"source":"glyph36","target":"glyph32","portsource":"glyph36","porttarget":"glyph32"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e50","sbgnclass":"catalysis","sbgncardinality":0,"source":"glyph37","target":"glyph33","portsource":"glyph37","porttarget":"glyph33"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e51","sbgnclass":"catalysis","sbgncardinality":0,"source":"glyph38","target":"glyph34","portsource":"glyph38","porttarget":"glyph34"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e52","sbgnclass":"consumption","sbgncardinality":0,"source":"glyph21","target":"glyph31","portsource":"glyph21","porttarget":"glyph31"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e53","sbgnclass":"consumption","sbgncardinality":0,"source":"glyph42","target":"glyph31","portsource":"glyph42","porttarget":"glyph31"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e54","sbgnclass":"production","sbgncardinality":0,"source":"glyph31","target":"glyph41","portsource":"glyph31","porttarget":"glyph41"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e55","sbgnclass":"production","sbgncardinality":0,"source":"glyph31","target":"glyph27","portsource":"glyph31","porttarget":"glyph27"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e56","sbgnclass":"consumption","sbgncardinality":0,"source":"glyph27","target":"glyph32","portsource":"glyph27","porttarget":"glyph32"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e57","sbgnclass":"production","sbgncardinality":0,"source":"glyph32","target":"glyph28","portsource":"glyph32","porttarget":"glyph28"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e58","sbgnclass":"consumption","sbgncardinality":0,"source":"glyph28","target":"glyph33","portsource":"glyph28","porttarget":"glyph33"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e59","sbgnclass":"production","sbgncardinality":0,"source":"glyph33","target":"glyph43","portsource":"glyph33","porttarget":"glyph43"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e60","sbgnclass":"production","sbgncardinality":0,"source":"glyph33","target":"glyph29","portsource":"glyph33","porttarget":"glyph29"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e61","sbgnclass":"consumption","sbgncardinality":0,"source":"glyph29","target":"glyph34","portsource":"glyph29","porttarget":"glyph34"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e62","sbgnclass":"consumption","sbgncardinality":0,"source":"glyph39","target":"glyph34","portsource":"glyph39","porttarget":"glyph34"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e63","sbgnclass":"production","sbgncardinality":0,"source":"glyph34","target":"glyph40","portsource":"glyph34","porttarget":"glyph40"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e64","sbgnclass":"production","sbgncardinality":0,"source":"glyph34","target":"glyph30","portsource":"glyph34","porttarget":"glyph30"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""},{"data":{"id":"e65","sbgnclass":"production","sbgncardinality":0,"source":"glyph14","target":"glyph5","portsource":"glyph14","porttarget":"glyph5"},"position":{},"group":"edges","removed":false,"selected":false,"selectable":true,"locked":false,"grabbed":false,"grabbable":true,"classes":""}, diff --git a/src/fcose/auxiliary.js b/src/fcose/auxiliary.js index a9db66a..af6d873 100644 --- a/src/fcose/auxiliary.js +++ b/src/fcose/auxiliary.js @@ -1,7 +1,9 @@ /* - * Auxiliary functions used in spectral layout (especially in power iteration) + * Auxiliary functions */ +const LinkedList = require('cose-base').layoutBase.LinkedList; + let auxiliary = {}; auxiliary.multMat = function(array1, array2){ @@ -115,4 +117,181 @@ auxiliary.normalize = function(array){ return result; }; +// get the top most nodes +auxiliary.getTopMostNodes = function(nodes) { + let nodesMap = {}; + for (let i = 0; i < nodes.length; i++) { + nodesMap[nodes[i].id()] = true; + } + let roots = nodes.filter(function (ele, i) { + if(typeof ele === "number") { + ele = i; + } + let parent = ele.parent()[0]; + while(parent != null){ + if(nodesMap[parent.id()]){ + return false; + } + parent = parent.parent()[0]; + } + return true; + }); + + return roots; +}; + +// find disconnected components and create dummy nodes that connect them +auxiliary.connectComponents = function(cy, eles, topMostNodes, dummyNodes){ + let queue = new LinkedList(); + let visited = new Set(); + let visitedTopMostNodes = []; + let currentNeighbor; + let minDegreeNode; + let minDegree; + + let isConnected = false; + let count = 1; + let nodesConnectedToDummy = []; + let components = []; + + do{ + let cmpt = cy.collection(); + components.push(cmpt); + + let currentNode = topMostNodes[0]; + let childrenOfCurrentNode = cy.collection(); + childrenOfCurrentNode.merge(currentNode).merge(currentNode.descendants()); + visitedTopMostNodes.push(currentNode); + + childrenOfCurrentNode.forEach(function(node) { + queue.push(node); + visited.add(node); + cmpt.merge(node); + }); + + while(queue.length != 0){ + currentNode = queue.shift(); + + // Traverse all neighbors of this node + let neighborNodes = cy.collection(); + currentNode.neighborhood().nodes().forEach(function(node){ + if(eles.contains(currentNode.edgesWith(node))){ + neighborNodes.merge(node); + } + }); + + for(let i = 0; i < neighborNodes.length; i++){ + let neighborNode = neighborNodes[i]; + currentNeighbor = topMostNodes.intersection(neighborNode.union(neighborNode.ancestors())); + if(currentNeighbor != null && !visited.has(currentNeighbor[0])){ + let childrenOfNeighbor = currentNeighbor.union(currentNeighbor.descendants()); + + childrenOfNeighbor.forEach(function(node){ + queue.push(node); + visited.add(node); + cmpt.merge(node); + if(topMostNodes.has(node)){ + visitedTopMostNodes.push(node); + } + }); + + } + } + } + + cmpt.forEach(node => { + node.connectedEdges().forEach(e => { // connectedEdges() usually cached + if( cmpt.has(e.source()) && cmpt.has(e.target()) ){ // has() is cheap + cmpt.merge(e); // forEach() only considers nodes -- sets N at call time + } + }); + }); + + if(visitedTopMostNodes.length == topMostNodes.length){ + isConnected = true; + } + + if(!isConnected || (isConnected && count > 1)){ + minDegreeNode = visitedTopMostNodes[0]; + minDegree = minDegreeNode.connectedEdges().length; + visitedTopMostNodes.forEach(function(node){ + if(node.connectedEdges().length < minDegree){ + minDegree = node.connectedEdges().length; + minDegreeNode = node; + } + }); + nodesConnectedToDummy.push(minDegreeNode.id()); + // TO DO: Check efficiency of this part + let temp = cy.collection(); + temp.merge(visitedTopMostNodes[0]); + visitedTopMostNodes.forEach(function(node){ + temp.merge(node); + }); + visitedTopMostNodes = []; + topMostNodes = topMostNodes.difference(temp); + count++; + } + + } + while(!isConnected); + + if(dummyNodes){ + if(nodesConnectedToDummy.length > 0 ){ + dummyNodes.set('dummy'+(dummyNodes.size+1), nodesConnectedToDummy); + } + } + return components; +}; + +auxiliary.calcBoundingBox = function(parentNode, xCoords, yCoords, nodeIndexes){ + // calculate bounds + let left = Number.MAX_VALUE; + let right = Number.MIN_VALUE; + let top = Number.MAX_VALUE; + let bottom = Number.MIN_VALUE; + let nodeLeft; + let nodeRight; + let nodeTop; + let nodeBottom; + + let nodes = parentNode.descendants().not(":parent"); + let s = nodes.length; + for (let i = 0; i < s; i++) + { + let node = nodes[i]; + + nodeLeft = xCoords[nodeIndexes.get(node.id())] - node.width()/2; + nodeRight = xCoords[nodeIndexes.get(node.id())] + node.width()/2; + nodeTop = yCoords[nodeIndexes.get(node.id())] - node.height()/2; + nodeBottom = yCoords[nodeIndexes.get(node.id())] + node.height()/2; + + if (left > nodeLeft) + { + left = nodeLeft; + } + + if (right < nodeRight) + { + right = nodeRight; + } + + if (top > nodeTop) + { + top = nodeTop; + } + + if (bottom < nodeBottom) + { + bottom = nodeBottom; + } + } + + let boundingBox = {}; + boundingBox.topLeftX = left; + boundingBox.topLeftY = top; + boundingBox.width = right - left; + boundingBox.height = bottom - top; + return boundingBox; +}; + module.exports = auxiliary; \ No newline at end of file diff --git a/src/fcose/cose.js b/src/fcose/cose.js index 211cb74..69544e6 100644 --- a/src/fcose/cose.js +++ b/src/fcose/cose.js @@ -2,6 +2,7 @@ The implementation of the postprocessing part that applies CoSE layout over the spectral layout */ +const aux = require('./auxiliary'); const CoSELayout = require('cose-base').CoSELayout; const CoSENode = require('cose-base').CoSENode; const PointD = require('cose-base').layoutBase.PointD; @@ -22,7 +23,7 @@ let coseLayout = function(options, spectralResult){ let yCoords; let idToLNode = {}; - if(options.randomize && nodes.length > 2){ + if(options.randomize && spectralResult){ nodeIndexes = spectralResult["nodeIndexes"]; xCoords = spectralResult["xCoords"]; yCoords = spectralResult["yCoords"]; @@ -30,29 +31,6 @@ let coseLayout = function(options, spectralResult){ /**** Postprocessing functions ****/ - // get the top most nodes - let getTopMostNodes = function(nodes) { - let nodesMap = {}; - for (let i = 0; i < nodes.length; i++) { - nodesMap[nodes[i].id()] = true; - } - let roots = nodes.filter(function (ele, i) { - if(typeof ele === "number") { - ele = i; - } - let parent = ele.parent()[0]; - while(parent != null){ - if(nodesMap[parent.id()]){ - return false; - } - parent = parent.parent()[0]; - } - return true; - }); - - return roots; - }; - // transfer cytoscape nodes to cose nodes let processChildrenList = function (parent, children, layout, options) { let size = children.length; @@ -67,14 +45,14 @@ let coseLayout = function(options, spectralResult){ if (theChild.outerWidth() != null && theChild.outerHeight() != null) { - if(options.randomize && nodes.length > 2){ + if(options.randomize && spectralResult){ if(!theChild.isParent()){ theNode = parent.add(new CoSENode(layout.graphManager, new PointD(xCoords[nodeIndexes.get(theChild.id())] - dimensions.w / 2, yCoords[nodeIndexes.get(theChild.id())] - dimensions.h / 2), new DimensionD(parseFloat(dimensions.w), parseFloat(dimensions.h)))); } else{ - let parentInfo = calcBoundingBox(theChild); + let parentInfo = aux.calcBoundingBox(theChild, xCoords, yCoords, nodeIndexes); theNode = parent.add(new CoSENode(layout.graphManager, new PointD(parentInfo.topLeftX, parentInfo.topLeftY), new DimensionD(parentInfo.width, parentInfo.height))); @@ -126,56 +104,6 @@ let coseLayout = function(options, spectralResult){ processChildrenList(theNewGraph, children_of_children, layout, options); } } - function calcBoundingBox(parentNode){ - // calculate bounds - let left = Number.MAX_VALUE; - let right = Number.MIN_VALUE; - let top = Number.MAX_VALUE; - let bottom = Number.MIN_VALUE; - let nodeLeft; - let nodeRight; - let nodeTop; - let nodeBottom; - - let nodes = parentNode.descendants().not(":parent"); - let s = nodes.length; - for (let i = 0; i < s; i++) - { - let node = nodes[i]; - - nodeLeft = xCoords[nodeIndexes.get(node.id())] - node.width()/2; - nodeRight = xCoords[nodeIndexes.get(node.id())] + node.width()/2; - nodeTop = yCoords[nodeIndexes.get(node.id())] - node.height()/2; - nodeBottom = yCoords[nodeIndexes.get(node.id())] + node.height()/2; - - if (left > nodeLeft) - { - left = nodeLeft; - } - - if (right < nodeRight) - { - right = nodeRight; - } - - if (top > nodeTop) - { - top = nodeTop; - } - - if (bottom < nodeBottom) - { - bottom = nodeBottom; - } - } - - let boundingBox = {}; - boundingBox.topLeftX = left; - boundingBox.topLeftY = top; - boundingBox.width = right - left; - boundingBox.height = top - bottom; - return boundingBox; - } }; // transfer cytoscape edges to cose edges @@ -234,7 +162,7 @@ let coseLayout = function(options, spectralResult){ let coseLayout = new CoSELayout(); let gm = coseLayout.newGraphManager(); - processChildrenList(gm.addRoot(), getTopMostNodes(nodes), coseLayout, options); + processChildrenList(gm.addRoot(), aux.getTopMostNodes(nodes), coseLayout, options); processEdges(coseLayout, gm, edges); diff --git a/src/fcose/index.js b/src/fcose/index.js index c475b19..51c543c 100644 --- a/src/fcose/index.js +++ b/src/fcose/index.js @@ -3,8 +3,9 @@ */ const assign = require('../assign'); -const { spectralLayout }= require('./spectral'); -const { coseLayout }= require('./cose'); +const aux = require('./auxiliary'); +const { spectralLayout } = require('./spectral'); +const { coseLayout } = require('./cose'); const defaults = Object.freeze({ @@ -28,6 +29,8 @@ const defaults = Object.freeze({ padding: 10, // whether to include labels in node dimensions. Valid in "proof" quality nodeDimensionsIncludeLabels: false, + // whether to pack disconnected components - valid only if randomize: true + packComponents: true, /* spectral layout options */ @@ -55,7 +58,7 @@ const defaults = Object.freeze({ // Maximum number of iterations to perform numIter: 2500, // For enabling tiling - tile: false, + tile: true, // Represents the amount of the vertical space to put between the zero degree members during the tiling operation(can also be a function) tilingPaddingVertical: 10, // Represents the amount of the horizontal space to put between the zero degree members during the tiling operation(can also be a function) @@ -82,45 +85,206 @@ class Layout { run(){ let layout = this; let options = this.options; + let cy = options.cy; let eles = options.eles; - let spectralResult; + let spectralResult = []; let xCoords; let yCoords; - let coseResult; + let coseResult = []; + let components; + + let layUtil; + let packingEnabled = false; + if(cy.layoutUtilities && options.packComponents && options.randomize){ + layUtil = cy.layoutUtilities("get"); + if(!layUtil) + layUtil = cy.layoutUtilities(); + packingEnabled = true; + } + + if(options.eles.length == 0) + return; + + if(options.eles.length != options.cy.elements().length){ + let prevNodes = eles.nodes(); + eles = eles.union(eles.descendants()); + + eles.forEach(function(ele){ + if(ele.isNode()){ + let connectedEdges = ele.connectedEdges(); + connectedEdges.forEach(function(edge){ + if(eles.contains(edge.source()) && eles.contains(edge.target()) && !prevNodes.contains(edge.source().union(edge.target()))){ + eles = eles.union(edge); + } + }); + } + }); + + options.eles = eles; + } + + if(packingEnabled){ + let topMostNodes = aux.getTopMostNodes(options.eles.nodes()); + components = aux.connectComponents(cy, options.eles, topMostNodes); + } + + if(options.randomize){ + if(packingEnabled){ + components.forEach(function(component){ + options.eles = component; + spectralResult.push(spectralLayout(options)); + }); + } + else{ + // Apply spectral layout + spectralResult.push(spectralLayout(options)); + if(spectralResult[0]){ + xCoords = spectralResult[0]["xCoords"]; + yCoords = spectralResult[0]["yCoords"]; + } + } + } - // If number of nodes is 1 or 2, either SVD or powerIteration causes problem - // So direct the graph to cose layout - if(options.randomize && eles.nodes().length > 2){ - // Apply spectral layout - spectralResult = spectralLayout(options); - xCoords = spectralResult["xCoords"]; - yCoords = spectralResult["yCoords"]; + if(options.quality == "default" || options.quality == "proof" || spectralResult.includes(false)){ + if(packingEnabled){ + if(options.quality == "draft" && spectralResult.includes(false)){ + spectralResult.forEach(function(value, index){ + if(!value){ + options.eles = components[index]; + let tempResult = coseLayout(options, spectralResult[index]); + let nodeIndexes = new Map(); + let xCoords = []; + let yCoords = []; + let count = 0; + Object.keys(tempResult).forEach(function (item) { + nodeIndexes.set(item, count++); + xCoords.push(tempResult[item].getCenterX()); + yCoords.push(tempResult[item].getCenterY()); + }); + spectralResult[index] = {nodeIndexes: nodeIndexes, xCoords: xCoords, yCoords: yCoords}; + } + }); + } + else{ + let toBeTiledNodes = cy.collection(); + if(options.tile){ + let nodeIndexes = new Map(); + let xCoords = []; + let yCoords = []; + let count = 0; + let tempSpectralResult = {nodeIndexes: nodeIndexes, xCoords: xCoords, yCoords: yCoords}; + let indexesToBeDeleted = []; + components.forEach(function(component, index){ + if(component.edges().length == 0){ + component.nodes().forEach(function(node, i){ + toBeTiledNodes.merge(component.nodes()[i]); + if(!node.isParent()){ + tempSpectralResult.nodeIndexes.set(component.nodes()[i].id(), count++); + tempSpectralResult.xCoords.push(component.nodes()[0].position().x); + tempSpectralResult.yCoords.push(component.nodes()[0].position().y); + } + }); + indexesToBeDeleted.push(index); + } + }); + if(toBeTiledNodes.length > 1){ + components.push(toBeTiledNodes); + for(let i = indexesToBeDeleted.length-1; i >= 0; i--){ + components.splice(indexesToBeDeleted[i], 1); + spectralResult.splice(indexesToBeDeleted[i], 1); + }; + spectralResult.push(tempSpectralResult); + } + } + components.forEach(function(component, index){ + options.eles = component; + coseResult.push(coseLayout(options, spectralResult[index])); + }); + } + } + else{ + // Apply cose layout as postprocessing + coseResult.push(coseLayout(options, spectralResult[0])); + } } - if(options.quality == "default" || options.quality == "proof" || eles.nodes().length <= 2){ - // Apply cose layout as postprocessing - coseResult = coseLayout(options, spectralResult); + if(packingEnabled){ + let subgraphs = []; + components.forEach(function(component, index){ + let nodeIndexes; + if(options.quality == "draft"){ + nodeIndexes = spectralResult[index].nodeIndexes; + } + let subgraph = {}; + subgraph.nodes = []; + subgraph.edges = []; + let nodeIndex; + component.nodes().forEach(function (node) { + if(options.quality == "draft"){ + if(!node.isParent()){ + nodeIndex = nodeIndexes.get(node.id()); + subgraph.nodes.push({x: spectralResult[index].xCoords[nodeIndex] - node.bb().w/2, y: spectralResult[index].yCoords[nodeIndex] - node.bb().h/2, width: node.bb().w, height: node.bb().h}); + } + else{ + let parentInfo = aux.calcBoundingBox(node, spectralResult[index].xCoords, spectralResult[index].yCoords, nodeIndexes); + subgraph.nodes.push({x: parentInfo.topLeftX, y: parentInfo.topLeftY, width: parentInfo.width, height: parentInfo.height}); + } + } + else{ + subgraph.nodes.push({x: coseResult[index][node.id()].getLeft(), y: coseResult[index][node.id()].getTop(), width: coseResult[index][node.id()].getWidth(), height: coseResult[index][node.id()].getHeight()}); + } + }); + subgraphs.push(subgraph); + }); + let shiftResult = layUtil.packComponents(subgraphs).shifts; + if(options.quality == "draft"){ + spectralResult.forEach(function(result, index){ + let newXCoords = result.xCoords.map(x => x + shiftResult[index].dx); + let newYCoords = result.yCoords.map(y => y + shiftResult[index].dy); + result.xCoords = newXCoords; + result.yCoords = newYCoords; + }); + } + else{ + coseResult.forEach(function(result, index){ + Object.keys(result).forEach(function (item) { + let nodeRectangle = result[item]; + nodeRectangle.setCenter(nodeRectangle.getCenterX() + shiftResult[index].dx, nodeRectangle.getCenterY() + shiftResult[index].dy); + }); + }); + } } // get each element's calculated position let getPositions = function(ele, i ){ - if(options.quality == "default" || options.quality == "proof") { + if(options.quality == "default" || options.quality == "proof" || (options.quality == "proof" && !packingEnabled && spectralResult.includes(false))) { if(typeof ele === "number") { ele = i; } + let pos; let theId = ele.data('id'); - let lNode = coseResult[theId]; - + coseResult.forEach(function(result){ + if (theId in result){ + pos = {x: result[theId].getRect().getCenterX(), y: result[theId].getRect().getCenterY()}; + } + }); return { - x: lNode.getRect().getCenterX(), - y: lNode.getRect().getCenterY() + x: pos.x, + y: pos.y }; } else{ + let pos; + spectralResult.forEach(function(result){ + let index = result.nodeIndexes.get(ele.id()); + if(index != undefined){ + pos = {x: result.xCoords[index], y: result.yCoords[index]}; + }; + }); return { - x: xCoords[i], - y: yCoords[i] + x: pos.x, + y: pos.y }; } }; @@ -128,6 +292,7 @@ class Layout { // quality = "draft" and randomize = false are contradictive so in that case positions don't change if((options.quality == "default" || options.quality == "proof") || options.randomize) { // transfer calculated positions to nodes (positions of only simple nodes are evaluated, compounds are positioned automatically) + options.eles = eles; eles.nodes().not(":parent").layoutPositions(layout, options, getPositions); } else{ diff --git a/src/fcose/spectral.js b/src/fcose/spectral.js index f75eac2..0895142 100644 --- a/src/fcose/spectral.js +++ b/src/fcose/spectral.js @@ -4,15 +4,15 @@ const aux = require('./auxiliary'); const numeric = require('numeric'); -const LinkedList = require('cose-base').layoutBase.LinkedList; // main function that spectral layout is processed let spectralLayout = function(options){ let cy = options.cy; let eles = options.eles; - let nodes = eles.nodes(); - + let nodes = eles.nodes(); + let parentNodes = eles.nodes(":parent"); + let dummyNodes = new Map(); // map to keep dummy nodes and their neighbors let nodeIndexes = new Map(); // map to keep indexes to nodes let parentChildMap = new Map(); // mapping btw. compound and its representative node @@ -39,107 +39,6 @@ let spectralLayout = function(options){ /**** Spectral-preprocessing functions ****/ - // get the top most nodes - let getTopMostNodes = function(nodes) { - let nodesMap = {}; - for (let i = 0; i < nodes.length; i++) { - nodesMap[nodes[i].id()] = true; - } - let roots = nodes.filter(function (ele, i) { - if(typeof ele === "number") { - ele = i; - } - let parent = ele.parent()[0]; - while(parent != null){ - if(nodesMap[parent.id()]){ - return false; - } - parent = parent.parent()[0]; - } - return true; - }); - - return roots; - }; - - // find disconnected components and create dummy nodes that connect them - let connectComponents = function(topMostNodes){ - let queue = new LinkedList(); - let visited = new Set(); - let visitedTopMostNodes = []; - let currentNeighbor; - let minDegreeNode; - let minDegree; - - let isConnected = false; - let count = 1; - let nodesConnectedToDummy = []; - - do{ - let currentNode = topMostNodes[0]; - let childrenOfCurrentNode = currentNode.union(currentNode.descendants()); - visitedTopMostNodes.push(currentNode); - - childrenOfCurrentNode.forEach(function(node) { - queue.push(node); - visited.add(node); - }); - - while(queue.length != 0){ - currentNode = queue.shift(); - - // Traverse all neighbors of this node - let neighborNodes = currentNode.neighborhood().nodes(); - for(let i = 0; i < neighborNodes.length; i++){ - let neighborNode = neighborNodes[i]; - currentNeighbor = topMostNodes.intersection(neighborNode.union(neighborNode.ancestors())); - if(currentNeighbor != null && !visited.has(currentNeighbor[0])){ - let childrenOfNeighbor = currentNeighbor.union(currentNeighbor.descendants()); - - childrenOfNeighbor.forEach(function(node){ - queue.push(node); - visited.add(node); - if(topMostNodes.has(node)){ - visitedTopMostNodes.push(node); - } - }); - - } - } - } - - if(visitedTopMostNodes.length == topMostNodes.length){ - isConnected = true; - } - - if(!isConnected || (isConnected && count > 1)){ - minDegreeNode = visitedTopMostNodes[0]; - minDegree = minDegreeNode.connectedEdges().length; - visitedTopMostNodes.forEach(function(node){ - if(node.connectedEdges().length < minDegree){ - minDegree = node.connectedEdges().length; - minDegreeNode = node; - } - }); - nodesConnectedToDummy.push(minDegreeNode.id()); - // TO DO: Check efficiency of this part - let temp = visitedTopMostNodes[0]; - visitedTopMostNodes.forEach(function(node){ - temp = temp.union(node); - }); - visitedTopMostNodes = []; - topMostNodes = topMostNodes.difference(temp); - count++; - } - - } - while(!isConnected); - - if(nodesConnectedToDummy.length > 0 ){ - dummyNodes.set('dummy'+(dummyNodes.size+1), nodesConnectedToDummy); - } - }; - /**** Spectral layout functions ****/ // determine which columns to be sampled @@ -390,10 +289,10 @@ let spectralLayout = function(options){ /**** Preparation for spectral layout (Preprocessing) ****/ // connect disconnected components (first top level, then inside of each compound node) - connectComponents(getTopMostNodes(nodes)); + aux.connectComponents(cy, eles, aux.getTopMostNodes(nodes), dummyNodes); - cy.nodes(":parent").forEach(function( ele ){ - connectComponents(getTopMostNodes(ele.descendants())); + parentNodes.forEach(function( ele ){ + aux.connectComponents(cy, eles, aux.getTopMostNodes(ele.descendants()), dummyNodes); }); // assign indexes to nodes (first real, then dummy nodes) @@ -414,29 +313,29 @@ let spectralLayout = function(options){ } // form a parent-child map to keep representative node of each compound node - cy.nodes(":parent").forEach(function( ele ){ - let children = ele.children(); + parentNodes.forEach(function( ele ){ + let children = ele.children(); -// let random = 0; - while(children.nodes(":childless").length == 0){ -// random = Math.floor(Math.random() * children.nodes().length); // if all children are compound then proceed randomly - children = children.nodes()[0].children(); - } - // select the representative node - we can apply different methods here -// random = Math.floor(Math.random() * children.nodes(":childless").length); - let index = 0; - let min = children.nodes(":childless")[0].connectedEdges().length; - children.nodes(":childless").forEach(function(ele2, i){ - if(ele2.connectedEdges().length < min){ - min = ele2.connectedEdges().length; - index = i; + // let random = 0; + while(children.nodes(":childless").length == 0){ + // random = Math.floor(Math.random() * children.nodes().length); // if all children are compound then proceed randomly + children = children.nodes()[0].children(); } - }); - parentChildMap.set(ele.id(), children.nodes(":childless")[index].id()); + // select the representative node - we can apply different methods here + // random = Math.floor(Math.random() * children.nodes(":childless").length); + let index = 0; + let min = children.nodes(":childless")[0].connectedEdges().length; + children.nodes(":childless").forEach(function(ele2, i){ + if(ele2.connectedEdges().length < min){ + min = ele2.connectedEdges().length; + index = i; + } + }); + parentChildMap.set(ele.id(), children.nodes(":childless")[index].id()); }); // add neighborhood relations (first real, then dummy nodes) - cy.nodes().forEach(function( ele ){ + nodes.forEach(function( ele ){ let eleIndex; if(ele.isParent()) @@ -445,10 +344,12 @@ let spectralLayout = function(options){ eleIndex = nodeIndexes.get(ele.id()); ele.neighborhood().nodes().forEach(function(node){ - if(node.isParent()) - allNodesNeighborhood[eleIndex].push(parentChildMap.get(node.id())); - else - allNodesNeighborhood[eleIndex].push(node.id()); + if(eles.contains(ele.edgesWith(node))){ + if(node.isParent()) + allNodesNeighborhood[eleIndex].push(parentChildMap.get(node.id())); + else + allNodesNeighborhood[eleIndex].push(node.id()); + } }); }); @@ -468,26 +369,37 @@ let spectralLayout = function(options){ // nodeSize now only considers the size of transformed graph nodeSize = nodeIndexes.size; - // if # of nodes in transformed graph is smaller than sample size, - // then use # of nodes as sample size - sampleSize = nodeSize < options.sampleSize ? nodeSize : options.sampleSize; - - // instantiates the partial matrices that will be used in spectral layout - for(let i = 0; i < nodeSize; i++){ - C[i] = []; - } - for(let i = 0; i < sampleSize; i++){ - INV[i] = []; - } + + let spectralResult; + + // If number of nodes in transformed graph is 1 or 2, either SVD or powerIteration causes problem + // So skip spectral and layout the graph with cose + if(nodeSize > 2) { + // if # of nodes in transformed graph is smaller than sample size, + // then use # of nodes as sample size + sampleSize = nodeSize < options.sampleSize ? nodeSize : options.sampleSize; + + // instantiates the partial matrices that will be used in spectral layout + for(let i = 0; i < nodeSize; i++){ + C[i] = []; + } + for(let i = 0; i < sampleSize; i++){ + INV[i] = []; + } - /**** Apply spectral layout ****/ + /**** Apply spectral layout ****/ - allBFS(samplingType); - sample(); - powerIteration(); + allBFS(samplingType); + sample(); + powerIteration(); - let spectralResult = { nodeIndexes: nodeIndexes, xCoords: xCoords, yCoords: yCoords }; - return spectralResult; + spectralResult = { nodeIndexes: nodeIndexes, xCoords: xCoords, yCoords: yCoords }; + return spectralResult; + } + else { + spectralResult = false; + return spectralResult; + } }; module.exports = { spectralLayout }; \ No newline at end of file