diff --git a/dist/vis.js b/dist/vis.js
index 17cd84b3..ec44132f 100644
--- a/dist/vis.js
+++ b/dist/vis.js
@@ -5,7 +5,7 @@
* A dynamic, browser-based visualization library.
*
* @version 4.0.0-SNAPSHOT
- * @date 2015-02-24
+ * @date 2015-02-25
*
* @license
* Copyright (C) 2011-2014 Almende B.V, http://almende.com
@@ -22767,7 +22767,7 @@ return /******/ (function(modules) { // webpackBootstrap
__webpack_require__(71);
var PhysicsEngine = __webpack_require__(72).PhysicsEngine;
- var ClusterEngine = __webpack_require__(76).ClusterEngine;
+ var ClusterEngine = __webpack_require__(73).ClusterEngine;
/**
@@ -24773,7 +24773,7 @@ return /******/ (function(modules) { // webpackBootstrap
}
}
- this._drawNodes(ctx, this.body.supportNodes, true);
+ //this._drawNodes(ctx,this.body.supportNodes,true);
// this.physics.nodesSolver._debug(ctx,"#F00F0F");
// restore original scaling and translation
@@ -25105,11 +25105,9 @@ return /******/ (function(modules) { // webpackBootstrap
};
- Network.prototype._revertPhysicsTick = function (nodes) {
- for (var nodeId in nodes) {
- if (nodes.hasOwnProperty(nodeId)) {
- nodes[nodeId].revertPosition();
- }
+ Network.prototype._revertPhysicsTick = function (nodes, nodeIndices) {
+ for (var i = 0; i < nodeIndices.length; i++) {
+ nodes[nodeIndices[i]].revertPosition();
}
};
@@ -25129,8 +25127,8 @@ return /******/ (function(modules) { // webpackBootstrap
// determine if the network has stabilzied
this.moving = mainMovingStatus || supportMovingStatus;
if (this.moving == false) {
- this._revertPhysicsTick(this.body.nodes);
- this._revertPhysicsTick(this.body.supportNodes);
+ this._revertPhysicsTick(this.body.nodes, this.body.nodeIndices);
+ this._revertPhysicsTick(this.body.supportNodes, this.body.supportNodeIndices);
} else {
// this is here to ensure that there is no start event when the network is already stable.
if (this.startedStabilization == false) {
@@ -27936,14 +27934,11 @@ return /******/ (function(modules) { // webpackBootstrap
if (body === undefined) {
throw "No body provided";
}
- var fields = ["edges", "physics"];
+ var fields = ["edges"];
var constants = util.selectiveBridgeObject(fields, networkConstants);
this.options = constants.edges;
- this.physics = constants.physics;
this.options.smoothCurves = networkConstants.smoothCurves;
-
-
this.body = body;
// initialize variables
@@ -28040,9 +28035,7 @@ return /******/ (function(modules) { // webpackBootstrap
}
}
-
-
- // A node is connected when it has a from and to node.
+ // A node is connected when it has a from and to node that both exist in the network.body.nodes.
this.connect();
this.widthFixed = this.widthFixed || properties.width !== undefined;
@@ -34363,16 +34356,12 @@ return /******/ (function(modules) { // webpackBootstrap
* Created by Alex on 2/23/2015.
*/
- var BarnesHutSolver = __webpack_require__(73).BarnesHutSolver;
- // TODO Create
- //import {Repulsion} from "./components/physics/Repulsion";
- //import {HierarchicalRepulsion} from "./components/physics/HierarchicalRepulsion";
-
- var SpringSolver = __webpack_require__(74).SpringSolver;
- // TODO Create
- //import {HierarchicalSpringSolver} from "./components/physics/HierarchicalSpringSolver";
-
- var CentralGravitySolver = __webpack_require__(75).CentralGravitySolver;
+ var BarnesHutSolver = __webpack_require__(74).BarnesHutSolver;
+ var Repulsion = __webpack_require__(75).Repulsion;
+ var HierarchicalRepulsion = __webpack_require__(76).HierarchicalRepulsion;
+ var SpringSolver = __webpack_require__(77).SpringSolver;
+ var HierarchicalSpringSolver = __webpack_require__(78).HierarchicalSpringSolver;
+ var CentralGravitySolver = __webpack_require__(79).CentralGravitySolver;
var PhysicsEngine = (function () {
function PhysicsEngine(body, options) {
_classCallCheck(this, PhysicsEngine);
@@ -34398,14 +34387,12 @@ return /******/ (function(modules) { // webpackBootstrap
var options;
if (this.options.model == "repulsion") {
options = this.options.repulsion;
- // TODO uncomment when created
- //this.nodesSolver = new Repulsion(this.body, this.physicsBody, options);
- //this.edgesSolver = new SpringSolver(this.body, options);
+ this.nodesSolver = new Repulsion(this.body, this.physicsBody, options);
+ this.edgesSolver = new SpringSolver(this.body, options);
} else if (this.options.model == "hierarchicalRepulsion") {
options = this.options.hierarchicalRepulsion;
- // TODO uncomment when created
- //this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options);
- //this.edgesSolver = new HierarchicalSpringSolver(this.body, options);
+ this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options);
+ this.edgesSolver = new HierarchicalSpringSolver(this.body, this.physicsBody, options);
} else {
// barnesHut
options = this.options.barnesHut;
@@ -34447,38 +34434,16 @@ return /******/ (function(modules) { // webpackBootstrap
console.error("Support node detected that does not have an edge!");
}
}
- console.log("here", this.body);
this.physicsBody.calculationNodeIndices = Object.keys(this.physicsBody.calculationNodes);
},
writable: true,
configurable: true
},
- calculateField: {
- value: function calculateField() {
- this.nodesSolver.solve();
- },
- writable: true,
- configurable: true
- },
- calculateSprings: {
- value: function calculateSprings() {
- this.edgesSolver.solve();
- },
- writable: true,
- configurable: true
- },
- calculateCentralGravity: {
- value: function calculateCentralGravity() {
- this.gravitySolver.solve();
- },
- writable: true,
- configurable: true
- },
step: {
value: function step() {
- this.calculateCentralGravity();
- this.calculateField();
- this.calculateSprings();
+ this.gravitySolver.solve();
+ this.nodesSolver.solve();
+ this.edgesSolver.solve();
},
writable: true,
configurable: true
@@ -34504,364 +34469,1078 @@ return /******/ (function(modules) { // webpackBootstrap
var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } };
/**
- * Created by Alex on 2/23/2015.
+ * Created by Alex on 24-Feb-15.
*/
- var BarnesHutSolver = (function () {
- function BarnesHutSolver(body, physicsBody, options) {
- _classCallCheck(this, BarnesHutSolver);
+ var util = __webpack_require__(1);
+
+ var ClusterEngine = (function () {
+ function ClusterEngine(body) {
+ _classCallCheck(this, ClusterEngine);
this.body = body;
- this.physicsBody = physicsBody;
- this.options = options;
- this.barnesHutTree;
+ this.clusteredNodes = {};
}
- _prototypeProperties(BarnesHutSolver, null, {
- solve: {
+ _prototypeProperties(ClusterEngine, null, {
+ clusterByConnectionCount: {
/**
- * 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
- */
- value: function solve() {
- if (this.options.gravitationalConstant != 0) {
- var node;
- var nodes = this.physicsBody.calculationNodes;
- var nodeIndices = this.physicsBody.calculationNodeIndices;
- var nodeCount = nodeIndices.length;
-
- // create the tree
- var barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices);
-
- // for debugging
- this.barnesHutTree = barnesHutTree;
+ *
+ * @param hubsize
+ * @param options
+ */
+ value: function clusterByConnectionCount(hubsize, options) {
+ if (hubsize === undefined) {
+ hubsize = this._getHubSize();
+ } else if (tyepof(hubsize) == "object") {
+ options = this._checkOptions(hubsize);
+ hubsize = this._getHubSize();
+ }
- // 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);
- }
+ var nodesToCluster = [];
+ for (var i = 0; i < this.body.nodeIndices.length; i++) {
+ var node = this.body.nodes[this.body.nodeIndices[i]];
+ if (node.edges.length >= hubsize) {
+ nodesToCluster.push(node.id);
}
}
+
+ for (var i = 0; i < nodesToCluster.length; i++) {
+ var node = this.body.nodes[nodesToCluster[i]];
+ this.clusterByConnection(node, options, {}, {}, true);
+ }
+ this.body.emitter.emit("_dataChanged");
},
writable: true,
configurable: true
},
- _getForceContribution: {
+ clusterByNodeData: {
/**
- * 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
- */
- value: function _getForceContribution(parentBranch, node) {
- // we get no force contribution from an empty region
- if (parentBranch.childrenCount > 0) {
- var dx, dy, distance;
+ * loop over all nodes, check if they adhere to the condition and cluster if needed.
+ * @param options
+ * @param doNotUpdateCalculationNodes
+ */
+ value: function clusterByNodeData() {
+ var options = arguments[0] === undefined ? {} : arguments[0];
+ var doNotUpdateCalculationNodes = arguments[1] === undefined ? false : arguments[1];
+ if (options.joinCondition === undefined) {
+ throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options.");
+ }
- // 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);
+ // check if the options object is fine, append if needed
+ options = this._checkOptions(options);
- // 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;
- }
- }
+ var childNodesObj = {};
+ var childEdgesObj = {};
+
+ // collect the nodes that will be in the cluster
+ for (var i = 0; i < this.body.nodeIndices.length; i++) {
+ var nodeId = this.body.nodeIndices[i];
+ var clonedOptions = this._cloneOptions(nodeId);
+ if (options.joinCondition(clonedOptions) == true) {
+ childNodesObj[nodeId] = this.body.nodes[nodeId];
}
}
+
+ this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes);
},
writable: true,
configurable: true
},
- _formBarnesHutTree: {
+ clusterOutliers: {
/**
- * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
- *
- * @param nodes
- * @param nodeIndices
- * @private
- */
- value: function _formBarnesHutTree(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;
+ * Cluster all nodes in the network that have only 1 edge
+ * @param options
+ * @param doNotUpdateCalculationNodes
+ */
+ value: function clusterOutliers(options, doNotUpdateCalculationNodes) {
+ options = this._checkOptions(options);
+ var clusters = [];
- // 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;
+ // collect the nodes that will be in the cluster
+ for (var i = 0; i < this.body.nodeIndices.length; i++) {
+ var childNodesObj = {};
+ var childEdgesObj = {};
+ var nodeId = this.body.nodeIndices[i];
+ if (this.body.nodes[nodeId].edges.length == 1) {
+ var edge = this.body.nodes[nodeId].edges[0];
+ var childNodeId = this._getConnectedId(edge, nodeId);
+ if (childNodeId != nodeId) {
+ if (options.joinCondition === undefined) {
+ childNodesObj[nodeId] = this.body.nodes[nodeId];
+ childNodesObj[childNodeId] = this.body.nodes[childNodeId];
+ } else {
+ var clonedOptions = this._cloneOptions(nodeId);
+ if (options.joinCondition(clonedOptions) == true) {
+ childNodesObj[nodeId] = this.body.nodes[nodeId];
+ }
+ clonedOptions = this._cloneOptions(childNodeId);
+ if (options.joinCondition(clonedOptions) == true) {
+ childNodesObj[childNodeId] = this.body.nodes[childNodeId];
+ }
+ }
+ clusters.push({ nodes: childNodesObj, edges: childEdgesObj });
}
}
}
- // 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 = 0.00001;
- 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);
- }
+ for (var i = 0; i < clusters.length; i++) {
+ this._cluster(clusters[i].nodes, clusters[i].edges, options, true);
}
- // make global
- return barnesHutTree;
+ if (doNotUpdateCalculationNodes !== true) {
+ this.body.emitter.emit("_dataChanged");
+ }
},
writable: true,
configurable: true
},
- _updateBranchMass: {
-
+ clusterByConnection: {
/**
- * this updates the mass of a branch. this is increased by adding a node.
- *
- * @param parentBranch
- * @param node
- * @private
- */
- value: function _updateBranchMass(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;
+ *
+ * @param nodeId
+ * @param options
+ * @param doNotUpdateCalculationNodes
+ */
+ value: function clusterByConnection(nodeId, options, doNotUpdateCalculationNodes) {
+ // kill conditions
+ if (nodeId === undefined) {
+ throw new Error("No nodeId supplied to clusterByConnection!");
+ }
+ if (this.body.nodes[nodeId] === undefined) {
+ throw new Error("The nodeId given to clusterByConnection does not exist!");
+ }
- parentBranch.mass = totalMass;
- var biggestSize = Math.max(Math.max(node.height, node.radius), node.width);
- parentBranch.maxWidth = parentBranch.maxWidth < biggestSize ? biggestSize : parentBranch.maxWidth;
- },
- writable: true,
- configurable: true
- },
- _placeInTree: {
+ var node = this.body.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;
- /**
- * determine in which branch the node will be placed.
- *
- * @param parentBranch
- * @param node
- * @param skipMassUpdate
- * @private
- */
- value: function _placeInTree(parentBranch, node, skipMassUpdate) {
- if (skipMassUpdate != true || skipMassUpdate === undefined) {
- // update the mass of the branch.
- this._updateBranchMass(parentBranch, 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 (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");
+ if (childNodeId !== parentNodeId) {
+ if (options.joinCondition === undefined) {
+ childEdgesObj[edge.id] = edge;
+ childNodesObj[childNodeId] = this.body.nodes[childNodeId];
+ } else {
+ // clone the options and insert some additional parameters that could be interesting.
+ var childClonedOptions = this._cloneOptions(childNodeId);
+ if (options.joinCondition(parentClonedOptions, childClonedOptions) == true) {
+ childEdgesObj[edge.id] = edge;
+ childNodesObj[childNodeId] = this.body.nodes[childNodeId];
+ }
+ }
} else {
- // in SE
- this._placeInRegion(parentBranch, node, "SE");
+ childEdgesObj[edge.id] = edge;
}
}
+
+ this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes);
},
writable: true,
configurable: true
},
- _placeInRegion: {
+ _cloneOptions: {
/**
- * actually place the node in a region (or branch)
- *
- * @param parentBranch
- * @param node
- * @param region
- * @private
- */
- value: function _placeInRegion(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 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
+ */
+ value: function _cloneOptions(objId, type) {
+ var clonedOptions = {};
+ if (type === undefined || type == "node") {
+ util.deepExtend(clonedOptions, this.body.nodes[objId].options, true);
+ util.deepExtend(clonedOptions, this.body.nodes[objId].properties, true);
+ clonedOptions.amountOfConnections = this.body.nodes[objId].edges.length;
+ } else {
+ util.deepExtend(clonedOptions, this.body.edges[objId].properties, true);
}
+ return clonedOptions;
},
writable: true,
configurable: true
},
- _splitBranch: {
+ _createClusterEdges: {
/**
- * 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
- */
- value: function _splitBranch(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");
+ * This function creates the edges that will be attached to the cluster.
+ *
+ * @param childNodesObj
+ * @param childEdgesObj
+ * @param newEdges
+ * @param options
+ * @private
+ */
+ value: function _createClusterEdges(childNodesObj, childEdgesObj, newEdges, options) {
+ var edge, childNodeId, childNode;
- if (containedNode != null) {
- this._placeInTree(parentBranch, containedNode);
+ 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);
+ 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(this.body.functions.createEdge(clonedOptions));
+ }
+ }
}
},
writable: true,
configurable: true
},
- _insertRegion: {
+ _checkOptions: {
/**
- * 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
- */
+ * 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
+ */
+ value: function _checkOptions() {
+ var options = arguments[0] === undefined ? {} : arguments[0];
+ if (options.clusterEdgeProperties === undefined) {
+ options.clusterEdgeProperties = {};
+ }
+ if (options.clusterNodeProperties === undefined) {
+ options.clusterNodeProperties = {};
+ }
+
+ return options;
+ },
+ writable: true,
+ configurable: true
+ },
+ _cluster: {
+
+ /**
+ *
+ * @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
+ */
+ value: function _cluster(childNodesObj, childEdgesObj, options) {
+ var doNotUpdateCalculationNodes = arguments[3] === undefined ? false : arguments[3];
+ // 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 = this.body.functions.createNode(clusterNodeProperties);
+ 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.body.edges[edgeId] !== undefined) {
+ if (this.body.edges[edgeId].via !== null) {
+ var viaId = this.body.edges[edgeId].via.id;
+ if (viaId) {
+ this.body.edges[edgeId].via = null;
+ delete this.body.supportNodes[viaId];
+ }
+ }
+ this.body.edges[edgeId].disconnect();
+ delete this.body.edges[edgeId];
+ }
+ }
+ }
+
+
+ // remove contained nodes from global
+ for (var nodeId in childNodesObj) {
+ if (childNodesObj.hasOwnProperty(nodeId)) {
+ this.clusteredNodes[nodeId] = { clusterId: clusterNodeProperties.id, node: this.body.nodes[nodeId] };
+ delete this.body.nodes[nodeId];
+ }
+ }
+
+
+ // finally put the cluster node into global
+ this.body.nodes[clusterNodeProperties.id] = clusterNode;
+
+
+ // push new edges to global
+ for (var i = 0; i < newEdges.length; i++) {
+ this.body.edges[newEdges[i].id] = newEdges[i];
+ this.body.edges[newEdges[i].id].connect();
+ }
+
+
+ // create bezier nodes for smooth curves if needed
+ this.body.emitter.emit("_newEdgesCreated");
+
+
+ // set ID to undefined so no duplicates arise
+ clusterNodeProperties.id = undefined;
+
+
+ // wrap up
+ if (doNotUpdateCalculationNodes !== true) {
+ this.body.emitter.emit("_dataChanged");
+ }
+ },
+ writable: true,
+ configurable: true
+ },
+ isCluster: {
+
+
+ /**
+ * Check if a node is a cluster.
+ * @param nodeId
+ * @returns {*}
+ */
+ value: function isCluster(nodeId) {
+ if (this.body.nodes[nodeId] !== undefined) {
+ return this.body.nodes[nodeId].isCluster;
+ } else {
+ console.log("Node does not exist.");
+ return false;
+ }
+ },
+ writable: true,
+ configurable: true
+ },
+ _getClusterPosition: {
+
+ /**
+ * 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
+ */
+ value: function _getClusterPosition(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) };
+ },
+ writable: true,
+ configurable: true
+ },
+ openCluster: {
+
+
+ /**
+ * 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
+ */
+ value: function openCluster(clusterNodeId, doNotUpdateCalculationNodes) {
+ // kill conditions
+ if (clusterNodeId === undefined) {
+ throw new Error("No clusterNodeId supplied to openCluster.");
+ }
+ if (this.body.nodes[clusterNodeId] === undefined) {
+ throw new Error("The clusterNodeId supplied to openCluster does not exist.");
+ }
+ if (this.body.nodes[clusterNodeId].containedNodes === undefined) {
+ console.log("The node:" + clusterNodeId + " is not a cluster.");return;
+ };
+
+ var node = this.body.nodes[clusterNodeId];
+ var containedNodes = node.containedNodes;
+ var containedEdges = node.containedEdges;
+
+ // release nodes
+ for (var nodeId in containedNodes) {
+ if (containedNodes.hasOwnProperty(nodeId)) {
+ this.body.nodes[nodeId] = containedNodes[nodeId];
+ // inherit position
+ this.body.nodes[nodeId].x = node.x;
+ this.body.nodes[nodeId].y = node.y;
+
+ // inherit speed
+ this.body.nodes[nodeId].vx = node.vx;
+ this.body.nodes[nodeId].vy = node.vy;
+
+ delete this.clusteredNodes[nodeId];
+ }
+ }
+
+ // release edges
+ for (var edgeId in containedEdges) {
+ if (containedEdges.hasOwnProperty(edgeId)) {
+ this.body.edges[edgeId] = containedEdges[edgeId];
+ this.body.edges[edgeId].connect();
+ var edge = this.body.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.body.emitter.emit("_newEdgesCreated", 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.body.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.body.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.body.nodes[edge.toArray[0].id] !== undefined) {
+ this._connectEdge(edge, edge.toArray[0].id, false);
+ }
+ } else {
+ var edgeId = edgeIds[i];
+ var viaId = this.body.edges[edgeId].via.id;
+ if (viaId) {
+ this.body.edges[edgeId].via = null;
+ delete this.body.supportNodes[viaId];
+ }
+ // this removes the edge from node.edges, which is why edgeIds is formed
+ this.body.edges[edgeId].disconnect();
+ delete this.body.edges[edgeId];
+ }
+ }
+
+ // remove clusterNode
+ delete this.body.nodes[clusterNodeId];
+
+ if (doNotUpdateCalculationNodes !== true) {
+ this.body.emitter.emit("_dataChanged");
+ }
+ },
+ writable: true,
+ configurable: true
+ },
+ _connectEdge: {
+
+
+
+ /**
+ * 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
+ */
+ value: function _connectEdge(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();
+ },
+ writable: true,
+ configurable: true
+ },
+ _getClusterStack: {
+
+ /**
+ * Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node
+ * @param nodeId
+ * @returns {Array}
+ * @private
+ */
+ value: function _getClusterStack(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.body.nodes[nodeId]);
+ return stack;
+ },
+ writable: true,
+ configurable: true
+ },
+ _getConnectedId: {
+
+
+ /**
+ * Get the Id the node is connected to
+ * @param edge
+ * @param nodeId
+ * @returns {*}
+ * @private
+ */
+ value: function _getConnectedId(edge, nodeId) {
+ if (edge.toId != nodeId) {
+ return edge.toId;
+ } else if (edge.fromId != nodeId) {
+ return edge.fromId;
+ } else {
+ return edge.fromId;
+ }
+ },
+ writable: true,
+ configurable: true
+ },
+ _getHubSize: {
+
+ /**
+ * 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
+ */
+ value: function _getHubSize() {
+ var average = 0;
+ var averageSquared = 0;
+ var hubCounter = 0;
+ var largestHub = 0;
+
+ for (var i = 0; i < this.body.nodeIndices.length; i++) {
+ var node = this.body.nodes[this.body.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;
+ },
+ writable: true,
+ configurable: true
+ }
+ });
+
+ return ClusterEngine;
+ })();
+
+ exports.ClusterEngine = ClusterEngine;
+ Object.defineProperty(exports, "__esModule", {
+ value: true
+ });
+
+/***/ },
+/* 74 */
+/***/ function(module, exports, __webpack_require__) {
+
+ "use strict";
+
+ var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); };
+
+ var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } };
+
+ /**
+ * Created by Alex on 2/23/2015.
+ */
+
+ var BarnesHutSolver = (function () {
+ function BarnesHutSolver(body, physicsBody, options) {
+ _classCallCheck(this, BarnesHutSolver);
+
+ this.body = body;
+ this.physicsBody = physicsBody;
+ this.options = options;
+ this.barnesHutTree;
+ }
+
+ _prototypeProperties(BarnesHutSolver, null, {
+ solve: {
+
+
+ /**
+ * 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
+ */
+ value: function solve() {
+ if (this.options.gravitationalConstant != 0) {
+ var node;
+ var nodes = this.physicsBody.calculationNodes;
+ var nodeIndices = this.physicsBody.calculationNodeIndices;
+ var nodeCount = nodeIndices.length;
+
+ // create the tree
+ var barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices);
+
+ // for debugging
+ this.barnesHutTree = barnesHutTree;
+
+ // 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);
+ }
+ }
+ }
+ },
+ writable: true,
+ configurable: true
+ },
+ _getForceContribution: {
+
+
+ /**
+ * 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
+ */
+ value: function _getForceContribution(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;
+ }
+ }
+ }
+ }
+ },
+ writable: true,
+ configurable: true
+ },
+ _formBarnesHutTree: {
+
+
+ /**
+ * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
+ *
+ * @param nodes
+ * @param nodeIndices
+ * @private
+ */
+ value: function _formBarnesHutTree(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 = 0.00001;
+ 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;
+ },
+ writable: true,
+ configurable: true
+ },
+ _updateBranchMass: {
+
+
+ /**
+ * this updates the mass of a branch. this is increased by adding a node.
+ *
+ * @param parentBranch
+ * @param node
+ * @private
+ */
+ value: function _updateBranchMass(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;
+ },
+ writable: true,
+ configurable: true
+ },
+ _placeInTree: {
+
+
+ /**
+ * determine in which branch the node will be placed.
+ *
+ * @param parentBranch
+ * @param node
+ * @param skipMassUpdate
+ * @private
+ */
+ value: function _placeInTree(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");
+ }
+ }
+ },
+ writable: true,
+ configurable: true
+ },
+ _placeInRegion: {
+
+
+ /**
+ * actually place the node in a region (or branch)
+ *
+ * @param parentBranch
+ * @param node
+ * @param region
+ * @private
+ */
+ value: function _placeInRegion(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;
+ }
+ },
+ writable: true,
+ configurable: true
+ },
+ _splitBranch: {
+
+
+ /**
+ * 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
+ */
+ value: function _splitBranch(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);
+ }
+ },
+ writable: true,
+ configurable: true
+ },
+ _insertRegion: {
+
+
+ /**
+ * 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
+ */
value: function _insertRegion(parentBranch, region) {
var minX, maxX, minY, maxY;
var childSize = 0.5 * parentBranch.size;
@@ -34996,118 +35675,6 @@ return /******/ (function(modules) { // webpackBootstrap
value: true
});
-/***/ },
-/* 74 */
-/***/ function(module, exports, __webpack_require__) {
-
- "use strict";
-
- var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); };
-
- var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } };
-
- /**
- * Created by Alex on 2/23/2015.
- */
-
- var SpringSolver = (function () {
- function SpringSolver(body, options) {
- _classCallCheck(this, SpringSolver);
-
- this.body = body;
- this.options = options;
- }
-
- _prototypeProperties(SpringSolver, null, {
- solve: {
- value: function solve() {
- this._calculateSpringForces();
- },
- writable: true,
- configurable: true
- },
- _calculateSpringForces: {
-
-
-
- /**
- * This function calculates the springforces on the nodes, accounting for the support nodes.
- *
- * @private
- */
- value: function _calculateSpringForces() {
- var edgeLength, edge, edgeId;
- var edges = this.body.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.body.nodes[edge.toId] !== undefined && this.body.nodes[edge.fromId] !== undefined) {
- edgeLength = edge.physics.springLength;
- if (edge.via != null) {
- var node1 = edge.to;
- var node2 = edge.via;
- var node3 = edge.from;
-
- this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
- this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
- } else {
- this._calculateSpringForce(edge.from, edge.to, edgeLength);
- }
- }
- }
- }
- }
- },
- writable: true,
- configurable: true
- },
- _calculateSpringForce: {
-
-
- /**
- * This is the code actually performing the calculation for the function above.
- *
- * @param node1
- * @param node2
- * @param edgeLength
- * @private
- */
- value: function _calculateSpringForce(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;
- },
- writable: true,
- configurable: true
- }
- });
-
- return SpringSolver;
- })();
-
- exports.SpringSolver = SpringSolver;
- Object.defineProperty(exports, "__esModule", {
- value: true
- });
-
/***/ },
/* 75 */
/***/ function(module, exports, __webpack_require__) {
@@ -35122,766 +35689,465 @@ return /******/ (function(modules) { // webpackBootstrap
* Created by Alex on 2/23/2015.
*/
- var CentralGravitySolver = (function () {
- function CentralGravitySolver(body, physicsBody, options) {
- _classCallCheck(this, CentralGravitySolver);
+ var RepulsionSolver = (function () {
+ function RepulsionSolver(body, physicsBody, options) {
+ _classCallCheck(this, RepulsionSolver);
this.body = body;
this.physicsBody = physicsBody;
- this.setOptions(options);
- }
-
- _prototypeProperties(CentralGravitySolver, null, {
- setOptions: {
- value: function setOptions(options) {
- this.options = options;
- },
- writable: true,
- configurable: true
- },
- solve: {
- value: function solve() {
- var dx, dy, distance, node, i;
- var nodes = this.physicsBody.calculationNodes;
- var calculationNodeIndices = this.physicsBody.calculationNodeIndices;
- var gravity = this.options.centralGravity;
- var gravityForce = 0;
-
- for (i = 0; i < calculationNodeIndices.length; i++) {
- node = nodes[calculationNodeIndices[i]];
- node.damping = this.options.damping;
- 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;
- }
- },
- writable: true,
- configurable: true
- }
- });
-
- return CentralGravitySolver;
- })();
-
- exports.CentralGravitySolver = CentralGravitySolver;
- Object.defineProperty(exports, "__esModule", {
- value: true
- });
-
-/***/ },
-/* 76 */
-/***/ function(module, exports, __webpack_require__) {
-
- "use strict";
-
- var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); };
-
- var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } };
-
- /**
- * Created by Alex on 24-Feb-15.
- */
-
- var util = __webpack_require__(1);
-
- var ClusterEngine = (function () {
- function ClusterEngine(body) {
- _classCallCheck(this, ClusterEngine);
-
- this.body = body;
- this.clusteredNodes = {};
- }
-
- _prototypeProperties(ClusterEngine, null, {
- clusterByConnectionCount: {
-
-
- /**
- *
- * @param hubsize
- * @param options
- */
- value: function clusterByConnectionCount(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.body.nodeIndices.length; i++) {
- var node = this.body.nodes[this.body.nodeIndices[i]];
- if (node.edges.length >= hubsize) {
- nodesToCluster.push(node.id);
- }
- }
-
- for (var i = 0; i < nodesToCluster.length; i++) {
- var node = this.body.nodes[nodesToCluster[i]];
- this.clusterByConnection(node, options, {}, {}, true);
- }
- this.body.emitter.emit("_dataChanged");
- },
- writable: true,
- configurable: true
- },
- clusterByNodeData: {
-
-
- /**
- * loop over all nodes, check if they adhere to the condition and cluster if needed.
- * @param options
- * @param doNotUpdateCalculationNodes
- */
- value: function clusterByNodeData() {
- var options = arguments[0] === undefined ? {} : arguments[0];
- var doNotUpdateCalculationNodes = arguments[1] === undefined ? false : arguments[1];
- 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.body.nodeIndices.length; i++) {
- var nodeId = this.body.nodeIndices[i];
- var clonedOptions = this._cloneOptions(nodeId);
- if (options.joinCondition(clonedOptions) == true) {
- childNodesObj[nodeId] = this.body.nodes[nodeId];
- }
- }
-
- this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes);
- },
- writable: true,
- configurable: true
- },
- clusterOutliers: {
-
-
- /**
- * Cluster all nodes in the network that have only 1 edge
- * @param options
- * @param doNotUpdateCalculationNodes
- */
- value: function clusterOutliers(options, doNotUpdateCalculationNodes) {
- options = this._checkOptions(options);
- var clusters = [];
-
- // collect the nodes that will be in the cluster
- for (var i = 0; i < this.body.nodeIndices.length; i++) {
- var childNodesObj = {};
- var childEdgesObj = {};
- var nodeId = this.body.nodeIndices[i];
- if (this.body.nodes[nodeId].edges.length == 1) {
- var edge = this.body.nodes[nodeId].edges[0];
- var childNodeId = this._getConnectedId(edge, nodeId);
- if (childNodeId != nodeId) {
- if (options.joinCondition === undefined) {
- childNodesObj[nodeId] = this.body.nodes[nodeId];
- childNodesObj[childNodeId] = this.body.nodes[childNodeId];
- } else {
- var clonedOptions = this._cloneOptions(nodeId);
- if (options.joinCondition(clonedOptions) == true) {
- childNodesObj[nodeId] = this.body.nodes[nodeId];
- }
- clonedOptions = this._cloneOptions(childNodeId);
- if (options.joinCondition(clonedOptions) == true) {
- childNodesObj[childNodeId] = this.body.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.body.emitter.emit("_dataChanged");
- }
- },
- writable: true,
- configurable: true
- },
- clusterByConnection: {
-
- /**
- *
- * @param nodeId
- * @param options
- * @param doNotUpdateCalculationNodes
- */
- value: function clusterByConnection(nodeId, options, doNotUpdateCalculationNodes) {
- // kill conditions
- if (nodeId === undefined) {
- throw new Error("No nodeId supplied to clusterByConnection!");
- }
- if (this.body.nodes[nodeId] === undefined) {
- throw new Error("The nodeId given to clusterByConnection does not exist!");
- }
-
- var node = this.body.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.body.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.body.nodes[childNodeId];
- }
- }
- } else {
- childEdgesObj[edge.id] = edge;
- }
- }
-
- this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes);
- },
- writable: true,
- configurable: true
- },
- _cloneOptions: {
-
-
- /**
- * 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
- */
- value: function _cloneOptions(objId, type) {
- var clonedOptions = {};
- if (type === undefined || type == "node") {
- util.deepExtend(clonedOptions, this.body.nodes[objId].options, true);
- util.deepExtend(clonedOptions, this.body.nodes[objId].properties, true);
- clonedOptions.amountOfConnections = this.body.nodes[objId].edges.length;
- } else {
- util.deepExtend(clonedOptions, this.body.edges[objId].properties, true);
- }
- return clonedOptions;
- },
- writable: true,
- configurable: true
- },
- _createClusterEdges: {
-
-
- /**
- * This function creates the edges that will be attached to the cluster.
- *
- * @param childNodesObj
- * @param childEdgesObj
- * @param newEdges
- * @param options
- * @private
- */
- value: function _createClusterEdges(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);
- 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(this.body.functions.createEdge(clonedOptions));
- }
- }
- }
- },
- writable: true,
- configurable: true
- },
- _checkOptions: {
-
-
- /**
- * 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
- */
- value: function _checkOptions() {
- var options = arguments[0] === undefined ? {} : arguments[0];
- if (options.clusterEdgeProperties === undefined) {
- options.clusterEdgeProperties = {};
- }
- if (options.clusterNodeProperties === undefined) {
- options.clusterNodeProperties = {};
- }
-
- return options;
- },
- writable: true,
- configurable: true
- },
- _cluster: {
-
- /**
- *
- * @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
- */
- value: function _cluster(childNodesObj, childEdgesObj, options) {
- var doNotUpdateCalculationNodes = arguments[3] === undefined ? false : arguments[3];
- // 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);
- }
+ this.options = options;
+ }
- 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";
- }
+ _prototypeProperties(RepulsionSolver, null, {
+ solve: {
+ /**
+ * Calculate the forces the nodes apply on each other based on a repulsion field.
+ * This field is linearly approximated.
+ *
+ * @private
+ */
+ value: function solve() {
+ var dx, dy, distance, fx, fy, repulsingForce, node1, node2;
- // 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;
- }
+ var nodes = this.physicsBody.calculationNodes;
+ var nodeIndices = this.physicsBody.calculationNodeIndices;
+ // repulsing forces between nodes
+ var nodeDistance = this.options.nodeDistance;
- // force the ID to remain the same
- clusterNodeProperties.id = clusterId;
+ // approximation constants
+ var a = -2 / 3 / nodeDistance;
+ var b = 4 / 3;
+ // we loop from i over all but the last entree in the array
+ // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
+ for (var i = 0; i < nodeIndices.length - 1; i++) {
+ node1 = nodes[nodeIndices[i]];
+ for (var j = i + 1; j < nodeIndices.length; j++) {
+ node2 = nodes[nodeIndices[j]];
- // create the clusterNode
- var clusterNode = this.body.functions.createNode(clusterNodeProperties);
- clusterNode.isCluster = true;
- clusterNode.containedNodes = childNodesObj;
- clusterNode.containedEdges = childEdgesObj;
+ dx = node2.x - node1.x;
+ dy = node2.y - node1.y;
+ distance = Math.sqrt(dx * dx + dy * dy);
+ // same condition as BarnesHutSolver, making sure nodes are never 100% overlapping.
+ if (distance == 0) {
+ distance = 0.1 * Math.random();
+ dx = distance;
+ }
- // delete contained edges from global
- for (var edgeId in childEdgesObj) {
- if (childEdgesObj.hasOwnProperty(edgeId)) {
- if (this.body.edges[edgeId] !== undefined) {
- if (this.body.edges[edgeId].via !== null) {
- var viaId = this.body.edges[edgeId].via.id;
- if (viaId) {
- this.body.edges[edgeId].via = null;
- delete this.body.supportNodes[viaId];
- }
+ if (distance < 2 * nodeDistance) {
+ if (distance < 0.5 * nodeDistance) {
+ repulsingForce = 1;
+ } else {
+ repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / nodeDistance - 1) * steepness))
}
- this.body.edges[edgeId].disconnect();
- delete this.body.edges[edgeId];
+ repulsingForce = repulsingForce / distance;
+
+ fx = dx * repulsingForce;
+ fy = dy * repulsingForce;
+ node1.fx -= fx;
+ node1.fy -= fy;
+ node2.fx += fx;
+ node2.fy += fy;
}
}
}
+ },
+ writable: true,
+ configurable: true
+ }
+ });
+ return RepulsionSolver;
+ })();
- // remove contained nodes from global
- for (var nodeId in childNodesObj) {
- if (childNodesObj.hasOwnProperty(nodeId)) {
- this.clusteredNodes[nodeId] = { clusterId: clusterNodeProperties.id, node: this.body.nodes[nodeId] };
- delete this.body.nodes[nodeId];
- }
- }
+ exports.RepulsionSolver = RepulsionSolver;
+ Object.defineProperty(exports, "__esModule", {
+ value: true
+ });
+/***/ },
+/* 76 */
+/***/ function(module, exports, __webpack_require__) {
- // finally put the cluster node into global
- this.body.nodes[clusterNodeProperties.id] = clusterNode;
+ "use strict";
+ var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); };
- // push new edges to global
- for (var i = 0; i < newEdges.length; i++) {
- this.body.edges[newEdges[i].id] = newEdges[i];
- this.body.edges[newEdges[i].id].connect();
- }
+ var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } };
+ /**
+ * Created by Alex on 2/23/2015.
+ */
- // create bezier nodes for smooth curves if needed
- this.body.emitter.emit("_newEdgesCreated");
+ var HierarchicalRepulsionSolver = (function () {
+ function HierarchicalRepulsionSolver(body, physicsBody, options) {
+ _classCallCheck(this, HierarchicalRepulsionSolver);
+ this.body = body;
+ this.physicsBody = physicsBody;
+ this.options = options;
+ }
- // set ID to undefined so no duplicates arise
- clusterNodeProperties.id = undefined;
+ _prototypeProperties(HierarchicalRepulsionSolver, null, {
+ solve: {
+
+ /**
+ * Calculate the forces the nodes apply on each other based on a repulsion field.
+ * This field is linearly approximated.
+ *
+ * @private
+ */
+ value: function solve() {
+ var dx, dy, distance, fx, fy, repulsingForce, node1, node2, i, j;
+ var nodes = this.physicsBody.calculationNodes;
+ var nodeIndices = this.physicsBody.calculationNodeIndices;
- // wrap up
- if (doNotUpdateCalculationNodes !== true) {
- this.body.emitter.emit("_dataChanged");
+ // repulsing forces between nodes
+ var nodeDistance = this.options.nodeDistance;
+
+ // we loop from i over all but the last entree in the array
+ // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
+ for (i = 0; i < nodeIndices.length - 1; i++) {
+ node1 = nodes[nodeIndices[i]];
+ for (j = i + 1; j < nodeIndices.length; j++) {
+ node2 = nodes[nodeIndices[j]];
+
+ // nodes only affect nodes on their level
+ if (node1.level == node2.level) {
+ dx = node2.x - node1.x;
+ dy = node2.y - node1.y;
+ distance = Math.sqrt(dx * dx + dy * dy);
+
+
+ var steepness = 0.05;
+ if (distance < nodeDistance) {
+ repulsingForce = -Math.pow(steepness * distance, 2) + Math.pow(steepness * nodeDistance, 2);
+ } else {
+ repulsingForce = 0;
+ }
+ // normalize force with
+ if (distance == 0) {
+ distance = 0.01;
+ } else {
+ repulsingForce = repulsingForce / distance;
+ }
+ fx = dx * repulsingForce;
+ fy = dy * repulsingForce;
+
+ node1.fx -= fx;
+ node1.fy -= fy;
+ node2.fx += fx;
+ node2.fy += fy;
+ }
+ }
}
},
writable: true,
configurable: true
- },
- isCluster: {
+ }
+ });
+
+ return HierarchicalRepulsionSolver;
+ })();
+
+ exports.HierarchicalRepulsionSolver = HierarchicalRepulsionSolver;
+ Object.defineProperty(exports, "__esModule", {
+ value: true
+ });
+
+/***/ },
+/* 77 */
+/***/ function(module, exports, __webpack_require__) {
+
+ "use strict";
+
+ var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); };
+
+ var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } };
+
+ /**
+ * Created by Alex on 2/23/2015.
+ */
+
+ var SpringSolver = (function () {
+ function SpringSolver(body, options) {
+ _classCallCheck(this, SpringSolver);
+
+ this.body = body;
+ this.options = options;
+ }
+ _prototypeProperties(SpringSolver, null, {
+ solve: {
/**
- * Check if a node is a cluster.
- * @param nodeId
- * @returns {*}
- */
- value: function isCluster(nodeId) {
- if (this.body.nodes[nodeId] !== undefined) {
- return this.body.nodes[nodeId].isCluster;
- } else {
- console.log("Node does not exist.");
- return false;
+ * This function calculates the springforces on the nodes, accounting for the support nodes.
+ *
+ * @private
+ */
+ value: function solve() {
+ var edgeLength, edge, edgeId;
+ var edges = this.body.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.body.nodes[edge.toId] !== undefined && this.body.nodes[edge.fromId] !== undefined) {
+ edgeLength = edge.properties.length === undefined ? this.options.springLength : edge.properties.length;
+ if (edge.via != null) {
+ var node1 = edge.to;
+ var node2 = edge.via;
+ var node3 = edge.from;
+
+ this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
+ this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
+ } else {
+ this._calculateSpringForce(edge.from, edge.to, edgeLength);
+ }
+ }
+ }
+ }
}
},
writable: true,
configurable: true
},
- _getClusterPosition: {
+ _calculateSpringForce: {
+
/**
- * 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
- */
- value: function _getClusterPosition(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) };
+ * This is the code actually performing the calculation for the function above.
+ *
+ * @param node1
+ * @param node2
+ * @param edgeLength
+ * @private
+ */
+ value: function _calculateSpringForce(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;
},
writable: true,
configurable: true
- },
- openCluster: {
+ }
+ });
+
+ return SpringSolver;
+ })();
+
+ exports.SpringSolver = SpringSolver;
+ Object.defineProperty(exports, "__esModule", {
+ value: true
+ });
+
+/***/ },
+/* 78 */
+/***/ function(module, exports, __webpack_require__) {
+
+ "use strict";
+
+ var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); };
+
+ var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } };
+ /**
+ * Created by Alex on 2/25/2015.
+ */
+
+ var HierarchicalSpringSolver = (function () {
+ function HierarchicalSpringSolver(body, physicsBody, options) {
+ _classCallCheck(this, HierarchicalSpringSolver);
+
+ this.body = body;
+ this.physicsBody = physicsBody;
+ this.options = options;
+ }
+
+ _prototypeProperties(HierarchicalSpringSolver, null, {
+ solve: {
/**
- * 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
- */
- value: function openCluster(clusterNodeId, doNotUpdateCalculationNodes) {
- // kill conditions
- if (clusterNodeId === undefined) {
- throw new Error("No clusterNodeId supplied to openCluster.");
- }
- if (this.body.nodes[clusterNodeId] === undefined) {
- throw new Error("The clusterNodeId supplied to openCluster does not exist.");
+ * This function calculates the springforces on the nodes, accounting for the support nodes.
+ *
+ * @private
+ */
+ value: function solve() {
+ var edgeLength, edge, edgeId;
+ var dx, dy, fx, fy, springForce, distance;
+ var edges = this.body.edges;
+
+ var nodes = this.physicsBody.calculationNodes;
+ var nodeIndices = this.physicsBody.calculationNodeIndices;
+
+ // initialize the spring force counters
+ for (var i = 0; i < nodeIndices.length; i++) {
+ var node1 = nodes[nodeIndices[i]];
+ node1.springFx = 0;
+ node1.springFy = 0;
}
- if (this.body.nodes[clusterNodeId].containedNodes === undefined) {
- console.log("The node:" + clusterNodeId + " is not a cluster.");return;
- };
- var node = this.body.nodes[clusterNodeId];
- var containedNodes = node.containedNodes;
- var containedEdges = node.containedEdges;
- // release nodes
- for (var nodeId in containedNodes) {
- if (containedNodes.hasOwnProperty(nodeId)) {
- this.body.nodes[nodeId] = containedNodes[nodeId];
- // inherit position
- this.body.nodes[nodeId].x = node.x;
- this.body.nodes[nodeId].y = node.y;
+ // 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.body.nodes[edge.toId] !== undefined && this.body.nodes[edge.fromId] !== undefined) {
+ edgeLength = edge.properties.length === undefined ? this.options.springLength : edge.properties.length;
- // inherit speed
- this.body.nodes[nodeId].vx = node.vx;
- this.body.nodes[nodeId].vy = node.vy;
+ dx = edge.from.x - edge.to.x;
+ dy = edge.from.y - edge.to.y;
+ distance = Math.sqrt(dx * dx + dy * dy);
+ distance = distance == 0 ? 0.01 : distance;
- delete this.clusteredNodes[nodeId];
- }
- }
+ // the 1/distance is so the fx and fy can be calculated without sine or cosine.
+ springForce = this.options.springConstant * (edgeLength - distance) / distance;
- // release edges
- for (var edgeId in containedEdges) {
- if (containedEdges.hasOwnProperty(edgeId)) {
- this.body.edges[edgeId] = containedEdges[edgeId];
- this.body.edges[edgeId].connect();
- var edge = this.body.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);
+ fx = dx * springForce;
+ fy = dy * springForce;
+
+ if (edge.to.level != edge.from.level) {
+ edge.to.springFx -= fx;
+ edge.to.springFy -= fy;
+ edge.from.springFx += fx;
+ edge.from.springFy += fy;
+ } else {
+ var factor = 0.5;
+ edge.to.fx -= factor * fx;
+ edge.to.fy -= factor * fy;
+ edge.from.fx += factor * fx;
+ edge.from.fy += factor * fy;
+ }
}
}
}
}
- this.body.emitter.emit("_newEdgesCreated", containedEdges);
-
+ // normalize spring forces
+ var springForce = 1;
+ var springFx, springFy;
+ for (var i = 0; i < nodeIndices.length; i++) {
+ var node = nodes[nodeIndices[i]];
+ springFx = Math.min(springForce, Math.max(-springForce, node.springFx));
+ springFy = Math.min(springForce, Math.max(-springForce, node.springFy));
- var edgeIds = [];
- for (var i = 0; i < node.edges.length; i++) {
- edgeIds.push(node.edges[i].id);
+ node.fx += springFx;
+ node.fy += springFy;
}
- // remove edges in clusterNode
- for (var i = 0; i < edgeIds.length; i++) {
- var edge = this.body.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.body.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.body.nodes[edge.toArray[0].id] !== undefined) {
- this._connectEdge(edge, edge.toArray[0].id, false);
- }
- } else {
- var edgeId = edgeIds[i];
- var viaId = this.body.edges[edgeId].via.id;
- if (viaId) {
- this.body.edges[edgeId].via = null;
- delete this.body.supportNodes[viaId];
- }
- // this removes the edge from node.edges, which is why edgeIds is formed
- this.body.edges[edgeId].disconnect();
- delete this.body.edges[edgeId];
- }
+ // retain energy balance
+ var totalFx = 0;
+ var totalFy = 0;
+ for (var i = 0; i < nodeIndices.length; i++) {
+ var node = nodes[nodeIndices[i]];
+ totalFx += node.fx;
+ totalFy += node.fy;
}
+ var correctionFx = totalFx / nodeIndices.length;
+ var correctionFy = totalFy / nodeIndices.length;
- // remove clusterNode
- delete this.body.nodes[clusterNodeId];
-
- if (doNotUpdateCalculationNodes !== true) {
- this.body.emitter.emit("_dataChanged");
+ for (var i = 0; i < nodeIndices.length; i++) {
+ var node = nodes[nodeIndices[i]];
+ node.fx -= correctionFx;
+ node.fy -= correctionFy;
}
},
writable: true,
configurable: true
- },
- _connectEdge: {
+ }
+ });
+ return HierarchicalSpringSolver;
+ })();
+ exports.HierarchicalSpringSolver = HierarchicalSpringSolver;
+ Object.defineProperty(exports, "__esModule", {
+ value: true
+ });
- /**
- * 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
- */
- value: function _connectEdge(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();
- },
- writable: true,
- configurable: true
- },
- _getClusterStack: {
+/***/ },
+/* 79 */
+/***/ function(module, exports, __webpack_require__) {
- /**
- * Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node
- * @param nodeId
- * @returns {Array}
- * @private
- */
- value: function _getClusterStack(nodeId) {
- var stack = [];
- var max = 100;
- var counter = 0;
+ "use strict";
- while (this.clusteredNodes[nodeId] !== undefined && counter < max) {
- stack.push(this.clusteredNodes[nodeId].node);
- nodeId = this.clusteredNodes[nodeId].clusterId;
- counter++;
- }
- stack.push(this.body.nodes[nodeId]);
- return stack;
- },
- writable: true,
- configurable: true
- },
- _getConnectedId: {
+ var _prototypeProperties = function (child, staticProps, instanceProps) { if (staticProps) Object.defineProperties(child, staticProps); if (instanceProps) Object.defineProperties(child.prototype, instanceProps); };
+ var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } };
- /**
- * Get the Id the node is connected to
- * @param edge
- * @param nodeId
- * @returns {*}
- * @private
- */
- value: function _getConnectedId(edge, nodeId) {
- if (edge.toId != nodeId) {
- return edge.toId;
- } else if (edge.fromId != nodeId) {
- return edge.fromId;
- } else {
- return edge.fromId;
- }
+ /**
+ * Created by Alex on 2/23/2015.
+ */
+
+ var CentralGravitySolver = (function () {
+ function CentralGravitySolver(body, physicsBody, options) {
+ _classCallCheck(this, CentralGravitySolver);
+
+ this.body = body;
+ this.physicsBody = physicsBody;
+ this.setOptions(options);
+ }
+
+ _prototypeProperties(CentralGravitySolver, null, {
+ setOptions: {
+ value: function setOptions(options) {
+ this.options = options;
},
writable: true,
configurable: true
},
- _getHubSize: {
-
- /**
- * 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
- */
- value: function _getHubSize() {
- var average = 0;
- var averageSquared = 0;
- var hubCounter = 0;
- var largestHub = 0;
-
- for (var i = 0; i < this.body.nodeIndices.length; i++) {
- var node = this.body.nodes[this.body.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);
+ solve: {
+ value: function solve() {
+ var dx, dy, distance, node, i;
+ var nodes = this.physicsBody.calculationNodes;
+ var calculationNodeIndices = this.physicsBody.calculationNodeIndices;
+ var gravity = this.options.centralGravity;
+ var gravityForce = 0;
- var hubThreshold = Math.floor(average + 2 * standardDeviation);
+ for (i = 0; i < calculationNodeIndices.length; i++) {
+ node = nodes[calculationNodeIndices[i]];
+ node.damping = this.options.damping;
+ dx = -node.x;
+ dy = -node.y;
+ distance = Math.sqrt(dx * dx + dy * dy);
- // always have at least one to cluster
- if (hubThreshold > largestHub) {
- hubThreshold = largestHub;
+ gravityForce = distance == 0 ? 0 : gravity / distance;
+ node.fx = dx * gravityForce;
+ node.fy = dy * gravityForce;
}
-
- return hubThreshold;
},
writable: true,
configurable: true
}
});
- return ClusterEngine;
+ return CentralGravitySolver;
})();
- exports.ClusterEngine = ClusterEngine;
+ exports.CentralGravitySolver = CentralGravitySolver;
Object.defineProperty(exports, "__esModule", {
value: true
});
diff --git a/lib/network/Edge.js b/lib/network/Edge.js
index d1bfe4a8..b91697d0 100644
--- a/lib/network/Edge.js
+++ b/lib/network/Edge.js
@@ -20,14 +20,11 @@ function Edge (properties, body, networkConstants) {
if (body === undefined) {
throw "No body provided";
}
- var fields = ['edges','physics'];
+ var fields = ['edges'];
var constants = util.selectiveBridgeObject(fields,networkConstants);
this.options = constants.edges;
- this.physics = constants.physics;
this.options['smoothCurves'] = networkConstants['smoothCurves'];
-
-
this.body = body;
// initialize variables
@@ -108,9 +105,7 @@ Edge.prototype.setProperties = function(properties) {
}
}
-
-
- // A node is connected when it has a from and to node.
+ // A node is connected when it has a from and to node that both exist in the network.body.nodes.
this.connect();
this.widthFixed = this.widthFixed || (properties.width !== undefined);
diff --git a/lib/network/Network.js b/lib/network/Network.js
index 10235dcc..1ac73604 100644
--- a/lib/network/Network.js
+++ b/lib/network/Network.js
@@ -2035,7 +2035,7 @@ Network.prototype._redraw = function(hidden = false) {
}
}
- this._drawNodes(ctx,this.body.supportNodes,true);
+ //this._drawNodes(ctx,this.body.supportNodes,true);
// this.physics.nodesSolver._debug(ctx,"#F00F0F");
// restore original scaling and translation
@@ -2371,11 +2371,9 @@ Network.prototype._discreteStepNodes = function(nodes, nodeIndices) {
};
-Network.prototype._revertPhysicsTick = function(nodes) {
- for (var nodeId in nodes) {
- if (nodes.hasOwnProperty(nodeId)) {
- nodes[nodeId].revertPosition();
- }
+Network.prototype._revertPhysicsTick = function(nodes, nodeIndices) {
+ for (let i = 0; i < nodeIndices.length; i++) {
+ nodes[nodeIndices[i]].revertPosition();
}
}
@@ -2395,8 +2393,8 @@ Network.prototype._physicsTick = function() {
// determine if the network has stabilzied
this.moving = mainMovingStatus || supportMovingStatus;
if (this.moving == false) {
- this._revertPhysicsTick(this.body.nodes);
- this._revertPhysicsTick(this.body.supportNodes);
+ this._revertPhysicsTick(this.body.nodes, this.body.nodeIndices);
+ this._revertPhysicsTick(this.body.supportNodes, this.body.supportNodeIndices);
}
else {
// this is here to ensure that there is no start event when the network is already stable.
diff --git a/lib/network/mixins/ClusterMixin.js b/lib/network/mixins/ClusterMixin.js
deleted file mode 100644
index 6a3fab05..00000000
--- a/lib/network/mixins/ClusterMixin.js
+++ /dev/null
@@ -1,613 +0,0 @@
-var Node = require('../Node');
-var Edge = require('../Edge');
-var util = require('../../util');
-
-/**
- *
- * @param hubsize
- * @param options
- */
-exports.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._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.");}
-
- // 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
- */
-exports.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._wrapUp();
- }
-}
-
-/**
- *
- * @param nodeId
- * @param options
- * @param doNotUpdateCalculationNodes
- */
-exports.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
- */
-exports._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
- */
-exports._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
- */
-exports._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
- */
-exports._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._wrapUp();
- }
-}
-
-
-
-/**
- * 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
- */
-exports._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
- */
-exports.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._wrapUp();
- }
-}
-
-
-/**
- * Recalculate navigation nodes, color edges dirty, update nodes list etc.
- * @private
- */
-exports._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
- */
-exports._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
- */
-exports._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
- */
-exports._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
- */
-exports._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/mixins/physics/BarnesHutMixin.js b/lib/network/mixins/physics/BarnesHutMixin.js
deleted file mode 100644
index b8ae1969..00000000
--- a/lib/network/mixins/physics/BarnesHutMixin.js
+++ /dev/null
@@ -1,399 +0,0 @@
-/**
- * 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
- */
-exports._calculateNodeForces = function() {
- if (this.constants.physics.barnesHut.gravitationalConstant != 0) {
- var node;
- var nodes = this.calculationNodes;
- var nodeIndices = this.calculationNodeIndices;
- var nodeCount = nodeIndices.length;
-
- this._formBarnesHutTree(nodes,nodeIndices);
-
- var barnesHutTree = this.barnesHutTree;
-
- // 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
- */
-exports._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.constants.physics.barnesHut.thetaInverted) {
- // duplicate code to reduce function calls to speed up program
- if (distance == 0) {
- distance = 0.1*Math.random();
- dx = distance;
- }
- var gravityForce = this.constants.physics.barnesHut.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.constants.physics.barnesHut.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
- */
-exports._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
- this.barnesHutTree = barnesHutTree
-};
-
-
-/**
- * this updates the mass of a branch. this is increased by adding a node.
- *
- * @param parentBranch
- * @param node
- * @private
- */
-exports._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
- */
-exports._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
- */
-exports._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
- */
-exports._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
- */
-exports._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
- */
-exports._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
- */
-exports._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();
- }
- */
-};
diff --git a/lib/network/mixins/physics/HierarchialRepulsionMixin.js b/lib/network/mixins/physics/HierarchialRepulsionMixin.js
deleted file mode 100644
index 5797e1e3..00000000
--- a/lib/network/mixins/physics/HierarchialRepulsionMixin.js
+++ /dev/null
@@ -1,154 +0,0 @@
-/**
- * Calculate the forces the nodes apply on eachother based on a repulsion field.
- * This field is linearly approximated.
- *
- * @private
- */
-exports._calculateNodeForces = function () {
- var dx, dy, distance, fx, fy,
- repulsingForce, node1, node2, i, j;
-
- var nodes = this.calculationNodes;
- var nodeIndices = this.calculationNodeIndices;
-
- // repulsing forces between nodes
- var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance;
-
- // we loop from i over all but the last entree in the array
- // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
- for (i = 0; i < nodeIndices.length - 1; i++) {
- node1 = nodes[nodeIndices[i]];
- for (j = i + 1; j < nodeIndices.length; j++) {
- node2 = nodes[nodeIndices[j]];
-
- // nodes only affect nodes on their level
- if (node1.level == node2.level) {
-
- dx = node2.x - node1.x;
- dy = node2.y - node1.y;
- distance = Math.sqrt(dx * dx + dy * dy);
-
-
- var steepness = 0.05;
- if (distance < nodeDistance) {
- repulsingForce = -Math.pow(steepness*distance,2) + Math.pow(steepness*nodeDistance,2);
- }
- else {
- repulsingForce = 0;
- }
- // normalize force with
- if (distance == 0) {
- distance = 0.01;
- }
- else {
- repulsingForce = repulsingForce / distance;
- }
- fx = dx * repulsingForce;
- fy = dy * repulsingForce;
-
- node1.fx -= fx;
- node1.fy -= fy;
- node2.fx += fx;
- node2.fy += fy;
- }
- }
- }
-};
-
-
-/**
- * this function calculates the effects of the springs in the case of unsmooth curves.
- *
- * @private
- */
-exports._calculateHierarchicalSpringForces = function () {
- var edgeLength, edge, edgeId;
- var dx, dy, fx, fy, springForce, distance;
- var edges = this.edges;
-
- var nodes = this.calculationNodes;
- var nodeIndices = this.calculationNodeIndices;
-
-
- for (var i = 0; i < nodeIndices.length; i++) {
- var node1 = nodes[nodeIndices[i]];
- node1.springFx = 0;
- node1.springFy = 0;
- }
-
-
- // 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 implies that the edges between big clusters are longer
- edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
-
- dx = (edge.from.x - edge.to.x);
- dy = (edge.from.y - edge.to.y);
- distance = Math.sqrt(dx * dx + dy * dy);
-
- if (distance == 0) {
- distance = 0.01;
- }
-
- // the 1/distance is so the fx and fy can be calculated without sine or cosine.
- springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
-
- fx = dx * springForce;
- fy = dy * springForce;
-
-
-
- if (edge.to.level != edge.from.level) {
- edge.to.springFx -= fx;
- edge.to.springFy -= fy;
- edge.from.springFx += fx;
- edge.from.springFy += fy;
- }
- else {
- var factor = 0.5;
- edge.to.fx -= factor*fx;
- edge.to.fy -= factor*fy;
- edge.from.fx += factor*fx;
- edge.from.fy += factor*fy;
- }
- }
- }
- }
- }
-
- // normalize spring forces
- var springForce = 1;
- var springFx, springFy;
- for (i = 0; i < nodeIndices.length; i++) {
- var node = nodes[nodeIndices[i]];
- springFx = Math.min(springForce,Math.max(-springForce,node.springFx));
- springFy = Math.min(springForce,Math.max(-springForce,node.springFy));
-
- node.fx += springFx;
- node.fy += springFy;
- }
-
- // retain energy balance
- var totalFx = 0;
- var totalFy = 0;
- for (i = 0; i < nodeIndices.length; i++) {
- var node = nodes[nodeIndices[i]];
- totalFx += node.fx;
- totalFy += node.fy;
- }
- var correctionFx = totalFx / nodeIndices.length;
- var correctionFy = totalFy / nodeIndices.length;
-
- for (i = 0; i < nodeIndices.length; i++) {
- var node = nodes[nodeIndices[i]];
- node.fx -= correctionFx;
- node.fy -= correctionFy;
- }
-
-};
\ No newline at end of file
diff --git a/lib/network/mixins/physics/PhysicsMixin.js b/lib/network/mixins/physics/PhysicsMixin.js
deleted file mode 100644
index a7764f7a..00000000
--- a/lib/network/mixins/physics/PhysicsMixin.js
+++ /dev/null
@@ -1,710 +0,0 @@
-var util = require('../../../util');
-var RepulsionMixin = require('./RepulsionMixin');
-var HierarchialRepulsionMixin = require('./HierarchialRepulsionMixin');
-var BarnesHutMixin = require('./BarnesHutMixin');
-
-/**
- * Toggling barnes Hut calculation on and off.
- *
- * @private
- */
-exports._toggleBarnesHut = function () {
- this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled;
- this._loadSelectedForceSolver();
- this.moving = true;
- this.start();
-};
-
-
-/**
- * This loads the node force solver based on the barnes hut or repulsion algorithm
- *
- * @private
- */
-exports._loadSelectedForceSolver = function () {
- // this overloads the this._calculateNodeForces
- if (this.constants.physics.barnesHut.enabled == true) {
- this._clearMixin(RepulsionMixin);
- this._clearMixin(HierarchialRepulsionMixin);
-
- this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity;
- this.constants.physics.springLength = this.constants.physics.barnesHut.springLength;
- this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant;
- this.constants.physics.damping = this.constants.physics.barnesHut.damping;
-
- this._loadMixin(BarnesHutMixin);
- }
- else if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
- this._clearMixin(BarnesHutMixin);
- this._clearMixin(RepulsionMixin);
-
- this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity;
- this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength;
- this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant;
- this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping;
-
- this._loadMixin(HierarchialRepulsionMixin);
- }
- else {
- this._clearMixin(BarnesHutMixin);
- this._clearMixin(HierarchialRepulsionMixin);
- this.barnesHutTree = undefined;
-
- this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity;
- this.constants.physics.springLength = this.constants.physics.repulsion.springLength;
- this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant;
- this.constants.physics.damping = this.constants.physics.repulsion.damping;
-
- this._loadMixin(RepulsionMixin);
- }
-};
-
-/**
- * Before calculating the forces, we check if we need to cluster to keep up performance and we check
- * if there is more than one node. If it is just one node, we dont calculate anything.
- *
- * @private
- */
-exports._initializeForceCalculation = function () {
- // stop calculation if there is only one node
- if (this.calculationNodeIndices.length == 1) {
- this.body.nodes[this.calculationNodeIndices[0]]._setForce(0, 0);
- }
- else {
- // we now start the force calculation
- this._calculateForces();
- }
-};
-
-
-/**
- * Calculate the external forces acting on the nodes
- * Forces are caused by: edges, repulsing forces between nodes, gravity
- * @private
- */
-exports._calculateForces = function () {
- // Gravity is required to keep separated groups from floating off
- // the forces are reset to zero in this loop by using _setForce instead
- // of _addForce
-
- this._calculateGravitationalForces();
- this._calculateNodeForces();
-
- if (this.constants.physics.springConstant > 0) {
- if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
- this._calculateSpringForcesWithSupport();
- }
- else {
- if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
- this._calculateHierarchicalSpringForces();
- }
- else {
- this._calculateSpringForces();
- }
- }
- }
-};
-
-
-/**
- * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also
- * handled in the calculateForces function. We then use a quadratic curve with the center node as control.
- * This function joins the datanodes and invisible (called support) nodes into one object.
- * We do this so we do not contaminate this.body.nodes with the support nodes.
- *
- * @private
- */
-exports._updateCalculationNodes = function () {
- if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
- this.calculationNodes = {};
- this.calculationNodeIndices = [];
-
- for (var nodeId in this.body.nodes) {
- if (this.body.nodes.hasOwnProperty(nodeId)) {
- this.calculationNodes[nodeId] = this.body.nodes[nodeId];
- }
- }
- var supportNodes = this.body.sectors['support']['nodes'];
- for (var supportNodeId in supportNodes) {
- if (supportNodes.hasOwnProperty(supportNodeId)) {
- if (this.body.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) {
- this.calculationNodes[supportNodeId] = supportNodes[supportNodeId];
- }
- else {
- supportNodes[supportNodeId]._setForce(0, 0);
- }
- }
- }
-
- this.calculationNodeIndices = Object.keys(this.calculationNodes);
- }
- else {
- this.calculationNodes = this.body.nodes;
- this.calculationNodeIndices = this.body.nodeIndices;
- }
-};
-
-
-/**
- * this function applies the central gravity effect to keep groups from floating off
- *
- * @private
- */
-exports._calculateGravitationalForces = function () {
- var dx, dy, distance, node, i;
- var nodes = this.calculationNodes;
- var gravity = this.constants.physics.centralGravity;
- var gravityForce = 0;
-
- for (i = 0; i < this.calculationNodeIndices.length; i++) {
- node = nodes[this.calculationNodeIndices[i]];
- node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters.
- // gravity does not apply when we are in a pocket sector
- if (this._sector() == "default" && gravity != 0) {
- 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;
- }
- else {
- node.fx = 0;
- node.fy = 0;
- }
- }
-};
-
-
-
-
-/**
- * this function calculates the effects of the springs in the case of unsmooth curves.
- *
- * @private
- */
-exports._calculateSpringForces = function () {
- var edgeLength, edge, edgeId;
- var dx, dy, fx, fy, springForce, distance;
- var edges = this.body.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.body.nodes.hasOwnProperty(edge.toId) && this.body.nodes.hasOwnProperty(edge.fromId)) {
- edgeLength = edge.physics.springLength;
-
- dx = (edge.from.x - edge.to.x);
- dy = (edge.from.y - edge.to.y);
- distance = Math.sqrt(dx * dx + dy * dy);
-
- if (distance == 0) {
- distance = 0.01;
- }
-
- // the 1/distance is so the fx and fy can be calculated without sine or cosine.
- springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
-
- fx = dx * springForce;
- fy = dy * springForce;
-
- edge.from.fx += fx;
- edge.from.fy += fy;
- edge.to.fx -= fx;
- edge.to.fy -= fy;
- }
- }
- }
- }
-};
-
-
-
-
-/**
- * This function calculates the springforces on the nodes, accounting for the support nodes.
- *
- * @private
- */
-exports._calculateSpringForcesWithSupport = function () {
- var edgeLength, edge, edgeId;
- var edges = this.body.edges;
- var calculationNodes = this.calculationNodes;
-
- // 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 (calculationNodes[edge.toId] !== undefined && calculationNodes[edge.fromId] !== undefined) {
- 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
- */
-exports._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);
-
- if (distance == 0) {
- distance = 0.01;
- }
-
- // the 1/distance is so the fx and fy can be calculated without sine or cosine.
- springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
-
- fx = dx * springForce;
- fy = dy * springForce;
-
- node1.fx += fx;
- node1.fy += fy;
- node2.fx -= fx;
- node2.fy -= fy;
-};
-
-
-exports._cleanupPhysicsConfiguration = function() {
- if (this.physicsConfiguration !== undefined) {
- while (this.physicsConfiguration.hasChildNodes()) {
- this.physicsConfiguration.removeChild(this.physicsConfiguration.firstChild);
- }
-
- this.physicsConfiguration.parentNode.removeChild(this.physicsConfiguration);
- this.physicsConfiguration = undefined;
- }
-}
-
-/**
- * Load the HTML for the physics config and bind it
- * @private
- */
-exports._loadPhysicsConfiguration = function () {
- if (this.physicsConfiguration === undefined) {
- this.backupConstants = {};
- util.deepExtend(this.backupConstants,this.constants);
-
- var maxGravitational = Math.max(20000, (-1 * this.constants.physics.barnesHut.gravitationalConstant) * 10);
- var maxSpring = Math.min(0.05, this.constants.physics.barnesHut.springConstant * 10)
-
- var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"];
- this.physicsConfiguration = document.createElement('div');
- this.physicsConfiguration.className = "PhysicsConfiguration";
- this.physicsConfiguration.innerHTML = '' +
- '
' +
- '' +
- '' +
- '' +
- ''
- this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement);
- this.optionsDiv = document.createElement("div");
- this.optionsDiv.style.fontSize = "14px";
- this.optionsDiv.style.fontFamily = "verdana";
- this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement);
-
- var rangeElement;
- rangeElement = document.getElementById('graph_BH_gc');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant");
- rangeElement = document.getElementById('graph_BH_cg');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity");
- rangeElement = document.getElementById('graph_BH_sc');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant");
- rangeElement = document.getElementById('graph_BH_sl');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength");
- rangeElement = document.getElementById('graph_BH_damp');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping");
-
- rangeElement = document.getElementById('graph_R_nd');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance");
- rangeElement = document.getElementById('graph_R_cg');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity");
- rangeElement = document.getElementById('graph_R_sc');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant");
- rangeElement = document.getElementById('graph_R_sl');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength");
- rangeElement = document.getElementById('graph_R_damp');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping");
-
- rangeElement = document.getElementById('graph_H_nd');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance");
- rangeElement = document.getElementById('graph_H_cg');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity");
- rangeElement = document.getElementById('graph_H_sc');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant");
- rangeElement = document.getElementById('graph_H_sl');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength");
- rangeElement = document.getElementById('graph_H_damp');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping");
- rangeElement = document.getElementById('graph_H_direction');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction");
- rangeElement = document.getElementById('graph_H_levsep');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation");
- rangeElement = document.getElementById('graph_H_nspac');
- rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing");
-
- var radioButton1 = document.getElementById("graph_physicsMethod1");
- var radioButton2 = document.getElementById("graph_physicsMethod2");
- var radioButton3 = document.getElementById("graph_physicsMethod3");
- radioButton2.checked = true;
- if (this.constants.physics.barnesHut.enabled) {
- radioButton1.checked = true;
- }
- if (this.constants.hierarchicalLayout.enabled) {
- radioButton3.checked = true;
- }
-
- var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
- var graph_repositionNodes = document.getElementById("graph_repositionNodes");
- var graph_generateOptions = document.getElementById("graph_generateOptions");
-
- graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this);
- graph_repositionNodes.onclick = graphRepositionNodes.bind(this);
- graph_generateOptions.onclick = graphGenerateOptions.bind(this);
- if (this.constants.smoothCurves == true && this.constants.dynamicSmoothCurves == false) {
- graph_toggleSmooth.style.background = "#A4FF56";
- }
- else {
- graph_toggleSmooth.style.background = "#FF8532";
- }
-
-
- switchConfigurations.apply(this);
-
- radioButton1.onchange = switchConfigurations.bind(this);
- radioButton2.onchange = switchConfigurations.bind(this);
- radioButton3.onchange = switchConfigurations.bind(this);
- }
-};
-
-/**
- * This overwrites the this.constants.
- *
- * @param constantsVariableName
- * @param value
- * @private
- */
-exports._overWriteGraphConstants = function (constantsVariableName, value) {
- var nameArray = constantsVariableName.split("_");
- if (nameArray.length == 1) {
- this.constants[nameArray[0]] = value;
- }
- else if (nameArray.length == 2) {
- this.constants[nameArray[0]][nameArray[1]] = value;
- }
- else if (nameArray.length == 3) {
- this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value;
- }
-};
-
-
-/**
- * this function is bound to the toggle smooth curves button. That is also why it is not in the prototype.
- */
-function graphToggleSmoothCurves () {
- this.constants.smoothCurves.enabled = !this.constants.smoothCurves.enabled;
- var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
- if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";}
- else {graph_toggleSmooth.style.background = "#FF8532";}
-
- this._configureSmoothCurves(false);
-}
-
-/**
- * this function is used to scramble the nodes
- *
- */
-function graphRepositionNodes () {
- for (var nodeId in this.calculationNodes) {
- if (this.calculationNodes.hasOwnProperty(nodeId)) {
- this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0;
- this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0;
- }
- }
- if (this.constants.hierarchicalLayout.enabled == true) {
- this._setupHierarchicalLayout();
- showValueOfRange.call(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance");
- showValueOfRange.call(this, 'graph_H_cg', 1, "physics_centralGravity");
- showValueOfRange.call(this, 'graph_H_sc', 1, "physics_springConstant");
- showValueOfRange.call(this, 'graph_H_sl', 1, "physics_springLength");
- showValueOfRange.call(this, 'graph_H_damp', 1, "physics_damping");
- }
- else {
- this.repositionNodes();
- }
- this.moving = true;
- this.start();
-}
-
-/**
- * this is used to generate an options file from the playing with physics system.
- */
-function graphGenerateOptions () {
- var options = "No options are required, default values used.";
- var optionsSpecific = [];
- var radioButton1 = document.getElementById("graph_physicsMethod1");
- var radioButton2 = document.getElementById("graph_physicsMethod2");
- if (radioButton1.checked == true) {
- if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);}
- if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
- if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
- if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
- if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
- if (optionsSpecific.length != 0) {
- options = "var options = {";
- options += "physics: {barnesHut: {";
- for (var i = 0; i < optionsSpecific.length; i++) {
- options += optionsSpecific[i];
- if (i < optionsSpecific.length - 1) {
- options += ", "
- }
- }
- options += '}}'
- }
- if (this.constants.smoothCurves.enabled != this.backupConstants.smoothCurves.enabled) {
- if (optionsSpecific.length == 0) {options = "var options = {";}
- else {options += ", "}
- options += "smoothCurves: " + this.constants.smoothCurves.enabled;
- }
- if (options != "No options are required, default values used.") {
- options += '};'
- }
- }
- else if (radioButton2.checked == true) {
- options = "var options = {";
- options += "physics: {barnesHut: {enabled: false}";
- if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);}
- if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
- if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
- if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
- if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
- if (optionsSpecific.length != 0) {
- options += ", repulsion: {";
- for (var i = 0; i < optionsSpecific.length; i++) {
- options += optionsSpecific[i];
- if (i < optionsSpecific.length - 1) {
- options += ", "
- }
- }
- options += '}}'
- }
- if (optionsSpecific.length == 0) {options += "}"}
- if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
- options += ", smoothCurves: " + this.constants.smoothCurves;
- }
- options += '};'
- }
- else {
- options = "var options = {";
- if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);}
- if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
- if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
- if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
- if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
- if (optionsSpecific.length != 0) {
- options += "physics: {hierarchicalRepulsion: {";
- for (var i = 0; i < optionsSpecific.length; i++) {
- options += optionsSpecific[i];
- if (i < optionsSpecific.length - 1) {
- options += ", ";
- }
- }
- options += '}},';
- }
- options += 'hierarchicalLayout: {';
- optionsSpecific = [];
- if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);}
- if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);}
- if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);}
- if (optionsSpecific.length != 0) {
- for (var i = 0; i < optionsSpecific.length; i++) {
- options += optionsSpecific[i];
- if (i < optionsSpecific.length - 1) {
- options += ", "
- }
- }
- options += '}'
- }
- else {
- options += "enabled:true}";
- }
- options += '};'
- }
-
-
- this.optionsDiv.innerHTML = options;
-}
-
-/**
- * this is used to switch between barnesHut, repulsion and hierarchical.
- *
- */
-function switchConfigurations () {
- var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"];
- var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value;
- var tableId = "graph_" + radioButton + "_table";
- var table = document.getElementById(tableId);
- table.style.display = "block";
- for (var i = 0; i < ids.length; i++) {
- if (ids[i] != tableId) {
- table = document.getElementById(ids[i]);
- table.style.display = "none";
- }
- }
- this._restoreNodes();
- if (radioButton == "R") {
- this.constants.hierarchicalLayout.enabled = false;
- this.constants.physics.hierarchicalRepulsion.enabled = false;
- this.constants.physics.barnesHut.enabled = false;
- }
- else if (radioButton == "H") {
- if (this.constants.hierarchicalLayout.enabled == false) {
- this.constants.hierarchicalLayout.enabled = true;
- this.constants.physics.hierarchicalRepulsion.enabled = true;
- this.constants.physics.barnesHut.enabled = false;
- this.constants.smoothCurves.enabled = false;
- this._setupHierarchicalLayout();
- }
- }
- else {
- this.constants.hierarchicalLayout.enabled = false;
- this.constants.physics.hierarchicalRepulsion.enabled = false;
- this.constants.physics.barnesHut.enabled = true;
- }
- this._loadSelectedForceSolver();
- var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
- if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";}
- else {graph_toggleSmooth.style.background = "#FF8532";}
- this.moving = true;
- this.start();
-}
-
-
-/**
- * this generates the ranges depending on the iniital values.
- *
- * @param id
- * @param map
- * @param constantsVariableName
- */
-function showValueOfRange (id,map,constantsVariableName) {
- var valueId = id + "_value";
- var rangeValue = document.getElementById(id).value;
-
- if (Array.isArray(map)) {
- document.getElementById(valueId).value = map[parseInt(rangeValue)];
- this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]);
- }
- else {
- document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue);
- this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue));
- }
-
- if (constantsVariableName == "hierarchicalLayout_direction" ||
- constantsVariableName == "hierarchicalLayout_levelSeparation" ||
- constantsVariableName == "hierarchicalLayout_nodeSpacing") {
- this._setupHierarchicalLayout();
- }
- this.moving = true;
- this.start();
-}
-
-
diff --git a/lib/network/mixins/physics/RepulsionMixin.js b/lib/network/mixins/physics/RepulsionMixin.js
deleted file mode 100644
index 32e4ac2d..00000000
--- a/lib/network/mixins/physics/RepulsionMixin.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * Calculate the forces the nodes apply on each other based on a repulsion field.
- * This field is linearly approximated.
- *
- * @private
- */
-exports._calculateNodeForces = function () {
- var dx, dy, angle, distance, fx, fy, combinedClusterSize,
- repulsingForce, node1, node2, i, j;
-
- var nodes = this.calculationNodes;
- var nodeIndices = this.calculationNodeIndices;
-
- // approximation constants
- var a_base = -2 / 3;
- var b = 4 / 3;
-
- // repulsing forces between nodes
- var nodeDistance = this.constants.physics.repulsion.nodeDistance;
- var minimumDistance = nodeDistance;
-
- // we loop from i over all but the last entree in the array
- // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
- for (i = 0; i < nodeIndices.length - 1; i++) {
- node1 = nodes[nodeIndices[i]];
- for (j = i + 1; j < nodeIndices.length; j++) {
- node2 = nodes[nodeIndices[j]];
- combinedClusterSize = node1.clusterSize + node2.clusterSize - 2;
-
- dx = node2.x - node1.x;
- dy = node2.y - node1.y;
- distance = Math.sqrt(dx * dx + dy * dy);
-
- // same condition as BarnesHutSolver, making sure nodes are never 100% overlapping.
- if (distance == 0) {
- distance = 0.1*Math.random();
- dx = distance;
- }
-
- minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification));
- var a = a_base / minimumDistance;
- if (distance < 2 * minimumDistance) {
- if (distance < 0.5 * minimumDistance) {
- repulsingForce = 1.0;
- }
- else {
- repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
- }
-
- // amplify the repulsion for clusters.
- repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification;
- repulsingForce = repulsingForce / Math.max(distance,0.01*minimumDistance);
-
- fx = dx * repulsingForce;
- fy = dy * repulsingForce;
- node1.fx -= fx;
- node1.fy -= fy;
- node2.fx += fx;
- node2.fy += fy;
-
- }
- }
- }
-};
diff --git a/lib/network/modules/Clustering.js b/lib/network/modules/Clustering.js
index 9bac3ffa..186c9fc3 100644
--- a/lib/network/modules/Clustering.js
+++ b/lib/network/modules/Clustering.js
@@ -46,7 +46,7 @@ class ClusterEngine {
* @param options
* @param doNotUpdateCalculationNodes
*/
- clusterByNodeData(options = {}, doNotUpdateCalculationNodes=false) {
+ clusterByNodeData(options = {}, doNotUpdateCalculationNodes = false) {
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
@@ -75,7 +75,7 @@ class ClusterEngine {
*/
clusterOutliers(options, doNotUpdateCalculationNodes) {
options = this._checkOptions(options);
- var clusters = []
+ var clusters = [];
// collect the nodes that will be in the cluster
for (var i = 0; i < this.body.nodeIndices.length; i++) {
diff --git a/lib/network/modules/PhysicsEngine.js b/lib/network/modules/PhysicsEngine.js
index c47f7cc6..03eb2099 100644
--- a/lib/network/modules/PhysicsEngine.js
+++ b/lib/network/modules/PhysicsEngine.js
@@ -3,13 +3,11 @@
*/
import {BarnesHutSolver} from "./components/physics/BarnesHutSolver";
-// TODO Create
-//import {Repulsion} from "./components/physics/Repulsion";
-//import {HierarchicalRepulsion} from "./components/physics/HierarchicalRepulsion";
+import {Repulsion} from "./components/physics/RepulsionSolver";
+import {HierarchicalRepulsion} from "./components/physics/HierarchicalRepulsionSolver";
import {SpringSolver} from "./components/physics/SpringSolver";
-// TODO Create
-//import {HierarchicalSpringSolver} from "./components/physics/HierarchicalSpringSolver";
+import {HierarchicalSpringSolver} from "./components/physics/HierarchicalSpringSolver";
import {CentralGravitySolver} from "./components/physics/CentralGravitySolver";
@@ -32,15 +30,13 @@ class PhysicsEngine {
var options;
if (this.options.model == "repulsion") {
options = this.options.repulsion;
- // TODO uncomment when created
- //this.nodesSolver = new Repulsion(this.body, this.physicsBody, options);
- //this.edgesSolver = new SpringSolver(this.body, options);
+ this.nodesSolver = new Repulsion(this.body, this.physicsBody, options);
+ this.edgesSolver = new SpringSolver(this.body, options);
}
else if (this.options.model == "hierarchicalRepulsion") {
options = this.options.hierarchicalRepulsion;
- // TODO uncomment when created
- //this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options);
- //this.edgesSolver = new HierarchicalSpringSolver(this.body, options);
+ this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options);
+ this.edgesSolver = new HierarchicalSpringSolver(this.body, this.physicsBody, options);
}
else { // barnesHut
options = this.options.barnesHut;
@@ -79,28 +75,14 @@ class PhysicsEngine {
console.error("Support node detected that does not have an edge!")
}
}
- console.log('here', this.body)
this.physicsBody.calculationNodeIndices = Object.keys(this.physicsBody.calculationNodes);
}
-
- calculateField() {
+ step() {
+ this.gravitySolver.solve();
this.nodesSolver.solve();
- }
-
- calculateSprings() {
this.edgesSolver.solve();
}
-
- calculateCentralGravity() {
- this.gravitySolver.solve();
- }
-
- step() {
- this.calculateCentralGravity();
- this.calculateField();
- this.calculateSprings();
- }
}
export {PhysicsEngine};
\ No newline at end of file
diff --git a/lib/network/modules/components/physics/HierarchicalRepulsionSolver.js b/lib/network/modules/components/physics/HierarchicalRepulsionSolver.js
new file mode 100644
index 00000000..f0120ac2
--- /dev/null
+++ b/lib/network/modules/components/physics/HierarchicalRepulsionSolver.js
@@ -0,0 +1,71 @@
+/**
+ * Created by Alex on 2/23/2015.
+ */
+
+class HierarchicalRepulsionSolver {
+ constructor(body, physicsBody, options) {
+ this.body = body;
+ this.physicsBody = physicsBody;
+ this.options = options;
+ }
+
+ /**
+ * Calculate the forces the nodes apply on each other based on a repulsion field.
+ * This field is linearly approximated.
+ *
+ * @private
+ */
+ solve() {
+ var dx, dy, distance, fx, fy,
+ repulsingForce, node1, node2, i, j;
+
+ var nodes = this.physicsBody.calculationNodes;
+ var nodeIndices = this.physicsBody.calculationNodeIndices;
+
+ // repulsing forces between nodes
+ var nodeDistance = this.options.nodeDistance;
+
+ // we loop from i over all but the last entree in the array
+ // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
+ for (i = 0; i < nodeIndices.length - 1; i++) {
+ node1 = nodes[nodeIndices[i]];
+ for (j = i + 1; j < nodeIndices.length; j++) {
+ node2 = nodes[nodeIndices[j]];
+
+ // nodes only affect nodes on their level
+ if (node1.level == node2.level) {
+
+ dx = node2.x - node1.x;
+ dy = node2.y - node1.y;
+ distance = Math.sqrt(dx * dx + dy * dy);
+
+
+ var steepness = 0.05;
+ if (distance < nodeDistance) {
+ repulsingForce = -Math.pow(steepness * distance, 2) + Math.pow(steepness * nodeDistance, 2);
+ }
+ else {
+ repulsingForce = 0;
+ }
+ // normalize force with
+ if (distance == 0) {
+ distance = 0.01;
+ }
+ else {
+ repulsingForce = repulsingForce / distance;
+ }
+ fx = dx * repulsingForce;
+ fy = dy * repulsingForce;
+
+ node1.fx -= fx;
+ node1.fy -= fy;
+ node2.fx += fx;
+ node2.fy += fy;
+ }
+ }
+ }
+ }
+}
+
+
+export {HierarchicalRepulsionSolver};
\ No newline at end of file
diff --git a/lib/network/modules/components/physics/HierarchicalSpringSolver.js b/lib/network/modules/components/physics/HierarchicalSpringSolver.js
new file mode 100644
index 00000000..23e3a936
--- /dev/null
+++ b/lib/network/modules/components/physics/HierarchicalSpringSolver.js
@@ -0,0 +1,103 @@
+/**
+ * Created by Alex on 2/25/2015.
+ */
+
+class HierarchicalSpringSolver {
+ constructor(body, physicsBody, options) {
+ this.body = body;
+ this.physicsBody = physicsBody;
+ this.options = options;
+ }
+
+ /**
+ * This function calculates the springforces on the nodes, accounting for the support nodes.
+ *
+ * @private
+ */
+ solve() {
+ var edgeLength, edge, edgeId;
+ var dx, dy, fx, fy, springForce, distance;
+ var edges = this.body.edges;
+
+ var nodes = this.physicsBody.calculationNodes;
+ var nodeIndices = this.physicsBody.calculationNodeIndices;
+
+ // initialize the spring force counters
+ for (let i = 0; i < nodeIndices.length; i++) {
+ let node1 = nodes[nodeIndices[i]];
+ node1.springFx = 0;
+ node1.springFy = 0;
+ }
+
+
+ // 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.body.nodes[edge.toId] !== undefined && this.body.nodes[edge.fromId] !== undefined) {
+ edgeLength = edge.properties.length === undefined ? this.options.springLength : edge.properties.length;
+
+ dx = (edge.from.x - edge.to.x);
+ dy = (edge.from.y - edge.to.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;
+
+ if (edge.to.level != edge.from.level) {
+ edge.to.springFx -= fx;
+ edge.to.springFy -= fy;
+ edge.from.springFx += fx;
+ edge.from.springFy += fy;
+ }
+ else {
+ let factor = 0.5;
+ edge.to.fx -= factor*fx;
+ edge.to.fy -= factor*fy;
+ edge.from.fx += factor*fx;
+ edge.from.fy += factor*fy;
+ }
+ }
+ }
+ }
+ }
+
+ // normalize spring forces
+ var springForce = 1;
+ var springFx, springFy;
+ for (let i = 0; i < nodeIndices.length; i++) {
+ var node = nodes[nodeIndices[i]];
+ springFx = Math.min(springForce,Math.max(-springForce,node.springFx));
+ springFy = Math.min(springForce,Math.max(-springForce,node.springFy));
+
+ node.fx += springFx;
+ node.fy += springFy;
+ }
+
+ // retain energy balance
+ var totalFx = 0;
+ var totalFy = 0;
+ for (let i = 0; i < nodeIndices.length; i++) {
+ var node = nodes[nodeIndices[i]];
+ totalFx += node.fx;
+ totalFy += node.fy;
+ }
+ var correctionFx = totalFx / nodeIndices.length;
+ var correctionFy = totalFy / nodeIndices.length;
+
+ for (let i = 0; i < nodeIndices.length; i++) {
+ var node = nodes[nodeIndices[i]];
+ node.fx -= correctionFx;
+ node.fy -= correctionFy;
+ }
+ }
+
+}
+
+export {HierarchicalSpringSolver};
\ No newline at end of file
diff --git a/lib/network/modules/components/physics/RepulsionSolver.js b/lib/network/modules/components/physics/RepulsionSolver.js
new file mode 100644
index 00000000..b4001698
--- /dev/null
+++ b/lib/network/modules/components/physics/RepulsionSolver.js
@@ -0,0 +1,70 @@
+/**
+ * Created by Alex on 2/23/2015.
+ */
+
+class RepulsionSolver {
+ constructor(body, physicsBody, options) {
+ this.body = body;
+ this.physicsBody = physicsBody;
+ this.options = options;
+ }
+
+ /**
+ * Calculate the forces the nodes apply on each other based on a repulsion field.
+ * This field is linearly approximated.
+ *
+ * @private
+ */
+ solve() {
+ var dx, dy, distance, fx, fy, repulsingForce, node1, node2;
+
+ var nodes = this.physicsBody.calculationNodes;
+ var nodeIndices = this.physicsBody.calculationNodeIndices;
+
+ // repulsing forces between nodes
+ var nodeDistance = this.options.nodeDistance;
+
+ // approximation constants
+ var a = (-2 / 3) /nodeDistance;
+ var b = 4 / 3;
+
+ // we loop from i over all but the last entree in the array
+ // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
+ for (let i = 0; i < nodeIndices.length - 1; i++) {
+ node1 = nodes[nodeIndices[i]];
+ for (let j = i + 1; j < nodeIndices.length; j++) {
+ node2 = nodes[nodeIndices[j]];
+
+ dx = node2.x - node1.x;
+ dy = node2.y - node1.y;
+ distance = Math.sqrt(dx * dx + dy * dy);
+
+ // same condition as BarnesHutSolver, making sure nodes are never 100% overlapping.
+ if (distance == 0) {
+ distance = 0.1*Math.random();
+ dx = distance;
+ }
+
+ if (distance < 2 * nodeDistance) {
+ if (distance < 0.5 * nodeDistance) {
+ repulsingForce = 1.0;
+ }
+ else {
+ repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / nodeDistance - 1) * steepness))
+ }
+ repulsingForce = repulsingForce / distance;
+
+ fx = dx * repulsingForce;
+ fy = dy * repulsingForce;
+ node1.fx -= fx;
+ node1.fy -= fy;
+ node2.fx += fx;
+ node2.fy += fy;
+ }
+ }
+ }
+ }
+}
+
+
+export {RepulsionSolver};
\ No newline at end of file
diff --git a/lib/network/modules/components/physics/SpringSolver.js b/lib/network/modules/components/physics/SpringSolver.js
index dc466acb..06a634e3 100644
--- a/lib/network/modules/components/physics/SpringSolver.js
+++ b/lib/network/modules/components/physics/SpringSolver.js
@@ -8,18 +8,12 @@ class SpringSolver {
this.options = options;
}
- solve() {
- this._calculateSpringForces();
- }
-
-
-
/**
* This function calculates the springforces on the nodes, accounting for the support nodes.
*
* @private
*/
- _calculateSpringForces() {
+ solve() {
var edgeLength, edge, edgeId;
var edges = this.body.edges;
@@ -30,7 +24,7 @@ class SpringSolver {
if (edge.connected === true) {
// only calculate forces if nodes are in the same sector
if (this.body.nodes[edge.toId] !== undefined && this.body.nodes[edge.fromId] !== undefined) {
- edgeLength = edge.physics.springLength;
+ edgeLength = edge.properties.length === undefined ? this.options.springLength : edge.properties.length;
if (edge.via != null) {
var node1 = edge.to;
var node2 = edge.via;