From 0e1b9d144c3ba8f0757c5a13d96d05692ba3e173 Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Thu, 16 Jan 2014 17:23:34 +0100 Subject: [PATCH] added snake removal, bugfixes, refactoring --- dist/vis.js | 443 ++++++++++++------- examples/graph/02.1_really_random_nodes.html | 2 +- src/graph/Graph.js | 216 +++++---- src/graph/cluster.js | 228 ++++++---- 4 files changed, 545 insertions(+), 344 deletions(-) diff --git a/dist/vis.js b/dist/vis.js index 43382583..95730e77 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -15046,7 +15046,7 @@ Cluster.prototype.openCluster = function(node) { // housekeeping this._updateNodeIndexList(); this._updateDynamicEdges(); - this._updateLabels(); + this.updateLabels(); // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded if (this.moving != isMovingBeforeClustering) { @@ -15054,8 +15054,14 @@ Cluster.prototype.openCluster = function(node) { } }; - - +/** + * This calls the updateClustes with default arguments + */ +Cluster.prototype.updateClustersDefault = function() { + if (this.constants.clustering.enableClustering) { + this.updateClusters(0,false,false); + } +} /** * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will @@ -15083,11 +15089,10 @@ Cluster.prototype.decreaseClusterLevel = function() { * If out, check if we can form clusters, if in, check if we can open clusters. * This function is only called from _zoom() * - * @param {Int} zoomDirection + * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn * @param {Boolean} recursive | enable or disable recursive calling of the opening of clusters * @param {Boolean} force | enable or disable forcing * - * @private */ Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) { var isMovingBeforeClustering = this.moving; @@ -15102,17 +15107,24 @@ Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) { } this._updateNodeIndexList(); - // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs and update the index again + // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) { this._aggregateHubs(force); + } + + // we now reduce snakes. + if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out + this.handleSnakes(); this._updateNodeIndexList(); } + + this.previousScale = this.scale; // rest of the housekeeping this._updateDynamicEdges(); - this._updateLabels(); + this.updateLabels(); // if a cluster was formed, we increase the clusterSession if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place @@ -15125,6 +15137,18 @@ Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) { } }; +/** + * This function handles the snakes. It is called on every updateClusters(). + */ +Cluster.prototype.handleSnakes = function() { + // after clustering we check how many snakes there are + var snakePercentage = this._getSnakeFraction(); + if (snakePercentage > this.constants.clustering.snakeThreshold) { + this._reduceAmountOfSnakes(1 - this.constants.clustering.snakeThreshold / snakePercentage) + + } +}; + /** * this functions starts clustering by hubs * The minimum hub threshold is set globally @@ -15133,7 +15157,7 @@ Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) { */ Cluster.prototype._aggregateHubs = function(force) { this._getHubSize(); - this._clusterByHub(force); + this._formClustersByHub(force); }; @@ -15150,7 +15174,7 @@ Cluster.prototype.forceAggregateHubs = function() { // housekeeping this._updateNodeIndexList(); this._updateDynamicEdges(); - this._updateLabels(); + this.updateLabels(); // if a cluster was formed, we increase the clusterSession if (this.nodeIndices.length != amountOfNodes) { @@ -15192,11 +15216,10 @@ Cluster.prototype._openClusters = function(recursive,force) { * @private */ Cluster.prototype._expandClusterNode = function(parentNode, recursive, force, openAll) { - var openedCluster = false; // first check if node is a cluster if (parentNode.clusterSize > 1) { // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20 - if (parentNode.clusterSize < 20 && force == false) { + if (parentNode.clusterSize < 20) { openAll = true; } recursive = openAll ? true : recursive; @@ -15213,21 +15236,16 @@ Cluster.prototype._expandClusterNode = function(parentNode, recursive, force, op if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1] || openAll) { this._expelChildFromParent(parentNode,containedNodeID,recursive,force,openAll); - openedCluster = true; } } else { if (this._parentNodeInActiveArea(parentNode)) { this._expelChildFromParent(parentNode,containedNodeID,recursive,force,openAll); - openedCluster = true; } } } } } - if (openedCluster == true) { - parentNode.clusterSessions.pop(); - } } }; @@ -15270,15 +15288,30 @@ Cluster.prototype._expelChildFromParent = function(parentNode, containedNodeID, parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length; // place the child node near the parent, not at the exact same location to avoid chaos in the system - childNode.x = parentNode.x + this.constants.edges.length * 0.2 * (0.5 - Math.random()) * parentNode.clusterSize; - childNode.y = parentNode.y + this.constants.edges.length * 0.2 * (0.5 - Math.random()) * parentNode.clusterSize; - - // remove the clusterSession from the child node - childNode.clusterSession = 0; + childNode.x = parentNode.x + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize; + childNode.y = parentNode.y + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize; // remove node from the list delete parentNode.containedNodes[containedNodeID]; + // check if there are other childs with this clusterSession in the parent. + var othersPresent = false; + for (var childNodeID in parentNode.containedNodes) { + if (parentNode.containedNodes.hasOwnProperty(childNodeID)) { + if (parentNode.containedNodes[childNodeID].clusterSession == childNode.clusterSession) { + othersPresent = true; + break; + } + } + } + // if there are no others, remove the cluster session from the list + if (othersPresent == false) { + parentNode.clusterSessions.pop(); + } + + // remove the clusterSession from the child node + childNode.clusterSession = 0; + // restart the simulation to reorganise all nodes this.moving = true; @@ -15384,66 +15417,86 @@ Cluster.prototype._forceClustersByZoom = function() { /** + * This function forms clusters from hubs, it loops over all nodes * - * @param {Boolean} force + * @param {Boolean} force | Disregard zoom level + * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges * @private */ -Cluster.prototype._clusterByHub = function(force) { - var dx,dy,length; - var minLength = this.constants.clustering.clusterLength/this.scale; - var allowCluster = false; - +Cluster.prototype._formClustersByHub = function(force, onlyEqual) { // we loop over all nodes in the list for (var nodeID in this.nodes) { // we check if it is still available since it can be used by the clustering in this loop if (this.nodes.hasOwnProperty(nodeID)) { - var hubNode = this.nodes[nodeID]; - - // we decide if the node is a hub - if (hubNode.dynamicEdgesLength >= this.hubThreshold) { - // we create a list of edges because the dynamicEdges change over the course of this loop - var edgesIDarray = []; - var amountOfInitialEdges = hubNode.dynamicEdges.length; - for (var j = 0; j < amountOfInitialEdges; j++) { - edgesIDarray.push(hubNode.dynamicEdges[j].id); - } + this._formClusterFromHub(this.nodes[nodeID],force,onlyEqual); + } + } +}; - // if the hub clustering is not forces, we check if one of the edges connected - // to a cluster is small enough based on the constants.clustering.clusterLength - if (force == false) { - allowCluster = false; - for (j = 0; j < amountOfInitialEdges; j++) { - var edge = this.edges[edgesIDarray[j]]; - if (edge !== undefined) { - if (edge.connected) { - dx = (edge.to.x - edge.from.x); - dy = (edge.to.y - edge.from.y); - length = Math.sqrt(dx * dx + dy * dy); - - if (length < minLength) { - allowCluster = true; - break; - } - } +/** + * This function forms a cluster from a specific preselected hub node + * + * @param {Node} hubNode | the node we will cluster as a hub + * @param {Boolean} force | Disregard zoom level + * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges + * @param [Number] absorptionSizeOffset | + * @private + */ +Cluster.prototype._formClusterFromHub = function(hubNode, force, onlyEqual, absorptionSizeOffset) { + if (absorptionSizeOffset === undefined) { + absorptionSizeOffset = 0; + } + + // we decide if the node is a hub + if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) || + (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) { + + // initialize variables + var dx,dy,length; + var minLength = this.constants.clustering.clusterLength/this.scale; + var allowCluster = false; + + // we create a list of edges because the dynamicEdges change over the course of this loop + var edgesIDarray = []; + var amountOfInitialEdges = hubNode.dynamicEdges.length; + for (var j = 0; j < amountOfInitialEdges; j++) { + edgesIDarray.push(hubNode.dynamicEdges[j].id); + } + + // if the hub clustering is not forces, we check if one of the edges connected + // to a cluster is small enough based on the constants.clustering.clusterLength + if (force == false) { + allowCluster = false; + for (j = 0; j < amountOfInitialEdges; j++) { + var edge = this.edges[edgesIDarray[j]]; + if (edge !== undefined) { + if (edge.connected) { + dx = (edge.to.x - edge.from.x); + dy = (edge.to.y - edge.from.y); + length = Math.sqrt(dx * dx + dy * dy); + + if (length < minLength) { + allowCluster = true; + break; } } } + } + } - // start the clustering if allowed - if ((!force && allowCluster) || force) { - // we loop over all edges INITIALLY connected to this hub - for (j = 0; j < amountOfInitialEdges; j++) { - edge = this.edges[edgesIDarray[j]]; + // start the clustering if allowed + if ((!force && allowCluster) || force) { + // we loop over all edges INITIALLY connected to this hub + for (j = 0; j < amountOfInitialEdges; j++) { + edge = this.edges[edgesIDarray[j]]; - // the edge can be clustered by this function in a previous loop - if (edge !== undefined) { - var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId]; + // the edge can be clustered by this function in a previous loop + if (edge !== undefined) { + var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId]; - // we do not want hubs to merge with other hubs. - if (childNode.dynamicEdges.length <= this.hubThreshold) { - this._addToCluster(hubNode,childNode,force); - } - } + // we do not want hubs to merge with other hubs. + if (childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) { + this._addToCluster(hubNode,childNode,force); } } } @@ -15453,7 +15506,6 @@ Cluster.prototype._clusterByHub = function(force) { - /** * This function adds the child node to the parent node, creating a cluster if it is not already. * This function is called only from updateClusters() @@ -15496,7 +15548,7 @@ Cluster.prototype._addToCluster = function(parentNode, childNode, force) { // giving the clusters a dynamic formationScale to ensure not all clusters open up when zoomed if (force == true) { - parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+2); + parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3); } else { parentNode.formationScale = this.scale; // The latest child has been added on this scale @@ -15674,7 +15726,6 @@ Cluster.prototype._connectEdgeBackToChild = function(parentNode, childNode) { * @private */ Cluster.prototype._validateEdges = function(parentNode) { - // TODO: check if good idea for (var i = 0; i < parentNode.dynamicEdges.length; i++) { var edge = parentNode.dynamicEdges[i]; if (parentNode.id != edge.toId && parentNode.id != edge.fromId) { @@ -15716,9 +15767,8 @@ Cluster.prototype._releaseContainedEdges = function(parentNode, childNode) { /** * This updates the node labels for all nodes (for debugging purposes) - * @private */ -Cluster.prototype._updateLabels = function() { +Cluster.prototype.updateLabels = function() { var nodeID; // update node labels for (nodeID in this.nodes) { @@ -15772,9 +15822,8 @@ Cluster.prototype._parentNodeInActiveArea = function(node) { * This is an adaptation of the original repositioning function. This is called if the system is clustered initially * It puts large clusters away from the center and randomizes the order. * - * @private */ -Cluster.prototype._repositionNodes = function() { +Cluster.prototype.repositionNodes = function() { for (var i = 0; i < this.nodeIndices.length; i++) { var node = this.nodes[this.nodeIndices[i]]; if (!node.isFixed()) { @@ -15830,24 +15879,46 @@ Cluster.prototype._getHubSize = function() { }; +/** + * We reduce the amount of "extension nodes" or snakes. These are not quickly clustered with the outliers and hubs methods + * with this amount we can cluster specifically on these snakes. + * + * @param {double} fraction | between 0 and 1, the percentage of snakes to reduce + * @private + */ +Cluster.prototype._reduceAmountOfSnakes = function(fraction) { + this.hubThreshold = 2; + for (nodeID in this.nodes) { + if (this.nodes.hasOwnProperty(nodeID)) { + if (this.nodes[nodeID].dynamicEdgesLength == 2 && this.nodes[nodeID].dynamicEdges.length >= 2) { + if (Math.random() <= fraction) { + this._formClusterFromHub(this.nodes[nodeID],true,true,1) + } + } + } + } +}; + /** * We get the amount of "extension nodes" or snakes. These are not quickly clustered with the outliers and hubs methods * with this amount we can cluster specifically on these snakes. * - * @returns {number} * @private */ -Cluster.prototype._getAmountOfSnakes = function() { +Cluster.prototype._getSnakeFraction = function() { var snakes = 0; + var total = 0; for (nodeID in this.nodes) { if (this.nodes.hasOwnProperty(nodeID)) { - if (this.nodes[nodeID].dynamicEdges.length == 2) { + if (this.nodes[nodeID].dynamicEdgesLength == 2 && this.nodes[nodeID].dynamicEdges.length >= 2) { snakes += 1; } + total += 1; } } - return snakes; + return snakes/total; }; + /** * @constructor Graph * Create a graph visualization, displaying nodes and edges. @@ -15913,13 +15984,14 @@ function Graph (container, data, options) { altLength: undefined } }, - clustering: { // TODO: naming of variables - maxNumberOfNodes: 100, // for automatic (initial) clustering // + clustering: { // TODO: naming of variables + enableClustering: false, + maxNumberOfNodes: 100, // for automatic (initial) clustering + snakeThreshold: 0.5, // maximum percentage of allowed snakes (long strings of connected nodes) clusterLength: 30, // threshold edge length for clusteringl - fontSizeMultiplier: 3, // how much the cluster font size grows per node (in px) + fontSizeMultiplier: 4, // how much the cluster font size grows per node (in px) forceAmplification: 0.7, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force distanceAmplification: 0.3, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force - edgeStrength: 0.01, edgeGrowth: 11, // amount of clusterSize connected to the edge is multiplied with this and added to edgeLength clusterSizeWidthFactor: 10, clusterSizeHeightFactor: 10, @@ -16001,23 +16073,30 @@ function Graph (container, data, options) { // apply options this.setOptions(options); - // draw data - this.setData(data); // TODO: option to render (start()) + var disableStart = this.constants.clustering.enableClustering; + + // load data + this.setData(data,disableStart); // // zoom so all data will fit on the screen this.zoomToFit(); - // cluster if the data set is big - this.clusterToFit(true); + if (this.constants.clustering.enableClustering) { + // cluster if the data set is big + this.clusterToFit(this.constants.clustering.maxNumberOfNodes, true); + + // updates the lables after clustering + this.updateLabels(); - // updates the lables after clustering - this._updateLabels(); + // this is called here because if clusterin is disabled, the start and stabilize are called in + // the setData function. - // find a stable position or start animating to a stable position - if (this.stabilize) { - this._doStabilize(); + // find a stable position or start animating to a stable position + if (this.stabilize) { + this._doStabilize(); + } + this.start(); } - this.start(); } /** @@ -16029,11 +16108,11 @@ Graph.prototype = Object.create(Cluster.prototype); /** * This function clusters until the maxNumberOfNodes has been reached * + * @param {Number} maxNumberOfNodes * @param {Boolean} reposition */ -Graph.prototype.clusterToFit = function(reposition) { +Graph.prototype.clusterToFit = function(maxNumberOfNodes, reposition) { var numberOfNodes = this.nodeIndices.length; - var maxNumberOfNodes = this.constants.clustering.maxNumberOfNodes; var maxLevels = 10; var level = 0; @@ -16054,7 +16133,7 @@ Graph.prototype.clusterToFit = function(reposition) { // after the clustering we reposition the nodes to avoid initial chaos if (level > 1 && reposition == true) { - this._repositionNodes(); + this.repositionNodes(); } }; @@ -16093,13 +16172,18 @@ Graph.prototype._updateNodeIndexList = function() { /** * Set nodes and edges, and optionally options as well. * - * @param {Object} data Object containing parameters: - * {Array | DataSet | DataView} [nodes] Array with nodes - * {Array | DataSet | DataView} [edges] Array with edges - * {String} [dot] String containing data in DOT format - * {Options} [options] Object with options + * @param {Object} data Object containing parameters: + * {Array | DataSet | DataView} [nodes] Array with nodes + * {Array | DataSet | DataView} [edges] Array with edges + * {String} [dot] String containing data in DOT format + * {Options} [options] Object with options + * @param {Boolean} [disableStart] | optional: disable the calling of the start function. */ -Graph.prototype.setData = function(data) { +Graph.prototype.setData = function(data, disableStart) { + if (disableStart === undefined) { + disableStart = false; + } + if (data && data.dot && (data.nodes || data.edges)) { throw new SyntaxError('Data must contain either parameter "dot" or ' + ' parameter pair "nodes" and "edges", but not both.'); @@ -16121,7 +16205,14 @@ Graph.prototype.setData = function(data) { this._setNodes(data && data.nodes); this._setEdges(data && data.edges); } - // updating the list of node indices + + if (!disableStart) { + // find a stable position or start animating to a stable position + if (this.stabilize) { + this._doStabilize(); + } + this.start(); + } }; /** @@ -16264,7 +16355,7 @@ Graph.prototype._create = function () { this.mouseTrap.bind("=",this.decreaseClusterLevel.bind(me)); this.mouseTrap.bind("-",this.increaseClusterLevel.bind(me)); this.mouseTrap.bind("s",this.singleStep.bind(me)); - this.mouseTrap.bind("h",this.forceAggregateHubs.bind(me)); + this.mouseTrap.bind("h",this.updateClustersDefault.bind(me)); this.mouseTrap.bind("f",this.toggleFreeze.bind(me)); // add the frame to the container element @@ -16447,6 +16538,7 @@ Graph.prototype._onTap = function (event) { if (node) { if (node.isSelected() && elapsedTime < 300) { this.openCluster(node); + this.openCluster(node); } // select this node this._selectNodes([nodeId]); @@ -16516,8 +16608,8 @@ Graph.prototype._onPinch = function (event) { */ Graph.prototype._zoom = function(scale, pointer) { var scaleOld = this._getScale(); - if (scale < 0.01) { - scale = 0.01; + if (scale < 0.001) { + scale = 0.001; } if (scale > 10) { scale = 10; @@ -16535,7 +16627,7 @@ Graph.prototype._zoom = function(scale, pointer) { // this.zoomCenter = {"x" : pointer.x,"y" : pointer.y }; this._setScale(scale); this._setTranslation(tx, ty); - this.updateClusters(0,false,false); + this.updateClustersDefault(); this._redraw(); //console.log("current zoomscale:",this.scale) @@ -16725,8 +16817,9 @@ Graph.prototype._unselectNodes = function(selection, triggerSelect) { // remove provided selections for (i = 0, iMax = selection.length; i < iMax; i++) { id = selection[i]; - this.nodes[id].unselect(); - + if (this.nodes.hasOwnProperty(id)) { + this.nodes[id].unselect(); + } var j = 0; while (j < this.selection.length) { if (this.selection[j] == id) { @@ -16743,7 +16836,9 @@ Graph.prototype._unselectNodes = function(selection, triggerSelect) { // remove all selections for (i = 0, iMax = this.selection.length; i < iMax; i++) { id = this.selection[i]; - this.nodes[id].unselect(); + if (this.nodes.hasOwnProperty(id)) { + this.nodes[id].unselect(); + } changed = true; } this.selection = []; @@ -17474,7 +17569,7 @@ Graph.prototype._drawEdges = function(ctx) { * @private */ Graph.prototype._doStabilize = function() { - var start = new Date(); + //var start = new Date(); // find stable position var count = 0; @@ -17487,7 +17582,7 @@ Graph.prototype._doStabilize = function() { count++; } - var end = new Date(); + // var end = new Date(); // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup }; @@ -17498,28 +17593,31 @@ Graph.prototype._doStabilize = function() { * @private */ Graph.prototype._calculateForces = function() { - if (this.nodeIndices.length == 1) { // stop calculation if there is only one node + // 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.maxNumberOfNodes * 4) { - console.log(this.nodeIndices.length, this.constants.clustering.maxNumberOfNodes * 4) - this.clusterToFit(false); + this.clusterToFit(this.constants.clustering.maxNumberOfNodes * 2, false); this._calculateForces(); } else { // create a local edge to the nodes and edges, that is faster - var id, dx, dy, angle, distance, fx, fy, + var dx, dy, angle, distance, fx, fy, repulsingForce, springForce, length, edgeLength, - nodes = this.nodes, - edges = this.edges; + 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 (var i = 0; i < this.nodeIndices.length; i++) { - var node = nodes[this.nodeIndices[i]]; + for (i = 0; i < this.nodeIndices.length; i++) { + node = nodes[this.nodeIndices[i]]; dx = -node.x - this.translation.x + this.frame.canvas.clientWidth*0.5; dy = -node.y - this.translation.y + this.frame.canvas.clientHeight*0.5; @@ -17531,7 +17629,7 @@ Graph.prototype._calculateForces = function() { node.updateDamping(this.nodeIndices.length); } - this._updateLabels(); + this.updateLabels(); // repulsing forces between nodes var minimumDistance = this.constants.nodes.distance, @@ -17540,11 +17638,11 @@ Graph.prototype._calculateForces = function() { // 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 (var i = 0; i < this.nodeIndices.length-1; i++) { - var node1 = nodes[this.nodeIndices[i]]; - for (var j = i+1; j < this.nodeIndices.length; j++) { - var node2 = nodes[this.nodeIndices[j]]; - var clusterSize = (node1.clusterSize + node2.clusterSize - 2); + 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); @@ -17564,7 +17662,6 @@ Graph.prototype._calculateForces = function() { //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; @@ -17577,38 +17674,53 @@ Graph.prototype._calculateForces = function() { } } - // TODO: re-implement repulsion of edges - for (var n = 0; n < nodes.length; n++) { - for (var l = 0; l < edges.length; l++) { - 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, - - // calculate normally distributed force - dx = nodes[n].x - lx, - dy = nodes[n].y - 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 / 2) - 1) * steepness)), // TODO: customize the repulsing force - fx = Math.cos(angle) * repulsingforce, - fy = Math.sin(angle) * repulsingforce; - nodes[n]._addForce(fx, fy); - edges[l].from._addForce(-fx/2,-fy/2); - edges[l].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 (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; + for (edgeID in edges) { + if (edges.hasOwnProperty(edgeID)) { + edge = edges[edgeID]; if (edge.connected) { - var clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2); + 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 @@ -17622,9 +17734,6 @@ Graph.prototype._calculateForces = function() { springForce = edge.stiffness * (edgeLength - length); - // boost strength of cluster springs - springForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.edgeStrength; - fx = Math.cos(angle) * springForce; fy = Math.sin(angle) * springForce; @@ -17633,7 +17742,7 @@ Graph.prototype._calculateForces = function() { } } } - /* +/* // TODO: re-implement repulsion of edges // repulsing forces between edges @@ -17670,7 +17779,7 @@ Graph.prototype._calculateForces = function() { edges[l2].to._addForce(fx, fy); } } - */ +*/ } }; @@ -17765,15 +17874,8 @@ Graph.prototype.singleStep = function() { } }; -/** - * Stop animating nodes and edges. - */ -Graph.prototype.stop = function () { - if (this.timer) { - window.clearInterval(this.timer); - this.timer = undefined; - } -}; + + /** * Freeze the animation @@ -17786,8 +17888,7 @@ Graph.prototype.toggleFreeze = function() { this.freezeSimulation = false; this.start(); } - console.log('freezeSimulation',this.freezeSimulation) -} +}; /** * vis.js module exports diff --git a/examples/graph/02.1_really_random_nodes.html b/examples/graph/02.1_really_random_nodes.html index 31088a73..75664226 100644 --- a/examples/graph/02.1_really_random_nodes.html +++ b/examples/graph/02.1_really_random_nodes.html @@ -38,7 +38,7 @@ var to = i; to = i; while (to == i) { - to = Math.floor(Math.random() * (nodeCount+1)); + to = Math.floor(Math.random() * (nodeCount)); } edges.push({ from: from, diff --git a/src/graph/Graph.js b/src/graph/Graph.js index 28860030..06601218 100644 --- a/src/graph/Graph.js +++ b/src/graph/Graph.js @@ -63,13 +63,14 @@ function Graph (container, data, options) { altLength: undefined } }, - clustering: { // TODO: naming of variables - maxNumberOfNodes: 100, // for automatic (initial) clustering // + clustering: { // TODO: naming of variables + enableClustering: false, + maxNumberOfNodes: 100, // for automatic (initial) clustering + snakeThreshold: 0.5, // maximum percentage of allowed snakes (long strings of connected nodes) clusterLength: 30, // threshold edge length for clusteringl - fontSizeMultiplier: 3, // how much the cluster font size grows per node (in px) + fontSizeMultiplier: 4, // how much the cluster font size grows per node (in px) forceAmplification: 0.7, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force distanceAmplification: 0.3, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force - edgeStrength: 0.01, edgeGrowth: 11, // amount of clusterSize connected to the edge is multiplied with this and added to edgeLength clusterSizeWidthFactor: 10, clusterSizeHeightFactor: 10, @@ -151,23 +152,30 @@ function Graph (container, data, options) { // apply options this.setOptions(options); - // draw data - this.setData(data); // TODO: option to render (start()) + var disableStart = this.constants.clustering.enableClustering; + + // load data + this.setData(data,disableStart); // // zoom so all data will fit on the screen this.zoomToFit(); - // cluster if the data set is big - this.clusterToFit(true); + if (this.constants.clustering.enableClustering) { + // cluster if the data set is big + this.clusterToFit(this.constants.clustering.maxNumberOfNodes, true); + + // updates the lables after clustering + this.updateLabels(); - // updates the lables after clustering - this._updateLabels(); + // this is called here because if clusterin is disabled, the start and stabilize are called in + // the setData function. - // find a stable position or start animating to a stable position - if (this.stabilize) { - this._doStabilize(); + // find a stable position or start animating to a stable position + if (this.stabilize) { + this._doStabilize(); + } + this.start(); } - this.start(); } /** @@ -179,11 +187,11 @@ Graph.prototype = Object.create(Cluster.prototype); /** * This function clusters until the maxNumberOfNodes has been reached * + * @param {Number} maxNumberOfNodes * @param {Boolean} reposition */ -Graph.prototype.clusterToFit = function(reposition) { +Graph.prototype.clusterToFit = function(maxNumberOfNodes, reposition) { var numberOfNodes = this.nodeIndices.length; - var maxNumberOfNodes = this.constants.clustering.maxNumberOfNodes; var maxLevels = 10; var level = 0; @@ -204,7 +212,7 @@ Graph.prototype.clusterToFit = function(reposition) { // after the clustering we reposition the nodes to avoid initial chaos if (level > 1 && reposition == true) { - this._repositionNodes(); + this.repositionNodes(); } }; @@ -243,13 +251,18 @@ Graph.prototype._updateNodeIndexList = function() { /** * Set nodes and edges, and optionally options as well. * - * @param {Object} data Object containing parameters: - * {Array | DataSet | DataView} [nodes] Array with nodes - * {Array | DataSet | DataView} [edges] Array with edges - * {String} [dot] String containing data in DOT format - * {Options} [options] Object with options + * @param {Object} data Object containing parameters: + * {Array | DataSet | DataView} [nodes] Array with nodes + * {Array | DataSet | DataView} [edges] Array with edges + * {String} [dot] String containing data in DOT format + * {Options} [options] Object with options + * @param {Boolean} [disableStart] | optional: disable the calling of the start function. */ -Graph.prototype.setData = function(data) { +Graph.prototype.setData = function(data, disableStart) { + if (disableStart === undefined) { + disableStart = false; + } + if (data && data.dot && (data.nodes || data.edges)) { throw new SyntaxError('Data must contain either parameter "dot" or ' + ' parameter pair "nodes" and "edges", but not both.'); @@ -271,7 +284,14 @@ Graph.prototype.setData = function(data) { this._setNodes(data && data.nodes); this._setEdges(data && data.edges); } - // updating the list of node indices + + if (!disableStart) { + // find a stable position or start animating to a stable position + if (this.stabilize) { + this._doStabilize(); + } + this.start(); + } }; /** @@ -414,7 +434,7 @@ Graph.prototype._create = function () { this.mouseTrap.bind("=",this.decreaseClusterLevel.bind(me)); this.mouseTrap.bind("-",this.increaseClusterLevel.bind(me)); this.mouseTrap.bind("s",this.singleStep.bind(me)); - this.mouseTrap.bind("h",this.forceAggregateHubs.bind(me)); + this.mouseTrap.bind("h",this.updateClustersDefault.bind(me)); this.mouseTrap.bind("f",this.toggleFreeze.bind(me)); // add the frame to the container element @@ -597,6 +617,7 @@ Graph.prototype._onTap = function (event) { if (node) { if (node.isSelected() && elapsedTime < 300) { this.openCluster(node); + this.openCluster(node); } // select this node this._selectNodes([nodeId]); @@ -666,8 +687,8 @@ Graph.prototype._onPinch = function (event) { */ Graph.prototype._zoom = function(scale, pointer) { var scaleOld = this._getScale(); - if (scale < 0.01) { - scale = 0.01; + if (scale < 0.001) { + scale = 0.001; } if (scale > 10) { scale = 10; @@ -685,7 +706,7 @@ Graph.prototype._zoom = function(scale, pointer) { // this.zoomCenter = {"x" : pointer.x,"y" : pointer.y }; this._setScale(scale); this._setTranslation(tx, ty); - this.updateClusters(0,false,false); + this.updateClustersDefault(); this._redraw(); //console.log("current zoomscale:",this.scale) @@ -875,8 +896,9 @@ Graph.prototype._unselectNodes = function(selection, triggerSelect) { // remove provided selections for (i = 0, iMax = selection.length; i < iMax; i++) { id = selection[i]; - this.nodes[id].unselect(); - + if (this.nodes.hasOwnProperty(id)) { + this.nodes[id].unselect(); + } var j = 0; while (j < this.selection.length) { if (this.selection[j] == id) { @@ -893,7 +915,9 @@ Graph.prototype._unselectNodes = function(selection, triggerSelect) { // remove all selections for (i = 0, iMax = this.selection.length; i < iMax; i++) { id = this.selection[i]; - this.nodes[id].unselect(); + if (this.nodes.hasOwnProperty(id)) { + this.nodes[id].unselect(); + } changed = true; } this.selection = []; @@ -1624,7 +1648,7 @@ Graph.prototype._drawEdges = function(ctx) { * @private */ Graph.prototype._doStabilize = function() { - var start = new Date(); + //var start = new Date(); // find stable position var count = 0; @@ -1637,7 +1661,7 @@ Graph.prototype._doStabilize = function() { count++; } - var end = new Date(); + // var end = new Date(); // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup }; @@ -1648,28 +1672,31 @@ Graph.prototype._doStabilize = function() { * @private */ Graph.prototype._calculateForces = function() { - if (this.nodeIndices.length == 1) { // stop calculation if there is only one node + // 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.maxNumberOfNodes * 4) { - console.log(this.nodeIndices.length, this.constants.clustering.maxNumberOfNodes * 4) - this.clusterToFit(false); + this.clusterToFit(this.constants.clustering.maxNumberOfNodes * 2, false); this._calculateForces(); } else { // create a local edge to the nodes and edges, that is faster - var id, dx, dy, angle, distance, fx, fy, + var dx, dy, angle, distance, fx, fy, repulsingForce, springForce, length, edgeLength, - nodes = this.nodes, - edges = this.edges; + 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 (var i = 0; i < this.nodeIndices.length; i++) { - var node = nodes[this.nodeIndices[i]]; + for (i = 0; i < this.nodeIndices.length; i++) { + node = nodes[this.nodeIndices[i]]; dx = -node.x - this.translation.x + this.frame.canvas.clientWidth*0.5; dy = -node.y - this.translation.y + this.frame.canvas.clientHeight*0.5; @@ -1681,7 +1708,7 @@ Graph.prototype._calculateForces = function() { node.updateDamping(this.nodeIndices.length); } - this._updateLabels(); + this.updateLabels(); // repulsing forces between nodes var minimumDistance = this.constants.nodes.distance, @@ -1690,11 +1717,11 @@ Graph.prototype._calculateForces = function() { // 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 (var i = 0; i < this.nodeIndices.length-1; i++) { - var node1 = nodes[this.nodeIndices[i]]; - for (var j = i+1; j < this.nodeIndices.length; j++) { - var node2 = nodes[this.nodeIndices[j]]; - var clusterSize = (node1.clusterSize + node2.clusterSize - 2); + 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); @@ -1714,7 +1741,6 @@ Graph.prototype._calculateForces = function() { //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; @@ -1727,38 +1753,53 @@ Graph.prototype._calculateForces = function() { } } - // TODO: re-implement repulsion of edges - for (var n = 0; n < nodes.length; n++) { - for (var l = 0; l < edges.length; l++) { - 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, - - // calculate normally distributed force - dx = nodes[n].x - lx, - dy = nodes[n].y - 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 / 2) - 1) * steepness)), // TODO: customize the repulsing force - fx = Math.cos(angle) * repulsingforce, - fy = Math.sin(angle) * repulsingforce; - nodes[n]._addForce(fx, fy); - edges[l].from._addForce(-fx/2,-fy/2); - edges[l].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 (id in edges) { - if (edges.hasOwnProperty(id)) { - var edge = edges[id]; + for (edgeID in edges) { + if (edges.hasOwnProperty(edgeID)) { + edge = edges[edgeID]; if (edge.connected) { - var clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2); + 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 @@ -1772,9 +1813,6 @@ Graph.prototype._calculateForces = function() { springForce = edge.stiffness * (edgeLength - length); - // boost strength of cluster springs - springForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.edgeStrength; - fx = Math.cos(angle) * springForce; fy = Math.sin(angle) * springForce; @@ -1783,7 +1821,7 @@ Graph.prototype._calculateForces = function() { } } } - /* +/* // TODO: re-implement repulsion of edges // repulsing forces between edges @@ -1820,7 +1858,7 @@ Graph.prototype._calculateForces = function() { edges[l2].to._addForce(fx, fy); } } - */ +*/ } }; @@ -1915,15 +1953,8 @@ Graph.prototype.singleStep = function() { } }; -/** - * Stop animating nodes and edges. - */ -Graph.prototype.stop = function () { - if (this.timer) { - window.clearInterval(this.timer); - this.timer = undefined; - } -}; + + /** * Freeze the animation @@ -1936,5 +1967,4 @@ Graph.prototype.toggleFreeze = function() { this.freezeSimulation = false; this.start(); } - console.log('freezeSimulation',this.freezeSimulation) -} +}; diff --git a/src/graph/cluster.js b/src/graph/cluster.js index ec155b2f..328cb060 100644 --- a/src/graph/cluster.js +++ b/src/graph/cluster.js @@ -22,7 +22,7 @@ Cluster.prototype.openCluster = function(node) { // housekeeping this._updateNodeIndexList(); this._updateDynamicEdges(); - this._updateLabels(); + this.updateLabels(); // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded if (this.moving != isMovingBeforeClustering) { @@ -30,8 +30,14 @@ Cluster.prototype.openCluster = function(node) { } }; - - +/** + * This calls the updateClustes with default arguments + */ +Cluster.prototype.updateClustersDefault = function() { + if (this.constants.clustering.enableClustering) { + this.updateClusters(0,false,false); + } +} /** * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will @@ -59,11 +65,10 @@ Cluster.prototype.decreaseClusterLevel = function() { * If out, check if we can form clusters, if in, check if we can open clusters. * This function is only called from _zoom() * - * @param {Int} zoomDirection + * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn * @param {Boolean} recursive | enable or disable recursive calling of the opening of clusters * @param {Boolean} force | enable or disable forcing * - * @private */ Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) { var isMovingBeforeClustering = this.moving; @@ -78,17 +83,24 @@ Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) { } this._updateNodeIndexList(); - // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs and update the index again + // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) { this._aggregateHubs(force); + } + + // we now reduce snakes. + if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out + this.handleSnakes(); this._updateNodeIndexList(); } + + this.previousScale = this.scale; // rest of the housekeeping this._updateDynamicEdges(); - this._updateLabels(); + this.updateLabels(); // if a cluster was formed, we increase the clusterSession if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place @@ -101,6 +113,18 @@ Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) { } }; +/** + * This function handles the snakes. It is called on every updateClusters(). + */ +Cluster.prototype.handleSnakes = function() { + // after clustering we check how many snakes there are + var snakePercentage = this._getSnakeFraction(); + if (snakePercentage > this.constants.clustering.snakeThreshold) { + this._reduceAmountOfSnakes(1 - this.constants.clustering.snakeThreshold / snakePercentage) + + } +}; + /** * this functions starts clustering by hubs * The minimum hub threshold is set globally @@ -109,7 +133,7 @@ Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) { */ Cluster.prototype._aggregateHubs = function(force) { this._getHubSize(); - this._clusterByHub(force); + this._formClustersByHub(force); }; @@ -126,7 +150,7 @@ Cluster.prototype.forceAggregateHubs = function() { // housekeeping this._updateNodeIndexList(); this._updateDynamicEdges(); - this._updateLabels(); + this.updateLabels(); // if a cluster was formed, we increase the clusterSession if (this.nodeIndices.length != amountOfNodes) { @@ -168,11 +192,10 @@ Cluster.prototype._openClusters = function(recursive,force) { * @private */ Cluster.prototype._expandClusterNode = function(parentNode, recursive, force, openAll) { - var openedCluster = false; // first check if node is a cluster if (parentNode.clusterSize > 1) { // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20 - if (parentNode.clusterSize < 20 && force == false) { + if (parentNode.clusterSize < 20) { openAll = true; } recursive = openAll ? true : recursive; @@ -189,21 +212,16 @@ Cluster.prototype._expandClusterNode = function(parentNode, recursive, force, op if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1] || openAll) { this._expelChildFromParent(parentNode,containedNodeID,recursive,force,openAll); - openedCluster = true; } } else { if (this._parentNodeInActiveArea(parentNode)) { this._expelChildFromParent(parentNode,containedNodeID,recursive,force,openAll); - openedCluster = true; } } } } } - if (openedCluster == true) { - parentNode.clusterSessions.pop(); - } } }; @@ -246,15 +264,30 @@ Cluster.prototype._expelChildFromParent = function(parentNode, containedNodeID, parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length; // place the child node near the parent, not at the exact same location to avoid chaos in the system - childNode.x = parentNode.x + this.constants.edges.length * 0.2 * (0.5 - Math.random()) * parentNode.clusterSize; - childNode.y = parentNode.y + this.constants.edges.length * 0.2 * (0.5 - Math.random()) * parentNode.clusterSize; - - // remove the clusterSession from the child node - childNode.clusterSession = 0; + childNode.x = parentNode.x + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize; + childNode.y = parentNode.y + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize; // remove node from the list delete parentNode.containedNodes[containedNodeID]; + // check if there are other childs with this clusterSession in the parent. + var othersPresent = false; + for (var childNodeID in parentNode.containedNodes) { + if (parentNode.containedNodes.hasOwnProperty(childNodeID)) { + if (parentNode.containedNodes[childNodeID].clusterSession == childNode.clusterSession) { + othersPresent = true; + break; + } + } + } + // if there are no others, remove the cluster session from the list + if (othersPresent == false) { + parentNode.clusterSessions.pop(); + } + + // remove the clusterSession from the child node + childNode.clusterSession = 0; + // restart the simulation to reorganise all nodes this.moving = true; @@ -360,66 +393,86 @@ Cluster.prototype._forceClustersByZoom = function() { /** + * This function forms clusters from hubs, it loops over all nodes * - * @param {Boolean} force + * @param {Boolean} force | Disregard zoom level + * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges * @private */ -Cluster.prototype._clusterByHub = function(force) { - var dx,dy,length; - var minLength = this.constants.clustering.clusterLength/this.scale; - var allowCluster = false; - +Cluster.prototype._formClustersByHub = function(force, onlyEqual) { // we loop over all nodes in the list for (var nodeID in this.nodes) { // we check if it is still available since it can be used by the clustering in this loop if (this.nodes.hasOwnProperty(nodeID)) { - var hubNode = this.nodes[nodeID]; - - // we decide if the node is a hub - if (hubNode.dynamicEdgesLength >= this.hubThreshold) { - // we create a list of edges because the dynamicEdges change over the course of this loop - var edgesIDarray = []; - var amountOfInitialEdges = hubNode.dynamicEdges.length; - for (var j = 0; j < amountOfInitialEdges; j++) { - edgesIDarray.push(hubNode.dynamicEdges[j].id); - } + this._formClusterFromHub(this.nodes[nodeID],force,onlyEqual); + } + } +}; - // if the hub clustering is not forces, we check if one of the edges connected - // to a cluster is small enough based on the constants.clustering.clusterLength - if (force == false) { - allowCluster = false; - for (j = 0; j < amountOfInitialEdges; j++) { - var edge = this.edges[edgesIDarray[j]]; - if (edge !== undefined) { - if (edge.connected) { - dx = (edge.to.x - edge.from.x); - dy = (edge.to.y - edge.from.y); - length = Math.sqrt(dx * dx + dy * dy); - - if (length < minLength) { - allowCluster = true; - break; - } - } +/** + * This function forms a cluster from a specific preselected hub node + * + * @param {Node} hubNode | the node we will cluster as a hub + * @param {Boolean} force | Disregard zoom level + * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges + * @param [Number] absorptionSizeOffset | + * @private + */ +Cluster.prototype._formClusterFromHub = function(hubNode, force, onlyEqual, absorptionSizeOffset) { + if (absorptionSizeOffset === undefined) { + absorptionSizeOffset = 0; + } + + // we decide if the node is a hub + if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) || + (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) { + + // initialize variables + var dx,dy,length; + var minLength = this.constants.clustering.clusterLength/this.scale; + var allowCluster = false; + + // we create a list of edges because the dynamicEdges change over the course of this loop + var edgesIDarray = []; + var amountOfInitialEdges = hubNode.dynamicEdges.length; + for (var j = 0; j < amountOfInitialEdges; j++) { + edgesIDarray.push(hubNode.dynamicEdges[j].id); + } + + // if the hub clustering is not forces, we check if one of the edges connected + // to a cluster is small enough based on the constants.clustering.clusterLength + if (force == false) { + allowCluster = false; + for (j = 0; j < amountOfInitialEdges; j++) { + var edge = this.edges[edgesIDarray[j]]; + if (edge !== undefined) { + if (edge.connected) { + dx = (edge.to.x - edge.from.x); + dy = (edge.to.y - edge.from.y); + length = Math.sqrt(dx * dx + dy * dy); + + if (length < minLength) { + allowCluster = true; + break; } } } + } + } - // start the clustering if allowed - if ((!force && allowCluster) || force) { - // we loop over all edges INITIALLY connected to this hub - for (j = 0; j < amountOfInitialEdges; j++) { - edge = this.edges[edgesIDarray[j]]; + // start the clustering if allowed + if ((!force && allowCluster) || force) { + // we loop over all edges INITIALLY connected to this hub + for (j = 0; j < amountOfInitialEdges; j++) { + edge = this.edges[edgesIDarray[j]]; - // the edge can be clustered by this function in a previous loop - if (edge !== undefined) { - var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId]; + // the edge can be clustered by this function in a previous loop + if (edge !== undefined) { + var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId]; - // we do not want hubs to merge with other hubs. - if (childNode.dynamicEdges.length <= this.hubThreshold) { - this._addToCluster(hubNode,childNode,force); - } - } + // we do not want hubs to merge with other hubs. + if (childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) { + this._addToCluster(hubNode,childNode,force); } } } @@ -429,7 +482,6 @@ Cluster.prototype._clusterByHub = function(force) { - /** * This function adds the child node to the parent node, creating a cluster if it is not already. * This function is called only from updateClusters() @@ -472,7 +524,7 @@ Cluster.prototype._addToCluster = function(parentNode, childNode, force) { // giving the clusters a dynamic formationScale to ensure not all clusters open up when zoomed if (force == true) { - parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+2); + parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3); } else { parentNode.formationScale = this.scale; // The latest child has been added on this scale @@ -650,7 +702,6 @@ Cluster.prototype._connectEdgeBackToChild = function(parentNode, childNode) { * @private */ Cluster.prototype._validateEdges = function(parentNode) { - // TODO: check if good idea for (var i = 0; i < parentNode.dynamicEdges.length; i++) { var edge = parentNode.dynamicEdges[i]; if (parentNode.id != edge.toId && parentNode.id != edge.fromId) { @@ -692,9 +743,8 @@ Cluster.prototype._releaseContainedEdges = function(parentNode, childNode) { /** * This updates the node labels for all nodes (for debugging purposes) - * @private */ -Cluster.prototype._updateLabels = function() { +Cluster.prototype.updateLabels = function() { var nodeID; // update node labels for (nodeID in this.nodes) { @@ -748,9 +798,8 @@ Cluster.prototype._parentNodeInActiveArea = function(node) { * This is an adaptation of the original repositioning function. This is called if the system is clustered initially * It puts large clusters away from the center and randomizes the order. * - * @private */ -Cluster.prototype._repositionNodes = function() { +Cluster.prototype.repositionNodes = function() { for (var i = 0; i < this.nodeIndices.length; i++) { var node = this.nodes[this.nodeIndices[i]]; if (!node.isFixed()) { @@ -806,21 +855,42 @@ Cluster.prototype._getHubSize = function() { }; +/** + * We reduce the amount of "extension nodes" or snakes. These are not quickly clustered with the outliers and hubs methods + * with this amount we can cluster specifically on these snakes. + * + * @param {double} fraction | between 0 and 1, the percentage of snakes to reduce + * @private + */ +Cluster.prototype._reduceAmountOfSnakes = function(fraction) { + this.hubThreshold = 2; + for (nodeID in this.nodes) { + if (this.nodes.hasOwnProperty(nodeID)) { + if (this.nodes[nodeID].dynamicEdgesLength == 2 && this.nodes[nodeID].dynamicEdges.length >= 2) { + if (Math.random() <= fraction) { + this._formClusterFromHub(this.nodes[nodeID],true,true,1) + } + } + } + } +}; + /** * We get the amount of "extension nodes" or snakes. These are not quickly clustered with the outliers and hubs methods * with this amount we can cluster specifically on these snakes. * - * @returns {number} * @private */ -Cluster.prototype._getAmountOfSnakes = function() { +Cluster.prototype._getSnakeFraction = function() { var snakes = 0; + var total = 0; for (nodeID in this.nodes) { if (this.nodes.hasOwnProperty(nodeID)) { - if (this.nodes[nodeID].dynamicEdges.length == 2) { + if (this.nodes[nodeID].dynamicEdgesLength == 2 && this.nodes[nodeID].dynamicEdges.length >= 2) { snakes += 1; } + total += 1; } } - return snakes; -}; \ No newline at end of file + return snakes/total; +};