diff --git a/lib/network/modules/LayoutEngine.js b/lib/network/modules/LayoutEngine.js index 57b9e092..3ce1645a 100644 --- a/lib/network/modules/LayoutEngine.js +++ b/lib/network/modules/LayoutEngine.js @@ -211,31 +211,33 @@ class LayoutEngine { setOptions(options, allOptions) { if (options !== undefined) { - let prevHierarchicalState = this.options.hierarchical.enabled; + let hierarchical = this.options.hierarchical; + let prevHierarchicalState = hierarchical.enabled; util.selectiveDeepExtend(["randomSeed", "improvedLayout"],this.options, options); util.mergeOptions(this.options, options, 'hierarchical'); if (options.randomSeed !== undefined) {this.initialRandomSeed = options.randomSeed;} - if (this.options.hierarchical.enabled === true) { + if (hierarchical.enabled === true) { if (prevHierarchicalState === true) { // refresh the overridden options for nodes and edges. this.body.emitter.emit('refresh', true); } // make sure the level separation is the right way up - if (this.options.hierarchical.direction === 'RL' || this.options.hierarchical.direction === 'DU') { - if (this.options.hierarchical.levelSeparation > 0) { - this.options.hierarchical.levelSeparation *= -1; + if (hierarchical.direction === 'RL' || hierarchical.direction === 'DU') { + if (hierarchical.levelSeparation > 0) { + hierarchical.levelSeparation *= -1; } } else { - if (this.options.hierarchical.levelSeparation < 0) { - this.options.hierarchical.levelSeparation *= -1; + if (hierarchical.levelSeparation < 0) { + hierarchical.levelSeparation *= -1; } } this.body.emitter.emit('_resetHierarchicalLayout'); - // because the hierarchical system needs it's own physics and smooth curve settings, we adapt the other options if needed. + // because the hierarchical system needs it's own physics and smooth curve settings, + // we adapt the other options if needed. return this.adaptAllOptionsForHierarchicalLayout(allOptions); } else { @@ -251,32 +253,36 @@ class LayoutEngine { adaptAllOptionsForHierarchicalLayout(allOptions) { if (this.options.hierarchical.enabled === true) { + let backupPhysics = this.optionsBackup.physics; + // set the physics if (allOptions.physics === undefined || allOptions.physics === true) { allOptions.physics = { - enabled:this.optionsBackup.physics.enabled === undefined ? true : this.optionsBackup.physics.enabled, - solver:'hierarchicalRepulsion' + enabled: backupPhysics.enabled === undefined ? true : backupPhysics.enabled, + solver :'hierarchicalRepulsion' }; - this.optionsBackup.physics.enabled = this.optionsBackup.physics.enabled === undefined ? true : this.optionsBackup.physics.enabled; - this.optionsBackup.physics.solver = this.optionsBackup.physics.solver || 'barnesHut'; + backupPhysics.enabled = backupPhysics.enabled === undefined ? true : backupPhysics.enabled; + backupPhysics.solver = backupPhysics.solver || 'barnesHut'; } else if (typeof allOptions.physics === 'object') { - this.optionsBackup.physics.enabled = allOptions.physics.enabled === undefined ? true : allOptions.physics.enabled; - this.optionsBackup.physics.solver = allOptions.physics.solver || 'barnesHut'; + backupPhysics.enabled = allOptions.physics.enabled === undefined ? true : allOptions.physics.enabled; + backupPhysics.solver = allOptions.physics.solver || 'barnesHut'; allOptions.physics.solver = 'hierarchicalRepulsion'; } else if (allOptions.physics !== false) { - this.optionsBackup.physics.solver ='barnesHut'; + backupPhysics.solver ='barnesHut'; allOptions.physics = {solver:'hierarchicalRepulsion'}; } // get the type of static smooth curve in case it is required + // NOTE: 'type' here is for the curve, so it will be orthogonal to the network direction let type = 'horizontal'; - if (this.options.hierarchical.direction === 'RL' || this.options.hierarchical.direction === 'LR') { - type = 'vertical'; + if (!this._isVertical()) { + type = 'vertical'; // sic } - // disable smooth curves if nothing is defined. If smooth curves have been turned on, turn them into static smooth curves. + // disable smooth curves if nothing is defined. If smooth curves have been turned on, + // turn them into static smooth curves. if (allOptions.edges === undefined) { this.optionsBackup.edges = {smooth:{enabled:true, type:'dynamic'}}; allOptions.edges = {smooth: false}; @@ -291,27 +297,34 @@ class LayoutEngine { allOptions.edges.smooth = {enabled: allOptions.edges.smooth, type:type} } else { + let smooth = allOptions.edges.smooth; + // allow custom types except for dynamic - if (allOptions.edges.smooth.type !== undefined && allOptions.edges.smooth.type !== 'dynamic') { - type = allOptions.edges.smooth.type; + if (smooth.type !== undefined && smooth.type !== 'dynamic') { + type = smooth.type; } + // TODO: this is options merging; see if the standard routines can be used here. this.optionsBackup.edges = { - smooth: allOptions.edges.smooth.enabled === undefined ? true : allOptions.edges.smooth.enabled, - type: allOptions.edges.smooth.type === undefined ? 'dynamic' : allOptions.edges.smooth.type, - roundness: allOptions.edges.smooth.roundness === undefined ? 0.5 : allOptions.edges.smooth.roundness, - forceDirection: allOptions.edges.smooth.forceDirection === undefined ? false : allOptions.edges.smooth.forceDirection + smooth : smooth.enabled === undefined ? true : smooth.enabled, + type : smooth.type === undefined ? 'dynamic': smooth.type, + roundness : smooth.roundness === undefined ? 0.5 : smooth.roundness, + forceDirection: smooth.forceDirection === undefined ? false : smooth.forceDirection }; + + + // NOTE: Copying an object to self; this is basically setting defaults for undefined variables allOptions.edges.smooth = { - enabled: allOptions.edges.smooth.enabled === undefined ? true : allOptions.edges.smooth.enabled, - type:type, - roundness: allOptions.edges.smooth.roundness === undefined ? 0.5 : allOptions.edges.smooth.roundness, - forceDirection: allOptions.edges.smooth.forceDirection === undefined ? false : allOptions.edges.smooth.forceDirection + enabled : smooth.enabled === undefined ? true : smooth.enabled, + type : type, + roundness : smooth.roundness === undefined ? 0.5 : smooth.roundness, + forceDirection: smooth.forceDirection === undefined ? false: smooth.forceDirection } } } - // force all edges into static smooth curves. Only applies to edges that do not use the global options for smooth. + // Force all edges into static smooth curves. + //Only applies to edges that do not use the global options for smooth. this.body.emitter.emit('_forceDisableDynamicCurves', type); } @@ -347,22 +360,25 @@ class LayoutEngine { */ layoutNetwork() { if (this.options.hierarchical.enabled !== true && this.options.improvedLayout === true) { + let indices = this.body.nodeIndices; + // first check if we should Kamada Kawai to layout. The threshold is if less than half of the visible // nodes have predefined positions we use this. let positionDefined = 0; - for (let i = 0; i < this.body.nodeIndices.length; i++) { - let node = this.body.nodes[this.body.nodeIndices[i]]; + for (let i = 0; i < indices.length; i++) { + let node = this.body.nodes[indices[i]]; if (node.predefinedPosition === true) { positionDefined += 1; } } // if less than half of the nodes have a predefined position we continue - if (positionDefined < 0.5 * this.body.nodeIndices.length) { + if (positionDefined < 0.5 * indices.length) { let MAX_LEVELS = 10; let level = 0; let clusterThreshold = 150; - //Performance enhancement, during clustering edges need only be simple straight lines. These options don't propagate outside the clustering phase. + // Performance enhancement, during clustering edges need only be simple straight lines. + // These options don't propagate outside the clustering phase. let clusterOptions = { clusterEdgeProperties:{ smooth: { @@ -372,12 +388,15 @@ class LayoutEngine { }; // if there are a lot of nodes, we cluster before we run the algorithm. - if (this.body.nodeIndices.length > clusterThreshold) { - let startLength = this.body.nodeIndices.length; - while (this.body.nodeIndices.length > clusterThreshold && level <= MAX_LEVELS) { + // NOTE: this part fails to find clusters for large scale-free networks, which should + // be easily clusterable. + // TODO: examine why this is so + if (indices.length > clusterThreshold) { + let startLength = indices.length; + while (indices.length > clusterThreshold && level <= MAX_LEVELS) { //console.time("clustering") level += 1; - let before = this.body.nodeIndices.length; + let before = indices.length; // if there are many nodes we do a hubsize cluster if (level % 3 === 0) { this.body.modules.clustering.clusterBridges(clusterOptions); @@ -385,11 +404,12 @@ class LayoutEngine { else { this.body.modules.clustering.clusterOutliers(clusterOptions); } - let after = this.body.nodeIndices.length; + let after = indices.length; if (before == after && level % 3 !== 0) { this._declusterAll(); this.body.emitter.emit("_layoutFailed"); - console.info("This network could not be positioned by this version of the improved layout algorithm. Please disable improvedLayout for better performance."); + console.info("This network could not be positioned by this version of the improved layout algorithm." + + " Please disable improvedLayout for better performance."); return; } //console.timeEnd("clustering") @@ -399,22 +419,24 @@ class LayoutEngine { this.body.modules.kamadaKawai.setOptions({springLength: Math.max(150, 2 * startLength)}) } if (level > MAX_LEVELS){ - console.info("The clustering didn't succeed within the amount of interations allowed, progressing with partial result."); + console.info("The clustering didn't succeed within the amount of interations allowed," + + " progressing with partial result."); } // position the system for these nodes and edges - this.body.modules.kamadaKawai.solve(this.body.nodeIndices, this.body.edgeIndices, true); + this.body.modules.kamadaKawai.solve(indices, this.body.edgeIndices, true); // shift to center point this._shiftToCenter(); // perturb the nodes a little bit to force the physics to kick in let offset = 70; - for (let i = 0; i < this.body.nodeIndices.length; i++) { + for (let i = 0; i < indices.length; i++) { // Only perturb the nodes that aren't fixed - if (this.body.nodes[this.body.nodeIndices[i]].predefinedPosition === false) { - this.body.nodes[this.body.nodeIndices[i]].x += (0.5 - this.seededRandom())*offset; - this.body.nodes[this.body.nodeIndices[i]].y += (0.5 - this.seededRandom())*offset; + let node = this.body.nodes[indices[i]]; + if (node.predefinedPosition === false) { + node.x += (0.5 - this.seededRandom())*offset; + node.y += (0.5 - this.seededRandom())*offset; } } @@ -435,8 +457,9 @@ class LayoutEngine { let range = NetworkUtil.getRangeCore(this.body.nodes, this.body.nodeIndices); let center = NetworkUtil.findCenter(range); for (let i = 0; i < this.body.nodeIndices.length; i++) { - this.body.nodes[this.body.nodeIndices[i]].x -= center.x; - this.body.nodes[this.body.nodeIndices[i]].y -= center.y; + let node = this.body.nodes[this.body.nodeIndices[i]]; + node.x -= center.x; + node.y -= center.y; } } @@ -500,18 +523,20 @@ class LayoutEngine { // if the user defined some levels but not all, alert and run without hierarchical layout if (undefinedLevel === true && definedLevel === true) { - throw new Error('To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.'); + throw new Error('To use the hierarchical layout, nodes require either no predefined levels' + + ' or levels have to be defined for all nodes.'); } else { // define levels if undefined by the users. Based on hubsize. if (undefinedLevel === true) { - if (this.options.hierarchical.sortMethod === 'hubsize') { + let sortMethod = this.options.hierarchical.sortMethod; + if (sortMethod === 'hubsize') { this._determineLevelsByHubsize(); } - else if (this.options.hierarchical.sortMethod === 'directed') { + else if (sortMethod === 'directed') { this._determineLevelsDirected(); } - else if (this.options.hierarchical.sortMethod === 'custom') { + else if (sortMethod === 'custom') { this._determineLevelsCustomCallback(); } } @@ -686,8 +711,9 @@ class LayoutEngine { let pos1 = this._getPositionForHierarchy(node1); let pos2 = this._getPositionForHierarchy(node2); let diffAbs = Math.abs(pos2 - pos1); - //console.log("NOW CHEcKING:", node1.id, node2.id, diffAbs); - if (diffAbs > this.options.hierarchical.nodeSpacing) { + let nodeSpacing = this.options.hierarchical.nodeSpacing; + //console.log("NOW CHECKING:", node1.id, node2.id, diffAbs); + if (diffAbs > nodeSpacing) { let branchNodes1 = {}; let branchNodes2 = {}; @@ -699,12 +725,13 @@ class LayoutEngine { let [min1,max1, minSpace1, maxSpace1] = getBranchBoundary(branchNodes1, maxLevel); let [min2,max2, minSpace2, maxSpace2] = getBranchBoundary(branchNodes2, maxLevel); - //console.log(node1.id, getBranchBoundary(branchNodes1, maxLevel), node2.id, getBranchBoundary(branchNodes2, maxLevel), maxLevel); + //console.log(node1.id, getBranchBoundary(branchNodes1, maxLevel), node2.id, + // getBranchBoundary(branchNodes2, maxLevel), maxLevel); let diffBranch = Math.abs(max1 - min2); - if (diffBranch > this.options.hierarchical.nodeSpacing) { - let offset = max1 - min2 + this.options.hierarchical.nodeSpacing; - if (offset < -minSpace2 + this.options.hierarchical.nodeSpacing) { - offset = -minSpace2 + this.options.hierarchical.nodeSpacing; + if (diffBranch > nodeSpacing) { + let offset = max1 - min2 + nodeSpacing; + if (offset < -minSpace2 + nodeSpacing) { + offset = -minSpace2 + nodeSpacing; //console.log("RESETTING OFFSET", max1 - min2 + this.options.hierarchical.nodeSpacing, -minSpace2, offset); } if (offset < 0) { @@ -939,18 +966,19 @@ class LayoutEngine { if (level !== undefined) { let index = this.distributionIndex[node.id]; let position = this._getPositionForHierarchy(node); + let ordering = this.distributionOrdering[level]; let minSpace = 1e9; let maxSpace = 1e9; if (index !== 0) { - let prevNode = this.distributionOrdering[level][index - 1]; + let prevNode = ordering[index - 1]; if ((useMap === true && map[prevNode.id] === undefined) || useMap === false) { let prevPos = this._getPositionForHierarchy(prevNode); minSpace = position - prevPos; } } - if (index != this.distributionOrdering[level].length - 1) { - let nextNode = this.distributionOrdering[level][index + 1]; + if (index != ordering.length - 1) { + let nextNode = ordering[index + 1]; if ((useMap === true && map[nextNode.id] === undefined) || useMap === false) { let nextPos = this._getPositionForHierarchy(nextNode); maxSpace = Math.min(maxSpace, nextPos - position); @@ -975,24 +1003,17 @@ class LayoutEngine { for (var i = 0; i < parents.length; i++) { let parentId = parents[i]; let parentNode = this.body.nodes[parentId]; - if (this.hierarchical.childrenReference[parentId]) { + let children = this.hierarchical.childrenReference[parentId]; + + if (children !== undefined) { // get the range of the children - let minPos = 1e9; - let maxPos = -1e9; - let children = this.hierarchical.childrenReference[parentId]; - if (children.length > 0) { - for (let i = 0; i < children.length; i++) { - let childNode = this.body.nodes[children[i]]; - minPos = Math.min(minPos, this._getPositionForHierarchy(childNode)); - maxPos = Math.max(maxPos, this._getPositionForHierarchy(childNode)); - } - } + let newPosition = this._getCenterPosition(children); let position = this._getPositionForHierarchy(parentNode); let [minSpace, maxSpace] = this._getSpaceAroundNode(parentNode); - let newPosition = 0.5 * (minPos + maxPos); let diff = position - newPosition; - if ((diff < 0 && Math.abs(diff) < maxSpace - this.options.hierarchical.nodeSpacing) || (diff > 0 && Math.abs(diff) < minSpace - this.options.hierarchical.nodeSpacing)) { + if ((diff < 0 && Math.abs(diff) < maxSpace - this.options.hierarchical.nodeSpacing) || + (diff > 0 && Math.abs(diff) < minSpace - this.options.hierarchical.nodeSpacing)) { this._setPositionForHierarchy(parentNode, newPosition, undefined, true); } } @@ -1022,9 +1043,13 @@ class LayoutEngine { for (let i = 0; i < nodeArray.length; i++) { let node = nodeArray[i]; if (this.positionedNodes[node.id] === undefined) { - let pos = this.options.hierarchical.nodeSpacing * handledNodeCount; - // we get the X or Y values we need and store them in pos and previousPos. The get and set make sure we get X or Y - if (handledNodeCount > 0) {pos = this._getPositionForHierarchy(nodeArray[i-1]) + this.options.hierarchical.nodeSpacing;} + let spacing = this.options.hierarchical.nodeSpacing; + let pos = spacing * handledNodeCount; + // We get the X or Y values we need and store them in pos and previousPos. + // The get and set make sure we get X or Y + if (handledNodeCount > 0) { + pos = this._getPositionForHierarchy(nodeArray[i-1]) + spacing; + } this._setPositionForHierarchy(node, pos, level); this._validatePositionAndContinue(node, level, pos); @@ -1045,15 +1070,17 @@ class LayoutEngine { * @private */ _placeBranchNodes(parentId, parentLevel) { + let childRef = this.hierarchical.childrenReference[parentId]; + // if this is not a parent, cancel the placing. This can happen with multiple parents to one child. - if (this.hierarchical.childrenReference[parentId] === undefined) { + if (childRef === undefined) { return; } // get a list of childNodes let childNodes = []; - for (let i = 0; i < this.hierarchical.childrenReference[parentId].length; i++) { - childNodes.push(this.body.nodes[this.hierarchical.childrenReference[parentId][i]]); + for (let i = 0; i < childRef.length; i++) { + childNodes.push(this.body.nodes[childRef[i]]); } // use the positions to order the nodes. @@ -1066,11 +1093,13 @@ class LayoutEngine { // check if the child node is below the parent node and if it has already been positioned. if (childNodeLevel > parentLevel && this.positionedNodes[childNode.id] === undefined) { // get the amount of space required for this node. If parent the width is based on the amount of children. + let spacing = this.options.hierarchical.nodeSpacing; let pos; - // we get the X or Y values we need and store them in pos and previousPos. The get and set make sure we get X or Y + // we get the X or Y values we need and store them in pos and previousPos. + // The get and set make sure we get X or Y if (i === 0) {pos = this._getPositionForHierarchy(this.body.nodes[parentId]);} - else {pos = this._getPositionForHierarchy(childNodes[i-1]) + this.options.hierarchical.nodeSpacing;} + else {pos = this._getPositionForHierarchy(childNodes[i-1]) + spacing;} this._setPositionForHierarchy(childNode, pos, childNodeLevel); this._validatePositionAndContinue(childNode, childNodeLevel, pos); } @@ -1080,14 +1109,8 @@ class LayoutEngine { } // center the parent nodes. - let minPos = 1e9; - let maxPos = -1e9; - for (let i = 0; i < childNodes.length; i++) { - let childNodeId = childNodes[i].id; - minPos = Math.min(minPos, this._getPositionForHierarchy(this.body.nodes[childNodeId])); - maxPos = Math.max(maxPos, this._getPositionForHierarchy(this.body.nodes[childNodeId])); - } - this._setPositionForHierarchy(this.body.nodes[parentId], 0.5 * (minPos + maxPos), parentLevel); + let center = this._getCenterPosition(childNodes); + this._setPositionForHierarchy(this.body.nodes[parentId], center, parentLevel); } @@ -1123,8 +1146,8 @@ class LayoutEngine { } /** - * Receives an array with node indices and returns an array with the actual node references. Used for sorting based on - * node properties. + * Receives an array with node indices and returns an array with the actual node references. + * Used for sorting based on node properties. * @param idArray */ _indexArrayToNodes(idArray) { @@ -1145,13 +1168,14 @@ class LayoutEngine { let distribution = {}; let nodeId, node; - // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. + // we fix Y because the hierarchy is vertical, + // we fix X so we do not give a node an x position for a second time. // the fix of X is removed after the x value has been set. for (nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { node = this.body.nodes[nodeId]; let level = this.hierarchical.levels[nodeId] === undefined ? 0 : this.hierarchical.levels[nodeId]; - if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { + if(this._isVertical()) { node.y = this.options.hierarchical.levelSeparation * level; node.options.fixed.y = true; } @@ -1239,7 +1263,7 @@ class LayoutEngine { let levelByDirection = (nodeA, nodeB, edge) => { let levelA = this.hierarchical.levels[nodeA.id]; // set initial level - if (levelA === undefined) {this.hierarchical.levels[nodeA.id] = minLevel;} + if (levelA === undefined) { levelA = this.hierarchical.levels[nodeA.id] = minLevel;} let diff = customCallback( NetworkUtil.cloneOptions(nodeA,'node'), @@ -1247,7 +1271,7 @@ class LayoutEngine { NetworkUtil.cloneOptions(edge,'edge') ); - this.hierarchical.levels[nodeB.id] = this.hierarchical.levels[nodeA.id] + diff; + this.hierarchical.levels[nodeB.id] = levelA + diff; }; this._crawlNetwork(levelByDirection); @@ -1266,12 +1290,12 @@ class LayoutEngine { let levelByDirection = (nodeA, nodeB, edge) => { let levelA = this.hierarchical.levels[nodeA.id]; // set initial level - if (levelA === undefined) {this.hierarchical.levels[nodeA.id] = minLevel;} + if (levelA === undefined) { levelA = this.hierarchical.levels[nodeA.id] = minLevel;} if (edge.toId == nodeB.id) { - this.hierarchical.levels[nodeB.id] = this.hierarchical.levels[nodeA.id] + 1; + this.hierarchical.levels[nodeB.id] = levelA + 1; } else { - this.hierarchical.levels[nodeB.id] = this.hierarchical.levels[nodeA.id] - 1; + this.hierarchical.levels[nodeB.id] = levelA - 1; } }; @@ -1317,16 +1341,17 @@ class LayoutEngine { progress[node.id] = true; let childNode; for (let i = 0; i < node.edges.length; i++) { - if (node.edges[i].connected === true) { - if (node.edges[i].toId === node.id) { - childNode = node.edges[i].from; + let edges = node.edges[i]; + if (edges.connected === true) { + if (edges.toId === node.id) { + childNode = edges.from; } else { - childNode = node.edges[i].to; + childNode = edges.to; } if (node.id !== childNode.id) { - callback(node, childNode, node.edges[i]); + callback(node, childNode, edges); crawler(childNode, tree); } } @@ -1369,15 +1394,17 @@ class LayoutEngine { return; } progress[parentId] = true; - if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { + if(this._isVertical()) { this.body.nodes[parentId].x += diff; } else { this.body.nodes[parentId].y += diff; } - if (this.hierarchical.childrenReference[parentId] !== undefined) { - for (let i = 0; i < this.hierarchical.childrenReference[parentId].length; i++) { - shifter(this.hierarchical.childrenReference[parentId][i]); + + let childRef = this.hierarchical.childrenReference[parentId]; + if (childRef !== undefined) { + for (let i = 0; i < childRef.length; i++) { + shifter(childRef[i]); } } }; @@ -1395,18 +1422,20 @@ class LayoutEngine { _findCommonParent(childA,childB) { let parents = {}; let iterateParents = (parents,child) => { - if (this.hierarchical.parentReference[child] !== undefined) { - for (let i = 0; i < this.hierarchical.parentReference[child].length; i++) { - let parent = this.hierarchical.parentReference[child][i]; + let parentRef = this.hierarchical.parentReference[child]; + if (parentRef !== undefined) { + for (let i = 0; i < parentRef.length; i++) { + let parent = parentRef[i]; parents[parent] = true; iterateParents(parents, parent) } } }; let findParent = (parents, child) => { - if (this.hierarchical.parentReference[child] !== undefined) { - for (let i = 0; i < this.hierarchical.parentReference[child].length; i++) { - let parent = this.hierarchical.parentReference[child][i]; + let parentRef = this.hierarchical.parentReference[child]; + if (parentRef !== undefined) { + for (let i = 0; i < parentRef.length; i++) { + let parent = parentRef[i]; if (parents[parent] !== undefined) { return {foundParent:parent, withChild:child}; } @@ -1445,7 +1474,7 @@ class LayoutEngine { this.distributionOrderingPresence[level][node.id] = true; } - if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { + if(this._isVertical()) { node.x = position; } else { @@ -1472,7 +1501,7 @@ class LayoutEngine { * @private */ _getPositionForHierarchy(node) { - if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { + if(this._isVertical()) { return node.x; } else { @@ -1487,7 +1516,7 @@ class LayoutEngine { */ _sortNodeArray(nodeArray) { if (nodeArray.length > 1) { - if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { + if(this._isVertical()) { nodeArray.sort(function (a, b) { return a.x - b.x; }) @@ -1499,6 +1528,36 @@ class LayoutEngine { } } } + + + /** + * Determine the center position of a branch from the passed list of child nodes + * + * This takes into account the positions of all the child nodes. + * @param childNodes {array} Array of either child nodes or node id's + * @return {number} + * @private + */ + _getCenterPosition(childNodes) { + let minPos = 1e9; + let maxPos = -1e9; + + for (let i = 0; i < childNodes.length; i++) { + let childNode; + if (childNodes[i].id !== undefined) { + childNode = childNodes[i]; + } else { + let childNodeId = childNodes[i]; + childNode = this.body.nodes[childNodeId]; + } + + let position = this._getPositionForHierarchy(childNode); + minPos = Math.min(minPos, position); + maxPos = Math.max(maxPos, position); + } + + return 0.5 * (minPos + maxPos); + } } export default LayoutEngine;