Browse Source

Fix for exploding directed network, first working version; refactored hierarchical state in LayoutEngine. (#3017)

gemini
wimrijnders 7 years ago
committed by yotamberk
parent
commit
b20fe1231c
1 changed files with 248 additions and 136 deletions
  1. +248
    -136
      lib/network/modules/LayoutEngine.js

+ 248
- 136
lib/network/modules/LayoutEngine.js View File

@ -3,6 +3,171 @@
let util = require('../../util');
import NetworkUtil from '../NetworkUtil';
/**
* 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.isTree = false;
}
/**
* Add the relation between given nodes to the current state.
*/
addRelation(parentNodeId, childNodeId) {
if (this.childrenReference[parentNodeId] === undefined) {
this.childrenReference[parentNodeId] = [];
}
this.childrenReference[parentNodeId].push(childNodeId);
if (this.parentReference[childNodeId] === undefined) {
this.parentReference[childNodeId] = [];
}
this.parentReference[childNodeId].push(parentNodeId);
}
/**
* Check if the current state is for a tree or forest network.
*
* This is the case if every node has at most one parent.
*
* Pre: parentReference init'ed properly for current network
*/
checkIfTree() {
for (let i in this.parentReference) {
if (this.parentReference[i].length > 1) {
this.isTree = false;
return;
}
}
this.isTree = true;
}
/**
* Ensure level for given id is defined.
*
* Sets level to zero for given node id if not already present
*/
ensureLevel(nodeId) {
if (this.levels[nodeId] === undefined) {
this.levels[nodeId] = 0;
}
}
/**
* get the maximum level of a branch.
*
* TODO: Never entered; find a test case to test this!
*/
getMaxLevel(nodeId) {
let accumulator = {};
let _getMaxLevel = (nodeId) => {
if (accumulator[nodeId] !== undefined) {
return accumulator[nodeId];
}
let level = this.levels[nodeId];
if (this.childrenReference[nodeId]) {
let children = this.childrenReference[nodeId];
if (children.length > 0) {
for (let i = 0; i < children.length; i++) {
level = Math.max(level,_getMaxLevel(children[i]));
}
}
}
accumulator[nodeId] = level;
return level;
};
return _getMaxLevel(nodeId);
}
levelDownstream(nodeA, nodeB) {
if (this.levels[nodeB.id] === undefined) {
// set initial level
if (this.levels[nodeA.id] === undefined) {
this.levels[nodeA.id] = 0;
}
// set level
this.levels[nodeB.id] = this.levels[nodeA.id] + 1;
}
}
/**
* Small util method to set the minimum levels of the nodes to zero.
*/
setMinLevelToZero(nodes) {
let minLevel = 1e9;
// get the minimum level
for (let nodeId in nodes) {
if (nodes.hasOwnProperty(nodeId)) {
if (this.levels[nodeId] !== undefined) {
minLevel = Math.min(this.levels[nodeId], minLevel);
}
}
}
// subtract the minimum from the set so we have a range starting from 0
for (let nodeId in nodes) {
if (nodes.hasOwnProperty(nodeId)) {
if (this.levels[nodeId] !== undefined) {
this.levels[nodeId] -= minLevel;
}
}
}
}
/**
* Get the min and max xy-coordinates of a given tree
*/
getTreeSize(nodes, index) {
let min_x = 1e9;
let max_x = -1e9;
let min_y = 1e9;
let max_y = -1e9;
for (let nodeId in this.trees) {
if (this.trees.hasOwnProperty(nodeId)) {
if (this.trees[nodeId] === index) {
let node = nodes[nodeId];
min_x = Math.min(node.x, min_x);
max_x = Math.max(node.x, max_x);
min_y = Math.min(node.y, min_y);
max_y = Math.max(node.y, max_y);
}
}
}
return {
min_x: min_x,
max_x: max_x,
min_y: min_y,
max_y: max_y
};
}
}
class LayoutEngine {
constructor(body) {
this.body = body;
@ -308,11 +473,8 @@ class LayoutEngine {
let definedLevel = false;
let definedPositions = true;
let undefinedLevel = false;
this.hierarchicalLevels = {};
this.lastNodeOnLevel = {};
this.hierarchicalChildrenReference = {};
this.hierarchicalParentReference = {};
this.hierarchicalTrees = {};
this.hierarchical = new HierarchicalStatus();
this.treeIndex = -1;
this.distributionOrdering = {};
@ -328,7 +490,7 @@ class LayoutEngine {
}
if (node.options.level !== undefined) {
definedLevel = true;
this.hierarchicalLevels[nodeId] = node.options.level;
this.hierarchical.levels[nodeId] = node.options.level;
}
else {
undefinedLevel = true;
@ -358,9 +520,7 @@ class LayoutEngine {
// fallback for cases where there are nodes but no edges
for (let nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
if (this.hierarchicalLevels[nodeId] === undefined) {
this.hierarchicalLevels[nodeId] = 0;
}
this.hierarchical.ensureLevel(nodeId);
}
}
// check the distribution of the nodes per level.
@ -402,9 +562,9 @@ class LayoutEngine {
// shift a single tree by an offset
let shiftTree = (index, offset) => {
for (let nodeId in this.hierarchicalTrees) {
if (this.hierarchicalTrees.hasOwnProperty(nodeId)) {
if (this.hierarchicalTrees[nodeId] === index) {
for (let nodeId in this.hierarchical.trees) {
if (this.hierarchical.trees.hasOwnProperty(nodeId)) {
if (this.hierarchical.trees[nodeId] === index) {
let node = this.body.nodes[nodeId];
let pos = this._getPositionForHierarchy(node);
this._setPositionForHierarchy(node, pos + offset, undefined, true);
@ -415,18 +575,12 @@ class LayoutEngine {
// get the width of a tree
let getTreeSize = (index) => {
let min = 1e9;
let max = -1e9;
for (let nodeId in this.hierarchicalTrees) {
if (this.hierarchicalTrees.hasOwnProperty(nodeId)) {
if (this.hierarchicalTrees[nodeId] === index) {
let pos = this._getPositionForHierarchy(this.body.nodes[nodeId]);
min = Math.min(pos, min);
max = Math.max(pos, max);
}
}
let res = this.hierarchical.getTreeSize(this.body.nodes, index);
if (this._isVertical()) {
return {min: res.min_x, max: res.max_x};
} else {
return {min: res.min_y, max: res.max_y};
}
return {min:min, max:max};
};
// get the width of all trees
@ -445,8 +599,8 @@ class LayoutEngine {
return;
}
map[source.id] = true;
if (this.hierarchicalChildrenReference[source.id]) {
let children = this.hierarchicalChildrenReference[source.id];
if (this.hierarchical.childrenReference[source.id]) {
let children = this.hierarchical.childrenReference[source.id];
if (children.length > 0) {
for (let i = 0; i < children.length; i++) {
getBranchNodes(this.body.nodes[children[i]], map);
@ -465,7 +619,7 @@ class LayoutEngine {
for (let branchNode in branchMap) {
if (branchMap.hasOwnProperty(branchNode)) {
let node = this.body.nodes[branchNode];
let level = this.hierarchicalLevels[node.id];
let level = this.hierarchical.levels[node.id];
let position = this._getPositionForHierarchy(node);
// get the space around the node.
@ -484,39 +638,18 @@ class LayoutEngine {
return [min, max, minSpace, maxSpace];
};
// get the maximum level of a branch.
let getMaxLevel = (nodeId) => {
let accumulator = {};
let _getMaxLevel = (nodeId) => {
if (accumulator[nodeId] !== undefined) {
return accumulator[nodeId];
}
let level = this.hierarchicalLevels[nodeId];
if (this.hierarchicalChildrenReference[nodeId]) {
let children = this.hierarchicalChildrenReference[nodeId];
if (children.length > 0) {
for (let i = 0; i < children.length; i++) {
level = Math.max(level,_getMaxLevel(children[i]));
}
}
}
accumulator[nodeId] = level;
return level;
};
return _getMaxLevel(nodeId);
};
// check what the maximum level is these nodes have in common.
let getCollisionLevel = (node1, node2) => {
let maxLevel1 = getMaxLevel(node1.id);
let maxLevel2 = getMaxLevel(node2.id);
let maxLevel1 = this.hierarchical.getMaxLevel(node1.id);
let maxLevel2 = this.hierarchical.getMaxLevel(node2.id);
return Math.min(maxLevel1, maxLevel2);
};
// check if two nodes have the same parent(s)
let hasSameParent = (node1, node2) => {
let parents1 = this.hierarchicalParentReference[node1.id];
let parents2 = this.hierarchicalParentReference[node2.id];
let parents1 = this.hierarchical.parentReference[node1.id];
let parents2 = this.hierarchical.parentReference[node2.id];
if (parents1 === undefined || parents2 === undefined) {
return false;
}
@ -539,7 +672,7 @@ class LayoutEngine {
if (levelNodes.length > 1) {
for (let j = 0; j < levelNodes.length - 1; j++) {
if (hasSameParent(levelNodes[j],levelNodes[j+1]) === true) {
if (this.hierarchicalTrees[levelNodes[j].id] === this.hierarchicalTrees[levelNodes[j+1].id]) {
if (this.hierarchical.trees[levelNodes[j].id] === this.hierarchical.trees[levelNodes[j+1].id]) {
callback(levelNodes[j],levelNodes[j+1], centerParents);
}
}}
@ -593,7 +726,7 @@ class LayoutEngine {
// console.log("ts",node.id);
let nodeId = node.id;
let allEdges = node.edges;
let nodeLevel = this.hierarchicalLevels[node.id];
let nodeLevel = this.hierarchical.levels[node.id];
// gather constants
let C2 = this.options.hierarchical.levelSeparation * this.options.hierarchical.levelSeparation;
@ -604,7 +737,7 @@ class LayoutEngine {
if (edge.toId != edge.fromId) {
let otherNode = edge.toId == nodeId ? edge.from : edge.to;
referenceNodes[allEdges[i].id] = otherNode;
if (this.hierarchicalLevels[otherNode.id] < nodeLevel) {
if (this.hierarchical.levels[otherNode.id] < nodeLevel) {
aboveEdges.push(edge);
}
}
@ -802,7 +935,7 @@ class LayoutEngine {
if (map === undefined) {
useMap = false;
}
let level = this.hierarchicalLevels[node.id];
let level = this.hierarchical.levels[node.id];
if (level !== undefined) {
let index = this.distributionIndex[node.id];
let position = this._getPositionForHierarchy(node);
@ -837,16 +970,16 @@ class LayoutEngine {
* @private
*/
_centerParent(node) {
if (this.hierarchicalParentReference[node.id]) {
let parents = this.hierarchicalParentReference[node.id];
if (this.hierarchical.parentReference[node.id]) {
let parents = this.hierarchical.parentReference[node.id];
for (var i = 0; i < parents.length; i++) {
let parentId = parents[i];
let parentNode = this.body.nodes[parentId];
if (this.hierarchicalChildrenReference[parentId]) {
if (this.hierarchical.childrenReference[parentId]) {
// get the range of the children
let minPos = 1e9;
let maxPos = -1e9;
let children = this.hierarchicalChildrenReference[parentId];
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]];
@ -893,7 +1026,7 @@ class LayoutEngine {
// 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;}
this._setPositionForHierarchy(node, pos, level);
this._validataPositionAndContinue(node, level, pos);
this._validatePositionAndContinue(node, level, pos);
handledNodeCount++;
}
@ -913,14 +1046,14 @@ class LayoutEngine {
*/
_placeBranchNodes(parentId, parentLevel) {
// if this is not a parent, cancel the placing. This can happen with multiple parents to one child.
if (this.hierarchicalChildrenReference[parentId] === undefined) {
if (this.hierarchical.childrenReference[parentId] === undefined) {
return;
}
// get a list of childNodes
let childNodes = [];
for (let i = 0; i < this.hierarchicalChildrenReference[parentId].length; i++) {
childNodes.push(this.body.nodes[this.hierarchicalChildrenReference[parentId][i]]);
for (let i = 0; i < this.hierarchical.childrenReference[parentId].length; i++) {
childNodes.push(this.body.nodes[this.hierarchical.childrenReference[parentId][i]]);
}
// use the positions to order the nodes.
@ -929,7 +1062,7 @@ class LayoutEngine {
// position the childNodes
for (let i = 0; i < childNodes.length; i++) {
let childNode = childNodes[i];
let childNodeLevel = this.hierarchicalLevels[childNode.id];
let childNodeLevel = this.hierarchical.levels[childNode.id];
// 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.
@ -939,7 +1072,7 @@ class LayoutEngine {
if (i === 0) {pos = this._getPositionForHierarchy(this.body.nodes[parentId]);}
else {pos = this._getPositionForHierarchy(childNodes[i-1]) + this.options.hierarchical.nodeSpacing;}
this._setPositionForHierarchy(childNode, pos, childNodeLevel);
this._validataPositionAndContinue(childNode, childNodeLevel, pos);
this._validatePositionAndContinue(childNode, childNodeLevel, pos);
}
else {
return;
@ -966,7 +1099,11 @@ class LayoutEngine {
* @param pos
* @private
*/
_validataPositionAndContinue(node, level, pos) {
_validatePositionAndContinue(node, level, pos) {
// This only works for strict hierarchical networks, i.e. trees and forests
// Early exit if this is not the case
if (!this.hierarchical.isTree) return;
// if overlap has been detected, we shift the branch
if (this.lastNodeOnLevel[level] !== undefined) {
let previousPos = this._getPositionForHierarchy(this.body.nodes[this.lastNodeOnLevel[level]]);
@ -1013,7 +1150,7 @@ class LayoutEngine {
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
let level = this.hierarchicalLevels[nodeId] === undefined ? 0 : this.hierarchicalLevels[nodeId];
let level = this.hierarchical.levels[nodeId] === undefined ? 0 : this.hierarchical.levels[nodeId];
if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') {
node.y = this.options.hierarchical.levelSeparation * level;
node.options.fixed.y = true;
@ -1043,7 +1180,7 @@ class LayoutEngine {
for (let nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
let node = this.body.nodes[nodeId];
if (this.hierarchicalLevels[nodeId] === undefined) {
if (this.hierarchical.levels[nodeId] === undefined) {
hubSize = node.edges.length < hubSize ? hubSize : node.edges.length;
}
}
@ -1062,15 +1199,8 @@ class LayoutEngine {
let hubSize = 1;
let levelDownstream = (nodeA, nodeB) => {
if (this.hierarchicalLevels[nodeB.id] === undefined) {
// set initial level
if (this.hierarchicalLevels[nodeA.id] === undefined) {
this.hierarchicalLevels[nodeA.id] = 0;
}
// set level
this.hierarchicalLevels[nodeB.id] = this.hierarchicalLevels[nodeA.id] + 1;
}
};
this.hierarchical.levelDownstream(nodeA, nodeB);
}
while (hubSize > 0) {
// determine hubs
@ -1089,8 +1219,11 @@ class LayoutEngine {
}
}
/**
* TODO: release feature
* TODO: Determine if this feature is needed at all
*
* @private
*/
_determineLevelsCustomCallback() {
@ -1101,10 +1234,12 @@ class LayoutEngine {
};
// TODO: perhaps move to HierarchicalStatus.
// But I currently don't see the point, this method is not used.
let levelByDirection = (nodeA, nodeB, edge) => {
let levelA = this.hierarchicalLevels[nodeA.id];
let levelA = this.hierarchical.levels[nodeA.id];
// set initial level
if (levelA === undefined) {this.hierarchicalLevels[nodeA.id] = minLevel;}
if (levelA === undefined) {this.hierarchical.levels[nodeA.id] = minLevel;}
let diff = customCallback(
NetworkUtil.cloneOptions(nodeA,'node'),
@ -1112,11 +1247,11 @@ class LayoutEngine {
NetworkUtil.cloneOptions(edge,'edge')
);
this.hierarchicalLevels[nodeB.id] = this.hierarchicalLevels[nodeA.id] + diff;
this.hierarchical.levels[nodeB.id] = this.hierarchical.levels[nodeA.id] + diff;
};
this._crawlNetwork(levelByDirection);
this._setMinLevelToZero();
this.hierarchical.setMinLevelToZero(this.body.nodes);
}
/**
@ -1127,45 +1262,21 @@ class LayoutEngine {
*/
_determineLevelsDirected() {
let minLevel = 10000;
let levelByDirection = (nodeA, nodeB, edge) => {
let levelA = this.hierarchicalLevels[nodeA.id];
let levelA = this.hierarchical.levels[nodeA.id];
// set initial level
if (levelA === undefined) {this.hierarchicalLevels[nodeA.id] = minLevel;}
if (levelA === undefined) {this.hierarchical.levels[nodeA.id] = minLevel;}
if (edge.toId == nodeB.id) {
this.hierarchicalLevels[nodeB.id] = this.hierarchicalLevels[nodeA.id] + 1;
this.hierarchical.levels[nodeB.id] = this.hierarchical.levels[nodeA.id] + 1;
}
else {
this.hierarchicalLevels[nodeB.id] = this.hierarchicalLevels[nodeA.id] - 1;
this.hierarchical.levels[nodeB.id] = this.hierarchical.levels[nodeA.id] - 1;
}
};
this._crawlNetwork(levelByDirection);
this._setMinLevelToZero();
}
/**
* Small util method to set the minimum levels of the nodes to zero.
* @private
*/
_setMinLevelToZero() {
let minLevel = 1e9;
// get the minimum level
for (let nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
if (this.hierarchicalLevels[nodeId] !== undefined) {
minLevel = Math.min(this.hierarchicalLevels[nodeId], minLevel);
}
}
}
// subtract the minimum from the set so we have a range starting from 0
for (let nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
if (this.hierarchicalLevels[nodeId] !== undefined) {
this.hierarchicalLevels[nodeId] -= minLevel;
}
}
}
this._crawlNetwork(levelByDirection);
this.hierarchical.setMinLevelToZero(this.body.nodes);
}
@ -1175,21 +1286,13 @@ class LayoutEngine {
*/
_generateMap() {
let fillInRelations = (parentNode, childNode) => {
if (this.hierarchicalLevels[childNode.id] > this.hierarchicalLevels[parentNode.id]) {
let parentNodeId = parentNode.id;
let childNodeId = childNode.id;
if (this.hierarchicalChildrenReference[parentNodeId] === undefined) {
this.hierarchicalChildrenReference[parentNodeId] = [];
}
this.hierarchicalChildrenReference[parentNodeId].push(childNodeId);
if (this.hierarchicalParentReference[childNodeId] === undefined) {
this.hierarchicalParentReference[childNodeId] = [];
}
this.hierarchicalParentReference[childNodeId].push(parentNodeId);
if (this.hierarchical.levels[childNode.id] > this.hierarchical.levels[parentNode.id]) {
this.hierarchical.addRelation(parentNode.id, childNode.id);
}
};
this._crawlNetwork(fillInRelations);
this.hierarchical.checkIfTree();
}
@ -1206,8 +1309,8 @@ class LayoutEngine {
let crawler = (node, tree) => {
if (progress[node.id] === undefined) {
if (this.hierarchicalTrees[node.id] === undefined) {
this.hierarchicalTrees[node.id] = tree;
if (this.hierarchical.trees[node.id] === undefined) {
this.hierarchical.trees[node.id] = tree;
this.treeIndex = Math.max(tree, this.treeIndex);
}
@ -1272,9 +1375,9 @@ class LayoutEngine {
else {
this.body.nodes[parentId].y += diff;
}
if (this.hierarchicalChildrenReference[parentId] !== undefined) {
for (let i = 0; i < this.hierarchicalChildrenReference[parentId].length; i++) {
shifter(this.hierarchicalChildrenReference[parentId][i]);
if (this.hierarchical.childrenReference[parentId] !== undefined) {
for (let i = 0; i < this.hierarchical.childrenReference[parentId].length; i++) {
shifter(this.hierarchical.childrenReference[parentId][i]);
}
}
};
@ -1292,18 +1395,18 @@ class LayoutEngine {
_findCommonParent(childA,childB) {
let parents = {};
let iterateParents = (parents,child) => {
if (this.hierarchicalParentReference[child] !== undefined) {
for (let i = 0; i < this.hierarchicalParentReference[child].length; i++) {
let parent = this.hierarchicalParentReference[child][i];
if (this.hierarchical.parentReference[child] !== undefined) {
for (let i = 0; i < this.hierarchical.parentReference[child].length; i++) {
let parent = this.hierarchical.parentReference[child][i];
parents[parent] = true;
iterateParents(parents, parent)
}
}
};
let findParent = (parents, child) => {
if (this.hierarchicalParentReference[child] !== undefined) {
for (let i = 0; i < this.hierarchicalParentReference[child].length; i++) {
let parent = this.hierarchicalParentReference[child][i];
if (this.hierarchical.parentReference[child] !== undefined) {
for (let i = 0; i < this.hierarchical.parentReference[child].length; i++) {
let parent = this.hierarchical.parentReference[child][i];
if (parents[parent] !== undefined) {
return {foundParent:parent, withChild:child};
}
@ -1350,6 +1453,18 @@ class LayoutEngine {
}
}
/**
* Utility function to cut down on typing this all the time.
*
* TODO: use this in all applicable situations in this class.
*
* @private
*/
_isVertical() {
return (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU');
}
/**
* Abstract the getting of the position of a node so we do not have to repeat the direction check all the time.
* @param node
@ -1384,9 +1499,6 @@ class LayoutEngine {
}
}
}
}
export default LayoutEngine;

Loading…
Cancel
Save