Browse Source

added public ready versions of kamadakawai and adaptive layout, clustering bugfixes and reactive network.

kamadaKawai
Alex de Mulder 8 years ago
parent
commit
64c1995d81
8 changed files with 221 additions and 108 deletions
  1. +4
    -0
      HISTORY.md
  2. +109
    -53
      dist/vis.js
  3. +2
    -0
      docs/network/layout.html
  4. +3
    -1
      docs/network/physics.html
  5. +35
    -1
      lib/network/modules/Canvas.js
  6. +60
    -50
      lib/network/modules/LayoutEngine.js
  7. +3
    -2
      lib/network/modules/PhysicsEngine.js
  8. +5
    -1
      lib/network/options.js

+ 4
- 0
HISTORY.md View File

@ -19,6 +19,10 @@ http://visjs.org
- Added Adaptive timestep to the physics solvers for increased performance during stabilization.
- Fixed bugs in clustering algorithm.
- Greatly improved performance in clustering.
- Fixed find node return types.
- Made the network keep its 'view' during a change of the size of the container.
- Added improvedLayout as experimental option for greatly improved stabilization times.
- Added adaptiveTimestep as experimental option for greatly improved stabilization times.
## 2015-07-27, version 4.7.0

+ 109
- 53
dist/vis.js View File

