From 0ff54067a1ade066b4204f99d0af69fa2971ccd0 Mon Sep 17 00:00:00 2001 From: AlexDM0 Date: Wed, 22 Jan 2014 14:09:49 +0100 Subject: [PATCH] created UI sector, starting to rework selection (generalize it over nodes and edges), edge implementation later. --- dist/vis.js | 597 +++++++++++++++++---------- examples/graph/img/UI/downarrow.png | Bin 0 -> 3930 bytes examples/graph/img/UI/leftarrow.png | Bin 0 -> 3936 bytes examples/graph/img/UI/minus.png | Bin 0 -> 3739 bytes examples/graph/img/UI/plus.png | Bin 0 -> 3970 bytes examples/graph/img/UI/rightarrow.png | Bin 0 -> 3988 bytes examples/graph/img/UI/uparrow.png | Bin 0 -> 3922 bytes src/graph/Graph.js | 475 ++++++++++++--------- src/graph/Node.js | 51 ++- src/graph/SectorsMixin.js | 72 +++- src/graph/SelectionMixin.js | 27 ++ 11 files changed, 773 insertions(+), 449 deletions(-) create mode 100644 examples/graph/img/UI/downarrow.png create mode 100644 examples/graph/img/UI/leftarrow.png create mode 100644 examples/graph/img/UI/minus.png create mode 100644 examples/graph/img/UI/plus.png create mode 100644 examples/graph/img/UI/rightarrow.png create mode 100644 examples/graph/img/UI/uparrow.png create mode 100644 src/graph/SelectionMixin.js diff --git a/dist/vis.js b/dist/vis.js index ad623cdd..e946d96b 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -8784,6 +8784,8 @@ function Node(properties, imagelist, grouplist, constants) { this.y = 0; this.xFixed = false; this.yFixed = false; + this.horizontalAlignLeft = true; // these are for the UI + this.verticalAlignTop = true; // these are for the UI this.radius = constants.nodes.radius; this.baseRadiusValue = constants.nodes.radius; this.radiusFixed = false; @@ -8888,6 +8890,10 @@ Node.prototype.setProperties = function(properties, constants) { if (properties.y !== undefined) {this.y = properties.y;} if (properties.value !== undefined) {this.value = properties.value;} + // UI properties + if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;} + if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;} + if (this.id === undefined) { throw "Node must have an id"; } @@ -9224,6 +9230,7 @@ Node.prototype.isOverlappingWith = function(obj) { Node.prototype._resizeImage = function (ctx) { // TODO: pre calculate the image size + if (!this.width || !this.height) { // undefined or 0 var width, height; if (this.value) { @@ -9246,9 +9253,9 @@ Node.prototype._resizeImage = function (ctx) { this.height = height; if (this.width && this.height) { - this.width += this.clusterSize * this.clusterSizeWidthFactor; - this.height += this.clusterSize * this.clusterSizeHeightFactor; - this.radius += this.clusterSize * this.clusterSizeRadiusFactor; + this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; } } @@ -9271,6 +9278,8 @@ Node.prototype._drawImage = function (ctx) { ctx.globalAlpha = 0.5; ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth); } + + // draw the image ctx.globalAlpha = 1.0; ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); yLabel = this.y + this.height / 2; @@ -9291,9 +9300,9 @@ Node.prototype._resizeBox = function (ctx) { this.width = textSize.width + 2 * margin; this.height = textSize.height + 2 * margin; - this.width += this.clusterSize * 0.5 * this.clusterSizeWidthFactor; - this.height += this.clusterSize * 0.5 * this.clusterSizeHeightFactor; - //this.radius += this.clusterSize * this.clusterSizeRadiusFactor; + this.width += (this.clusterSize - 1) * 0.5 * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * 0.5 * this.clusterSizeHeightFactor; +// this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor; } }; @@ -9340,9 +9349,9 @@ Node.prototype._resizeDatabase = function (ctx) { this.height = size; // scaling used for clustering - this.width += this.clusterSize * this.clusterSizeWidthFactor; - this.height += this.clusterSize * this.clusterSizeHeightFactor; - this.radius += this.clusterSize * this.clusterSizeRadiusFactor; + this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; } }; @@ -9389,9 +9398,9 @@ Node.prototype._resizeCircle = function (ctx) { this.height = diameter; // scaling used for clustering - this.width += this.clusterSize * this.clusterSizeWidthFactor; - this.height += this.clusterSize * this.clusterSizeHeightFactor; - this.radius += this.clusterSize * 0.5*this.clusterSizeRadiusFactor; +// this.width += (this.clusterSize - 1) * 0.5 * this.clusterSizeWidthFactor; +// this.height += (this.clusterSize - 1) * 0.5 * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor; } }; @@ -9437,9 +9446,9 @@ Node.prototype._resizeEllipse = function (ctx) { } // scaling used for clustering - this.width += this.clusterSize * this.clusterSizeWidthFactor; - this.height += this.clusterSize * this.clusterSizeHeightFactor; - this.radius += this.clusterSize * this.clusterSizeRadiusFactor; + this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; } }; @@ -9502,9 +9511,9 @@ Node.prototype._resizeShape = function (ctx) { this.height = size; // scaling used for clustering - this.width += this.clusterSize * this.clusterSizeWidthFactor; - this.height += this.clusterSize * this.clusterSizeHeightFactor; - this.radius += this.clusterSize * 0.5 * this.clusterSizeRadiusFactor; + this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor; } }; @@ -9561,9 +9570,9 @@ Node.prototype._resizeText = function (ctx) { this.height = textSize.height + 2 * margin; // scaling used for clustering - this.width += this.clusterSize * this.clusterSizeWidthFactor; - this.height += this.clusterSize * this.clusterSizeHeightFactor; - this.radius += this.clusterSize * this.clusterSizeRadiusFactor; + this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; } }; @@ -10621,6 +10630,19 @@ var SectorMixin = { }, + /** + * This function sets the global references to nodes, edges and nodeIndices to + * those of the UI sector. + * + * @private + */ + _switchToUISector : function() { + this.nodeIndices = this.sectors["UI"]["nodeIndices"]; + this.nodes = this.sectors["UI"]["nodes"]; + this.edges = this.sectors["UI"]["edges"]; + }, + + /** * This function sets the global references to nodes, edges and nodeIndices back to * those of the currently active sector. @@ -10898,11 +10920,11 @@ var SectorMixin = { * @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 {*} [args] | Optional: arguments to pass to the runFunction + * @param {*} [argument] | Optional: arguments to pass to the runFunction * @private */ - _doInAllActiveSectors : function(runFunction,args) { - if (args === undefined) { + _doInAllActiveSectors : function(runFunction,argument) { + if (argument === undefined) { for (var sector in this.sectors["active"]) { if (this.sectors["active"].hasOwnProperty(sector)) { // switch the global references to those of this sector @@ -10916,7 +10938,13 @@ var SectorMixin = { if (this.sectors["active"].hasOwnProperty(sector)) { // switch the global references to those of this sector this._switchToActiveSector(sector); - this[runFunction](args); + var args = Array.prototype.splice.call(arguments, 1); + if (args.length > 1) { + this[runFunction](args[0],args[1]); + } + else { + this[runFunction](argument); + } } } } @@ -10929,13 +10957,13 @@ var SectorMixin = { * This runs a function in all frozen sectors. This is used in the _redraw(). * * @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 + * | we don't pass the function itself because then the "this" is the window object * | instead of the Graph object - * @param {*} [args] | Optional: arguments to pass to the runFunction + * @param {*} [argument] | Optional: arguments to pass to the runFunction * @private */ - _doInAllFrozenSectors : function(runFunction,args) { - if (args === undefined) { + _doInAllFrozenSectors : function(runFunction,argument) { + if (argument === undefined) { for (var sector in this.sectors["frozen"]) { if (this.sectors["frozen"].hasOwnProperty(sector)) { // switch the global references to those of this sector @@ -10949,7 +10977,13 @@ var SectorMixin = { if (this.sectors["frozen"].hasOwnProperty(sector)) { // switch the global references to those of this sector this._switchToFrozenSector(sector); - this[runFunction](args); + var args = Array.prototype.splice.call(arguments, 1); + if (args.length > 1) { + this[runFunction](args[0],args[1]); + } + else { + this[runFunction](argument); + } } } } @@ -10957,11 +10991,38 @@ var SectorMixin = { }, + /** + * This runs a function in all frozen sectors. This is used in the _redraw(). + * + * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors + * | we don't 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 + */ + _doInUISector : function(runFunction,argument) { + this._switchToUISector(); + if (argument === undefined) { + this[runFunction](); + } + else { + var args = Array.prototype.splice.call(arguments, 1); + if (args.length > 1) { + this[runFunction](args[0],args[1]); + } + else { + this[runFunction](argument); + } + } + this._loadLatestSector(); + }, + + /** * This runs a function in all sectors. This is used in the _redraw(). * * @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 + * | we don't 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 @@ -12139,12 +12200,25 @@ function Graph (container, data, options) { maxIterations: 1000 // maximum number of iteration to stabilize }; + this.groups = new Groups(); // object with groups + this.images = new Images(); // object with images + this.images.setOnloadCallback(function () { + graph._redraw(); + }); + + + // create a frame and canvas + this._create(); + // call the constructor of the cluster object this._loadClusterSystem(); // call the sector constructor this._loadSectorSystem(); + // load the UI system. + this._loadUISystem(); + var graph = this; this.freezeSimulation = false;// freeze the simulation this.tapTimer = 0; // timer to detect doubleclick or double tap @@ -12197,21 +12271,12 @@ function Graph (container, data, options) { } }; - this.groups = new Groups(); // object with groups - this.images = new Images(); // object with images - this.images.setOnloadCallback(function () { - graph._redraw(); - }); - // properties of the data this.moving = false; // True if any of the nodes have an undefined position this.selection = []; this.timer = undefined; - // create a frame and canvas - this._create(); - // apply options this.setOptions(options); @@ -13206,6 +13271,8 @@ Graph.prototype.setSize = function(width, height) { this.frame.canvas.width = this.frame.canvas.clientWidth; this.frame.canvas.height = this.frame.canvas.clientHeight; + + this._relocateUI(); }; /** @@ -13547,6 +13614,8 @@ Graph.prototype._redraw = function() { // restore original scaling and translation ctx.restore(); + + this._doInUISector("_drawNodes",ctx,true); }; /** @@ -13645,9 +13714,14 @@ Graph.prototype._yToCanvas = function(y) { * Redraw all nodes * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); * @param {CanvasRenderingContext2D} ctx + * @param {Boolean} [alwaysShow] * @private */ -Graph.prototype._drawNodes = function(ctx) { +Graph.prototype._drawNodes = function(ctx,alwaysShow) { + if (alwaysShow === undefined) { + alwaysShow = false; + } + // first draw the unselected nodes var nodes = this.nodes; var selected = []; @@ -13659,7 +13733,7 @@ Graph.prototype._drawNodes = function(ctx) { selected.push(id); } else { - if (nodes[id].inArea()) { + if (nodes[id].inArea() || alwaysShow) { nodes[id].draw(ctx); } } @@ -13668,7 +13742,7 @@ Graph.prototype._drawNodes = function(ctx) { // draw the selected nodes on top for (var s = 0, sMax = selected.length; s < sMax; s++) { - if (nodes[selected[s]].inArea()) { + if (nodes[selected[s]].inArea() || alwaysShow) { nodes[selected[s]].draw(ctx); } } @@ -13746,211 +13820,197 @@ Graph.prototype._initializeForceCalculation = function() { * @private */ Graph.prototype._calculateForces = function() { - // stop calculation if there is only one node - if (this.nodeIndices.length == 1) { - this.nodes[this.nodeIndices[0]]._setForce(0,0); - } - // if there are too many nodes on screen, we cluster without repositioning - else if (this.nodeIndices.length > this.constants.clustering.absoluteMaxNumberOfNodes && this.constants.clustering.enabled == true) { - this.clusterToFit(this.constants.clustering.reduceToMaxNumberOfNodes, false); - this._initializeForceCalculation(); - } - else { - this.canvasTopLeft = {"x": (0-this.translation.x)/this.scale, - "y": (0-this.translation.y)/this.scale}; - this.canvasBottomRight = {"x": (this.frame.canvas.clientWidth -this.translation.x)/this.scale, - "y": (this.frame.canvas.clientHeight-this.translation.y)/this.scale}; - var centerPos = {"x":0.5*(this.canvasTopLeft.x + this.canvasBottomRight.x), - "y":0.5*(this.canvasTopLeft.y + this.canvasBottomRight.y)} - - // create a local edge to the nodes and edges, that is faster - var dx, dy, angle, distance, fx, fy, - repulsingForce, springForce, length, edgeLength, - node, node1, node2, edge, edgeId, i, j, nodeId, xCenter, yCenter; - var clusterSize; - var nodes = this.nodes; - var edges = this.edges; - - - // 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 gravity = 0.08; - for (i = 0; i < this.nodeIndices.length; i++) { - node = nodes[this.nodeIndices[i]]; - // gravity does not apply when we are in a pocket sector - if (this._sector() == "default") { - dx = -node.x + centerPos.x; - dy = -node.y + centerPos.y; - - angle = Math.atan2(dy, dx); - fx = Math.cos(angle) * gravity; - fy = Math.sin(angle) * gravity; - } - else { - fx = 0; - fy = 0; - } - node._setForce(fx, fy); + this.canvasTopLeft = {"x": (0-this.translation.x)/this.scale, + "y": (0-this.translation.y)/this.scale}; + this.canvasBottomRight = {"x": (this.frame.canvas.clientWidth -this.translation.x)/this.scale, + "y": (this.frame.canvas.clientHeight-this.translation.y)/this.scale}; + var centerPos = {"x":0.5*(this.canvasTopLeft.x + this.canvasBottomRight.x), + "y":0.5*(this.canvasTopLeft.y + this.canvasBottomRight.y)} + + // create a local edge to the nodes and edges, that is faster + var dx, dy, angle, distance, fx, fy, + repulsingForce, springForce, length, edgeLength, + node, node1, node2, edge, edgeId, i, j, nodeId, xCenter, yCenter; + var clusterSize; + var nodes = this.nodes; + var edges = this.edges; - node.updateDamping(this.nodeIndices.length); - } + // 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 gravity = 0.08; + for (i = 0; i < this.nodeIndices.length; i++) { + node = nodes[this.nodeIndices[i]]; + // gravity does not apply when we are in a pocket sector + if (this._sector() == "default") { + dx = -node.x + centerPos.x; + dy = -node.y + centerPos.y; + + angle = Math.atan2(dy, dx); + fx = Math.cos(angle) * gravity; + fy = Math.sin(angle) * gravity; + } + else { + fx = 0; + fy = 0; + } + node._setForce(fx, fy); - this.updateLabels(); + node.updateDamping(this.nodeIndices.length); + } - // repulsing forces between nodes - var minimumDistance = this.constants.nodes.distance, - steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance + // repulsing forces between nodes + var minimumDistance = this.constants.nodes.distance, + steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance - // 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]]; - clusterSize = (node1.clusterSize + node2.clusterSize - 2); - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); + // 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]]; + clusterSize = (node1.clusterSize + node2.clusterSize - 2); + dx = node2.x - node1.x; + dy = node2.y - node1.y; + 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)); - if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045 - angle = Math.atan2(dy, dx); + // clusters have a larger region of influence + minimumDistance = (clusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + clusterSize * this.constants.clustering.distanceAmplification)); + if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045 + angle = Math.atan2(dy, dx); - if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307 - repulsingForce = 1.0; - } - else { - // TODO: correct factor for repulsing force - //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force - //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force - repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force - } - // amplify the repulsion for clusters. - repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification; + if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307 + repulsingForce = 1.0; + } + else { + // TODO: correct factor for repulsing force + //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force + } + // amplify the repulsion for clusters. + repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification; - fx = Math.cos(angle) * repulsingForce; - fy = Math.sin(angle) * repulsingForce; + fx = Math.cos(angle) * repulsingForce; + fy = Math.sin(angle) * repulsingForce; - node1._addForce(-fx, -fy); - node2._addForce(fx, fy); - } + node1._addForce(-fx, -fy); + node2._addForce(fx, fy); } } + } /* - // repulsion of the edges on the nodes and - for (var nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - node = nodes[nodeId]; - for(var edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - edge = edges[edgeId]; - - // get the center of the edge - xCenter = edge.from.x+(edge.to.x - edge.from.x)/2; - yCenter = edge.from.y+(edge.to.y - edge.from.y)/2; - - // calculate normally distributed force - dx = node.x - xCenter; - dy = node.y - yCenter; - distance = Math.sqrt(dx * dx + dy * dy); - if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045 - angle = Math.atan2(dy, dx); - - if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307 - repulsingForce = 1.0; - } - else { - // TODO: correct factor for repulsing force - //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force - //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force - repulsingForce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)); // TODO: customize the repulsing force - } - fx = Math.cos(angle) * repulsingForce; - fy = Math.sin(angle) * repulsingForce; - node._addForce(fx, fy); - edge.from._addForce(-fx/2,-fy/2); - edge.to._addForce(-fx/2,-fy/2); + // repulsion of the edges on the nodes and + for (var nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + node = nodes[nodeId]; + for(var edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + edge = edges[edgeId]; + + // get the center of the edge + xCenter = edge.from.x+(edge.to.x - edge.from.x)/2; + yCenter = edge.from.y+(edge.to.y - edge.from.y)/2; + + // calculate normally distributed force + dx = node.x - xCenter; + dy = node.y - yCenter; + distance = Math.sqrt(dx * dx + dy * dy); + if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045 + angle = Math.atan2(dy, dx); + + if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307 + repulsingForce = 1.0; + } + else { + // TODO: correct factor for repulsing force + //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force + repulsingForce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)); // TODO: customize the repulsing force } + fx = Math.cos(angle) * repulsingForce; + fy = Math.sin(angle) * repulsingForce; + node._addForce(fx, fy); + edge.from._addForce(-fx/2,-fy/2); + edge.to._addForce(-fx/2,-fy/2); } } } } + } */ - // 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)) { - clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2); - dx = (edge.to.x - edge.from.x); - dy = (edge.to.y - edge.from.y); - //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin - //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin - //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2; - edgeLength = edge.length; - // this implies that the edges between big clusters are longer - edgeLength += clusterSize * this.constants.clustering.edgeGrowth; - length = Math.sqrt(dx * dx + dy * dy); - angle = Math.atan2(dy, dx); + // 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)) { + clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2); + dx = (edge.to.x - edge.from.x); + dy = (edge.to.y - edge.from.y); + //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin + //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin + //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2; + edgeLength = edge.length; + // this implies that the edges between big clusters are longer + edgeLength += clusterSize * this.constants.clustering.edgeGrowth; + length = Math.sqrt(dx * dx + dy * dy); + angle = Math.atan2(dy, dx); - springForce = edge.stiffness * (edgeLength - length); + springForce = edge.stiffness * (edgeLength - length); - fx = Math.cos(angle) * springForce; - fy = Math.sin(angle) * springForce; + fx = Math.cos(angle) * springForce; + fy = Math.sin(angle) * springForce; - edge.from._addForce(-fx, -fy); - edge.to._addForce(fx, fy); - } + edge.from._addForce(-fx, -fy); + edge.to._addForce(fx, fy); } } } + } /* - // TODO: re-implement repulsion of edges - - // repulsing forces between edges - var minimumDistance = this.constants.edges.distance, - steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance - for (var l = 0; l < edges.length; l++) { - //Keep distance from other edge centers - for (var l2 = l + 1; l2 < this.edges.length; l2++) { - //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin - //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin - //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0), - var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, - ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, - l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2, - l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2, - - // calculate normally distributed force - dx = l2x - lx, - dy = l2y - ly, - distance = Math.sqrt(dx * dx + dy * dy), - angle = Math.atan2(dy, dx), - - - // TODO: correct factor for repulsing force - //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force - //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force - repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force - fx = Math.cos(angle) * repulsingforce, - fy = Math.sin(angle) * repulsingforce; - - edges[l].from._addForce(-fx, -fy); - edges[l].to._addForce(-fx, -fy); - edges[l2].from._addForce(fx, fy); - edges[l2].to._addForce(fx, fy); - } - } + // TODO: re-implement repulsion of edges + + // repulsing forces between edges + var minimumDistance = this.constants.edges.distance, + steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance + for (var l = 0; l < edges.length; l++) { + //Keep distance from other edge centers + for (var l2 = l + 1; l2 < this.edges.length; l2++) { + //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin + //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin + //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0), + var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, + ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, + l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2, + l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2, + + // calculate normally distributed force + dx = l2x - lx, + dy = l2y - ly, + distance = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(dy, dx), + + + // TODO: correct factor for repulsing force + //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force + repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force + fx = Math.cos(angle) * repulsingforce, + fy = Math.sin(angle) * repulsingforce; + + edges[l].from._addForce(-fx, -fy); + edges[l].to._addForce(-fx, -fy); + edges[l2].from._addForce(fx, fy); + edges[l2].to._addForce(fx, fy); + } + } */ - } }; @@ -14076,12 +14136,17 @@ Graph.prototype._loadSectorSystem = function() { this.sectors = {}; this.activeSector = ["default"]; this.sectors["active"] = {}; - this.sectors["active"][this.activeSector[this.activeSector.length-1]] = {"nodes":{}, - "edges":{}, - "nodeIndices":[], - "formationScale": 1.0, - "drawingNode": undefined}; + this.sectors["active"]["default"] = {"nodes":{}, + "edges":{}, + "nodeIndices":[], + "formationScale": 1.0, + "drawingNode": undefined}; this.sectors["frozen"] = {}; + this.sectors["UI"] = {"nodes":{}, + "edges":{}, + "nodeIndices":[], + "formationScale": 1.0, + "drawingNode": undefined}; this.nodeIndices = this.sectors["active"][this.activeSector[this.activeSector.length-1]]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields for (var mixinFunction in SectorMixin) { @@ -14090,6 +14155,90 @@ Graph.prototype._loadSectorSystem = function() { } } }; + + + +Graph.prototype._loadUISystem = function() { + this._loadUIElements(); +} + +Graph.prototype._loadUIElements = function() { + var DIR = 'img/UI/'; + this.UIclientWidth = this.frame.canvas.clientWidth; + this.UIclientHeight = this.frame.canvas.clientHeight; + var UINodes = [ + {id: 'UI_up', shape: 'image', image: DIR + 'uparrow.png', + verticalAlignTop: false, x: 50, y: this.UIclientHeight - 50}, + {id: 'UI_down', shape: 'image', image: DIR + 'downarrow.png', + verticalAlignTop: false, x: 50, y: this.UIclientHeight - 20}, + {id: 'UI_left', shape: 'image', image: DIR + 'leftarrow.png', + verticalAlignTop: false, x: 20, y: this.UIclientHeight - 20}, + {id: 'UI_right', shape: 'image', image: DIR + 'rightarrow.png', + verticalAlignTop: false, x: 80, y: this.UIclientHeight - 20}, + {id: 'UI_plus', shape: 'image', image: DIR + 'plus.png', + verticalAlignTop: false, x: 130, y: this.UIclientHeight - 20}, + {id: 'UI_minus', shape: 'image', image: DIR + 'minus.png', + verticalAlignTop: false, x: 160, y: this.UIclientHeight - 20} + ]; + + for (var i = 0; i < UINodes.length; i++) { + this.sectors["UI"]['nodes'][UINodes[i]['id']] = new Node(UINodes[i], this.images, this.groups, this.constants); + } +}; + +Graph.prototype._relocateUI = function() { + var xOffset = this.UIclientWidth - this.frame.canvas.clientWidth; + var yOffset = this.UIclientHeight - this.frame.canvas.clientHeight; + this.UIclientWidth = this.frame.canvas.clientWidth; + this.UIclientHeight = this.frame.canvas.clientHeight; + var node = null; + for (var nodeId in this.sectors["UI"]["nodes"]) { + if (this.sectors["UI"]["nodes"].hasOwnProperty(nodeId)) { + node = this.sectors["UI"]["nodes"][nodeId]; + if (!node.horizontalAlignLeft) { + node.x -= xOffset; + } + if (!node.verticalAlignTop) { + node.y -= yOffset; + } + } + } +}; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /** * vis.js module exports */ diff --git a/examples/graph/img/UI/downarrow.png b/examples/graph/img/UI/downarrow.png new file mode 100644 index 0000000000000000000000000000000000000000..83ad74d6d7716e8b3806059f5604ef6b31c75769 GIT binary patch literal 3930 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 literal 0 HcmV?d00001 diff --git a/examples/graph/img/UI/leftarrow.png b/examples/graph/img/UI/leftarrow.png new file mode 100644 index 0000000000000000000000000000000000000000..24f9b66c1af7be0e1f4e499215c6f50a7d5a13a1 GIT binary patch literal 3936 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 literal 0 HcmV?d00001 diff --git a/src/graph/Graph.js b/src/graph/Graph.js index ce05e4bd..94bf5a5b 100644 --- a/src/graph/Graph.js +++ b/src/graph/Graph.js @@ -87,12 +87,25 @@ function Graph (container, data, options) { maxIterations: 1000 // maximum number of iteration to stabilize }; + this.groups = new Groups(); // object with groups + this.images = new Images(); // object with images + this.images.setOnloadCallback(function () { + graph._redraw(); + }); + + + // create a frame and canvas + this._create(); + // call the constructor of the cluster object this._loadClusterSystem(); // call the sector constructor this._loadSectorSystem(); + // load the UI system. + this._loadUISystem(); + var graph = this; this.freezeSimulation = false;// freeze the simulation this.tapTimer = 0; // timer to detect doubleclick or double tap @@ -145,21 +158,12 @@ function Graph (container, data, options) { } }; - this.groups = new Groups(); // object with groups - this.images = new Images(); // object with images - this.images.setOnloadCallback(function () { - graph._redraw(); - }); - // properties of the data this.moving = false; // True if any of the nodes have an undefined position this.selection = []; this.timer = undefined; - // create a frame and canvas - this._create(); - // apply options this.setOptions(options); @@ -1154,6 +1158,8 @@ Graph.prototype.setSize = function(width, height) { this.frame.canvas.width = this.frame.canvas.clientWidth; this.frame.canvas.height = this.frame.canvas.clientHeight; + + this._relocateUI(); }; /** @@ -1495,6 +1501,8 @@ Graph.prototype._redraw = function() { // restore original scaling and translation ctx.restore(); + + this._doInUISector("_drawNodes",ctx,true); }; /** @@ -1593,9 +1601,14 @@ Graph.prototype._yToCanvas = function(y) { * Redraw all nodes * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); * @param {CanvasRenderingContext2D} ctx + * @param {Boolean} [alwaysShow] * @private */ -Graph.prototype._drawNodes = function(ctx) { +Graph.prototype._drawNodes = function(ctx,alwaysShow) { + if (alwaysShow === undefined) { + alwaysShow = false; + } + // first draw the unselected nodes var nodes = this.nodes; var selected = []; @@ -1607,7 +1620,7 @@ Graph.prototype._drawNodes = function(ctx) { selected.push(id); } else { - if (nodes[id].inArea()) { + if (nodes[id].inArea() || alwaysShow) { nodes[id].draw(ctx); } } @@ -1616,7 +1629,7 @@ Graph.prototype._drawNodes = function(ctx) { // draw the selected nodes on top for (var s = 0, sMax = selected.length; s < sMax; s++) { - if (nodes[selected[s]].inArea()) { + if (nodes[selected[s]].inArea() || alwaysShow) { nodes[selected[s]].draw(ctx); } } @@ -1694,211 +1707,197 @@ Graph.prototype._initializeForceCalculation = function() { * @private */ Graph.prototype._calculateForces = function() { - // stop calculation if there is only one node - if (this.nodeIndices.length == 1) { - this.nodes[this.nodeIndices[0]]._setForce(0,0); - } - // if there are too many nodes on screen, we cluster without repositioning - else if (this.nodeIndices.length > this.constants.clustering.absoluteMaxNumberOfNodes && this.constants.clustering.enabled == true) { - this.clusterToFit(this.constants.clustering.reduceToMaxNumberOfNodes, false); - this._initializeForceCalculation(); - } - else { - this.canvasTopLeft = {"x": (0-this.translation.x)/this.scale, - "y": (0-this.translation.y)/this.scale}; - this.canvasBottomRight = {"x": (this.frame.canvas.clientWidth -this.translation.x)/this.scale, - "y": (this.frame.canvas.clientHeight-this.translation.y)/this.scale}; - var centerPos = {"x":0.5*(this.canvasTopLeft.x + this.canvasBottomRight.x), - "y":0.5*(this.canvasTopLeft.y + this.canvasBottomRight.y)} - - // create a local edge to the nodes and edges, that is faster - var dx, dy, angle, distance, fx, fy, - repulsingForce, springForce, length, edgeLength, - node, node1, node2, edge, edgeId, i, j, nodeId, xCenter, yCenter; - var clusterSize; - var nodes = this.nodes; - var edges = this.edges; - - - // 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 gravity = 0.08; - for (i = 0; i < this.nodeIndices.length; i++) { - node = nodes[this.nodeIndices[i]]; - // gravity does not apply when we are in a pocket sector - if (this._sector() == "default") { - dx = -node.x + centerPos.x; - dy = -node.y + centerPos.y; - - angle = Math.atan2(dy, dx); - fx = Math.cos(angle) * gravity; - fy = Math.sin(angle) * gravity; - } - else { - fx = 0; - fy = 0; - } - node._setForce(fx, fy); + this.canvasTopLeft = {"x": (0-this.translation.x)/this.scale, + "y": (0-this.translation.y)/this.scale}; + this.canvasBottomRight = {"x": (this.frame.canvas.clientWidth -this.translation.x)/this.scale, + "y": (this.frame.canvas.clientHeight-this.translation.y)/this.scale}; + var centerPos = {"x":0.5*(this.canvasTopLeft.x + this.canvasBottomRight.x), + "y":0.5*(this.canvasTopLeft.y + this.canvasBottomRight.y)} + + // create a local edge to the nodes and edges, that is faster + var dx, dy, angle, distance, fx, fy, + repulsingForce, springForce, length, edgeLength, + node, node1, node2, edge, edgeId, i, j, nodeId, xCenter, yCenter; + var clusterSize; + var nodes = this.nodes; + var edges = this.edges; - node.updateDamping(this.nodeIndices.length); - } + // 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 gravity = 0.08; + for (i = 0; i < this.nodeIndices.length; i++) { + node = nodes[this.nodeIndices[i]]; + // gravity does not apply when we are in a pocket sector + if (this._sector() == "default") { + dx = -node.x + centerPos.x; + dy = -node.y + centerPos.y; + + angle = Math.atan2(dy, dx); + fx = Math.cos(angle) * gravity; + fy = Math.sin(angle) * gravity; + } + else { + fx = 0; + fy = 0; + } + node._setForce(fx, fy); - this.updateLabels(); + node.updateDamping(this.nodeIndices.length); + } - // repulsing forces between nodes - var minimumDistance = this.constants.nodes.distance, - steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance + // repulsing forces between nodes + var minimumDistance = this.constants.nodes.distance, + steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance - // 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]]; - clusterSize = (node1.clusterSize + node2.clusterSize - 2); - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); + // 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]]; + clusterSize = (node1.clusterSize + node2.clusterSize - 2); + dx = node2.x - node1.x; + dy = node2.y - node1.y; + 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)); - if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045 - angle = Math.atan2(dy, dx); + // clusters have a larger region of influence + minimumDistance = (clusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + clusterSize * this.constants.clustering.distanceAmplification)); + if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045 + angle = Math.atan2(dy, dx); - if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307 - repulsingForce = 1.0; - } - else { - // TODO: correct factor for repulsing force - //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force - //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force - repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force - } - // amplify the repulsion for clusters. - repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification; + if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307 + repulsingForce = 1.0; + } + else { + // TODO: correct factor for repulsing force + //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force + } + // amplify the repulsion for clusters. + repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification; - fx = Math.cos(angle) * repulsingForce; - fy = Math.sin(angle) * repulsingForce; + fx = Math.cos(angle) * repulsingForce; + fy = Math.sin(angle) * repulsingForce; - node1._addForce(-fx, -fy); - node2._addForce(fx, fy); - } + node1._addForce(-fx, -fy); + node2._addForce(fx, fy); } } + } /* - // repulsion of the edges on the nodes and - for (var nodeId in nodes) { - if (nodes.hasOwnProperty(nodeId)) { - node = nodes[nodeId]; - for(var edgeId in edges) { - if (edges.hasOwnProperty(edgeId)) { - edge = edges[edgeId]; - - // get the center of the edge - xCenter = edge.from.x+(edge.to.x - edge.from.x)/2; - yCenter = edge.from.y+(edge.to.y - edge.from.y)/2; - - // calculate normally distributed force - dx = node.x - xCenter; - dy = node.y - yCenter; - distance = Math.sqrt(dx * dx + dy * dy); - if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045 - angle = Math.atan2(dy, dx); - - if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307 - repulsingForce = 1.0; - } - else { - // TODO: correct factor for repulsing force - //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force - //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force - repulsingForce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)); // TODO: customize the repulsing force - } - fx = Math.cos(angle) * repulsingForce; - fy = Math.sin(angle) * repulsingForce; - node._addForce(fx, fy); - edge.from._addForce(-fx/2,-fy/2); - edge.to._addForce(-fx/2,-fy/2); + // repulsion of the edges on the nodes and + for (var nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + node = nodes[nodeId]; + for(var edgeId in edges) { + if (edges.hasOwnProperty(edgeId)) { + edge = edges[edgeId]; + + // get the center of the edge + xCenter = edge.from.x+(edge.to.x - edge.from.x)/2; + yCenter = edge.from.y+(edge.to.y - edge.from.y)/2; + + // calculate normally distributed force + dx = node.x - xCenter; + dy = node.y - yCenter; + distance = Math.sqrt(dx * dx + dy * dy); + if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045 + angle = Math.atan2(dy, dx); + + if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307 + repulsingForce = 1.0; } + else { + // TODO: correct factor for repulsing force + //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force + repulsingForce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)); // TODO: customize the repulsing force + } + fx = Math.cos(angle) * repulsingForce; + fy = Math.sin(angle) * repulsingForce; + node._addForce(fx, fy); + edge.from._addForce(-fx/2,-fy/2); + edge.to._addForce(-fx/2,-fy/2); } } } } + } */ - // 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)) { - clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2); - dx = (edge.to.x - edge.from.x); - dy = (edge.to.y - edge.from.y); - //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin - //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin - //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2; - edgeLength = edge.length; - // this implies that the edges between big clusters are longer - edgeLength += clusterSize * this.constants.clustering.edgeGrowth; - length = Math.sqrt(dx * dx + dy * dy); - angle = Math.atan2(dy, dx); + // 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)) { + clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2); + dx = (edge.to.x - edge.from.x); + dy = (edge.to.y - edge.from.y); + //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin + //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin + //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2; + edgeLength = edge.length; + // this implies that the edges between big clusters are longer + edgeLength += clusterSize * this.constants.clustering.edgeGrowth; + length = Math.sqrt(dx * dx + dy * dy); + angle = Math.atan2(dy, dx); - springForce = edge.stiffness * (edgeLength - length); + springForce = edge.stiffness * (edgeLength - length); - fx = Math.cos(angle) * springForce; - fy = Math.sin(angle) * springForce; + fx = Math.cos(angle) * springForce; + fy = Math.sin(angle) * springForce; - edge.from._addForce(-fx, -fy); - edge.to._addForce(fx, fy); - } + edge.from._addForce(-fx, -fy); + edge.to._addForce(fx, fy); } } } + } /* - // TODO: re-implement repulsion of edges - - // repulsing forces between edges - var minimumDistance = this.constants.edges.distance, - steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance - for (var l = 0; l < edges.length; l++) { - //Keep distance from other edge centers - for (var l2 = l + 1; l2 < this.edges.length; l2++) { - //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin - //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin - //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0), - var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, - ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, - l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2, - l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2, - - // calculate normally distributed force - dx = l2x - lx, - dy = l2y - ly, - distance = Math.sqrt(dx * dx + dy * dy), - angle = Math.atan2(dy, dx), - - - // TODO: correct factor for repulsing force - //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force - //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force - repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force - fx = Math.cos(angle) * repulsingforce, - fy = Math.sin(angle) * repulsingforce; - - edges[l].from._addForce(-fx, -fy); - edges[l].to._addForce(-fx, -fy); - edges[l2].from._addForce(fx, fy); - edges[l2].to._addForce(fx, fy); - } - } + // TODO: re-implement repulsion of edges + + // repulsing forces between edges + var minimumDistance = this.constants.edges.distance, + steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance + for (var l = 0; l < edges.length; l++) { + //Keep distance from other edge centers + for (var l2 = l + 1; l2 < this.edges.length; l2++) { + //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin + //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin + //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0), + var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2, + ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2, + l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2, + l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2, + + // calculate normally distributed force + dx = l2x - lx, + dy = l2y - ly, + distance = Math.sqrt(dx * dx + dy * dy), + angle = Math.atan2(dy, dx), + + + // TODO: correct factor for repulsing force + //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force + //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force + repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force + fx = Math.cos(angle) * repulsingforce, + fy = Math.sin(angle) * repulsingforce; + + edges[l].from._addForce(-fx, -fy); + edges[l].to._addForce(-fx, -fy); + edges[l2].from._addForce(fx, fy); + edges[l2].to._addForce(fx, fy); + } + } */ - } }; @@ -2024,12 +2023,17 @@ Graph.prototype._loadSectorSystem = function() { this.sectors = {}; this.activeSector = ["default"]; this.sectors["active"] = {}; - this.sectors["active"][this.activeSector[this.activeSector.length-1]] = {"nodes":{}, - "edges":{}, - "nodeIndices":[], - "formationScale": 1.0, - "drawingNode": undefined}; + this.sectors["active"]["default"] = {"nodes":{}, + "edges":{}, + "nodeIndices":[], + "formationScale": 1.0, + "drawingNode": undefined}; this.sectors["frozen"] = {}; + this.sectors["UI"] = {"nodes":{}, + "edges":{}, + "nodeIndices":[], + "formationScale": 1.0, + "drawingNode": undefined}; this.nodeIndices = this.sectors["active"][this.activeSector[this.activeSector.length-1]]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields for (var mixinFunction in SectorMixin) { @@ -2037,4 +2041,87 @@ Graph.prototype._loadSectorSystem = function() { Graph.prototype[mixinFunction] = SectorMixin[mixinFunction]; } } -}; \ No newline at end of file +}; + + + +Graph.prototype._loadUISystem = function() { + this._loadUIElements(); +} + +Graph.prototype._loadUIElements = function() { + var DIR = 'img/UI/'; + this.UIclientWidth = this.frame.canvas.clientWidth; + this.UIclientHeight = this.frame.canvas.clientHeight; + var UINodes = [ + {id: 'UI_up', shape: 'image', image: DIR + 'uparrow.png', + verticalAlignTop: false, x: 50, y: this.UIclientHeight - 50}, + {id: 'UI_down', shape: 'image', image: DIR + 'downarrow.png', + verticalAlignTop: false, x: 50, y: this.UIclientHeight - 20}, + {id: 'UI_left', shape: 'image', image: DIR + 'leftarrow.png', + verticalAlignTop: false, x: 20, y: this.UIclientHeight - 20}, + {id: 'UI_right', shape: 'image', image: DIR + 'rightarrow.png', + verticalAlignTop: false, x: 80, y: this.UIclientHeight - 20}, + {id: 'UI_plus', shape: 'image', image: DIR + 'plus.png', + verticalAlignTop: false, x: 130, y: this.UIclientHeight - 20}, + {id: 'UI_minus', shape: 'image', image: DIR + 'minus.png', + verticalAlignTop: false, x: 160, y: this.UIclientHeight - 20} + ]; + + for (var i = 0; i < UINodes.length; i++) { + this.sectors["UI"]['nodes'][UINodes[i]['id']] = new Node(UINodes[i], this.images, this.groups, this.constants); + } +}; + +Graph.prototype._relocateUI = function() { + var xOffset = this.UIclientWidth - this.frame.canvas.clientWidth; + var yOffset = this.UIclientHeight - this.frame.canvas.clientHeight; + this.UIclientWidth = this.frame.canvas.clientWidth; + this.UIclientHeight = this.frame.canvas.clientHeight; + var node = null; + for (var nodeId in this.sectors["UI"]["nodes"]) { + if (this.sectors["UI"]["nodes"].hasOwnProperty(nodeId)) { + node = this.sectors["UI"]["nodes"][nodeId]; + if (!node.horizontalAlignLeft) { + node.x -= xOffset; + } + if (!node.verticalAlignTop) { + node.y -= yOffset; + } + } + } +}; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/graph/Node.js b/src/graph/Node.js index ca1442c0..1f067753 100644 --- a/src/graph/Node.js +++ b/src/graph/Node.js @@ -44,6 +44,8 @@ function Node(properties, imagelist, grouplist, constants) { this.y = 0; this.xFixed = false; this.yFixed = false; + this.horizontalAlignLeft = true; // these are for the UI + this.verticalAlignTop = true; // these are for the UI this.radius = constants.nodes.radius; this.baseRadiusValue = constants.nodes.radius; this.radiusFixed = false; @@ -148,6 +150,10 @@ Node.prototype.setProperties = function(properties, constants) { if (properties.y !== undefined) {this.y = properties.y;} if (properties.value !== undefined) {this.value = properties.value;} + // UI properties + if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;} + if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;} + if (this.id === undefined) { throw "Node must have an id"; } @@ -484,6 +490,7 @@ Node.prototype.isOverlappingWith = function(obj) { Node.prototype._resizeImage = function (ctx) { // TODO: pre calculate the image size + if (!this.width || !this.height) { // undefined or 0 var width, height; if (this.value) { @@ -506,9 +513,9 @@ Node.prototype._resizeImage = function (ctx) { this.height = height; if (this.width && this.height) { - this.width += this.clusterSize * this.clusterSizeWidthFactor; - this.height += this.clusterSize * this.clusterSizeHeightFactor; - this.radius += this.clusterSize * this.clusterSizeRadiusFactor; + this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; } } @@ -531,6 +538,8 @@ Node.prototype._drawImage = function (ctx) { ctx.globalAlpha = 0.5; ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth); } + + // draw the image ctx.globalAlpha = 1.0; ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height); yLabel = this.y + this.height / 2; @@ -551,9 +560,9 @@ Node.prototype._resizeBox = function (ctx) { this.width = textSize.width + 2 * margin; this.height = textSize.height + 2 * margin; - this.width += this.clusterSize * 0.5 * this.clusterSizeWidthFactor; - this.height += this.clusterSize * 0.5 * this.clusterSizeHeightFactor; - //this.radius += this.clusterSize * this.clusterSizeRadiusFactor; + this.width += (this.clusterSize - 1) * 0.5 * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * 0.5 * this.clusterSizeHeightFactor; +// this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor; } }; @@ -600,9 +609,9 @@ Node.prototype._resizeDatabase = function (ctx) { this.height = size; // scaling used for clustering - this.width += this.clusterSize * this.clusterSizeWidthFactor; - this.height += this.clusterSize * this.clusterSizeHeightFactor; - this.radius += this.clusterSize * this.clusterSizeRadiusFactor; + this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; } }; @@ -649,9 +658,9 @@ Node.prototype._resizeCircle = function (ctx) { this.height = diameter; // scaling used for clustering - this.width += this.clusterSize * this.clusterSizeWidthFactor; - this.height += this.clusterSize * this.clusterSizeHeightFactor; - this.radius += this.clusterSize * 0.5*this.clusterSizeRadiusFactor; +// this.width += (this.clusterSize - 1) * 0.5 * this.clusterSizeWidthFactor; +// this.height += (this.clusterSize - 1) * 0.5 * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor; } }; @@ -697,9 +706,9 @@ Node.prototype._resizeEllipse = function (ctx) { } // scaling used for clustering - this.width += this.clusterSize * this.clusterSizeWidthFactor; - this.height += this.clusterSize * this.clusterSizeHeightFactor; - this.radius += this.clusterSize * this.clusterSizeRadiusFactor; + this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; } }; @@ -762,9 +771,9 @@ Node.prototype._resizeShape = function (ctx) { this.height = size; // scaling used for clustering - this.width += this.clusterSize * this.clusterSizeWidthFactor; - this.height += this.clusterSize * this.clusterSizeHeightFactor; - this.radius += this.clusterSize * 0.5 * this.clusterSizeRadiusFactor; + this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor; } }; @@ -821,9 +830,9 @@ Node.prototype._resizeText = function (ctx) { this.height = textSize.height + 2 * margin; // scaling used for clustering - this.width += this.clusterSize * this.clusterSizeWidthFactor; - this.height += this.clusterSize * this.clusterSizeHeightFactor; - this.radius += this.clusterSize * this.clusterSizeRadiusFactor; + this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor; + this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor; + this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor; } }; diff --git a/src/graph/SectorsMixin.js b/src/graph/SectorsMixin.js index 860103b5..3950dc00 100644 --- a/src/graph/SectorsMixin.js +++ b/src/graph/SectorsMixin.js @@ -70,6 +70,19 @@ var SectorMixin = { }, + /** + * This function sets the global references to nodes, edges and nodeIndices to + * those of the UI sector. + * + * @private + */ + _switchToUISector : function() { + this.nodeIndices = this.sectors["UI"]["nodeIndices"]; + this.nodes = this.sectors["UI"]["nodes"]; + this.edges = this.sectors["UI"]["edges"]; + }, + + /** * This function sets the global references to nodes, edges and nodeIndices back to * those of the currently active sector. @@ -347,11 +360,11 @@ var SectorMixin = { * @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 {*} [args] | Optional: arguments to pass to the runFunction + * @param {*} [argument] | Optional: arguments to pass to the runFunction * @private */ - _doInAllActiveSectors : function(runFunction,args) { - if (args === undefined) { + _doInAllActiveSectors : function(runFunction,argument) { + if (argument === undefined) { for (var sector in this.sectors["active"]) { if (this.sectors["active"].hasOwnProperty(sector)) { // switch the global references to those of this sector @@ -365,7 +378,13 @@ var SectorMixin = { if (this.sectors["active"].hasOwnProperty(sector)) { // switch the global references to those of this sector this._switchToActiveSector(sector); - this[runFunction](args); + var args = Array.prototype.splice.call(arguments, 1); + if (args.length > 1) { + this[runFunction](args[0],args[1]); + } + else { + this[runFunction](argument); + } } } } @@ -378,13 +397,13 @@ var SectorMixin = { * This runs a function in all frozen sectors. This is used in the _redraw(). * * @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 + * | we don't pass the function itself because then the "this" is the window object * | instead of the Graph object - * @param {*} [args] | Optional: arguments to pass to the runFunction + * @param {*} [argument] | Optional: arguments to pass to the runFunction * @private */ - _doInAllFrozenSectors : function(runFunction,args) { - if (args === undefined) { + _doInAllFrozenSectors : function(runFunction,argument) { + if (argument === undefined) { for (var sector in this.sectors["frozen"]) { if (this.sectors["frozen"].hasOwnProperty(sector)) { // switch the global references to those of this sector @@ -398,7 +417,13 @@ var SectorMixin = { if (this.sectors["frozen"].hasOwnProperty(sector)) { // switch the global references to those of this sector this._switchToFrozenSector(sector); - this[runFunction](args); + var args = Array.prototype.splice.call(arguments, 1); + if (args.length > 1) { + this[runFunction](args[0],args[1]); + } + else { + this[runFunction](argument); + } } } } @@ -406,11 +431,38 @@ var SectorMixin = { }, + /** + * This runs a function in all frozen sectors. This is used in the _redraw(). + * + * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors + * | we don't 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 + */ + _doInUISector : function(runFunction,argument) { + this._switchToUISector(); + if (argument === undefined) { + this[runFunction](); + } + else { + var args = Array.prototype.splice.call(arguments, 1); + if (args.length > 1) { + this[runFunction](args[0],args[1]); + } + else { + this[runFunction](argument); + } + } + this._loadLatestSector(); + }, + + /** * This runs a function in all sectors. This is used in the _redraw(). * * @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 + * | we don't 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 diff --git a/src/graph/SelectionMixin.js b/src/graph/SelectionMixin.js new file mode 100644 index 00000000..156a34c7 --- /dev/null +++ b/src/graph/SelectionMixin.js @@ -0,0 +1,27 @@ + +var SelectionMixin = { + + _getNodeAt : function(pointer) { + + }, + + _getEdgeAt : function(pointer) { + + }, + + _handleTap : function() { + + }, + + _selectObject : function() { + + }, + + _deselectObject : function() { + + } + + + + +}