Browse Source

clustering, looking for keypressevent

css_transitions
Alex de Mulder 10 years ago
parent
commit
782869463e
5 changed files with 449 additions and 310 deletions
  1. +1
    -0
      package.json
  2. +154
    -129
      src/graph/Graph.js
  3. +67
    -26
      src/graph/Node.js
  4. +3
    -0
      src/module/imports.js
  5. +224
    -155
      vis.js

+ 1
- 0
package.json View File

@ -30,6 +30,7 @@
"browserify": "latest",
"moment": "latest",
"hammerjs": "1.0.5",
"shortcut": "latest",
"node-watch": "latest"
}
}

+ 154
- 129
src/graph/Graph.js View File

@ -72,13 +72,14 @@ function Graph (container, data, options) {
}
},
clustering: {
clusterLength: 50, // threshold length for 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 cluster_size between two nodes multiply this value (+1) with the repulsion force
distanceAmplification: 0.1, // amount of cluster_size between two nodes multiply this value (+1) with the repulsion force
edgeGrowth: 10, // amount of cluster_size connected to the edge is multiplied with this and added to edgeLength
widthGrowth: 10, // growth factor = ((parent_size + child_size) / parent_size) * widthGrowthFactor
heightGrowth: 10, // growth factor = ((parent_size + child_size) / parent_size) * heightGrowthFactor
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.1 // parent.mass += massTransferCoefficient * child.mass
},
minForce: 0.05,
@ -87,11 +88,11 @@ function Graph (container, data, options) {
};
var graph = this;
this.node_indices = []; // the node indices list is used to speed up the computation of the repulsion fields
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.previous_scale = this.scale; // this is used to check if the zoom operation is zooming in or out
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
@ -150,15 +151,37 @@ function Graph (container, data, 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;
if (numberOfNodes >= 100) {
this.increaseClusterLevel();
}
};
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;
}
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.collapseClusterLevel = function() {
Graph.prototype.increaseClusterLevel = function() {
this._formClusters(true);
};
@ -167,10 +190,10 @@ Graph.prototype.collapseClusterLevel = 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.expandClusterLevel = function() {
for (var i = 0; i < this.node_indices.length; i++) {
var node = this.nodes[this.node_indices[i]];
if (node.cluster_size > 1 && node.remaining_edges == 1) {
Graph.prototype.decreaseClusterLevel = function() {
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
if (node.clusterSize > 1 && node.remainingEdges == 1) {
this._expandClusterNode(node,false,true);
}
}
@ -210,24 +233,26 @@ Graph.prototype.openCluster = function(node) {
* @private
*/
Graph.prototype._updateClusters = function() {
var moving_before_clustering = this.moving;
var isMovingBeforeClustering = this.moving;
if (this.previous_scale > this.scale) { // zoom out
if (this.previousScale > this.scale) { // zoom out
this._formClusters(false);
}
else if (this.previous_scale < this.scale) { // zoom out
else if (this.previousScale < this.scale) { // zoom out
this._openClusters();
}
this._updateClusterLabels();
this._updateNodeLabels();
this.previous_scale = this.scale;
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 != moving_before_clustering) {
if (this.moving != isMovingBeforeClustering) {
this.start();
}
// console.log(this.scale);
};
/**
@ -236,10 +261,10 @@ Graph.prototype._updateClusters = function() {
*/
Graph.prototype._updateLabels = function() {
// update node labels
for (var node_id in this.nodes) {
if (this.nodes.hasOwnProperty(node_id)) {
var node = this.nodes[node_id];
node.label = String(node.remaining_edges).concat(":",String(node.cluster_size));
for (var nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) {
var node = this.nodes[nodeID];
node.label = String(node.remainingEdges).concat(":",String(node.clusterSize));
}
}
};
@ -250,11 +275,11 @@ Graph.prototype._updateLabels = function() {
*/
Graph.prototype._updateClusterLabels = function() {
// update node labels
for (var node_id in this.nodes) {
if (this.nodes.hasOwnProperty(node_id)) {
var node = this.nodes[node_id];
if (node.cluster_size > 1) {
node.label = "[".concat(String(node.cluster_size),"]");
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),"]");
}
}
}
@ -266,9 +291,9 @@ Graph.prototype._updateClusterLabels = function() {
*/
Graph.prototype._updateNodeLabels = function() {
// update node labels
for (var node_id in this.nodes) {
var node = this.nodes[node_id];
if (node.cluster_size == 1) {
for (var nodeID in this.nodes) {
var node = this.nodes[nodeID];
if (node.clusterSize == 1) {
node.label = String(node.id);
}
}
@ -276,14 +301,14 @@ Graph.prototype._updateNodeLabels = function() {
/**
* This function loops over all nodes in the node_indices list. For each node it checks if it is a cluster and if it
* 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() {
for (var i = 0; i < this.node_indices.length; i++) {
var node = this.nodes[this.node_indices[i]];
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
this._expandClusterNode(node,true,false);
}
@ -296,44 +321,43 @@ Graph.prototype._openClusters = function() {
* If the node contains child nodes, this function is recursively called on the child nodes as well.
* This recursive behaviour is optional and can be set by the recursive argument.
*
* @param node | Node object: to check for cluster and expand
* @param parentNode | Node object: to check for cluster and expand
* @param recursive | Boolean: enable or disable recursive calling
* @param force_expand | Boolean: enable or disable forcing the last node to join the cluster to be expelled
* @param forceExpand | Boolean: enable or disable forcing the last node to join the cluster to be expelled
* @private
*/
Graph.prototype._expandClusterNode = function(node, recursive, force_expand) {
Graph.prototype._expandClusterNode = function(parentNode, recursive, forceExpand) {
// first check if node is a cluster
if (node.cluster_size > 1) {
if (parentNode.clusterSize > 1) {
// if the last child has been added on a smaller scale than current scale (@optimization)
if (node.formation_scale < this.scale || force_expand == true) {
if (parentNode.formationScale < this.scale || forceExpand == true) {
// we will check if any of the contained child nodes should be removed from the cluster
var largest_cluster = 1;
for (var contained_node_id in node.contained_nodes) {
if (node.contained_nodes.hasOwnProperty(contained_node_id)) {
var child_node = node.contained_nodes[contained_node_id];
var largestClusterSize = 1;
for (var containedNodeID in parentNode.containedNodes) {
if (parentNode.containedNodes.hasOwnProperty(containedNodeID)) {
var child_node = 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 (force_expand == true) {
if (largest_cluster < child_node.cluster_size) {
largest_cluster = child_node.cluster_size;
if (forceExpand == true) {
if (largestClusterSize < child_node.clusterSize) {
largestClusterSize = child_node.clusterSize;
}
}
else {
this._expelChildFromParent(node,contained_node_id,recursive,force_expand);
this._expelChildFromParent(parentNode,containedNodeID,recursive,forceExpand);
}
}
}
// we have determined the largest cluster size, we will now expel these
if (force_expand == true) {
for (var contained_node_id in node.contained_nodes) {
if (node.contained_nodes.hasOwnProperty(contained_node_id)) {
var child_node = node.contained_nodes[contained_node_id];
if (child_node.cluster_size == largest_cluster) {
this._expelChildFromParent(node,contained_node_id,recursive,force_expand);
if (forceExpand == true) {
for (var containedNodeID in parentNode.containedNodes) {
if (parentNode.containedNodes.hasOwnProperty(containedNodeID)) {
var child_node = parentNode.containedNodes[containedNodeID];
if (child_node.clusterSize == largestClusterSize) {
this._expelChildFromParent(parentNode,containedNodeID,recursive,forceExpand);
}
}
}
@ -348,45 +372,46 @@ Graph.prototype._expandClusterNode = function(node, recursive, force_expand) {
* the child node from the parent contained_node object and put it back into the global nodes object.
* The same holds for the edge that was connected to the child node. It is moved back into the global edges object.
*
* @param node | Node object: the parent node
* @param contained_node_id | String: child_node id as it is contained in the contained_nodes 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 force_expand | Boolean: This will disregard the zoom level and will expel this child from the parent
* @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(node, contained_node_id, recursive, force_expand) {
var child_node = node.contained_nodes[contained_node_id];
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 (child_node.formation_scale < this.scale || force_expand == true) {
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[contained_node_id] = child_node;
this.edges[node.contained_edges[contained_node_id]["edge_id"]] = node.contained_edges[contained_node_id]["edge_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
node.mass -= this.constants.clustering.massTransferCoefficient * child_node.mass;
node.width -= child_node.cluster_size * this.constants.clustering.widthGrowth;
node.height -= child_node.cluster_size * this.constants.clustering.heightGrowth;
node.fontSize -= this.constants.clustering.fontSizeMultiplier * child_node.cluster_size;
node.cluster_size -= child_node.cluster_size;
node.remaining_edges += 1;
parentNode.mass -= this.constants.clustering.massTransferCoefficient * childNode.mass;
parentNode.fontSize -= this.constants.clustering.fontSizeMultiplier * childNode.clusterSize;
parentNode.clusterSize -= childNode.clusterSize;
parentNode.remainingEdges += 1;
// place the child node near the parent, not at the exact same location to avoid chaos in the system
child_node.x = node.x + Math.floor(Math.random() * node.width);
child_node.y = node.y + Math.floor(Math.random() * node.height);
childNode.x = parentNode.x;
childNode.y = parentNode.y;
// remove node from the list
delete node.contained_nodes[contained_node_id];
delete node.contained_edges[contained_node_id];
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(child_node,recursive,force_expand);
this._expandClusterNode(childNode,recursive,forceExpand);
}
};
@ -401,62 +426,61 @@ Graph.prototype._expelChildFromParent = function(node, contained_node_id, recurs
* @private
* @param force_level_collapse | Boolean
*/
Graph.prototype._formClusters = function(force_level_collapse) {
Graph.prototype._formClusters = function(forceLevelCollapse) {
var min_length = this.constants.clustering.clusterLength/this.scale;
var dx,dy,length,
edges = this.edges;
// create an array of edge ids
var edges_id_array = []
var edgesIDarray = []
for (var id in edges) {
if (edges.hasOwnProperty(id)) {
edges_id_array.push(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 < edges_id_array.length; i++) {
var edge_id = edges_id_array[i];
var edge = edges[edge_id];
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 || force_level_collapse == true) {
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) {
var parent_node = edge.to
var child_node = edge.from
}
else {
var parent_node = edge.from
var child_node = edge.to
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 (child_node.remaining_edges == 1) {
this._addToCluster(parent_node,child_node,edge,edge_id,force_level_collapse);
delete this.edges[edges_id_array[i]];
if (childNode.remainingEdges == 1) {
this._addToCluster(parentNode,childNode,edge,forceLevelCollapse);
delete this.edges[edgesIDarray[i]];
}
else if (parent_node.remaining_edges == 1) {
this._addToCluster(child_node,parent_node,edge,edge_id,force_level_collapse);
delete this.edges[edges_id_array[i]];
else if (parentNode.remainingEdges == 1) {
this._addToCluster(childNode,parentNode,edge,forceLevelCollapse);
delete this.edges[edgesIDarray[i]];
}
}
}
}
this._updateNodeIndexList();
if (force_level_collapse == true)
if (forceLevelCollapse == true)
this._applyClusterLevel();
};
@ -468,31 +492,31 @@ Graph.prototype._formClusters = function(force_level_collapse) {
* @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 remaining_edges at the very end of the clustering, ensuring single level collapse
* @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(parent_node, child_node, edge, edge_id, force_level_collapse) {
Graph.prototype._addToCluster = function(parentNode, childNode, edge, forceLevelCollapse) {
// join child node and edge in parent node
parent_node.contained_nodes[child_node.id] = child_node;
parent_node.contained_edges[child_node.id] = {"edge_id":edge_id, "edge_object":edge}; // the edge gets the node ID so we can easily recover it when expanding the cluster
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(child_node.id)) {
delete this.nodes[child_node.id];
if (this.nodes.hasOwnProperty(childNode.id)) {
delete this.nodes[childNode.id];
}
//var grow_coefficient = (parent_node.cluster_size + child_node.cluster_size) / parent_node.cluster_size;
parent_node.mass += this.constants.clustering.massTransferCoefficient * child_node.mass;
parent_node.width += child_node.cluster_size * this.constants.clustering.widthGrowth;
parent_node.height += child_node.cluster_size * this.constants.clustering.heightGrowth;
parent_node.cluster_size += child_node.cluster_size;
parent_node.fontSize += this.constants.clustering.fontSizeMultiplier * child_node.cluster_size;
parent_node.formation_scale = this.scale; // The latest child has been added on this scale
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
parent_node.contained_nodes[child_node.id].formation_scale = this.scale; // this child has been added at this scale.
if (force_level_collapse == true)
parent_node.remaining_edges_unapplied -= 1;
// 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
parent_node.remaining_edges -= 1;
parentNode.remainingEdges -= 1;
// restart the simulation to reorganise all nodes
this.moving = true;
@ -500,29 +524,29 @@ Graph.prototype._addToCluster = function(parent_node, child_node, edge, edge_id,
/**
* This function will apply the changes made to the remaining_edges during the formation of the clusters.
* 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.node_indices.length; i++) {
var node = this.nodes[this.node_indices[i]];
node.remaining_edges = node.remaining_edges_unapplied;
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
node.remainingEdges = node.remainingEdges_unapplied;
}
};
/**
* Update the this.node_indices with the most recent node index list
* Update the this.nodeIndices with the most recent node index list
* @private
*/
Graph.prototype._updateNodeIndexList = function() {
this.node_indices = [];
this.nodeIndices = [];
for (var idx in this.nodes) {
if (this.nodes.hasOwnProperty(idx)) {
this.node_indices.push(idx);
this.nodeIndices.push(idx);
}
}
};
@ -706,6 +730,7 @@ Graph.prototype._create = function () {
this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
// add the frame to the container element
this.containerElement.appendChild(this.frame);
};
@ -1953,17 +1978,17 @@ 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.node_indices.length-1; i++) {
var node1 = nodes[this.node_indices[i]];
for (var j = i+1; j < this.node_indices.length; j++) {
var node2 = nodes[this.node_indices[j]];
var cluster_size = (node1.cluster_size + node2.cluster_size - 2);
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);
dx = node2.x - node1.x;
dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy);
minimumDistance = (cluster_size == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + cluster_size * this.constants.clustering.distanceAmplification));
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);
@ -1982,7 +2007,7 @@ Graph.prototype._calculateForces = function() {
}
// amplify the repulsion for clusters.
repulsingForce *= (cluster_size == 0) ? 1 : 1 + cluster_size * this.constants.clustering.forceAmplification;
repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification;
fx = Math.cos(angle) * repulsingForce;
fy = Math.sin(angle) * repulsingForce;
@ -2031,7 +2056,7 @@ Graph.prototype._calculateForces = function() {
//edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2;
edgeLength = edge.length;
// this implies that the edges between big clusters are longer
edgeLength += (edge.to.cluster_size + edge.from.cluster_size - 2) * this.constants.clustering.edgeGrowth;
edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
length = Math.sqrt(dx * dx + dy * dy);
angle = Math.atan2(dy, dx);
@ -2045,7 +2070,7 @@ Graph.prototype._calculateForces = function() {
}
}
}
/*
// TODO: re-implement repulsion of edges
// repulsing forces between edges
@ -2082,7 +2107,7 @@ Graph.prototype._calculateForces = function() {
edges[l2].to._addForce(fx, fy);
}
}
*/
};

+ 67
- 26
src/graph/Node.js View File

@ -55,8 +55,12 @@ function Node(properties, imagelist, grouplist, constants) {
// creating the variables for clustering
this.resetCluster();
this.remaining_edges = 0;
this.remaining_edges_unapplied = 0;
this.remainingEdges = 0;
this.remainingEdges_unapplied = 0;
this.clusterSizeWidthFactor = constants.clustering.clusterSizeWidthFactor;
this.clusterSizeHeightFactor = constants.clustering.clusterSizeHeightFactor;
this.clusterSizeRadiusFactor = constants.clustering.clusterSizeRadiusFactor;
// mass, force, velocity
this.mass = 50; // kg (mass is adjusted for the number of connected edges)
@ -73,10 +77,10 @@ function Node(properties, imagelist, grouplist, constants) {
*/
Node.prototype.resetCluster = function() {
// clustering variables
this.formation_scale = undefined; // this is used to determine when to open the cluster
this.cluster_size = 1; // this signifies the total amount of nodes in this cluster
this.contained_nodes = {};
this.contained_edges = {};
this.formationScale = undefined; // this is used to determine when to open the cluster
this.clusterSize = 1; // this signifies the total amount of nodes in this cluster
this.containedNodes = {};
this.containedEdges = {};
};
/**
@ -87,8 +91,8 @@ Node.prototype.attachEdge = function(edge) {
if (this.edges.indexOf(edge) == -1) {
this.edges.push(edge);
}
this.remaining_edges = this.edges.length;
this.remaining_edges_unapplied = this.edges.length;
this.remainingEdges = this.edges.length;
this.remainingEdges_unapplied = this.edges.length;
this._updateMass();
};
@ -101,8 +105,8 @@ Node.prototype.detachEdge = function(edge) {
if (index != -1) {
this.edges.splice(index, 1);
}
this.remaining_edges = this.edges.length;
this.remaining_edges_unapplied = this.edges.length;
this.remainingEdges = this.edges.length;
this.remainingEdges_unapplied = this.edges.length;
this._updateMass();
};
@ -276,8 +280,7 @@ Node.parseColor = function(color) {
*/
Node.prototype.select = function() {
this.selected = true;
// why do this?
// this._reset();
this._reset();
};
/**
@ -285,8 +288,15 @@ Node.prototype.select = function() {
*/
Node.prototype.unselect = function() {
this.selected = false;
// why do this?
// this._reset();
this._reset();
};
/**
* Reset the calculated size of the node, forces it to recalculate its size
*/
Node.prototype.clearSizeCache = function() {
this._reset();
};
/**
@ -510,6 +520,10 @@ Node.prototype._resizeImage = function (ctx) {
}
this.width = width;
this.height = height;
this.width += this.clusterSize * this.clusterSizeWidthFactor;
this.height += this.clusterSize * this.clusterSizeHeightFactor;
this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
}
};
@ -539,6 +553,10 @@ Node.prototype._resizeBox = function (ctx) {
var textSize = this.getTextSize(ctx);
this.width = textSize.width + 2 * margin;
this.height = textSize.height + 2 * margin;
this.width += this.clusterSize * this.clusterSizeWidthFactor;
this.height += this.clusterSize * this.clusterSizeHeightFactor;
this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
}
};
@ -548,7 +566,7 @@ Node.prototype._drawBox = function (ctx) {
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
if (this.cluster_size > 1) {
if (this.clusterSize > 1) {
ctx.strokeStyle = this.selected ? this.color.cluster.highlight.border : this.color.cluster.border;
ctx.fillStyle = this.selected ? this.color.cluster.highlight.background : this.color.cluster.background;
}
@ -556,7 +574,7 @@ Node.prototype._drawBox = function (ctx) {
ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
}
ctx.lineWidth = (this.selected ? 2.0 : 1.0) + (this.cluster_size > 1) ? 2.0 : 0.0;
ctx.lineWidth = (this.selected ? 2.0 : 1.0) + (this.clusterSize > 1) ? 2.0 : 0.0;
ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
ctx.fill();
ctx.stroke();
@ -572,6 +590,11 @@ Node.prototype._resizeDatabase = function (ctx) {
var size = textSize.width + 2 * margin;
this.width = size;
this.height = size;
// scaling used for clustering
this.width += this.clusterSize * this.clusterSizeWidthFactor;
this.height += this.clusterSize * this.clusterSizeHeightFactor;
this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
}
};
@ -580,7 +603,7 @@ Node.prototype._drawDatabase = function (ctx) {
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
if (this.cluster_size > 1) {
if (this.clusterSize > 1) {
ctx.strokeStyle = this.selected ? this.color.cluster.highlight.border : this.color.cluster.border;
ctx.fillStyle = this.selected ? this.color.cluster.highlight.background : this.color.cluster.background;
}
@ -588,7 +611,7 @@ Node.prototype._drawDatabase = function (ctx) {
ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
}
ctx.lineWidth = (this.selected ? 2.0 : 1.0) + (this.cluster_size > 1) ? 2.0 : 0.0;
ctx.lineWidth = (this.selected ? 2.0 : 1.0) + (this.clusterSize > 1) ? 2.0 : 0.0;
ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
ctx.fill();
ctx.stroke();
@ -606,6 +629,11 @@ Node.prototype._resizeCircle = function (ctx) {
this.width = diameter;
this.height = diameter;
// scaling used for clustering
this.width += this.clusterSize * this.clusterSizeWidthFactor;
this.height += this.clusterSize * this.clusterSizeHeightFactor;
this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
}
};
@ -614,7 +642,7 @@ Node.prototype._drawCircle = function (ctx) {
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
if (this.cluster_size > 1) {
if (this.clusterSize > 1) {
ctx.strokeStyle = this.selected ? this.color.cluster.highlight.border : this.color.cluster.border;
ctx.fillStyle = this.selected ? this.color.cluster.highlight.background : this.color.cluster.background;
}
@ -622,7 +650,7 @@ Node.prototype._drawCircle = function (ctx) {
ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
}
ctx.lineWidth = (this.selected ? 2.0 : 1.0) + (this.cluster_size > 1) ? 2.0 : 0.0;
ctx.lineWidth = (this.selected ? 2.0 : 1.0) + (this.clusterSize > 1) ? 2.0 : 0.0;
ctx.circle(this.x, this.y, this.radius);
ctx.fill();
ctx.stroke();
@ -639,6 +667,11 @@ Node.prototype._resizeEllipse = function (ctx) {
if (this.width < this.height) {
this.width = this.height;
}
// scaling used for clustering
this.width += this.clusterSize * this.clusterSizeWidthFactor;
this.height += this.clusterSize * this.clusterSizeHeightFactor;
this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
}
};
@ -647,7 +680,7 @@ Node.prototype._drawEllipse = function (ctx) {
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
if (this.cluster_size > 1) {
if (this.clusterSize > 1) {
ctx.strokeStyle = this.selected ? this.color.cluster.highlight.border : this.color.cluster.border;
ctx.fillStyle = this.selected ? this.color.cluster.highlight.background : this.color.cluster.background;
}
@ -655,11 +688,10 @@ Node.prototype._drawEllipse = function (ctx) {
ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
}
ctx.lineWidth = (this.selected ? 2.0 : 1.0) + (this.cluster_size > 1) ? 2.0 : 0.0;
ctx.lineWidth = (this.selected ? 2.0 : 1.0) + (this.clusterSize > 1) ? 2.0 : 0.0;
ctx.ellipse(this.left, this.top, this.width, this.height);
ctx.fill();
ctx.stroke();
this._label(ctx, this.label, this.x, this.y);
};
@ -688,6 +720,11 @@ Node.prototype._resizeShape = function (ctx) {
var size = 2 * this.radius;
this.width = size;
this.height = size;
// scaling used for clustering
this.width += this.clusterSize * this.clusterSizeWidthFactor;
this.height += this.clusterSize * this.clusterSizeHeightFactor;
this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
}
};
@ -696,8 +733,7 @@ Node.prototype._drawShape = function (ctx, shape) {
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
if (this.cluster_size > 1) {
if (this.clusterSize > 1) {
ctx.strokeStyle = this.selected ? this.color.cluster.highlight.border : this.color.cluster.border;
ctx.fillStyle = this.selected ? this.color.cluster.highlight.background : this.color.cluster.background;
}
@ -705,7 +741,7 @@ Node.prototype._drawShape = function (ctx, shape) {
ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
}
ctx.lineWidth = (this.selected ? 2.0 : 1.0) + (this.cluster_size > 1) ? 2.0 : 0.0;
ctx.lineWidth = (this.selected ? 2.0 : 1.0) + (this.clusterSize > 1) ? 2.0 : 0.0;
ctx[shape](this.x, this.y, this.radius);
ctx.fill();
@ -722,6 +758,11 @@ Node.prototype._resizeText = function (ctx) {
var textSize = this.getTextSize(ctx);
this.width = textSize.width + 2 * margin;
this.height = textSize.height + 2 * margin;
// scaling used for clustering
this.width += this.clusterSize * this.clusterSizeWidthFactor;
this.height += this.clusterSize * this.clusterSizeHeightFactor;
this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
}
};

+ 3
- 0
src/module/imports.js View File

@ -4,6 +4,7 @@
// Try to load dependencies from the global window object.
// If not available there, load via require.
var moment = (typeof window !== 'undefined') && window['moment'] || require('moment');
var Hammer;
@ -16,3 +17,5 @@ else {
throw Error('hammer.js is only available in a browser, not in node.js.');
}
}

+ 224
- 155
vis.js View File

@ -3816,6 +3816,7 @@ else {
// Try to load dependencies from the global window object.
// If not available there, load via require.
var moment = (typeof window !== 'undefined') && window['moment'] || require('moment');
var Hammer;
@ -3830,6 +3831,8 @@ else {
}
// Internet Explorer 8 and older does not support Array.indexOf, so we define
// it here in that case.
// http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
@ -12502,8 +12505,12 @@ function Node(properties, imagelist, grouplist, constants) {
// creating the variables for clustering
this.resetCluster();
this.remaining_edges = 0;
this.remaining_edges_unapplied = 0;
this.remainingEdges = 0;
this.remainingEdges_unapplied = 0;
this.clusterSizeWidthFactor = constants.clustering.clusterSizeWidthFactor;
this.clusterSizeHeightFactor = constants.clustering.clusterSizeHeightFactor;
this.clusterSizeRadiusFactor = constants.clustering.clusterSizeRadiusFactor;
// mass, force, velocity
this.mass = 50; // kg (mass is adjusted for the number of connected edges)
@ -12520,10 +12527,10 @@ function Node(properties, imagelist, grouplist, constants) {
*/
Node.prototype.resetCluster = function() {
// clustering variables
this.formation_scale = undefined; // this is used to determine when to open the cluster
this.cluster_size = 1; // this signifies the total amount of nodes in this cluster
this.contained_nodes = {};
this.contained_edges = {};
this.formationScale = undefined; // this is used to determine when to open the cluster
this.clusterSize = 1; // this signifies the total amount of nodes in this cluster
this.containedNodes = {};
this.containedEdges = {};
};
/**
@ -12534,8 +12541,8 @@ Node.prototype.attachEdge = function(edge) {
if (this.edges.indexOf(edge) == -1) {
this.edges.push(edge);
}
this.remaining_edges = this.edges.length;
this.remaining_edges_unapplied = this.edges.length;
this.remainingEdges = this.edges.length;
this.remainingEdges_unapplied = this.edges.length;
this._updateMass();
};
@ -12548,8 +12555,8 @@ Node.prototype.detachEdge = function(edge) {
if (index != -1) {
this.edges.splice(index, 1);
}
this.remaining_edges = this.edges.length;
this.remaining_edges_unapplied = this.edges.length;
this.remainingEdges = this.edges.length;
this.remainingEdges_unapplied = this.edges.length;
this._updateMass();
};
@ -12723,8 +12730,7 @@ Node.parseColor = function(color) {
*/
Node.prototype.select = function() {
this.selected = true;
// why do this?
// this._reset();
this._reset();
};
/**
@ -12732,8 +12738,15 @@ Node.prototype.select = function() {
*/
Node.prototype.unselect = function() {
this.selected = false;
// why do this?
// this._reset();
this._reset();
};
/**
* Reset the calculated size of the node, forces it to recalculate its size
*/
Node.prototype.clearSizeCache = function() {
this._reset();
};
/**
@ -12957,6 +12970,10 @@ Node.prototype._resizeImage = function (ctx) {
}
this.width = width;
this.height = height;
this.width += this.clusterSize * this.clusterSizeWidthFactor;
this.height += this.clusterSize * this.clusterSizeHeightFactor;
this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
}
};
@ -12986,6 +13003,10 @@ Node.prototype._resizeBox = function (ctx) {
var textSize = this.getTextSize(ctx);
this.width = textSize.width + 2 * margin;
this.height = textSize.height + 2 * margin;
this.width += this.clusterSize * this.clusterSizeWidthFactor;
this.height += this.clusterSize * this.clusterSizeHeightFactor;
this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
}
};
@ -12995,7 +13016,7 @@ Node.prototype._drawBox = function (ctx) {
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
if (this.cluster_size > 1) {
if (this.clusterSize > 1) {
ctx.strokeStyle = this.selected ? this.color.cluster.highlight.border : this.color.cluster.border;
ctx.fillStyle = this.selected ? this.color.cluster.highlight.background : this.color.cluster.background;
}
@ -13003,7 +13024,7 @@ Node.prototype._drawBox = function (ctx) {
ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
}
ctx.lineWidth = (this.selected ? 2.0 : 1.0) + (this.cluster_size > 1) ? 2.0 : 0.0;
ctx.lineWidth = (this.selected ? 2.0 : 1.0) + (this.clusterSize > 1) ? 2.0 : 0.0;
ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
ctx.fill();
ctx.stroke();
@ -13019,6 +13040,11 @@ Node.prototype._resizeDatabase = function (ctx) {
var size = textSize.width + 2 * margin;
this.width = size;
this.height = size;
// scaling used for clustering
this.width += this.clusterSize * this.clusterSizeWidthFactor;
this.height += this.clusterSize * this.clusterSizeHeightFactor;
this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
}
};
@ -13027,7 +13053,7 @@ Node.prototype._drawDatabase = function (ctx) {
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
if (this.cluster_size > 1) {
if (this.clusterSize > 1) {
ctx.strokeStyle = this.selected ? this.color.cluster.highlight.border : this.color.cluster.border;
ctx.fillStyle = this.selected ? this.color.cluster.highlight.background : this.color.cluster.background;
}
@ -13035,7 +13061,7 @@ Node.prototype._drawDatabase = function (ctx) {
ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
}
ctx.lineWidth = (this.selected ? 2.0 : 1.0) + (this.cluster_size > 1) ? 2.0 : 0.0;
ctx.lineWidth = (this.selected ? 2.0 : 1.0) + (this.clusterSize > 1) ? 2.0 : 0.0;
ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
ctx.fill();
ctx.stroke();
@ -13053,6 +13079,11 @@ Node.prototype._resizeCircle = function (ctx) {
this.width = diameter;
this.height = diameter;
// scaling used for clustering
this.width += this.clusterSize * this.clusterSizeWidthFactor;
this.height += this.clusterSize * this.clusterSizeHeightFactor;
this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
}
};
@ -13061,7 +13092,7 @@ Node.prototype._drawCircle = function (ctx) {
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
if (this.cluster_size > 1) {
if (this.clusterSize > 1) {
ctx.strokeStyle = this.selected ? this.color.cluster.highlight.border : this.color.cluster.border;
ctx.fillStyle = this.selected ? this.color.cluster.highlight.background : this.color.cluster.background;
}
@ -13069,7 +13100,7 @@ Node.prototype._drawCircle = function (ctx) {
ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
}
ctx.lineWidth = (this.selected ? 2.0 : 1.0) + (this.cluster_size > 1) ? 2.0 : 0.0;
ctx.lineWidth = (this.selected ? 2.0 : 1.0) + (this.clusterSize > 1) ? 2.0 : 0.0;
ctx.circle(this.x, this.y, this.radius);
ctx.fill();
ctx.stroke();
@ -13086,6 +13117,11 @@ Node.prototype._resizeEllipse = function (ctx) {
if (this.width < this.height) {
this.width = this.height;
}
// scaling used for clustering
this.width += this.clusterSize * this.clusterSizeWidthFactor;
this.height += this.clusterSize * this.clusterSizeHeightFactor;
this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
}
};
@ -13094,7 +13130,7 @@ Node.prototype._drawEllipse = function (ctx) {
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
if (this.cluster_size > 1) {
if (this.clusterSize > 1) {
ctx.strokeStyle = this.selected ? this.color.cluster.highlight.border : this.color.cluster.border;
ctx.fillStyle = this.selected ? this.color.cluster.highlight.background : this.color.cluster.background;
}
@ -13102,11 +13138,10 @@ Node.prototype._drawEllipse = function (ctx) {
ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
}
ctx.lineWidth = (this.selected ? 2.0 : 1.0) + (this.cluster_size > 1) ? 2.0 : 0.0;
ctx.lineWidth = (this.selected ? 2.0 : 1.0) + (this.clusterSize > 1) ? 2.0 : 0.0;
ctx.ellipse(this.left, this.top, this.width, this.height);
ctx.fill();
ctx.stroke();
this._label(ctx, this.label, this.x, this.y);
};
@ -13135,6 +13170,11 @@ Node.prototype._resizeShape = function (ctx) {
var size = 2 * this.radius;
this.width = size;
this.height = size;
// scaling used for clustering
this.width += this.clusterSize * this.clusterSizeWidthFactor;
this.height += this.clusterSize * this.clusterSizeHeightFactor;
this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
}
};
@ -13143,8 +13183,7 @@ Node.prototype._drawShape = function (ctx, shape) {
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
if (this.cluster_size > 1) {
if (this.clusterSize > 1) {
ctx.strokeStyle = this.selected ? this.color.cluster.highlight.border : this.color.cluster.border;
ctx.fillStyle = this.selected ? this.color.cluster.highlight.background : this.color.cluster.background;
}
@ -13152,7 +13191,7 @@ Node.prototype._drawShape = function (ctx, shape) {
ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
}
ctx.lineWidth = (this.selected ? 2.0 : 1.0) + (this.cluster_size > 1) ? 2.0 : 0.0;
ctx.lineWidth = (this.selected ? 2.0 : 1.0) + (this.clusterSize > 1) ? 2.0 : 0.0;
ctx[shape](this.x, this.y, this.radius);
ctx.fill();
@ -13169,6 +13208,11 @@ Node.prototype._resizeText = function (ctx) {
var textSize = this.getTextSize(ctx);
this.width = textSize.width + 2 * margin;
this.height = textSize.height + 2 * margin;
// scaling used for clustering
this.width += this.clusterSize * this.clusterSizeWidthFactor;
this.height += this.clusterSize * this.clusterSizeHeightFactor;
this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
}
};
@ -14127,13 +14171,14 @@ function Graph (container, data, options) {
}
},
clustering: {
clusterLength: 50, // threshold length for 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 cluster_size between two nodes multiply this value (+1) with the repulsion force
distanceAmplification: 0.1, // amount of cluster_size between two nodes multiply this value (+1) with the repulsion force
edgeGrowth: 10, // amount of cluster_size connected to the edge is multiplied with this and added to edgeLength
widthGrowth: 10, // growth factor = ((parent_size + child_size) / parent_size) * widthGrowthFactor
heightGrowth: 10, // growth factor = ((parent_size + child_size) / parent_size) * heightGrowthFactor
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.1 // parent.mass += massTransferCoefficient * child.mass
},
minForce: 0.05,
@ -14142,11 +14187,11 @@ function Graph (container, data, options) {
};
var graph = this;
this.node_indices = []; // the node indices list is used to speed up the computation of the repulsion fields
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.previous_scale = this.scale; // this is used to check if the zoom operation is zooming in or out
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
@ -14205,15 +14250,37 @@ function Graph (container, data, 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;
if (numberOfNodes >= 100) {
this.increaseClusterLevel();
}
};
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;
}
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.collapseClusterLevel = function() {
Graph.prototype.increaseClusterLevel = function() {
this._formClusters(true);
};
@ -14222,10 +14289,10 @@ Graph.prototype.collapseClusterLevel = 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.expandClusterLevel = function() {
for (var i = 0; i < this.node_indices.length; i++) {
var node = this.nodes[this.node_indices[i]];
if (node.cluster_size > 1 && node.remaining_edges == 1) {
Graph.prototype.decreaseClusterLevel = function() {
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
if (node.clusterSize > 1 && node.remainingEdges == 1) {
this._expandClusterNode(node,false,true);
}
}
@ -14265,24 +14332,26 @@ Graph.prototype.openCluster = function(node) {
* @private
*/
Graph.prototype._updateClusters = function() {
var moving_before_clustering = this.moving;
var isMovingBeforeClustering = this.moving;
if (this.previous_scale > this.scale) { // zoom out
if (this.previousScale > this.scale) { // zoom out
this._formClusters(false);
}
else if (this.previous_scale < this.scale) { // zoom out
else if (this.previousScale < this.scale) { // zoom out
this._openClusters();
}
this._updateClusterLabels();
this._updateNodeLabels();
this.previous_scale = this.scale;
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 != moving_before_clustering) {
if (this.moving != isMovingBeforeClustering) {
this.start();
}
// console.log(this.scale);
};
/**
@ -14291,10 +14360,10 @@ Graph.prototype._updateClusters = function() {
*/
Graph.prototype._updateLabels = function() {
// update node labels
for (var node_id in this.nodes) {
if (this.nodes.hasOwnProperty(node_id)) {
var node = this.nodes[node_id];
node.label = String(node.remaining_edges).concat(":",String(node.cluster_size));
for (var nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) {
var node = this.nodes[nodeID];
node.label = String(node.remainingEdges).concat(":",String(node.clusterSize));
}
}
};
@ -14305,11 +14374,11 @@ Graph.prototype._updateLabels = function() {
*/
Graph.prototype._updateClusterLabels = function() {
// update node labels
for (var node_id in this.nodes) {
if (this.nodes.hasOwnProperty(node_id)) {
var node = this.nodes[node_id];
if (node.cluster_size > 1) {
node.label = "[".concat(String(node.cluster_size),"]");
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),"]");
}
}
}
@ -14321,9 +14390,9 @@ Graph.prototype._updateClusterLabels = function() {
*/
Graph.prototype._updateNodeLabels = function() {
// update node labels
for (var node_id in this.nodes) {
var node = this.nodes[node_id];
if (node.cluster_size == 1) {
for (var nodeID in this.nodes) {
var node = this.nodes[nodeID];
if (node.clusterSize == 1) {
node.label = String(node.id);
}
}
@ -14331,14 +14400,14 @@ Graph.prototype._updateNodeLabels = function() {
/**
* This function loops over all nodes in the node_indices list. For each node it checks if it is a cluster and if it
* 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() {
for (var i = 0; i < this.node_indices.length; i++) {
var node = this.nodes[this.node_indices[i]];
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
this._expandClusterNode(node,true,false);
}
@ -14351,44 +14420,43 @@ Graph.prototype._openClusters = function() {
* If the node contains child nodes, this function is recursively called on the child nodes as well.
* This recursive behaviour is optional and can be set by the recursive argument.
*
* @param node | Node object: to check for cluster and expand
* @param parentNode | Node object: to check for cluster and expand
* @param recursive | Boolean: enable or disable recursive calling
* @param force_expand | Boolean: enable or disable forcing the last node to join the cluster to be expelled
* @param forceExpand | Boolean: enable or disable forcing the last node to join the cluster to be expelled
* @private
*/
Graph.prototype._expandClusterNode = function(node, recursive, force_expand) {
Graph.prototype._expandClusterNode = function(parentNode, recursive, forceExpand) {
// first check if node is a cluster
if (node.cluster_size > 1) {
if (parentNode.clusterSize > 1) {
// if the last child has been added on a smaller scale than current scale (@optimization)
if (node.formation_scale < this.scale || force_expand == true) {
if (parentNode.formationScale < this.scale || forceExpand == true) {
// we will check if any of the contained child nodes should be removed from the cluster
var largest_cluster = 1;
for (var contained_node_id in node.contained_nodes) {
if (node.contained_nodes.hasOwnProperty(contained_node_id)) {
var child_node = node.contained_nodes[contained_node_id];
var largestClusterSize = 1;
for (var containedNodeID in parentNode.containedNodes) {
if (parentNode.containedNodes.hasOwnProperty(containedNodeID)) {
var child_node = 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 (force_expand == true) {
if (largest_cluster < child_node.cluster_size) {
largest_cluster = child_node.cluster_size;
if (forceExpand == true) {
if (largestClusterSize < child_node.clusterSize) {
largestClusterSize = child_node.clusterSize;
}
}
else {
this._expelChildFromParent(node,contained_node_id,recursive,force_expand);
this._expelChildFromParent(parentNode,containedNodeID,recursive,forceExpand);
}
}
}
// we have determined the largest cluster size, we will now expel these
if (force_expand == true) {
for (var contained_node_id in node.contained_nodes) {
if (node.contained_nodes.hasOwnProperty(contained_node_id)) {
var child_node = node.contained_nodes[contained_node_id];
if (child_node.cluster_size == largest_cluster) {
this._expelChildFromParent(node,contained_node_id,recursive,force_expand);
if (forceExpand == true) {
for (var containedNodeID in parentNode.containedNodes) {
if (parentNode.containedNodes.hasOwnProperty(containedNodeID)) {
var child_node = parentNode.containedNodes[containedNodeID];
if (child_node.clusterSize == largestClusterSize) {
this._expelChildFromParent(parentNode,containedNodeID,recursive,forceExpand);
}
}
}
@ -14403,45 +14471,46 @@ Graph.prototype._expandClusterNode = function(node, recursive, force_expand) {
* the child node from the parent contained_node object and put it back into the global nodes object.
* The same holds for the edge that was connected to the child node. It is moved back into the global edges object.
*
* @param node | Node object: the parent node
* @param contained_node_id | String: child_node id as it is contained in the contained_nodes 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 force_expand | Boolean: This will disregard the zoom level and will expel this child from the parent
* @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(node, contained_node_id, recursive, force_expand) {
var child_node = node.contained_nodes[contained_node_id];
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 (child_node.formation_scale < this.scale || force_expand == true) {
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[contained_node_id] = child_node;
this.edges[node.contained_edges[contained_node_id]["edge_id"]] = node.contained_edges[contained_node_id]["edge_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
node.mass -= this.constants.clustering.massTransferCoefficient * child_node.mass;
node.width -= child_node.cluster_size * this.constants.clustering.widthGrowth;
node.height -= child_node.cluster_size * this.constants.clustering.heightGrowth;
node.fontSize -= this.constants.clustering.fontSizeMultiplier * child_node.cluster_size;
node.cluster_size -= child_node.cluster_size;
node.remaining_edges += 1;
parentNode.mass -= this.constants.clustering.massTransferCoefficient * childNode.mass;
parentNode.fontSize -= this.constants.clustering.fontSizeMultiplier * childNode.clusterSize;
parentNode.clusterSize -= childNode.clusterSize;
parentNode.remainingEdges += 1;
// place the child node near the parent, not at the exact same location to avoid chaos in the system
child_node.x = node.x + Math.floor(Math.random() * node.width);
child_node.y = node.y + Math.floor(Math.random() * node.height);
childNode.x = parentNode.x;
childNode.y = parentNode.y;
// remove node from the list
delete node.contained_nodes[contained_node_id];
delete node.contained_edges[contained_node_id];
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(child_node,recursive,force_expand);
this._expandClusterNode(childNode,recursive,forceExpand);
}
};
@ -14456,62 +14525,61 @@ Graph.prototype._expelChildFromParent = function(node, contained_node_id, recurs
* @private
* @param force_level_collapse | Boolean
*/
Graph.prototype._formClusters = function(force_level_collapse) {
Graph.prototype._formClusters = function(forceLevelCollapse) {
var min_length = this.constants.clustering.clusterLength/this.scale;
var dx,dy,length,
edges = this.edges;
// create an array of edge ids
var edges_id_array = []
var edgesIDarray = []
for (var id in edges) {
if (edges.hasOwnProperty(id)) {
edges_id_array.push(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 < edges_id_array.length; i++) {
var edge_id = edges_id_array[i];
var edge = edges[edge_id];
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 || force_level_collapse == true) {
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) {
var parent_node = edge.to
var child_node = edge.from
}
else {
var parent_node = edge.from
var child_node = edge.to
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 (child_node.remaining_edges == 1) {
this._addToCluster(parent_node,child_node,edge,edge_id,force_level_collapse);
delete this.edges[edges_id_array[i]];
if (childNode.remainingEdges == 1) {
this._addToCluster(parentNode,childNode,edge,forceLevelCollapse);
delete this.edges[edgesIDarray[i]];
}
else if (parent_node.remaining_edges == 1) {
this._addToCluster(child_node,parent_node,edge,edge_id,force_level_collapse);
delete this.edges[edges_id_array[i]];
else if (parentNode.remainingEdges == 1) {
this._addToCluster(childNode,parentNode,edge,forceLevelCollapse);
delete this.edges[edgesIDarray[i]];
}
}
}
}
this._updateNodeIndexList();
if (force_level_collapse == true)
if (forceLevelCollapse == true)
this._applyClusterLevel();
};
@ -14523,31 +14591,31 @@ Graph.prototype._formClusters = function(force_level_collapse) {
* @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 remaining_edges at the very end of the clustering, ensuring single level collapse
* @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(parent_node, child_node, edge, edge_id, force_level_collapse) {
Graph.prototype._addToCluster = function(parentNode, childNode, edge, forceLevelCollapse) {
// join child node and edge in parent node
parent_node.contained_nodes[child_node.id] = child_node;
parent_node.contained_edges[child_node.id] = {"edge_id":edge_id, "edge_object":edge}; // the edge gets the node ID so we can easily recover it when expanding the cluster
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(child_node.id)) {
delete this.nodes[child_node.id];
if (this.nodes.hasOwnProperty(childNode.id)) {
delete this.nodes[childNode.id];
}
//var grow_coefficient = (parent_node.cluster_size + child_node.cluster_size) / parent_node.cluster_size;
parent_node.mass += this.constants.clustering.massTransferCoefficient * child_node.mass;
parent_node.width += child_node.cluster_size * this.constants.clustering.widthGrowth;
parent_node.height += child_node.cluster_size * this.constants.clustering.heightGrowth;
parent_node.cluster_size += child_node.cluster_size;
parent_node.fontSize += this.constants.clustering.fontSizeMultiplier * child_node.cluster_size;
parent_node.formation_scale = this.scale; // The latest child has been added on this scale
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();
parent_node.contained_nodes[child_node.id].formation_scale = this.scale; // this child has been added at this scale.
if (force_level_collapse == true)
parent_node.remaining_edges_unapplied -= 1;
parentNode.containedNodes[childNode.id].formationScale = this.scale; // this child has been added at this scale.
if (forceLevelCollapse == true)
parentNode.remainingEdges_unapplied -= 1;
else
parent_node.remaining_edges -= 1;
parentNode.remainingEdges -= 1;
// restart the simulation to reorganise all nodes
this.moving = true;
@ -14555,29 +14623,29 @@ Graph.prototype._addToCluster = function(parent_node, child_node, edge, edge_id,
/**
* This function will apply the changes made to the remaining_edges during the formation of the clusters.
* 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.node_indices.length; i++) {
var node = this.nodes[this.node_indices[i]];
node.remaining_edges = node.remaining_edges_unapplied;
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
node.remainingEdges = node.remainingEdges_unapplied;
}
};
/**
* Update the this.node_indices with the most recent node index list
* Update the this.nodeIndices with the most recent node index list
* @private
*/
Graph.prototype._updateNodeIndexList = function() {
this.node_indices = [];
this.nodeIndices = [];
for (var idx in this.nodes) {
if (this.nodes.hasOwnProperty(idx)) {
this.node_indices.push(idx);
this.nodeIndices.push(idx);
}
}
};
@ -14761,6 +14829,7 @@ Graph.prototype._create = function () {
this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
// add the frame to the container element
this.containerElement.appendChild(this.frame);
};
@ -16008,17 +16077,17 @@ 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.node_indices.length-1; i++) {
var node1 = nodes[this.node_indices[i]];
for (var j = i+1; j < this.node_indices.length; j++) {
var node2 = nodes[this.node_indices[j]];
var cluster_size = (node1.cluster_size + node2.cluster_size - 2);
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);
dx = node2.x - node1.x;
dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy);
minimumDistance = (cluster_size == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + cluster_size * this.constants.clustering.distanceAmplification));
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);
@ -16037,7 +16106,7 @@ Graph.prototype._calculateForces = function() {
}
// amplify the repulsion for clusters.
repulsingForce *= (cluster_size == 0) ? 1 : 1 + cluster_size * this.constants.clustering.forceAmplification;
repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification;
fx = Math.cos(angle) * repulsingForce;
fy = Math.sin(angle) * repulsingForce;
@ -16086,7 +16155,7 @@ Graph.prototype._calculateForces = function() {
//edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2;
edgeLength = edge.length;
// this implies that the edges between big clusters are longer
edgeLength += (edge.to.cluster_size + edge.from.cluster_size - 2) * this.constants.clustering.edgeGrowth;
edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
length = Math.sqrt(dx * dx + dy * dy);
angle = Math.atan2(dy, dx);
@ -16100,7 +16169,7 @@ Graph.prototype._calculateForces = function() {
}
}
}
/*
// TODO: re-implement repulsion of edges
// repulsing forces between edges
@ -16137,7 +16206,7 @@ Graph.prototype._calculateForces = function() {
edges[l2].to._addForce(fx, fy);
}
}
*/
};

Loading…
Cancel
Save