Browse Source

setup for modularization, move to v4 branch

flowchartTest
Alex de Mulder 9 years ago
parent
commit
0521284a4f
13 changed files with 1323 additions and 26 deletions
  1. +88
    -12
      dist/vis.js
  2. +8
    -0
      lib/network/Network.js
  3. +14
    -6
      lib/network/mixins/ClusterMixin.js
  4. +2
    -2
      lib/network/mixins/physics/BarnesHutMixin.js
  5. +1
    -1
      lib/network/mixins/physics/RepulsionMixin.js
  6. +635
    -5
      lib/network/modules/ClusterEngine.js
  7. +33
    -0
      lib/network/modules/PhysicsEngine.js
  8. +0
    -0
      lib/network/modules/clustering/backend.js
  9. +0
    -0
      lib/network/modules/clustering/public.js
  10. +0
    -0
      lib/network/modules/clustering/support.js
  11. +409
    -0
      lib/network/modules/components/BarnesHutSolver.js
  12. +32
    -0
      lib/network/modules/components/CentralGravitySolver.js
  13. +101
    -0
      lib/network/modules/components/SpringSolver.js

+ 88
- 12
dist/vis.js View File

@ -31142,13 +31142,15 @@ return /******/ (function(modules) { // webpackBootstrap
this._wrapUp();
}
/**
* loop over all nodes, check if they adhere to the condition and cluster if needed.
* @param options
* @param doNotUpdateCalculationNodes
*/
exports.clusterByNodeData = function(options, doNotUpdateCalculationNodes) {
if (options === undefined) {
throw new Error("Cannot call clusterByNodeData without options.")
}
if (options.joinCondition === undefined) {
throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options.");
}
if (options === undefined) {throw new Error("Cannot call clusterByNodeData without options.");}
if (options.joinCondition === undefined) {throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options.");}
// check if the options object is fine, append if needed
options = this._checkOptions(options);
@ -31168,6 +31170,12 @@ return /******/ (function(modules) { // webpackBootstrap
this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes);
}
/**
* Cluster all nodes in the network that have only 1 edge
* @param options
* @param doNotUpdateCalculationNodes
*/
exports.clusterOutliers = function(options, doNotUpdateCalculationNodes) {
options = this._checkOptions(options);
@ -31205,7 +31213,9 @@ return /******/ (function(modules) { // webpackBootstrap
this._cluster(clusters[i].nodes, clusters[i].edges, options, true)
}
this._wrapUp();
if (doNotUpdateCalculationNodes !== true) {
this._wrapUp();
}
}
/**
@ -31225,17 +31235,15 @@ return /******/ (function(modules) { // webpackBootstrap
if (options.clusterNodeProperties.y === undefined) {options.clusterNodeProperties.y = node.y; options.clusterNodeProperties.allowedToMoveY = !node.yFixed;}
var childNodesObj = {};
var edge;
var childEdgesObj = {}
var childNodeId;
var parentNodeId = node.id;
var parentClonedOptions = this._cloneOptions(parentNodeId);
childNodesObj[parentNodeId] = node;
// collect the nodes that will be in the cluster
for (var i = 0; i < node.edges.length; i++) {
edge = node.edges[i];
childNodeId = this._getConnectedId(edge, parentNodeId);
var edge = node.edges[i];
var childNodeId = this._getConnectedId(edge, parentNodeId);
if (childNodeId !== parentNodeId) {
if (options.joinCondition === undefined) {
@ -31259,6 +31267,14 @@ return /******/ (function(modules) { // webpackBootstrap
this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes);
}
/**
* This returns a clone of the options or properties of the edge or node to be used for construction of new edges or check functions for new nodes.
* @param objId
* @param type
* @returns {{}}
* @private
*/
exports._cloneOptions = function(objId, type) {
var clonedOptions = {};
if (type === undefined || type == 'node') {
@ -31272,6 +31288,16 @@ return /******/ (function(modules) { // webpackBootstrap
return clonedOptions;
}
/**
* This function creates the edges that will be attached to the cluster.
*
* @param childNodesObj
* @param childEdgesObj
* @param newEdges
* @param options
* @private
*/
exports._createClusterEdges = function (childNodesObj, childEdgesObj, newEdges, options) {
var edge, childNodeId, childNode;
@ -31320,12 +31346,18 @@ return /******/ (function(modules) { // webpackBootstrap
}
/**
* This function checks the options that can be supplied to the different cluster functions
* for certain fields and inserts defaults if needed
* @param options
* @returns {*}
* @private
*/
exports._checkOptions = function(options) {
if (options === undefined) {options = {};}
if (options.clusterEdgeProperties === undefined) {options.clusterEdgeProperties = {};}
if (options.clusterNodeProperties === undefined) {options.clusterNodeProperties = {};}
return options;
}
@ -31398,6 +31430,7 @@ return /******/ (function(modules) { // webpackBootstrap
// create the clusterNode
var clusterNode = new Node(clusterNodeProperties, this.images, this.groups, this.constants);
clusterNode.isCluster = true;
clusterNode.containedNodes = childNodesObj;
clusterNode.containedEdges = childEdgesObj;
@ -31455,6 +31488,22 @@ return /******/ (function(modules) { // webpackBootstrap
}
/**
* Check if a node is a cluster.
* @param nodeId
* @returns {*}
*/
exports.isCluster = function(nodeId) {
if (this.nodes[nodeId] !== undefined) {
return this.nodes[nodeId].isCluster;
}
else {
console.log("Node does not exist.")
return false;
}
}
/**
* get the position of the cluster node based on what's inside
* @param {object} childNodesObj | object with node objects, id as keys
@ -31570,6 +31619,11 @@ return /******/ (function(modules) { // webpackBootstrap
}
}
/**
* Recalculate navigation nodes, color edges dirty, update nodes list etc.
* @private
*/
exports._wrapUp = function() {
this._updateNodeIndexList();
this._updateCalculationNodes();
@ -31580,6 +31634,15 @@ return /******/ (function(modules) { // webpackBootstrap
}
}
/**
* Connect an edge that was previously contained from cluster A to cluster B if the node that it was originally connected to
* is currently residing in cluster B
* @param edge
* @param nodeId
* @param from
* @private
*/
exports._connectEdge = function(edge, nodeId, from) {
var clusterStack = this._getClusterStack(nodeId);
if (from == true) {
@ -31597,6 +31660,12 @@ return /******/ (function(modules) { // webpackBootstrap
edge.connect();
}
/**
* Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node
* @param nodeId
* @returns {Array}
* @private
*/
exports._getClusterStack = function(nodeId) {
var stack = [];
var max = 100;
@ -31612,6 +31681,13 @@ return /******/ (function(modules) { // webpackBootstrap
}
/**
* Get the Id the node is connected to
* @param edge
* @param nodeId
* @returns {*}
* @private
*/
exports._getConnectedId = function(edge, nodeId) {
if (edge.toId != nodeId) {
return edge.toId;

+ 8
- 0
lib/network/Network.js View File

@ -286,6 +286,14 @@ function Network (container, data, options) {
this.draggingNodes = false;
// containers for nodes and edges
this.body = {
calculationNodes: {},
calculationNodeIndices: {},
nodeIndices: {},
nodes: {},
edges: {}
}
this.calculationNodes = {};
this.calculationNodeIndices = [];
this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation

+ 14
- 6
lib/network/mixins/ClusterMixin.js View File

@ -31,13 +31,15 @@ exports.clusterByConnectionCount = function(hubsize, options) {
this._wrapUp();
}
/**
* loop over all nodes, check if they adhere to the condition and cluster if needed.
* @param options
* @param doNotUpdateCalculationNodes
*/
exports.clusterByNodeData = function(options, doNotUpdateCalculationNodes) {
if (options === undefined) {
throw new Error("Cannot call clusterByNodeData without options.")
}
if (options.joinCondition === undefined) {
throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options.");
}
if (options === undefined) {throw new Error("Cannot call clusterByNodeData without options.");}
if (options.joinCondition === undefined) {throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options.");}
// check if the options object is fine, append if needed
options = this._checkOptions(options);
@ -57,6 +59,12 @@ exports.clusterByNodeData = function(options, doNotUpdateCalculationNodes) {
this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes);
}
/**
* Cluster all nodes in the network that have only 1 edge
* @param options
* @param doNotUpdateCalculationNodes
*/
exports.clusterOutliers = function(options, doNotUpdateCalculationNodes) {
options = this._checkOptions(options);

+ 2
- 2
lib/network/mixins/physics/BarnesHutMixin.js View File

@ -19,7 +19,7 @@ exports._calculateNodeForces = function() {
for (var i = 0; i < nodeCount; i++) {
node = nodes[nodeIndices[i]];
if (node.options.mass > 0) {
// starting with root is irrelevant, it never passes the BarnesHut condition
// starting with root is irrelevant, it never passes the BarnesHutSolver condition
this._getForceContribution(barnesHutTree.root.children.NW,node);
this._getForceContribution(barnesHutTree.root.children.NE,node);
this._getForceContribution(barnesHutTree.root.children.SW,node);
@ -48,7 +48,7 @@ exports._getForceContribution = function(parentBranch,node) {
dy = parentBranch.centerOfMass.y - node.y;
distance = Math.sqrt(dx * dx + dy * dy);
// BarnesHut condition
// BarnesHutSolver condition
// original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed
// calcSize = 1/s --> d * 1/s > 1/theta = passed
if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.thetaInverted) {

+ 1
- 1
lib/network/mixins/physics/RepulsionMixin.js View File

@ -31,7 +31,7 @@ exports._calculateNodeForces = function () {
dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy);
// same condition as BarnesHut, making sure nodes are never 100% overlapping.
// same condition as BarnesHutSolver, making sure nodes are never 100% overlapping.
if (distance == 0) {
distance = 0.1*Math.random();
dx = distance;

+ 635
- 5
lib/network/modules/ClusterEngine.js View File

@ -2,15 +2,645 @@
* Created by Alex on 2/20/2015.
*/
var public = require("./clustering/public");
var support = require("./clustering/support");
var backend = require("./clustering/backend");
var Node = require('../Node');
var Edge = require('../Edge');
var util = require('../../util');
function ClusterEngine(network) {
this.network = network;
function ClusterEngine(data,options) {
this.nodes = data.nodes;
this.edges = data.edges;
this.nodeIndices = data.nodeIndices;
this.emitter = data.emitter;
this.clusteredNodes = {};
}
/**
*
* @param hubsize
* @param options
*/
ClusterEngine.prototype.clusterByConnectionCount = function(hubsize, options) {
if (hubsize === undefined) {
hubsize = this._getHubSize();
}
else if (tyepof(hubsize) == "object") {
options = this._checkOptions(hubsize);
hubsize = this._getHubSize();
}
var nodesToCluster = [];
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
if (node.edges.length >= hubsize) {
nodesToCluster.push(node.id);
}
}
for (var i = 0; i < nodesToCluster.length; i++) {
var node = this.nodes[nodesToCluster[i]];
this.clusterByConnection(node,options,{},{},true);
}
this.emitter.emit('dataChanged');
}
/**
* loop over all nodes, check if they adhere to the condition and cluster if needed.
* @param options
* @param doNotUpdateCalculationNodes
*/
ClusterEngine.prototype.clusterByNodeData = function(options, doNotUpdateCalculationNodes) {
if (options === undefined) {throw new Error("Cannot call clusterByNodeData without options.");}
if (options.joinCondition === undefined) {throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options.");}
// check if the options object is fine, append if needed
options = this._checkOptions(options);
var childNodesObj = {};
var childEdgesObj = {}
// collect the nodes that will be in the cluster
for (var i = 0; i < this.nodeIndices.length; i++) {
var nodeId = this.nodeIndices[i];
var clonedOptions = this._cloneOptions(nodeId);
if (options.joinCondition(clonedOptions) == true) {
childNodesObj[nodeId] = this.nodes[nodeId];
}
}
this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes);
}
/**
* Cluster all nodes in the network that have only 1 edge
* @param options
* @param doNotUpdateCalculationNodes
*/
ClusterEngine.prototype.clusterOutliers = function(options, doNotUpdateCalculationNodes) {
options = this._checkOptions(options);
var clusters = []
// collect the nodes that will be in the cluster
for (var i = 0; i < this.nodeIndices.length; i++) {
var childNodesObj = {};
var childEdgesObj = {};
var nodeId = this.nodeIndices[i];
if (this.nodes[nodeId].edges.length == 1) {
var edge = this.nodes[nodeId].edges[0];
var childNodeId = this._getConnectedId(edge, nodeId);
if (childNodeId != nodeId) {
if (options.joinCondition === undefined) {
childNodesObj[nodeId] = this.nodes[nodeId];
childNodesObj[childNodeId] = this.nodes[childNodeId];
}
else {
var clonedOptions = this._cloneOptions(nodeId);
if (options.joinCondition(clonedOptions) == true) {
childNodesObj[nodeId] = this.nodes[nodeId];
}
clonedOptions = this._cloneOptions(childNodeId);
if (options.joinCondition(clonedOptions) == true) {
childNodesObj[childNodeId] = this.nodes[childNodeId];
}
}
clusters.push({nodes:childNodesObj, edges:childEdgesObj})
}
}
}
for (var i = 0; i < clusters.length; i++) {
this._cluster(clusters[i].nodes, clusters[i].edges, options, true)
}
if (doNotUpdateCalculationNodes !== true) {
this.emitter.emit('dataChanged');
}
}
/**
*
* @param nodeId
* @param options
* @param doNotUpdateCalculationNodes
*/
ClusterEngine.prototype.clusterByConnection = function(nodeId, options, doNotUpdateCalculationNodes) {
// kill conditions
if (nodeId === undefined) {throw new Error("No nodeId supplied to clusterByConnection!");}
if (this.nodes[nodeId] === undefined) {throw new Error("The nodeId given to clusterByConnection does not exist!");}
var node = this.nodes[nodeId];
options = this._checkOptions(options, node);
if (options.clusterNodeProperties.x === undefined) {options.clusterNodeProperties.x = node.x; options.clusterNodeProperties.allowedToMoveX = !node.xFixed;}
if (options.clusterNodeProperties.y === undefined) {options.clusterNodeProperties.y = node.y; options.clusterNodeProperties.allowedToMoveY = !node.yFixed;}
var childNodesObj = {};
var childEdgesObj = {}
var parentNodeId = node.id;
var parentClonedOptions = this._cloneOptions(parentNodeId);
childNodesObj[parentNodeId] = node;
// collect the nodes that will be in the cluster
for (var i = 0; i < node.edges.length; i++) {
var edge = node.edges[i];
var childNodeId = this._getConnectedId(edge, parentNodeId);
if (childNodeId !== parentNodeId) {
if (options.joinCondition === undefined) {
childEdgesObj[edge.id] = edge;
childNodesObj[childNodeId] = this.nodes[childNodeId];
}
else {
// clone the options and insert some additional parameters that could be interesting.
var childClonedOptions = this._cloneOptions(childNodeId);
if (options.joinCondition(parentClonedOptions, childClonedOptions) == true) {
childEdgesObj[edge.id] = edge;
childNodesObj[childNodeId] = this.nodes[childNodeId];
}
}
}
else {
childEdgesObj[edge.id] = edge;
}
}
this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes);
}
/**
* This returns a clone of the options or properties of the edge or node to be used for construction of new edges or check functions for new nodes.
* @param objId
* @param type
* @returns {{}}
* @private
*/
ClusterEngine.prototype._cloneOptions = function(objId, type) {
var clonedOptions = {};
if (type === undefined || type == 'node') {
util.deepExtend(clonedOptions, this.nodes[objId].options, true);
util.deepExtend(clonedOptions, this.nodes[objId].properties, true);
clonedOptions.amountOfConnections = this.nodes[objId].edges.length;
}
else {
util.deepExtend(clonedOptions, this.edges[objId].properties, true);
}
return clonedOptions;
}
/**
* This function creates the edges that will be attached to the cluster.
*
* @param childNodesObj
* @param childEdgesObj
* @param newEdges
* @param options
* @private
*/
ClusterEngine.prototype._createClusterEdges = function (childNodesObj, childEdgesObj, newEdges, options) {
var edge, childNodeId, childNode;
var childKeys = Object.keys(childNodesObj);
for (var i = 0; i < childKeys.length; i++) {
childNodeId = childKeys[i];
childNode = childNodesObj[childNodeId];
// mark all edges for removal from global and construct new edges from the cluster to others
for (var j = 0; j < childNode.edges.length; j++) {
edge = childNode.edges[j];
childEdgesObj[edge.id] = edge;
var otherNodeId = edge.toId;
var otherOnTo = true;
if (edge.toId != childNodeId) {
otherNodeId = edge.toId;
otherOnTo = true;
}
else if (edge.fromId != childNodeId) {
otherNodeId = edge.fromId;
otherOnTo = false;
}
if (childNodesObj[otherNodeId] === undefined) {
var clonedOptions = this._cloneOptions(edge.id, 'edge');
util.deepExtend(clonedOptions, options.clusterEdgeProperties);
// avoid forcing the default color on edges that inherit color
if (edge.properties.color === undefined) {
delete clonedOptions.color;
}
if (otherOnTo === true) {
clonedOptions.from = options.clusterNodeProperties.id;
clonedOptions.to = otherNodeId;
}
else {
clonedOptions.from = otherNodeId;
clonedOptions.to = options.clusterNodeProperties.id;
}
clonedOptions.id = 'clusterEdge:' + util.randomUUID();
newEdges.push(new Edge(clonedOptions,this,this.constants))
}
}
}
}
/**
* This function checks the options that can be supplied to the different cluster functions
* for certain fields and inserts defaults if needed
* @param options
* @returns {*}
* @private
*/
ClusterEngine.prototype._checkOptions = function(options) {
if (options === undefined) {options = {};}
if (options.clusterEdgeProperties === undefined) {options.clusterEdgeProperties = {};}
if (options.clusterNodeProperties === undefined) {options.clusterNodeProperties = {};}
return options;
}
/**
*
* @param {Object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node
* @param {Object} childEdgesObj | object with edge objects, id as keys
* @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties}
* @param {Boolean} doNotUpdateCalculationNodes | when true, do not wrap up
* @private
*/
ClusterEngine.prototype._cluster = function(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes) {
// kill condition: no children so cant cluster
if (Object.keys(childNodesObj).length == 0) {return;}
// check if we have an unique id;
if (options.clusterNodeProperties.id === undefined) {options.clusterNodeProperties.id = 'cluster:' + util.randomUUID();}
var clusterId = options.clusterNodeProperties.id;
// create the new edges that will connect to the cluster
var newEdges = [];
this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, options);
// construct the clusterNodeProperties
var clusterNodeProperties = options.clusterNodeProperties;
if (options.processProperties !== undefined) {
// get the childNode options
var childNodesOptions = [];
for (var nodeId in childNodesObj) {
var clonedOptions = this._cloneOptions(nodeId);
childNodesOptions.push(clonedOptions);
}
// get clusterproperties based on childNodes
var childEdgesOptions = [];
for (var edgeId in childEdgesObj) {
var clonedOptions = this._cloneOptions(edgeId, 'edge');
childEdgesOptions.push(clonedOptions);
}
clusterNodeProperties = options.processProperties(clusterNodeProperties, childNodesOptions, childEdgesOptions);
if (!clusterNodeProperties) {
throw new Error("The processClusterProperties function does not return properties!");
}
}
if (clusterNodeProperties.label === undefined) {
clusterNodeProperties.label = 'cluster';
}
// give the clusterNode a postion if it does not have one.
var pos = undefined
if (clusterNodeProperties.x === undefined) {
pos = this._getClusterPosition(childNodesObj);
clusterNodeProperties.x = pos.x;
clusterNodeProperties.allowedToMoveX = true;
}
if (clusterNodeProperties.x === undefined) {
if (pos === undefined) {
pos = this._getClusterPosition(childNodesObj);
}
clusterNodeProperties.y = pos.y;
clusterNodeProperties.allowedToMoveY = true;
}
// force the ID to remain the same
clusterNodeProperties.id = clusterId;
// create the clusterNode
var clusterNode = new Node(clusterNodeProperties, this.images, this.groups, this.constants);
clusterNode.isCluster = true;
clusterNode.containedNodes = childNodesObj;
clusterNode.containedEdges = childEdgesObj;
// delete contained edges from global
for (var edgeId in childEdgesObj) {
if (childEdgesObj.hasOwnProperty(edgeId)) {
if (this.edges[edgeId] !== undefined) {
if (this.edges[edgeId].via !== null) {
var viaId = this.edges[edgeId].via.id;
if (viaId) {
this.edges[edgeId].via = null
delete this.sectors['support']['nodes'][viaId];
}
}
this.edges[edgeId].disconnect();
delete this.edges[edgeId];
}
}
}
// remove contained nodes from global
for (var nodeId in childNodesObj) {
if (childNodesObj.hasOwnProperty(nodeId)) {
this.clusteredNodes[nodeId] = {clusterId:clusterNodeProperties.id, node: this.nodes[nodeId]};
delete this.nodes[nodeId];
}
}
// finally put the cluster node into global
this.nodes[clusterNodeProperties.id] = clusterNode;
// push new edges to global
for (var i = 0; i < newEdges.length; i++) {
this.edges[newEdges[i].id] = newEdges[i];
this.edges[newEdges[i].id].connect();
}
// create bezier nodes for smooth curves if needed
this._createBezierNodes(newEdges);
// set ID to undefined so no duplicates arise
clusterNodeProperties.id = undefined;
// wrap up
if (doNotUpdateCalculationNodes !== true) {
this.emitter.emit('dataChanged');
}
}
/**
* Check if a node is a cluster.
* @param nodeId
* @returns {*}
*/
ClusterEngine.prototype.isCluster = function(nodeId) {
if (this.nodes[nodeId] !== undefined) {
return this.nodes[nodeId].isCluster;
}
else {
console.log("Node does not exist.")
return false;
}
}
/**
* get the position of the cluster node based on what's inside
* @param {object} childNodesObj | object with node objects, id as keys
* @returns {{x: number, y: number}}
* @private
*/
ClusterEngine.prototype._getClusterPosition = function(childNodesObj) {
var childKeys = Object.keys(childNodesObj);
var minX = childNodesObj[childKeys[0]].x;
var maxX = childNodesObj[childKeys[0]].x;
var minY = childNodesObj[childKeys[0]].y;
var maxY = childNodesObj[childKeys[0]].y;
var node;
for (var i = 0; i < childKeys.lenght; i++) {
node = childNodesObj[childKeys[0]];
minX = node.x < minX ? node.x : minX;
maxX = node.x > maxX ? node.x : maxX;
minY = node.y < minY ? node.y : minY;
maxY = node.y > maxY ? node.y : maxY;
}
return {x: 0.5*(minX + maxX), y: 0.5*(minY + maxY)};
}
/**
* Open a cluster by calling this function.
* @param {String} clusterNodeId | the ID of the cluster node
* @param {Boolean} doNotUpdateCalculationNodes | wrap up afterwards if not true
*/
ClusterEngine.prototype.openCluster = function(clusterNodeId, doNotUpdateCalculationNodes) {
// kill conditions
if (clusterNodeId === undefined) {throw new Error("No clusterNodeId supplied to openCluster.");}
if (this.nodes[clusterNodeId] === undefined) {throw new Error("The clusterNodeId supplied to openCluster does not exist.");}
if (this.nodes[clusterNodeId].containedNodes === undefined) {console.log("The node:" + clusterNodeId + " is not a cluster."); return};
var node = this.nodes[clusterNodeId];
var containedNodes = node.containedNodes;
var containedEdges = node.containedEdges;
// release nodes
for (var nodeId in containedNodes) {
if (containedNodes.hasOwnProperty(nodeId)) {
this.nodes[nodeId] = containedNodes[nodeId];
// inherit position
this.nodes[nodeId].x = node.x;
this.nodes[nodeId].y = node.y;
// inherit speed
this.nodes[nodeId].vx = node.vx;
this.nodes[nodeId].vy = node.vy;
delete this.clusteredNodes[nodeId];
}
}
// release edges
for (var edgeId in containedEdges) {
if (containedEdges.hasOwnProperty(edgeId)) {
this.edges[edgeId] = containedEdges[edgeId];
this.edges[edgeId].connect();
var edge = this.edges[edgeId];
if (edge.connected === false) {
if (this.clusteredNodes[edge.fromId] !== undefined) {
this._connectEdge(edge, edge.fromId, true);
}
if (this.clusteredNodes[edge.toId] !== undefined) {
this._connectEdge(edge, edge.toId, false);
}
}
}
}
this._createBezierNodes(containedEdges);
var edgeIds = [];
for (var i = 0; i < node.edges.length; i++) {
edgeIds.push(node.edges[i].id);
}
// remove edges in clusterNode
for (var i = 0; i < edgeIds.length; i++) {
var edge = this.edges[edgeIds[i]];
// if the edge should have been connected to a contained node
if (edge.fromArray.length > 0 && edge.fromId == clusterNodeId) {
// the node in the from array was contained in the cluster
if (this.nodes[edge.fromArray[0].id] !== undefined) {
this._connectEdge(edge, edge.fromArray[0].id, true);
}
}
else if (edge.toArray.length > 0 && edge.toId == clusterNodeId) {
// the node in the to array was contained in the cluster
if (this.nodes[edge.toArray[0].id] !== undefined) {
this._connectEdge(edge, edge.toArray[0].id, false);
}
}
else {
var edgeId = edgeIds[i];
var viaId = this.edges[edgeId].via.id;
if (viaId) {
this.edges[edgeId].via = null
delete this.sectors['support']['nodes'][viaId];
}
// this removes the edge from node.edges, which is why edgeIds is formed
this.edges[edgeId].disconnect();
delete this.edges[edgeId];
}
}
// remove clusterNode
delete this.nodes[clusterNodeId];
if (doNotUpdateCalculationNodes !== true) {
this.emitter.emit('dataChanged');
}
}
/**
* Recalculate navigation nodes, color edges dirty, update nodes list etc.
* @private
*/
ClusterEngine.prototype._wrapUp = function() {
this._updateNodeIndexList();
this._updateCalculationNodes();
this._markAllEdgesAsDirty();
if (this.initializing !== true) {
this.moving = true;
this.start();
}
}
/**
* Connect an edge that was previously contained from cluster A to cluster B if the node that it was originally connected to
* is currently residing in cluster B
* @param edge
* @param nodeId
* @param from
* @private
*/
ClusterEngine.prototype._connectEdge = function(edge, nodeId, from) {
var clusterStack = this._getClusterStack(nodeId);
if (from == true) {
edge.from = clusterStack[clusterStack.length - 1];
edge.fromId = clusterStack[clusterStack.length - 1].id;
clusterStack.pop()
edge.fromArray = clusterStack;
}
else {
edge.to = clusterStack[clusterStack.length - 1];
edge.toId = clusterStack[clusterStack.length - 1].id;
clusterStack.pop();
edge.toArray = clusterStack;
}
edge.connect();
}
/**
* Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node
* @param nodeId
* @returns {Array}
* @private
*/
ClusterEngine.prototype._getClusterStack = function(nodeId) {
var stack = [];
var max = 100;
var counter = 0;
while (this.clusteredNodes[nodeId] !== undefined && counter < max) {
stack.push(this.clusteredNodes[nodeId].node);
nodeId = this.clusteredNodes[nodeId].clusterId;
counter++;
}
stack.push(this.nodes[nodeId]);
return stack;
}
/**
* Get the Id the node is connected to
* @param edge
* @param nodeId
* @returns {*}
* @private
*/
ClusterEngine.prototype._getConnectedId = function(edge, nodeId) {
if (edge.toId != nodeId) {
return edge.toId;
}
else if (edge.fromId != nodeId) {
return edge.fromId;
}
else {
return edge.fromId;
}
}
/**
* We determine how many connections denote an important hub.
* We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
*
* @private
*/
ClusterEngine.prototype._getHubSize = function() {
var average = 0;
var averageSquared = 0;
var hubCounter = 0;
var largestHub = 0;
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
if (node.edges.length > largestHub) {
largestHub = node.edges.length;
}
average += node.edges.length;
averageSquared += Math.pow(node.edges.length,2);
hubCounter += 1;
}
average = average / hubCounter;
averageSquared = averageSquared / hubCounter;
var variance = averageSquared - Math.pow(average,2);
var standardDeviation = Math.sqrt(variance);
var hubThreshold = Math.floor(average + 2*standardDeviation);
// always have at least one to cluster
if (hubThreshold > largestHub) {
hubThreshold = largestHub;
}
return hubThreshold;
};

+ 33
- 0
lib/network/modules/PhysicsEngine.js View File

@ -0,0 +1,33 @@
/**
* Created by Alex on 2/23/2015.
*/
var BarnesHut = require("./compontents/BarnesHutSolver")
var SpringSolver = require("./compontents/SpringSolver")
var CentralGravitySolver = require("./compontents/CentralGravitySolver")
function PhysicsEngine(body, options) {
this.body = body;
this.nodesSolver = new BarnesHut(body, options);
this.edgesSolver = new SpringSolver(body, options);
this.gravitySolver = new CentralGravitySolver(body, options);
}
PhysicsEngine.prototype.calculateField = function () {
this.nodesSolver.solve();
};
PhysicsEngine.prototype.calculateSprings = function () {
this.edgesSolver.solve();
};
PhysicsEngine.prototype.calculateCentralGravity = function () {
this.gravitySolver.solve();
};
PhysicsEngine.prototype.calculate = function () {
this.calculateCentralGravity();
this.calculateField();
this.calculateSprings();
};

+ 0
- 0
lib/network/modules/clustering/backend.js View File


+ 0
- 0
lib/network/modules/clustering/public.js View File


+ 0
- 0
lib/network/modules/clustering/support.js View File


+ 409
- 0
lib/network/modules/components/BarnesHutSolver.js View File

@ -0,0 +1,409 @@
/**
* Created by Alex on 2/23/2015.
*/
function BarnesHutSolver(body, options) {
this.body = body;
this.options = options;
}
/**
* This function calculates the forces the nodes apply on eachother based on a gravitational model.
* The Barnes Hut method is used to speed up this N-body simulation.
*
* @private
*/
BarnesHutSolver.prototype.solve = function() {
if (this.options.gravitationalConstant != 0) {
var node;
var nodes = this.body.calculationNodes;
var nodeIndices = this.body.calculationNodeIndices;
var nodeCount = nodeIndices.length;
var barnesHutTree = this._formBarnesHutTree(nodes,nodeIndices);
// place the nodes one by one recursively
for (var i = 0; i < nodeCount; i++) {
node = nodes[nodeIndices[i]];
if (node.options.mass > 0) {
// starting with root is irrelevant, it never passes the BarnesHutSolver condition
this._getForceContribution(barnesHutTree.root.children.NW,node);
this._getForceContribution(barnesHutTree.root.children.NE,node);
this._getForceContribution(barnesHutTree.root.children.SW,node);
this._getForceContribution(barnesHutTree.root.children.SE,node);
}
}
}
};
/**
* This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
* If a region contains a single node, we check if it is not itself, then we apply the force.
*
* @param parentBranch
* @param node
* @private
*/
BarnesHutSolver.prototype._getForceContribution = function(parentBranch,node) {
// we get no force contribution from an empty region
if (parentBranch.childrenCount > 0) {
var dx,dy,distance;
// get the distance from the center of mass to the node.
dx = parentBranch.centerOfMass.x - node.x;
dy = parentBranch.centerOfMass.y - node.y;
distance = Math.sqrt(dx * dx + dy * dy);
// BarnesHutSolver condition
// original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed
// calcSize = 1/s --> d * 1/s > 1/theta = passed
if (distance * parentBranch.calcSize > this.options.thetaInverted) {
// duplicate code to reduce function calls to speed up program
if (distance == 0) {
distance = 0.1*Math.random();
dx = distance;
}
var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance);
var fx = dx * gravityForce;
var fy = dy * gravityForce;
node.fx += fx;
node.fy += fy;
}
else {
// Did not pass the condition, go into children if available
if (parentBranch.childrenCount == 4) {
this._getForceContribution(parentBranch.children.NW,node);
this._getForceContribution(parentBranch.children.NE,node);
this._getForceContribution(parentBranch.children.SW,node);
this._getForceContribution(parentBranch.children.SE,node);
}
else { // parentBranch must have only one node, if it was empty we wouldnt be here
if (parentBranch.children.data.id != node.id) { // if it is not self
// duplicate code to reduce function calls to speed up program
if (distance == 0) {
distance = 0.5*Math.random();
dx = distance;
}
var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance);
var fx = dx * gravityForce;
var fy = dy * gravityForce;
node.fx += fx;
node.fy += fy;
}
}
}
}
};
/**
* This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
*
* @param nodes
* @param nodeIndices
* @private
*/
BarnesHutSolver.prototype._formBarnesHutTree = function(nodes,nodeIndices) {
var node;
var nodeCount = nodeIndices.length;
var minX = Number.MAX_VALUE,
minY = Number.MAX_VALUE,
maxX =-Number.MAX_VALUE,
maxY =-Number.MAX_VALUE;
// get the range of the nodes
for (var i = 0; i < nodeCount; i++) {
var x = nodes[nodeIndices[i]].x;
var y = nodes[nodeIndices[i]].y;
if (nodes[nodeIndices[i]].options.mass > 0) {
if (x < minX) { minX = x; }
if (x > maxX) { maxX = x; }
if (y < minY) { minY = y; }
if (y > maxY) { maxY = y; }
}
}
// make the range a square
var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y
if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize
else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize
var minimumTreeSize = 1e-5;
var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX));
var halfRootSize = 0.5 * rootSize;
var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY);
// construct the barnesHutTree
var barnesHutTree = {
root:{
centerOfMass: {x:0, y:0},
mass:0,
range: {
minX: centerX-halfRootSize,maxX:centerX+halfRootSize,
minY: centerY-halfRootSize,maxY:centerY+halfRootSize
},
size: rootSize,
calcSize: 1 / rootSize,
children: { data:null},
maxWidth: 0,
level: 0,
childrenCount: 4
}
};
this._splitBranch(barnesHutTree.root);
// place the nodes one by one recursively
for (i = 0; i < nodeCount; i++) {
node = nodes[nodeIndices[i]];
if (node.options.mass > 0) {
this._placeInTree(barnesHutTree.root,node);
}
}
// make global
return barnesHutTree
};
/**
* this updates the mass of a branch. this is increased by adding a node.
*
* @param parentBranch
* @param node
* @private
*/
BarnesHutSolver.prototype._updateBranchMass = function(parentBranch, node) {
var totalMass = parentBranch.mass + node.options.mass;
var totalMassInv = 1/totalMass;
parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass;
parentBranch.centerOfMass.x *= totalMassInv;
parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass;
parentBranch.centerOfMass.y *= totalMassInv;
parentBranch.mass = totalMass;
var biggestSize = Math.max(Math.max(node.height,node.radius),node.width);
parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth;
};
/**
* determine in which branch the node will be placed.
*
* @param parentBranch
* @param node
* @param skipMassUpdate
* @private
*/
BarnesHutSolver.prototype._placeInTree = function(parentBranch,node,skipMassUpdate) {
if (skipMassUpdate != true || skipMassUpdate === undefined) {
// update the mass of the branch.
this._updateBranchMass(parentBranch,node);
}
if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW
if (parentBranch.children.NW.range.maxY > node.y) { // in NW
this._placeInRegion(parentBranch,node,"NW");
}
else { // in SW
this._placeInRegion(parentBranch,node,"SW");
}
}
else { // in NE or SE
if (parentBranch.children.NW.range.maxY > node.y) { // in NE
this._placeInRegion(parentBranch,node,"NE");
}
else { // in SE
this._placeInRegion(parentBranch,node,"SE");
}
}
};
/**
* actually place the node in a region (or branch)
*
* @param parentBranch
* @param node
* @param region
* @private
*/
BarnesHutSolver.prototype._placeInRegion = function(parentBranch,node,region) {
switch (parentBranch.children[region].childrenCount) {
case 0: // place node here
parentBranch.children[region].children.data = node;
parentBranch.children[region].childrenCount = 1;
this._updateBranchMass(parentBranch.children[region],node);
break;
case 1: // convert into children
// if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
// we move one node a pixel and we do not put it in the tree.
if (parentBranch.children[region].children.data.x == node.x &&
parentBranch.children[region].children.data.y == node.y) {
node.x += Math.random();
node.y += Math.random();
}
else {
this._splitBranch(parentBranch.children[region]);
this._placeInTree(parentBranch.children[region],node);
}
break;
case 4: // place in branch
this._placeInTree(parentBranch.children[region],node);
break;
}
};
/**
* this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch
* after the split is complete.
*
* @param parentBranch
* @private
*/
BarnesHutSolver.prototype._splitBranch = function(parentBranch) {
// if the branch is shaded with a node, replace the node in the new subset.
var containedNode = null;
if (parentBranch.childrenCount == 1) {
containedNode = parentBranch.children.data;
parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0;
}
parentBranch.childrenCount = 4;
parentBranch.children.data = null;
this._insertRegion(parentBranch,"NW");
this._insertRegion(parentBranch,"NE");
this._insertRegion(parentBranch,"SW");
this._insertRegion(parentBranch,"SE");
if (containedNode != null) {
this._placeInTree(parentBranch,containedNode);
}
};
/**
* This function subdivides the region into four new segments.
* Specifically, this inserts a single new segment.
* It fills the children section of the parentBranch
*
* @param parentBranch
* @param region
* @param parentRange
* @private
*/
BarnesHutSolver.prototype._insertRegion = function(parentBranch, region) {
var minX,maxX,minY,maxY;
var childSize = 0.5 * parentBranch.size;
switch (region) {
case "NW":
minX = parentBranch.range.minX;
maxX = parentBranch.range.minX + childSize;
minY = parentBranch.range.minY;
maxY = parentBranch.range.minY + childSize;
break;
case "NE":
minX = parentBranch.range.minX + childSize;
maxX = parentBranch.range.maxX;
minY = parentBranch.range.minY;
maxY = parentBranch.range.minY + childSize;
break;
case "SW":
minX = parentBranch.range.minX;
maxX = parentBranch.range.minX + childSize;
minY = parentBranch.range.minY + childSize;
maxY = parentBranch.range.maxY;
break;
case "SE":
minX = parentBranch.range.minX + childSize;
maxX = parentBranch.range.maxX;
minY = parentBranch.range.minY + childSize;
maxY = parentBranch.range.maxY;
break;
}
parentBranch.children[region] = {
centerOfMass:{x:0,y:0},
mass:0,
range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
size: 0.5 * parentBranch.size,
calcSize: 2 * parentBranch.calcSize,
children: {data:null},
maxWidth: 0,
level: parentBranch.level+1,
childrenCount: 0
};
};
/**
* This function is for debugging purposed, it draws the tree.
*
* @param ctx
* @param color
* @private
*/
BarnesHutSolver.prototype._drawTree = function(ctx,color) {
if (this.barnesHutTree !== undefined) {
ctx.lineWidth = 1;
this._drawBranch(this.barnesHutTree.root,ctx,color);
}
};
/**
* This function is for debugging purposes. It draws the branches recursively.
*
* @param branch
* @param ctx
* @param color
* @private
*/
BarnesHutSolver.prototype._drawBranch = function(branch,ctx,color) {
if (color === undefined) {
color = "#FF0000";
}
if (branch.childrenCount == 4) {
this._drawBranch(branch.children.NW,ctx);
this._drawBranch(branch.children.NE,ctx);
this._drawBranch(branch.children.SE,ctx);
this._drawBranch(branch.children.SW,ctx);
}
ctx.strokeStyle = color;
ctx.beginPath();
ctx.moveTo(branch.range.minX,branch.range.minY);
ctx.lineTo(branch.range.maxX,branch.range.minY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(branch.range.maxX,branch.range.minY);
ctx.lineTo(branch.range.maxX,branch.range.maxY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(branch.range.maxX,branch.range.maxY);
ctx.lineTo(branch.range.minX,branch.range.maxY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(branch.range.minX,branch.range.maxY);
ctx.lineTo(branch.range.minX,branch.range.minY);
ctx.stroke();
/*
if (branch.mass > 0) {
ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
ctx.stroke();
}
*/
};
module.exports = BarnesHutSolver;

+ 32
- 0
lib/network/modules/components/CentralGravitySolver.js View File

@ -0,0 +1,32 @@
/**
* Created by Alex on 2/23/2015.
*/
function CentralGravitySolver(body, options) {
this.body = body;
this.options = options;
}
CentralGravitySolver.prototype.solve = function () {
var dx, dy, distance, node, i;
var nodes = this.body.calculationNodes;
var gravity = this.options.centralGravity;
var gravityForce = 0;
for (i = 0; i < this.body.calculationNodeIndices.length; i++) {
node = nodes[this.body.calculationNodeIndices[i]];
node.damping = this.options.damping; // possibly add function to alter damping properties of clusters.
dx = -node.x;
dy = -node.y;
distance = Math.sqrt(dx * dx + dy * dy);
gravityForce = (distance == 0) ? 0 : (gravity / distance);
node.fx = dx * gravityForce;
node.fy = dy * gravityForce;
}
};
module.exports = CentralGravitySolver;

+ 101
- 0
lib/network/modules/components/SpringSolver.js View File

@ -0,0 +1,101 @@
/**
* Created by Alex on 2/23/2015.
*/
function SpringSolver(body, options) {
this.body = body;
this.options = options;
}
/**
* this function calculates the effects of the springs in the case of unsmooth curves.
*
* @private
*/
SpringSolver.prototype._calculateSpringForces = function () {
var edgeLength, edge, edgeId;
var edges = this.edges;
// forces caused by the edges, modelled as springs
for (edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
edge = edges[edgeId];
if (edge.connected === true) {
// only calculate forces if nodes are in the same sector
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
edgeLength = edge.physics.springLength;
this._calculateSpringForce(edge.from, edge.to, edgeLength);
}
}
}
}
};
/**
* This function calculates the springforces on the nodes, accounting for the support nodes.
*
* @private
*/
SpringSolver.prototype._calculateSpringForcesWithSupport = function () {
var edgeLength, edge, edgeId;
var edges = this.edges;
// forces caused by the edges, modelled as springs
for (edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
edge = edges[edgeId];
if (edge.connected === true) {
// only calculate forces if nodes are in the same sector
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
if (edge.via != null) {
var node1 = edge.to;
var node2 = edge.via;
var node3 = edge.from;
edgeLength = edge.physics.springLength;
this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
}
}
}
}
}
};
/**
* This is the code actually performing the calculation for the function above. It is split out to avoid repetition.
*
* @param node1
* @param node2
* @param edgeLength
* @private
*/
SpringSolver.prototype._calculateSpringForce = function (node1, node2, edgeLength) {
var dx, dy, fx, fy, springForce, distance;
dx = (node1.x - node2.x);
dy = (node1.y - node2.y);
distance = Math.sqrt(dx * dx + dy * dy);
distance = distance == 0 ? 0.01 : distance;
// the 1/distance is so the fx and fy can be calculated without sine or cosine.
springForce = this.options.springConstant * (edgeLength - distance) / distance;
fx = dx * springForce;
fy = dy * springForce;
node1.fx += fx;
node1.fy += fy;
node2.fx -= fx;
node2.fy -= fy;
};
module.exports = SpringSolver;

Loading…
Cancel
Save