Browse Source

Finalized Barnes Hut force calculation, set it to default. Completed smooth curves. Added comments.

css_transitions
Alex de Mulder 10 years ago
parent
commit
fc95490f8a
8 changed files with 313 additions and 207 deletions
  1. +0
    -32
      dist/vis.min.js
  2. +0
    -3
      examples/graph/19_scale_free_graph_clustering.html
  3. +52
    -44
      src/graph/Graph.js
  4. +5
    -2
      src/graph/Node.js
  5. +100
    -62
      src/graph/graphMixins/ClusterMixin.js
  6. +53
    -31
      src/graph/graphMixins/physics/PhysicsMixin.js
  7. +102
    -32
      src/graph/graphMixins/physics/barnesHut.js
  8. +1
    -1
      src/graph/graphMixins/physics/repulsion.js

+ 0
- 32
dist/vis.min.js
File diff suppressed because it is too large
View File


+ 0
- 3
examples/graph/19_scale_free_graph_clustering.html View File

@ -88,9 +88,6 @@
};
*/
var options = {
edges: {
length: 50
},
clustering: {
enabled: clusteringOn,
clusterEdgeThreshold: clusterEdgeThreshold

+ 52
- 44
src/graph/Graph.js View File

@ -70,16 +70,16 @@ function Graph (container, data, options) {
},
physics: {
barnesHut: {
enabled: false,
theta: 1 / 0.3, // inverted to save time during calculation
gravitationalConstant: -10000,
centralGravity: 0.08,
springLength: 100,
springConstant: 0.02
enabled: true,
theta: 1 / 0.4, // inverted to save time during calculation
gravitationalConstant: -7500,
centralGravity: 0.9,
springLength: 20,
springConstant: 0.06
},
repulsion: {
centralGravity: 0.01,
springLength: 100,
springLength: 60,
springConstant: 0.05
},
centralGravity: null,
@ -98,11 +98,11 @@ function Graph (container, data, options) {
fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px).
forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
maxFontSize: 1000,
distanceAmplification: 0.03, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
distanceAmplification: 0.1, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
edgeGrowth: 1, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
nodeScaling: {width: 5, // (px PNiC) | growth of the width per node in cluster.
height: 5, // (px PNiC) | growth of the height per node in cluster.
radius: 5}, // (px PNiC) | growth of the radius per node in cluster.
nodeScaling: {width: 1, // (px PNiC) | growth of the width per node in cluster.
height: 1, // (px PNiC) | growth of the height per node in cluster.
radius: 1}, // (px PNiC) | growth of the radius per node in cluster.
maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster.
activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open.
clusterLevelDifference: 2
@ -417,7 +417,6 @@ Graph.prototype.setOptions = function (options) {
if (options.stabilize !== undefined) {this.stabilize = options.stabilize;}
if (options.selectable !== undefined) {this.selectable = options.selectable;}
/*
if (options.physics) {
if (options.physics.barnesHut) {
this.constants.physics.barnesHut.enabled = true;
@ -437,7 +436,7 @@ Graph.prototype.setOptions = function (options) {
}
}
}
*/
if (options.clustering) {
this.constants.clustering.enabled = true;
for (var prop in options.clustering) {
@ -1264,13 +1263,11 @@ Graph.prototype._addNodes = function(ids) {
var node = new Node(data, this.images, this.groups, this.constants);
this.nodes[id] = node; // note: this may replace an existing node
if (!node.isFixed() && this.createNodeOnClick != true) {
// TODO: position new nodes in a smarter way!
var radius = this.constants.edges.length;
var count = ids.length;
var angle = 2 * Math.PI * (i / count);
node.x = radius * Math.cos(angle);
node.y = radius * Math.sin(angle);
if ((node.xFixed == false || node.yFixed == false) && this.createNodeOnClick != true) {
var radius = this.constants.physics.springLength * 0.1*ids.length;
var angle = 2 * Math.PI * Math.random();
if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
// note: no not use node.isMoving() here, as that gives the current
// velocity of the node, which is zero after creation of the node.
@ -1431,6 +1428,7 @@ Graph.prototype._updateEdges = function (ids) {
}
}
this._createBezierNodes();
this.moving = true;
this._updateValueRange(edges);
};
@ -1446,6 +1444,9 @@ Graph.prototype._removeEdges = function (ids) {
var id = ids[i];
var edge = edges[id];
if (edge) {
if (edge.via != null) {
delete this.sectors['support']['nodes'][edge.via.id];
}
edge.disconnect();
delete edges[id];
}
@ -1453,6 +1454,7 @@ Graph.prototype._removeEdges = function (ids) {
this.moving = true;
this._updateValueRange(edges);
this._setCalculationNodes();
};
/**
@ -1741,10 +1743,9 @@ Graph.prototype._doStabilize = function() {
* @private
*/
Graph.prototype._isMoving = function(vmin) {
var vminCorrected = vmin / Math.max(this.scale,0.05);
var nodes = this.nodes;
for (var id in nodes) {
if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vminCorrected)) {
if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
return true;
}
}
@ -1760,7 +1761,7 @@ Graph.prototype._isMoving = function(vmin) {
* @private
*/
Graph.prototype._discreteStepNodes = function() {
var interval = 1.2;
var interval = 1.0;
var nodes = this.nodes;
if (this.constants.maxVelocity > 0) {
@ -1777,8 +1778,13 @@ Graph.prototype._discreteStepNodes = function() {
}
}
}
var vmin = this.constants.minVelocity;
this.moving = this._isMoving(vmin);
var vminCorrected = this.constants.minVelocity / Math.max(this.scale,0.05);
if (vminCorrected > 0.5*this.constants.maxVelocity) {
this.moving = true;
}
else {
this.moving = this._isMoving(vminCorrected);
}
};
@ -1820,15 +1826,15 @@ Graph.prototype.start = function() {
graph._zoom(graph.scale*(1 + graph.zoomIncrement), center);
}
var calctimeStart = Date.now();
// var calctimeStart = Date.now();
graph.start();
graph.start();
var calctime = Date.now() - calctimeStart;
var rendertimeStart = Date.now();
// var calctime = Date.now() - calctimeStart;
// var rendertimeStart = Date.now();
graph._redraw();
var rendertime = Date.now() - rendertimeStart;
// var rendertime = Date.now() - rendertimeStart;
//this.end = window.performance.now();
//this.time = this.end - this.startTime;
@ -1882,25 +1888,27 @@ Graph.prototype.toggleFreeze = function() {
Graph.prototype._createBezierNodes = function() {
for (var edgeId in this.edges) {
if (this.edges.hasOwnProperty(edgeId)) {
var edge = this.edges[edgeId];
if (edge.smooth == true) {
if (edge.via == null) {
this.sectors['support']['nodes'][edge.id] = new Node(
{id:edge.id,
mass:1,
shape:'circle',
internalMultiplier:1,
damping: 0.9},{},{},this.constants);
edge.via = this.sectors['support']['nodes'][edge.id];
edge.via.parentEdgeId = edge.id;
edge.positionBezierNode();
if (this.constants.smoothCurves == true) {
for (var edgeId in this.edges) {
if (this.edges.hasOwnProperty(edgeId)) {
var edge = this.edges[edgeId];
if (edge.smooth == true) {
if (edge.via == null) {
var nodeId = "edgeId:".concat(edge.id);
this.sectors['support']['nodes'][nodeId] = new Node(
{id:nodeId,
mass:1,
shape:'circle',
internalMultiplier:1,
damping: 1},{},{},this.constants);
edge.via = this.sectors['support']['nodes'][nodeId];
edge.via.parentEdgeId = edge.id;
edge.positionBezierNode();
}
}
}
}
}
};

+ 5
- 2
src/graph/Node.js View File

@ -60,6 +60,7 @@ function Node(properties, imagelist, grouplist, constants) {
this.dampingBase = 0.9;
this.damping = 0.9; // this is manipulated in the updateDamping function
this.mass = 1; // kg
this.setProperties(properties, constants);
@ -74,7 +75,7 @@ function Node(properties, imagelist, grouplist, constants) {
this.growthIndicator = 0;
// mass, force, velocity
this.mass = 1; // kg
this.fx = 0.0; // external force x
this.fy = 0.0; // external force y
this.vx = 0.0; // velocity x
@ -150,6 +151,8 @@ Node.prototype.setProperties = function(properties, constants) {
// physics
if (properties.internalMultiplier !== undefined) {this.internalMultiplier = properties.internalMultiplier;}
if (properties.damping !== undefined) {this.dampingBase = properties.damping;}
if (properties.mass !== undefined) {this.mass = properties.mass;}
// navigation controls properties
if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;}
@ -984,7 +987,7 @@ Node.prototype.setScale = function(scale) {
* @param {Number} numberOfNodes
*/
Node.prototype.updateDamping = function() {
this.damping = Math.min(Math.max(1.5,this.dampingBase),this.dampingBase + 0.01*this.growthIndicator);
this.damping = Math.min(Math.max(1.2,this.dampingBase),this.dampingBase + 0.01*this.growthIndicator);
};

+ 100
- 62
src/graph/graphMixins/ClusterMixin.js View File

@ -8,23 +8,24 @@
*/
var ClusterMixin = {
/**
* This is only called in the constructor of the graph object
* */
/**
* This is only called in the constructor of the graph object
*
*/
startWithClustering : function() {
// cluster if the data set is big
this.clusterToFit(this.constants.clustering.initialMaxNodes, true);
// cluster if the data set is big
this.clusterToFit(this.constants.clustering.initialMaxNodes, true);
// updates the lables after clustering
this.updateLabels();
// updates the lables after clustering
this.updateLabels();
// this is called here because if clusterin is disabled, the start and stabilize are called in
// the setData function.
if (this.stabilize) {
this._doStabilize();
}
this.start();
},
// this is called here because if clusterin is disabled, the start and stabilize are called in
// the setData function.
if (this.stabilize) {
this._doStabilize();
}
this.start();
},
/**
* This function clusters until the initialMaxNodes has been reached
@ -56,7 +57,7 @@ var ClusterMixin = {
if (level > 0 && reposition == true) {
this.repositionNodes();
}
},
},
/**
* This function can be called to open up a specific cluster. It is only called by
@ -68,12 +69,16 @@ var ClusterMixin = {
var isMovingBeforeClustering = this.moving;
if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) &&
!(this._sector() == "default" && this.nodeIndices.length == 1)) {
// this loads a new sector, loads the nodes and edges and nodeIndices of it.
this._addSector(node);
var level = 0;
// we decluster until we reach a decent number of nodes
while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) {
this.decreaseClusterLevel();
level += 1;
}
}
else {
this._expandClusterNode(node,false,true);
@ -89,7 +94,7 @@ var ClusterMixin = {
if (this.moving != isMovingBeforeClustering) {
this.start();
}
},
},
/**
@ -109,7 +114,7 @@ var ClusterMixin = {
*/
increaseClusterLevel : function() {
this.updateClusters(-1,false,true);
},
},
/**
@ -119,7 +124,7 @@ var ClusterMixin = {
*/
decreaseClusterLevel : function() {
this.updateClusters(1,false,true);
},
},
/**
@ -192,7 +197,9 @@ var ClusterMixin = {
this.start();
}
}
},
this._setCalculationNodes();
},
/**
* This function handles the chains. It is called on every updateClusters().
@ -204,7 +211,7 @@ var ClusterMixin = {
this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage)
}
},
},
/**
* this functions starts clustering by hubs
@ -215,7 +222,7 @@ var ClusterMixin = {
_aggregateHubs : function(force) {
this._getHubSize();
this._formClustersByHub(force,false);
},
},
/**
@ -244,7 +251,7 @@ var ClusterMixin = {
this.start();
}
}
},
},
/**
* If a cluster takes up more than a set percentage of the screen, open the cluster
@ -263,7 +270,7 @@ var ClusterMixin = {
}
}
}
},
},
/**
@ -278,7 +285,7 @@ var ClusterMixin = {
this._expandClusterNode(node,recursive,force);
this._setCalculationNodes();
}
},
},
/**
* This function checks if a node has to be opened. This is done by checking the zoom level.
@ -324,8 +331,7 @@ var ClusterMixin = {
}
}
}
},
},
/**
* ONLY CALLED FROM _expandClusterNode
@ -342,14 +348,13 @@ var ClusterMixin = {
* @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
* @private
*/
_expelChildFromParent : function(parentNode, containedNodeId, recursive, force, openAll) {
_expelChildFromParent : function(parentNode, containedNodeId, recursive, force, openAll) {
var childNode = parentNode.containedNodes[containedNodeId];
// if child node has been added on smaller scale than current, kick out
if (childNode.formationScale < this.scale || force == true) {
// remove the selection, first remove the selection from the connected edges
this._unselectConnectedEdges(parentNode);
parentNode.unselect();
// unselect all selected items
this._unselectAll();
// put the child node back in the global nodes object
this.nodes[containedNodeId] = childNode;
@ -370,8 +375,8 @@ var ClusterMixin = {
parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
// place the child node near the parent, not at the exact same location to avoid chaos in the system
childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random()) * childNode.clusterSize;
childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random()) * childNode.clusterSize;
childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random());
childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random());
// remove node from the list
delete parentNode.containedNodes[containedNodeId];
@ -391,24 +396,37 @@ var ClusterMixin = {
parentNode.clusterSessions.pop();
}
this._repositionBezierNodes(childNode);
// this._repositionBezierNodes(parentNode);
// remove the clusterSession from the child node
childNode.clusterSession = 0;
// restart the simulation to reorganise all nodes
this.moving = true;
// recalculate the size of the node on the next time the node is rendered
parentNode.clearSizeCache();
// this unselects the rest of the edges
this._unselectConnectedEdges(parentNode);
// restart the simulation to reorganise all nodes
this.moving = true;
}
// check if a further expansion step is possible if recursivity is enabled
if (recursive == true) {
this._expandClusterNode(childNode,recursive,force,openAll);
}
},
},
/**
* position the bezier nodes at the center of the edges
*
* @param node
* @private
*/
_repositionBezierNodes : function(node) {
for (var i = 0; i < node.dynamicEdges.length; i++) {
node.dynamicEdges[i].positionBezierNode();
}
},
/**
@ -427,7 +445,8 @@ var ClusterMixin = {
else {
this._forceClustersByZoom();
}
},
},
/**
* This function handles the clustering by zooming out, this is based on a minimum edge distance
@ -470,7 +489,7 @@ var ClusterMixin = {
}
}
}
},
},
/**
* This function forces the graph to cluster all nodes with only one connecting edge to their
@ -501,9 +520,16 @@ var ClusterMixin = {
}
}
}
},
},
/**
* To keep the nodes of roughly equal size we normalize the cluster levels.
* This function clusters a node to its smallest connected neighbour.
*
* @param node
* @private
*/
_clusterToSmallestNeighbour : function(node) {
var smallestNeighbour = -1;
var smallestNeighbourNode = null;
@ -546,7 +572,7 @@ var ClusterMixin = {
this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual);
}
}
},
},
/**
* This function forms a cluster from a specific preselected hub node
@ -616,7 +642,7 @@ var ClusterMixin = {
}
}
}
},
},
@ -687,7 +713,7 @@ var ClusterMixin = {
// restart the simulation to reorganise all nodes
this.moving = true;
},
},
/**
@ -717,7 +743,7 @@ var ClusterMixin = {
}
node.dynamicEdgesLength -= correction;
}
},
},
/**
@ -746,7 +772,7 @@ var ClusterMixin = {
break;
}
}
},
},
/**
* This function connects an edge that was connected to a child node to the parent node.
@ -777,9 +803,17 @@ var ClusterMixin = {
this._addToReroutedEdges(parentNode,childNode,edge);
}
},
},
/**
* If a node is connected to itself, a circular edge is drawn. When clustering we want to contain
* these edges inside of the cluster.
*
* @param parentNode
* @param childNode
* @private
*/
_containCircularEdgesFromNode : function(parentNode, childNode) {
// manage all the edges connected to the child and parent nodes
for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
@ -850,7 +884,7 @@ var ClusterMixin = {
// remove the entry from the rerouted edges
delete parentNode.reroutedEdges[childNode.id];
}
},
},
/**
@ -868,7 +902,7 @@ var ClusterMixin = {
parentNode.dynamicEdges.splice(i,1);
}
}
},
},
/**
@ -893,7 +927,7 @@ var ClusterMixin = {
// remove the entry from the contained edges
delete parentNode.containedEdges[childNode.id];
},
},
@ -939,9 +973,15 @@ var ClusterMixin = {
// }
// }
},
},
/**
* We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes
* if the rest of the nodes are already a few cluster levels in.
* To fix this we use this function. It determines the min and max cluster level and sends nodes that have not
* clustered enough to the clusterToSmallestNeighbours function.
*/
normalizeClusterLevels : function() {
var maxLevel = 0;
var minLevel = 1e9;
@ -992,7 +1032,7 @@ var ClusterMixin = {
&&
Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
)
},
},
/**
@ -1003,17 +1043,14 @@ var ClusterMixin = {
repositionNodes : function() {
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
if (!node.isFixed()) {
if ((node.xFixed == false || node.yFixed == false) && this.createNodeOnClick != true) {
var radius = this.constants.physics.springLength * (1 + 0.1*node.mass);
var angle = 2 * Math.PI * Math.random();
node.x = radius * Math.cos(angle);
node.y = radius * Math.sin(angle);
if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
}
}
},
},
/**
@ -1054,7 +1091,7 @@ var ClusterMixin = {
// console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
// console.log("hubThreshold:",this.hubThreshold);
},
},
/**
@ -1077,7 +1114,7 @@ var ClusterMixin = {
}
}
}
},
},
/**
* We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
@ -1097,5 +1134,6 @@ var ClusterMixin = {
}
}
return chains/total;
}
}
};

+ 53
- 31
src/graph/graphMixins/physics/PhysicsMixin.js View File

@ -5,12 +5,19 @@
var physicsMixin = {
/**
* Toggling barnes Hut calculation on and off.
*
* @private
*/
_toggleBarnesHut : function() {
this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled;
this._loadSelectedForceSolver();
this.moving = true;
this.start();
},
/**
* 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.
@ -51,13 +58,22 @@ var physicsMixin = {
if (this.constants.smoothCurves == true) {
this._calculateSpringForcesOnSupport();
this._calculateSpringForcesWithSupport();
}
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.nodes with the support nodes.
*
* @private
*/
_setCalculationNodes : function() {
if (this.constants.smoothCurves == true) {
this.calculationNodes = {};
@ -74,6 +90,9 @@ var physicsMixin = {
if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) {
this.calculationNodes[supportNodeId] = supportNodes[supportNodeId];
}
else {
supportNodes[supportNodeId]._setForce(0,0);
}
}
}
@ -90,17 +109,11 @@ var physicsMixin = {
},
_clearForces : function() {
var node, i;
var nodes = this.nodes;
for (i = 0; i < this.nodeIndices.length; i++) {
node = nodes[this.nodeIndices[i]];
node._setForce(0, 0);
node.updateDamping(this.nodeIndices.length);
}
},
/**
* this function applies the central gravity effect to keep groups from floating off
*
* @private
*/
_calculateGravitationalForces : function() {
var dx, dy, angle, fx, fy, node, i;
var nodes = this.calculationNodes;
@ -110,8 +123,8 @@ var physicsMixin = {
node = nodes[this.calculationNodeIndices[i]];
// gravity does not apply when we are in a pocket sector
if (this._sector() == "default") {
dx = -node.x;// + screenCenterPos.x;
dy = -node.y;// + screenCenterPos.y;
dx = -node.x;
dy = -node.y;
angle = Math.atan2(dy, dx);
fx = Math.cos(angle) * gravity;
@ -126,8 +139,14 @@ var physicsMixin = {
}
},
/**
* this function calculates the effects of the springs in the case of unsmooth curves.
*
* @private
*/
_calculateSpringForces : function() {
var dx, dy, angle, fx, fy, springForce, length, edgeLength, edge, edgeId;
var edgeLength, edge, edgeId;
var edges = this.edges;
// forces caused by the edges, modelled as springs
@ -137,30 +156,23 @@ var physicsMixin = {
if (edge.connected) {
// only calculate forces if nodes are in the same sector
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
dx = (edge.to.x - edge.from.x);
dy = (edge.to.y - edge.from.y);
edgeLength = edge.length;
// this implies that the edges between big clusters are longer
edgeLength += (edge.to.growthIndicator + edge.from.growthIndicator) * this.constants.clustering.edgeGrowth;
length = Math.sqrt(dx * dx + dy * dy);
angle = Math.atan2(dy, dx);
springForce = this.constants.physics.springConstant * (edgeLength - length);
fx = Math.cos(angle) * springForce;
fy = Math.sin(angle) * springForce;
edge.from._addForce(-fx, -fy);
edge.to._addForce(fx, fy);
this._calculateSpringForce(edge.from,edge.to,edgeLength);
}
}
}
}
},
_calculateSpringForcesOnSupport : function() {
/**
* This function calculates the springforces on the nodes, accounting for the support nodes.
*
* @private
*/
_calculateSpringForcesWithSupport : function() {
var edgeLength, edge, edgeId, growthIndicator;
var edges = this.edges;
@ -177,11 +189,11 @@ var physicsMixin = {
var node3 = edge.from;
edgeLength = 0.5*edge.length;
growthIndicator = 0.5*(node1.growthIndicator + node3.growthIndicator);
// this implies that the edges between big clusters are longer
edgeLength += growthIndicator * this.constants.clustering.edgeGrowth;
this._calculateSpringForce(node1,node2,edgeLength);
this._calculateSpringForce(node2,node3,edgeLength);
}
@ -191,6 +203,15 @@ var physicsMixin = {
}
},
/**
* 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
*/
_calculateSpringForce : function(node1,node2,edgeLength) {
var dx, dy, angle, fx, fy, springForce, length;
@ -198,6 +219,7 @@ var physicsMixin = {
dy = (node1.y - node2.y);
length = Math.sqrt(dx * dx + dy * dy);
angle = Math.atan2(dy, dx);
springForce = this.constants.physics.springConstant * (edgeLength - length);
fx = Math.cos(angle) * springForce;

+ 102
- 32
src/graph/graphMixins/physics/barnesHut.js View File

@ -4,7 +4,12 @@
var barnesHutMixin = {
/**
* 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
*/
_calculateNodeForces : function() {
var node;
var nodes = this.calculationNodes;
@ -13,7 +18,6 @@ var barnesHutMixin = {
this._formBarnesHutTree(nodes,nodeIndices);
var barnesHutTree = this.barnesHutTree;
// place the nodes one by one recursively
@ -27,14 +31,23 @@ var barnesHutMixin = {
}
},
/**
* 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
*/
_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;
dx = parentBranch.centerOfMass.x - node.x;
dy = parentBranch.centerOfMass.y - node.y;
distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) { // distance is 0 if it looks to apply a force on itself.
@ -60,8 +73,17 @@ var barnesHutMixin = {
}
},
/**
* The gravitational force applied on the node by the mass of the branch.
*
* @param parentBranch
* @param node
* @param dx
* @param dy
* @param distance
* @private
*/
_getForceOnNode : function(parentBranch, node, dx ,dy, distance) {
//console.log(Math.max(Math.max(node.height,node.radius),node.width),parentBranch.maxWidth,distance);
// even if the parentBranch only has one node, its Center of Mass is at the right place (the node in this case).
var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance);
var angle = Math.atan2(dy, dx);
@ -71,6 +93,13 @@ var barnesHutMixin = {
},
/**
* This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
*
* @param nodes
* @param nodeIndices
* @private
*/
_formBarnesHutTree : function(nodes,nodeIndices) {
var node;
var nodeCount = nodeIndices.length;
@ -95,13 +124,19 @@ var barnesHutMixin = {
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}, // Center of Mass
centerOfMass:{x:0,y:0}, // Center of Mass
mass:0,
range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
size: Math.abs(maxX - minX),
calcSize: 1 / Math.abs(maxX - minX),
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,
@ -124,11 +159,11 @@ var barnesHutMixin = {
var totalMass = parentBranch.mass + node.mass;
var totalMassInv = 1/totalMass;
parentBranch.CenterOfMass.x = parentBranch.CenterOfMass.x * parentBranch.mass + node.x * node.mass;
parentBranch.CenterOfMass.x *= totalMassInv;
parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.mass;
parentBranch.centerOfMass.x *= totalMassInv;
parentBranch.CenterOfMass.y = parentBranch.CenterOfMass.y * parentBranch.mass + node.y * node.mass;
parentBranch.CenterOfMass.y *= totalMassInv;
parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.mass;
parentBranch.centerOfMass.y *= totalMassInv;
parentBranch.mass = totalMass;
var biggestSize = Math.max(Math.max(node.height,node.radius),node.width);
@ -137,10 +172,12 @@ var barnesHutMixin = {
},
_placeInTree : function(parentBranch,node) {
// update the mass of the branch.
this._updateBranchMass(parentBranch,node);
_placeInTree : function(parentBranch,node,skipMassUpdate) {
if (skipMassUpdate != true || skipMassUpdate === undefined) {
// update the mass of the branch.
this._updateBranchMass(parentBranch,node);
}
//console.log(parentBranch.children.NW.range.maxX,parentBranch.children.NW.range.maxY, node.x,node.y);
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");
@ -150,7 +187,7 @@ var barnesHutMixin = {
}
}
else { // in NE or SE
if (parentBranch.children.NE.range.maxY > node.y) { // in NE
if (parentBranch.children.NW.range.maxY > node.y) { // in NE
this._placeInRegion(parentBranch,node,"NE");
}
else { // in SE
@ -168,8 +205,16 @@ var barnesHutMixin = {
this._updateBranchMass(parentBranch.children[region],node);
break;
case 1: // convert into children
this._splitBranch(parentBranch.children[region]);
this._placeInTree(parentBranch.children[region],node);
// 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 += 0.1;
}
else {
this._splitBranch(parentBranch.children[region]);
this._placeInTree(parentBranch.children[region],node);
}
break;
case 4: // place in branch
this._placeInTree(parentBranch.children[region],node);
@ -178,12 +223,19 @@ var barnesHutMixin = {
},
/**
* 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
*/
_splitBranch : function(parentBranch) {
// if the branch is filled 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.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0;
}
parentBranch.childrenCount = 4;
parentBranch.children.data = null;
@ -210,47 +262,56 @@ var barnesHutMixin = {
*/
_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 + parentBranch.size;
maxX = parentBranch.range.minX + childSize;
minY = parentBranch.range.minY;
maxY = parentBranch.range.minY + parentBranch.size;
maxY = parentBranch.range.minY + childSize;
break;
case "NE":
minX = parentBranch.range.minX + parentBranch.size;
minX = parentBranch.range.minX + childSize;
maxX = parentBranch.range.maxX;
minY = parentBranch.range.minY;
maxY = parentBranch.range.minY + parentBranch.size;
maxY = parentBranch.range.minY + childSize;
break;
case "SW":
minX = parentBranch.range.minX;
maxX = parentBranch.range.minX + parentBranch.size;
minY = parentBranch.range.minY + parentBranch.size;
maxX = parentBranch.range.minX + childSize;
minY = parentBranch.range.minY + childSize;
maxY = parentBranch.range.maxY;
break;
case "SE":
minX = parentBranch.range.minX + parentBranch.size;
minX = parentBranch.range.minX + childSize;
maxX = parentBranch.range.maxX;
minY = parentBranch.range.minY + parentBranch.size;
minY = parentBranch.range.minY + childSize;
maxY = parentBranch.range.maxY;
break;
}
parentBranch.children[region] = {
CenterOfMass:{x:0,y:0},
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,
level: parentBranch.level+1,
childrenCount: 0
};
},
/**
* This function is for debugging purposed, it draws the tree.
*
* @param ctx
* @param color
* @private
*/
_drawTree : function(ctx,color) {
if (this.barnesHutTree !== undefined) {
@ -260,6 +321,15 @@ var barnesHutMixin = {
}
},
/**
* This function is for debugging purposes. It draws the branches recursively.
*
* @param branch
* @param ctx
* @param color
* @private
*/
_drawBranch : function(branch,ctx,color) {
if (color === undefined) {
color = "#FF0000";
@ -294,7 +364,7 @@ var barnesHutMixin = {
/*
if (branch.mass > 0) {
ctx.circle(branch.CenterOfMass.x, branch.CenterOfMass.y, 3*branch.mass);
ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
ctx.stroke();
}
*/

+ 1
- 1
src/graph/graphMixins/physics/repulsion.js View File

@ -30,7 +30,7 @@ var repulsionMixin = {
node1 = nodes[nodeIndices[i]];
for (j = i+1; j < nodeIndices.length; j++) {
node2 = nodes[nodeIndices[j]];
combinedClusterSize = (node1.growthIndicator + node2.growthIndicator);
combinedClusterSize = node1.clusterSize + node2.clusterSize - 2;
dx = node2.x - node1.x;
dy = node2.y - node1.y;

Loading…
Cancel
Save