Skip to content

Commit

Permalink
fix(layout-engine): invalid hierarchical layout (#77)
Browse files Browse the repository at this point in the history
* test(layout-engine): add failing test for this issue

* fix(layout-engine): generate valid hierarchical layouts

TODO: This is very slow at the moment, it needs to be optimized for
better performance in production.

* chore(dist): update

* fix(layout-engine): position directed hierarchy to the bottom

This mostly minimizes the amount of levels edges span between two nodes.

* chore(dist): update

* chore(layout-engine): remove unused code

* perf(layout-engine): improve level determination perf

In my tests up to 2500 % improvement in performance depending on the
size and structure of the graph.

* perf(layout-engine): use faster cycle detection

Cycles are detected as the levels are being filled. This get's rid of
the incredibly slow O(V²*E) cycle detection. Still uses the same
fallback logic for cyclic “hierarchies”.

* chore(dist): update

* test(layout-engine): add one more test case

* fix(layout-engine): ignore circular edges in hierarchical layout

* test(layout-engine): randomize edge order

* style(layout-engine): refomat new files

* chore(dist): update

* chore(dist): update
  • Loading branch information
Thomaash committed Oct 1, 2019
1 parent 541ead2 commit 120d543
Show file tree
Hide file tree
Showing 16 changed files with 859 additions and 146 deletions.
23 changes: 23 additions & 0 deletions dist/types/network/modules/layout-engine/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
declare type Levels = Record<string | number, number>;
interface Edge {
connected: boolean;
from: Node;
fromId: string | number;
to: Node;
toId: string | number;
}
interface Node {
id: string | number;
edges: Edge[];
}
/**
* Assign levels to nodes according to their positions in the hierarchy.
*
* @param nodes - Nodes of the graph.
* @param levels - If present levels will be added to it, if not a new object will be created.
*
* @returns Populated node levels.
*/
export declare function fillLevelsByDirection(nodes: Node[], levels?: Levels): Levels;
export {};
//# sourceMappingURL=index.d.ts.map
1 change: 1 addition & 0 deletions dist/types/network/modules/layout-engine/index.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 15 additions & 15 deletions dist/vis-network.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
.vis .overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;

/* Must be displayed above for example selected Timeline items */
z-index: 10;
}

.vis-active {
box-shadow: 0 0 10px #86d5f8;
}

div.vis-configuration {
position:relative;
display:block;
Expand Down Expand Up @@ -286,21 +301,6 @@ input.vis-configuration.vis-config-range:focus::-ms-fill-upper {
border-width: 12px;
margin-top: -12px;
}
.vis .overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;

/* Must be displayed above for example selected Timeline items */
z-index: 10;
}

.vis-active {
box-shadow: 0 0 10px #86d5f8;
}

