Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(hierarchical-layout): add shake towards option #177

Merged
merged 5 commits into from
Oct 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/network/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ <h3>Options</h3>
edgeMinimization: true,
parentCentralization: true,
direction: 'UD', // UD, DU, LR, RL
sortMethod: 'hubsize' // hubsize, directed
sortMethod: 'hubsize', // hubsize, directed
shakeTowards: 'leaves' // roots, leaves
}
}
}
Expand Down Expand Up @@ -141,6 +142,7 @@ <h3>Options</h3>
<tr parent="hierarchical" class="hidden"><td class="indent">hierarchical.sortMethod</td><td>String</td><td><code>'hubsize'</code></td> <td>The algorithm used to ascertain the levels of the nodes based on the data. The possible options are: <code>hubsize, directed</code>. <br><br>
Hubsize takes the nodes with the most edges and puts them at the top. From that the rest of the hierarchy is evaluated. <br><br>
Directed adheres to the to and from data of the edges. A --> B so B is a level lower than A.</td></tr>
<tr parent="hierarchical" class="hidden"><td class="indent">hierarchical.shakeTowards</td><td>String</td><td><code>'roots'</code></td> <td>Controls whether in <code>directed</code> layout should all the roots be lined up at the top and their child nodes as close to their roots as possible (<code>roots</code>) or all the leaves lined up at the bottom and their parents as close to their children as possible (<code>leaves</code>, default).</td></tr>
</table>

</div>
Expand All @@ -159,4 +161,4 @@ <h3>Options</h3>
<script src="../js/tipuesearch.config.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Tipue-Search/5.0.0/tipuesearch.min.js"></script>
<!-- controller -->
<script src="../js/main.js"></script>
<script src="../js/main.js"></script>
69 changes: 44 additions & 25 deletions examples/network/layout/hierarchicalLayoutMethods.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

<script type="text/javascript">
var network = null;
var layoutMethod = "directed";

