Browse Source

Merge branch 'kamadaKawai' into develop

webworkersNetwork
Alex de Mulder 9 years ago
parent
commit
a007ecd36d
15 changed files with 1752 additions and 486 deletions
  1. +9
    -1
      HISTORY.md
  2. +920
    -241
      dist/vis.js
  3. +2
    -0
      docs/network/layout.html
  4. +3
    -1
      docs/network/physics.html
  5. +9
    -2
      lib/network/Network.js
  6. +3
    -0
      lib/network/gephiParser.js
  7. +35
    -1
      lib/network/modules/Canvas.js
  8. +235
    -177
      lib/network/modules/Clustering.js
  9. +219
    -0
      lib/network/modules/KamadaKawai.js
  10. +82
    -6
      lib/network/modules/LayoutEngine.js
  11. +143
    -34
      lib/network/modules/PhysicsEngine.js
  12. +46
    -0
      lib/network/modules/components/algorithms/FloydWarshall.js
  13. +3
    -0
      lib/network/modules/components/edges/BezierEdgeDynamic.js
  14. +5
    -1
      lib/network/options.js
  15. +38
    -22
      test/networkTest.html

+ 9
- 1
HISTORY.md View File

@ -14,7 +14,15 @@ http://visjs.org
- Fixed cleaning up of nodes. - Fixed cleaning up of nodes.
- Improved the positioning and CSS of the configurator and the color picker. - Improved the positioning and CSS of the configurator and the color picker.
- Fixed dynamic updating of label properties. - Fixed dynamic updating of label properties.
- Added support for labels in edges and titles for both nodes and edges during gephi import.
- Added KamadaKawai layout engine for improved initial layout.
- 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 ## 2015-07-27, version 4.7.0

+ 920
- 241
dist/vis.js
File diff suppressed because it is too large
View File


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

