Browse Source

fixed BUG! barnes hut functional but slower than almende linear approx algorithm. Now to fix the naming of functions and explain code...

css_transitions
Alex de Mulder 10 years ago
parent
commit
da444af45e
7 changed files with 70 additions and 71 deletions
  1. +1
    -1
      .gitignore
  2. +3
    -3
      examples/graph/02_random_nodes.html
  3. +2
    -2
      src/graph/ClusterMixin.js
  4. +1
    -3
      src/graph/Edge.js
  5. +27
    -10
      src/graph/Graph.js
  6. +4
    -2
      src/graph/Node.js
  7. +32
    -50
      src/graph/physicsMixin.js

+ 1
- 1
.gitignore View File

@ -1,8 +1,8 @@
.idea .idea
node_modules node_modules
.project .project
dist
.settings/.jsdtscope .settings/.jsdtscope
.settings/org.eclipse.wst.jsdt.ui.superType.container .settings/org.eclipse.wst.jsdt.ui.superType.container
.settings/org.eclipse.wst.jsdt.ui.superType.name .settings/org.eclipse.wst.jsdt.ui.superType.name
npm-debug.log npm-debug.log
dist/

+ 3
- 3
examples/graph/02_random_nodes.html View File

@ -90,8 +90,7 @@
edges: { edges: {
length: 50 length: 50
}, },
stabilize: false,
clustering : true
stabilize: false
}; };
graph = new vis.Graph(container, data, options); graph = new vis.Graph(container, data, options);
@ -104,7 +103,8 @@
</head> </head>
<body onload="draw();"> <body onload="draw();">
calculation time:<span id="calctimereporter" style="display:inline;"></span> ms
render time:<span id="rendertimereporter"style="display:inline;"></span> ms
<form onsubmit="draw(); return false;"> <form onsubmit="draw(); return false;">
<label for="nodeCount">Number of nodes:</label> <label for="nodeCount">Number of nodes:</label>
<input id="nodeCount" type="text" value="25" style="width: 50px;"> <input id="nodeCount" type="text" value="25" style="width: 50px;">

+ 2
- 2
src/graph/ClusterMixin.js View File