div.vis-network div.vis-manipulation {
box-sizing: content-box;

Expand Down
189 changes: 149 additions & 40 deletions dist/vis-network.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* A dynamic, browser-based visualization library.
*
* @version 0.0.0-no-version
* @date 2019-09-10T10:15:21Z
* @date 2019-10-01T08:44:23Z
*
* @copyright (c) 2011-2017 Almende B.V, http://almende.com
* @copyright (c) 2018-2019 visjs contributors, https://github.com/visjs
Expand Down Expand Up @@ -39891,6 +39891,7 @@ var timsort = createCommonjsModule$2(function (module, exports) {
unwrapExports(timsort);

var timsort$1 = timsort;
var timsort_1 = timsort$1.sort;

/**
* Interface definition for direction strategy classes.
Expand Down Expand Up @@ -40121,7 +40122,7 @@ function (_DirectionInterface) {
}, {
key: "sort",
value: function sort(nodeArray) {
timsort$1.sort(nodeArray, function (a, b) {
timsort_1(nodeArray, function (a, b) {
return a.x - b.x;
});
}
Expand Down Expand Up @@ -40217,7 +40218,7 @@ function (_DirectionInterface2) {
}, {
key: "sort",
value: function sort(nodeArray) {
timsort$1.sort(nodeArray, function (a, b) {
timsort_1(nodeArray, function (a, b) {
return a.y - b.y;
});
}
Expand All @@ -40241,6 +40242,148 @@ function (_DirectionInterface2) {
return HorizontalStrategy;
}(DirectionInterface);

/**
* Try to assign levels to nodes according to their positions in the cyclic “hierarchy”.
*
* @param nodes - Nodes of the graph.
* @param levels - If present levels will be added to it, if not a new object will be created.
*
* @returns Populated node levels.
*/
function fillLevelsByDirectionCyclic(nodes, levels) {
var edges = new Set();
nodes.forEach(function (node) {
node.edges.forEach(function (edge) {
if (edge.connected) {
edges.add(edge);
}
});
});
edges.forEach(function (edge) {
var fromId = edge.from.id;
var toId = edge.to.id;

if (levels[fromId] == null) {
levels[fromId] = 0;
}

if (levels[toId] == null || levels[fromId] >= levels[toId]) {
levels[toId] = levels[fromId] + 1;
}
});
return levels;
}
/**
* Assign levels to nodes according to their positions in the hierarchy.
*
* @param nodes - Nodes of the graph.
* @param levels - If present levels will be added to it, if not a new object will be created.
*
* @returns Populated node levels.
*/


function fillLevelsByDirection(nodes) {
var levels = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Object.create(null);
var limit = nodes.length;
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;

try {
var _loop = function _loop() {
var leaf = _step.value;

if (!leaf.edges.every(function (edge) {
return edge.to === leaf;
})) {
// Not a leaf.
return "continue";
}

levels[leaf.id] = 0;
var stack = [leaf];
var done = 0;
var node = void 0;

while (node = stack.pop()) {
var edges = node.edges;
var newLevel = levels[node.id] - 1;
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;

try {
for (var _iterator2 = edges[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var edge = _step2.value;

if (!edge.connected || edge.to !== node || edge.to === edge.from) {
continue;
}

var fromId = edge.fromId;
var oldLevel = levels[fromId];

if (oldLevel == null || oldLevel > newLevel) {
levels[fromId] = newLevel;
stack.push(edge.from);
}
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return != null) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}

if (done > limit) {
// This would run forever on a cyclic graph.
return {
v: fillLevelsByDirectionCyclic(nodes, levels)
};
} else {
++done;
}
}
};

for (var _iterator = nodes[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var _ret = _loop();

switch (_ret) {
case "continue":
continue;

default:
if (_typeof_1$1(_ret) === "object") return _ret.v;
}
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return != null) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}

return levels;
}

/**
* There's a mix-up with terms in the code. Following are the formal definitions:
*
Expand Down Expand Up @@ -41919,43 +42062,9 @@ function () {
value: function _determineLevelsDirected() {
var _this8 = this;

var minLevel = 10000;
/**
* Check if there is an edge going the opposite direction for given edge
*
* @param {Edge} edge edge to check
* @returns {boolean} true if there's another edge going into the opposite direction
*/

var isBidirectional = function isBidirectional(edge) {
util.forEach(_this8.body.edges, function (otherEdge) {
if (otherEdge.toId === edge.fromId && otherEdge.fromId === edge.toId) {
return true;
}
});
return false;
};

var levelByDirection = function levelByDirection(nodeA, nodeB, edge) {
var levelA = _this8.hierarchical.levels[nodeA.id];
var levelB = _this8.hierarchical.levels[nodeB.id];

if (isBidirectional(edge) ) ; // set initial level


if (levelA === undefined) {
levelA = _this8.hierarchical.levels[nodeA.id] = minLevel;
}

if (edge.toId == nodeB.id) {
_this8.hierarchical.levels[nodeB.id] = levelA + 1;
} else {
_this8.hierarchical.levels[nodeB.id] = levelA - 1;
}
};

this._crawlNetwork(levelByDirection);

this.hierarchical.levels = fillLevelsByDirection(this.body.nodeIndices.map(function (id) {
return _this8.body.nodes[id];
}), this.hierarchical.levels);
this.hierarchical.setMinLevelToZero(this.body.nodes);
}
/**
Expand Down
2 changes: 1 addition & 1 deletion dist/vis-network.esm.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/vis-network.esm.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/vis-network.esm.min.js.map

Large diffs are not rendered by default.

Loading

0 comments on commit 120d543

Please sign in to comment.