@ -101,6 +101,7 @@
var options = { var options = {
layout: { layout: {
randomSeed: undefined, randomSeed: undefined,
improvedLayout:true,
hierarchical: { hierarchical: {
enabled:false, enabled:false,
levelSeparation: 150, levelSeparation: 150,
@ -127,6 +128,7 @@ network.setOptions(options);
<table class="options" id="optionTable"> <table class="options" id="optionTable">
<tr><th>Name</th><th>Type</th><th>Default</th><td>Description</td></tr> <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><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 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.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> <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, onlyDynamicEdges: false,
fit: true 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.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 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>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> </table>
</div> </div>

+ 9
- 2
lib/network/Network.js View File

@ -24,11 +24,11 @@ import InteractionHandler from './modules/InteractionHandler';
import SelectionHandler from "./modules/SelectionHandler"; import SelectionHandler from "./modules/SelectionHandler";
import LayoutEngine from "./modules/LayoutEngine"; import LayoutEngine from "./modules/LayoutEngine";
import ManipulationSystem from "./modules/ManipulationSystem"; import ManipulationSystem from "./modules/ManipulationSystem";
import Configurator from "./../shared/Configurator";
import Configurator from "./../shared/Configurator";
import Validator from "./../shared/Validator"; import Validator from "./../shared/Validator";
import {printStyle} from "./../shared/Validator"; import {printStyle} from "./../shared/Validator";
import {allOptions, configureOptions} from './options.js'; import {allOptions, configureOptions} from './options.js';
import KamadaKawai from "./modules/KamadaKawai.js"
/** /**
@ -92,6 +92,7 @@ function Network(container, data, options) {
createEdge: function() {}, createEdge: function() {},
getPointer: function() {} getPointer: function() {}
}, },
modules: {},
view: { view: {
scale: 1, scale: 1,
translation: {x: 0, y: 0} translation: {x: 0, y: 0}
@ -119,6 +120,9 @@ function Network(container, data, options) {
this.nodesHandler = new NodesHandler(this.body, this.images, this.groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options this.nodesHandler = new NodesHandler(this.body, this.images, this.groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options
this.edgesHandler = new EdgesHandler(this.body, this.images, this.groups); // Handle adding, deleting and updating of edges as well as global options this.edgesHandler = new EdgesHandler(this.body, this.images, this.groups); // Handle adding, deleting and updating of edges as well as global options
this.body.modules["kamadaKawai"] = new KamadaKawai(this.body,150,0.05); // Layouting algorithm.
this.body.modules["clustering"] = this.clustering;
// create the DOM elements // create the DOM elements
this.canvas._create(); this.canvas._create();
@ -332,6 +336,9 @@ Network.prototype.setData = function (data) {
// emit change in data // emit change in data
this.body.emitter.emit("_dataChanged"); this.body.emitter.emit("_dataChanged");
// emit data loaded
this.body.emitter.emit("_dataLoaded");
// find a stable position or start animating to a stable position // find a stable position or start animating to a stable position
this.body.emitter.emit("initPhysics"); this.body.emitter.emit("initPhysics");
}; };

+ 3
- 0
lib/network/gephiParser.js View File

@ -27,6 +27,8 @@ function parseGephi(gephiJSON, optionsObj) {
edge['from'] = gEdge.source; edge['from'] = gEdge.source;
edge['to'] = gEdge.target; edge['to'] = gEdge.target;
edge['attributes'] = gEdge.attributes; edge['attributes'] = gEdge.attributes;
edge['label'] = gEdge.label;
edge['title'] = gEdge.attributes !== undefined ? gEdge.attributes.title : undefined;
// edge['value'] = gEdge.attributes !== undefined ? gEdge.attributes.Weight : undefined; // edge['value'] = gEdge.attributes !== undefined ? gEdge.attributes.Weight : undefined;
// edge['width'] = edge['value'] !== undefined ? undefined : edgegEdge.size; // edge['width'] = edge['value'] !== undefined ? undefined : edgegEdge.size;
if (gEdge.color && options.inheritColor === false) { if (gEdge.color && options.inheritColor === false) {
@ -44,6 +46,7 @@ function parseGephi(gephiJSON, optionsObj) {
node['x'] = gNode.x; node['x'] = gNode.x;
node['y'] = gNode.y; node['y'] = gNode.y;
node['label'] = gNode.label; node['label'] = gNode.label;
node['title'] = gNode.attributes !== undefined ? gNode.attributes.title : undefined;
if (options.nodes.parseColor === true) { if (options.nodes.parseColor === true) {
node['color'] = gNode.color; node['color'] = gNode.color;
} }

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

@ -16,6 +16,7 @@ class Canvas {
this.pixelRatio = 1; this.pixelRatio = 1;
this.resizeTimer = undefined; this.resizeTimer = undefined;
this.resizeFunction = this._onResize.bind(this); this.resizeFunction = this._onResize.bind(this);
this.cameraState = {};
this.options = {}; this.options = {};
this.defaultOptions = { this.defaultOptions = {
@ -82,6 +83,38 @@ class Canvas {
this.body.emitter.emit("_redraw"); 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) { _prepareValue(value) {
if (typeof value === 'number') { if (typeof value === 'number') {
return value + 'px'; return value + 'px';
@ -194,6 +227,7 @@ class Canvas {
* or '30%') * or '30%')
*/ */
setSize(width = this.options.width, height = this.options.height) { setSize(width = this.options.width, height = this.options.height) {
this._getCameraState();
width = this._prepareValue(width); width = this._prepareValue(width);
height= this._prepareValue(height); height= this._prepareValue(height);
@ -238,7 +272,7 @@ class Canvas {
oldHeight: Math.round(oldHeight / this.pixelRatio) oldHeight: Math.round(oldHeight / this.pixelRatio)
}); });
} }
this._setCameraState();
return emitEvent; return emitEvent;
}; };

+ 235
- 177
lib/network/modules/Clustering.js View File

@ -42,8 +42,9 @@ class ClusterEngine {
} }
for (let i = 0; i < nodesToCluster.length; i++) { for (let i = 0; i < nodesToCluster.length; i++) {
this.clusterByConnection(nodesToCluster[i],options,false);
this.clusterByConnection(nodesToCluster[i],options,true);
} }
this.body.emitter.emit('_dataChanged'); this.body.emitter.emit('_dataChanged');
} }
@ -73,7 +74,9 @@ class ClusterEngine {
// collect the nodes that will be in the cluster // collect the nodes that will be in the cluster
for (let i = 0; i < node.edges.length; i++) { for (let i = 0; i < node.edges.length; i++) {
let edge = node.edges[i]; let edge = node.edges[i];
childEdgesObj[edge.id] = edge;
if (edge.hiddenByCluster !== true) {
childEdgesObj[edge.id] = edge;
}
} }
} }
} }
@ -83,53 +86,72 @@ class ClusterEngine {
/** /**
* Cluster all nodes in the network that have only 1 edge
* @param options
* @param refreshData
*/
clusterOutliers(options, refreshData = true) {
* Cluster all nodes in the network that have only X edges
* @param edgeCount
* @param options
* @param refreshData
*/
clusterByEdgeCount(edgeCount, options, refreshData = true) {
options = this._checkOptions(options); options = this._checkOptions(options);
let clusters = []; let clusters = [];
let usedNodes = {};
let edge, edges, node, nodeId, visibleEdges;
// collect the nodes that will be in the cluster // collect the nodes that will be in the cluster
for (let i = 0; i < this.body.nodeIndices.length; i++) { for (let i = 0; i < this.body.nodeIndices.length; i++) {
let childNodesObj = {}; let childNodesObj = {};
let childEdgesObj = {}; let childEdgesObj = {};
let nodeId = this.body.nodeIndices[i];
let visibleEdges = 0;
let edge;
for (let j = 0; j < this.body.nodes[nodeId].edges.length; j++) {
if (this.body.nodes[nodeId].edges[j].options.hidden === false) {
visibleEdges++;
edge = this.body.nodes[nodeId].edges[j];
nodeId = this.body.nodeIndices[i];
// if this node is already used in another cluster this session, we do not have to re-evaluate it.
if (usedNodes[nodeId] === undefined) {
visibleEdges = 0;
node = this.body.nodes[nodeId];
edges = [];
for (let j = 0; j < node.edges.length; j++) {
edge = node.edges[j];
if (edge.hiddenByCluster !== true) {
edges.push(edge);
}
} }
}
if (visibleEdges === 1) {
// this is an outlier
let childNodeId = this._getConnectedId(edge, nodeId);
if (childNodeId !== nodeId) {
if (options.joinCondition === undefined) {
if (this._checkIfUsed(clusters,nodeId,edge.id) === false && this._checkIfUsed(clusters,childNodeId,edge.id) === false) {
childEdgesObj[edge.id] = edge;
childNodesObj[nodeId] = this.body.nodes[nodeId];
childNodesObj[childNodeId] = this.body.nodes[childNodeId];
}
}
else {
let clonedOptions = this._cloneOptions(this.body.nodes[nodeId]);
if (options.joinCondition(clonedOptions) === true && this._checkIfUsed(clusters,nodeId,edge.id) === false) {
childEdgesObj[edge.id] = edge;
childNodesObj[nodeId] = this.body.nodes[nodeId];
// this node qualifies, we collect its neighbours to start the clustering process.
if (edges.length === edgeCount) {
let gatheringSuccessful = true;
for (let j = 0; j < edges.length; j++) {
edge = edges[j];
let childNodeId = this._getConnectedId(edge, nodeId);
// if unused and if not referencing itself
if (childNodeId !== nodeId && usedNodes[nodeId] === undefined) {
// add the nodes to the list by the join condition.
if (options.joinCondition === undefined) {
childEdgesObj[edge.id] = edge;
childNodesObj[nodeId] = this.body.nodes[nodeId];
childNodesObj[childNodeId] = this.body.nodes[childNodeId];
usedNodes[nodeId] = true;
}
else {
let clonedOptions = this._cloneOptions(this.body.nodes[nodeId]);
if (options.joinCondition(clonedOptions) === true) {
childEdgesObj[edge.id] = edge;
childNodesObj[nodeId] = this.body.nodes[nodeId];
usedNodes[nodeId] = true;
}
else {
// this node does not qualify after all.
gatheringSuccessful = false;
break;
}
}
} }
clonedOptions = this._cloneOptions(this.body.nodes[childNodeId]);
if (options.joinCondition(clonedOptions) === true && this._checkIfUsed(clusters,nodeId,edge.id) === false) {
childEdgesObj[edge.id] = edge;
childNodesObj[childNodeId] = this.body.nodes[childNodeId];
else {
// this node does not qualify after all.
gatheringSuccessful = false;
break;
} }
} }
if (Object.keys(childNodesObj).length > 0 && Object.keys(childEdgesObj).length > 0) {
// add to the cluster queue
if (Object.keys(childNodesObj).length > 0 && Object.keys(childEdgesObj).length > 0 && gatheringSuccessful === true) {
clusters.push({nodes: childNodesObj, edges: childEdgesObj}) clusters.push({nodes: childNodesObj, edges: childEdgesObj})
} }
} }
@ -145,17 +167,26 @@ class ClusterEngine {
} }
} }
/**
* Cluster all nodes in the network that have only 1 edge
* @param options
* @param refreshData
*/
clusterOutliers(options, refreshData = true) {
this.clusterByEdgeCount(1,options,refreshData);
}
_checkIfUsed(clusters, nodeId, edgeId) {
for (let i = 0; i < clusters.length; i++) {
let cluster = clusters[i];
if (cluster.nodes[nodeId] !== undefined || cluster.edges[edgeId] !== undefined) {
return true;
}
}
return false;
/**
* Cluster all nodes in the network that have only 2 edge
* @param options
* @param refreshData
*/
clusterBridges(options, refreshData = true) {
this.clusterByEdgeCount(2,options,refreshData);
} }
/** /**
* suck all connected nodes of a node into the node. * suck all connected nodes of a node into the node.
* @param nodeId * @param nodeId
@ -187,25 +218,31 @@ class ClusterEngine {
// collect the nodes that will be in the cluster // collect the nodes that will be in the cluster
for (let i = 0; i < node.edges.length; i++) { for (let i = 0; i < node.edges.length; i++) {
let edge = node.edges[i]; let edge = node.edges[i];
let childNodeId = this._getConnectedId(edge, parentNodeId);
if (edge.hiddenByCluster !== true) {
let childNodeId = this._getConnectedId(edge, parentNodeId);
if (childNodeId !== parentNodeId) {
if (options.joinCondition === undefined) {
childEdgesObj[edge.id] = edge;
childNodesObj[childNodeId] = this.body.nodes[childNodeId];
}
else {
// clone the options and insert some additional parameters that could be interesting.
let childClonedOptions = this._cloneOptions(this.body.nodes[childNodeId]);
if (options.joinCondition(parentClonedOptions, childClonedOptions) === true) {
// if the child node is not in a cluster (may not be needed now with the edge.hiddenByCluster check)
if (this.clusteredNodes[childNodeId] === undefined) {
if (childNodeId !== parentNodeId) {
if (options.joinCondition === undefined) {
childEdgesObj[edge.id] = edge;
childNodesObj[childNodeId] = this.body.nodes[childNodeId];
}
else {
// clone the options and insert some additional parameters that could be interesting.
let childClonedOptions = this._cloneOptions(this.body.nodes[childNodeId]);
if (options.joinCondition(parentClonedOptions, childClonedOptions) === true) {
childEdgesObj[edge.id] = edge;
childNodesObj[childNodeId] = this.body.nodes[childNodeId];
}
}
}
else {
// swallow the edge if it is self-referencing.
childEdgesObj[edge.id] = edge; childEdgesObj[edge.id] = edge;
childNodesObj[childNodeId] = this.body.nodes[childNodeId];
} }
} }
} }
else {
childEdgesObj[edge.id] = edge;
}
} }
this._cluster(childNodesObj, childEdgesObj, options, refreshData); this._cluster(childNodesObj, childEdgesObj, options, refreshData);
@ -235,18 +272,21 @@ class ClusterEngine {
/** /**
* This function creates the edges that will be attached to the cluster.
* This function creates the edges that will be attached to the cluster
* It looks for edges that are connected to the nodes from the "outside' of the cluster.
* *
* @param childNodesObj * @param childNodesObj
* @param childEdgesObj
* @param newEdges * @param newEdges
* @param options * @param options
* @private * @private
*/ */
_createClusterEdges (childNodesObj, childEdgesObj, newEdges, clusterNodeProperties, clusterEdgeProperties) {
_createClusterEdges (childNodesObj, clusterNodeProperties, clusterEdgeProperties) {
let edge, childNodeId, childNode, toId, fromId, otherNodeId; let edge, childNodeId, childNode, toId, fromId, otherNodeId;
// loop over all child nodes and their edges to find edges going out of the cluster
// these edges will be replaced by clusterEdges.
let childKeys = Object.keys(childNodesObj); let childKeys = Object.keys(childNodesObj);
let createEdges = [];
for (let i = 0; i < childKeys.length; i++) { for (let i = 0; i < childKeys.length; i++) {
childNodeId = childKeys[i]; childNodeId = childKeys[i];
childNode = childNodesObj[childNodeId]; childNode = childNodesObj[childNodeId];
@ -254,31 +294,56 @@ class ClusterEngine {
// construct new edges from the cluster to others // construct new edges from the cluster to others
for (let j = 0; j < childNode.edges.length; j++) { for (let j = 0; j < childNode.edges.length; j++) {
edge = childNode.edges[j]; edge = childNode.edges[j];
childEdgesObj[edge.id] = edge;
// childNodeId position will be replaced by the cluster.
if (edge.toId == childNodeId) { // this is a double equals because ints and strings can be interchanged here.
toId = clusterNodeProperties.id;
fromId = edge.fromId;
otherNodeId = fromId;
}
else {
toId = edge.toId;
fromId = clusterNodeProperties.id;
otherNodeId = toId;
}
// we only handle edges that are visible to the system, not the disabled ones from the clustering process.
if (edge.hiddenByCluster !== true) {
// set up the from and to.
if (edge.toId == childNodeId) { // this is a double equals because ints and strings can be interchanged here.
toId = clusterNodeProperties.id;
fromId = edge.fromId;
otherNodeId = fromId;
}
else {
toId = edge.toId;
fromId = clusterNodeProperties.id;
otherNodeId = toId;
}
// if the node connected to the cluster is also in the cluster we do not need a new edge.
if (childNodesObj[otherNodeId] === undefined) {
let clonedOptions = this._cloneOptions(edge, 'edge');
util.deepExtend(clonedOptions, clusterEdgeProperties);
clonedOptions.from = fromId;
clonedOptions.to = toId;
clonedOptions.id = 'clusterEdge:' + util.randomUUID();
newEdges.push(this.body.functions.createEdge(clonedOptions));
// Only edges from the cluster outwards are being replaced.
if (childNodesObj[otherNodeId] === undefined) {
createEdges.push({edge: edge, fromId: fromId, toId: toId});
}
} }
} }
} }
// here we actually create the replacement edges. We could not do this in the loop above as the creation process
// would add an edge to the edges array we are iterating over.
for (let j = 0; j < createEdges.length; j++) {
let edge = createEdges[j].edge;
// copy the options of the edge we will replace
let clonedOptions = this._cloneOptions(edge, 'edge');
// make sure the properties of clusterEdges are superimposed on it
util.deepExtend(clonedOptions, clusterEdgeProperties);
// set up the edge
clonedOptions.from = createEdges[j].fromId;
clonedOptions.to = createEdges[j].toId;
clonedOptions.id = 'clusterEdge:' + util.randomUUID();
//clonedOptions.id = '(cf: ' + createEdges[j].fromId + " to: " + createEdges[j].toId + ")" + Math.random();
// create the edge and give a reference to the one it replaced.
let newEdge = this.body.functions.createEdge(clonedOptions);
newEdge.clusteringEdgeReplacingId = edge.id;
// connect the edge.
this.body.edges[newEdge.id] = newEdge;
newEdge.connect();
// hide the replaced edge
edge.setOptions({physics:false, hidden:true});
edge.hiddenByCluster = true;
}
} }
/** /**
@ -304,8 +369,17 @@ class ClusterEngine {
* @private * @private
*/ */
_cluster(childNodesObj, childEdgesObj, options, refreshData = true) { _cluster(childNodesObj, childEdgesObj, options, refreshData = true) {
// kill condition: no children so cant cluster
if (Object.keys(childNodesObj).length === 0) {return;}
// kill condition: no children so can't cluster or only one node in the cluster, dont bother
if (Object.keys(childNodesObj).length < 2) {return;}
// check if this cluster call is not trying to cluster anything that is in another cluster.
for (let nodeId in childNodesObj) {
if (childNodesObj.hasOwnProperty(nodeId)) {
if (this.clusteredNodes[nodeId] !== undefined) {
return;
}
}
}
let clusterNodeProperties = util.deepExtend({},options.clusterNodeProperties); let clusterNodeProperties = util.deepExtend({},options.clusterNodeProperties);
@ -314,17 +388,21 @@ class ClusterEngine {
// get the childNode options // get the childNode options
let childNodesOptions = []; let childNodesOptions = [];
for (let nodeId in childNodesObj) { for (let nodeId in childNodesObj) {
let clonedOptions = this._cloneOptions(childNodesObj[nodeId]);
childNodesOptions.push(clonedOptions);
if (childNodesObj.hasOwnProperty(nodeId)) {
let clonedOptions = this._cloneOptions(childNodesObj[nodeId]);
childNodesOptions.push(clonedOptions);
}
} }
// get clusterproperties based on childNodes // get clusterproperties based on childNodes
let childEdgesOptions = []; let childEdgesOptions = [];
for (let edgeId in childEdgesObj) { for (let edgeId in childEdgesObj) {
// these cluster edges will be removed on creation of the cluster.
if (edgeId.substr(0,12) !== "clusterEdge:") {
let clonedOptions = this._cloneOptions(childEdgesObj[edgeId], 'edge');
childEdgesOptions.push(clonedOptions);
if (childEdgesObj.hasOwnProperty(edgeId)) {
// these cluster edges will be removed on creation of the cluster.
if (edgeId.substr(0, 12) !== "clusterEdge:") {
let clonedOptions = this._cloneOptions(childEdgesObj[edgeId], 'edge');
childEdgesOptions.push(clonedOptions);
}
} }
} }
@ -350,9 +428,7 @@ class ClusterEngine {
clusterNodeProperties.x = pos.x; clusterNodeProperties.x = pos.x;
} }
if (clusterNodeProperties.y === undefined) { if (clusterNodeProperties.y === undefined) {
if (pos === undefined) {
pos = this._getClusterPosition(childNodesObj);
}
if (pos === undefined) {pos = this._getClusterPosition(childNodesObj);}
clusterNodeProperties.y = pos.y; clusterNodeProperties.y = pos.y;
} }
@ -371,28 +447,15 @@ class ClusterEngine {
this.body.nodes[clusterNodeProperties.id] = clusterNode; this.body.nodes[clusterNodeProperties.id] = clusterNode;
// create the new edges that will connect to the cluster // create the new edges that will connect to the cluster
let newEdges = [];
this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, clusterNodeProperties, options.clusterEdgeProperties);
this._createClusterEdges(childNodesObj, clusterNodeProperties, options.clusterEdgeProperties);
// disable the childEdges // disable the childEdges
for (let edgeId in childEdgesObj) { for (let edgeId in childEdgesObj) {
if (childEdgesObj.hasOwnProperty(edgeId)) { if (childEdgesObj.hasOwnProperty(edgeId)) {
if (this.body.edges[edgeId] !== undefined) { if (this.body.edges[edgeId] !== undefined) {
let edge = this.body.edges[edgeId]; let edge = this.body.edges[edgeId];
// if this is a cluster edge that is fully encompassed in the cluster, we want to delete it
// this check verifies that both of the connected nodes are in this cluster
if (edgeId.substr(0,12) === "clusterEdge:" && childNodesObj[edge.fromId] !== undefined && childNodesObj[edge.toId] !== undefined) {
edge.cleanup();
// this removes the edge from node.edges, which is why edgeIds is formed
edge.disconnect();
delete childEdgesObj[edgeId];
delete this.body.edges[edgeId];
}
else {
edge.setOptions({physics:false, hidden:true});
//edge.options.hidden = true;
}
edge.setOptions({physics:false, hidden:true});
edge.hiddenByCluster = true;
} }
} }
} }
@ -405,12 +468,6 @@ class ClusterEngine {
} }
} }
// push new edges to global
for (let i = 0; i < newEdges.length; i++) {
this.body.edges[newEdges[i].id] = newEdges[i];
this.body.edges[newEdges[i].id].connect();
}
// set ID to undefined so no duplicates arise // set ID to undefined so no duplicates arise
clusterNodeProperties.id = undefined; clusterNodeProperties.id = undefined;
@ -496,8 +553,8 @@ class ClusterEngine {
if (containedNodes.hasOwnProperty(nodeId)) { if (containedNodes.hasOwnProperty(nodeId)) {
let containedNode = this.body.nodes[nodeId]; let containedNode = this.body.nodes[nodeId];
if (newPositions[nodeId] !== undefined) { if (newPositions[nodeId] !== undefined) {
containedNode.x = newPositions[nodeId].x || clusterNode.x;
containedNode.y = newPositions[nodeId].y || clusterNode.y;
containedNode.x = (newPositions[nodeId].x === undefined ? clusterNode.x : newPositions[nodeId].x);
containedNode.y = (newPositions[nodeId].y === undefined ? clusterNode.y : newPositions[nodeId].y);
} }
} }
} }
@ -525,78 +582,79 @@ class ClusterEngine {
containedNode.vy = clusterNode.vy; containedNode.vy = clusterNode.vy;
// we use these methods to avoid reinstantiating the shape, which happens with setOptions. // we use these methods to avoid reinstantiating the shape, which happens with setOptions.
//containedNode.toggleHidden(false);
//containedNode.togglePhysics(true);
containedNode.setOptions({hidden:false, physics:true}); containedNode.setOptions({hidden:false, physics:true});
delete this.clusteredNodes[nodeId]; delete this.clusteredNodes[nodeId];
} }
} }
// release edges
for (let edgeId in containedEdges) {
if (containedEdges.hasOwnProperty(edgeId)) {
let edge = containedEdges[edgeId];
// if this edge was a temporary edge and it's connected nodes do not exist anymore, we remove it from the data
if (this.body.nodes[edge.fromId] === undefined || this.body.nodes[edge.toId] === undefined || edge.toId == clusterNodeId || edge.fromId == clusterNodeId) {
edge.cleanup();
// this removes the edge from node.edges, which is why edgeIds is formed
edge.disconnect();
delete this.body.edges[edgeId];
}
else {
// one of the nodes connected to this edge is in a cluster. We give the edge to that cluster so it will be released when that cluster is opened.
if (this.clusteredNodes[edge.fromId] !== undefined || this.clusteredNodes[edge.toId] !== undefined) {
let fromId, toId;
let clusteredNode = this.clusteredNodes[edge.fromId] || this.clusteredNodes[edge.toId];
let clusterId = clusteredNode.clusterId;
let clusterNode = this.body.nodes[clusterId];
clusterNode.containedEdges[edgeId] = edge;
if (this.clusteredNodes[edge.fromId] !== undefined) {
fromId = clusterId;
toId = edge.toId;
}
else {
fromId = edge.fromId;
toId = clusterId;
}
// if both from and to nodes are visible, we create a new temporary edge
if (this.body.nodes[fromId].options.hidden !== true && this.body.nodes[toId].options.hidden !== true) {
let clonedOptions = this._cloneOptions(edge, 'edge');
let id = 'clusterEdge:' + util.randomUUID();
util.deepExtend(clonedOptions, clusterNode.clusterEdgeProperties);
util.deepExtend(clonedOptions, {from: fromId, to: toId, hidden: false, physics: true, id: id});
let newEdge = this.body.functions.createEdge(clonedOptions);
this.body.edges[id] = newEdge;
this.body.edges[id].connect();
}
// copy the clusterNode edges because we cannot iterate over an object that we add or remove from.
let edgesToBeDeleted = [];
for (let i = 0; i < clusterNode.edges.length; i++) {
edgesToBeDeleted.push(clusterNode.edges[i]);
}
// actually handling the deleting.
for (let i = 0; i < edgesToBeDeleted.length; i++) {
let edge = edgesToBeDeleted[i];
let otherNodeId = this._getConnectedId(edge, clusterNodeId);
// if the other node is in another cluster, we transfer ownership of this edge to the other cluster
if (this.clusteredNodes[otherNodeId] !== undefined) {
// transfer ownership:
let otherCluster = this.body.nodes[this.clusteredNodes[otherNodeId].clusterId];
let transferEdge = this.body.edges[edge.clusteringEdgeReplacingId];
if (transferEdge !== undefined) {
otherCluster.containedEdges[transferEdge.id] = transferEdge;
// delete local reference
delete containedEdges[transferEdge.id];
// create new cluster edge from the otherCluster:
// get to and from
let fromId = transferEdge.fromId;
let toId = transferEdge.toId;
if (transferEdge.toId == otherNodeId) {
toId = this.clusteredNodes[otherNodeId].clusterId;
} }
else { else {
edge.setOptions({physics:true, hidden:false});
//edge.options.hidden = false;
//edge.togglePhysics(true);
fromId = this.clusteredNodes[otherNodeId].clusterId;
} }
// clone the options and apply the cluster options to them
let clonedOptions = this._cloneOptions(transferEdge, 'edge');
util.deepExtend(clonedOptions, otherCluster.clusterEdgeProperties);
// apply the edge specific options to it.
let id = 'clusterEdge:' + util.randomUUID();
util.deepExtend(clonedOptions, {from: fromId, to: toId, hidden: false, physics: true, id: id});
// create it
let newEdge = this.body.functions.createEdge(clonedOptions);
newEdge.clusteringEdgeReplacingId = transferEdge.id;
this.body.edges[id] = newEdge;
this.body.edges[id].connect();
} }
} }
else {
let replacedEdge = this.body.edges[edge.clusteringEdgeReplacingId];
if (replacedEdge !== undefined) {
replacedEdge.setOptions({physics: true, hidden: false});
replacedEdge.hiddenByCluster = false;
}
}
edge.cleanup();
// this removes the edge from node.edges, which is why edgeIds is formed
edge.disconnect();
delete this.body.edges[edge.id];
} }
// remove all temporary edges, make an array of ids so we don't remove from the list we're iterating over.
let removeIds = [];
for (let i = 0; i < clusterNode.edges.length; i++) {
let edgeId = clusterNode.edges[i].id;
removeIds.push(edgeId);
}
// actually removing the edges
for (let i = 0; i < removeIds.length; i++) {
let edgeId = removeIds[i];
this.body.edges[edgeId].cleanup();
// this removes the edge from node.edges, which is why edgeIds is formed
this.body.edges[edgeId].disconnect();
delete this.body.edges[edgeId];
// handle the releasing of the edges
for (let edgeId in containedEdges) {
if (containedEdges.hasOwnProperty(edgeId)) {
let edge = containedEdges[edgeId];
edge.setOptions({physics: true, hidden: false});
}
} }
// remove clusterNode // remove clusterNode