@ -618,7 +618,7 @@ var ClusterMixin = {
// update the properties of the child and parent // update the properties of the child and parent
var massBefore = parentNode.mass; var massBefore = parentNode.mass;
childNode.clusterSession = this.clusterSession; childNode.clusterSession = this.clusterSession;
parentNode.mass += childNode.mass;
parentNode.mass += this.constants.clustering.massTransferCoefficient * childNode.mass;
parentNode.clusterSize += childNode.clusterSize; parentNode.clusterSize += childNode.clusterSize;
parentNode.fontSize += this.constants.clustering.fontSizeMultiplier * childNode.clusterSize; parentNode.fontSize += this.constants.clustering.fontSizeMultiplier * childNode.clusterSize;
@ -931,7 +931,7 @@ var ClusterMixin = {
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.isFixed()) {
var radius = this.constants.physics.springLength * (1 + 0.6*node.clusterSize);
var radius = this.constants.edges.length * (1 + 0.6*node.clusterSize);
var angle = 2 * Math.PI * Math.random(); var angle = 2 * Math.PI * Math.random();
node.x = radius * Math.cos(angle); node.x = radius * Math.cos(angle);
node.y = radius * Math.sin(angle); node.y = radius * Math.sin(angle);

+ 1
- 3
src/graph/Edge.js View File

@ -31,7 +31,7 @@ function Edge (properties, graph, constants) {
this.title = undefined; this.title = undefined;
this.width = constants.edges.width; this.width = constants.edges.width;
this.value = undefined; this.value = undefined;
this.length = constants.edges.length;
this.length = constants.physics.springLength;
this.selected = false; this.selected = false;
this.from = null; // a node this.from = null; // a node
@ -49,7 +49,6 @@ function Edge (properties, graph, constants) {
// 2012-08-08 // 2012-08-08
this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
this.springConstant = constants.physics.springConstant;
this.color = constants.edges.color; this.color = constants.edges.color;
this.widthFixed = false; this.widthFixed = false;
this.lengthFixed = false; this.lengthFixed = false;
@ -103,7 +102,6 @@ Edge.prototype.setProperties = function(properties, constants) {
this.widthFixed = this.widthFixed || (properties.width !== undefined); this.widthFixed = this.widthFixed || (properties.width !== undefined);
this.lengthFixed = this.lengthFixed || (properties.length !== undefined); this.lengthFixed = this.lengthFixed || (properties.length !== undefined);
this.stiffness = 1 / this.length;
// set draw method based on style // set draw method based on style
switch (this.style) { switch (this.style) {

+ 27
- 10
src/graph/Graph.js View File

@ -66,11 +66,12 @@ function Graph (container, data, options) {
} }
}, },
physics: { physics: {
springConstant:0.05,
springLength: 100,
centralGravity: 0.1,
nodeGravityConstant: -10000,
barnesHutTheta: 0.2
enableBarnesHut: false,
barnesHutTheta: 1 / 0.4, // inverted to save time during calculation
barnesHutGravitationalConstant: -10000,
centralGravity: 0.08,
springLength: 50,
springConstant: 0.02
}, },
clustering: { // Per Node in Cluster = PNiC clustering: { // Per Node in Cluster = PNiC
enabled: false, // (Boolean) | global on/off switch for clustering. enabled: false, // (Boolean) | global on/off switch for clustering.
@ -88,7 +89,8 @@ function Graph (container, data, options) {
nodeScaling: {width: 10, // (px PNiC) | growth of the width per node in cluster. nodeScaling: {width: 10, // (px PNiC) | growth of the width per node in cluster.
height: 10, // (px PNiC) | growth of the height per node in cluster. height: 10, // (px PNiC) | growth of the height per node in cluster.
radius: 10}, // (px PNiC) | growth of the radius per node in cluster. radius: 10}, // (px PNiC) | growth of the radius per node in cluster.
activeAreaBoxSize: 100 // (px) | box area around the curser where clusters are popped open.
activeAreaBoxSize: 100, // (px) | box area around the curser where clusters are popped open.
massTransferCoefficient: 1 // (multiplier) | parent.mass += massTransferCoefficient * child.mass
}, },
navigation: { navigation: {
enabled: false, enabled: false,
@ -101,7 +103,7 @@ function Graph (container, data, options) {
dataManipulationToolbar: { dataManipulationToolbar: {
enabled: false enabled: false
}, },
minVelocity: 0.2, // px/s
minVelocity: 0.1, // px/s
maxIterations: 1000 // maximum number of iteration to stabilize maxIterations: 1000 // maximum number of iteration to stabilize
}; };
@ -642,7 +644,7 @@ Graph.prototype._createKeyBinds = function() {
this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown"); this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown");
this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup"); this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup");
} }
this.mousetrap.bind("b",this._formBarnesHutTree.bind(me));
this.mousetrap.bind("b",this._toggleBarnesHut.bind(me));
if (this.constants.dataManipulationToolbar.enabled == true) { if (this.constants.dataManipulationToolbar.enabled == true) {
this.mousetrap.bind("escape",this._createManipulatorBar.bind(me)); this.mousetrap.bind("escape",this._createManipulatorBar.bind(me));
@ -1502,7 +1504,7 @@ Graph.prototype._redraw = function() {
this._doInAllSectors("_drawEdges",ctx); this._doInAllSectors("_drawEdges",ctx);
this._doInAllSectors("_drawNodes",ctx,true); this._doInAllSectors("_drawNodes",ctx,true);
this._drawTree(ctx,"#F00F0F");
// this._drawTree(ctx,"#F00F0F");
// restore original scaling and translation // restore original scaling and translation
ctx.restore(); ctx.restore();
@ -1710,7 +1712,7 @@ Graph.prototype._isMoving = function(vmin) {
* @private * @private
*/ */
Graph.prototype._discreteStepNodes = function() { Graph.prototype._discreteStepNodes = function() {
var interval = 0.5;
var interval = 1;
var nodes = this.nodes; var nodes = this.nodes;
this.constants.maxVelocity = 30; this.constants.maxVelocity = 30;
@ -1769,14 +1771,28 @@ Graph.prototype.start = function() {
graph._zoom(graph.scale*(1 + graph.zoomIncrement), center); graph._zoom(graph.scale*(1 + graph.zoomIncrement), center);
} }
var calctimeStart = Date.now();
graph.start(); graph.start();
graph.start();
var calctime = Date.now() - calctimeStart;
var rendertimeStart = Date.now();
graph._redraw(); graph._redraw();
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;
//console.log('refresh time: ' + this.time); //console.log('refresh time: ' + this.time);
//this.startTime = window.performance.now(); //this.startTime = window.performance.now();
var DOMelement = document.getElementById("calctimereporter");
if (DOMelement !== undefined) {
DOMelement.innerHTML = calctime;
}
DOMelement = document.getElementById("rendertimereporter");
if (DOMelement !== undefined) {
DOMelement.innerHTML = rendertime;
}
}, this.renderTimestep); }, this.renderTimestep);
} }
} }
@ -1823,6 +1839,7 @@ Graph.prototype.toggleFreeze = function() {
* @private * @private
*/ */
Graph.prototype._loadPhysicsSystem = function() { Graph.prototype._loadPhysicsSystem = function() {
this.forceCalculationTime = 0;
for (var mixinFunction in physicsMixin) { for (var mixinFunction in physicsMixin) {
if (physicsMixin.hasOwnProperty(mixinFunction)) { if (physicsMixin.hasOwnProperty(mixinFunction)) {
Graph.prototype[mixinFunction] = physicsMixin[mixinFunction]; Graph.prototype[mixinFunction] = physicsMixin[mixinFunction];

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

@ -993,8 +993,10 @@ Node.prototype.clearVelocity = function() {
*/ */
Node.prototype.updateVelocity = function(massBeforeClustering) { Node.prototype.updateVelocity = function(massBeforeClustering) {
var energyBefore = this.vx * this.vx * massBeforeClustering; var energyBefore = this.vx * this.vx * massBeforeClustering;
this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
//this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
this.vx = Math.sqrt(energyBefore/this.mass);
energyBefore = this.vy * this.vy * massBeforeClustering; energyBefore = this.vy * this.vy * massBeforeClustering;
this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
//this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
this.vy = Math.sqrt(energyBefore/this.mass);
}; };

+ 32
- 50
src/graph/physicsMixin.js View File

@ -5,6 +5,12 @@
var physicsMixin = { var physicsMixin = {
_toggleBarnesHut : function() {
this.constants.physics.enableBarnesHut = !this.constants.physics.enableBarnesHut;
this.moving = true;
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.
@ -23,8 +29,13 @@ var physicsMixin = {
} }
// we now start the force calculation // we now start the force calculation
// this._calculateForcesBarnesHut();
this._calculateForcesOriginal();
if (this.constants.physics.enableBarnesHut == true) {
this._calculateForcesBarnesHut();
}
else {
this.barnesHutTree = undefined;
this._calculateForcesRepulsion();
}
} }
}, },
@ -34,28 +45,15 @@ var physicsMixin = {
* Forces are caused by: edges, repulsing forces between nodes, gravity * Forces are caused by: edges, repulsing forces between nodes, gravity
* @private * @private
*/ */
_calculateForcesOriginal : function() {
_calculateForcesRepulsion : function() {
// Gravity is required to keep separated groups from floating off // Gravity is required to keep separated groups from floating off
// the forces are reset to zero in this loop by using _setForce instead // the forces are reset to zero in this loop by using _setForce instead
// of _addForce // of _addForce
this._calculateGravitationalForces();
// var startTimeAll = Date.now();
this._calculateGravitationalForces(1);
// var startTimeRepulsion = Date.now();
// All nodes repel eachother.
this._calculateRepulsionForces(); this._calculateRepulsionForces();
// var endTimeRepulsion = Date.now();
// the edges are strings
this._calculateSpringForces(1);
// var endTimeAll = Date.now();
// echo("Time repulsion part:", endTimeRepulsion - startTimeRepulsion);
// echo("Time total force calc:", endTimeAll - startTimeAll);
this._calculateSpringForces();
}, },
/** /**
@ -64,27 +62,11 @@ var physicsMixin = {
* @private * @private
*/ */
_calculateForcesBarnesHut : function() { _calculateForcesBarnesHut : 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
// var startTimeAll = Date.now();
this._clearForces();
this._calculateGravitationalForces();
// var startTimeRepulsion = Date.now();
// All nodes repel eachother.
this._calculateBarnesHutForces(); this._calculateBarnesHutForces();
// var endTimeRepulsion = Date.now();
// the edges are strings
this._calculateSpringForces(1);
// var endTimeAll = Date.now();
// echo("Time repulsion part:", endTimeRepulsion - startTimeRepulsion);
// echo("Time total force calc:", endTimeAll - startTimeAll);
this._calculateSpringForces();
}, },
@ -99,7 +81,7 @@ var physicsMixin = {
} }
}, },
_calculateGravitationalForces : function(boost) {
_calculateGravitationalForces : function() {
var dx, dy, angle, fx, fy, node, i; var dx, dy, angle, fx, fy, node, i;
var nodes = this.nodes; var nodes = this.nodes;
var gravity = this.constants.physics.centralGravity; var gravity = this.constants.physics.centralGravity;
@ -135,7 +117,6 @@ var physicsMixin = {
// repulsing forces between nodes // repulsing forces between nodes
var minimumDistance = this.constants.nodes.distance; var minimumDistance = this.constants.nodes.distance;
//var steepness = 10;
// we loop from i over all but the last entree in the array // 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 // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
@ -148,8 +129,6 @@ var physicsMixin = {
dy = node2.y - node1.y; dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy); distance = Math.sqrt(dx * dx + dy * dy);
// clusters have a larger region of influence
minimumDistance = (clusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + clusterSize * this.constants.clustering.distanceAmplification)); minimumDistance = (clusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + clusterSize * this.constants.clustering.distanceAmplification));
var a = a_base / minimumDistance; var a = a_base / minimumDistance;
if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045 if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045
@ -174,7 +153,7 @@ var physicsMixin = {
} }
}, },
_calculateSpringForces : function(boost) {
_calculateSpringForces : function() {
var dx, dy, angle, fx, fy, springForce, length, edgeLength, edge, edgeId, clusterSize; var dx, dy, angle, fx, fy, springForce, length, edgeLength, edge, edgeId, clusterSize;
var edges = this.edges; var edges = this.edges;
@ -196,7 +175,7 @@ var physicsMixin = {
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 = edge.springConstant * (edgeLength - length);
springForce = this.constants.physics.springConstant * (edgeLength - length);
fx = Math.cos(angle) * springForce; fx = Math.cos(angle) * springForce;
fy = Math.sin(angle) * springForce; fy = Math.sin(angle) * springForce;
@ -241,11 +220,8 @@ var physicsMixin = {
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.
// we invert it here because we need the inverted distance for the force calculation too.
var distanceInv = 1/distance;
// BarnesHut condition // BarnesHut condition
if (parentBranch.size * distanceInv > this.constants.physics.barnesHutTheta) {
if (distance * parentBranch.calcSize < this.constants.physics.barnesHutTheta) {
// Did not pass the condition, go into children if available // Did not pass the condition, go into children if available
if (parentBranch.childrenCount == 4) { if (parentBranch.childrenCount == 4) {
this._getForceContribution(parentBranch.children.NW,node); this._getForceContribution(parentBranch.children.NW,node);
@ -255,20 +231,20 @@ var physicsMixin = {
} }
else { // parentBranch must have only one node, if it was empty we wouldnt be here 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 if (parentBranch.children.data.id != node.id) { // if it is not self
this._getForceOnNode(parentBranch, node, dx ,dy, distanceInv);
this._getForceOnNode(parentBranch, node, dx ,dy, distance);
} }
} }
} }
else { else {
this._getForceOnNode(parentBranch, node, dx ,dy, distanceInv);
this._getForceOnNode(parentBranch, node, dx ,dy, distance);
} }
} }
} }
}, },
_getForceOnNode : function(parentBranch, node, dx ,dy, distanceInv) {
_getForceOnNode : function(parentBranch, node, dx ,dy, 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.nodeGravityConstant * parentBranch.mass * node.mass * distanceInv * distanceInv;
var gravityForce = this.constants.physics.barnesHutGravitationalConstant * parentBranch.mass * node.mass / (distance * distance);
var angle = Math.atan2(dy, dx); var angle = Math.atan2(dy, dx);
var fx = Math.cos(angle) * gravityForce; var fx = Math.cos(angle) * gravityForce;
var fy = Math.sin(angle) * gravityForce; var fy = Math.sin(angle) * gravityForce;
@ -308,6 +284,7 @@ var physicsMixin = {
mass:0, mass:0,
range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY}, range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
size: Math.abs(maxX - minX), size: Math.abs(maxX - minX),
calcSize: 1 / Math.abs(maxX - minX),
children: {data:null}, children: {data:null},
level: 0, level: 0,
childrenCount: 4 childrenCount: 4
@ -324,6 +301,7 @@ var physicsMixin = {
this.barnesHutTree = barnesHutTree this.barnesHutTree = barnesHutTree
}, },
_updateBranchMass : function(parentBranch, node) { _updateBranchMass : function(parentBranch, node) {
var totalMass = parentBranch.mass + node.mass; var totalMass = parentBranch.mass + node.mass;
var totalMassInv = 1/totalMass; var totalMassInv = 1/totalMass;
@ -337,6 +315,7 @@ var physicsMixin = {
parentBranch.mass = totalMass; parentBranch.mass = totalMass;
}, },
_placeInTree : function(parentBranch,node) { _placeInTree : function(parentBranch,node) {
// update the mass of the branch. // update the mass of the branch.
this._updateBranchMass(parentBranch,node); this._updateBranchMass(parentBranch,node);
@ -359,6 +338,7 @@ var physicsMixin = {
} }
}, },
_placeInRegion : function(parentBranch,node,region) { _placeInRegion : function(parentBranch,node,region) {
switch (parentBranch.children[region].childrenCount) { switch (parentBranch.children[region].childrenCount) {
case 0: // place node here case 0: // place node here
@ -376,6 +356,7 @@ var physicsMixin = {
} }
}, },
_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;
@ -441,6 +422,7 @@ var physicsMixin = {
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,
children: {data:null}, children: {data:null},
level: parentBranch.level +1, level: parentBranch.level +1,
childrenCount: 0 childrenCount: 0

Loading…
Cancel
Save