Browse Source

added snake removal, bugfixes, refactoring

css_transitions
Alex de Mulder 10 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
this._updateNodeIndexList();
this._updateDynamicEdges();
this._updateLabels();
this.updateLabels();
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
@ -15054,8 +15054,14 @@ Cluster.prototype.openCluster = function(node) {
}
};
/**
* This calls the updateClustes with default arguments
*/
Cluster.prototype.updateClustersDefault = function() {
if (this.constants.clustering.enableClustering) {
this.updateClusters(0,false,false);
}
}
/**
* This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
@ -15083,11 +15089,10 @@ Cluster.prototype.decreaseClusterLevel = function() {
* If out, check if we can form clusters, if in, check if we can open clusters.
* This function is only called from _zoom()
*
* @param {Int} zoomDirection
* @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn
* @param {Boolean} recursive | enable or disable recursive calling of the opening of clusters
* @param {Boolean} force | enable or disable forcing
*
* @private
*/
Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) {
var isMovingBeforeClustering = this.moving;
@ -15102,17 +15107,24 @@ Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) {
}
this._updateNodeIndexList();
// if a cluster was NOT formed and the user zoomed out, we try clustering by hubs and update the index again
// if a cluster was NOT formed and the user zoomed out, we try clustering by hubs
if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
this._aggregateHubs(force);
}
// we now reduce snakes.
if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
this.handleSnakes();
this._updateNodeIndexList();
}
this.previousScale = this.scale;
// rest of the housekeeping
this._updateDynamicEdges();
this._updateLabels();
this.updateLabels();
// if a cluster was formed, we increase the clusterSession
if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
@ -15125,6 +15137,18 @@ Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) {
}
};
/**
* This function handles the snakes. It is called on every updateClusters().
*/
Cluster.prototype.handleSnakes = function() {
// after clustering we check how many snakes there are
var snakePercentage = this._getSnakeFraction();
if (snakePercentage > this.constants.clustering.snakeThreshold) {
this._reduceAmountOfSnakes(1 - this.constants.clustering.snakeThreshold / snakePercentage)
}
};
/**
* this functions starts clustering by hubs
* The minimum hub threshold is set globally
@ -15133,7 +15157,7 @@ Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) {
*/
Cluster.prototype._aggregateHubs = function(force) {
this._getHubSize();
this._clusterByHub(force);
this._formClustersByHub(force);
};
@ -15150,7 +15174,7 @@ Cluster.prototype.forceAggregateHubs = function() {
// housekeeping
this._updateNodeIndexList();
this._updateDynamicEdges();
this._updateLabels();
this.updateLabels();
// if a cluster was formed, we increase the clusterSession
if (this.nodeIndices.length != amountOfNodes) {
@ -15192,11 +15216,10 @@ Cluster.prototype._openClusters = function(recursive,force) {
* @private
*/
Cluster.prototype._expandClusterNode = function(parentNode, recursive, force, openAll) {
var openedCluster = false;
// first check if node is a cluster
if (parentNode.clusterSize > 1) {
// this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20
if (parentNode.clusterSize < 20 && force == false) {
if (parentNode.clusterSize < 20) {
openAll = true;
}
recursive = openAll ? true : recursive;
@ -15213,21 +15236,16 @@ Cluster.prototype._expandClusterNode = function(parentNode, recursive, force, op
if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1]
|| openAll) {
this._expelChildFromParent(parentNode,containedNodeID,recursive,force,openAll);
openedCluster = true;
}
}
else {
if (this._parentNodeInActiveArea(parentNode)) {
this._expelChildFromParent(parentNode,containedNodeID,recursive,force,openAll);
openedCluster = true;
}
}
}
}
}
if (openedCluster == true) {
parentNode.clusterSessions.pop();
}
}
};
@ -15270,15 +15288,30 @@ Cluster.prototype._expelChildFromParent = function(parentNode, containedNodeID,
parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
// place the child node near the parent, not at the exact same location to avoid chaos in the system
childNode.x = parentNode.x + this.constants.edges.length * 0.2 * (0.5 - Math.random()) * parentNode.clusterSize;
childNode.y = parentNode.y + this.constants.edges.length * 0.2 * (0.5 - Math.random()) * parentNode.clusterSize;
// remove the clusterSession from the child node
childNode.clusterSession = 0;
childNode.x = parentNode.x + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize;
childNode.y = parentNode.y + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize;
// remove node from the list
delete parentNode.containedNodes[containedNodeID];
// check if there are other childs with this clusterSession in the parent.
var othersPresent = false;
for (var childNodeID in parentNode.containedNodes) {
if (parentNode.containedNodes.hasOwnProperty(childNodeID)) {
if (parentNode.containedNodes[childNodeID].clusterSession == childNode.clusterSession) {
othersPresent = true;
break;
}
}
}
// if there are no others, remove the cluster session from the list
if (othersPresent == false) {
parentNode.clusterSessions.pop();
}
// remove the clusterSession from the child node
childNode.clusterSession = 0;
// restart the simulation to reorganise all nodes
this.moving = true;
@ -15384,66 +15417,86 @@ Cluster.prototype._forceClustersByZoom = function() {
/**
* This function forms clusters from hubs, it loops over all nodes
*
* @param {Boolean} force
* @param {Boolean} force | Disregard zoom level
* @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
* @private
*/
Cluster.prototype._clusterByHub = function(force) {
var dx,dy,length;
var minLength = this.constants.clustering.clusterLength/this.scale;
var allowCluster = false;
Cluster.prototype._formClustersByHub = function(force, onlyEqual) {
// we loop over all nodes in the list
for (var nodeID in this.nodes) {
// we check if it is still available since it can be used by the clustering in this loop
if (this.nodes.hasOwnProperty(nodeID)) {
var hubNode = this.nodes[nodeID];
// we decide if the node is a hub
if (hubNode.dynamicEdgesLength >= this.hubThreshold) {
// we create a list of edges because the dynamicEdges change over the course of this loop
var edgesIDarray = [];
var amountOfInitialEdges = hubNode.dynamicEdges.length;
for (var j = 0; j < amountOfInitialEdges; j++) {
edgesIDarray.push(hubNode.dynamicEdges[j].id);
}
this._formClusterFromHub(this.nodes[nodeID],force,onlyEqual);
}
}
};
// if the hub clustering is not forces, we check if one of the edges connected
// to a cluster is small enough based on the constants.clustering.clusterLength
if (force == false) {
allowCluster = false;
for (j = 0; j < amountOfInitialEdges; j++) {
var edge = this.edges[edgesIDarray[j]];
if (edge !== undefined) {
if (edge.connected) {
dx = (edge.to.x - edge.from.x);
dy = (edge.to.y - edge.from.y);
length = Math.sqrt(dx * dx + dy * dy);
if (length < minLength) {
allowCluster = true;
break;
}
}
/**
* This function forms a cluster from a specific preselected hub node
*
* @param {Node} hubNode | the node we will cluster as a hub
* @param {Boolean} force | Disregard zoom level
* @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
* @param [Number] absorptionSizeOffset |
* @private
*/
Cluster.prototype._formClusterFromHub = function(hubNode, force, onlyEqual, absorptionSizeOffset) {
if (absorptionSizeOffset === undefined) {
absorptionSizeOffset = 0;
}
// we decide if the node is a hub
if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) ||
(hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) {
// initialize variables
var dx,dy,length;
var minLength = this.constants.clustering.clusterLength/this.scale;
var allowCluster = false;
// we create a list of edges because the dynamicEdges change over the course of this loop
var edgesIDarray = [];
var amountOfInitialEdges = hubNode.dynamicEdges.length;
for (var j = 0; j < amountOfInitialEdges; j++) {
edgesIDarray.push(hubNode.dynamicEdges[j].id);
}
// if the hub clustering is not forces, we check if one of the edges connected
// to a cluster is small enough based on the constants.clustering.clusterLength
if (force == false) {
allowCluster = false;
for (j = 0; j < amountOfInitialEdges; j++) {
var edge = this.edges[edgesIDarray[j]];
if (edge !== undefined) {
if (edge.connected) {
dx = (edge.to.x - edge.from.x);
dy = (edge.to.y - edge.from.y);
length = Math.sqrt(dx * dx + dy * dy);
if (length < minLength) {
allowCluster = true;
break;
}
}
}
}
}
// start the clustering if allowed
if ((!force && allowCluster) || force) {
// we loop over all edges INITIALLY connected to this hub
for (j = 0; j < amountOfInitialEdges; j++) {
edge = this.edges[edgesIDarray[j]];
// start the clustering if allowed
if ((!force && allowCluster) || force) {
// we loop over all edges INITIALLY connected to this hub
for (j = 0; j < amountOfInitialEdges; j++) {
edge = this.edges[edgesIDarray[j]];
// the edge can be clustered by this function in a previous loop
if (edge !== undefined) {
var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
// the edge can be clustered by this function in a previous loop
if (edge !== undefined) {
var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
// we do not want hubs to merge with other hubs.
if (childNode.dynamicEdges.length <= this.hubThreshold) {
this._addToCluster(hubNode,childNode,force);
}
}
// we do not want hubs to merge with other hubs.
if (childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) {
this._addToCluster(hubNode,childNode,force);
}
}
}
@ -15453,7 +15506,6 @@ Cluster.prototype._clusterByHub = function(force) {
/**
* This function adds the child node to the parent node, creating a cluster if it is not already.
* This function is called only from updateClusters()
@ -15496,7 +15548,7 @@ Cluster.prototype._addToCluster = function(parentNode, childNode, force) {
// giving the clusters a dynamic formationScale to ensure not all clusters open up when zoomed
if (force == true) {
parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+2);
parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3);
}
else {
parentNode.formationScale = this.scale; // The latest child has been added on this scale
@ -15674,7 +15726,6 @@ Cluster.prototype._connectEdgeBackToChild = function(parentNode, childNode) {
* @private
*/
Cluster.prototype._validateEdges = function(parentNode) {
// TODO: check if good idea
for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
var edge = parentNode.dynamicEdges[i];
if (parentNode.id != edge.toId && parentNode.id != edge.fromId) {
@ -15716,9 +15767,8 @@ Cluster.prototype._releaseContainedEdges = function(parentNode, childNode) {
/**
* This updates the node labels for all nodes (for debugging purposes)
* @private
*/
Cluster.prototype._updateLabels = function() {
Cluster.prototype.updateLabels = function() {
var nodeID;
// update node labels
for (nodeID in this.nodes) {
@ -15772,9 +15822,8 @@ Cluster.prototype._parentNodeInActiveArea = function(node) {
* This is an adaptation of the original repositioning function. This is called if the system is clustered initially
* It puts large clusters away from the center and randomizes the order.
*
* @private
*/
Cluster.prototype._repositionNodes = function() {
Cluster.prototype.repositionNodes = function() {
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
if (!node.isFixed()) {
@ -15830,24 +15879,46 @@ Cluster.prototype._getHubSize = function() {
};
/**
* We reduce the amount of "extension nodes" or snakes. These are not quickly clustered with the outliers and hubs methods
* with this amount we can cluster specifically on these snakes.
*
* @param {double} fraction | between 0 and 1, the percentage of snakes to reduce
* @private
*/
Cluster.prototype._reduceAmountOfSnakes = function(fraction) {
this.hubThreshold = 2;
for (nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) {
if (this.nodes[nodeID].dynamicEdgesLength == 2 && this.nodes[nodeID].dynamicEdges.length >= 2) {
if (Math.random() <= fraction) {
this._formClusterFromHub(this.nodes[nodeID],true,true,1)
}
}
}
}
};
/**
* We get the amount of "extension nodes" or snakes. These are not quickly clustered with the outliers and hubs methods
* with this amount we can cluster specifically on these snakes.
*
* @returns {number}
* @private
*/
Cluster.prototype._getAmountOfSnakes = function() {
Cluster.prototype._getSnakeFraction = function() {
var snakes = 0;
var total = 0;
for (nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) {
if (this.nodes[nodeID].dynamicEdges.length == 2) {
if (this.nodes[nodeID].dynamicEdgesLength == 2 && this.nodes[nodeID].dynamicEdges.length >= 2) {
snakes += 1;
}
total += 1;
}
}
return snakes;
return snakes/total;
};
/**
* @constructor Graph
* Create a graph visualization, displaying nodes and edges.
@ -15913,13 +15984,14 @@ function Graph (container, data, options) {
altLength: undefined
}
},
clustering: { // TODO: naming of variables
maxNumberOfNodes: 100, // for automatic (initial) clustering //
clustering: { // TODO: naming of variables
enableClustering: false,
maxNumberOfNodes: 100, // for automatic (initial) clustering
snakeThreshold: 0.5, // maximum percentage of allowed snakes (long strings of connected nodes)
clusterLength: 30, // threshold edge length for clusteringl
fontSizeMultiplier: 3, // how much the cluster font size grows per node (in px)
fontSizeMultiplier: 4, // how much the cluster font size grows per node (in px)
forceAmplification: 0.7, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force
distanceAmplification: 0.3, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force
edgeStrength: 0.01,
edgeGrowth: 11, // amount of clusterSize connected to the edge is multiplied with this and added to edgeLength
clusterSizeWidthFactor: 10,
clusterSizeHeightFactor: 10,
@ -16001,23 +16073,30 @@ function Graph (container, data, options) {
// apply options
this.setOptions(options);
// draw data
this.setData(data); // TODO: option to render (start())
var disableStart = this.constants.clustering.enableClustering;
// load data
this.setData(data,disableStart); //
// zoom so all data will fit on the screen
this.zoomToFit();
// cluster if the data set is big
this.clusterToFit(true);
if (this.constants.clustering.enableClustering) {
// cluster if the data set is big
this.clusterToFit(this.constants.clustering.maxNumberOfNodes, true);
// updates the lables after clustering
this.updateLabels();
// updates the lables after clustering
this._updateLabels();
// this is called here because if clusterin is disabled, the start and stabilize are called in
// the setData function.
// find a stable position or start animating to a stable position
if (this.stabilize) {
this._doStabilize();
// find a stable position or start animating to a stable position
if (this.stabilize) {
this._doStabilize();
}
this.start();
}
this.start();
}
/**
@ -16029,11 +16108,11 @@ Graph.prototype = Object.create(Cluster.prototype);
/**
* This function clusters until the maxNumberOfNodes has been reached
*
* @param {Number} maxNumberOfNodes
* @param {Boolean} reposition
*/
Graph.prototype.clusterToFit = function(reposition) {
Graph.prototype.clusterToFit = function(maxNumberOfNodes, reposition) {
var numberOfNodes = this.nodeIndices.length;
var maxNumberOfNodes = this.constants.clustering.maxNumberOfNodes;
var maxLevels = 10;
var level = 0;
@ -16054,7 +16133,7 @@ Graph.prototype.clusterToFit = function(reposition) {
// after the clustering we reposition the nodes to avoid initial chaos
if (level > 1 && reposition == true) {
this._repositionNodes();
this.repositionNodes();
}
};
@ -16093,13 +16172,18 @@ Graph.prototype._updateNodeIndexList = function() {
/**
* Set nodes and edges, and optionally options as well.
*
* @param {Object} data Object containing parameters:
* {Array | DataSet | DataView} [nodes] Array with nodes
* {Array | DataSet | DataView} [edges] Array with edges
* {String} [dot] String containing data in DOT format
* {Options} [options] Object with options
* @param {Object} data Object containing parameters:
* {Array | DataSet | DataView} [nodes] Array with nodes
* {Array | DataSet | DataView} [edges] Array with edges
* {String} [dot] String containing data in DOT format
* {Options} [options] Object with options
* @param {Boolean} [disableStart] | optional: disable the calling of the start function.
*/
Graph.prototype.setData = function(data) {
Graph.prototype.setData = function(data, disableStart) {
if (disableStart === undefined) {
disableStart = false;
}
if (data && data.dot && (data.nodes || data.edges)) {
throw new SyntaxError('Data must contain either parameter "dot" or ' +
' parameter pair "nodes" and "edges", but not both.');
@ -16121,7 +16205,14 @@ Graph.prototype.setData = function(data) {
this._setNodes(data && data.nodes);
this._setEdges(data && data.edges);
}
// updating the list of node indices
if (!disableStart) {
// find a stable position or start animating to a stable position
if (this.stabilize) {
this._doStabilize();
}
this.start();
}
};
/**
@ -16264,7 +16355,7 @@ Graph.prototype._create = function () {
this.mouseTrap.bind("=",this.decreaseClusterLevel.bind(me));
this.mouseTrap.bind("-",this.increaseClusterLevel.bind(me));
this.mouseTrap.bind("s",this.singleStep.bind(me));
this.mouseTrap.bind("h",this.forceAggregateHubs.bind(me));
this.mouseTrap.bind("h",this.updateClustersDefault.bind(me));
this.mouseTrap.bind("f",this.toggleFreeze.bind(me));
// add the frame to the container element
@ -16447,6 +16538,7 @@ Graph.prototype._onTap = function (event) {
if (node) {
if (node.isSelected() && elapsedTime < 300) {
this.openCluster(node);
this.openCluster(node);
}
// select this node
this._selectNodes([nodeId]);
@ -16516,8 +16608,8 @@ Graph.prototype._onPinch = function (event) {
*/
Graph.prototype._zoom = function(scale, pointer) {
var scaleOld = this._getScale();
if (scale < 0.01) {
scale = 0.01;
if (scale < 0.001) {
scale = 0.001;
}
if (scale > 10) {
scale = 10;
@ -16535,7 +16627,7 @@ Graph.prototype._zoom = function(scale, pointer) {
// this.zoomCenter = {"x" : pointer.x,"y" : pointer.y };
this._setScale(scale);
this._setTranslation(tx, ty);
this.updateClusters(0,false,false);
this.updateClustersDefault();
this._redraw();
//console.log("current zoomscale:",this.scale)
@ -16725,8 +16817,9 @@ Graph.prototype._unselectNodes = function(selection, triggerSelect) {
// remove provided selections
for (i = 0, iMax = selection.length; i < iMax; i++) {
id = selection[i];
this.nodes[id].unselect();
if (this.nodes.hasOwnProperty(id)) {
this.nodes[id].unselect();
}
var j = 0;
while (j < this.selection.length) {
if (this.selection[j] == id) {
@ -16743,7 +16836,9 @@ Graph.prototype._unselectNodes = function(selection, triggerSelect) {
// remove all selections
for (i = 0, iMax = this.selection.length; i < iMax; i++) {
id = this.selection[i];
this.nodes[id].unselect();
if (this.nodes.hasOwnProperty(id)) {
this.nodes[id].unselect();
}
changed = true;
}
this.selection = [];
@ -17474,7 +17569,7 @@ Graph.prototype._drawEdges = function(ctx) {
* @private
*/
Graph.prototype._doStabilize = function() {
var start = new Date();
//var start = new Date();
// find stable position
var count = 0;
@ -17487,7 +17582,7 @@ Graph.prototype._doStabilize = function() {
count++;
}
var end = new Date();
// var end = new Date();
// console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup
};
@ -17498,28 +17593,31 @@ Graph.prototype._doStabilize = function() {
* @private
*/
Graph.prototype._calculateForces = function() {
if (this.nodeIndices.length == 1) { // stop calculation if there is only one node
// stop calculation if there is only one node
if (this.nodeIndices.length == 1) {
this.nodes[this.nodeIndices[0]]._setForce(0,0);
}
// if there are too many nodes on screen, we cluster without repositioning
else if (this.nodeIndices.length > this.constants.clustering.maxNumberOfNodes * 4) {
console.log(this.nodeIndices.length, this.constants.clustering.maxNumberOfNodes * 4)
this.clusterToFit(false);
this.clusterToFit(this.constants.clustering.maxNumberOfNodes * 2, false);
this._calculateForces();
}
else {
// create a local edge to the nodes and edges, that is faster
var id, dx, dy, angle, distance, fx, fy,
var dx, dy, angle, distance, fx, fy,
repulsingForce, springForce, length, edgeLength,
nodes = this.nodes,
edges = this.edges;
node, node1, node2, edge, edgeID, i, j, nodeID, xCenter, yCenter;
var clusterSize;
var nodes = this.nodes;
var edges = this.edges;
// Gravity is required to keep separated groups from floating off
// the forces are reset to zero in this loop by using _setForce instead
// of _addForce
var gravity = 0.08;
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = nodes[this.nodeIndices[i]];
for (i = 0; i < this.nodeIndices.length; i++) {
node = nodes[this.nodeIndices[i]];
dx = -node.x - this.translation.x + this.frame.canvas.clientWidth*0.5;
dy = -node.y - this.translation.y + this.frame.canvas.clientHeight*0.5;
@ -17531,7 +17629,7 @@ Graph.prototype._calculateForces = function() {
node.updateDamping(this.nodeIndices.length);
}
this._updateLabels();
this.updateLabels();
// repulsing forces between nodes
var minimumDistance = this.constants.nodes.distance,
@ -17540,11 +17638,11 @@ Graph.prototype._calculateForces = function() {
// we loop from i over all but the last entree in the array
// j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
for (var i = 0; i < this.nodeIndices.length-1; i++) {
var node1 = nodes[this.nodeIndices[i]];
for (var j = i+1; j < this.nodeIndices.length; j++) {
var node2 = nodes[this.nodeIndices[j]];
var clusterSize = (node1.clusterSize + node2.clusterSize - 2);
for (i = 0; i < this.nodeIndices.length-1; i++) {
node1 = nodes[this.nodeIndices[i]];
for (j = i+1; j < this.nodeIndices.length; j++) {
node2 = nodes[this.nodeIndices[j]];
clusterSize = (node1.clusterSize + node2.clusterSize - 2);
dx = node2.x - node1.x;
dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy);
@ -17564,7 +17662,6 @@ Graph.prototype._calculateForces = function() {
//repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
}
// amplify the repulsion for clusters.
repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification;
@ -17577,38 +17674,53 @@ Graph.prototype._calculateForces = function() {
}
}
// TODO: re-implement repulsion of edges
for (var n = 0; n < nodes.length; n++) {
for (var l = 0; l < edges.length; l++) {
var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
// calculate normally distributed force
dx = nodes[n].x - lx,
dy = nodes[n].y - ly,
distance = Math.sqrt(dx * dx + dy * dy),
angle = Math.atan2(dy, dx),
// TODO: correct factor for repulsing force
//var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
//repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force
fx = Math.cos(angle) * repulsingforce,
fy = Math.sin(angle) * repulsingforce;
nodes[n]._addForce(fx, fy);
edges[l].from._addForce(-fx/2,-fy/2);
edges[l].to._addForce(-fx/2,-fy/2);
}
}
/*
// repulsion of the edges on the nodes and
for (var nodeID in nodes) {
if (nodes.hasOwnProperty(nodeID)) {
node = nodes[nodeID];
for(var edgeID in edges) {
if (edges.hasOwnProperty(edgeID)) {
edge = edges[edgeID];
// get the center of the edge
xCenter = edge.from.x+(edge.to.x - edge.from.x)/2;
yCenter = edge.from.y+(edge.to.y - edge.from.y)/2;
// calculate normally distributed force
dx = node.x - xCenter;
dy = node.y - yCenter;
distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045
angle = Math.atan2(dy, dx);
if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307
repulsingForce = 1.0;
}
else {
// TODO: correct factor for repulsing force
//var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
//repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
repulsingForce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)); // TODO: customize the repulsing force
}
fx = Math.cos(angle) * repulsingForce;
fy = Math.sin(angle) * repulsingForce;
node._addForce(fx, fy);
edge.from._addForce(-fx/2,-fy/2);
edge.to._addForce(-fx/2,-fy/2);
}
}
}
}
}
*/
// forces caused by the edges, modelled as springs
for (id in edges) {
if (edges.hasOwnProperty(id)) {
var edge = edges[id];
for (edgeID in edges) {
if (edges.hasOwnProperty(edgeID)) {
edge = edges[edgeID];
if (edge.connected) {
var clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2);
clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2);
dx = (edge.to.x - edge.from.x);
dy = (edge.to.y - edge.from.y);
//edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin
@ -17622,9 +17734,6 @@ Graph.prototype._calculateForces = function() {
springForce = edge.stiffness * (edgeLength - length);
// boost strength of cluster springs
springForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.edgeStrength;
fx = Math.cos(angle) * springForce;
fy = Math.sin(angle) * springForce;
@ -17633,7 +17742,7 @@ Graph.prototype._calculateForces = function() {
}
}
}
/*
/*
// TODO: re-implement repulsion of edges
// repulsing forces between edges
@ -17670,7 +17779,7 @@ Graph.prototype._calculateForces = function() {
edges[l2].to._addForce(fx, fy);
}
}
*/
*/
}
};
@ -17765,15 +17874,8 @@ Graph.prototype.singleStep = function() {
}
};
/**
* Stop animating nodes and edges.
*/
Graph.prototype.stop = function () {
if (this.timer) {
window.clearInterval(this.timer);
this.timer = undefined;
}
};
/**
* Freeze the animation
@ -17786,8 +17888,7 @@ Graph.prototype.toggleFreeze = function() {
this.freezeSimulation = false;
this.start();
}
console.log('freezeSimulation',this.freezeSimulation)
}
};
/**
* vis.js module exports

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

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

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

@ -63,13 +63,14 @@ function Graph (container, data, options) {
altLength: undefined
}
},
clustering: { // TODO: naming of variables
maxNumberOfNodes: 100, // for automatic (initial) clustering //
clustering: { // TODO: naming of variables
enableClustering: false,
maxNumberOfNodes: 100, // for automatic (initial) clustering
snakeThreshold: 0.5, // maximum percentage of allowed snakes (long strings of connected nodes)
clusterLength: 30, // threshold edge length for clusteringl
fontSizeMultiplier: 3, // how much the cluster font size grows per node (in px)
fontSizeMultiplier: 4, // how much the cluster font size grows per node (in px)
forceAmplification: 0.7, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force
distanceAmplification: 0.3, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force
edgeStrength: 0.01,
edgeGrowth: 11, // amount of clusterSize connected to the edge is multiplied with this and added to edgeLength
clusterSizeWidthFactor: 10,
clusterSizeHeightFactor: 10,
@ -151,23 +152,30 @@ function Graph (container, data, options) {
// apply options
this.setOptions(options);
// draw data
this.setData(data); // TODO: option to render (start())
var disableStart = this.constants.clustering.enableClustering;
// load data
this.setData(data,disableStart); //
// zoom so all data will fit on the screen
this.zoomToFit();
// cluster if the data set is big
this.clusterToFit(true);
if (this.constants.clustering.enableClustering) {
// cluster if the data set is big
this.clusterToFit(this.constants.clustering.maxNumberOfNodes, true);
// updates the lables after clustering
this.updateLabels();
// updates the lables after clustering
this._updateLabels();
// this is called here because if clusterin is disabled, the start and stabilize are called in
// the setData function.
// find a stable position or start animating to a stable position
if (this.stabilize) {
this._doStabilize();
// find a stable position or start animating to a stable position
if (this.stabilize) {
this._doStabilize();
}
this.start();
}
this.start();
}
/**
@ -179,11 +187,11 @@ Graph.prototype = Object.create(Cluster.prototype);
/**
* This function clusters until the maxNumberOfNodes has been reached
*
* @param {Number} maxNumberOfNodes
* @param {Boolean} reposition
*/
Graph.prototype.clusterToFit = function(reposition) {
Graph.prototype.clusterToFit = function(maxNumberOfNodes, reposition) {
var numberOfNodes = this.nodeIndices.length;
var maxNumberOfNodes = this.constants.clustering.maxNumberOfNodes;
var maxLevels = 10;
var level = 0;
@ -204,7 +212,7 @@ Graph.prototype.clusterToFit = function(reposition) {
// after the clustering we reposition the nodes to avoid initial chaos
if (level > 1 && reposition == true) {
this._repositionNodes();
this.repositionNodes();
}
};
@ -243,13 +251,18 @@ Graph.prototype._updateNodeIndexList = function() {
/**
* Set nodes and edges, and optionally options as well.
*
* @param {Object} data Object containing parameters:
* {Array | DataSet | DataView} [nodes] Array with nodes
* {Array | DataSet | DataView} [edges] Array with edges
* {String} [dot] String containing data in DOT format
* {Options} [options] Object with options
* @param {Object} data Object containing parameters:
* {Array | DataSet | DataView} [nodes] Array with nodes
* {Array | DataSet | DataView} [edges] Array with edges
* {String} [dot] String containing data in DOT format
* {Options} [options] Object with options
* @param {Boolean} [disableStart] | optional: disable the calling of the start function.
*/
Graph.prototype.setData = function(data) {
Graph.prototype.setData = function(data, disableStart) {
if (disableStart === undefined) {
disableStart = false;
}
if (data && data.dot && (data.nodes || data.edges)) {
throw new SyntaxError('Data must contain either parameter "dot" or ' +
' parameter pair "nodes" and "edges", but not both.');
@ -271,7 +284,14 @@ Graph.prototype.setData = function(data) {
this._setNodes(data && data.nodes);
this._setEdges(data && data.edges);
}
// updating the list of node indices
if (!disableStart) {
// find a stable position or start animating to a stable position
if (this.stabilize) {
this._doStabilize();
}
this.start();
}
};
/**
@ -414,7 +434,7 @@ Graph.prototype._create = function () {
this.mouseTrap.bind("=",this.decreaseClusterLevel.bind(me));
this.mouseTrap.bind("-",this.increaseClusterLevel.bind(me));
this.mouseTrap.bind("s",this.singleStep.bind(me));
this.mouseTrap.bind("h",this.forceAggregateHubs.bind(me));
this.mouseTrap.bind("h",this.updateClustersDefault.bind(me));
this.mouseTrap.bind("f",this.toggleFreeze.bind(me));
// add the frame to the container element
@ -597,6 +617,7 @@ Graph.prototype._onTap = function (event) {
if (node) {
if (node.isSelected() && elapsedTime < 300) {
this.openCluster(node);
this.openCluster(node);
}
// select this node
this._selectNodes([nodeId]);
@ -666,8 +687,8 @@ Graph.prototype._onPinch = function (event) {
*/
Graph.prototype._zoom = function(scale, pointer) {
var scaleOld = this._getScale();
if (scale < 0.01) {
scale = 0.01;
if (scale < 0.001) {
scale = 0.001;
}
if (scale > 10) {
scale = 10;
@ -685,7 +706,7 @@ Graph.prototype._zoom = function(scale, pointer) {
// this.zoomCenter = {"x" : pointer.x,"y" : pointer.y };
this._setScale(scale);
this._setTranslation(tx, ty);
this.updateClusters(0,false,false);
this.updateClustersDefault();
this._redraw();
//console.log("current zoomscale:",this.scale)
@ -875,8 +896,9 @@ Graph.prototype._unselectNodes = function(selection, triggerSelect) {
// remove provided selections
for (i = 0, iMax = selection.length; i < iMax; i++) {
id = selection[i];
this.nodes[id].unselect();
if (this.nodes.hasOwnProperty(id)) {
this.nodes[id].unselect();
}
var j = 0;
while (j < this.selection.length) {
if (this.selection[j] == id) {
@ -893,7 +915,9 @@ Graph.prototype._unselectNodes = function(selection, triggerSelect) {
// remove all selections
for (i = 0, iMax = this.selection.length; i < iMax; i++) {
id = this.selection[i];
this.nodes[id].unselect();
if (this.nodes.hasOwnProperty(id)) {
this.nodes[id].unselect();
}
changed = true;
}
this.selection = [];
@ -1624,7 +1648,7 @@ Graph.prototype._drawEdges = function(ctx) {
* @private
*/
Graph.prototype._doStabilize = function() {
var start = new Date();
//var start = new Date();
// find stable position
var count = 0;
@ -1637,7 +1661,7 @@ Graph.prototype._doStabilize = function() {
count++;
}
var end = new Date();
// var end = new Date();
// console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup
};
@ -1648,28 +1672,31 @@ Graph.prototype._doStabilize = function() {
* @private
*/
Graph.prototype._calculateForces = function() {
if (this.nodeIndices.length == 1) { // stop calculation if there is only one node
// stop calculation if there is only one node
if (this.nodeIndices.length == 1) {
this.nodes[this.nodeIndices[0]]._setForce(0,0);
}
// if there are too many nodes on screen, we cluster without repositioning
else if (this.nodeIndices.length > this.constants.clustering.maxNumberOfNodes * 4) {
console.log(this.nodeIndices.length, this.constants.clustering.maxNumberOfNodes * 4)
this.clusterToFit(false);
this.clusterToFit(this.constants.clustering.maxNumberOfNodes * 2, false);
this._calculateForces();
}
else {
// create a local edge to the nodes and edges, that is faster
var id, dx, dy, angle, distance, fx, fy,
var dx, dy, angle, distance, fx, fy,
repulsingForce, springForce, length, edgeLength,
nodes = this.nodes,
edges = this.edges;
node, node1, node2, edge, edgeID, i, j, nodeID, xCenter, yCenter;
var clusterSize;
var nodes = this.nodes;
var edges = this.edges;
// Gravity is required to keep separated groups from floating off
// the forces are reset to zero in this loop by using _setForce instead
// of _addForce
var gravity = 0.08;
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = nodes[this.nodeIndices[i]];
for (i = 0; i < this.nodeIndices.length; i++) {
node = nodes[this.nodeIndices[i]];
dx = -node.x - this.translation.x + this.frame.canvas.clientWidth*0.5;
dy = -node.y - this.translation.y + this.frame.canvas.clientHeight*0.5;
@ -1681,7 +1708,7 @@ Graph.prototype._calculateForces = function() {
node.updateDamping(this.nodeIndices.length);
}
this._updateLabels();
this.updateLabels();
// repulsing forces between nodes
var minimumDistance = this.constants.nodes.distance,
@ -1690,11 +1717,11 @@ Graph.prototype._calculateForces = function() {
// we loop from i over all but the last entree in the array
// j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
for (var i = 0; i < this.nodeIndices.length-1; i++) {
var node1 = nodes[this.nodeIndices[i]];
for (var j = i+1; j < this.nodeIndices.length; j++) {
var node2 = nodes[this.nodeIndices[j]];
var clusterSize = (node1.clusterSize + node2.clusterSize - 2);
for (i = 0; i < this.nodeIndices.length-1; i++) {
node1 = nodes[this.nodeIndices[i]];
for (j = i+1; j < this.nodeIndices.length; j++) {
node2 = nodes[this.nodeIndices[j]];
clusterSize = (node1.clusterSize + node2.clusterSize - 2);
dx = node2.x - node1.x;
dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy);
@ -1714,7 +1741,6 @@ Graph.prototype._calculateForces = function() {
//repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
}
// amplify the repulsion for clusters.
repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification;
@ -1727,38 +1753,53 @@ Graph.prototype._calculateForces = function() {
}
}
// TODO: re-implement repulsion of edges
for (var n = 0; n < nodes.length; n++) {
for (var l = 0; l < edges.length; l++) {
var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
// calculate normally distributed force
dx = nodes[n].x - lx,
dy = nodes[n].y - ly,
distance = Math.sqrt(dx * dx + dy * dy),
angle = Math.atan2(dy, dx),
// TODO: correct factor for repulsing force
//var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
//repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force
fx = Math.cos(angle) * repulsingforce,
fy = Math.sin(angle) * repulsingforce;
nodes[n]._addForce(fx, fy);
edges[l].from._addForce(-fx/2,-fy/2);
edges[l].to._addForce(-fx/2,-fy/2);
}
}
/*
// repulsion of the edges on the nodes and
for (var nodeID in nodes) {
if (nodes.hasOwnProperty(nodeID)) {
node = nodes[nodeID];
for(var edgeID in edges) {
if (edges.hasOwnProperty(edgeID)) {
edge = edges[edgeID];
// get the center of the edge
xCenter = edge.from.x+(edge.to.x - edge.from.x)/2;
yCenter = edge.from.y+(edge.to.y - edge.from.y)/2;
// calculate normally distributed force
dx = node.x - xCenter;
dy = node.y - yCenter;
distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045
angle = Math.atan2(dy, dx);
if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307
repulsingForce = 1.0;
}
else {
// TODO: correct factor for repulsing force
//var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
//repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
repulsingForce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)); // TODO: customize the repulsing force
}
fx = Math.cos(angle) * repulsingForce;
fy = Math.sin(angle) * repulsingForce;
node._addForce(fx, fy);
edge.from._addForce(-fx/2,-fy/2);
edge.to._addForce(-fx/2,-fy/2);
}
}
}
}
}
*/
// forces caused by the edges, modelled as springs
for (id in edges) {
if (edges.hasOwnProperty(id)) {
var edge = edges[id];
for (edgeID in edges) {
if (edges.hasOwnProperty(edgeID)) {
edge = edges[edgeID];
if (edge.connected) {
var clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2);
clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2);
dx = (edge.to.x - edge.from.x);
dy = (edge.to.y - edge.from.y);
//edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin
@ -1772,9 +1813,6 @@ Graph.prototype._calculateForces = function() {
springForce = edge.stiffness * (edgeLength - length);
// boost strength of cluster springs
springForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.edgeStrength;
fx = Math.cos(angle) * springForce;
fy = Math.sin(angle) * springForce;
@ -1783,7 +1821,7 @@ Graph.prototype._calculateForces = function() {
}
}
}
/*
/*
// TODO: re-implement repulsion of edges
// repulsing forces between edges
@ -1820,7 +1858,7 @@ Graph.prototype._calculateForces = function() {
edges[l2].to._addForce(fx, fy);
}
}
*/
*/
}
};
@ -1915,15 +1953,8 @@ Graph.prototype.singleStep = function() {
}
};
/**
* Stop animating nodes and edges.
*/
Graph.prototype.stop = function () {
if (this.timer) {
window.clearInterval(this.timer);
this.timer = undefined;
}
};
/**
* Freeze the animation
@ -1936,5 +1967,4 @@ Graph.prototype.toggleFreeze = function() {
this.freezeSimulation = false;
this.start();
}
console.log('freezeSimulation',this.freezeSimulation)
}
};

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

@ -22,7 +22,7 @@ Cluster.prototype.openCluster = function(node) {
// housekeeping
this._updateNodeIndexList();
this._updateDynamicEdges();
this._updateLabels();
this.updateLabels();
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
@ -30,8 +30,14 @@ Cluster.prototype.openCluster = function(node) {
}
};
/**
* This calls the updateClustes with default arguments
*/
Cluster.prototype.updateClustersDefault = function() {
if (this.constants.clustering.enableClustering) {
this.updateClusters(0,false,false);
}
}
/**
* This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
@ -59,11 +65,10 @@ Cluster.prototype.decreaseClusterLevel = function() {
* If out, check if we can form clusters, if in, check if we can open clusters.
* This function is only called from _zoom()
*
* @param {Int} zoomDirection
* @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn
* @param {Boolean} recursive | enable or disable recursive calling of the opening of clusters
* @param {Boolean} force | enable or disable forcing
*
* @private
*/
Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) {
var isMovingBeforeClustering = this.moving;
@ -78,17 +83,24 @@ Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) {
}
this._updateNodeIndexList();
// if a cluster was NOT formed and the user zoomed out, we try clustering by hubs and update the index again
// if a cluster was NOT formed and the user zoomed out, we try clustering by hubs
if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
this._aggregateHubs(force);
}
// we now reduce snakes.
if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
this.handleSnakes();
this._updateNodeIndexList();
}
this.previousScale = this.scale;
// rest of the housekeeping
this._updateDynamicEdges();
this._updateLabels();
this.updateLabels();
// if a cluster was formed, we increase the clusterSession
if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
@ -101,6 +113,18 @@ Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) {
}
};
/**
* This function handles the snakes. It is called on every updateClusters().
*/
Cluster.prototype.handleSnakes = function() {
// after clustering we check how many snakes there are
var snakePercentage = this._getSnakeFraction();
if (snakePercentage > this.constants.clustering.snakeThreshold) {
this._reduceAmountOfSnakes(1 - this.constants.clustering.snakeThreshold / snakePercentage)
}
};
/**
* this functions starts clustering by hubs
* The minimum hub threshold is set globally
@ -109,7 +133,7 @@ Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) {
*/
Cluster.prototype._aggregateHubs = function(force) {
this._getHubSize();
this._clusterByHub(force);
this._formClustersByHub(force);
};
@ -126,7 +150,7 @@ Cluster.prototype.forceAggregateHubs = function() {
// housekeeping
this._updateNodeIndexList();
this._updateDynamicEdges();
this._updateLabels();
this.updateLabels();
// if a cluster was formed, we increase the clusterSession
if (this.nodeIndices.length != amountOfNodes) {
@ -168,11 +192,10 @@ Cluster.prototype._openClusters = function(recursive,force) {
* @private
*/
Cluster.prototype._expandClusterNode = function(parentNode, recursive, force, openAll) {
var openedCluster = false;
// first check if node is a cluster
if (parentNode.clusterSize > 1) {
// this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20
if (parentNode.clusterSize < 20 && force == false) {
if (parentNode.clusterSize < 20) {
openAll = true;
}
recursive = openAll ? true : recursive;
@ -189,21 +212,16 @@ Cluster.prototype._expandClusterNode = function(parentNode, recursive, force, op
if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1]
|| openAll) {
this._expelChildFromParent(parentNode,containedNodeID,recursive,force,openAll);
openedCluster = true;
}
}
else {
if (this._parentNodeInActiveArea(parentNode)) {
this._expelChildFromParent(parentNode,containedNodeID,recursive,force,openAll);
openedCluster = true;
}
}
}
}
}
if (openedCluster == true) {
parentNode.clusterSessions.pop();
}
}
};
@ -246,15 +264,30 @@ Cluster.prototype._expelChildFromParent = function(parentNode, containedNodeID,
parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
// place the child node near the parent, not at the exact same location to avoid chaos in the system
childNode.x = parentNode.x + this.constants.edges.length * 0.2 * (0.5 - Math.random()) * parentNode.clusterSize;
childNode.y = parentNode.y + this.constants.edges.length * 0.2 * (0.5 - Math.random()) * parentNode.clusterSize;
// remove the clusterSession from the child node
childNode.clusterSession = 0;
childNode.x = parentNode.x + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize;
childNode.y = parentNode.y + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize;
// remove node from the list
delete parentNode.containedNodes[containedNodeID];
// check if there are other childs with this clusterSession in the parent.
var othersPresent = false;
for (var childNodeID in parentNode.containedNodes) {
if (parentNode.containedNodes.hasOwnProperty(childNodeID)) {
if (parentNode.containedNodes[childNodeID].clusterSession == childNode.clusterSession) {
othersPresent = true;
break;
}
}
}
// if there are no others, remove the cluster session from the list
if (othersPresent == false) {
parentNode.clusterSessions.pop();
}
// remove the clusterSession from the child node
childNode.clusterSession = 0;
// restart the simulation to reorganise all nodes
this.moving = true;
@ -360,66 +393,86 @@ Cluster.prototype._forceClustersByZoom = function() {
/**
* This function forms clusters from hubs, it loops over all nodes
*
* @param {Boolean} force
* @param {Boolean} force | Disregard zoom level
* @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
* @private
*/
Cluster.prototype._clusterByHub = function(force) {
var dx,dy,length;
var minLength = this.constants.clustering.clusterLength/this.scale;
var allowCluster = false;
Cluster.prototype._formClustersByHub = function(force, onlyEqual) {
// we loop over all nodes in the list
for (var nodeID in this.nodes) {
// we check if it is still available since it can be used by the clustering in this loop
if (this.nodes.hasOwnProperty(nodeID)) {
var hubNode = this.nodes[nodeID];
// we decide if the node is a hub
if (hubNode.dynamicEdgesLength >= this.hubThreshold) {
// we create a list of edges because the dynamicEdges change over the course of this loop
var edgesIDarray = [];
var amountOfInitialEdges = hubNode.dynamicEdges.length;
for (var j = 0; j < amountOfInitialEdges; j++) {
edgesIDarray.push(hubNode.dynamicEdges[j].id);
}
this._formClusterFromHub(this.nodes[nodeID],force,onlyEqual);
}
}
};
// if the hub clustering is not forces, we check if one of the edges connected
// to a cluster is small enough based on the constants.clustering.clusterLength
if (force == false) {
allowCluster = false;
for (j = 0; j < amountOfInitialEdges; j++) {
var edge = this.edges[edgesIDarray[j]];
if (edge !== undefined) {
if (edge.connected) {
dx = (edge.to.x - edge.from.x);
dy = (edge.to.y - edge.from.y);
length = Math.sqrt(dx * dx + dy * dy);
if (length < minLength) {
allowCluster = true;
break;
}
}
/**
* This function forms a cluster from a specific preselected hub node
*
* @param {Node} hubNode | the node we will cluster as a hub
* @param {Boolean} force | Disregard zoom level
* @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
* @param [Number] absorptionSizeOffset |
* @private
*/
Cluster.prototype._formClusterFromHub = function(hubNode, force, onlyEqual, absorptionSizeOffset) {
if (absorptionSizeOffset === undefined) {
absorptionSizeOffset = 0;
}
// we decide if the node is a hub
if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) ||
(hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) {
// initialize variables
var dx,dy,length;
var minLength = this.constants.clustering.clusterLength/this.scale;
var allowCluster = false;
// we create a list of edges because the dynamicEdges change over the course of this loop
var edgesIDarray = [];
var amountOfInitialEdges = hubNode.dynamicEdges.length;
for (var j = 0; j < amountOfInitialEdges; j++) {
edgesIDarray.push(hubNode.dynamicEdges[j].id);
}
// if the hub clustering is not forces, we check if one of the edges connected
// to a cluster is small enough based on the constants.clustering.clusterLength
if (force == false) {
allowCluster = false;
for (j = 0; j < amountOfInitialEdges; j++) {
var edge = this.edges[edgesIDarray[j]];
if (edge !== undefined) {
if (edge.connected) {
dx = (edge.to.x - edge.from.x);
dy = (edge.to.y - edge.from.y);
length = Math.sqrt(dx * dx + dy * dy);
if (length < minLength) {
allowCluster = true;
break;
}
}
}
}
}
// start the clustering if allowed
if ((!force && allowCluster) || force) {
// we loop over all edges INITIALLY connected to this hub
for (j = 0; j < amountOfInitialEdges; j++) {
edge = this.edges[edgesIDarray[j]];
// start the clustering if allowed
if ((!force && allowCluster) || force) {
// we loop over all edges INITIALLY connected to this hub
for (j = 0; j < amountOfInitialEdges; j++) {
edge = this.edges[edgesIDarray[j]];
// the edge can be clustered by this function in a previous loop
if (edge !== undefined) {
var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
// the edge can be clustered by this function in a previous loop
if (edge !== undefined) {
var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
// we do not want hubs to merge with other hubs.
if (childNode.dynamicEdges.length <= this.hubThreshold) {
this._addToCluster(hubNode,childNode,force);
}
}
// we do not want hubs to merge with other hubs.
if (childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) {
this._addToCluster(hubNode,childNode,force);
}
}
}
@ -429,7 +482,6 @@ Cluster.prototype._clusterByHub = function(force) {
/**
* This function adds the child node to the parent node, creating a cluster if it is not already.
* This function is called only from updateClusters()
@ -472,7 +524,7 @@ Cluster.prototype._addToCluster = function(parentNode, childNode, force) {
// giving the clusters a dynamic formationScale to ensure not all clusters open up when zoomed
if (force == true) {
parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+2);
parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3);
}
else {
parentNode.formationScale = this.scale; // The latest child has been added on this scale
@ -650,7 +702,6 @@ Cluster.prototype._connectEdgeBackToChild = function(parentNode, childNode) {
* @private
*/
Cluster.prototype._validateEdges = function(parentNode) {
// TODO: check if good idea
for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
var edge = parentNode.dynamicEdges[i];
if (parentNode.id != edge.toId && parentNode.id != edge.fromId) {
@ -692,9 +743,8 @@ Cluster.prototype._releaseContainedEdges = function(parentNode, childNode) {
/**
* This updates the node labels for all nodes (for debugging purposes)
* @private
*/
Cluster.prototype._updateLabels = function() {
Cluster.prototype.updateLabels = function() {
var nodeID;
// update node labels
for (nodeID in this.nodes) {
@ -748,9 +798,8 @@ Cluster.prototype._parentNodeInActiveArea = function(node) {
* This is an adaptation of the original repositioning function. This is called if the system is clustered initially
* It puts large clusters away from the center and randomizes the order.
*
* @private
*/
Cluster.prototype._repositionNodes = function() {
Cluster.prototype.repositionNodes = function() {
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
if (!node.isFixed()) {
@ -806,21 +855,42 @@ Cluster.prototype._getHubSize = function() {
};
/**
* We reduce the amount of "extension nodes" or snakes. These are not quickly clustered with the outliers and hubs methods
* with this amount we can cluster specifically on these snakes.
*
* @param {double} fraction | between 0 and 1, the percentage of snakes to reduce
* @private
*/
Cluster.prototype._reduceAmountOfSnakes = function(fraction) {
this.hubThreshold = 2;
for (nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) {
if (this.nodes[nodeID].dynamicEdgesLength == 2 && this.nodes[nodeID].dynamicEdges.length >= 2) {
if (Math.random() <= fraction) {
this._formClusterFromHub(this.nodes[nodeID],true,true,1)
}
}
}
}
};
/**
* We get the amount of "extension nodes" or snakes. These are not quickly clustered with the outliers and hubs methods
* with this amount we can cluster specifically on these snakes.
*
* @returns {number}
* @private
*/
Cluster.prototype._getAmountOfSnakes = function() {
Cluster.prototype._getSnakeFraction = function() {
var snakes = 0;
var total = 0;
for (nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) {
if (this.nodes[nodeID].dynamicEdges.length == 2) {
if (this.nodes[nodeID].dynamicEdgesLength == 2 && this.nodes[nodeID].dynamicEdges.length >= 2) {
snakes += 1;
}
total += 1;
}
}
return snakes;
};
return snakes/total;
};

Loading…
Cancel
Save