+ 219
- 0
lib/network/modules/KamadaKawai.js View File

@ -0,0 +1,219 @@
/**
* Created by Alex on 8/7/2015.
*/
// distance finding algorithm
import FloydWarshall from "./components/algorithms/FloydWarshall.js"
/**
* KamadaKawai positions the nodes initially based on
*
* "AN ALGORITHM FOR DRAWING GENERAL UNDIRECTED GRAPHS"
* -- Tomihisa KAMADA and Satoru KAWAI in 1989
*
* Possible optimizations in the distance calculation can be implemented.
*/
class KamadaKawai {
constructor(body, edgeLength, edgeStrength) {
this.body = body;
this.springLength = edgeLength;
this.springConstant = edgeStrength;
this.distanceSolver = new FloydWarshall();
}
/**
* Not sure if needed but can be used to update the spring length and spring constant
* @param options
*/
setOptions(options) {
if (options) {
if (options.springLength) {
this.springLength = options.springLength;
}
if (options.springConstant) {
this.springConstant = options.springConstant;
}
}
}
/**
* Position the system
* @param nodesArray
* @param edgesArray
*/
solve(nodesArray, edgesArray, ignoreClusters = false) {
// get distance matrix
let D_matrix = this.distanceSolver.getDistances(this.body, nodesArray, edgesArray); // distance matrix
// get the L Matrix
this._createL_matrix(D_matrix);
// get the K Matrix
this._createK_matrix(D_matrix);
// calculate positions
let threshold = 0.01;
let innerThreshold = 1;
let iterations = 0;
let maxIterations = Math.max(1000,Math.min(10*this.body.nodeIndices.length,6000));
let maxInnerIterations = 5;
let maxEnergy = 1e9;
let highE_nodeId = 0, dE_dx = 0, dE_dy = 0, delta_m = 0, subIterations = 0;
while (maxEnergy > threshold && iterations < maxIterations) {
iterations += 1;
[highE_nodeId, maxEnergy, dE_dx, dE_dy] = this._getHighestEnergyNode(ignoreClusters);
delta_m = maxEnergy;
subIterations = 0;
while(delta_m > innerThreshold && subIterations < maxInnerIterations) {
subIterations += 1;
this._moveNode(highE_nodeId, dE_dx, dE_dy);
[delta_m,dE_dx,dE_dy] = this._getEnergy(highE_nodeId);
}
}
}
/**
* get the node with the highest energy
* @returns {*[]}
* @private
*/
_getHighestEnergyNode(ignoreClusters) {
let nodesArray = this.body.nodeIndices;
let nodes = this.body.nodes;
let maxEnergy = 0;
let maxEnergyNodeId = nodesArray[0];
let dE_dx_max = 0, dE_dy_max = 0;
for (let nodeIdx = 0; nodeIdx < nodesArray.length; nodeIdx++) {
let m = nodesArray[nodeIdx];
// by not evaluating nodes with predefined positions we should only move nodes that have no positions.
if ((nodes[m].predefinedPosition === false || nodes[m].isCluster === true && ignoreClusters === true) || nodes[m].options.fixed.x === true || nodes[m].options.fixed.y === true) {
let [delta_m,dE_dx,dE_dy] = this._getEnergy(m);
if (maxEnergy < delta_m) {
maxEnergy = delta_m;
maxEnergyNodeId = m;
dE_dx_max = dE_dx;
dE_dy_max = dE_dy;
}
}
}
return [maxEnergyNodeId, maxEnergy, dE_dx_max, dE_dy_max];
}
/**
* calculate the energy of a single node
* @param m
* @returns {*[]}
* @private
*/
_getEnergy(m) {
let nodesArray = this.body.nodeIndices;
let nodes = this.body.nodes;
let x_m = nodes[m].x;
let y_m = nodes[m].y;
let dE_dx = 0;
let dE_dy = 0;
for (let iIdx = 0; iIdx < nodesArray.length; iIdx++) {
let i = nodesArray[iIdx];
if (i !== m) {
let x_i = nodes[i].x;
let y_i = nodes[i].y;
let denominator = 1.0 / Math.sqrt(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2));
dE_dx += this.K_matrix[m][i] * ((x_m - x_i) - this.L_matrix[m][i] * (x_m - x_i) * denominator);
dE_dy += this.K_matrix[m][i] * ((y_m - y_i) - this.L_matrix[m][i] * (y_m - y_i) * denominator);
}
}
let delta_m = Math.sqrt(Math.pow(dE_dx, 2) + Math.pow(dE_dy, 2));
return [delta_m, dE_dx, dE_dy];
}
/**
* move the node based on it's energy
* the dx and dy are calculated from the linear system proposed by Kamada and Kawai
* @param m
* @param dE_dx
* @param dE_dy
* @private
*/
_moveNode(m, dE_dx, dE_dy) {
let nodesArray = this.body.nodeIndices;
let nodes = this.body.nodes;
let d2E_dx2 = 0;
let d2E_dxdy = 0;
let d2E_dy2 = 0;
let x_m = nodes[m].x;
let y_m = nodes[m].y;
for (let iIdx = 0; iIdx < nodesArray.length; iIdx++) {
let i = nodesArray[iIdx];
if (i !== m) {
let x_i = nodes[i].x;
let y_i = nodes[i].y;
let denominator = 1.0 / Math.pow(Math.pow(x_m - x_i, 2) + Math.pow(y_m - y_i, 2), 1.5);
d2E_dx2 += this.K_matrix[m][i] * (1 - this.L_matrix[m][i] * Math.pow(y_m - y_i, 2) * denominator);
d2E_dxdy += this.K_matrix[m][i] * (this.L_matrix[m][i] * (x_m - x_i) * (y_m - y_i) * denominator);
d2E_dy2 += this.K_matrix[m][i] * (1 - this.L_matrix[m][i] * Math.pow(x_m - x_i, 2) * denominator);
}
}
// make the variable names easier to make the solving of the linear system easier to read
let A = d2E_dx2, B = d2E_dxdy, C = dE_dx, D = d2E_dy2, E = dE_dy;
// solve the linear system for dx and dy
let dy = (C / A + E / B) / (B / A - D / B);
let dx = -(B * dy + C) / A;
// move the node
nodes[m].x += dx;
nodes[m].y += dy;
}
/**
* Create the L matrix: edge length times shortest path
* @param D_matrix
* @private
*/
_createL_matrix(D_matrix) {
let nodesArray = this.body.nodeIndices;
let edgeLength = this.springLength;
this.L_matrix = [];
for (let i = 0; i < nodesArray.length; i++) {
this.L_matrix[nodesArray[i]] = {};
for (let j = 0; j < nodesArray.length; j++) {
this.L_matrix[nodesArray[i]][nodesArray[j]] = edgeLength * D_matrix[nodesArray[i]][nodesArray[j]];
}
}
}
/**
* Create the K matrix: spring constants times shortest path
* @param D_matrix
* @private
*/
_createK_matrix(D_matrix) {
let nodesArray = this.body.nodeIndices;
let edgeStrength = this.springConstant;
this.K_matrix = [];
for (let i = 0; i < nodesArray.length; i++) {
this.K_matrix[nodesArray[i]] = {};
for (let j = 0; j < nodesArray.length; j++) {
this.K_matrix[nodesArray[i]][nodesArray[j]] = edgeStrength * Math.pow(D_matrix[nodesArray[i]][nodesArray[j]], -2);
}
}
}
}
export default KamadaKawai;