@ -5,7 +5,7 @@
* A dynamic, browser-based visualization library.
*
* @version 4.7.1-SNAPSHOT
* @date 2015-08-16
* @date 2015-08-19
*
* @license
* Copyright (C) 2011-2014 Almende B.V, http://almende.com
@ -10642,7 +10642,7 @@ return /******/ (function(modules) { // webpackBootstrap
if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax;
if (options.backgroundColor !== undefined) this._setBackgroundColor(options.backgroundColor);
if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition;
if (options.cameraState !== undefined) cameraPosition = options.cameraState;
if (cameraPosition !== undefined) {
this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical);
@ -33079,7 +33079,8 @@ return /******/ (function(modules) { // webpackBootstrap
onlyDynamicEdges: false,
fit: true
},
timestep: 0.5
timestep: 0.5,
adaptiveTimestep: true
};
util.extend(this.options, this.defaultOptions);
this.timestep = 0.5;
@ -33627,7 +33628,7 @@ return /******/ (function(modules) { // webpackBootstrap
}
// enable adaptive timesteps
this.adaptiveTimestep = true;
this.adaptiveTimestep = true && this.options.adaptiveTimestep;
// this sets the width of all nodes initially which could be required for the avoidOverlap
this.body.emitter.emit("_resizeNodes");
@ -36147,6 +36148,7 @@ return /******/ (function(modules) { // webpackBootstrap
this.pixelRatio = 1;
this.resizeTimer = undefined;
this.resizeFunction = this._onResize.bind(this);
this.cameraState = {};
this.options = {};
this.defaultOptions = {
@ -36219,6 +36221,42 @@ return /******/ (function(modules) { // webpackBootstrap
this.setSize();
this.body.emitter.emit("_redraw");
}
/**
* Get and store the cameraState
* @private
*/
}, {
key: '_getCameraState',
value: function _getCameraState() {
this.cameraState.previousWidth = this.frame.canvas.width;
this.cameraState.scale = this.body.view.scale;
this.cameraState.position = this.DOMtoCanvas({ x: 0.5 * this.frame.canvas.width, y: 0.5 * this.frame.canvas.height });
}
/**
* Set the cameraState
* @private
*/
}, {
key: '_setCameraState',
value: function _setCameraState() {
if (this.cameraState.scale !== undefined) {
this.body.view.scale = this.body.view.scale * (this.frame.canvas.clientWidth / this.cameraState.previousWidth);
// this comes from the view module.
var viewCenter = this.DOMtoCanvas({
x: 0.5 * this.frame.canvas.clientWidth,
y: 0.5 * this.frame.canvas.clientHeight
});
var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node
x: viewCenter.x - this.cameraState.position.x,
y: viewCenter.y - this.cameraState.position.y
};
this.body.view.translation.x += distanceFromCenter.x * this.body.view.scale;
this.body.view.translation.y += distanceFromCenter.y * this.body.view.scale;
}
}
}, {
key: '_prepareValue',
value: function _prepareValue(value) {
@ -36360,6 +36398,7 @@ return /******/ (function(modules) { // webpackBootstrap
var width = arguments.length <= 0 || arguments[0] === undefined ? this.options.width : arguments[0];
var height = arguments.length <= 1 || arguments[1] === undefined ? this.options.height : arguments[1];
this._getCameraState();
width = this._prepareValue(width);
height = this._prepareValue(height);
@ -36403,7 +36442,7 @@ return /******/ (function(modules) { // webpackBootstrap
oldHeight: Math.round(oldHeight / this.pixelRatio)
});
}
this._setCameraState();
return emitEvent;
}
}, {
@ -38887,6 +38926,7 @@ return /******/ (function(modules) { // webpackBootstrap
this.defaultOptions = {
randomSeed: undefined,
improvedLayout: true,
hierarchical: {
enabled: false,
levelSeparation: 150,
@ -38921,7 +38961,7 @@ return /******/ (function(modules) { // webpackBootstrap
value: function setOptions(options, allOptions) {
if (options !== undefined) {
var prevHierarchicalState = this.options.hierarchical.enabled;
util.selectiveDeepExtend(["randomSeed", "improvedLayout"], this.options, options);
util.mergeOptions(this.options, options, 'hierarchical');
if (options.randomSeed !== undefined) {
this.initialRandomSeed = options.randomSeed;
@ -39051,58 +39091,70 @@ return /******/ (function(modules) { // webpackBootstrap
}, {
key: 'layoutNetwork',
value: function layoutNetwork() {
// first check if we should KamadaKawai to layout. The threshold is if less than half of the visible
// nodes have predefined positions we use this.
var positionDefined = 0;
for (var i = 0; i < this.body.nodeIndices.length; i++) {
var node = this.body.nodes[this.body.nodeIndices[i]];
if (node.predefinedPosition === true) {
positionDefined += 1;
}
}
// if less than half of the nodes have a predefined position we continue
if (positionDefined < 0.5 * this.body.nodeIndices.length) {
var levels = 0;
var clusterThreshold = 100;
// if there are a lot of nodes, we cluster before we run the algorithm.
if (this.body.nodeIndices.length > clusterThreshold) {
var startLength = this.body.nodeIndices.length;
while (this.body.nodeIndices.length > clusterThreshold) {
levels += 1;
// if there are many nodes we do a hubsize cluster
if (levels % 3 === 0) {
this.body.modules.clustering.clusterBridges();
} else {
this.body.modules.clustering.clusterOutliers();
}
if (this.options.hierarchical.enabled !== true && this.options.improvedLayout === true) {
// first check if we should KamadaKawai to layout. The threshold is if less than half of the visible
// nodes have predefined positions we use this.
var positionDefined = 0;
for (var i = 0; i < this.body.nodeIndices.length; i++) {
var node = this.body.nodes[this.body.nodeIndices[i]];
if (node.predefinedPosition === true) {
positionDefined += 1;
}
// increase the size of the edges
this.body.modules.kamadaKawai.setOptions({ springLength: Math.max(150, 2 * startLength) });
}
// position the system for these nodes and edges
this.body.modules.kamadaKawai.solve(this.body.nodeIndices, this.body.edgeIndices, true);
// uncluster all clusters
if (levels > 0) {
var clustersPresent = true;
while (clustersPresent === true) {
clustersPresent = false;
for (var i = 0; i < this.body.nodeIndices.length; i++) {
if (this.body.nodes[this.body.nodeIndices[i]].isCluster === true) {
clustersPresent = true;
this.body.modules.clustering.openCluster(this.body.nodeIndices[i], {}, false);
}
// if less than half of the nodes have a predefined position we continue
if (positionDefined < 0.5 * this.body.nodeIndices.length) {
var levels = 0;
var clusterThreshold = 100;
// if there are a lot of nodes, we cluster before we run the algorithm.
if (this.body.nodeIndices.length > clusterThreshold) {
var startLength = this.body.nodeIndices.length;
while (this.body.nodeIndices.length > clusterThreshold) {
levels += 1;
var before = this.body.nodeIndices.length;
// if there are many nodes we do a hubsize cluster
if (levels % 3 === 0) {
this.body.modules.clustering.clusterBridges();
} else {
this.body.modules.clustering.clusterOutliers();
}
var after = this.body.nodeIndices.length;
if (before == after && levels % 3 !== 0) {
this._declusterAll();
console.info("This network could not be positioned by this version of the improved layout algorithm.");
return;
}
}
if (clustersPresent === true) {
this.body.emitter.emit('_dataChanged');
}
// increase the size of the edges
this.body.modules.kamadaKawai.setOptions({ springLength: Math.max(150, 2 * startLength) });
}
}
// reposition all bezier nodes.
this.body.emitter.emit("_repositionBezierNodes");
// position the system for these nodes and edges
this.body.modules.kamadaKawai.solve(this.body.nodeIndices, this.body.edgeIndices, true);
// uncluster all clusters
this._declusterAll();
// reposition all bezier nodes.
this.body.emitter.emit("_repositionBezierNodes");
}
}
}
}, {
key: '_declusterAll',
value: function _declusterAll() {
var clustersPresent = true;
while (clustersPresent === true) {
clustersPresent = false;
for (var i = 0; i < this.body.nodeIndices.length; i++) {
if (this.body.nodes[this.body.nodeIndices[i]].isCluster === true) {
clustersPresent = true;
this.body.modules.clustering.openCluster(this.body.nodeIndices[i], {}, false);
}
}
if (clustersPresent === true) {
this.body.emitter.emit('_dataChanged');
}
}
}
}, {
@ -40779,6 +40831,7 @@ return /******/ (function(modules) { // webpackBootstrap
},
layout: {
randomSeed: { 'undefined': 'undefined', number: number },
improvedLayout: { boolean: boolean },
hierarchical: {
enabled: { boolean: boolean },
levelSeparation: { number: number },
@ -40932,6 +40985,7 @@ return /******/ (function(modules) { // webpackBootstrap
__type__: { object: object, boolean: boolean }
},
timestep: { number: number },
adaptiveTimestep: { boolean: boolean },
__type__: { object: object, boolean: boolean }
},
@ -41071,6 +41125,7 @@ return /******/ (function(modules) { // webpackBootstrap
},
layout: {
//randomSeed: [0, 0, 500, 1],
//improvedLayout: true,
hierarchical: {
enabled: false,
levelSeparation: [150, 20, 500, 5],
@ -41140,6 +41195,7 @@ return /******/ (function(modules) { // webpackBootstrap
solver: ['barnesHut', 'forceAtlas2Based', 'repulsion', 'hierarchicalRepulsion'],
timestep: [0.5, 0.01, 1, 0.01]
},
//adaptiveTimestep: true
global: {
locale: ['en', 'nl']
}

+ 2
- 0
docs/network/layout.html View File

@ -101,6 +101,7 @@
var options = {
layout: {
randomSeed: undefined,
improvedLayout:true,
hierarchical: {
enabled:false,
levelSeparation: 150,
@ -127,6 +128,7 @@ network.setOptions(options);
<table class="options" id="optionTable">
<tr><th>Name</th><th>Type</th><th>Default</th><td>Description</td></tr>
<tr><td>randomSeed</td><td>Number</td><td><code>undefined</code></td> <td>When NOT using the hierarchical layout, the nodes are randomly positioned initially. This means that the settled result is different every time. If you provide a random seed manually, the layout will be the same every time. Ideally you try with an undefined seed, reload until you are happy with the layout and use the <code>getSeed()</code> method to ascertain the seed.</td></tr>
<tr id="layout"><td>improvedLayout</td><td>Boolean</td><td><code>true</code></td> <td>When enabled, the network will use the Kamada Kawai algorithm for initial layout. For networks larger than 100 nodes, clustering will be performed automatically to reduce the amount of nodes. This can greatly improve the stabilization times. If the network is very interconnected (no or few leaf nodes), this may not work and it will revert back to the old method. Performance will be improved in the future.</td></tr>
<tr class='toggle collapsible' onclick="toggleTable('optionTable','hierarchical', this);"><td><span parent="repulsion" class="right-caret"></span> hierarchical</td><td>Object or Boolean</td><td><code>Object</code></td> <td>When true, the layout engine positions the nodes in a hierarchical fashion using default settings. For customization you can supply an object.</td></tr>
<tr parent="hierarchical" class="hidden"><td class="indent">hierarchical.enabled</td><td>Boolean</td><td><code>false</code></td> <td>Toggle the usage of the hierarchical layout system. If this option is not defined, it is set to true if any of the properties in this object are defined.</td></tr>
<tr parent="hierarchical" class="hidden"><td class="indent">hierarchical.levelSeparation</td><td>Number</td><td><code>150</code></td> <td>The distance between the different levels.</td></tr>

+ 3
- 1
docs/network/physics.html View File

@ -138,7 +138,8 @@ var options = {
onlyDynamicEdges: false,
fit: true
},
timestep: 0.5
timestep: 0.5,
adaptiveTimestep: true
}
}
@ -201,6 +202,7 @@ network.setOptions(options);
<tr parent="stabilization" class="hidden"><td class="indent">stabilization.onlyDynamicEdges</td> <td>Boolean</td> <td><code>false</code></td> <td>If you have predefined the position of all nodes and only want to stabilize the dynamic smooth edges, set this to true. It freezes all nodes except the invisible dynamic smooth curve support nodes. If you want the visible nodes to move and stabilize, do not use this.</td></tr>
<tr parent="stabilization" class="hidden"><td class="indent">stabilization.fit</td> <td>Boolean</td> <td><code>true</code></td> <td>Toggle whether or not you want the view to zoom to fit all nodes when the stabilization is finished.</td></tr>
<tr><td>timestep</td> <td>Number</td> <td><code>0.5</code></td> <td>The physics simulation is discrete. This means we take a step in time, calculate the forces, move the nodes and take another step. If you increase this number the steps will be too large and the network can get unstable. If you see a lot of jittery movement in the network, you may want to reduce this value a little.</td></tr>
<tr><td>adaptiveTimestep</td> <td>Boolean</td> <td><code>true</code></td> <td>If this is enabled, the timestep will intelligently be adapted <b>(only during the stabilization stage if stabilization is enabled!)</b> to greatly decrease stabilization times. The timestep configured above is taken as the minimum timestep. <a href="layout.html#layout" target="_blank">This can be further improved by using the improvedLayout algorithm</a>.</td></tr>
</table>
</div>

+ 35
- 1
lib/network/modules/Canvas.js View File

@ -16,6 +16,7 @@ class Canvas {
this.pixelRatio = 1;
this.resizeTimer = undefined;
this.resizeFunction = this._onResize.bind(this);
this.cameraState = {};
this.options = {};
this.defaultOptions = {
@ -82,6 +83,38 @@ class Canvas {
this.body.emitter.emit("_redraw");
}
/**
* Get and store the cameraState
* @private
*/
_getCameraState() {
this.cameraState.previousWidth = this.frame.canvas.width;
this.cameraState.scale = this.body.view.scale;
this.cameraState.position = this.DOMtoCanvas({x: 0.5 * this.frame.canvas.width, y: 0.5 * this.frame.canvas.height});
}
/**
* Set the cameraState
* @private
*/
_setCameraState() {
if (this.cameraState.scale !== undefined) {
this.body.view.scale = this.body.view.scale * (this.frame.canvas.clientWidth / this.cameraState.previousWidth);
// this comes from the view module.
var viewCenter = this.DOMtoCanvas({
x: 0.5 * this.frame.canvas.clientWidth,
y: 0.5 * this.frame.canvas.clientHeight
});
var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node
x: viewCenter.x - this.cameraState.position.x,
y: viewCenter.y - this.cameraState.position.y
};
this.body.view.translation.x += distanceFromCenter.x * this.body.view.scale;
this.body.view.translation.y += distanceFromCenter.y * this.body.view.scale;
}
}
_prepareValue(value) {
if (typeof value === 'number') {
return value + 'px';
@ -194,6 +227,7 @@ class Canvas {
* or '30%')
*/
setSize(width = this.options.width, height = this.options.height) {
this._getCameraState();
width = this._prepareValue(width);
height= this._prepareValue(height);
@ -238,7 +272,7 @@ class Canvas {
oldHeight: Math.round(oldHeight / this.pixelRatio)
});
}
this._setCameraState();
return emitEvent;
};

+ 60
- 50
lib/network/modules/LayoutEngine.js View File

@ -14,6 +14,7 @@ class LayoutEngine {
this.defaultOptions = {
randomSeed: undefined,
improvedLayout: true,
hierarchical: {
enabled:false,
levelSeparation: 150,
@ -43,11 +44,9 @@ class LayoutEngine {
setOptions(options, allOptions) {
if (options !== undefined) {
let prevHierarchicalState = this.options.hierarchical.enabled;
util.selectiveDeepExtend(["randomSeed", "improvedLayout"],this.options, options);
util.mergeOptions(this.options, options, 'hierarchical');
if (options.randomSeed !== undefined) {
this.initialRandomSeed = options.randomSeed;
}
if (options.randomSeed !== undefined) {this.initialRandomSeed = options.randomSeed;}
if (this.options.hierarchical.enabled === true) {
if (prevHierarchicalState === true) {
@ -176,59 +175,70 @@ class LayoutEngine {
* cluster them first to reduce the amount.
*/
layoutNetwork() {
// first check if we should KamadaKawai to layout. The threshold is if less than half of the visible
// nodes have predefined positions we use this.
let positionDefined = 0;
for (let i = 0; i < this.body.nodeIndices.length; i++) {
let node = this.body.nodes[this.body.nodeIndices[i]];
if (node.predefinedPosition === true) {
positionDefined += 1;
if (this.options.hierarchical.enabled !== true && this.options.improvedLayout === true) {
// first check if we should KamadaKawai to layout. The threshold is if less than half of the visible
// nodes have predefined positions we use this.
let positionDefined = 0;
for (let i = 0; i < this.body.nodeIndices.length; i++) {
let node = this.body.nodes[this.body.nodeIndices[i]];
if (node.predefinedPosition === true) {
positionDefined += 1;
}
}
}
// if less than half of the nodes have a predefined position we continue
if (positionDefined < 0.5 * this.body.nodeIndices.length) {
let levels = 0;
let clusterThreshold = 100;
// if there are a lot of nodes, we cluster before we run the algorithm.
if (this.body.nodeIndices.length > clusterThreshold) {
let startLength = this.body.nodeIndices.length;
while(this.body.nodeIndices.length > clusterThreshold) {
levels += 1;
// if there are many nodes we do a hubsize cluster
if (levels % 3 === 0) {
this.body.modules.clustering.clusterBridges();
}
else {
this.body.modules.clustering.clusterOutliers();
}
}
// increase the size of the edges
this.body.modules.kamadaKawai.setOptions({springLength: Math.max(150,2*startLength)})
}
// position the system for these nodes and edges
this.body.modules.kamadaKawai.solve(this.body.nodeIndices, this.body.edgeIndices, true);
// uncluster all clusters
if (levels > 0) {
let clustersPresent = true;
while (clustersPresent === true) {
clustersPresent = false;
for (let i = 0; i < this.body.nodeIndices.length; i++) {
if (this.body.nodes[this.body.nodeIndices[i]].isCluster === true) {
clustersPresent = true;
this.body.modules.clustering.openCluster(this.body.nodeIndices[i], {}, false);
// if less than half of the nodes have a predefined position we continue
if (positionDefined < 0.5 * this.body.nodeIndices.length) {
let levels = 0;
let clusterThreshold = 100;
// if there are a lot of nodes, we cluster before we run the algorithm.
if (this.body.nodeIndices.length > clusterThreshold) {
let startLength = this.body.nodeIndices.length;
while (this.body.nodeIndices.length > clusterThreshold) {
levels += 1;
let before = this.body.nodeIndices.length;
// if there are many nodes we do a hubsize cluster
if (levels % 3 === 0) {
this.body.modules.clustering.clusterBridges();
}
else {
this.body.modules.clustering.clusterOutliers();
}
let after = this.body.nodeIndices.length;
if (before == after && levels % 3 !== 0) {
this._declusterAll();
console.info("This network could not be positioned by this version of the improved layout algorithm.");
return;
}
}
if (clustersPresent === true) {
this.body.emitter.emit('_dataChanged');
}
// increase the size of the edges
this.body.modules.kamadaKawai.setOptions({springLength: Math.max(150, 2 * startLength)})
}
// position the system for these nodes and edges
this.body.modules.kamadaKawai.solve(this.body.nodeIndices, this.body.edgeIndices, true);
// uncluster all clusters
this._declusterAll();
// reposition all bezier nodes.
this.body.emitter.emit("_repositionBezierNodes");
}
}
}
// reposition all bezier nodes.
this.body.emitter.emit("_repositionBezierNodes");
_declusterAll() {
let clustersPresent = true;
while (clustersPresent === true) {
clustersPresent = false;
for (let i = 0; i < this.body.nodeIndices.length; i++) {
if (this.body.nodes[this.body.nodeIndices[i]].isCluster === true) {
clustersPresent = true;
this.body.modules.clustering.openCluster(this.body.nodeIndices[i], {}, false);
}
}
if (clustersPresent === true) {
this.body.emitter.emit('_dataChanged');
}
}
}

+ 3
- 2
lib/network/modules/PhysicsEngine.js View File

@ -81,7 +81,8 @@ class PhysicsEngine {
onlyDynamicEdges: false,
fit: true
},
timestep: 0.5
timestep: 0.5,
adaptiveTimestep: true
};
util.extend(this.options, this.defaultOptions);
this.timestep = 0.5;
@ -597,7 +598,7 @@ class PhysicsEngine {
}
// enable adaptive timesteps
this.adaptiveTimestep = true;
this.adaptiveTimestep = true && this.options.adaptiveTimestep;
// this sets the width of all nodes initially which could be required for the avoidOverlap
this.body.emitter.emit("_resizeNodes");

+ 5
- 1
lib/network/options.js View File

@ -117,6 +117,7 @@ let allOptions = {
},
layout: {
randomSeed: { 'undefined': 'undefined', number },
improvedLayout: { boolean },
hierarchical: {
enabled: { boolean },
levelSeparation: { number },
@ -270,6 +271,7 @@ let allOptions = {
__type__: { object, boolean }
},
timestep: { number },
adaptiveTimestep: { boolean },
__type__: { object, boolean }
},
@ -410,6 +412,7 @@ let configureOptions = {
},
layout: {
//randomSeed: [0, 0, 500, 1],
//improvedLayout: true,
hierarchical: {
enabled: false,
levelSeparation: [150, 20, 500, 5],
@ -477,7 +480,8 @@ let configureOptions = {
maxVelocity: [50, 0, 150, 1],
minVelocity: [0.1, 0.01, 0.5, 0.01],
solver: ['barnesHut', 'forceAtlas2Based', 'repulsion', 'hierarchicalRepulsion'],
timestep: [0.5, 0.01, 1, 0.01]
timestep: [0.5, 0.01, 1, 0.01],
//adaptiveTimestep: true
},
global: {
locale: ['en', 'nl']

Loading…
Cancel
Save