From 782869463e4534ce50b26a5644fd34cb9e5874db Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Thu, 9 Jan 2014 18:11:27 +0100 Subject: [PATCH] clustering, looking for keypressevent --- package.json | 1 + src/graph/Graph.js | 283 +++++++++++++++++-------------- src/graph/Node.js | 93 ++++++++--- src/module/imports.js | 3 + vis.js | 379 +++++++++++++++++++++++++----------------- 5 files changed, 449 insertions(+), 310 deletions(-) diff --git a/package.json b/package.json index b8457222..cb8df2ae 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "browserify": "latest", "moment": "latest", "hammerjs": "1.0.5", + "shortcut": "latest", "node-watch": "latest" } } diff --git a/src/graph/Graph.js b/src/graph/Graph.js index be59e377..954df922 100644 --- a/src/graph/Graph.js +++ b/src/graph/Graph.js @@ -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); } } - +*/ }; diff --git a/src/graph/Node.js b/src/graph/Node.js index fd6b974b..8027555b 100644 --- a/src/graph/Node.js +++ b/src/graph/Node.js @@ -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; } }; diff --git a/src/module/imports.js b/src/module/imports.js index a466d2e9..74a7afd7 100644 --- a/src/module/imports.js +++ b/src/module/imports.js @@ -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.'); } } + + diff --git a/vis.js b/vis.js index 4aa693d9..48b3a5e9 100644 --- a/vis.js +++ b/vis.js @@ -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); } } - +*/ };