Browse Source

added snake removal, bugfixes, refactoring

css_transitions
Alex de Mulder 11 years ago
parent
commit
0e1b9d144c
4 changed files with 545 additions and 344 deletions
  1. +272
    -171
      dist/vis.js
  2. +1
    -1
      examples/graph/02.1_really_random_nodes.html
  3. +123
    -93
      src/graph/Graph.js
  4. +149
    -79
      src/graph/cluster.js

+ 272
- 171
dist/vis.js View File

@ -15046,7 +15046,7 @@ Cluster.prototype.openCluster = function(node) {
// housekeeping // housekeeping
this._updateNodeIndexList(); this._updateNodeIndexList();
this._updateDynamicEdges(); this._updateDynamicEdges();
this._updateLabels();
this.updateLabels();
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) { 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 * 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. * If out, check if we can form clusters, if in, check if we can open clusters.
* This function is only called from _zoom() * 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} recursive | enable or disable recursive calling of the opening of clusters
* @param {Boolean} force | enable or disable forcing * @param {Boolean} force | enable or disable forcing
* *
* @private
*/ */
Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) { Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) {
var isMovingBeforeClustering = this.moving; var isMovingBeforeClustering = this.moving;
@ -15102,17 +15107,24 @@ Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) {
} }
this._updateNodeIndexList(); 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)) { if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
this._aggregateHubs(force); this._aggregateHubs(force);
}
// we now reduce snakes.
if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
this.handleSnakes();
this._updateNodeIndexList(); this._updateNodeIndexList();
} }
this.previousScale = this.scale; this.previousScale = this.scale;
// rest of the housekeeping // rest of the housekeeping
this._updateDynamicEdges(); this._updateDynamicEdges();
this._updateLabels();
this.updateLabels();
// if a cluster was formed, we increase the clusterSession // if a cluster was formed, we increase the clusterSession
if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place 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 * this functions starts clustering by hubs
* The minimum hub threshold is set globally * The minimum hub threshold is set globally
@ -15133,7 +15157,7 @@ Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) {
*/ */
Cluster.prototype._aggregateHubs = function(force) { Cluster.prototype._aggregateHubs = function(force) {
this._getHubSize(); this._getHubSize();
this._clusterByHub(force);
this._formClustersByHub(force);
}; };
@ -15150,7 +15174,7 @@ Cluster.prototype.forceAggregateHubs = function() {
// housekeeping // housekeeping
this._updateNodeIndexList(); this._updateNodeIndexList();
this._updateDynamicEdges(); this._updateDynamicEdges();
this._updateLabels();
this.updateLabels();
// if a cluster was formed, we increase the clusterSession // if a cluster was formed, we increase the clusterSession
if (this.nodeIndices.length != amountOfNodes) { if (this.nodeIndices.length != amountOfNodes) {
@ -15192,11 +15216,10 @@ Cluster.prototype._openClusters = function(recursive,force) {
* @private * @private
*/ */
Cluster.prototype._expandClusterNode = function(parentNode, recursive, force, openAll) { Cluster.prototype._expandClusterNode = function(parentNode, recursive, force, openAll) {
var openedCluster = false;
// first check if node is a cluster // first check if node is a cluster
if (parentNode.clusterSize > 1) { 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 // 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; openAll = true;
} }
recursive = openAll ? true : recursive; 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] if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1]
|| openAll) { || openAll) {
this._expelChildFromParent(parentNode,containedNodeID,recursive,force,openAll); this._expelChildFromParent(parentNode,containedNodeID,recursive,force,openAll);
openedCluster = true;
} }
} }
else { else {
if (this._parentNodeInActiveArea(parentNode)) { if (this._parentNodeInActiveArea(parentNode)) {
this._expelChildFromParent(parentNode,containedNodeID,recursive,force,openAll); 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; parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
// place the child node near the parent, not at the exact same location to avoid chaos in the system // 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 // remove node from the list
delete parentNode.containedNodes[containedNodeID]; 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 // restart the simulation to reorganise all nodes
this.moving = true; 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 * @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 // we loop over all nodes in the list
for (var nodeID in this.nodes) { for (var nodeID in this.nodes) {
// we check if it is still available since it can be used by the clustering in this loop // we check if it is still available since it can be used by the clustering in this loop
if (this.nodes.hasOwnProperty(nodeID)) { 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 adds the child node to the parent node, creating a cluster if it is not already.
* This function is called only from updateClusters() * 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 // giving the clusters a dynamic formationScale to ensure not all clusters open up when zoomed
if (force == true) { 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 { else {
parentNode.formationScale = this.scale; // The latest child has been added on this scale parentNode.formationScale = this.scale; // The latest child has been added on this scale
@ -15674,7 +15726,6 @@ Cluster.prototype._connectEdgeBackToChild = function(parentNode, childNode) {
* @private * @private
*/ */
Cluster.prototype._validateEdges = function(parentNode) { Cluster.prototype._validateEdges = function(parentNode) {
// TODO: check if good idea
for (var i = 0; i < parentNode.dynamicEdges.length; i++) { for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
var edge = parentNode.dynamicEdges[i]; var edge = parentNode.dynamicEdges[i];
if (parentNode.id != edge.toId && parentNode.id != edge.fromId) { 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) * This updates the node labels for all nodes (for debugging purposes)
* @private
*/ */
Cluster.prototype._updateLabels = function() {
Cluster.prototype.updateLabels = function() {
var nodeID; var nodeID;
// update node labels // update node labels
for (nodeID in this.nodes) { 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 * 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. * 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++) { for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]]; var node = this.nodes[this.nodeIndices[i]];
if (!node.isFixed()) { 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 * 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. * with this amount we can cluster specifically on these snakes.
* *
* @returns {number}
* @private * @private
*/ */
Cluster.prototype._getAmountOfSnakes = function() {
Cluster.prototype._getSnakeFraction = function() {
var snakes = 0; var snakes = 0;
var total = 0;
for (nodeID in this.nodes) { for (nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) { 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; snakes += 1;
} }
total += 1;
} }
} }
return snakes;
return snakes/total;
}; };
/** /**
* @constructor Graph * @constructor Graph
* Create a graph visualization, displaying nodes and edges. * Create a graph visualization, displaying nodes and edges.
@ -15913,13 +15984,14 @@ function Graph (container, data, options) {
altLength: undefined 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 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 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 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 edgeGrowth: 11, // amount of clusterSize connected to the edge is multiplied with this and added to edgeLength
clusterSizeWidthFactor: 10, clusterSizeWidthFactor: 10,
clusterSizeHeightFactor: 10, clusterSizeHeightFactor: 10,
@ -16001,23 +16073,30 @@ function Graph (container, data, options) {
// apply options // apply options
this.setOptions(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 // zoom so all data will fit on the screen
this.zoomToFit(); 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 * This function clusters until the maxNumberOfNodes has been reached
* *
* @param {Number} maxNumberOfNodes
* @param {Boolean} reposition * @param {Boolean} reposition
*/ */
Graph.prototype.clusterToFit = function(reposition) {
Graph.prototype.clusterToFit = function(maxNumberOfNodes, reposition) {
var numberOfNodes = this.nodeIndices.length; var numberOfNodes = this.nodeIndices.length;
var maxNumberOfNodes = this.constants.clustering.maxNumberOfNodes;
var maxLevels = 10; var maxLevels = 10;
var level = 0; var level = 0;
@ -16054,7 +16133,7 @@ Graph.prototype.clusterToFit = function(reposition) {
// after the clustering we reposition the nodes to avoid initial chaos // after the clustering we reposition the nodes to avoid initial chaos
if (level > 1 && reposition == true) { 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. * 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)) { if (data && data.dot && (data.nodes || data.edges)) {
throw new SyntaxError('Data must contain either parameter "dot" or ' + throw new SyntaxError('Data must contain either parameter "dot" or ' +
' parameter pair "nodes" and "edges", but not both.'); ' parameter pair "nodes" and "edges", but not both.');
@ -16121,7 +16205,14 @@ Graph.prototype.setData = function(data) {
this._setNodes(data && data.nodes); this._setNodes(data && data.nodes);
this._setEdges(data && data.edges); 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.decreaseClusterLevel.bind(me));
this.mouseTrap.bind("-",this.increaseClusterLevel.bind(me)); this.mouseTrap.bind("-",this.increaseClusterLevel.bind(me));
this.mouseTrap.bind("s",this.singleStep.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)); this.mouseTrap.bind("f",this.toggleFreeze.bind(me));
// add the frame to the container element // add the frame to the container element
@ -16447,6 +16538,7 @@ Graph.prototype._onTap = function (event) {
if (node) { if (node) {
if (node.isSelected() && elapsedTime < 300) { if (node.isSelected() && elapsedTime < 300) {
this.openCluster(node); this.openCluster(node);
this.openCluster(node);
} }
// select this node // select this node
this._selectNodes([nodeId]); this._selectNodes([nodeId]);
@ -16516,8 +16608,8 @@ Graph.prototype._onPinch = function (event) {
*/ */
Graph.prototype._zoom = function(scale, pointer) { Graph.prototype._zoom = function(scale, pointer) {
var scaleOld = this._getScale(); var scaleOld = this._getScale();
if (scale < 0.01) {
scale = 0.01;
if (scale < 0.001) {
scale = 0.001;
} }
if (scale > 10) { if (scale > 10) {
scale = 10; scale = 10;
@ -16535,7 +16627,7 @@ Graph.prototype._zoom = function(scale, pointer) {
// this.zoomCenter = {"x" : pointer.x,"y" : pointer.y }; // this.zoomCenter = {"x" : pointer.x,"y" : pointer.y };
this._setScale(scale); this._setScale(scale);
this._setTranslation(tx, ty); this._setTranslation(tx, ty);
this.updateClusters(0,false,false);
this.updateClustersDefault();
this._redraw(); this._redraw();
//console.log("current zoomscale:",this.scale) //console.log("current zoomscale:",this.scale)
@ -16725,8 +16817,9 @@ Graph.prototype._unselectNodes = function(selection, triggerSelect) {
// remove provided selections // remove provided selections
for (i = 0, iMax = selection.length; i < iMax; i++) { for (i = 0, iMax = selection.length; i < iMax; i++) {
id = selection[i]; id = selection[i];
this.nodes[id].unselect();
if (this.nodes.hasOwnProperty(id)) {
this.nodes[id].unselect();
}
var j = 0; var j = 0;
while (j < this.selection.length) { while (j < this.selection.length) {
if (this.selection[j] == id) { if (this.selection[j] == id) {
@ -16743,7 +16836,9 @@ Graph.prototype._unselectNodes = function(selection, triggerSelect) {
// remove all selections // remove all selections
for (i = 0, iMax = this.selection.length; i < iMax; i++) { for (i = 0, iMax = this.selection.length; i < iMax; i++) {
id = this.selection[i]; id = this.selection[i];
this.nodes[id].unselect();
if (this.nodes.hasOwnProperty(id)) {
this.nodes[id].unselect();
}
changed = true; changed = true;
} }
this.selection = []; this.selection = [];
@ -17474,7 +17569,7 @@ Graph.prototype._drawEdges = function(ctx) {
* @private * @private
*/ */
Graph.prototype._doStabilize = function() { Graph.prototype._doStabilize = function() {
var start = new Date();
//var start = new Date();
// find stable position // find stable position
var count = 0; var count = 0;
@ -17487,7 +17582,7 @@ Graph.prototype._doStabilize = function() {
count++; count++;
} }
var end = new Date();
// var end = new Date();
// console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup
}; };
@ -17498,28 +17593,31 @@ Graph.prototype._doStabilize = function() {
* @private * @private
*/ */
Graph.prototype._calculateForces = function() { 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); 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) { 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(); this._calculateForces();
} }
else { else {
// create a local edge to the nodes and edges, that is faster // 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, 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 // Gravity is required to keep separated groups from floating off
// the forces are reset to zero in this loop by using _setForce instead // the forces are reset to zero in this loop by using _setForce instead
// of _addForce // of _addForce
var gravity = 0.08; 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; dx = -node.x - this.translation.x + this.frame.canvas.clientWidth*0.5;
dy = -node.y - this.translation.y + this.frame.canvas.clientHeight*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); node.updateDamping(this.nodeIndices.length);
} }
this._updateLabels();
this.updateLabels();
// repulsing forces between nodes // repulsing forces between nodes
var minimumDistance = this.constants.nodes.distance, 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 // 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 // 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; dx = node2.x - node1.x;
dy = node2.y - node1.y; dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy); 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 = 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 repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
} }
// amplify the repulsion for clusters. // amplify the repulsion for clusters.
repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification; 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 // 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) { 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); dx = (edge.to.x - edge.from.x);
dy = (edge.to.y - edge.from.y); 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.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); 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; fx = Math.cos(angle) * springForce;
fy = Math.sin(angle) * springForce; fy = Math.sin(angle) * springForce;
@ -17633,7 +17742,7 @@ Graph.prototype._calculateForces = function() {
} }
} }
} }
/*
/*
// TODO: re-implement repulsion of edges // TODO: re-implement repulsion of edges
// repulsing forces between edges // repulsing forces between edges
@ -17670,7 +17779,7 @@ Graph.prototype._calculateForces = function() {
edges[l2].to._addForce(fx, fy); 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 * Freeze the animation
@ -17786,8 +17888,7 @@ Graph.prototype.toggleFreeze = function() {
this.freezeSimulation = false; this.freezeSimulation = false;
this.start(); this.start();
} }
console.log('freezeSimulation',this.freezeSimulation)
}
};
/** /**
* vis.js module exports * vis.js module exports

+ 1
- 1
examples/graph/02.1_really_random_nodes.html View File

@ -38,7 +38,7 @@
var to = i; var to = i;
to = i; to = i;
while (to == i) { while (to == i) {
to = Math.floor(Math.random() * (nodeCount+1));
to = Math.floor(Math.random() * (nodeCount));
} }
edges.push({ edges.push({
from: from, from: from,

+ 123
- 93
src/graph/Graph.js View File

@ -63,13 +63,14 @@ function Graph (container, data, options) {
altLength: undefined 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 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 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 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 edgeGrowth: 11, // amount of clusterSize connected to the edge is multiplied with this and added to edgeLength
clusterSizeWidthFactor: 10, clusterSizeWidthFactor: 10,
clusterSizeHeightFactor: 10, clusterSizeHeightFactor: 10,
@ -151,23 +152,30 @@ function Graph (container, data, options) {
// apply options // apply options
this.setOptions(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 // zoom so all data will fit on the screen
this.zoomToFit(); 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 * This function clusters until the maxNumberOfNodes has been reached
* *
* @param {Number} maxNumberOfNodes
* @param {Boolean} reposition * @param {Boolean} reposition
*/ */
Graph.prototype.clusterToFit = function(reposition) {
Graph.prototype.clusterToFit = function(maxNumberOfNodes, reposition) {
var numberOfNodes = this.nodeIndices.length; var numberOfNodes = this.nodeIndices.length;
var maxNumberOfNodes = this.constants.clustering.maxNumberOfNodes;
var maxLevels = 10; var maxLevels = 10;
var level = 0; var level = 0;
@ -204,7 +212,7 @@ Graph.prototype.clusterToFit = function(reposition) {
// after the clustering we reposition the nodes to avoid initial chaos // after the clustering we reposition the nodes to avoid initial chaos
if (level > 1 && reposition == true) { 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. * 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)) { if (data && data.dot && (data.nodes || data.edges)) {
throw new SyntaxError('Data must contain either parameter "dot" or ' + throw new SyntaxError('Data must contain either parameter "dot" or ' +
' parameter pair "nodes" and "edges", but not both.'); ' parameter pair "nodes" and "edges", but not both.');
@ -271,7 +284,14 @@ Graph.prototype.setData = function(data) {
this._setNodes(data && data.nodes); this._setNodes(data && data.nodes);
this._setEdges(data && data.edges); 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.decreaseClusterLevel.bind(me));
this.mouseTrap.bind("-",this.increaseClusterLevel.bind(me)); this.mouseTrap.bind("-",this.increaseClusterLevel.bind(me));
this.mouseTrap.bind("s",this.singleStep.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)); this.mouseTrap.bind("f",this.toggleFreeze.bind(me));
// add the frame to the container element // add the frame to the container element
@ -597,6 +617,7 @@ Graph.prototype._onTap = function (event) {
if (node) { if (node) {
if (node.isSelected() && elapsedTime < 300) { if (node.isSelected() && elapsedTime < 300) {
this.openCluster(node); this.openCluster(node);
this.openCluster(node);
} }
// select this node // select this node
this._selectNodes([nodeId]); this._selectNodes([nodeId]);
@ -666,8 +687,8 @@ Graph.prototype._onPinch = function (event) {
*/ */
Graph.prototype._zoom = function(scale, pointer) { Graph.prototype._zoom = function(scale, pointer) {
var scaleOld = this._getScale(); var scaleOld = this._getScale();
if (scale < 0.01) {
scale = 0.01;
if (scale < 0.001) {
scale = 0.001;
} }
if (scale > 10) { if (scale > 10) {
scale = 10; scale = 10;
@ -685,7 +706,7 @@ Graph.prototype._zoom = function(scale, pointer) {
// this.zoomCenter = {"x" : pointer.x,"y" : pointer.y }; // this.zoomCenter = {"x" : pointer.x,"y" : pointer.y };
this._setScale(scale); this._setScale(scale);
this._setTranslation(tx, ty); this._setTranslation(tx, ty);
this.updateClusters(0,false,false);
this.updateClustersDefault();
this._redraw(); this._redraw();
//console.log("current zoomscale:",this.scale) //console.log("current zoomscale:",this.scale)
@ -875,8 +896,9 @@ Graph.prototype._unselectNodes = function(selection, triggerSelect) {
// remove provided selections // remove provided selections
for (i = 0, iMax = selection.length; i < iMax; i++) { for (i = 0, iMax = selection.length; i < iMax; i++) {
id = selection[i]; id = selection[i];
this.nodes[id].unselect();
if (this.nodes.hasOwnProperty(id)) {
this.nodes[id].unselect();
}
var j = 0; var j = 0;
while (j < this.selection.length) { while (j < this.selection.length) {
if (this.selection[j] == id) { if (this.selection[j] == id) {
@ -893,7 +915,9 @@ Graph.prototype._unselectNodes = function(selection, triggerSelect) {
// remove all selections // remove all selections
for (i = 0, iMax = this.selection.length; i < iMax; i++) { for (i = 0, iMax = this.selection.length; i < iMax; i++) {
id = this.selection[i]; id = this.selection[i];
this.nodes[id].unselect();
if (this.nodes.hasOwnProperty(id)) {
this.nodes[id].unselect();
}
changed = true; changed = true;
} }
this.selection = []; this.selection = [];
@ -1624,7 +1648,7 @@ Graph.prototype._drawEdges = function(ctx) {
* @private * @private
*/ */
Graph.prototype._doStabilize = function() { Graph.prototype._doStabilize = function() {
var start = new Date();
//var start = new Date();
// find stable position // find stable position
var count = 0; var count = 0;
@ -1637,7 +1661,7 @@ Graph.prototype._doStabilize = function() {
count++; count++;
} }
var end = new Date();
// var end = new Date();
// console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup
}; };
@ -1648,28 +1672,31 @@ Graph.prototype._doStabilize = function() {
* @private * @private
*/ */
Graph.prototype._calculateForces = function() { 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); 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) { 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(); this._calculateForces();
} }
else { else {
// create a local edge to the nodes and edges, that is faster // 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, 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 // Gravity is required to keep separated groups from floating off
// the forces are reset to zero in this loop by using _setForce instead // the forces are reset to zero in this loop by using _setForce instead
// of _addForce // of _addForce
var gravity = 0.08; 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; dx = -node.x - this.translation.x + this.frame.canvas.clientWidth*0.5;
dy = -node.y - this.translation.y + this.frame.canvas.clientHeight*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); node.updateDamping(this.nodeIndices.length);
} }
this._updateLabels();
this.updateLabels();
// repulsing forces between nodes // repulsing forces between nodes
var minimumDistance = this.constants.nodes.distance, 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 // 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 // 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; dx = node2.x - node1.x;
dy = node2.y - node1.y; dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy); 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 = 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 repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
} }
// amplify the repulsion for clusters. // amplify the repulsion for clusters.
repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification; 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 // 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) { 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); dx = (edge.to.x - edge.from.x);
dy = (edge.to.y - edge.from.y); 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.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); 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; fx = Math.cos(angle) * springForce;
fy = Math.sin(angle) * springForce; fy = Math.sin(angle) * springForce;
@ -1783,7 +1821,7 @@ Graph.prototype._calculateForces = function() {
} }
} }
} }
/*
/*
// TODO: re-implement repulsion of edges // TODO: re-implement repulsion of edges
// repulsing forces between edges // repulsing forces between edges
@ -1820,7 +1858,7 @@ Graph.prototype._calculateForces = function() {
edges[l2].to._addForce(fx, fy); 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 * Freeze the animation
@ -1936,5 +1967,4 @@ Graph.prototype.toggleFreeze = function() {
this.freezeSimulation = false; this.freezeSimulation = false;
this.start(); this.start();
} }
console.log('freezeSimulation',this.freezeSimulation)
}
};

+ 149
- 79
src/graph/cluster.js View File

@ -22,7 +22,7 @@ Cluster.prototype.openCluster = function(node) {
// housekeeping // housekeeping
this._updateNodeIndexList(); this._updateNodeIndexList();
this._updateDynamicEdges(); this._updateDynamicEdges();
this._updateLabels();
this.updateLabels();
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) { 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 * 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. * If out, check if we can form clusters, if in, check if we can open clusters.
* This function is only called from _zoom() * 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} recursive | enable or disable recursive calling of the opening of clusters
* @param {Boolean} force | enable or disable forcing * @param {Boolean} force | enable or disable forcing
* *
* @private
*/ */
Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) { Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) {
var isMovingBeforeClustering = this.moving; var isMovingBeforeClustering = this.moving;
@ -78,17 +83,24 @@ Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) {
} }
this._updateNodeIndexList(); 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)) { if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
this._aggregateHubs(force); this._aggregateHubs(force);
}
// we now reduce snakes.
if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
this.handleSnakes();
this._updateNodeIndexList(); this._updateNodeIndexList();
} }
this.previousScale = this.scale; this.previousScale = this.scale;
// rest of the housekeeping // rest of the housekeeping
this._updateDynamicEdges(); this._updateDynamicEdges();
this._updateLabels();
this.updateLabels();
// if a cluster was formed, we increase the clusterSession // if a cluster was formed, we increase the clusterSession
if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place 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 * this functions starts clustering by hubs
* The minimum hub threshold is set globally * The minimum hub threshold is set globally
@ -109,7 +133,7 @@ Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) {
*/ */
Cluster.prototype._aggregateHubs = function(force) { Cluster.prototype._aggregateHubs = function(force) {
this._getHubSize(); this._getHubSize();
this._clusterByHub(force);
this._formClustersByHub(force);
}; };
@ -126,7 +150,7 @@ Cluster.prototype.forceAggregateHubs = function() {
// housekeeping // housekeeping
this._updateNodeIndexList(); this._updateNodeIndexList();
this._updateDynamicEdges(); this._updateDynamicEdges();
this._updateLabels();
this.updateLabels();
// if a cluster was formed, we increase the clusterSession // if a cluster was formed, we increase the clusterSession
if (this.nodeIndices.length != amountOfNodes) { if (this.nodeIndices.length != amountOfNodes) {
@ -168,11 +192,10 @@ Cluster.prototype._openClusters = function(recursive,force) {
* @private * @private
*/ */
Cluster.prototype._expandClusterNode = function(parentNode, recursive, force, openAll) { Cluster.prototype._expandClusterNode = function(parentNode, recursive, force, openAll) {
var openedCluster = false;
// first check if node is a cluster // first check if node is a cluster
if (parentNode.clusterSize > 1) { 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 // 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; openAll = true;
} }
recursive = openAll ? true : recursive; 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] if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1]
|| openAll) { || openAll) {
this._expelChildFromParent(parentNode,containedNodeID,recursive,force,openAll); this._expelChildFromParent(parentNode,containedNodeID,recursive,force,openAll);
openedCluster = true;
} }
} }
else { else {
if (this._parentNodeInActiveArea(parentNode)) { if (this._parentNodeInActiveArea(parentNode)) {
this._expelChildFromParent(parentNode,containedNodeID,recursive,force,openAll); 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; parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
// place the child node near the parent, not at the exact same location to avoid chaos in the system // 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 // remove node from the list
delete parentNode.containedNodes[containedNodeID]; 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 // restart the simulation to reorganise all nodes
this.moving = true; 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 * @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 // we loop over all nodes in the list
for (var nodeID in this.nodes) { for (var nodeID in this.nodes) {
// we check if it is still available since it can be used by the clustering in this loop // we check if it is still available since it can be used by the clustering in this loop
if (this.nodes.hasOwnProperty(nodeID)) { 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 adds the child node to the parent node, creating a cluster if it is not already.
* This function is called only from updateClusters() * 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 // giving the clusters a dynamic formationScale to ensure not all clusters open up when zoomed
if (force == true) { 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 { else {
parentNode.formationScale = this.scale; // The latest child has been added on this scale parentNode.formationScale = this.scale; // The latest child has been added on this scale
@ -650,7 +702,6 @@ Cluster.prototype._connectEdgeBackToChild = function(parentNode, childNode) {
* @private * @private
*/ */
Cluster.prototype._validateEdges = function(parentNode) { Cluster.prototype._validateEdges = function(parentNode) {
// TODO: check if good idea
for (var i = 0; i < parentNode.dynamicEdges.length; i++) { for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
var edge = parentNode.dynamicEdges[i]; var edge = parentNode.dynamicEdges[i];
if (parentNode.id != edge.toId && parentNode.id != edge.fromId) { 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) * This updates the node labels for all nodes (for debugging purposes)
* @private
*/ */
Cluster.prototype._updateLabels = function() {
Cluster.prototype.updateLabels = function() {
var nodeID; var nodeID;
// update node labels // update node labels
for (nodeID in this.nodes) { 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 * 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. * 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++) { for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]]; var node = this.nodes[this.nodeIndices[i]];
if (!node.isFixed()) { 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 * 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. * with this amount we can cluster specifically on these snakes.
* *
* @returns {number}
* @private * @private
*/ */
Cluster.prototype._getAmountOfSnakes = function() {
Cluster.prototype._getSnakeFraction = function() {
var snakes = 0; var snakes = 0;
var total = 0;
for (nodeID in this.nodes) { for (nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) { 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; snakes += 1;
} }
total += 1;
} }
} }
return snakes;
};
return snakes/total;
};

Loading…
Cancel
Save