From 4b367b512cfaa579eea9df2a48b28fdaefc1c8a5 Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Mon, 10 Feb 2014 18:36:36 +0100 Subject: [PATCH] added most of smooth curve system. Check example 16 for bug related to size. --- examples/graph/02_random_nodes.html | 2 +- src/graph/Edge.js | 19 ++- src/graph/Graph.js | 37 +++++- src/graph/Node.js | 21 ++-- src/graph/graphMixins/ClusterMixin.js | 2 + src/graph/graphMixins/MixinLoader.js | 10 +- src/graph/graphMixins/SectorsMixin.js | 47 +++++++- src/graph/graphMixins/physics/PhysicsMixin.js | 109 ++++++++++++++++-- src/graph/graphMixins/physics/barnesHut.js | 13 +-- src/graph/graphMixins/physics/repulsion.js | 19 ++- 10 files changed, 241 insertions(+), 38 deletions(-) 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;