From 0521284a4f0bf97dda12e4fb225be03c1b27bfe1 Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Tue, 24 Feb 2015 10:10:47 +0100 Subject: [PATCH] setup for modularization, move to v4 branch --- dist/vis.js | 100 ++- lib/network/Network.js | 8 + lib/network/mixins/ClusterMixin.js | 20 +- lib/network/mixins/physics/BarnesHutMixin.js | 4 +- lib/network/mixins/physics/RepulsionMixin.js | 2 +- lib/network/modules/ClusterEngine.js | 640 +++++++++++++++++- lib/network/modules/PhysicsEngine.js | 33 + lib/network/modules/clustering/backend.js | 0 lib/network/modules/clustering/public.js | 0 lib/network/modules/clustering/support.js | 0 .../modules/components/BarnesHutSolver.js | 409 +++++++++++ .../components/CentralGravitySolver.js | 32 + .../modules/components/SpringSolver.js | 101 +++ 13 files changed, 1323 insertions(+), 26 deletions(-) create mode 100644 lib/network/modules/PhysicsEngine.js delete mode 100644 lib/network/modules/clustering/backend.js delete mode 100644 lib/network/modules/clustering/public.js delete mode 100644 lib/network/modules/clustering/support.js create mode 100644 lib/network/modules/components/BarnesHutSolver.js create mode 100644 lib/network/modules/components/CentralGravitySolver.js create mode 100644 lib/network/modules/components/SpringSolver.js diff --git a/dist/vis.js b/dist/vis.js index 4ce885bd..13f90280 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -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; diff --git a/lib/network/Network.js b/lib/network/Network.js index 7d50bc99..d4f890a2 100644 --- a/lib/network/Network.js +++ b/lib/network/Network.js @@ -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 diff --git a/lib/network/mixins/ClusterMixin.js b/lib/network/mixins/ClusterMixin.js index bf5fde36..bde1f858 100644 --- a/lib/network/mixins/ClusterMixin.js +++ b/lib/network/mixins/ClusterMixin.js @@ -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); diff --git a/lib/network/mixins/physics/BarnesHutMixin.js b/lib/network/mixins/physics/BarnesHutMixin.js index b70f474c..b8ae1969 100644 --- a/lib/network/mixins/physics/BarnesHutMixin.js +++ b/lib/network/mixins/physics/BarnesHutMixin.js @@ -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) { diff --git a/lib/network/mixins/physics/RepulsionMixin.js b/lib/network/mixins/physics/RepulsionMixin.js index 4b3ef6ac..32e4ac2d 100644 --- a/lib/network/mixins/physics/RepulsionMixin.js +++ b/lib/network/mixins/physics/RepulsionMixin.js @@ -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; diff --git a/lib/network/modules/ClusterEngine.js b/lib/network/modules/ClusterEngine.js index 0ef8e7cc..76dd3acf 100644 --- a/lib/network/modules/ClusterEngine.js +++ b/lib/network/modules/ClusterEngine.js @@ -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; +}; + + diff --git a/lib/network/modules/PhysicsEngine.js b/lib/network/modules/PhysicsEngine.js new file mode 100644 index 00000000..5de2daae --- /dev/null +++ b/lib/network/modules/PhysicsEngine.js @@ -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(); +}; \ No newline at end of file diff --git a/lib/network/modules/clustering/backend.js b/lib/network/modules/clustering/backend.js deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/network/modules/clustering/public.js b/lib/network/modules/clustering/public.js deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/network/modules/clustering/support.js b/lib/network/modules/clustering/support.js deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/network/modules/components/BarnesHutSolver.js b/lib/network/modules/components/BarnesHutSolver.js new file mode 100644 index 00000000..0ba9237f --- /dev/null +++ b/lib/network/modules/components/BarnesHutSolver.js @@ -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; \ No newline at end of file diff --git a/lib/network/modules/components/CentralGravitySolver.js b/lib/network/modules/components/CentralGravitySolver.js new file mode 100644 index 00000000..154450f8 --- /dev/null +++ b/lib/network/modules/components/CentralGravitySolver.js @@ -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; \ No newline at end of file diff --git a/lib/network/modules/components/SpringSolver.js b/lib/network/modules/components/SpringSolver.js new file mode 100644 index 00000000..c22abc68 --- /dev/null +++ b/lib/network/modules/components/SpringSolver.js @@ -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; \ No newline at end of file