Skip to content
This repository has been archived by the owner on Jul 29, 2019. It is now read-only.

Added option layout.hierarchical.userControlsFreeAxis (revision 2) #4183

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
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
17 changes: 16 additions & 1 deletion docs/network/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ <h3>Options</h3>
edgeMinimization: true,
parentCentralization: true,
direction: 'UD', // UD, DU, LR, RL
sortMethod: 'hubsize' // hubsize, directed
sortMethod: 'hubsize', // hubsize, directed
userControlsFreeAxis: false
}
}
}
Expand Down Expand Up @@ -103,6 +104,20 @@ <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.userControlsFreeAxis</td>
<td>Boolean</td>
<td><code>false</code></td>
<td>Whether or not the value specified by a node's <code>Dataset</code> <code>x</code> or <code>y</code> values will affect a node's layout along its "free" axis.<br><br>
If the hierarchical layout <code>direction</code> is either <code>"DU"</code> or <code>"UD"</code>, vis.js will layout the node using the Node's <code>x</code> property,
or if <code>direction</code>is either <code>"LR"</code> or <code>"RL"</code>, vis.js will use the node's <code>y</code> property.<br><br>
If the property vis wants to use (e.g., the <code>x</code> or <code>y</code> property on the node within the target <code>Dataset</code>)
is undefined, the default behavior is invoked, as though this option were set to <code>false</code>. This provides the ability to initally utilize Vis's default layout,
then later call <code>Network.storePositions()</code> and have the hierarchical layout engine respect the retreived values.<br><br>
This option is helpful because updating a hierarchically layed-out graph will trigger a redraw, and without this option, if a node has been moved along its free axis,
it will be returned to its default position.<br><br>
This option is useful with physics disabled.</td>
</tr>
</table>

</div>
Expand Down
115 changes: 114 additions & 1 deletion examples/network/other/manipulation.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,97 @@
// randomly create some nodes and edges
var data = getScaleFreeNetwork(25);
var seed = 2;

function bakeLevels() {
let direction = network.layoutEngine.options.hierarchical.direction;
let levelAxis = "";

//
// direction can only be "UD" | "DU" | "LR" | "RL", other values are caught as errors earlier in the program
// once levels are baked, they should not change even if the user alters the direction of the layout
//

if (direction == "UD" || direction == "DU") {
levelAxis = "y";
}
else {
levelAxis = "x";
}

let nodes = network.body.nodes;
let nodeIds = Object.getOwnPropertyNames(nodes);
let nodeLevelMap = new Map();
let uniqueLevelsInCanvasSpace = new Set();

for (let nodeId of nodeIds) {
let node = nodes[nodeId];
uniqueLevelsInCanvasSpace.add(node[levelAxis]);
if (nodeLevelMap.has(node[levelAxis])) {
nodeLevelMap.get(node[levelAxis]).push(node.id);
}
else {
nodeLevelMap.set(node[levelAxis], [node.id]);
}
}

let orderedUniqueLevelsInCanvasSpace = Array.from(uniqueLevelsInCanvasSpace);
orderedUniqueLevelsInCanvasSpace.sort((a,b) => { return a-b; });
canvasLevelMap = new Map();

for (let i = 0; i < orderedUniqueLevelsInCanvasSpace.length; i++) {
canvasLevelMap.set(orderedUniqueLevelsInCanvasSpace[i], i);
}

let updateList = []

for (let nodeId of nodeIds) {
let node = nodes[nodeId];
updateList.push({
"id": nodeId,
"level": canvasLevelMap.get(node[levelAxis])
});
}

network.body.data.nodes.update(updateList);

}

function storeFreeAxisPositions() {
let direction = network.layoutEngine.options.hierarchical.direction;
let freeAxis = "";

if (direction == "UD" || direction == "DU") {
freeAxis = "x";
}
else {
freeAxis = "y";
}

let positions = network.getPositions();
let keys = Object.getOwnPropertyNames(positions);
let updateList = [];

for (let key of keys) {
let pos = positions[key];
updateList.push({
id: key,
[freeAxis]: pos[freeAxis]
});
}

network.body.data.nodes.update(updateList);
}

function toggleBakeLevelsButton() {
let target = document.getElementById("bake-levels");
let isDisabled = target.hasAttribute("disabled");
if (isDisabled) {
target.removeAttribute("disabled");
}
else {
target.setAttribute("disabled", "")
}
}

