From 75942e3fb6a19a822b328f30dec6f0c4d49bbe3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hasan=20Balc=C4=B1?= Date: Wed, 2 Oct 2019 10:14:30 +0300 Subject: [PATCH] Add functionality for packing disconnected components #6 --- README.md | 3 + cytoscape-fcose.js | 782 +++++++++++++++++++++++------------------ demo-compound.html | 16 +- src/fcose/auxiliary.js | 181 +++++++++- src/fcose/cose.js | 78 +--- src/fcose/index.js | 156 ++++++-- src/fcose/spectral.js | 114 +----- 7 files changed, 781 insertions(+), 549 deletions(-) diff --git a/README.md b/README.md index f1121f6..2ba901f 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.1 ## Usage instructions @@ -93,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 */ diff --git a/cytoscape-fcose.js b/cytoscape-fcose.js index 7c7faab..c588430 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 */ @@ -185,12 +495,22 @@ 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; + 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; @@ -212,37 +532,134 @@ var Layout = function () { options.eles = eles; } + if (packingEnabled) { + var topMostNodes = aux.getTopMostNodes(options.eles.nodes()); + components = aux.connectComponents(cy, options.eles, topMostNodes); + } + if (options.randomize) { - // Apply spectral layout - spectralResult = spectralLayout(options); - if (spectralLayout) { - xCoords = spectralResult["xCoords"]; - yCoords = spectralResult["yCoords"]; + 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 { + 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" || !spectralResult) { - // 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" || !spectralResult) { + 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 }; } }; @@ -250,6 +667,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'."); @@ -263,7 +681,7 @@ var Layout = function () { module.exports = Layout; /***/ }), -/* 2 */ +/* 3 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -285,132 +703,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__) { @@ -422,6 +714,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; @@ -450,29 +743,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; @@ -490,7 +760,7 @@ var coseLayout = function coseLayout(options, 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 { @@ -536,51 +806,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 @@ -623,7 +848,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); @@ -645,9 +870,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) { @@ -683,120 +907,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 = []; - - var _loop = function _loop() { - 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); - }); - - 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); - if (topMostNodes.has(node)) { - visitedTopMostNodes.push(node); - } - }); - } - } - }; - - while (queue.length != 0) { - _loop2(); - } - - 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 (nodesConnectedToDummy.length > 0) { - dummyNodes.set('dummy' + (dummyNodes.size + 1), nodesConnectedToDummy); - } - }; - /**** Spectral layout functions ****/ // determine which columns to be sampled @@ -1040,10 +1150,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); parentNodes.forEach(function (ele) { - connectComponents(getTopMostNodes(ele.descendants())); + aux.connectComponents(cy, eles, aux.getTopMostNodes(ele.descendants()), dummyNodes); }); // assign indexes to nodes (first real, then dummy nodes) @@ -1120,7 +1230,7 @@ var spectralLayout = function spectralLayout(options) { }); }); - var _loop3 = function _loop3(_key) { + var _loop = function _loop(_key) { var eleIndex = nodeIndexes.get(_key); var disconnectedId = void 0; dummyNodes.get(_key).forEach(function (id) { @@ -1139,7 +1249,7 @@ var spectralLayout = function spectralLayout(options) { for (var _iterator2 = dummyNodes.keys()[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { var _key = _step2.value; - _loop3(_key); + _loop(_key); } // nodeSize now only considers the size of transformed graph @@ -1200,7 +1310,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 7be1f28..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 @@ -139,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'} }, @@ -175,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'} } ] }); @@ -196,6 +203,7 @@ quality: 'default', randomize: true, animationEasing: 'ease-out', + packComponents: document.getElementById("packing").checked }); layout.run(); @@ -209,8 +217,8 @@

cytoscape-fcose demo

    - - +     +
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 fbb8432..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; @@ -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; @@ -74,7 +52,7 @@ let coseLayout = function(options, spectralResult){ 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 8f0ccd2..b37b8d2 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 */ @@ -82,13 +85,24 @@ 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; @@ -109,39 +123,138 @@ class Layout { options.eles = eles; } - + + if(packingEnabled){ + let topMostNodes = aux.getTopMostNodes(options.eles.nodes()); + components = aux.connectComponents(cy, options.eles, topMostNodes); + } + if(options.randomize){ - // Apply spectral layout - spectralResult = spectralLayout(options); - if(spectralLayout){ - xCoords = spectralResult["xCoords"]; - yCoords = spectralResult["yCoords"]; + 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){ - // Apply cose layout as postprocessing - coseResult = coseLayout(options, spectralResult); + 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{ + 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(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" || !spectralResult) { + 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 }; } }; @@ -149,6 +262,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 ff31353..0895142 100644 --- a/src/fcose/spectral.js +++ b/src/fcose/spectral.js @@ -4,7 +4,6 @@ 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){ @@ -40,115 +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 = cy.collection(); - childrenOfCurrentNode.merge(currentNode).merge(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 = 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); - 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 = cy.collection(); - temp.merge(visitedTopMostNodes[0]); - visitedTopMostNodes.forEach(function(node){ - temp.merge(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 @@ -399,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); parentNodes.forEach(function( ele ){ - connectComponents(getTopMostNodes(ele.descendants())); + aux.connectComponents(cy, eles, aux.getTopMostNodes(ele.descendants()), dummyNodes); }); // assign indexes to nodes (first real, then dummy nodes)