Browse Source

added most of smooth curve system. Check example 16 for bug related to size.

css_transitions
Alex de Mulder 10 years ago
parent
commit
4b367b512c
10 changed files with 241 additions and 38 deletions
  1. +1
    -1
      examples/graph/02_random_nodes.html
  2. +17
    -2
      src/graph/Edge.js
  3. +34
    -3
      src/graph/Graph.js
  4. +12
    -9
      src/graph/Node.js
  5. +2
    -0
      src/graph/graphMixins/ClusterMixin.js
  6. +8
    -2
      src/graph/graphMixins/MixinLoader.js
  7. +46
    -1
      src/graph/graphMixins/SectorsMixin.js
  8. +102
    -7
      src/graph/graphMixins/physics/PhysicsMixin.js
  9. +6
    -7
      src/graph/graphMixins/physics/barnesHut.js
  10. +13
    -6
      src/graph/graphMixins/physics/repulsion.js

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

@ -88,7 +88,7 @@
*/
var options = {
edges: {
length: 50
},
stabilize: false
};

+ 17
- 2
src/graph/Edge.js View File

@ -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);
}
}

+ 34
- 3
src/graph/Graph.js View File

@ -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)) {

+ 12
- 9
src/graph/Node.js View File

@ -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);
};

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

@ -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();
}
},

+ 8
- 2
src/graph/graphMixins/MixinLoader.js View File

@ -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

+ 46
- 1
src/graph/graphMixins/SectorsMixin.js View File

@ -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);
}
}
},

+ 102
- 7
src/graph/graphMixins/physics/PhysicsMixin.js View File

@ -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);
}
}

+ 6
- 7
src/graph/graphMixins/physics/barnesHut.js View File

@ -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;

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

@ -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;

Loading…
Cancel
Save