function setDefaultLocale() {
var defaultLocal = navigator.language;
Expand Down Expand Up @@ -92,7 +183,10 @@
// create a network
var container = document.getElementById('mynetwork');
var options = {
layout: {randomSeed:seed}, // just to make sure the layout is the same when the locale is changed
configure: true,
layout: {
randomSeed:seed
}, // just to make sure the layout is the same when the locale is changed
locale: document.getElementById('locale').value,
manipulation: {
addNode: function (data, callback) {
Expand Down Expand Up @@ -127,6 +221,18 @@
}
};
network = new vis.Network(container, data, options);
network.on("configChange", (e) => {
if (e.layout) {
if (e.layout.hierarchical) {
if (e.layout.hierarchical === true || e.layout.hierarchical.enabled === false) {
toggleBakeLevelsButton();
}
}
}
});
network.on("dragEnd", (e) => {
storeFreeAxisPositions();
});
}

function clearPopUp() {
Expand Down Expand Up @@ -187,6 +293,13 @@ <h2>Editing the nodes and edges (localized)</h2>
<input type="button" value="cancel" id="cancelButton" />
</div>
<br />
<input disabled type="button" id="bake-levels" value="Bake hierarchical levels" onClick="bakeLevels()"
title="
+ This will permanently set the levels assigned to each node by Vis.js' built-in hierarchical layout algorithm.
+ This button is active when 'layout.hierarchical' is enabled.
+ In a realworld use-case, the programmer will probably manually set each node's levels according to their own algorithm.
+ 'userControlsFreeAxis' is best used with physics disabled.
+ It is not recommended to swap between directions when the user is in control of the free axis." >
<div id="mynetwork"></div>

</body>
Expand Down
2 changes: 1 addition & 1 deletion lib/network/Network.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ Network.prototype.setOptions = function (options) {

// the hierarchical system can adapt the edges and the physics to it's own options because not all combinations work with the hierarichical system.
options = this.layoutEngine.setOptions(options.layout, options);

this.canvas.setOptions(options); // options for canvas are in globals

// pass the options to the modules
Expand Down
42 changes: 40 additions & 2 deletions lib/network/modules/LayoutEngine.js
Original file line number Diff line number Diff line change
Expand Up @@ -743,11 +743,17 @@ class LayoutEngine {
this._determineLevelsCustomCallback();
}
}



//
// to be iterated over later, in the userControlsFreeAxis option section
// we iterate over nodeIds once to call ensureLevel, so we might as well
// capture them now
//
let nodeIds = [];
// fallback for cases where there are nodes but no edges
for (let nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
nodeIds.push(nodeId);
this.hierarchical.ensureLevel(nodeId);
}
}
Expand All @@ -765,6 +771,38 @@ class LayoutEngine {

// shift to center so gravity does not have to do much
this._shiftToCenter();

//
// if option hierarchical.userControlsFreeAxis is set (to true)
// we will attempt to set each node's free axis position based on Dataset value
// if the node's free axis position is undefined, we skip it and keep the original LayoutEngine-generated value
//
// the free axis in direction DU/UD is x
// the free axis in direction LR/RL is y
//
if (this.options.hierarchical.userControlsFreeAxis) {
let direction = this.options.hierarchical.direction;
let dataSet = this.body.data.nodes.getDataSet();
let freeAxis = "";

if (direction === "UD" || direction === "DU") {
freeAxis = "x";
}
else { // direction == "LR" || direction == "RL", any other values are caught as errors earlier in the program
freeAxis = "y";
}

for (let nodeId of nodeIds) {
let targetPosition = dataSet._data[nodeId][freeAxis];
if (targetPosition != undefined) {
this.body.nodes[nodeId][freeAxis] = targetPosition;
}
else { // explicitly assign it the freeaxis value as issued by layout engine
dataSet._data[nodeId][freeAxis] = this.body.nodes[nodeId][freeAxis]
}
}
}

}
}
}
Expand Down
4 changes: 3 additions & 1 deletion lib/network/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ let allOptions = {
parentCentralization: { boolean: bool },
direction: { string: ['UD', 'DU', 'LR', 'RL'] }, // UD, DU, LR, RL
sortMethod: { string: ['hubsize', 'directed'] }, // hubsize, directed
userControlsFreeAxis: { boolean: bool },
__type__: { object, boolean: bool }
},
__type__: { object }
Expand Down Expand Up @@ -553,7 +554,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
userControlsFreeAxis: false
}
},
interaction: {
Expand Down