Browse Source

Seperated clustering functions in cluster object

css_transitions
Alex de Mulder 10 years ago
parent
commit
7687d4ef44
5 changed files with 638 additions and 617 deletions
  1. +2
    -0
      Jakefile.js
  2. +8
    -407
      src/graph/Graph.js
  3. +408
    -0
      src/graph/cluster.js
  4. +214
    -204
      vis.js
  5. +6
    -6
      vis.min.js

+ 2
- 0
Jakefile.js View File

@ -79,8 +79,10 @@ task('build', {async: true}, function () {
'./src/graph/Popup.js',
'./src/graph/Groups.js',
'./src/graph/Images.js',
'./src/graph/Cluster.js',
'./src/graph/Graph.js',
'./src/module/exports.js'
],

+ 8
- 407
src/graph/Graph.js View File

@ -72,7 +72,7 @@ function Graph (container, data, options) {
}
},
clustering: {
clusterLength: 50, // threshold edge length for clustering
clusterLength: 30, // threshold edge length for clustering
fontSizeMultiplier: 2, // how much the cluster font size grows per node (in px)
forceAmplification: 0.6, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force
distanceAmplification: 0.1, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force
@ -87,8 +87,9 @@ function Graph (container, data, options) {
maxIterations: 1000 // maximum number of iteration to stabilize
};
Cluster.call(this);
var graph = this;
this.clusterSession = 0;
this.nodeIndices = []; // the node indices list is used to speed up the computation of the repulsion fields
this.nodes = {}; // object with Node objects
this.edges = {}; // object with Edge objects
@ -150,16 +151,18 @@ function Graph (container, data, options) {
// apply options
this.setOptions(options);
// draw data
this.setData(data);
// zoom so all data will fit on the screen
this.zoomToFit();
// cluster if the dataset is big
// cluster if the data set is big
this.clusterToFit();
}
Graph.prototype = Object.create(Cluster.prototype);
Graph.prototype.clusterToFit = function() {
var numberOfNodes = this.nodeIndices.length;
@ -187,405 +190,6 @@ Graph.prototype.zoomToFit = function() {
this._setScale(zoomLevel);
};
/**
* This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
* be clustered with their connected node. This can be repeated as many times as needed.
* This can be called externally (by a keybind for instance) to reduce the complexity of big datasets.
*/
Graph.prototype.increaseClusterLevel = function() {
var isMovingBeforeClustering = this.moving;
this._formClusters(true);
this._updateLabels();
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
}
};
/**
* This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will
* be unpacked if they are a cluster. This can be repeated as many times as needed.
* This can be called externally (by a key-bind for instance) to look into clusters without zooming.
*/
Graph.prototype.decreaseClusterLevel = function() {
var isMovingBeforeClustering = this.moving;
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
if (node.clusterSize > 1) {
this._expandClusterNode(node,false,true);
}
}
this._updateNodeIndexList();
this._updateLabels();
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
}
this.clusterSession = (this.clusterSession == 0) ? 0 : this.clusterSession - 1;
};
/**
* This function can be called to open up a specific cluster.
* It will recursively unpack the entire cluster back to individual nodes.
*
* @param node | Node object: cluster to open.
*/
Graph.prototype.fullyOpenCluster = function(node) {
var isMovingBeforeClustering = this.moving;
this._expandClusterNode(node,true,true);
this._updateNodeIndexList();
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
}
};
/**
* This function can be called to open up a specific cluster.
* It will unpack the cluster back one level.
*
* @param node | Node object: cluster to open.
*/
Graph.prototype.openCluster = function(node) {
var isMovingBeforeClustering = this.moving;
this._expandClusterNode(node,false,true);
this._updateNodeIndexList();
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
}
};
/**
* This function checks if the zoom action is in or out.
* If out, check if we can form clusters, if in, check if we can open clusters.
* This function is only called from _zoom()
*
* @private
*/
Graph.prototype._updateClusters = function() {
var isMovingBeforeClustering = this.moving;
if (this.previousScale > this.scale) { // zoom out
this._formClusters(false);
}
else if (this.previousScale < this.scale) { // zoom out
this._openClusters();
}
this._updateClusterLabels();
this._updateNodeLabels();
this._updateLabels();
this.previousScale = this.scale;
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
}
};
/**
* This updates the node labels for all nodes (for debugging purposes)
* @private
*/
Graph.prototype._updateLabels = function() {
// update node labels
for (var nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) {
var node = this.nodes[nodeID];
node.label = String(node.remainingEdges).concat(":",node.remainingEdges_unapplied,":",String(node.clusterSize));
}
}
};
/**
* This updates the node labels for all clusters
* @private
*/
Graph.prototype._updateClusterLabels = function() {
// update node labels
for (var nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) {
var node = this.nodes[nodeID];
if (node.clusterSize > 1) {
node.label = "[".concat(String(node.clusterSize),"]");
}
}
}
};
/**
* This updates the node labels for all nodes that are NOT clusters
* @private
*/
Graph.prototype._updateNodeLabels = function() {
// update node labels
for (var nodeID in this.nodes) {
var node = this.nodes[nodeID];
if (node.clusterSize == 1) {
node.label = String(node.id);
}
}
};
/**
* This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it
* has to be opened based on the current zoom level.
*
* @private
*/
Graph.prototype._openClusters = function() {
var amountOfNodes = this.nodeIndices.length;
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
this._expandClusterNode(node,true,false);
}
this._updateNodeIndexList();
if (this.nodeIndices.length != amountOfNodes) { // this means a clustering operation has taken place
this.clusterSession += 1;
}
};
/**
* This function checks if a node has to be opened. This is done by checking the zoom level.
* If the node contains child nodes, this function is recursively called on the child nodes as well.
* This recursive behaviour is optional and can be set by the recursive argument.
*
* @param parentNode | Node object: to check for cluster and expand
* @param recursive | Boolean: enable or disable recursive calling
* @param forceExpand | Boolean: enable or disable forcing the last node to join the cluster to be expelled
* @private
*/
Graph.prototype._expandClusterNode = function(parentNode, recursive, forceExpand) {
// first check if node is a cluster
if (parentNode.clusterSize > 1) {
// if the last child has been added on a smaller scale than current scale (@optimization)
if (parentNode.formationScale < this.scale || forceExpand == true) {
// we will check if any of the contained child nodes should be removed from the cluster
for (var containedNodeID in parentNode.containedNodes) {
if (parentNode.containedNodes.hasOwnProperty(containedNodeID)) {
var childNode = parentNode.containedNodes[containedNodeID];
// force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
// the largest cluster is the one that comes from outside
// TODO: introduce a level system for keeping track of which node was added when.
if (forceExpand == true) {
if (childNode.clusterSession == this.clusterSession - 1) {
this._expelChildFromParent(parentNode,containedNodeID,recursive,forceExpand);
}
}
else {
this._expelChildFromParent(parentNode,containedNodeID,recursive,forceExpand);
}
}
}
}
}
};
/**
* This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove
* the child node from the parent contained_node object and put it back into the global nodes object.
* The same holds for the edge that was connected to the child node. It is moved back into the global edges object.
*
* @param parentNode | Node object: the parent node
* @param containedNodeID | String: child_node id as it is contained in the containedNodes object of the parent node
* @param recursive | Boolean: This will also check if the child needs to be expanded.
* With force and recursive both true, the entire cluster is unpacked
* @param forceExpand | Boolean: This will disregard the zoom level and will expel this child from the parent
* @private
*/
Graph.prototype._expelChildFromParent = function(parentNode, containedNodeID, recursive, forceExpand) {
var childNode = parentNode.containedNodes[containedNodeID];
// if child node has been added on smaller scale than current, kick out
if (childNode.formationScale < this.scale || forceExpand == true) {
// put the child node back in the global nodes object and the corresponding edge in the global edges object
this.nodes[containedNodeID] = childNode;
this.edges[parentNode.containedEdges[containedNodeID].id] = parentNode.containedEdges[containedNodeID];
// undo the changes from the clustering operation on the parent node
parentNode.mass -= this.constants.clustering.massTransferCoefficient * childNode.mass;
parentNode.fontSize -= this.constants.clustering.fontSizeMultiplier * childNode.clusterSize;
parentNode.clusterSize -= childNode.clusterSize;
parentNode.remainingEdges += 1;
parentNode.remainingEdges_unapplied = parentNode.remainingEdges;
// place the child node near the parent, not at the exact same location to avoid chaos in the system
childNode.x = parentNode.x;
childNode.y = parentNode.y;
// remove the clusterSession from the child node
childNode.clusterSession = 0;
// remove node from the list
delete parentNode.containedNodes[containedNodeID];
delete parentNode.containedEdges[containedNodeID];
// restart the simulation to reorganise all nodes
this.moving = true;
// recalculate the size of the node on the next time the node is rendered
parentNode.clearSizeCache();
}
// check if a further expansion step is possible if recursivity is enabled
if (recursive == true) {
this._expandClusterNode(childNode,recursive,forceExpand);
}
};
/**
* This function checks if any nodes at the end of their trees have edges below a threshold length
* This function is called only from _updateClusters()
* forceLevelCollapse ignores the length of the edge and collapses one level
* This means that a node with only one edge will be clustered with its connected node
*
* @private
* @param force_level_collapse | Boolean
*/
Graph.prototype._formClusters = function(forceLevelCollapse) {
var amountOfNodes = this.nodeIndices.length;
var min_length = this.constants.clustering.clusterLength/this.scale;
var dx,dy,length,
edges = this.edges;
// create an array of edge ids
var edgesIDarray = []
for (var id in edges) {
if (edges.hasOwnProperty(id)) {
edgesIDarray.push(id);
}
}
// check if any edges are shorter than min_length and start the clustering
// the clustering favours the node with the larger mass
for (var i = 0; i < edgesIDarray.length; i++) {
var edgeID = edgesIDarray[i];
var edge = edges[edgeID];
edge.id = edgeID;
if (edge.connected) {
dx = (edge.to.x - edge.from.x);
dy = (edge.to.y - edge.from.y);
length = Math.sqrt(dx * dx + dy * dy);
if (length < min_length || forceLevelCollapse == true) {
// checking for clustering possibilities
// first check which node is larger
var parentNode = edge.from
var childNode = edge.to
if (edge.to.mass > edge.from.mass) {
parentNode = edge.to
childNode = edge.from
}
// we allow clustering from outside in, ideally the child node in on the outside
// if we do not cluster from outside in, we would have to reconnect edges or keep a second set of edges for the
// clusters. This will also have to be altered in the force calculation and rendering.
// This method is non-destructive and does not require a second set of data.
if (childNode.remainingEdges == 1 && childNode.remainingEdges_unapplied != 0) {
this._addToCluster(parentNode,childNode,edge,forceLevelCollapse);
delete this.edges[edgesIDarray[i]];
}
else if (parentNode.remainingEdges == 1 && parentNode.remainingEdges_unapplied != 0) {
this._addToCluster(childNode,parentNode,edge,forceLevelCollapse);
delete this.edges[edgesIDarray[i]];
}
}
}
}
this._updateNodeIndexList();
if (forceLevelCollapse == true) {
this._applyClusterLevel();
}
if (this.nodeIndices.length != amountOfNodes) { // this means a clustering operation has taken place
this.clusterSession += 1;
}
};
/**
* This function adds the child node to the parent node, creating a cluster if it is not already.
* This function is called only from _updateClusters()
*
* @param parent_node | Node object: this is the node that will house the child node
* @param child_node | Node object: this node will be deleted from the global this.nodes and stored in the parent node
* @param edge | Edge object: this edge will be deleted from the global this.edges and stored in the parent node
* @param force_level_collapse | Boolean: true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
* @private
*/
Graph.prototype._addToCluster = function(parentNode, childNode, edge, forceLevelCollapse) {
// join child node and edge in parent node
parentNode.containedNodes[childNode.id] = childNode;
parentNode.containedEdges[childNode.id] = edge; // the edge gets the node ID so we can easily recover it when expanding the cluster
if (this.nodes.hasOwnProperty(childNode.id)) {
delete this.nodes[childNode.id];
}
childNode.clusterSession = this.clusterSession;
parentNode.mass += this.constants.clustering.massTransferCoefficient * childNode.mass;
parentNode.clusterSize += childNode.clusterSize;
parentNode.fontSize += this.constants.clustering.fontSizeMultiplier * childNode.clusterSize;
parentNode.formationScale = this.scale; // The latest child has been added on this scale
// recalculate the size of the node on the next time the node is rendered
parentNode.clearSizeCache();
parentNode.containedNodes[childNode.id].formationScale = this.scale; // this child has been added at this scale.
if (forceLevelCollapse == true) {
parentNode.remainingEdges_unapplied -= 1;
}
else {
parentNode.remainingEdges -= 1;
}
// restart the simulation to reorganise all nodes
this.moving = true;
};
/**
* This function will apply the changes made to the remainingEdges during the formation of the clusters.
* This is a seperate function to allow for level-wise collapsing of the node tree.
* It has to be called if a level is collapsed. It is called by _formClusters().
* @private
*/
Graph.prototype._applyClusterLevel = function() {
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
node.remainingEdges = node.remainingEdges_unapplied;
}
};
/**
* Update the this.nodeIndices with the most recent node index list
@ -1475,7 +1079,6 @@ Graph.prototype._getConnectionCount = function(level) {
return hubs;
};
/**
* Set a new size for the graph
* @param {string} width Width in pixels or percentage (for example '800px'
@ -1875,6 +1478,7 @@ Graph.prototype._getTranslation = function() {
Graph.prototype._setScale = function(scale) {
this.scale = scale;
};
/**
* Get the current scale of the graph
* @return {Number} scale Scaling factor 1.0 is unscaled
@ -2041,8 +1645,8 @@ Graph.prototype._calculateForces = function() {
distance = Math.sqrt(dx * dx + dy * dy);
// clusters have a larger region of influence
minimumDistance = (clusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + clusterSize * this.constants.clustering.distanceAmplification));
if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045
angle = Math.atan2(dy, dx);
@ -2053,9 +1657,6 @@ Graph.prototype._calculateForces = function() {
// TODO: correct factor for repulsing force
//repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
//repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
// clusters have a larger region of influence
repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
}

+ 408
- 0
src/graph/cluster.js View File

@ -0,0 +1,408 @@
/**
* @constructor Cluster
* Contains the cluster properties for the graph object
*/
function Cluster() {
this.clusterSession = 0;
}
/**
* This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
* be clustered with their connected node. This can be repeated as many times as needed.
* This can be called externally (by a keybind for instance) to reduce the complexity of big datasets.
*/
Cluster.prototype.increaseClusterLevel = function() {
var isMovingBeforeClustering = this.moving;
this._formClusters(true);
this._updateLabels();
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
}
};
/**
* This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will
* be unpacked if they are a cluster. This can be repeated as many times as needed.
* This can be called externally (by a key-bind for instance) to look into clusters without zooming.
*/
Cluster.prototype.decreaseClusterLevel = function() {
var isMovingBeforeClustering = this.moving;
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
if (node.clusterSize > 1) {
this._expandClusterNode(node,false,true);
}
}
this._updateNodeIndexList();
this._updateLabels();
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
}
this.clusterSession = (this.clusterSession == 0) ? 0 : this.clusterSession - 1;
};
/**
* This function can be called to open up a specific cluster.
* It will recursively unpack the entire cluster back to individual nodes.
*
* @param node | Node object: cluster to open.
*/
Cluster.prototype.fullyOpenCluster = function(node) {
var isMovingBeforeClustering = this.moving;
this._expandClusterNode(node,true,true);
this._updateNodeIndexList();
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
}
};
/**
* This function can be called to open up a specific cluster.
* It will unpack the cluster back one level.
*
* @param node | Node object: cluster to open.
*/
Cluster.prototype.openCluster = function(node) {
var isMovingBeforeClustering = this.moving;
this._expandClusterNode(node,false,true);
this._updateNodeIndexList();
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
}
};
/**
* This function checks if the zoom action is in or out.
* If out, check if we can form clusters, if in, check if we can open clusters.
* This function is only called from _zoom()
*
* @private
*/
Cluster.prototype._updateClusters = function() {
var isMovingBeforeClustering = this.moving;
if (this.previousScale > this.scale) { // zoom out
this._formClusters(false);
}
else if (this.previousScale < this.scale) { // zoom out
this._openClusters();
}
this._updateClusterLabels();
this._updateNodeLabels();
this._updateLabels();
this.previousScale = this.scale;
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
}
};
/**
* This updates the node labels for all nodes (for debugging purposes)
* @private
*/
Cluster.prototype._updateLabels = function() {
// update node labels
for (var nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) {
var node = this.nodes[nodeID];
node.label = String(node.remainingEdges).concat(":",node.remainingEdges_unapplied,":",String(node.clusterSize));
}
}
};
/**
* This updates the node labels for all clusters
* @private
*/
Cluster.prototype._updateClusterLabels = function() {
// update node labels
for (var nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) {
var node = this.nodes[nodeID];
if (node.clusterSize > 1) {
node.label = "[".concat(String(node.clusterSize),"]");
}
}
}
};
/**
* This updates the node labels for all nodes that are NOT clusters
* @private
*/
Cluster.prototype._updateNodeLabels = function() {
// update node labels
for (var nodeID in this.nodes) {
var node = this.nodes[nodeID];
if (node.clusterSize == 1) {
node.label = String(node.id);
}
}
};
/**
* This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it
* has to be opened based on the current zoom level.
*
* @private
*/
Cluster.prototype._openClusters = function() {
var amountOfNodes = this.nodeIndices.length;
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
this._expandClusterNode(node,true,false);
}
this._updateNodeIndexList();
if (this.nodeIndices.length != amountOfNodes) { // this means a clustering operation has taken place
this.clusterSession -= 1;
}
};
/**
* This function checks if a node has to be opened. This is done by checking the zoom level.
* If the node contains child nodes, this function is recursively called on the child nodes as well.
* This recursive behaviour is optional and can be set by the recursive argument.
*
* @param parentNode | Node object: to check for cluster and expand
* @param recursive | Boolean: enable or disable recursive calling
* @param forceExpand | Boolean: enable or disable forcing the last node to join the cluster to be expelled
* @private
*/
Cluster.prototype._expandClusterNode = function(parentNode, recursive, forceExpand) {
// first check if node is a cluster
if (parentNode.clusterSize > 1) {
// if the last child has been added on a smaller scale than current scale (@optimization)
if (parentNode.formationScale < this.scale || forceExpand == true) {
// we will check if any of the contained child nodes should be removed from the cluster
for (var containedNodeID in parentNode.containedNodes) {
if (parentNode.containedNodes.hasOwnProperty(containedNodeID)) {
var childNode = parentNode.containedNodes[containedNodeID];
// force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
// the largest cluster is the one that comes from outside
if (forceExpand == true) {
if (childNode.clusterSession == this.clusterSession - 1) {
this._expelChildFromParent(parentNode,containedNodeID,recursive,forceExpand);
}
}
else {
this._expelChildFromParent(parentNode,containedNodeID,recursive,forceExpand);
}
}
}
}
}
};
/**
* This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove
* the child node from the parent contained_node object and put it back into the global nodes object.
* The same holds for the edge that was connected to the child node. It is moved back into the global edges object.
*
* @param parentNode | Node object: the parent node
* @param containedNodeID | String: child_node id as it is contained in the containedNodes object of the parent node
* @param recursive | Boolean: This will also check if the child needs to be expanded.
* With force and recursive both true, the entire cluster is unpacked
* @param forceExpand | Boolean: This will disregard the zoom level and will expel this child from the parent
* @private
*/
Cluster.prototype._expelChildFromParent = function(parentNode, containedNodeID, recursive, forceExpand) {
var childNode = parentNode.containedNodes[containedNodeID];
// if child node has been added on smaller scale than current, kick out
if (childNode.formationScale < this.scale || forceExpand == true) {
// put the child node back in the global nodes object and the corresponding edge in the global edges object
this.nodes[containedNodeID] = childNode;
this.edges[parentNode.containedEdges[containedNodeID].id] = parentNode.containedEdges[containedNodeID];
// undo the changes from the clustering operation on the parent node
parentNode.mass -= this.constants.clustering.massTransferCoefficient * childNode.mass;
parentNode.fontSize -= this.constants.clustering.fontSizeMultiplier * childNode.clusterSize;
parentNode.clusterSize -= childNode.clusterSize;
parentNode.remainingEdges += 1;
parentNode.remainingEdges_unapplied = parentNode.remainingEdges;
// place the child node near the parent, not at the exact same location to avoid chaos in the system
childNode.x = parentNode.x;
childNode.y = parentNode.y;
// remove the clusterSession from the child node
childNode.clusterSession = 0;
// remove node from the list
delete parentNode.containedNodes[containedNodeID];
delete parentNode.containedEdges[containedNodeID];
// restart the simulation to reorganise all nodes
this.moving = true;
// recalculate the size of the node on the next time the node is rendered
parentNode.clearSizeCache();
}
// check if a further expansion step is possible if recursivity is enabled
if (recursive == true) {
this._expandClusterNode(childNode,recursive,forceExpand);
}
};
/**
* This function checks if any nodes at the end of their trees have edges below a threshold length
* This function is called only from _updateClusters()
* forceLevelCollapse ignores the length of the edge and collapses one level
* This means that a node with only one edge will be clustered with its connected node
*
* @private
* @param force_level_collapse | Boolean
*/
Cluster.prototype._formClusters = function(forceLevelCollapse) {
var amountOfNodes = this.nodeIndices.length;
var min_length = this.constants.clustering.clusterLength/this.scale;
var dx,dy,length,
edges = this.edges;
// create an array of edge ids
var edgesIDarray = []
for (var id in edges) {
if (edges.hasOwnProperty(id)) {
edgesIDarray.push(id);
}
}
// check if any edges are shorter than min_length and start the clustering
// the clustering favours the node with the larger mass
for (var i = 0; i < edgesIDarray.length; i++) {
var edgeID = edgesIDarray[i];
var edge = edges[edgeID];
edge.id = edgeID;
if (edge.connected) {
dx = (edge.to.x - edge.from.x);
dy = (edge.to.y - edge.from.y);
length = Math.sqrt(dx * dx + dy * dy);
if (length < min_length || forceLevelCollapse == true) {
// checking for clustering possibilities
// first check which node is larger
var parentNode = edge.from
var childNode = edge.to
if (edge.to.mass > edge.from.mass) {
parentNode = edge.to
childNode = edge.from
}
// we allow clustering from outside in, ideally the child node in on the outside
// if we do not cluster from outside in, we would have to reconnect edges or keep a second set of edges for the
// clusters. This will also have to be altered in the force calculation and rendering.
// This method is non-destructive and does not require a second set of data.
if (childNode.remainingEdges == 1 && childNode.remainingEdges_unapplied != 0) {
this._addToCluster(parentNode,childNode,edge,forceLevelCollapse);
delete this.edges[edgesIDarray[i]];
}
else if (parentNode.remainingEdges == 1 && parentNode.remainingEdges_unapplied != 0) {
this._addToCluster(childNode,parentNode,edge,forceLevelCollapse);
delete this.edges[edgesIDarray[i]];
}
}
}
}
this._updateNodeIndexList();
if (forceLevelCollapse == true) {
this._applyClusterLevel();
}
if (this.nodeIndices.length != amountOfNodes) { // this means a clustering operation has taken place
this.clusterSession += 1;
}
console.log(this.clusterSession)
};
/**
* This function adds the child node to the parent node, creating a cluster if it is not already.
* This function is called only from _updateClusters()
*
* @param parent_node | Node object: this is the node that will house the child node
* @param child_node | Node object: this node will be deleted from the global this.nodes and stored in the parent node
* @param edge | Edge object: this edge will be deleted from the global this.edges and stored in the parent node
* @param force_level_collapse | Boolean: true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
* @private
*/
Cluster.prototype._addToCluster = function(parentNode, childNode, edge, forceLevelCollapse) {
// join child node and edge in parent node
parentNode.containedNodes[childNode.id] = childNode;
parentNode.containedEdges[childNode.id] = edge; // the edge gets the node ID so we can easily recover it when expanding the cluster
if (this.nodes.hasOwnProperty(childNode.id)) {
delete this.nodes[childNode.id];
}
childNode.clusterSession = this.clusterSession;
parentNode.mass += this.constants.clustering.massTransferCoefficient * childNode.mass;
parentNode.clusterSize += childNode.clusterSize;
parentNode.fontSize += this.constants.clustering.fontSizeMultiplier * childNode.clusterSize;
parentNode.formationScale = this.scale; // The latest child has been added on this scale
// recalculate the size of the node on the next time the node is rendered
parentNode.clearSizeCache();
parentNode.containedNodes[childNode.id].formationScale = this.scale; // this child has been added at this scale.
if (forceLevelCollapse == true) {
parentNode.remainingEdges_unapplied -= 1;
}
else {
parentNode.remainingEdges -= 1;
}
// restart the simulation to reorganise all nodes
this.moving = true;
};
/**
* This function will apply the changes made to the remainingEdges during the formation of the clusters.
* This is a seperate function to allow for level-wise collapsing of the node tree.
* It has to be called if a level is collapsed. It is called by _formClusters().
* @private
*/
Cluster.prototype._applyClusterLevel = function() {
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
node.remainingEdges = node.remainingEdges_unapplied;
}
};

