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 00000000..83ad74d6 Binary files /dev/null and b/examples/graph/img/UI/downarrow.png differ diff --git a/examples/graph/img/UI/leftarrow.png b/examples/graph/img/UI/leftarrow.png new file mode 100644 index 00000000..24f9b66c Binary files /dev/null and b/examples/graph/img/UI/leftarrow.png differ diff --git a/examples/graph/img/UI/minus.png b/examples/graph/img/UI/minus.png new file mode 100644 index 00000000..af0011e2 Binary files /dev/null and b/examples/graph/img/UI/minus.png differ diff --git a/examples/graph/img/UI/plus.png b/examples/graph/img/UI/plus.png new file mode 100644 index 00000000..255395e3 Binary files /dev/null and b/examples/graph/img/UI/plus.png differ diff --git a/examples/graph/img/UI/rightarrow.png b/examples/graph/img/UI/rightarrow.png new file mode 100644 index 00000000..a4dad7f7 Binary files /dev/null and b/examples/graph/img/UI/rightarrow.png differ diff --git a/examples/graph/img/UI/uparrow.png b/examples/graph/img/UI/uparrow.png new file mode 100644 index 00000000..80843618 Binary files /dev/null and b/examples/graph/img/UI/uparrow.png differ 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() { + + } + + + + +}