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 2 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
118 changes: 117 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;
console.log(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 (nodeId of nodeIds) {
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]);
}
}

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);
}

updateList = []

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

console.log(network);
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 (key of keys) {
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,21 @@
}
};
network = new vis.Network(container, data, options);
network.on("configChange", (e) => {
if (e.layout) {
if (e.layout.hierarchical) {
if (e.layout.hierarchical === true) {
toggleBakeLevelsButton();
}
if (e.layout.hierarchical.enabled === false) {
toggleBakeLevelsButton();
}
}
}
});
network.on("dragEnd", (e) => {
storeFreeAxisPositions();
});
}

function clearPopUp() {
Expand Down Expand Up @@ -187,6 +296,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 both 'layout.hierarchical' and its sub-option 'layout.hierarchical.userControlsFreeAxis' are 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
15 changes: 15 additions & 0 deletions lib/network/Network.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,21 @@ 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);
console.log(this.layoutEngine);
//
// this clears "userControlsFreeAxis" when "hierarchical" is disabled
// without doing this, and performing the following steps:
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May still be a good idea to clear the userControlsFreeAxis option if hierarchical layout is disabled, but the bug I was attempting to fix is probably #4228.

// enable layout.hierarchical
// enable layout.userControlsFreeAxis
// disable layout.hierarchical
// enable layout.hierarchical
// edges were getting placed into the body.nodes in LayoutEngine::setupHierarchicalLayout
//
// is there a better way to perform this, or better place to put this? It feels like a hack.
//
if (this.layoutEngine.options.hierarchical.enabled == false) {
this.layoutEngine.options.hierarchical["userControlsFreeAxis"] = false;
}

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

Expand Down
43 changes: 41 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,39 @@ 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) {
console.log(nodeId);
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