diff --git a/examples/network/layout/hierarchicalLayoutMethods.html b/examples/network/layout/hierarchicalLayoutMethods.html index 7c22d0db7e..f43ae197dd 100644 --- a/examples/network/layout/hierarchicalLayoutMethods.html +++ b/examples/network/layout/hierarchicalLayoutMethods.html @@ -1,126 +1,298 @@ - + - - Vis Network | Layouts | Hierarchical Layout Difference - - - - - - - - - - -

Hierarchical layout difference

-
- This example shows a the effect of the different hierarchical layout - methods. Hubsize is based on the amount of edges connected to a node. The - node with the most connections (the largest hub) is drawn at the top of - the tree. The direction method is based on the direction of the edges. Try - switching between the methods by clicking on the buttons bellow. -
-

- Layout method: - -

-

- Shake towards: - - (Applies to directed only.) -

+ conf.addEventListener("change", handleConfChange); + handleConfChange(); + }); + + + + + + +
+
+

Vis Network

+

Layouts

+

Hierarchical Layout Methods

+
+ +

+ This example shows the effect of the different hierarchical layouting + methods, node shaking and how hierarchical layouts work with clustering. + Also note that it's impossible to properly position a "hierarchy" with a + cycle (If node 1 is above node 2 and node 2 is above node 1 which one is + actually on top?) +

+ +

Hub Size

+

+ The hub size layouting method is based on the amount of edges connected + to a node. The node with the most connections (the largest hub) is drawn + at the top of the tree. +

+ +

Direction

+

+ The direction layouting method is based on the direction of the edges. + The from nodes are placed above the to nodes in the hierarchy. Nodes + that can be placed on multiple levels are by default shaken towards + towards the leaves. All the leaves are then in a single line at the very + bottom of the hierarchy. Optionally they can be shaken towards the roots + which results in the roots being in a single line at the very top of the + hierarchy. +

+ +

Interactive Configuration

+
+

+ Layout method: +
+ + + +
+ + + +

+ +

+ Shake towards (Applies to directed only.): +
+ + + +
+ + + +

+ +

+ Cluster: +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +

+
+
+ diff --git a/lib/network/modules/LayoutEngine.js b/lib/network/modules/LayoutEngine.js index a2728dd3b7..7c424f6cdf 100644 --- a/lib/network/modules/LayoutEngine.js +++ b/lib/network/modules/LayoutEngine.js @@ -1503,19 +1503,16 @@ class LayoutEngine { * @private */ _determineLevelsDirected() { - const nodes = this.body.nodeIndices.map(id => this.body.nodes[id]); + const nodes = this.body.nodeIndices.reduce((acc, id) => { + acc.set(id, this.body.nodes[id]); + return acc; + }, new Map()); const levels = this.hierarchical.levels; if (this.options.hierarchical.shakeTowards === "roots") { - this.hierarchical.levels = fillLevelsByDirectionRoots( - nodes, - this.hierarchical.levels - ); + this.hierarchical.levels = fillLevelsByDirectionRoots(nodes, levels); } else { - this.hierarchical.levels = fillLevelsByDirectionLeaves( - nodes, - this.hierarchical.levels - ); + this.hierarchical.levels = fillLevelsByDirectionLeaves(nodes, levels); } this.hierarchical.setMinLevelToZero(this.body.nodes); diff --git a/lib/network/modules/layout-engine/index.ts b/lib/network/modules/layout-engine/index.ts index 50dff0e974..7484c53a9d 100644 --- a/lib/network/modules/layout-engine/index.ts +++ b/lib/network/modules/layout-engine/index.ts @@ -1,25 +1,29 @@ type Levels = Record; +type Id = string | number; interface Edge { connected: boolean; from: Node; - fromId: string | number; + fromId: Id; to: Node; - toId: string | number; + toId: Id; } interface Node { - id: string | number; + id: Id; edges: Edge[]; } /** * Try to assign levels to nodes according to their positions in the cyclic “hierarchy”. * - * @param nodes - Nodes of the graph. + * @param nodes - Visible 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: Node[], levels: Levels): Levels { +function fillLevelsByDirectionCyclic( + nodes: Map, + levels: Levels +): Levels { const edges = new Set(); nodes.forEach((node): void => { node.edges.forEach((edge): void => { @@ -48,18 +52,23 @@ function fillLevelsByDirectionCyclic(nodes: Node[], levels: Levels): Levels { /** * Assign levels to nodes according to their positions in the hierarchy. Leaves will be lined up at the bottom and all other nodes as close to their children as possible. * - * @param nodes - Nodes of the graph. + * @param nodes - Visible 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 function fillLevelsByDirectionLeaves( - nodes: Node[], + nodes: Map, levels: Levels = Object.create(null) ): Levels { return fillLevelsByDirection( // Pick only leaves (nodes without children). - (node): boolean => !node.edges.every((edge): boolean => edge.to === node), + (node): boolean => + node.edges + // Take only visible nodes into account. + .filter((edge): boolean => nodes.has(edge.toId)) + // Check that all edges lead to this node (leaf). + .every((edge): boolean => edge.to === node), // Use the lowest level. (newLevel, oldLevel): boolean => oldLevel > newLevel, // Go against the direction of the edges. @@ -72,18 +81,23 @@ export function fillLevelsByDirectionLeaves( /** * Assign levels to nodes according to their positions in the hierarchy. Roots will be lined up at the top and all nodes as close to their parents as possible. * - * @param nodes - Nodes of the graph. + * @param nodes - Visible 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 function fillLevelsByDirectionRoots( - nodes: Node[], + nodes: Map, levels: Levels = Object.create(null) ): Levels { return fillLevelsByDirection( // Pick only roots (nodes without parents). - (node): boolean => !node.edges.every((edge): boolean => edge.from === node), + (node): boolean => + node.edges + // Take only visible nodes into account. + .filter((edge): boolean => nodes.has(edge.toId)) + // Check that all edges lead from this node (root). + .every((edge): boolean => edge.from === node), // Use the highest level. (newLevel, oldLevel): boolean => oldLevel < newLevel, // Go in the direction of the edges. @@ -99,7 +113,7 @@ export function fillLevelsByDirectionRoots( * @param isEntryNode - Checks and return true if the graph should be traversed from this node. * @param shouldLevelBeReplaced - Checks and returns true if the level of given node should be updated to the new value. * @param direction - Wheter the graph should be traversed in the direction of the edges `"to"` or in the other way `"from"`. - * @param nodes - Nodes of the graph. + * @param nodes - Visible 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. @@ -108,25 +122,35 @@ function fillLevelsByDirection( isEntryNode: (node: Node) => boolean, shouldLevelBeReplaced: (newLevel: number, oldLevel: number) => boolean, direction: "to" | "from", - nodes: Node[], + nodes: Map, levels: Levels ): Levels { - const limit = nodes.length; - const edgeIdProp = direction + "Id"; + const limit = nodes.size; + const edgeIdProp: "fromId" | "toId" = (direction + "Id") as "fromId" | "toId"; const newLevelDiff = direction === "to" ? 1 : -1; - for (const entryNode of nodes) { - if (isEntryNode(entryNode)) { + for (const [entryNodeId, entryNode] of nodes) { + if ( + // Skip if the node is not visible. + !nodes.has(entryNodeId) || + // Skip if the node is not an entry node. + !isEntryNode(entryNode) + ) { continue; } // Line up all the entry nodes on level 0. - levels[entryNode.id] = 0; + levels[entryNodeId] = 0; const stack: Node[] = [entryNode]; let done = 0; let node: Node | undefined; while ((node = stack.pop())) { + if (!nodes.has(entryNodeId)) { + // Skip if the node is not visible. + continue; + } + const newLevel = levels[node.id] + newLevelDiff; node.edges @@ -137,7 +161,11 @@ function fillLevelsByDirection( // Ignore circular edges. edge.to !== edge.from && // Ignore edges leading to the node that's currently being processed. - edge[direction] !== node + edge[direction] !== node && + // Ignore edges connecting to an invisible node. + nodes.has(edge.toId) && + // Ignore edges connecting from an invisible node. + nodes.has(edge.fromId) ) .forEach((edge): void => { const targetNodeId = edge[edgeIdProp];