function destroy() {
if (network !== null) {
Expand Down Expand Up @@ -65,7 +64,8 @@
var options = {
layout: {
hierarchical: {
sortMethod: layoutMethod
sortMethod: document.getElementById("layout-method").value,
shakeTowards: document.getElementById("shake-towards").value
}
},
edges: {
Expand All @@ -80,29 +80,48 @@

</head>

<body onload="draw();">
<h2>Hierarchical layout difference</h2>
<div style="width:700px; font-size:14px; text-align: justify;">
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 with the dropdown box below.
</div>
Layout method:
<select id="layout">
<option value="hubsize">hubsize</option>
<option value="directed">directed</option>
</select><br/>
<br />
<body onload="draw();">
<h2>Hierarchical layout difference</h2>
<div style="width:700px; font-size:14px; text-align: justify;">
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.
</div>

<div id="mynetwork"></div>
<p>
Layout method:
<input type="button" id="layout-method" value="directed" />
</p>
<p>
Shake towards:
<input type="button" id="shake-towards" value="leaves" />
(Applies to <code>directed</code> only.)
</p>

<p id="selection"></p>
<script language="JavaScript">
var dropdown = document.getElementById("layout");
dropdown.onchange = function() {
layoutMethod = dropdown.value;
draw();
}
</script>
</body>
<div id="mynetwork"></div>

<p id="selection"></p>
<script language="JavaScript">
(function() {
const values = ["hubsize", "directed"];
var button = document.getElementById("layout-method");
button.onclick = function() {
button.value =
values[(values.indexOf(button.value) + 1) % values.length];
draw();
};
})();
(function() {
const values = ["roots", "leaves"];
var button = document.getElementById("shake-towards");
button.onclick = function() {
button.value =
values[(values.indexOf(button.value) + 1) % values.length];
draw();
};
})();
</script>
</body>
</html>
24 changes: 19 additions & 5 deletions lib/network/modules/LayoutEngine.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ import TimSort from 'timsort';
import util from 'vis-util';
import NetworkUtil from '../NetworkUtil';
import { HorizontalStrategy, VerticalStrategy } from './components/DirectionStrategy.js';
import { fillLevelsByDirection } from './layout-engine'
import {
fillLevelsByDirectionLeaves,
fillLevelsByDirectionRoots
} from "./layout-engine";


/**
Expand Down Expand Up @@ -1500,10 +1503,21 @@ class LayoutEngine {
* @private
*/
_determineLevelsDirected() {
this.hierarchical.levels = fillLevelsByDirection(
this.body.nodeIndices.map(id => this.body.nodes[id]),
this.hierarchical.levels
);
const nodes = this.body.nodeIndices.map(id => this.body.nodes[id]);
const levels = this.hierarchical.levels;

if (this.options.hierarchical.shakeTowards === "roots") {
this.hierarchical.levels = fillLevelsByDirectionRoots(
nodes,
this.hierarchical.levels
);
} else {
this.hierarchical.levels = fillLevelsByDirectionLeaves(
nodes,
this.hierarchical.levels
);
}

this.hierarchical.setMinLevelToZero(this.body.nodes);
}

Expand Down
102 changes: 82 additions & 20 deletions lib/network/modules/layout-engine/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,46 +46,108 @@ function fillLevelsByDirectionCyclic(nodes: Node[], levels: Levels): Levels {
}

/**
* Assign levels to nodes according to their positions in the hierarchy.
* 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 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[],
levels: Levels = Object.create(null)
): Levels {
return fillLevelsByDirection(
// Pick only leaves (nodes without children).
(node): boolean => !node.edges.every((edge): boolean => edge.to === node),
// Use the lowest level.
(newLevel, oldLevel): boolean => oldLevel > newLevel,
// Go against the direction of the edges.
"from",
nodes,
levels
);
}

/**
* 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 levels - If present levels will be added to it, if not a new object will be created.
*
* @returns Populated node levels.
*/
export function fillLevelsByDirection(
export function fillLevelsByDirectionRoots(
nodes: Node[],
levels: Levels = Object.create(null)
): Levels {
return fillLevelsByDirection(
// Pick only roots (nodes without parents).
(node): boolean => !node.edges.every((edge): boolean => edge.from === node),
// Use the highest level.
(newLevel, oldLevel): boolean => oldLevel < newLevel,
// Go in the direction of the edges.
"to",
nodes,
levels
);
}

/**
* Assign levels to nodes according to their positions in the hierarchy.
*
* @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 levels - If present levels will be added to it, if not a new object will be created.
*
* @returns Populated node levels.
*/
function fillLevelsByDirection(
isEntryNode: (node: Node) => boolean,
shouldLevelBeReplaced: (newLevel: number, oldLevel: number) => boolean,
direction: "to" | "from",
nodes: Node[],
levels: Levels
): Levels {
const limit = nodes.length;
const edgeIdProp = direction + "Id";
const newLevelDiff = direction === "to" ? 1 : -1;

for (const leaf of nodes) {
if (!leaf.edges.every((edge): boolean => edge.to === leaf)) {
// Not a leaf.
for (const entryNode of nodes) {
if (isEntryNode(entryNode)) {
continue;
}

levels[leaf.id] = 0;
const stack: Node[] = [leaf];
// Line up all the entry nodes on level 0.
levels[entryNode.id] = 0;

const stack: Node[] = [entryNode];
let done = 0;
let node: Node | undefined;
while ((node = stack.pop())) {
const edges = node.edges;
const newLevel = levels[node.id] - 1;
const newLevel = levels[node.id] + newLevelDiff;

for (const edge of edges) {
if (!edge.connected || edge.to !== node || edge.to === edge.from) {
continue;
}
node.edges
.filter(
(edge): boolean =>
// Ignore disconnected edges.
edge.connected &&
// Ignore circular edges.
edge.to !== edge.from &&
// Ignore edges leading to the node that's currently being processed.
edge[direction] !== node
)
.forEach((edge): void => {
const targetNodeId = edge[edgeIdProp];
const oldLevel = levels[targetNodeId];

const fromId = edge.fromId;
const oldLevel = levels[fromId];
if (oldLevel == null || oldLevel > newLevel) {
levels[fromId] = newLevel;
stack.push(edge.from);
}
}
if (oldLevel == null || shouldLevelBeReplaced(newLevel, oldLevel)) {
levels[targetNodeId] = newLevel;
stack.push(edge[direction]);
}
});

if (done > limit) {
// This would run forever on a cyclic graph.
Expand Down
4 changes: 3 additions & 1 deletion lib/network/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ let allOptions = {
parentCentralization: { boolean: bool },
direction: { string: ['UD', 'DU', 'LR', 'RL'] }, // UD, DU, LR, RL
sortMethod: { string: ['hubsize', 'directed'] }, // hubsize, directed
shakeTowards: { string: ['leaves', 'roots'] }, // leaves, roots
__type__: { object, boolean: bool }
},
__type__: { object }
Expand Down Expand Up @@ -563,7 +564,8 @@ let configureOptions = {
edgeMinimization: true,
parentCentralization: true,
direction: ['UD', 'DU', 'LR', 'RL'], // UD, DU, LR, RL
sortMethod: ['hubsize', 'directed'] // hubsize, directed
sortMethod: ['hubsize', 'directed'], // hubsize, directed
shakeTowards: ['leaves', 'roots'] // leaves, roots
}
},
interaction: {
Expand Down