diff --git a/docs/network/layout.html b/docs/network/layout.html index 2b11a8904..98874895e 100644 --- a/docs/network/layout.html +++ b/docs/network/layout.html @@ -66,7 +66,8 @@

Options

edgeMinimization: true, parentCentralization: true, direction: 'UD', // UD, DU, LR, RL - sortMethod: 'hubsize' // hubsize, directed + sortMethod: 'hubsize', // hubsize, directed + userControlsFreeAxis: false } } } @@ -103,6 +104,20 @@

Options

hierarchical.sortMethodString'hubsize' The algorithm used to ascertain the levels of the nodes based on the data. The possible options are: hubsize, directed.

Hubsize takes the nodes with the most edges and puts them at the top. From that the rest of the hierarchy is evaluated.

Directed adheres to the to and from data of the edges. A --> B so B is a level lower than A. + + hierarchical.userControlsFreeAxis + Boolean + false + Whether or not the value specified by a node's Dataset x or y values will affect a node's layout along its "free" axis.

+ If the hierarchical layout direction is either "DU" or "UD", vis.js will layout the node using the Node's x property, + or if directionis either "LR" or "RL", vis.js will use the node's y property.

+ If the property vis wants to use (e.g., the x or y property on the node within the target Dataset) + is undefined, the default behavior is invoked, as though this option were set to false. This provides the ability to initally utilize Vis's default layout, + then later call Network.storePositions() and have the hierarchical layout engine respect the retreived values.

+ 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.

+ This option is useful with physics disabled. + diff --git a/examples/network/other/manipulation.html b/examples/network/other/manipulation.html index f86b6b90c..5921790a0 100644 --- a/examples/network/other/manipulation.html +++ b/examples/network/other/manipulation.html @@ -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; @@ -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) { @@ -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() { @@ -187,6 +293,13 @@

Editing the nodes and edges (localized)


+
diff --git a/lib/network/Network.js b/lib/network/Network.js index f0f3a240c..c5ee7226d 100644 --- a/lib/network/Network.js +++ b/lib/network/Network.js @@ -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 diff --git a/lib/network/modules/LayoutEngine.js b/lib/network/modules/LayoutEngine.js index ad907ebd8..17fdc7954 100644 --- a/lib/network/modules/LayoutEngine.js +++ b/lib/network/modules/LayoutEngine.js @@ -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); } } @@ -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] + } + } + } + } } } diff --git a/lib/network/options.js b/lib/network/options.js index ff614a5df..12e0c84f7 100644 --- a/lib/network/options.js +++ b/lib/network/options.js @@ -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 } @@ -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: {