From 9664c27b36fb02122da799e2548b90e37ce5ab58 Mon Sep 17 00:00:00 2001 From: wimrijnders Date: Thu, 20 Jul 2017 22:19:49 +0200 Subject: [PATCH] Refactor LayoutEngine for further work (#3227) * Added commenting for better understanding * Refactored specific heirarchy fields to HierarchyStatus; more commenting. * Small edits to LayoutEngine --- lib/network/modules/LayoutEngine.js | 248 +++++++++++++++++++--------- 1 file changed, 170 insertions(+), 78 deletions(-) diff --git a/lib/network/modules/LayoutEngine.js b/lib/network/modules/LayoutEngine.js index 1c54d34f..c75505f2 100644 --- a/lib/network/modules/LayoutEngine.js +++ b/lib/network/modules/LayoutEngine.js @@ -1,5 +1,35 @@ 'use strict'; - +/** + * There's a mix-up with terms in the code. Following are the formal definitions: + * + * tree - a strict hierarchical network, i.e. every node has at most one parent + * forest - a collection of trees. These distinct trees are thus not connected. + * + * So: + * - in a network that is not a tree, there exist nodes with multiple parents. + * - a network consisting of unconnected sub-networks, of which at least one + * is not a tree, is not a forest. + * + * In the code, the definitions are: + * + * tree - any disconnected sub-network, strict hierarchical or not. + * forest - a bunch of these sub-networks + * + * The difference between tree and not-tree is important in the code, notably within + * to the block-shifting algorithm. The algorithm assumes formal trees and fails + * for not-trees, often in a spectacular manner (search for 'exploding network' in the issues). + * + * In order to distinguish the definitions in the following code, the adjective 'formal' is + * used. If 'formal' is absent, you must assume the non-formal definition. + * + * ---------------------------------------------------------------------------------- + * NOTES + * ===== + * + * A hierarchical layout is a different thing from a hierarchical network. + * The layout is a way to arrange the nodes in the view; this can be done + * on non-hierarchical networks as well. The converse is also possible. + */ let util = require('../../util'); var NetworkUtil = require('../NetworkUtil').default; @@ -8,19 +38,20 @@ var NetworkUtil = require('../NetworkUtil').default; * Container for derived data on current network, relating to hierarchy. * * Local, private class. - * - * TODO: Perhaps move more code for hierarchy state handling to this class. - * Till now, only the required and most obvious has been done. */ class HierarchicalStatus { constructor() { - this.childrenReference = {}; - this.parentReference = {}; - this.levels = {}; - this.trees = {}; + this.childrenReference = {}; // child id's per node id + this.parentReference = {}; // parent id's per node id + this.trees = {}; // tree id per node id; i.e. to which tree does given node id belong + + this.distributionOrdering = {}; // The nodes per level, in the display order + this.levels = {}; // hierarchy level per node id + this.distributionIndex = {}; // The position of the node in the level sorting order, per node id. - this.isTree = false; + this.isTree = false; // True if current network is a formal tree + this.treeIndex = -1; // Highest tree id in current network. } @@ -41,7 +72,7 @@ class HierarchicalStatus { /** - * Check if the current state is for a tree or forest network. + * Check if the current state is for a formal tree or formal forest. * * This is the case if every node has at most one parent. * @@ -59,6 +90,25 @@ class HierarchicalStatus { } + /** + * Return the number of separate trees in the current network. + */ + numTrees() { + return (this.treeIndex + 1); // This assumes the indexes are assigned consecitively + } + + + /** + * Assign a tree id to a node + */ + setTreeIndex(node, treeId) { + if (this.trees[node.id] === undefined) { + this.trees[node.id] = treeId; + this.treeIndex = Math.max(treeId, this.treeIndex); + } + } + + /** * Ensure level for given id is defined. * @@ -165,6 +215,72 @@ class HierarchicalStatus { max_y: max_y }; } + + + /** + * Check if two nodes have the same parent(s) + * + * @return true if the two nodes have a same ancestor node, false otherwise + */ + hasSameParent(node1, node2) { + let parents1 = this.parentReference[node1.id]; + let parents2 = this.parentReference[node2.id]; + if (parents1 === undefined || parents2 === undefined) { + return false; + } + + for (let i = 0; i < parents1.length; i++) { + for (let j = 0; j < parents2.length; j++) { + if (parents1[i] == parents2[j]) { + return true; + } + } + } + return false; + } + + + /** + * Check if two nodes are in the same tree. + * + * @return true if this is so, false otherwise + */ + inSameSubNetwork(node1, node2) { + return (this.trees[node1.id] === this.trees[node2.id]); + } + + + /** + * Get a list of the distinct levels in the current network + */ + getLevels() { + return Object.keys(this.distributionOrdering); + } + + + /** + * Add a node to the ordering per level + */ + addToOrdering(node, level) { + if (this.distributionOrdering[level] === undefined) { + this.distributionOrdering[level] = []; + } + + var isPresent = false; + var curLevel = this.distributionOrdering[level]; + for (var n in curLevel) { + //if (curLevel[n].id === node.id) { + if (curLevel[n] === node) { + isPresent = true; + break; + } + } + + if (!isPresent) { + this.distributionOrdering[level].push(node); + this.distributionIndex[node.id] = this.distributionOrdering[level].length - 1; + } + } } @@ -504,12 +620,6 @@ class LayoutEngine { let undefinedLevel = false; this.lastNodeOnLevel = {}; this.hierarchical = new HierarchicalStatus(); - this.treeIndex = -1; - - this.distributionOrdering = {}; - this.distributionIndex = {}; - this.distributionOrderingPresence = {}; - for (nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { @@ -593,9 +703,11 @@ class LayoutEngine { // shift a single tree by an offset let shiftTree = (index, offset) => { - for (let nodeId in this.hierarchical.trees) { - if (this.hierarchical.trees.hasOwnProperty(nodeId)) { - if (this.hierarchical.trees[nodeId] === index) { + let trees = this.hierarchical.trees; + + for (let nodeId in trees) { + if (trees.hasOwnProperty(nodeId)) { + if (trees[nodeId] === index) { let node = this.body.nodes[nodeId]; let pos = this._getPositionForHierarchy(node); this._setPositionForHierarchy(node, pos + offset, undefined, true); @@ -617,7 +729,7 @@ class LayoutEngine { // get the width of all trees let getTreeSizes = () => { let treeWidths = []; - for (let i = 0; i <= this.treeIndex; i++) { + for (let i = 0; i <= this.hierarchical.numTrees(); i++) { treeWidths.push(getTreeSize(i)); } return treeWidths; @@ -677,40 +789,32 @@ class LayoutEngine { return Math.min(maxLevel1, maxLevel2); }; - // check if two nodes have the same parent(s) - let hasSameParent = (node1, node2) => { - let parents1 = this.hierarchical.parentReference[node1.id]; - let parents2 = this.hierarchical.parentReference[node2.id]; - if (parents1 === undefined || parents2 === undefined) { - return false; - } - - for (let i = 0; i < parents1.length; i++) { - for (let j = 0; j < parents2.length; j++) { - if (parents1[i] == parents2[j]) { - return true; - } - } - } - return false; - }; - // condense elements. These can be nodes or branches depending on the callback. + /** + * Condense elements. These can be nodes or branches depending on the callback. + */ let shiftElementsCloser = (callback, levels, centerParents) => { + let hier = this.hierarchical; + for (let i = 0; i < levels.length; i++) { let level = levels[i]; - let levelNodes = this.distributionOrdering[level]; + let levelNodes = hier.distributionOrdering[level]; if (levelNodes.length > 1) { for (let j = 0; j < levelNodes.length - 1; j++) { - if (hasSameParent(levelNodes[j],levelNodes[j+1]) === true) { - if (this.hierarchical.trees[levelNodes[j].id] === this.hierarchical.trees[levelNodes[j+1].id]) { - callback(levelNodes[j],levelNodes[j+1], centerParents); - } - }} + let node1 = levelNodes[j]; + let node2 = levelNodes[j+1]; + + // NOTE: logic maintained as it was; if nodes have same ancestor, + // then of course they are in the same sub-network. + if (hier.hasSameParent(node1, node2) && hier.inSameSubNetwork(node1, node2) ) { + callback(node1, node2, centerParents); + } + } } } }; + // callback for shifting branches let branchShiftCallback = (node1, node2, centerParent = false) => { //window.CALLBACKS.push(() => { @@ -884,13 +988,13 @@ class LayoutEngine { // method to remove whitespace between branches. Because we do bottom up, we can center the parents. let minimizeEdgeLengthBottomUp = (iterations) => { - let levels = Object.keys(this.distributionOrdering); + let levels = this.hierarchical.getLevels(); levels = levels.reverse(); for (let i = 0; i < iterations; i++) { stillShifting = false; for (let j = 0; j < levels.length; j++) { let level = levels[j]; - let levelNodes = this.distributionOrdering[level]; + let levelNodes = this.hierarchical.distributionOrdering[level]; for (let k = 0; k < levelNodes.length; k++) { minimizeEdgeLength(1000, levelNodes[k]); } @@ -904,7 +1008,7 @@ class LayoutEngine { // method to remove whitespace between branches. Because we do bottom up, we can center the parents. let shiftBranchesCloserBottomUp = (iterations) => { - let levels = Object.keys(this.distributionOrdering); + let levels = this.hierarchical.getLevels(); levels = levels.reverse(); for (let i = 0; i < iterations; i++) { stillShifting = false; @@ -926,11 +1030,11 @@ class LayoutEngine { // center all parents let centerAllParentsBottomUp = () => { - let levels = Object.keys(this.distributionOrdering); + let levels = this.hierarchical.getLevels(); levels = levels.reverse(); for (let i = 0; i < levels.length; i++) { let level = levels[i]; - let levelNodes = this.distributionOrdering[level]; + let levelNodes = this.hierarchical.distributionOrdering[level]; for (let j = 0; j < levelNodes.length; j++) { this._centerParent(levelNodes[j]); } @@ -970,9 +1074,9 @@ class LayoutEngine { } let level = this.hierarchical.levels[node.id]; if (level !== undefined) { - let index = this.distributionIndex[node.id]; + let index = this.hierarchical.distributionIndex[node.id]; let position = this._getPositionForHierarchy(node); - let ordering = this.distributionOrdering[level]; + let ordering = this.hierarchical.distributionOrdering[level]; let minSpace = 1e9; let maxSpace = 1e9; if (index !== 0) { @@ -998,6 +1102,7 @@ class LayoutEngine { } } + /** * We use this method to center a parent node and check if it does not cross other nodes when it does. * @param node @@ -1028,7 +1133,6 @@ class LayoutEngine { } - /** * This function places the nodes on the canvas based on the hierarchial distribution. * @@ -1129,7 +1233,7 @@ class LayoutEngine { * @private */ _validatePositionAndContinue(node, level, pos) { - // This only works for strict hierarchical networks, i.e. trees and forests + // This method only works for formal trees and formal forests // Early exit if this is not the case if (!this.hierarchical.isTree) return; @@ -1143,11 +1247,8 @@ class LayoutEngine { } } - // store change in position. - this.lastNodeOnLevel[level] = node.id; - + this.lastNodeOnLevel[level] = node.id; // store change in position. this.positionedNodes[node.id] = true; - this._placeBranchNodes(node.id, level); } @@ -1362,15 +1463,10 @@ class LayoutEngine { */ _crawlNetwork(callback = function() {}, startingNodeId) { let progress = {}; - let treeIndex = 0; let crawler = (node, tree) => { if (progress[node.id] === undefined) { - - if (this.hierarchical.trees[node.id] === undefined) { - this.hierarchical.trees[node.id] = tree; - this.treeIndex = Math.max(tree, this.treeIndex); - } + this.hierarchical.setTreeIndex(node, tree); progress[node.id] = true; let childNode; @@ -1378,14 +1474,14 @@ class LayoutEngine { for (let i = 0; i < edges.length; i++) { let edge = edges[i]; if (edge.connected === true) { - if (edge.toId == node.id) { // '==' because id's can be string and numeric + if (edge.toId == node.id) { // Not '===' because id's can be string and numeric childNode = edge.from; } else { childNode = edge.to; } - if (node.id != childNode.id) { // '!=' because id's can be string and numeric + if (node.id != childNode.id) { // Not '!==' because id's can be string and numeric callback(node, childNode, edge); crawler(childNode, tree); } @@ -1395,17 +1491,22 @@ class LayoutEngine { }; - // we can crawl from a specific node or over all nodes. if (startingNodeId === undefined) { + // Crawl over all nodes + let treeIndex = 0; // Serves to pass a unique id for the current distinct tree + for (let i = 0; i < this.body.nodeIndices.length; i++) { - let node = this.body.nodes[this.body.nodeIndices[i]]; - if (progress[node.id] === undefined) { + let nodeId = this.body.nodeIndices[i]; + + if (progress[nodeId] === undefined) { + let node = this.body.nodes[nodeId]; crawler(node, treeIndex); treeIndex += 1; } } } else { + // Crawl from the given starting node let node = this.body.nodes[startingNodeId]; if (node === undefined) { console.error("Node not found:", startingNodeId); @@ -1497,16 +1598,7 @@ class LayoutEngine { _setPositionForHierarchy(node, position, level, doNotUpdate = false) { //console.log('_setPositionForHierarchy',node.id, position) if (doNotUpdate !== true) { - if (this.distributionOrdering[level] === undefined) { - this.distributionOrdering[level] = []; - this.distributionOrderingPresence[level] = {}; - } - - if (this.distributionOrderingPresence[level][node.id] === undefined) { - this.distributionOrdering[level].push(node); - this.distributionIndex[node.id] = this.distributionOrdering[level].length - 1; - } - this.distributionOrderingPresence[level][node.id] = true; + this.hierarchical.addToOrdering(node, level); } if(this._isVertical()) {