/** * @constructor Cluster * Contains the cluster properties for the graph object */ function Cluster() { this.clusterSession = 0; this.hubThreshold = 5; } /** * This function can be called to open up a specific cluster. * It will unpack the cluster back one level. * * @param node | Node object: cluster to open. */ Cluster.prototype.openCluster = function(node) { if (node.clusterSize > 15) { this._addUniverse(node); } var isMovingBeforeClustering = this.moving; this._expandClusterNode(node,false,true); // housekeeping this._updateNodeIndexList(); this._updateDynamicEdges(); this.updateLabels(); // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded if (this.moving != isMovingBeforeClustering) { this.start(); } }; /** * 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 * be clustered with their connected node. This can be repeated as many times as needed. * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets. */ Cluster.prototype.increaseClusterLevel = function() { this.updateClusters(-1,false,true); }; /** * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will * be unpacked if they are a cluster. This can be repeated as many times as needed. * This can be called externally (by a key-bind for instance) to look into clusters without zooming. */ Cluster.prototype.decreaseClusterLevel = function() { this.updateClusters(1,false,true); }; /** * This function clusters on zoom, it can be called with a predefined zoom direction * If out, check if we can form clusters, if in, check if we can open clusters. * This function is only called from _zoom() * * @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 * */ Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) { var isMovingBeforeClustering = this.moving; var amountOfNodes = this.nodeIndices.length; // on zoom out collapse the universe back to default // if (this.previousScale > this.scale && zoomDirection == 0) { // this._collapseUniverse(); // } // check if we zoom in or out if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out this._formClusters(force); } else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom out this._openClusters(recursive,force); this._openClustersBySize(); } this._updateNodeIndexList(); // 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(); // if a cluster was formed, we increase the clusterSession if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place this.clusterSession += 1; } // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded if (this.moving != isMovingBeforeClustering) { this.start(); } }; /** * 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 * * @private */ Cluster.prototype._aggregateHubs = function(force) { this._getHubSize(); this._formClustersByHub(force); }; /** * This function is fired by keypress. It forces hubs to form. * */ Cluster.prototype.forceAggregateHubs = function() { var isMovingBeforeClustering = this.moving; var amountOfNodes = this.nodeIndices.length; this._aggregateHubs(true); // housekeeping this._updateNodeIndexList(); this._updateDynamicEdges(); this.updateLabels(); // if a cluster was formed, we increase the clusterSession if (this.nodeIndices.length != amountOfNodes) { this.clusterSession += 1; } // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded if (this.moving != isMovingBeforeClustering) { this.start(); } }; /** * If a cluster takes up more than a set percentage of the screen, open the cluster * * @private */ Cluster.prototype._openClustersBySize = function() { for (nodeID in this.nodes) { if (this.nodes.hasOwnProperty(nodeID)) { node = this.nodes[nodeID]; if (node.inView() == true) { if ((node.width*this.scale > this.constants.clustering.relativeOpenFactor * this.frame.canvas.clientWidth) || (node.height*this.scale > this.constants.clustering.relativeOpenFactor * this.frame.canvas.clientHeight)) { this.openCluster(node); this.openCluster(node); } } } } }; /** * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it * has to be opened based on the current zoom level. * * @private */ Cluster.prototype._openClusters = function(recursive,force) { for (var i = 0; i < this.nodeIndices.length; i++) { var node = this.nodes[this.nodeIndices[i]]; this._expandClusterNode(node,recursive,force); } }; /** * This function checks if a node has to be opened. This is done by checking the zoom level. * If the node contains child nodes, this function is recursively called on the child nodes as well. * This recursive behaviour is optional and can be set by the recursive argument. * * @param {Node} parentNode | to check for cluster and expand * @param {Boolean} recursive | enable or disable recursive calling * @param {Boolean} force | enable or disable forcing * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released * @private */ Cluster.prototype._expandClusterNode = function(parentNode, recursive, force, openAll) { // 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) { openAll = true; } recursive = openAll ? true : recursive; // if the last child has been added on a smaller scale than current scale (@optimization) if (parentNode.formationScale < this.scale || force == true) { // we will check if any of the contained child nodes should be removed from the cluster for (var containedNodeID in parentNode.containedNodes) { if (parentNode.containedNodes.hasOwnProperty(containedNodeID)) { var childNode = parentNode.containedNodes[containedNodeID]; // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that // the largest cluster is the one that comes from outside if (force == true) { if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1] || openAll) { this._expelChildFromParent(parentNode,containedNodeID,recursive,force,openAll); } } else { if (this._parentNodeInActiveArea(parentNode)) { this._expelChildFromParent(parentNode,containedNodeID,recursive,force,openAll); } } } } } } }; /** * ONLY CALLED FROM _expandClusterNode * * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove * the child node from the parent contained_node object and put it back into the global nodes object. * The same holds for the edge that was connected to the child node. It is moved back into the global edges object. * * @param {Node} parentNode | the parent node * @param {String} containedNodeID | child_node id as it is contained in the containedNodes object of the parent node * @param {Boolean} recursive | This will also check if the child needs to be expanded. * With force and recursive both true, the entire cluster is unpacked * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released * @private */ Cluster.prototype._expelChildFromParent = function(parentNode, containedNodeID, recursive, force, openAll) { var childNode = parentNode.containedNodes[containedNodeID]; // if child node has been added on smaller scale than current, kick out if (childNode.formationScale < this.scale || force == true) { // put the child node back in the global nodes object this.nodes[containedNodeID] = childNode; // release the contained edges from this childNode back into the global edges this._releaseContainedEdges(parentNode,childNode); // reconnect rerouted edges to the childNode this._connectEdgeBackToChild(parentNode,childNode); // validate all edges in dynamicEdges this._validateEdges(parentNode); // undo the changes from the clustering operation on the parent node parentNode.mass -= this.constants.clustering.massTransferCoefficient * childNode.mass; parentNode.fontSize -= this.constants.clustering.fontSizeMultiplier * childNode.clusterSize; parentNode.clusterSize -= childNode.clusterSize; 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.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; // recalculate the size of the node on the next time the node is rendered parentNode.clearSizeCache(); } // check if a further expansion step is possible if recursivity is enabled if (recursive == true) { this._expandClusterNode(childNode,recursive,force,openAll); } }; /** * This function checks if any nodes at the end of their trees have edges below a threshold length * This function is called only from updateClusters() * forceLevelCollapse ignores the length of the edge and collapses one level * This means that a node with only one edge will be clustered with its connected node * * @private * @param {Boolean} force */ Cluster.prototype._formClusters = function(force) { if (force == false) { this._formClustersByZoom(); } else { this._forceClustersByZoom(); } }; /** * This function handles the clustering by zooming out, this is based on a minimum edge distance * * @private */ Cluster.prototype._formClustersByZoom = function() { var dx,dy,length, minLength = this.constants.clustering.clusterLength/this.scale; // check if any edges are shorter than minLength and start the clustering // the clustering favours the node with the larger mass for (var edgeID in this.edges) { if (this.edges.hasOwnProperty(edgeID)) { var edge = this.edges[edgeID]; 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) { // first check which node is larger var parentNode = edge.from; var childNode = edge.to; if (edge.to.mass > edge.from.mass) { parentNode = edge.to; childNode = edge.from; } if (childNode.dynamicEdgesLength == 1) { this._addToCluster(parentNode,childNode,false); } else if (parentNode.dynamicEdgesLength == 1) { this._addToCluster(childNode,parentNode,false); } } } } } }; /** * This function forces the graph to cluster all nodes with only one connecting edge to their * connected node. * * @private */ Cluster.prototype._forceClustersByZoom = function() { for (var nodeID in this.nodes) { // another node could have absorbed this child. if (this.nodes.hasOwnProperty(nodeID)) { var childNode = this.nodes[nodeID]; // the edges can be swallowed by another decrease if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) { var edge = childNode.dynamicEdges[0]; var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId]; // group to the largest node if (parentNode.mass > childNode.mass) { this._addToCluster(parentNode,childNode,true); } else { this._addToCluster(childNode,parentNode,true); } } } } }; /** * This function forms clusters from hubs, it loops over all nodes * * @param {Boolean} force | Disregard zoom level * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges * @private */ 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)) { this._formClusterFromHub(this.nodes[nodeID],force,onlyEqual); } } }; /** * 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]]; // 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 + absorptionSizeOffset)) { this._addToCluster(hubNode,childNode,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() * * @param {Node} parentNode | this is the node that will house the child node * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse * @private */ Cluster.prototype._addToCluster = function(parentNode, childNode, force) { // join child node in the parent node parentNode.containedNodes[childNode.id] = childNode; // manage all the edges connected to the child and parent nodes for (var i = 0; i < childNode.dynamicEdges.length; i++) { var edge = childNode.dynamicEdges[i]; if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode this._addToContainedEdges(parentNode,childNode,edge); } else { this._connectEdgeToCluster(parentNode,childNode,edge); } } childNode.dynamicEdges = []; // remove the childNode from the global nodes object delete this.nodes[childNode.id]; var massBefore = parentNode.mass; childNode.clusterSession = this.clusterSession; parentNode.mass += this.constants.clustering.massTransferCoefficient * childNode.mass; parentNode.clusterSize += childNode.clusterSize; parentNode.fontSize += this.constants.clustering.fontSizeMultiplier * childNode.clusterSize; // keep track of the clustersessions so we can open the cluster up as it has been formed. if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) { parentNode.clusterSessions.push(this.clusterSession); } // 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+3); } else { parentNode.formationScale = this.scale; // The latest child has been added on this scale } // recalculate the size of the node on the next time the node is rendered parentNode.clearSizeCache(); // set the pop-out scale for the childnode parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale; // nullify the movement velocity of the child, this is to avoid hectic behaviour childNode.clearVelocity(); // the mass has altered, preservation of energy dictates the velocity to be updated parentNode.updateVelocity(massBefore); // restart the simulation to reorganise all nodes this.moving = true; }; /** * This function will apply the changes made to the remainingEdges during the formation of the clusters. * This is a seperate function to allow for level-wise collapsing of the node tree. * It has to be called if a level is collapsed. It is called by _formClusters(). * @private */ Cluster.prototype._updateDynamicEdges = function() { for (var i = 0; i < this.nodeIndices.length; i++) { var node = this.nodes[this.nodeIndices[i]]; node.dynamicEdgesLength = node.dynamicEdges.length; // this corrects for multiple edges pointing at the same other node var correction = 0; if (node.dynamicEdgesLength > 1) { for (var j = 0; j < node.dynamicEdgesLength - 1; j++) { var edgeToId = node.dynamicEdges[j].toId; var edgeFromId = node.dynamicEdges[j].fromId; for (var k = j+1; k < node.dynamicEdgesLength; k++) { if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) || (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) { correction += 1; } } } } node.dynamicEdgesLength -= correction; } }; /** * This adds an edge from the childNode to the contained edges of the parent node * * @param parentNode | Node object * @param childNode | Node object * @param edge | Edge object * @private */ Cluster.prototype._addToContainedEdges = function(parentNode, childNode, edge) { // create an array object if it does not yet exist for this childNode if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) { parentNode.containedEdges[childNode.id] = [] } // add this edge to the list parentNode.containedEdges[childNode.id].push(edge); // remove the edge from the global edges object delete this.edges[edge.id]; // remove the edge from the parent object for (var i = 0; i < parentNode.dynamicEdges.length; i++) { if (parentNode.dynamicEdges[i].id == edge.id) { parentNode.dynamicEdges.splice(i,1); break; } } }; /** * This function connects an edge that was connected to a child node to the parent node. * It keeps track of which nodes it has been connected to with the originalID array. * * @param parentNode | Node object * @param childNode | Node object * @param edge | Edge object * @private */ Cluster.prototype._connectEdgeToCluster = function(parentNode, childNode, edge) { if (edge.toId == childNode.id) { // edge connected to other node on the "to" side edge.originalToID.push(childNode.id); edge.to = parentNode; edge.toId = parentNode.id; } else { // edge connected to other node with the "from" side edge.originalFromID.push(childNode.id); edge.from = parentNode; edge.fromId = parentNode.id; } this._addToReroutedEdges(parentNode,childNode,edge); }; /** * This adds an edge from the childNode to the rerouted edges of the parent node * * @param parentNode | Node object * @param childNode | Node object * @param edge | Edge object * @private */ Cluster.prototype._addToReroutedEdges = function(parentNode, childNode, edge) { // create an array object if it does not yet exist for this childNode // we store the edge in the rerouted edges so we can restore it when the cluster pops open if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) { parentNode.reroutedEdges[childNode.id] = []; } parentNode.reroutedEdges[childNode.id].push(edge); // this edge becomes part of the dynamicEdges of the cluster node parentNode.dynamicEdges.push(edge); }; /** * This function connects an edge that was connected to a cluster node back to the child node. * * @param parentNode | Node object * @param childNode | Node object * @private */ Cluster.prototype._connectEdgeBackToChild = function(parentNode, childNode) { if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) { for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) { var edge = parentNode.reroutedEdges[childNode.id][i]; if (edge.originalFromID[edge.originalFromID.length-1] == childNode.id) { edge.originalFromID.pop(); edge.fromId = childNode.id; edge.from = childNode; } else { edge.originalToID.pop(); edge.toId = childNode.id; edge.to = childNode; } // append this edge to the list of edges connecting to the childnode childNode.dynamicEdges.push(edge); // remove the edge from the parent object for (var j = 0; j < parentNode.dynamicEdges.length; j++) { if (parentNode.dynamicEdges[j].id == edge.id) { parentNode.dynamicEdges.splice(j,1); break; } } } // remove the entry from the rerouted edges delete parentNode.reroutedEdges[childNode.id]; } }; /** * When loops are clustered, an edge can be both in the rerouted array and the contained array. * This function is called last to verify that all edges in dynamicEdges are in fact connected to the * parentNode * * @param parentNode | Node object * @private */ Cluster.prototype._validateEdges = function(parentNode) { for (var i = 0; i < parentNode.dynamicEdges.length; i++) { var edge = parentNode.dynamicEdges[i]; if (parentNode.id != edge.toId && parentNode.id != edge.fromId) { parentNode.dynamicEdges.splice(i,1); } } }; /** * This function released the contained edges back into the global domain and puts them back into the * dynamic edges of both parent and child. * * @param {Node} parentNode | * @param {Node} childNode | * @private */ Cluster.prototype._releaseContainedEdges = function(parentNode, childNode) { for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) { var edge = parentNode.containedEdges[childNode.id][i]; // put the edge back in the global edges object this.edges[edge.id] = edge; // put the edge back in the dynamic edges of the child and parent childNode.dynamicEdges.push(edge); parentNode.dynamicEdges.push(edge); } // remove the entry from the contained edges delete parentNode.containedEdges[childNode.id]; }; // ------------------- UTILITY FUNCTIONS ---------------------------- // /** * This updates the node labels for all nodes (for debugging purposes) */ Cluster.prototype.updateLabels = function() { var nodeID; // update node labels for (nodeID in this.nodes) { if (this.nodes.hasOwnProperty(nodeID)) { var node = this.nodes[nodeID]; if (node.clusterSize > 1) { node.label = "[".concat(String(node.clusterSize),"]"); } } } // update node labels for (nodeID in this.nodes) { if (this.nodes.hasOwnProperty(nodeID)) { node = this.nodes[nodeID]; if (node.clusterSize == 1) { node.label = String(node.id); } } } /* Debug Override */ // for (nodeID in this.nodes) { // if (this.nodes.hasOwnProperty(nodeID)) { // node = this.nodes[nodeID]; // node.label = String(Math.round(node.width)).concat(":",Math.round(node.width*this.scale)); // } // } }; /** * This function determines if the cluster we want to decluster is in the active area * this means around the zoom center * * @param {Node} node * @returns {boolean} * @private */ Cluster.prototype._parentNodeInActiveArea = function(node) { return ( Math.abs(node.x - this.zoomCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale && Math.abs(node.y - this.zoomCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale ) }; /** * 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. * */ Cluster.prototype.repositionNodes = function() { for (var i = 0; i < this.nodeIndices.length; i++) { var node = this.nodes[this.nodeIndices[i]]; if (!node.isFixed()) { var radius = this.constants.edges.length * (1 + 0.6*node.clusterSize); var angle = 2 * Math.PI * Math.random(); node.x = radius * Math.cos(angle); node.y = radius * Math.sin(angle); } } }; /** * We determine how many connections denote an important hub. * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%) * * @private */ Cluster.prototype._getHubSize = function() { var average = 0; var averageSquared = 0; var hubCounter = 0; var largestHub = 0; for (var i = 0; i < this.nodeIndices.length; i++) { var node = this.nodes[this.nodeIndices[i]]; if (node.dynamicEdgesLength > largestHub) { largestHub = node.dynamicEdgesLength; } average += node.dynamicEdgesLength; averageSquared += Math.pow(node.dynamicEdgesLength,2); hubCounter += 1; } average = average / hubCounter; averageSquared = averageSquared / hubCounter; var variance = averageSquared - Math.pow(average,2); var standardDeviation = Math.sqrt(variance); this.hubThreshold = Math.floor(average + 2*standardDeviation); // always have at least one to cluster if (this.hubThreshold > largestHub) { this.hubThreshold = largestHub; } // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation); // console.log("hubThreshold:",this.hubThreshold); }; /** * 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. * * @private */ Cluster.prototype._getSnakeFraction = function() { var snakes = 0; var total = 0; for (nodeID in this.nodes) { if (this.nodes.hasOwnProperty(nodeID)) { if (this.nodes[nodeID].dynamicEdgesLength == 2 && this.nodes[nodeID].dynamicEdges.length >= 2) { snakes += 1; } total += 1; } } return snakes/total; };