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 = { var options = {
edges: {
length: 50
},
clustering: { clustering: {
enabled: clusteringOn, enabled: clusteringOn,
clusterEdgeThreshold: clusterEdgeThreshold clusterEdgeThreshold: clusterEdgeThreshold

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

@ -70,16 +70,16 @@ function Graph (container, data, options) {
}, },
physics: { physics: {
barnesHut: { 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: { repulsion: {
centralGravity: 0.01, centralGravity: 0.01,
springLength: 100,
springLength: 60,
springConstant: 0.05 springConstant: 0.05
}, },
centralGravity: null, 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). 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). forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
maxFontSize: 1000, 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. 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. 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. activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open.
clusterLevelDifference: 2 clusterLevelDifference: 2
@ -417,7 +417,6 @@ Graph.prototype.setOptions = function (options) {
if (options.stabilize !== undefined) {this.stabilize = options.stabilize;} if (options.stabilize !== undefined) {this.stabilize = options.stabilize;}
if (options.selectable !== undefined) {this.selectable = options.selectable;} if (options.selectable !== undefined) {this.selectable = options.selectable;}
/*
if (options.physics) { if (options.physics) {
if (options.physics.barnesHut) { if (options.physics.barnesHut) {
this.constants.physics.barnesHut.enabled = true; this.constants.physics.barnesHut.enabled = true;
@ -437,7 +436,7 @@ Graph.prototype.setOptions = function (options) {
} }
} }
} }
*/
if (options.clustering) { if (options.clustering) {
this.constants.clustering.enabled = true; this.constants.clustering.enabled = true;
for (var prop in options.clustering) { 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); var node = new Node(data, this.images, this.groups, this.constants);
this.nodes[id] = node; // note: this may replace an existing node 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 // note: no not use node.isMoving() here, as that gives the current
// velocity of the node, which is zero after creation of the node. // 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.moving = true;
this._updateValueRange(edges); this._updateValueRange(edges);
}; };
@ -1446,6 +1444,9 @@ Graph.prototype._removeEdges = function (ids) {
var id = ids[i]; var id = ids[i];
var edge = edges[id]; var edge = edges[id];
if (edge) { if (edge) {
if (edge.via != null) {
delete this.sectors['support']['nodes'][edge.via.id];
}
edge.disconnect(); edge.disconnect();
delete edges[id]; delete edges[id];
} }
@ -1453,6 +1454,7 @@ Graph.prototype._removeEdges = function (ids) {
this.moving = true; this.moving = true;
this._updateValueRange(edges); this._updateValueRange(edges);
this._setCalculationNodes();
}; };
/** /**
@ -1741,10 +1743,9 @@ Graph.prototype._doStabilize = function() {
* @private * @private
*/ */
Graph.prototype._isMoving = function(vmin) { Graph.prototype._isMoving = function(vmin) {
var vminCorrected = vmin / Math.max(this.scale,0.05);
var nodes = this.nodes; var nodes = this.nodes;
for (var id in nodes) { for (var id in nodes) {
if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vminCorrected)) {
if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
return true; return true;
} }
} }
@ -1760,7 +1761,7 @@ Graph.prototype._isMoving = function(vmin) {
* @private * @private
*/ */
Graph.prototype._discreteStepNodes = function() { Graph.prototype._discreteStepNodes = function() {
var interval = 1.2;
var interval = 1.0;
var nodes = this.nodes; var nodes = this.nodes;
if (this.constants.maxVelocity > 0) { 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); graph._zoom(graph.scale*(1 + graph.zoomIncrement), center);
} }
var calctimeStart = Date.now();
// var calctimeStart = Date.now();
graph.start(); graph.start();
graph.start(); graph.start();
var calctime = Date.now() - calctimeStart;
var rendertimeStart = Date.now();
// var calctime = Date.now() - calctimeStart;
// var rendertimeStart = Date.now();
graph._redraw(); graph._redraw();
var rendertime = Date.now() - rendertimeStart;
// var rendertime = Date.now() - rendertimeStart;
//this.end = window.performance.now(); //this.end = window.performance.now();
//this.time = this.end - this.startTime; //this.time = this.end - this.startTime;
@ -1882,25 +1888,27 @@ Graph.prototype.toggleFreeze = function() {
Graph.prototype._createBezierNodes = 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.dampingBase = 0.9;
this.damping = 0.9; // this is manipulated in the updateDamping function this.damping = 0.9; // this is manipulated in the updateDamping function
this.mass = 1; // kg
this.setProperties(properties, constants); this.setProperties(properties, constants);
@ -74,7 +75,7 @@ function Node(properties, imagelist, grouplist, constants) {
this.growthIndicator = 0; this.growthIndicator = 0;
// mass, force, velocity // mass, force, velocity
this.mass = 1; // kg
this.fx = 0.0; // external force x this.fx = 0.0; // external force x
this.fy = 0.0; // external force y this.fy = 0.0; // external force y
this.vx = 0.0; // velocity x this.vx = 0.0; // velocity x
@ -150,6 +151,8 @@ Node.prototype.setProperties = function(properties, constants) {
// physics // physics
if (properties.internalMultiplier !== undefined) {this.internalMultiplier = properties.internalMultiplier;} if (properties.internalMultiplier !== undefined) {this.internalMultiplier = properties.internalMultiplier;}
if (properties.damping !== undefined) {this.dampingBase = properties.damping;} if (properties.damping !== undefined) {this.dampingBase = properties.damping;}
if (properties.mass !== undefined) {this.mass = properties.mass;}
// navigation controls properties // navigation controls properties
if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;} if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;} if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;}
@ -984,7 +987,7 @@ Node.prototype.setScale = function(scale) {
* @param {Number} numberOfNodes * @param {Number} numberOfNodes
*/ */
Node.prototype.updateDamping = function() { 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 = { 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() { 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 * This function clusters until the initialMaxNodes has been reached
@ -56,7 +57,7 @@ var ClusterMixin = {
if (level > 0 && reposition == true) { if (level > 0 && reposition == true) {
this.repositionNodes(); this.repositionNodes();
} }
},
},
/** /**
* This function can be called to open up a specific cluster. It is only called by * 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; var isMovingBeforeClustering = this.moving;
if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) && if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) &&
!(this._sector() == "default" && this.nodeIndices.length == 1)) { !(this._sector() == "default" && this.nodeIndices.length == 1)) {
// this loads a new sector, loads the nodes and edges and nodeIndices of it.
this._addSector(node); this._addSector(node);
var level = 0; var level = 0;
// we decluster until we reach a decent number of nodes
while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) { while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) {
this.decreaseClusterLevel(); this.decreaseClusterLevel();
level += 1; level += 1;
} }
} }
else { else {
this._expandClusterNode(node,false,true); this._expandClusterNode(node,false,true);
@ -89,7 +94,7 @@ var ClusterMixin = {
if (this.moving != isMovingBeforeClustering) { if (this.moving != isMovingBeforeClustering) {
this.start(); this.start();
} }
},
},
/** /**
@ -109,7 +114,7 @@ var ClusterMixin = {
*/ */
increaseClusterLevel : function() { increaseClusterLevel : function() {
this.updateClusters(-1,false,true); this.updateClusters(-1,false,true);
},
},
/** /**
@ -119,7 +124,7 @@ var ClusterMixin = {
*/ */
decreaseClusterLevel : function() { decreaseClusterLevel : function() {
this.updateClusters(1,false,true); this.updateClusters(1,false,true);
},
},
/** /**
@ -192,7 +197,9 @@ var ClusterMixin = {
this.start(); this.start();
} }
} }
},
this._setCalculationNodes();
},
/** /**
* This function handles the chains. It is called on every updateClusters(). * 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._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage)
} }
},
},
/** /**
* this functions starts clustering by hubs * this functions starts clustering by hubs
@ -215,7 +222,7 @@ var ClusterMixin = {
_aggregateHubs : function(force) { _aggregateHubs : function(force) {
this._getHubSize(); this._getHubSize();
this._formClustersByHub(force,false); this._formClustersByHub(force,false);
},
},
/** /**
@ -244,7 +251,7 @@ var ClusterMixin = {
this.start(); this.start();
} }
} }
},
},
/** /**
* If a cluster takes up more than a set percentage of the screen, open the cluster * 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._expandClusterNode(node,recursive,force);
this._setCalculationNodes(); this._setCalculationNodes();
} }
},
},
/** /**
* This function checks if a node has to be opened. This is done by checking the zoom level. * 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 * 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 * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
* @private * @private
*/ */
_expelChildFromParent : function(parentNode, containedNodeId, recursive, force, openAll) {
_expelChildFromParent : function(parentNode, containedNodeId, recursive, force, openAll) {
var childNode = parentNode.containedNodes[containedNodeId]; var childNode = parentNode.containedNodes[containedNodeId];
// if child node has been added on smaller scale than current, kick out // if child node has been added on smaller scale than current, kick out
if (childNode.formationScale < this.scale || force == true) { 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 // put the child node back in the global nodes object
this.nodes[containedNodeId] = childNode; this.nodes[containedNodeId] = childNode;
@ -370,8 +375,8 @@ var ClusterMixin = {
parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length; parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
// place the child node near the parent, not at the exact same location to avoid chaos in the system // 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 // remove node from the list
delete parentNode.containedNodes[containedNodeId]; delete parentNode.containedNodes[containedNodeId];
@ -391,24 +396,37 @@ var ClusterMixin = {
parentNode.clusterSessions.pop(); parentNode.clusterSessions.pop();
} }
this._repositionBezierNodes(childNode);
// this._repositionBezierNodes(parentNode);
// remove the clusterSession from the child node // remove the clusterSession from the child node
childNode.clusterSession = 0; 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 // recalculate the size of the node on the next time the node is rendered
parentNode.clearSizeCache(); 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 // check if a further expansion step is possible if recursivity is enabled
if (recursive == true) { if (recursive == true) {
this._expandClusterNode(childNode,recursive,force,openAll); 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 { else {
this._forceClustersByZoom(); this._forceClustersByZoom();
} }
},
},
/** /**
* This function handles the clustering by zooming out, this is based on a minimum edge distance * 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 * 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) { _clusterToSmallestNeighbour : function(node) {
var smallestNeighbour = -1; var smallestNeighbour = -1;
var smallestNeighbourNode = null; var smallestNeighbourNode = null;
@ -546,7 +572,7 @@ var ClusterMixin = {
this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual); this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual);
} }
} }
},
},
/** /**
* This function forms a cluster from a specific preselected hub node * 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 // restart the simulation to reorganise all nodes
this.moving = true; this.moving = true;
},
},
/** /**
@ -717,7 +743,7 @@ var ClusterMixin = {
} }
node.dynamicEdgesLength -= correction; node.dynamicEdgesLength -= correction;
} }
},
},
/** /**
@ -746,7 +772,7 @@ var ClusterMixin = {
break; break;
} }
} }
},
},
/** /**
* This function connects an edge that was connected to a child node to the parent node. * 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); 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) { _containCircularEdgesFromNode : function(parentNode, childNode) {
// manage all the edges connected to the child and parent nodes // manage all the edges connected to the child and parent nodes
for (var i = 0; i < parentNode.dynamicEdges.length; i++) { for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
@ -850,7 +884,7 @@ var ClusterMixin = {
// remove the entry from the rerouted edges // remove the entry from the rerouted edges
delete parentNode.reroutedEdges[childNode.id]; delete parentNode.reroutedEdges[childNode.id];
} }
},
},
/** /**
@ -868,7 +902,7 @@ var ClusterMixin = {
parentNode.dynamicEdges.splice(i,1); parentNode.dynamicEdges.splice(i,1);
} }
} }
},
},
/** /**
@ -893,7 +927,7 @@ var ClusterMixin = {
// remove the entry from the contained edges // remove the entry from the contained edges
delete parentNode.containedEdges[childNode.id]; 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() { normalizeClusterLevels : function() {
var maxLevel = 0; var maxLevel = 0;
var minLevel = 1e9; var minLevel = 1e9;
@ -992,7 +1032,7 @@ var ClusterMixin = {
&& &&
Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
) )
},
},
/** /**
@ -1003,17 +1043,14 @@ var ClusterMixin = {
repositionNodes : function() { repositionNodes : function() {
for (var i = 0; i < this.nodeIndices.length; i++) { for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[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 radius = this.constants.physics.springLength * (1 + 0.1*node.mass);
var angle = 2 * Math.PI * Math.random(); 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("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
// console.log("hubThreshold:",this.hubThreshold); // 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 * 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; return chains/total;
}
}
}; };

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

@ -5,12 +5,19 @@
var physicsMixin = { var physicsMixin = {
/**
* Toggling barnes Hut calculation on and off.
*
* @private
*/
_toggleBarnesHut : function() { _toggleBarnesHut : function() {
this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled; this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled;
this._loadSelectedForceSolver(); this._loadSelectedForceSolver();
this.moving = true; this.moving = true;
this.start(); this.start();
}, },
/** /**
* Before calculating the forces, we check if we need to cluster to keep up performance and we check * 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. * 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) { if (this.constants.smoothCurves == true) {
this._calculateSpringForcesOnSupport();
this._calculateSpringForcesWithSupport();
} }
else { else {
this._calculateSpringForces(); 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() { _setCalculationNodes : function() {
if (this.constants.smoothCurves == true) { if (this.constants.smoothCurves == true) {
this.calculationNodes = {}; this.calculationNodes = {};
@ -74,6 +90,9 @@ var physicsMixin = {
if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) { if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) {
this.calculationNodes[supportNodeId] = supportNodes[supportNodeId]; 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() { _calculateGravitationalForces : function() {
var dx, dy, angle, fx, fy, node, i; var dx, dy, angle, fx, fy, node, i;
var nodes = this.calculationNodes; var nodes = this.calculationNodes;
@ -110,8 +123,8 @@ var physicsMixin = {
node = nodes[this.calculationNodeIndices[i]]; node = nodes[this.calculationNodeIndices[i]];
// gravity does not apply when we are in a pocket sector // gravity does not apply when we are in a pocket sector
if (this._sector() == "default") { 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); angle = Math.atan2(dy, dx);
fx = Math.cos(angle) * gravity; 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() { _calculateSpringForces : function() {
var dx, dy, angle, fx, fy, springForce, length, edgeLength, edge, edgeId;
var edgeLength, edge, edgeId;
var edges = this.edges; var edges = this.edges;
// forces caused by the edges, modelled as springs // forces caused by the edges, modelled as springs
@ -137,30 +156,23 @@ var physicsMixin = {
if (edge.connected) { if (edge.connected) {
// only calculate forces if nodes are in the same sector // only calculate forces if nodes are in the same sector
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { 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; edgeLength = edge.length;
// this implies that the edges between big clusters are longer // this implies that the edges between big clusters are longer
edgeLength += (edge.to.growthIndicator + edge.from.growthIndicator) * this.constants.clustering.edgeGrowth; 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 edgeLength, edge, edgeId, growthIndicator;
var edges = this.edges; var edges = this.edges;
@ -177,11 +189,11 @@ var physicsMixin = {
var node3 = edge.from; var node3 = edge.from;
edgeLength = 0.5*edge.length; edgeLength = 0.5*edge.length;
growthIndicator = 0.5*(node1.growthIndicator + node3.growthIndicator); growthIndicator = 0.5*(node1.growthIndicator + node3.growthIndicator);
// this implies that the edges between big clusters are longer // this implies that the edges between big clusters are longer
edgeLength += growthIndicator * this.constants.clustering.edgeGrowth; edgeLength += growthIndicator * this.constants.clustering.edgeGrowth;
this._calculateSpringForce(node1,node2,edgeLength); this._calculateSpringForce(node1,node2,edgeLength);
this._calculateSpringForce(node2,node3,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) { _calculateSpringForce : function(node1,node2,edgeLength) {
var dx, dy, angle, fx, fy, springForce, length; var dx, dy, angle, fx, fy, springForce, length;
@ -198,6 +219,7 @@ var physicsMixin = {
dy = (node1.y - node2.y); dy = (node1.y - node2.y);
length = Math.sqrt(dx * dx + dy * dy); length = Math.sqrt(dx * dx + dy * dy);
angle = Math.atan2(dy, dx); angle = Math.atan2(dy, dx);
springForce = this.constants.physics.springConstant * (edgeLength - length); springForce = this.constants.physics.springConstant * (edgeLength - length);
fx = Math.cos(angle) * springForce; fx = Math.cos(angle) * springForce;

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

@ -4,7 +4,12 @@
var barnesHutMixin = { 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() { _calculateNodeForces : function() {
var node; var node;
var nodes = this.calculationNodes; var nodes = this.calculationNodes;
@ -13,7 +18,6 @@ var barnesHutMixin = {
this._formBarnesHutTree(nodes,nodeIndices); this._formBarnesHutTree(nodes,nodeIndices);
var barnesHutTree = this.barnesHutTree; var barnesHutTree = this.barnesHutTree;
// place the nodes one by one recursively // 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) { _getForceContribution : function(parentBranch,node) {
// we get no force contribution from an empty region // we get no force contribution from an empty region
if (parentBranch.childrenCount > 0) { if (parentBranch.childrenCount > 0) {
var dx,dy,distance; var dx,dy,distance;
// get the distance from the center of mass to the node. // 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); distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) { // distance is 0 if it looks to apply a force on itself. 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) { _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). // 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 gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance);
var angle = Math.atan2(dy, dx); 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) { _formBarnesHutTree : function(nodes,nodeIndices) {
var node; var node;
var nodeCount = nodeIndices.length; var nodeCount = nodeIndices.length;
@ -95,13 +124,19 @@ var barnesHutMixin = {
else {minX += 0.5 * sizeDiff; maxX -= 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 // construct the barnesHutTree
var barnesHutTree = {root:{ var barnesHutTree = {root:{
CenterOfMass:{x:0,y:0}, // Center of Mass
centerOfMass:{x:0,y:0}, // Center of Mass
mass:0, 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}, children: {data:null},
maxWidth: 0, maxWidth: 0,
level: 0, level: 0,
@ -124,11 +159,11 @@ var barnesHutMixin = {
var totalMass = parentBranch.mass + node.mass; var totalMass = parentBranch.mass + node.mass;
var totalMassInv = 1/totalMass; 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; parentBranch.mass = totalMass;
var biggestSize = Math.max(Math.max(node.height,node.radius),node.width); 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.maxX > node.x) { // in NW or SW
if (parentBranch.children.NW.range.maxY > node.y) { // in NW if (parentBranch.children.NW.range.maxY > node.y) { // in NW
this._placeInRegion(parentBranch,node,"NW"); this._placeInRegion(parentBranch,node,"NW");
@ -150,7 +187,7 @@ var barnesHutMixin = {
} }
} }
else { // in NE or SE 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"); this._placeInRegion(parentBranch,node,"NE");
} }
else { // in SE else { // in SE
@ -168,8 +205,16 @@ var barnesHutMixin = {
this._updateBranchMass(parentBranch.children[region],node); this._updateBranchMass(parentBranch.children[region],node);
break; break;
case 1: // convert into children 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; break;
case 4: // place in branch case 4: // place in branch
this._placeInTree(parentBranch.children[region],node); 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) { _splitBranch : function(parentBranch) {
// if the branch is filled with a node, replace the node in the new subset. // if the branch is filled with a node, replace the node in the new subset.
var containedNode = null; var containedNode = null;
if (parentBranch.childrenCount == 1) { if (parentBranch.childrenCount == 1) {
containedNode = parentBranch.children.data; 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.childrenCount = 4;
parentBranch.children.data = null; parentBranch.children.data = null;
@ -210,47 +262,56 @@ var barnesHutMixin = {
*/ */
_insertRegion : function(parentBranch, region) { _insertRegion : function(parentBranch, region) {
var minX,maxX,minY,maxY; var minX,maxX,minY,maxY;
var childSize = 0.5 * parentBranch.size;
switch (region) { switch (region) {
case "NW": case "NW":
minX = parentBranch.range.minX; minX = parentBranch.range.minX;
maxX = parentBranch.range.minX + parentBranch.size;
maxX = parentBranch.range.minX + childSize;
minY = parentBranch.range.minY; minY = parentBranch.range.minY;
maxY = parentBranch.range.minY + parentBranch.size;
maxY = parentBranch.range.minY + childSize;
break; break;
case "NE": case "NE":
minX = parentBranch.range.minX + parentBranch.size;
minX = parentBranch.range.minX + childSize;
maxX = parentBranch.range.maxX; maxX = parentBranch.range.maxX;
minY = parentBranch.range.minY; minY = parentBranch.range.minY;
maxY = parentBranch.range.minY + parentBranch.size;
maxY = parentBranch.range.minY + childSize;
break; break;
case "SW": case "SW":
minX = parentBranch.range.minX; 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; maxY = parentBranch.range.maxY;
break; break;
case "SE": case "SE":
minX = parentBranch.range.minX + parentBranch.size;
minX = parentBranch.range.minX + childSize;
maxX = parentBranch.range.maxX; maxX = parentBranch.range.maxX;
minY = parentBranch.range.minY + parentBranch.size;
minY = parentBranch.range.minY + childSize;
maxY = parentBranch.range.maxY; maxY = parentBranch.range.maxY;
break; break;
} }
parentBranch.children[region] = { parentBranch.children[region] = {
CenterOfMass:{x:0,y:0},
centerOfMass:{x:0,y:0},
mass:0, mass:0,
range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY}, range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
size: 0.5 * parentBranch.size, size: 0.5 * parentBranch.size,
calcSize: 2 * parentBranch.calcSize, calcSize: 2 * parentBranch.calcSize,
children: {data:null}, children: {data:null},
maxWidth: 0, maxWidth: 0,
level: parentBranch.level +1,
level: parentBranch.level+1,
childrenCount: 0 childrenCount: 0
}; };
}, },
/**
* This function is for debugging purposed, it draws the tree.
*
* @param ctx
* @param color
* @private
*/
_drawTree : function(ctx,color) { _drawTree : function(ctx,color) {
if (this.barnesHutTree !== undefined) { 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) { _drawBranch : function(branch,ctx,color) {
if (color === undefined) { if (color === undefined) {
color = "#FF0000"; color = "#FF0000";
@ -294,7 +364,7 @@ var barnesHutMixin = {
/* /*
if (branch.mass > 0) { 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(); ctx.stroke();
} }
*/ */

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

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

Loading…
Cancel
Save