diff --git a/examples/graph/02_random_nodes.html b/examples/graph/02_random_nodes.html
index 69ac60f8..e7ed640d 100755
--- a/examples/graph/02_random_nodes.html
+++ b/examples/graph/02_random_nodes.html
@@ -88,7 +88,7 @@
*/
var options = {
edges: {
- length: 50
+
},
stabilize: false
};
diff --git a/src/graph/Edge.js b/src/graph/Edge.js
index c5192f90..65c27f88 100644
--- a/src/graph/Edge.js
+++ b/src/graph/Edge.js
@@ -33,9 +33,11 @@ function Edge (properties, graph, constants) {
this.value = undefined;
this.length = constants.physics.springLength;
this.selected = false;
+ this.smooth = constants.smoothCurves;
this.from = null; // a node
this.to = null; // a node
+ this.via = null; // a temp node
// we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
// by storing the original information we can revert to the original connection when the cluser is opened.
@@ -54,7 +56,6 @@ function Edge (properties, graph, constants) {
this.lengthFixed = false;
this.setProperties(properties, constants);
-
}
/**
@@ -73,6 +74,7 @@ Edge.prototype.setProperties = function(properties, constants) {
if (properties.id !== undefined) {this.id = properties.id;}
if (properties.style !== undefined) {this.style = properties.style;}
if (properties.label !== undefined) {this.label = properties.label;}
+
if (this.label) {
this.fontSize = constants.edges.fontSize;
this.fontFace = constants.edges.fontFace;
@@ -81,6 +83,7 @@ Edge.prototype.setProperties = function(properties, constants) {
if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
}
+
if (properties.title !== undefined) {this.title = properties.title;}
if (properties.width !== undefined) {this.width = properties.width;}
if (properties.value !== undefined) {this.value = properties.value;}
@@ -284,7 +287,12 @@ Edge.prototype._line = function (ctx) {
// draw a straight line
ctx.beginPath();
ctx.moveTo(this.from.x, this.from.y);
- ctx.lineTo(this.to.x, this.to.y);
+ if (this.smooth == true) {
+ ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
+ }
+ else {
+ ctx.lineTo(this.to.x, this.to.y);
+ }
ctx.stroke();
};
@@ -625,4 +633,11 @@ Edge.prototype.select = function() {
Edge.prototype.unselect = function() {
this.selected = false;
+}
+
+Edge.prototype.positionBezierNode = function() {
+ if (this.via !== null) {
+ this.via.x = 0.5 * (this.from.x + this.to.x);
+ this.via.y = 0.5 * (this.from.y + this.to.y);
+ }
}
\ No newline at end of file
diff --git a/src/graph/Graph.js b/src/graph/Graph.js
index ba319788..6801ed85 100644
--- a/src/graph/Graph.js
+++ b/src/graph/Graph.js
@@ -118,6 +118,7 @@ function Graph (container, data, options) {
dataManipulationToolbar: {
enabled: false
},
+ smoothCurves: true,
maxVelocity: 35,
minVelocity: 0.1, // px/s
maxIterations: 1000 // maximum number of iteration to stabilize
@@ -157,6 +158,9 @@ function Graph (container, data, options) {
var graph = this;
this.freezeSimulation = false;// freeze the simulation
+
+ this.calculationNodes = {};
+ this.calculationNodeIndices = [];
this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
this.nodes = {}; // object with Node objects
this.edges = {}; // object with Edge objects
@@ -1398,6 +1402,7 @@ Graph.prototype._addEdges = function (ids) {
this.moving = true;
this._updateValueRange(edges);
+ this._createBezierNodes();
};
/**
@@ -1545,8 +1550,9 @@ Graph.prototype._redraw = function() {
this._doInAllSectors("_drawAllSectorNodes",ctx);
this._doInAllSectors("_drawEdges",ctx);
- this._doInAllSectors("_drawNodes",ctx,true);
+ this._doInAllSectors("_drawNodes",ctx,false);
+// this._doInSupportSector("_drawNodes",ctx,true);
// this._drawTree(ctx,"#F00F0F");
// restore original scaling and translation
@@ -1660,7 +1666,6 @@ Graph.prototype._drawNodes = function(ctx,alwaysShow) {
if (alwaysShow === undefined) {
alwaysShow = false;
}
-
// first draw the unselected nodes
var nodes = this.nodes;
var selected = [];
@@ -1755,7 +1760,7 @@ Graph.prototype._isMoving = function(vmin) {
* @private
*/
Graph.prototype._discreteStepNodes = function() {
- var interval = 1;
+ var interval = 1.2;
var nodes = this.nodes;
if (this.constants.maxVelocity > 0) {
@@ -1789,6 +1794,9 @@ Graph.prototype.start = function() {
if (this.moving) {
this._doInAllActiveSectors("_initializeForceCalculation");
this._doInAllActiveSectors("_discreteStepNodes");
+ if (this.constants.smoothCurves) {
+ this._doInSupportSector("_discreteStepNodes");
+ }
this._findCenter(this._getRange())
}
@@ -1873,6 +1881,29 @@ 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();
+ }
+ }
+ }
+ }
+
+};
+
+
Graph.prototype._initializeMixinLoaders = function () {
for (var mixinFunction in graphMixinLoaders) {
if (graphMixinLoaders.hasOwnProperty(mixinFunction)) {
diff --git a/src/graph/Node.js b/src/graph/Node.js
index 33009663..c9f96466 100644
--- a/src/graph/Node.js
+++ b/src/graph/Node.js
@@ -58,6 +58,9 @@ function Node(properties, imagelist, grouplist, constants) {
this.grouplist = grouplist;
+ this.dampingBase = 0.9;
+ this.damping = 0.9; // this is manipulated in the updateDamping function
+
this.setProperties(properties, constants);
// creating the variables for clustering
@@ -77,11 +80,11 @@ function Node(properties, imagelist, grouplist, constants) {
this.vx = 0.0; // velocity x
this.vy = 0.0; // velocity y
this.minForce = constants.minForce;
- this.damping = 0.9; // this is manipulated in the updateDamping function
this.graphScaleInv = 1;
this.canvasTopLeft = {"x": -300, "y": -300};
this.canvasBottomRight = {"x": 300, "y": 300};
+ this.parentEdgeId = null;
}
/**
@@ -143,10 +146,10 @@ Node.prototype.setProperties = function(properties, constants) {
if (properties.y !== undefined) {this.y = properties.y;}
if (properties.value !== undefined) {this.value = properties.value;}
- // physics
- if (properties.internalMultiplier !== undefined) {this.internalMultiplier = properties.value;}
-
+ // physics
+ if (properties.internalMultiplier !== undefined) {this.internalMultiplier = properties.internalMultiplier;}
+ if (properties.damping !== undefined) {this.dampingBase = properties.damping;}
// navigation controls properties
if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;}
@@ -591,7 +594,7 @@ Node.prototype._resizeBox = function (ctx) {
this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
- this.growthIndicator = this.width - textSize.width + 2 * margin;
+ this.growthIndicator = this.width - (textSize.width + 2 * margin);
// this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
}
@@ -693,7 +696,7 @@ Node.prototype._resizeCircle = function (ctx) {
// this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
// this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
- this.growthIndicator = this.radius - diameter;
+ this.growthIndicator = this.radius - 0.5*diameter;
}
};
@@ -869,7 +872,7 @@ Node.prototype._resizeText = function (ctx) {
this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
- this.growthIndicator = this.width - textSize.width + 2 * margin;
+ this.growthIndicator = this.width - (textSize.width + 2 * margin);
}
};
@@ -980,8 +983,8 @@ Node.prototype.setScale = function(scale) {
*
* @param {Number} numberOfNodes
*/
-Node.prototype.updateDamping = function(numberOfNodes) {
- this.damping = Math.min(1.5,0.9 + 0.01*this.growthIndicator);
+Node.prototype.updateDamping = function() {
+ this.damping = Math.min(Math.max(1.5,this.dampingBase),this.dampingBase + 0.01*this.growthIndicator);
};
diff --git a/src/graph/graphMixins/ClusterMixin.js b/src/graph/graphMixins/ClusterMixin.js
index d377e326..ccc8fbba 100644
--- a/src/graph/graphMixins/ClusterMixin.js
+++ b/src/graph/graphMixins/ClusterMixin.js
@@ -81,6 +81,7 @@ var ClusterMixin = {
// update the index list, dynamic edges and labels
this._updateNodeIndexList();
this._updateDynamicEdges();
+ this._setCalculationNodes();
this.updateLabels();
}
@@ -275,6 +276,7 @@ var ClusterMixin = {
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
this._expandClusterNode(node,recursive,force);
+ this._setCalculationNodes();
}
},
diff --git a/src/graph/graphMixins/MixinLoader.js b/src/graph/graphMixins/MixinLoader.js
index fd13ef13..062e65ad 100644
--- a/src/graph/graphMixins/MixinLoader.js
+++ b/src/graph/graphMixins/MixinLoader.js
@@ -65,6 +65,7 @@ var graphMixinLoaders = {
}
else {
this._clearMixin(barnesHutMixin);
+ this.barnesHutTree = undefined;
this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity;
this.constants.physics.springLength = this.constants.physics.repulsion.springLength;
@@ -101,13 +102,18 @@ var graphMixinLoaders = {
"edges":{},
"nodeIndices":[],
"formationScale": 1.0,
- "drawingNode": undefined },
+ "drawingNode": undefined };
this.sectors["frozen"] = { },
this.sectors["navigation"] = {"nodes":{},
"edges":{},
"nodeIndices":[],
"formationScale": 1.0,
- "drawingNode": undefined },
+ "drawingNode": undefined };
+ this.sectors["support"] = {"nodes":{},
+ "edges":{},
+ "nodeIndices":[],
+ "formationScale": 1.0,
+ "drawingNode": undefined };
this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
diff --git a/src/graph/graphMixins/SectorsMixin.js b/src/graph/graphMixins/SectorsMixin.js
index f242dcfb..1661c28c 100644
--- a/src/graph/graphMixins/SectorsMixin.js
+++ b/src/graph/graphMixins/SectorsMixin.js
@@ -56,6 +56,20 @@ var SectorMixin = {
},
+ /**
+ * This function sets the global references to nodes, edges and nodeIndices back to
+ * those of the supplied active sector.
+ *
+ * @param sectorId
+ * @private
+ */
+ _switchToSupportSector : function() {
+ this.nodeIndices = this.sectors["support"]["nodeIndices"];
+ this.nodes = this.sectors["support"]["nodes"];
+ this.edges = this.sectors["support"]["edges"];
+ },
+
+
/**
* This function sets the global references to nodes, edges and nodeIndices back to
* those of the supplied frozen sector.
@@ -349,6 +363,9 @@ var SectorMixin = {
// finally, we update the node index list.
this._updateNodeIndexList();
+
+ // we refresh the list with calulation nodes and calculation node indices.
+ this._setCalculationNodes();
}
}
},
@@ -393,6 +410,35 @@ var SectorMixin = {
},
+ /**
+ * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
+ *
+ * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
+ * | we dont pass the function itself because then the "this" is the window object
+ * | instead of the Graph object
+ * @param {*} [argument] | Optional: arguments to pass to the runFunction
+ * @private
+ */
+ _doInSupportSector : function(runFunction,argument) {
+ if (argument === undefined) {
+ this._switchToSupportSector();
+ this[runFunction]();
+ }
+ else {
+ this._switchToSupportSector();
+ var args = Array.prototype.splice.call(arguments, 1);
+ if (args.length > 1) {
+ this[runFunction](args[0],args[1]);
+ }
+ else {
+ this[runFunction](argument);
+ }
+ }
+ // we revert the global references back to our active sector
+ this._loadLatestSector();
+ },
+
+
/**
* This runs a function in all frozen sectors. This is used in the _redraw().
*
@@ -483,7 +529,6 @@ var SectorMixin = {
this._doInAllFrozenSectors(runFunction,argument);
}
}
-
},
diff --git a/src/graph/graphMixins/physics/PhysicsMixin.js b/src/graph/graphMixins/physics/PhysicsMixin.js
index 2f155f94..1f7dc85c 100644
--- a/src/graph/graphMixins/physics/PhysicsMixin.js
+++ b/src/graph/graphMixins/physics/PhysicsMixin.js
@@ -40,25 +40,74 @@ var physicsMixin = {
* @private
*/
_calculateForces : function() {
- this.barnesHutTree = undefined;
// Gravity is required to keep separated groups from floating off
// the forces are reset to zero in this loop by using _setForce instead
// of _addForce
+ this._setCalculationNodes();
+
this._calculateGravitationalForces();
this._calculateNodeForces();
- this._calculateSpringForces();
+
+ if (this.constants.smoothCurves == true) {
+ this._calculateSpringForcesOnSupport();
+ }
+ else {
+ this._calculateSpringForces();
+ }
},
+ _setCalculationNodes : function() {
+ if (this.constants.smoothCurves == true) {
+ this.calculationNodes = {};
+ this.calculationNodeIndices = [];
- _calculateGravitationalForces : function() {
- var dx, dy, angle, fx, fy, node, i;
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ this.calculationNodes[nodeId] = this.nodes[nodeId];
+ }
+ }
+ var supportNodes = this.sectors['support']['nodes'];
+ for (var supportNodeId in supportNodes) {
+ if (supportNodes.hasOwnProperty(supportNodeId)) {
+ if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) {
+ this.calculationNodes[supportNodeId] = supportNodes[supportNodeId];
+ }
+ }
+ }
+
+ for (var idx in this.calculationNodes) {
+ if (this.calculationNodes.hasOwnProperty(idx)) {
+ this.calculationNodeIndices.push(idx);
+ }
+ }
+ }
+ else {
+ this.calculationNodes = this.nodes;
+ this.calculationNodeIndices = this.nodeIndices;
+ }
+ },
+
+
+ _clearForces : function() {
+ var node, i;
var nodes = this.nodes;
- var gravity = this.constants.physics.centralGravity;
for (i = 0; i < this.nodeIndices.length; i++) {
node = nodes[this.nodeIndices[i]];
+ node._setForce(0, 0);
+ node.updateDamping(this.nodeIndices.length);
+ }
+ },
+
+ _calculateGravitationalForces : function() {
+ var dx, dy, angle, fx, fy, node, i;
+ var nodes = this.calculationNodes;
+ var gravity = this.constants.physics.centralGravity;
+
+ for (i = 0; i < this.calculationNodeIndices.length; i++) {
+ 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;
@@ -73,7 +122,7 @@ var physicsMixin = {
fy = 0;
}
node._setForce(fx, fy);
- node.updateDamping(this.nodeIndices.length);
+ node.updateDamping();
}
},
@@ -109,6 +158,52 @@ var physicsMixin = {
}
}
}
- }
+ },
+
+ _calculateSpringForcesOnSupport : function() {
+ var edgeLength, edge, edgeId, growthIndicator;
+ var edges = this.edges;
+ // forces caused by the edges, modelled as springs
+ for (edgeId in edges) {
+ if (edges.hasOwnProperty(edgeId)) {
+ edge = edges[edgeId];
+ if (edge.connected) {
+ // only calculate forces if nodes are in the same sector
+ if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
+ if (edge.via != null) {
+ var node1 = edge.to;
+ var node2 = edge.via;
+ var node3 = edge.from;
+
+ edgeLength = 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);
+ }
+ }
+ }
+ }
+ }
+ },
+
+ _calculateSpringForce : function(node1,node2,edgeLength) {
+ var dx, dy, angle, fx, fy, springForce, length;
+
+ dx = (node1.x - node2.x);
+ 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;
+ fy = Math.sin(angle) * springForce;
+
+ node1._addForce(fx, fy);
+ node2._addForce(-fx, -fy);
+ }
}
\ No newline at end of file
diff --git a/src/graph/graphMixins/physics/barnesHut.js b/src/graph/graphMixins/physics/barnesHut.js
index fd34fcee..2321320b 100644
--- a/src/graph/graphMixins/physics/barnesHut.js
+++ b/src/graph/graphMixins/physics/barnesHut.js
@@ -6,13 +6,14 @@ var barnesHutMixin = {
_calculateNodeForces : function() {
- this._formBarnesHutTree();
-
- var nodes = this.nodes;
- var nodeIndices = this.nodeIndices;
var node;
+ var nodes = this.calculationNodes;
+ var nodeIndices = this.calculationNodeIndices;
var nodeCount = nodeIndices.length;
+ this._formBarnesHutTree(nodes,nodeIndices);
+
+
var barnesHutTree = this.barnesHutTree;
// place the nodes one by one recursively
@@ -70,9 +71,7 @@ var barnesHutMixin = {
},
- _formBarnesHutTree : function() {
- var nodes = this.nodes;
- var nodeIndices = this.nodeIndices;
+ _formBarnesHutTree : function(nodes,nodeIndices) {
var node;
var nodeCount = nodeIndices.length;
diff --git a/src/graph/graphMixins/physics/repulsion.js b/src/graph/graphMixins/physics/repulsion.js
index 26eacda9..3c01932f 100644
--- a/src/graph/graphMixins/physics/repulsion.js
+++ b/src/graph/graphMixins/physics/repulsion.js
@@ -14,7 +14,9 @@ var repulsionMixin = {
_calculateNodeForces : function() {
var dx, dy, angle, distance, fx, fy, combinedClusterSize,
repulsingForce, node1, node2, i, j;
- var nodes = this.nodes;
+
+ var nodes = this.calculationNodes;
+ var nodeIndices = this.calculationNodeIndices;
// approximation constants
var a_base = -2/3;
@@ -22,14 +24,14 @@ var repulsionMixin = {
// repulsing forces between nodes
var minimumDistance = this.constants.nodes.distance;
-
// we loop from i over all but the last entree in the array
// j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
- for (i = 0; i < this.nodeIndices.length-1; i++) {
- node1 = nodes[this.nodeIndices[i]];
- for (j = i+1; j < this.nodeIndices.length; j++) {
- node2 = nodes[this.nodeIndices[j]];
+ for (i = 0; i < nodeIndices.length-1; i++) {
+ node1 = nodes[nodeIndices[i]];
+ for (j = i+1; j < nodeIndices.length; j++) {
+ node2 = nodes[nodeIndices[j]];
combinedClusterSize = (node1.growthIndicator + node2.growthIndicator);
+
dx = node2.x - node1.x;
dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy);
@@ -45,6 +47,11 @@ var repulsionMixin = {
else {
repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
}
+
+ if (this.sectors['support']['nodes'].hasOwnProperty(node1.id)) {
+ // console.log(combinedClusterSize, repulsingForce, minimumDistance);
+ }
+
// amplify the repulsion for clusters.
repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification;
repulsingForce *= node1.internalMultiplier * node2.internalMultiplier;