+ 82
- 6
lib/network/modules/LayoutEngine.js View File

@ -1,6 +1,6 @@
'use strict' 'use strict'
var util = require('../../util');
let util = require('../../util');
class LayoutEngine { class LayoutEngine {
constructor(body) { constructor(body) {
@ -11,8 +11,10 @@ class LayoutEngine {
this.options = {}; this.options = {};
this.optionsBackup = {}; this.optionsBackup = {};
this.defaultOptions = { this.defaultOptions = {
randomSeed: undefined, randomSeed: undefined,
improvedLayout: true,
hierarchical: { hierarchical: {
enabled:false, enabled:false,
levelSeparation: 150, levelSeparation: 150,
@ -31,6 +33,9 @@ class LayoutEngine {
this.body.emitter.on('_dataChanged', () => { this.body.emitter.on('_dataChanged', () => {
this.setupHierarchicalLayout(); this.setupHierarchicalLayout();
}); });
this.body.emitter.on('_dataLoaded', () => {
this.layoutNetwork();
});
this.body.emitter.on('_resetHierarchicalLayout', () => { this.body.emitter.on('_resetHierarchicalLayout', () => {
this.setupHierarchicalLayout(); this.setupHierarchicalLayout();
}); });
@ -39,11 +44,9 @@ class LayoutEngine {
setOptions(options, allOptions) { setOptions(options, allOptions) {
if (options !== undefined) { if (options !== undefined) {
let prevHierarchicalState = this.options.hierarchical.enabled; let prevHierarchicalState = this.options.hierarchical.enabled;
util.selectiveDeepExtend(["randomSeed", "improvedLayout"],this.options, options);
util.mergeOptions(this.options, options, 'hierarchical'); 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 (this.options.hierarchical.enabled === true) {
if (prevHierarchicalState === true) { if (prevHierarchicalState === true) {
@ -145,7 +148,7 @@ class LayoutEngine {
} }
seededRandom() { seededRandom() {
var x = Math.sin(this.randomSeed++) * 10000;
let x = Math.sin(this.randomSeed++) * 10000;
return x - Math.floor(x); return x - Math.floor(x);
} }
@ -166,6 +169,79 @@ class LayoutEngine {
} }
} }
/**
* Use KamadaKawai to position nodes. This is quite a heavy algorithm so if there are a lot of nodes we
* cluster them first to reduce the amount.
*/
layoutNetwork() {
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;
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;
}
}
// 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");
}
}
}
_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');
}
}
}
getSeed() { getSeed() {
return this.initialRandomSeed; return this.initialRandomSeed;
} }

+ 143
- 34
lib/network/modules/PhysicsEngine.js View File

@ -19,9 +19,15 @@ class PhysicsEngine {
this.simulationInterval = 1000 / 60; this.simulationInterval = 1000 / 60;
this.requiresTimeout = true; this.requiresTimeout = true;
this.previousStates = {}; this.previousStates = {};
this.referenceState = {};
this.freezeCache = {}; this.freezeCache = {};
this.renderTimer = undefined; this.renderTimer = undefined;
this.initialStabilizationEmitted = false;
// parameters for the adaptive timestep
this.adaptiveTimestep = false;
this.adaptiveTimestepEnabled = false;
this.adaptiveCounter = 0;
this.adaptiveInterval = 3;
this.stabilized = false; this.stabilized = false;
this.startedStabilization = false; this.startedStabilization = false;
@ -66,7 +72,7 @@ class PhysicsEngine {
damping: 0.09 damping: 0.09
}, },
maxVelocity: 50, maxVelocity: 50,
minVelocity: 0.1, // px/s
minVelocity: 0.75, // px/s
solver: 'barnesHut', solver: 'barnesHut',
stabilization: { stabilization: {
enabled: true, enabled: true,
@ -75,9 +81,11 @@ class PhysicsEngine {
onlyDynamicEdges: false, onlyDynamicEdges: false,
fit: true fit: true
}, },
timestep: 0.5
timestep: 0.5,
adaptiveTimestep: true
}; };
util.extend(this.options, this.defaultOptions); util.extend(this.options, this.defaultOptions);
this.timestep = 0.5;
this.bindEventListeners(); this.bindEventListeners();
} }
@ -104,6 +112,11 @@ class PhysicsEngine {
}); });
} }
/**
* set the physics options
* @param options
*/
setOptions(options) { setOptions(options) {
if (options !== undefined) { if (options !== undefined) {
if (options === false) { if (options === false) {
@ -124,12 +137,18 @@ class PhysicsEngine {
this.physicsEnabled = false; this.physicsEnabled = false;
this.stopSimulation(); this.stopSimulation();
} }
// set the timestep
this.timestep = this.options.timestep;
} }
} }
this.init(); this.init();
} }
/**
* configure the engine.
*/
init() { init() {
var options; var options;
if (this.options.solver === 'forceAtlas2Based') { if (this.options.solver === 'forceAtlas2Based') {
@ -160,6 +179,10 @@ class PhysicsEngine {
this.modelOptions = options; this.modelOptions = options;
} }
/**
* initialize the engine
*/
initPhysics() { initPhysics() {
if (this.physicsEnabled === true && this.options.enabled === true) { if (this.physicsEnabled === true && this.options.enabled === true) {
if (this.options.stabilization.enabled === true) { if (this.options.stabilization.enabled === true) {
@ -168,7 +191,7 @@ class PhysicsEngine {
else { else {
this.stabilized = false; this.stabilized = false;
this.ready = true; this.ready = true;
this.body.emitter.emit('fit', {}, true);
this.body.emitter.emit('fit', {}, false);
this.startSimulation(); this.startSimulation();
} }
} }
@ -185,6 +208,9 @@ class PhysicsEngine {
if (this.physicsEnabled === true && this.options.enabled === true) { if (this.physicsEnabled === true && this.options.enabled === true) {
this.stabilized = false; this.stabilized = false;
// when visible, adaptivity is disabled.
this.adaptiveTimestep = false;
// this sets the width of all nodes initially which could be required for the avoidOverlap // this sets the width of all nodes initially which could be required for the avoidOverlap
this.body.emitter.emit("_resizeNodes"); this.body.emitter.emit("_resizeNodes");
if (this.viewFunction === undefined) { if (this.viewFunction === undefined) {
@ -236,21 +262,17 @@ class PhysicsEngine {
} }
if (this.stabilized === true) { if (this.stabilized === true) {
if (this.stabilizationIterations > 1) {
// trigger the 'stabilized' event.
// The event is triggered on the next tick, to prevent the case that
// it is fired while initializing the Network, in which case you would not
// be able to catch it
this.startedStabilization = false;
//this._emitStabilized();
}
this.stopSimulation(); this.stopSimulation();
} }
} }
/**
* trigger the stabilized event.
* @private
*/
_emitStabilized() { _emitStabilized() {
if (this.stabilizationIterations > 1 || this.initialStabilizationEmitted === false) {
this.initialStabilizationEmitted = true;
if (this.stabilizationIterations > 1) {
setTimeout(() => { setTimeout(() => {
this.body.emitter.emit('stabilized', {iterations: this.stabilizationIterations}); this.body.emitter.emit('stabilized', {iterations: this.stabilizationIterations});
this.stabilizationIterations = 0; this.stabilizationIterations = 0;
@ -265,8 +287,62 @@ class PhysicsEngine {
*/ */
physicsTick() { physicsTick() {
if (this.stabilized === false) { if (this.stabilized === false) {
this.calculateForces();
this.stabilized = this.moveNodes();
// adaptivity means the timestep adapts to the situation, only applicable for stabilization
if (this.adaptiveTimestep === true && this.adaptiveTimestepEnabled === true) {
// this is the factor for increasing the timestep on success.
let factor = 1.2;
// we assume the adaptive interval is
if (this.adaptiveCounter % this.adaptiveInterval === 0) { // we leave the timestep stable for "interval" iterations.
// first the big step and revert. Revert saves the reference state.
this.timestep = 2 * this.timestep;
this.calculateForces();
this.moveNodes();
this.revert();
// now the normal step. Since this is the last step, it is the more stable one and we will take this.
this.timestep = 0.5 * this.timestep;
// since it's half the step, we do it twice.
this.calculateForces();
this.moveNodes();
this.calculateForces();
this.moveNodes();
// we compare the two steps. if it is acceptable we double the step.
if (this._evaluateStepQuality() === true) {
this.timestep = factor * this.timestep;
}
else {
// if not, we decrease the step to a minimum of the options timestep.
// if the decreased timestep is smaller than the options step, we do not reset the counter
// we assume that the options timestep is stable enough.
if (this.timestep/factor < this.options.timestep) {
this.timestep = this.options.timestep;
}
else {
// if the timestep was larger than 2 times the option one we check the adaptivity again to ensure
// that large instabilities do not form.
this.adaptiveCounter = -1; // check again next iteration
this.timestep = Math.max(this.options.timestep, this.timestep/factor);
}
}
}
else {
// normal step, keeping timestep constant
this.calculateForces();
this.moveNodes();
}
// increment the counter
this.adaptiveCounter += 1;
}
else {
// case for the static timestep, we reset it to the one in options and take a normal step.
this.timestep = this.options.timestep;
this.calculateForces();
this.moveNodes();
}
// determine if the network has stabilzied // determine if the network has stabilzied
if (this.stabilized === true) { if (this.stabilized === true) {
@ -346,6 +422,9 @@ class PhysicsEngine {
let nodeId = nodeIds[i]; let nodeId = nodeIds[i];
if (nodes[nodeId] !== undefined) { if (nodes[nodeId] !== undefined) {
if (nodes[nodeId].options.physics === true) { if (nodes[nodeId].options.physics === true) {
this.referenceState[nodeId] = {
positions: {x:nodes[nodeId].x, y:nodes[nodeId].y}
};
velocities[nodeId].x = this.previousStates[nodeId].vx; velocities[nodeId].x = this.previousStates[nodeId].vx;
velocities[nodeId].y = this.previousStates[nodeId].vy; velocities[nodeId].y = this.previousStates[nodeId].vy;
nodes[nodeId].x = this.previousStates[nodeId].x; nodes[nodeId].x = this.previousStates[nodeId].x;
@ -358,35 +437,54 @@ class PhysicsEngine {
} }
} }
/**
* This compares the reference state to the current state
*/
_evaluateStepQuality() {
let dx, dy, dpos;
let nodes = this.body.nodes;
let reference = this.referenceState;
let posThreshold = 0.3;
for (let nodeId in this.referenceState) {
if (this.referenceState.hasOwnProperty(nodeId)) {
dx = nodes[nodeId].x - reference[nodeId].positions.x;
dy = nodes[nodeId].y - reference[nodeId].positions.y;
dpos = Math.sqrt(Math.pow(dx,2) + Math.pow(dy,2))
if (dpos > posThreshold) {
return false;
}
}
}
return true;
}
/** /**
* move the nodes one timestap and check if they are stabilized * move the nodes one timestap and check if they are stabilized
* @returns {boolean} * @returns {boolean}
*/ */
moveNodes() { moveNodes() {
var nodesPresent = false;
var nodeIndices = this.physicsBody.physicsNodeIndices; var nodeIndices = this.physicsBody.physicsNodeIndices;
var maxVelocity = this.options.maxVelocity ? this.options.maxVelocity : 1e9; var maxVelocity = this.options.maxVelocity ? this.options.maxVelocity : 1e9;
var stabilized = true;
var vminCorrected = this.options.minVelocity / Math.max(this.body.view.scale,0.05);
var maxNodeVelocity = 0;
var averageNodeVelocity = 0;
// the velocity threshold (energy in the system) for the adaptivity toggle
var velocityAdaptiveThreshold = 5;
for (let i = 0; i < nodeIndices.length; i++) { for (let i = 0; i < nodeIndices.length; i++) {
let nodeId = nodeIndices[i]; let nodeId = nodeIndices[i];
let nodeVelocity = this._performStep(nodeId, maxVelocity); let nodeVelocity = this._performStep(nodeId, maxVelocity);
// stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized // stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized
stabilized = nodeVelocity < vminCorrected && stabilized === true;
nodesPresent = true;
maxNodeVelocity = Math.max(maxNodeVelocity,nodeVelocity);
averageNodeVelocity += nodeVelocity;
} }
if (nodesPresent === true) {
if (vminCorrected > 0.5*this.options.maxVelocity) {
return false;
}
else {
return stabilized;
}
}
return true;
// evaluating the stabilized and adaptiveTimestepEnabled conditions
this.adaptiveTimestepEnabled = (averageNodeVelocity/nodeIndices.length) < velocityAdaptiveThreshold;
this.stabilized = maxNodeVelocity < this.options.minVelocity;
} }
@ -400,7 +498,7 @@ class PhysicsEngine {
*/ */
_performStep(nodeId,maxVelocity) { _performStep(nodeId,maxVelocity) {
let node = this.body.nodes[nodeId]; let node = this.body.nodes[nodeId];
let timestep = this.options.timestep;
let timestep = this.timestep;
let forces = this.physicsBody.forces; let forces = this.physicsBody.forces;
let velocities = this.physicsBody.velocities; let velocities = this.physicsBody.velocities;
@ -499,6 +597,8 @@ class PhysicsEngine {
return; return;
} }
// enable adaptive timesteps
this.adaptiveTimestep = true && this.options.adaptiveTimestep;
// this sets the width of all nodes initially which could be required for the avoidOverlap // this sets the width of all nodes initially which could be required for the avoidOverlap
this.body.emitter.emit("_resizeNodes"); this.body.emitter.emit("_resizeNodes");
@ -522,11 +622,15 @@ class PhysicsEngine {
setTimeout(() => this._stabilizationBatch(),0); setTimeout(() => this._stabilizationBatch(),0);
} }
/**
* One batch of stabilization
* @private
*/
_stabilizationBatch() { _stabilizationBatch() {
var count = 0; var count = 0;
while (this.stabilized === false && count < this.options.stabilization.updateInterval && this.stabilizationIterations < this.targetIterations) { while (this.stabilized === false && count < this.options.stabilization.updateInterval && this.stabilizationIterations < this.targetIterations) {
this.physicsTick(); this.physicsTick();
this.stabilizationIterations++;
count++; count++;
} }
@ -539,6 +643,11 @@ class PhysicsEngine {
} }
} }
/**
* Wrap up the stabilization, fit and emit the events.
* @private
*/
_finalizeStabilization() { _finalizeStabilization() {
this.body.emitter.emit('_allowRedraw'); this.body.emitter.emit('_allowRedraw');
if (this.options.stabilization.fit === true) { if (this.options.stabilization.fit === true) {
@ -548,7 +657,7 @@ class PhysicsEngine {
if (this.options.stabilization.onlyDynamicEdges === true) { if (this.options.stabilization.onlyDynamicEdges === true) {
this._restoreFrozenNodes(); this._restoreFrozenNodes();
} }
this.body.emitter.emit('stabilizationIterationsDone'); this.body.emitter.emit('stabilizationIterationsDone');
this.body.emitter.emit('_requestRedraw'); this.body.emitter.emit('_requestRedraw');

+ 46
- 0
lib/network/modules/components/algorithms/FloydWarshall.js View File

@ -0,0 +1,46 @@
/**
* Created by Alex on 10-Aug-15.
*/
class FloydWarshall {
constructor(){}
getDistances(body, nodesArray, edgesArray) {
let D_matrix = {};
let edges = body.edges;
// prepare matrix with large numbers
for (let i = 0; i < nodesArray.length; i++) {
D_matrix[nodesArray[i]] = {};
D_matrix[nodesArray[i]] = {};
for (let j = 0; j < nodesArray.length; j++) {
D_matrix[nodesArray[i]][nodesArray[j]] = (i == j ? 0 : 1e9);
D_matrix[nodesArray[i]][nodesArray[j]] = (i == j ? 0 : 1e9);
}
}
// put the weights for the edges in. This assumes unidirectionality.
for (let i = 0; i < edgesArray.length; i++) {
let edge = edges[edgesArray[i]];
D_matrix[edge.fromId][edge.toId] = 1;
D_matrix[edge.toId][edge.fromId] = 1;
}
let nodeCount = nodesArray.length;
// Adapted FloydWarshall based on unidirectionality to greatly reduce complexity.
for (let k = 0; k < nodeCount; k++) {
for (let i = 0; i < nodeCount-1; i++) {
for (let j = i+1; j < nodeCount; j++) {
D_matrix[nodesArray[i]][nodesArray[j]] = Math.min(D_matrix[nodesArray[i]][nodesArray[j]],D_matrix[nodesArray[i]][nodesArray[k]] + D_matrix[nodesArray[k]][nodesArray[j]])
D_matrix[nodesArray[j]][nodesArray[i]] = D_matrix[nodesArray[i]][nodesArray[j]];
}
}
}
return D_matrix;
}
}
export default FloydWarshall;

+ 3
- 0
lib/network/modules/components/edges/BezierEdgeDynamic.js View File

@ -4,6 +4,8 @@ class BezierEdgeDynamic extends BezierEdgeBase {
constructor(options, body, labelModule) { constructor(options, body, labelModule) {
//this.via = undefined; // Here for completeness but not allowed to defined before super() is invoked. //this.via = undefined; // Here for completeness but not allowed to defined before super() is invoked.
super(options, body, labelModule); // --> this calls the setOptions below super(options, body, labelModule); // --> this calls the setOptions below
this._boundFunction = () => {this.positionBezierNode();};
this.body.emitter.on("_repositionBezierNodes", this._boundFunction);
} }
setOptions(options) { setOptions(options) {
@ -41,6 +43,7 @@ class BezierEdgeDynamic extends BezierEdgeBase {
* @returns {boolean} * @returns {boolean}
*/ */
cleanup() { cleanup() {
this.body.emitter.off("_repositionBezierNodes", this._boundFunction);
if (this.via !== undefined) { if (this.via !== undefined) {
delete this.body.nodes[this.via.id]; delete this.body.nodes[this.via.id];
this.via = undefined; this.via = undefined;

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

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

+ 38
- 22
test/networkTest.html View File

@ -24,33 +24,49 @@
<script type="text/javascript"> <script type="text/javascript">
var nodes = new vis.DataSet() var nodes = new vis.DataSet()
nodes.add({
id: 'foo',
label: 'foo',
group: 'a'
})
nodes.add({id:'A', label:"A", x:0, y:0})
nodes.add({id:'B', label:"B", x:100, y:0})
nodes.add({id:'C', label:"C", x:200, y:0})
nodes.add({id:'D', label:"D", x:0, y:100})
nodes.add({id:'E', label:"E", x:100, y:100})
nodes.add({id:'F', label:"F", x:200, y:100})
nodes.add({id:'G', label:"G", x:-100, y:200})
var options = {
groups: {
a: {
shape: 'image',
image: 'http://sc.chinaz.com/Files/pic/icons/1075/Hustler_063.png',
},
b: {
shape: 'image',
image: 'http://d2.72sc.com/pic/141128/knymkdvpqvz.png'
}
}
}
var edges = new vis.DataSet()
edges.add({id:'1', from:"A", to:"B"})
edges.add({id:'2', from:"B", to:"C"})
edges.add({id:'3', from:"D", to:"E"})
edges.add({id:'4', from:"E", to:"F"})
edges.add({id:'5', from:"D", to:"G"})
var options = {physics:false, edges:{smooth:false}};
var network = new vis.Network(document.getElementById("mynetwork"), { var network = new vis.Network(document.getElementById("mynetwork"), {
nodes: nodes
nodes: nodes,
edges: edges
}, options) }, options)
setTimeout(function() {
nodes.update({id: 'foo', group: 'b'})
console.log("update")
},3000)
console.log("MAKE C1")
network.cluster({
joinCondition:function(nodeOptions) {
return nodeOptions.id == "B" || nodeOptions.id == "E";
},
clusterNodeProperties: {id:'C1', label:"C1"}
})
console.log("MAKE C2")
network.cluster({
joinCondition:function(nodeOptions) {
return nodeOptions.id == "D" || nodeOptions.id == "G";
},
clusterNodeProperties: {id:'C2', label:"C2"}
})
network.openCluster('C1', {
releaseFunction: function (cpos, npos) {
console.log(npos)
return npos;
}
})
</script> </script>

Loading…
Cancel
Save