+ 214
- 204
vis.js View File

@ -14911,200 +14911,21 @@ Images.prototype.load = function(url) {
};
/**
* @constructor Graph
* Create a graph visualization, displaying nodes and edges.
*
* @param {Element} container The DOM element in which the Graph will
* be created. Normally a div element.
* @param {Object} data An object containing parameters
* {Array} nodes
* {Array} edges
* @param {Object} options Options
* @constructor Cluster
* Contains the cluster properties for the graph object
*/
function Graph (container, data, options) {
// create variables and set default values
this.containerElement = container;
this.width = '100%';
this.height = '100%';
this.refreshRate = 50; // milliseconds
this.stabilize = true; // stabilize before displaying the graph
this.selectable = true;
// set constant values
this.constants = {
nodes: {
radiusMin: 5,
radiusMax: 20,
radius: 5,
distance: 100, // px
shape: 'ellipse',
image: undefined,
widthMin: 16, // px
widthMax: 64, // px
fontColor: 'black',
fontSize: 14, // px
//fontFace: verdana,
fontFace: 'arial',
color: {
border: '#2B7CE9',
background: '#97C2FC',
highlight: {
border: '#2B7CE9',
background: '#D2E5FF'
},
cluster: {
border: '#256a2d',
background: '#2cd140',
highlight: {
border: '#899539',
background: '#c5dc29'
}
}
},
borderColor: '#2B7CE9',
backgroundColor: '#97C2FC',
highlightColor: '#D2E5FF',
group: undefined
},
edges: {
widthMin: 1,
widthMax: 15,
width: 1,
style: 'line',
color: '#343434',
fontColor: '#343434',
fontSize: 14, // px
fontFace: 'arial',
//distance: 100, //px
length: 100, // px
dash: {
length: 10,
gap: 5,
altLength: undefined
}
},
clustering: {
clusterLength: 50, // threshold edge length for clustering
fontSizeMultiplier: 2, // how much the cluster font size grows per node (in px)
forceAmplification: 0.6, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force
distanceAmplification: 0.1, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force
edgeGrowth: 10, // amount of clusterSize connected to the edge is multiplied with this and added to edgeLength
clusterSizeWidthFactor: 10,
clusterSizeHeightFactor: 10,
clusterSizeRadiusFactor: 10,
massTransferCoefficient: 0.2 // parent.mass += massTransferCoefficient * child.mass
},
minForce: 0.05,
minVelocity: 0.02, // px/s
maxIterations: 1000 // maximum number of iteration to stabilize
};
var graph = this;
function Cluster() {
this.clusterSession = 0;
this.nodeIndices = []; // the node indices list is used to speed up the computation of the repulsion fields
this.nodes = {}; // object with Node objects
this.edges = {}; // object with Edge objects
this.scale = 1; // defining the global scale variable in the constructor
this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
// TODO: create a counter to keep track on the number of nodes having values
// TODO: create a counter to keep track on the number of nodes currently moving
// TODO: create a counter to keep track on the number of edges having values
this.nodesData = null; // A DataSet or DataView
this.edgesData = null; // A DataSet or DataView
// create event listeners used to subscribe on the DataSets of the nodes and edges
var me = this;
this.nodesListeners = {
'add': function (event, params) {
me._addNodes(params.items);
me.start();
},
'update': function (event, params) {
me._updateNodes(params.items);
me.start();
},
'remove': function (event, params) {
me._removeNodes(params.items);
me.start();
}
};
this.edgesListeners = {
'add': function (event, params) {
me._addEdges(params.items);
me.start();
},
'update': function (event, params) {
me._updateEdges(params.items);
me.start();
},
'remove': function (event, params) {
me._removeEdges(params.items);
me.start();
}
};
this.groups = new Groups(); // object with groups
this.images = new Images(); // object with images
this.images.setOnloadCallback(function () {
graph._redraw();
});
// properties of the data
this.moving = false; // True if any of the nodes have an undefined position
this.selection = [];
this.timer = undefined;
// create a frame and canvas
this._create();
// apply options
this.setOptions(options);
// draw data
this.setData(data);
// zoom so all data will fit on the screen
this.zoomToFit();
// cluster if the dataset is big
this.clusterToFit();
}
Graph.prototype.clusterToFit = function() {
var numberOfNodes = this.nodeIndices.length;
var maxNumberOfNodes = 100;
var maxLevels = 10;
var level = 0;
while (numberOfNodes >= maxNumberOfNodes && level < maxLevels) {
this.increaseClusterLevel();
numberOfNodes = this.nodeIndices.length;
level += 1;
}
};
Graph.prototype.zoomToFit = function() {
var numberOfNodes = this.nodeIndices.length;
var zoomLevel = 105 / (numberOfNodes + 80); // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
if (zoomLevel > 1.0) {
zoomLevel = 1.0;
}
if (!('mousewheelScale' in this.pinch)) {
this.pinch.mousewheelScale = zoomLevel;
}
this._setScale(zoomLevel);
};
/**
* This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
* be clustered with their connected node. This can be repeated as many times as needed.
* This can be called externally (by a keybind for instance) to reduce the complexity of big datasets.
*/
Graph.prototype.increaseClusterLevel = function() {
Cluster.prototype.increaseClusterLevel = function() {
var isMovingBeforeClustering = this.moving;
this._formClusters(true);
@ -15122,7 +14943,7 @@ Graph.prototype.increaseClusterLevel = function() {
* be unpacked if they are a cluster. This can be repeated as many times as needed.
* This can be called externally (by a key-bind for instance) to look into clusters without zooming.
*/
Graph.prototype.decreaseClusterLevel = function() {
Cluster.prototype.decreaseClusterLevel = function() {
var isMovingBeforeClustering = this.moving;
for (var i = 0; i < this.nodeIndices.length; i++) {
@ -15150,7 +14971,7 @@ Graph.prototype.decreaseClusterLevel = function() {
*
* @param node | Node object: cluster to open.
*/
Graph.prototype.fullyOpenCluster = function(node) {
Cluster.prototype.fullyOpenCluster = function(node) {
var isMovingBeforeClustering = this.moving;
this._expandClusterNode(node,true,true);
@ -15169,7 +14990,7 @@ Graph.prototype.fullyOpenCluster = function(node) {
*
* @param node | Node object: cluster to open.
*/
Graph.prototype.openCluster = function(node) {
Cluster.prototype.openCluster = function(node) {
var isMovingBeforeClustering = this.moving;
this._expandClusterNode(node,false,true);
@ -15189,7 +15010,7 @@ Graph.prototype.openCluster = function(node) {
*
* @private
*/
Graph.prototype._updateClusters = function() {
Cluster.prototype._updateClusters = function() {
var isMovingBeforeClustering = this.moving;
if (this.previousScale > this.scale) { // zoom out
@ -15214,7 +15035,7 @@ Graph.prototype._updateClusters = function() {
* This updates the node labels for all nodes (for debugging purposes)
* @private
*/
Graph.prototype._updateLabels = function() {
Cluster.prototype._updateLabels = function() {
// update node labels
for (var nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) {
@ -15228,7 +15049,7 @@ Graph.prototype._updateLabels = function() {
* This updates the node labels for all clusters
* @private
*/
Graph.prototype._updateClusterLabels = function() {
Cluster.prototype._updateClusterLabels = function() {
// update node labels
for (var nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) {
@ -15244,7 +15065,7 @@ Graph.prototype._updateClusterLabels = function() {
* This updates the node labels for all nodes that are NOT clusters
* @private
*/
Graph.prototype._updateNodeLabels = function() {
Cluster.prototype._updateNodeLabels = function() {
// update node labels
for (var nodeID in this.nodes) {
var node = this.nodes[nodeID];
@ -15261,7 +15082,7 @@ Graph.prototype._updateNodeLabels = function() {
*
* @private
*/
Graph.prototype._openClusters = function() {
Cluster.prototype._openClusters = function() {
var amountOfNodes = this.nodeIndices.length;
for (var i = 0; i < this.nodeIndices.length; i++) {
@ -15272,7 +15093,7 @@ Graph.prototype._openClusters = function() {
this._updateNodeIndexList();
if (this.nodeIndices.length != amountOfNodes) { // this means a clustering operation has taken place
this.clusterSession += 1;
this.clusterSession -= 1;
}
};
@ -15287,7 +15108,7 @@ Graph.prototype._openClusters = function() {
* @param forceExpand | Boolean: enable or disable forcing the last node to join the cluster to be expelled
* @private
*/
Graph.prototype._expandClusterNode = function(parentNode, recursive, forceExpand) {
Cluster.prototype._expandClusterNode = function(parentNode, recursive, forceExpand) {
// first check if node is a cluster
if (parentNode.clusterSize > 1) {
// if the last child has been added on a smaller scale than current scale (@optimization)
@ -15299,7 +15120,6 @@ Graph.prototype._expandClusterNode = function(parentNode, recursive, forceExpand
// force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
// the largest cluster is the one that comes from outside
// TODO: introduce a level system for keeping track of which node was added when.
if (forceExpand == true) {
if (childNode.clusterSession == this.clusterSession - 1) {
this._expelChildFromParent(parentNode,containedNodeID,recursive,forceExpand);
@ -15327,7 +15147,7 @@ Graph.prototype._expandClusterNode = function(parentNode, recursive, forceExpand
* @param forceExpand | Boolean: This will disregard the zoom level and will expel this child from the parent
* @private
*/
Graph.prototype._expelChildFromParent = function(parentNode, containedNodeID, recursive, forceExpand) {
Cluster.prototype._expelChildFromParent = function(parentNode, containedNodeID, recursive, forceExpand) {
var childNode = parentNode.containedNodes[containedNodeID];
// if child node has been added on smaller scale than current, kick out
@ -15378,13 +15198,13 @@ Graph.prototype._expelChildFromParent = function(parentNode, containedNodeID, re
* @private
* @param force_level_collapse | Boolean
*/
Graph.prototype._formClusters = function(forceLevelCollapse) {
Cluster.prototype._formClusters = function(forceLevelCollapse) {
var amountOfNodes = this.nodeIndices.length;
var min_length = this.constants.clustering.clusterLength/this.scale;
var dx,dy,length,
edges = this.edges;
edges = this.edges;
// create an array of edge ids
var edgesIDarray = []
@ -15441,6 +15261,7 @@ Graph.prototype._formClusters = function(forceLevelCollapse) {
if (this.nodeIndices.length != amountOfNodes) { // this means a clustering operation has taken place
this.clusterSession += 1;
}
console.log(this.clusterSession)
};
@ -15454,7 +15275,7 @@ Graph.prototype._formClusters = function(forceLevelCollapse) {
* @param force_level_collapse | Boolean: true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
* @private
*/
Graph.prototype._addToCluster = function(parentNode, childNode, edge, forceLevelCollapse) {
Cluster.prototype._addToCluster = function(parentNode, childNode, edge, forceLevelCollapse) {
// join child node and edge in parent node
parentNode.containedNodes[childNode.id] = childNode;
parentNode.containedEdges[childNode.id] = edge; // the edge gets the node ID so we can easily recover it when expanding the cluster
@ -15491,13 +15312,205 @@ Graph.prototype._addToCluster = function(parentNode, childNode, edge, forceLevel
* It has to be called if a level is collapsed. It is called by _formClusters().
* @private
*/
Graph.prototype._applyClusterLevel = function() {
Cluster.prototype._applyClusterLevel = function() {
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
node.remainingEdges = node.remainingEdges_unapplied;
}
};
/**
* @constructor Graph
* Create a graph visualization, displaying nodes and edges.
*
* @param {Element} container The DOM element in which the Graph will
* be created. Normally a div element.
* @param {Object} data An object containing parameters
* {Array} nodes
* {Array} edges
* @param {Object} options Options
*/
function Graph (container, data, options) {
// create variables and set default values
this.containerElement = container;
this.width = '100%';
this.height = '100%';
this.refreshRate = 50; // milliseconds
this.stabilize = true; // stabilize before displaying the graph
this.selectable = true;
// set constant values
this.constants = {
nodes: {
radiusMin: 5,
radiusMax: 20,
radius: 5,
distance: 100, // px
shape: 'ellipse',
image: undefined,
widthMin: 16, // px
widthMax: 64, // px
fontColor: 'black',
fontSize: 14, // px
//fontFace: verdana,
fontFace: 'arial',
color: {
border: '#2B7CE9',
background: '#97C2FC',
highlight: {
border: '#2B7CE9',
background: '#D2E5FF'
},
cluster: {
border: '#256a2d',
background: '#2cd140',
highlight: {
border: '#899539',
background: '#c5dc29'
}
}
},
borderColor: '#2B7CE9',
backgroundColor: '#97C2FC',
highlightColor: '#D2E5FF',
group: undefined
},
edges: {
widthMin: 1,
widthMax: 15,
width: 1,
style: 'line',
color: '#343434',
fontColor: '#343434',
fontSize: 14, // px
fontFace: 'arial',
//distance: 100, //px
length: 100, // px
dash: {
length: 10,
gap: 5,
altLength: undefined
}
},
clustering: {
clusterLength: 30, // threshold edge length for clustering
fontSizeMultiplier: 2, // how much the cluster font size grows per node (in px)
forceAmplification: 0.6, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force
distanceAmplification: 0.1, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force
edgeGrowth: 10, // amount of clusterSize connected to the edge is multiplied with this and added to edgeLength
clusterSizeWidthFactor: 10,
clusterSizeHeightFactor: 10,
clusterSizeRadiusFactor: 10,
massTransferCoefficient: 0.2 // parent.mass += massTransferCoefficient * child.mass
},
minForce: 0.05,
minVelocity: 0.02, // px/s
maxIterations: 1000 // maximum number of iteration to stabilize
};
Cluster.call(this);
var graph = this;
this.nodeIndices = []; // the node indices list is used to speed up the computation of the repulsion fields
this.nodes = {}; // object with Node objects
this.edges = {}; // object with Edge objects
this.scale = 1; // defining the global scale variable in the constructor
this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
// TODO: create a counter to keep track on the number of nodes having values
// TODO: create a counter to keep track on the number of nodes currently moving
// TODO: create a counter to keep track on the number of edges having values
this.nodesData = null; // A DataSet or DataView
this.edgesData = null; // A DataSet or DataView
// create event listeners used to subscribe on the DataSets of the nodes and edges
var me = this;
this.nodesListeners = {
'add': function (event, params) {
me._addNodes(params.items);
me.start();
},
'update': function (event, params) {
me._updateNodes(params.items);
me.start();
},
'remove': function (event, params) {
me._removeNodes(params.items);
me.start();
}
};
this.edgesListeners = {
'add': function (event, params) {
me._addEdges(params.items);
me.start();
},
'update': function (event, params) {
me._updateEdges(params.items);
me.start();
},
'remove': function (event, params) {
me._removeEdges(params.items);
me.start();
}
};
this.groups = new Groups(); // object with groups
this.images = new Images(); // object with images
this.images.setOnloadCallback(function () {
graph._redraw();
});
// properties of the data
this.moving = false; // True if any of the nodes have an undefined position
this.selection = [];
this.timer = undefined;
// create a frame and canvas
this._create();
// apply options
this.setOptions(options);
// draw data
this.setData(data);
// zoom so all data will fit on the screen
this.zoomToFit();
// cluster if the data set is big
this.clusterToFit();
}
Graph.prototype = Object.create(Cluster.prototype);
Graph.prototype.clusterToFit = function() {
var numberOfNodes = this.nodeIndices.length;
var maxNumberOfNodes = 100;
var maxLevels = 10;
var level = 0;
while (numberOfNodes >= maxNumberOfNodes && level < maxLevels) {
this.increaseClusterLevel();
numberOfNodes = this.nodeIndices.length;
level += 1;
}
};
Graph.prototype.zoomToFit = function() {
var numberOfNodes = this.nodeIndices.length;
var zoomLevel = 105 / (numberOfNodes + 80); // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
if (zoomLevel > 1.0) {
zoomLevel = 1.0;
}
if (!('mousewheelScale' in this.pinch)) {
this.pinch.mousewheelScale = zoomLevel;
}
this._setScale(zoomLevel);
};
/**
* Update the this.nodeIndices with the most recent node index list
@ -16387,7 +16400,6 @@ Graph.prototype._getConnectionCount = function(level) {
return hubs;
};
/**
* Set a new size for the graph
* @param {string} width Width in pixels or percentage (for example '800px'
@ -16787,6 +16799,7 @@ Graph.prototype._getTranslation = function() {
Graph.prototype._setScale = function(scale) {
this.scale = scale;
};
/**
* Get the current scale of the graph
* @return {Number} scale Scaling factor 1.0 is unscaled
@ -16953,8 +16966,8 @@ Graph.prototype._calculateForces = function() {
distance = Math.sqrt(dx * dx + dy * dy);
// clusters have a larger region of influence
minimumDistance = (clusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + clusterSize * this.constants.clustering.distanceAmplification));
if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045
angle = Math.atan2(dy, dx);
@ -16965,9 +16978,6 @@ Graph.prototype._calculateForces = function() {
// TODO: correct factor for repulsing force
//repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
//repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
// clusters have a larger region of influence
repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
}

+ 6
- 6
vis.min.js
File diff suppressed because it is too large
View File


Loading…
Cancel
Save