Browse Source

Implemented Clustering, still open TODOs

css_transitions
Alex de Mulder 11 years ago
parent
commit
b0994cdd89
4 changed files with 672 additions and 70 deletions
  1. +8
    -0
      examples/graph/02_random_nodes.html
  2. +227
    -17
      src/graph/Graph.js
  3. +104
    -17
      src/graph/Node.js
  4. +333
    -36
      vis.js

+ 8
- 0
examples/graph/02_random_nodes.html View File

@ -74,6 +74,7 @@
nodes: nodes, nodes: nodes,
edges: edges edges: edges
}; };
/*
var options = { var options = {
nodes: { nodes: {
shape: 'circle' shape: 'circle'
@ -83,6 +84,13 @@
}, },
stabilize: false stabilize: false
}; };
*/
var options = {
edges: {
length: 50
},
stabilize: false
};
graph = new vis.Graph(container, data, options); graph = new vis.Graph(container, data, options);
// add event listeners // add event listeners

+ 227
- 17
src/graph/Graph.js View File

@ -39,6 +39,14 @@ function Graph (container, data, options) {
highlight: { highlight: {
border: '#2B7CE9', border: '#2B7CE9',
background: '#D2E5FF' background: '#D2E5FF'
},
cluster: {
border: '#000000',
background: '#F0F0F0',
highlight: {
border: '#FF0FFF',
background: '#FF000F'
}
} }
}, },
borderColor: '#2B7CE9', borderColor: '#2B7CE9',
@ -63,6 +71,13 @@ function Graph (container, data, options) {
altLength: undefined altLength: undefined
} }
}, },
clustering: {
clusterLength: 30, // threshold length for clustering
zoomOffset: 0.1,
widthGrowth: 15, // growth factor = ((parent_size + child_size) / parent_size) * widthGrowthFactor
heightGrowth: 15, // growth factor = ((parent_size + child_size) / parent_size) * heightGrowthFactor
massTransferCoefficient: 0.1 // parent.mass += massTransferCoefficient * child.mass
},
minForce: 0.05, minForce: 0.05,
minVelocity: 0.02, // px/s minVelocity: 0.02, // px/s
maxIterations: 1000 // maximum number of iteration to stabilize maxIterations: 1000 // maximum number of iteration to stabilize
@ -72,6 +87,8 @@ function Graph (container, data, options) {
this.node_indices = []; // the node indices list is used to speed up the computation of the repulsion fields this.node_indices = []; // the node indices list is used to speed up the computation of the repulsion fields
this.nodes = {}; // object with Node objects this.nodes = {}; // object with Node objects
this.edges = {}; // object with Edge objects this.edges = {}; // object with Edge objects
this.scale = 1; // defining the global scale variable in the constructor
this.previous_scale = 1; // use this to check if the zoom operation is 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 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 nodes currently moving
// TODO: create a counter to keep track on the number of edges having values // TODO: create a counter to keep track on the number of edges having values
@ -130,7 +147,184 @@ function Graph (container, data, options) {
// draw data // draw data
this.setData(data); this.setData(data);
}
};
/**
* This function checks if the zoom action is in or out.
* If out, check if we can form clusters, if in, check if we can open clusters.
* This function is only called from _zoom()
*
* @private
*/
Graph.prototype._updateClusters = function() {
if (this.previous_scale > this.scale) { // zoom out
this._formClusters();
}
else if (this.previous_scale < this.scale) { // zoom out
this._openClusters();
}
this._updateNodeIndexList();
for (var nid in this.nodes) {
var node = this.nodes[nid];
node.label = String(node.remaining_edges);
}
this.previous_scale = this.scale;
};
/**
* This function loops over all nodes in the node_indices 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]];
this._expandClusterNode(node,true);
}
};
/**
* This function checks if a node has to be opened. This is done by checking the zoom level.
* If the node contains child nodes, this function is recursively called on the child nodes as well.
* This recursive behaviour is optional and can be set by the recursive argument.
*
* @param node | Node object: to check for cluster and expand
* @param recursive | Boolean: enable or disable recursive calling
* @private
*/
Graph.prototype._expandClusterNode = function(node, recursive) {
if (node.formation_scale != undefined) {
if (this.scale > node.formation_scale) {
for (var contained_node_id in node.contained_nodes) {
if (node.contained_nodes.hasOwnProperty(contained_node_id)) {
// put the child node back in the global nodes object
this.nodes[contained_node_id] = node.contained_nodes[contained_node_id];
var child_node = this.nodes[contained_node_id];
// remove mass from child node from parent node
node.mass -= this.constants.clustering.massTransferCoefficient * this.nodes[contained_node_id].mass;
// decrease the size again
node.cluster_size -= child_node.cluster_size;
var grow_coefficient = this.constants.clustering.zoomOffset + (node.cluster_size + child_node.cluster_size) / node.cluster_size;
node.width -= grow_coefficient * this.constants.clustering.widthGrowth;
node.height -= grow_coefficient * this.constants.clustering.heightGrowth;
node.fontSize -= 1 * child_node.cluster_size;
// check if a further expansion step is possible if recursivity is enabled
if (recursive == true) {
this._expandClusterNode(child_node,true);
}
}
}
// put the edges back in the global this.edges
for (var contained_edge_id in node.contained_edges) {
if (node.contained_edges.hasOwnProperty(contained_edge_id)) {
this.edges[contained_edge_id] = node.contained_edges[contained_edge_id];
node.remaining_edges += 1;
}
}
// reset the cluster settings of this node
node.resetCluster();
}
}
};
/**
* This function checks if any nodes at the end of their trees have edges below a threshold length
* This function is called only from _updateClusters()
*
* @private
*/
Graph.prototype._formClusters = function() {
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 = []
for (var id in edges) {
if (edges.hasOwnProperty(id)) {
edges_id_array.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 = edges[edges_id_array[i]];
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) {
// checking for clustering possiblities
// first check which node is larger
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
}
// 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);
delete this.edges[edges_id_array[i]];
}
else if (parent_node.remaining_edges == 1) {
this._addToCluster(child_node,parent_node,edge);
delete this.edges[edges_id_array[i]];
}
}
}
}
};
/**
* This function adds the childnode to the parentnode, creating a cluster if it is not already.
* This function is called only from _updateClusters()
*
* @param parent_node | Node object: this is the node that will house the child node
* @param child_node | Node object: this node will be deleted from the global this.nodes and stored in the parent node
* @param edge | Edge object: this edge will be deleted from the global this.edges and stored in the parent node
* @private
*/
Graph.prototype._addToCluster = function(parent_node, child_node, edge) {
// 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;
if (this.nodes.hasOwnProperty(child_node.id)) {
delete this.nodes[child_node.id];
}
var grow_coefficient = this.constants.clustering.zoomOffset + (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 += grow_coefficient * this.constants.clustering.widthGrowth;
parent_node.height += grow_coefficient * this.constants.clustering.heightGrowth;
parent_node.remaining_edges -= 1;
parent_node.formation_scale = this.scale;
parent_node.cluster_size += child_node.cluster_size;
parent_node.fontSize += 1 * child_node.cluster_size;
};
/** /**
* Update the this.node_indices with the most recent node index list * Update the this.node_indices with the most recent node index list
@ -144,7 +338,7 @@ Graph.prototype._updateNodeIndexList = function() {
this.node_indices.push(idx); this.node_indices.push(idx);
} }
} }
}
};
/** /**
* Set nodes and edges, and optionally options as well. * Set nodes and edges, and optionally options as well.
@ -580,6 +774,7 @@ Graph.prototype._zoom = function(scale, pointer) {
this._setScale(scale); this._setScale(scale);
this._setTranslation(tx, ty); this._setTranslation(tx, ty);
this._updateClusters();
this._redraw(); this._redraw();
return scale; return scale;
@ -1578,19 +1773,25 @@ Graph.prototype._calculateForces = function() {
dy = node2.y - node1.y; dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy); distance = Math.sqrt(dx * dx + dy * dy);
//if (distance < 10*minimumDistance) {
//
if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045
angle = Math.atan2(dy, dx); angle = Math.atan2(dy, dx);
// TODO: correct factor for repulsing force
//repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
//repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307
repulsingForce = 1.0;
}
else {
// TODO: correct factor for repulsing force
//repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
//repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
}
fx = Math.cos(angle) * repulsingForce; fx = Math.cos(angle) * repulsingForce;
fy = Math.sin(angle) * repulsingForce; fy = Math.sin(angle) * repulsingForce;
node1._addForce(-fx, -fy); node1._addForce(-fx, -fy);
node2._addForce(fx, fy); node2._addForce(fx, fy);
// }
}
} }
} }
@ -1645,7 +1846,8 @@ Graph.prototype._calculateForces = function() {
} }
} }
/* TODO: re-implement repulsion of edges
// TODO: re-implement repulsion of edges
// repulsing forces between edges // repulsing forces between edges
var minimumDistance = this.constants.edges.distance, var minimumDistance = this.constants.edges.distance,
steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
@ -1680,7 +1882,7 @@ Graph.prototype._calculateForces = function() {
edges[l2].to._addForce(fx, fy); edges[l2].to._addForce(fx, fy);
} }
} }
*/
}; };
@ -1721,16 +1923,9 @@ Graph.prototype._discreteStepNodes = function() {
*/ */
Graph.prototype.start = function() { Graph.prototype.start = function() {
if (this.moving) { if (this.moving) {
var start = window.performance.now();
this._calculateForces(); this._calculateForces();
this._discreteStepNodes(); this._discreteStepNodes();
var end = window.performance.now();
var time = end - start;
console.log('Execution time: ' + time);
var vmin = this.constants.minVelocity; var vmin = this.constants.minVelocity;
this.moving = this._isMoving(vmin); this.moving = this._isMoving(vmin);
} }
@ -1741,8 +1936,23 @@ Graph.prototype.start = function() {
var graph = this; var graph = this;
this.timer = window.setTimeout(function () { this.timer = window.setTimeout(function () {
graph.timer = undefined; graph.timer = undefined;
// benchmark the calculation
var start = window.performance.now();
graph.start(); graph.start();
// Optionally call this twice for faster convergence
// graph.start();
var end = window.performance.now();
var time = end - start;
// console.log('Simulation time: ' + time);
start = window.performance.now();
graph._redraw(); graph._redraw();
end = window.performance.now();
time = end - start;
// console.log('Drawing time: ' + time);
}, this.refreshRate); }, this.refreshRate);
} }
} }

+ 104
- 17
src/graph/Node.js View File

@ -25,6 +25,7 @@
function Node(properties, imagelist, grouplist, constants) { function Node(properties, imagelist, grouplist, constants) {
this.selected = false; this.selected = false;
this.edges = []; // all edges connected to this node this.edges = []; // all edges connected to this node
this.group = constants.nodes.group; this.group = constants.nodes.group;
@ -50,6 +51,10 @@ function Node(properties, imagelist, grouplist, constants) {
this.imagelist = imagelist; this.imagelist = imagelist;
this.grouplist = grouplist; this.grouplist = grouplist;
// creating the variables for clustering
this.resetCluster();
this.remaining_edges = 0;
this.setProperties(properties, constants); this.setProperties(properties, constants);
// mass, force, velocity // mass, force, velocity
@ -62,6 +67,17 @@ function Node(properties, imagelist, grouplist, constants) {
this.damping = 0.9; // damping factor this.damping = 0.9; // damping factor
}; };
/**
* (re)setting the clustering variables and objects
*/
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 = {};
};
/** /**
* Attach a edge to the node * Attach a edge to the node
* @param {Edge} edge * @param {Edge} edge
@ -70,6 +86,8 @@ Node.prototype.attachEdge = function(edge) {
if (this.edges.indexOf(edge) == -1) { if (this.edges.indexOf(edge) == -1) {
this.edges.push(edge); this.edges.push(edge);
} }
this.remaining_edges = this.edges.length;
this.remaining_edges_tmp = this.edges.length;
this._updateMass(); this._updateMass();
}; };
@ -82,6 +100,8 @@ Node.prototype.detachEdge = function(edge) {
if (index != -1) { if (index != -1) {
this.edges.splice(index, 1); this.edges.splice(index, 1);
} }
this.remaining_edges = this.edges.length;
this.remaining_edges_tmp = this.edges.length;
this._updateMass(); this._updateMass();
}; };
@ -192,6 +212,14 @@ Node.parseColor = function(color) {
highlight: { highlight: {
border: color, border: color,
background: color background: color
},
cluster: {
border: color,
background: color,
highlight: {
border: color,
background: color
}
} }
}; };
// TODO: automatically generate a nice highlight color // TODO: automatically generate a nice highlight color
@ -200,6 +228,7 @@ Node.parseColor = function(color) {
c = {}; c = {};
c.background = color.background || 'white'; c.background = color.background || 'white';
c.border = color.border || c.background; c.border = color.border || c.background;
if (util.isString(color.highlight)) { if (util.isString(color.highlight)) {
c.highlight = { c.highlight = {
border: color.highlight, border: color.highlight,
@ -211,6 +240,32 @@ Node.parseColor = function(color) {
c.highlight.background = color.highlight && color.highlight.background || c.background; c.highlight.background = color.highlight && color.highlight.background || c.background;
c.highlight.border = color.highlight && color.highlight.border || c.border; c.highlight.border = color.highlight && color.highlight.border || c.border;
} }
// check if cluster colorgroup has been defined
if (util.isString(color.cluster)) {
c.cluster = {
border: color.cluster,
background: color.cluster
}
}
else {
c.cluster = {};
c.cluster.background = color.cluster && color.cluster.background || c.background;
c.cluster.border = color.cluster && color.cluster.border || c.border;
}
// check if cluster highlight colorgroup has been defined
if (util.isString(color.cluster.highlight)) {
c.cluster.highlight = {
border: color.cluster.highlight,
background: color.cluster.highlight
}
}
else {
c.cluster.highlight = {};
c.cluster.highlight.background = color.cluster.highlight && color.cluster.highlight.background || c.background;
c.cluster.highlight.border = color.cluster.highlight && color.cluster.highlight.border || c.border;
}
} }
return c; return c;
}; };
@ -220,7 +275,8 @@ Node.parseColor = function(color) {
*/ */
Node.prototype.select = function() { Node.prototype.select = function() {
this.selected = true; this.selected = true;
this._reset();
// why do this?
// this._reset();
}; };
/** /**
@ -228,7 +284,8 @@ Node.prototype.select = function() {
*/ */
Node.prototype.unselect = function() { Node.prototype.unselect = function() {
this.selected = false; this.selected = false;
this._reset();
// why do this?
// this._reset();
}; };
/** /**
@ -490,9 +547,15 @@ Node.prototype._drawBox = function (ctx) {
this.left = this.x - this.width / 2; this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2; this.top = this.y - this.height / 2;
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;
if (this.cluster_size > 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;
}
else {
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.roundRect(this.left, this.top, this.width, this.height, this.radius); ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
ctx.fill(); ctx.fill();
ctx.stroke(); ctx.stroke();
@ -516,9 +579,15 @@ Node.prototype._drawDatabase = function (ctx) {
this.left = this.x - this.width / 2; this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2; this.top = this.y - this.height / 2;
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;
if (this.cluster_size > 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;
}
else {
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.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height); ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
ctx.fill(); ctx.fill();
ctx.stroke(); ctx.stroke();
@ -544,9 +613,15 @@ Node.prototype._drawCircle = function (ctx) {
this.left = this.x - this.width / 2; this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2; this.top = this.y - this.height / 2;
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;
if (this.cluster_size > 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;
}
else {
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.circle(this.x, this.y, this.radius); ctx.circle(this.x, this.y, this.radius);
ctx.fill(); ctx.fill();
ctx.stroke(); ctx.stroke();
@ -571,9 +646,15 @@ Node.prototype._drawEllipse = function (ctx) {
this.left = this.x - this.width / 2; this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2; this.top = this.y - this.height / 2;
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;
if (this.cluster_size > 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;
}
else {
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.ellipse(this.left, this.top, this.width, this.height); ctx.ellipse(this.left, this.top, this.width, this.height);
ctx.fill(); ctx.fill();
ctx.stroke(); ctx.stroke();
@ -615,9 +696,15 @@ Node.prototype._drawShape = function (ctx, shape) {
this.left = this.x - this.width / 2; this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2; this.top = this.y - this.height / 2;
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;
if (this.cluster_size > 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;
}
else {
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[shape](this.x, this.y, this.radius); ctx[shape](this.x, this.y, this.radius);
ctx.fill(); ctx.fill();

+ 333
- 36
vis.js View File

@ -4,8 +4,8 @@
* *
* A dynamic, browser-based visualization library. * A dynamic, browser-based visualization library.
* *
* @version @@version
* @date @@date
* @version 0.3.0-SNAPSHOT
* @date 2014-01-08
* *
* @license * @license
* Copyright (C) 2011-2013 Almende B.V, http://almende.com * Copyright (C) 2011-2013 Almende B.V, http://almende.com
@ -12472,6 +12472,7 @@ if (typeof CanvasRenderingContext2D !== 'undefined') {
function Node(properties, imagelist, grouplist, constants) { function Node(properties, imagelist, grouplist, constants) {
this.selected = false; this.selected = false;
this.edges = []; // all edges connected to this node this.edges = []; // all edges connected to this node
this.group = constants.nodes.group; this.group = constants.nodes.group;
@ -12497,6 +12498,10 @@ function Node(properties, imagelist, grouplist, constants) {
this.imagelist = imagelist; this.imagelist = imagelist;
this.grouplist = grouplist; this.grouplist = grouplist;
// creating the variables for clustering
this.resetCluster();
this.remaining_edges = 0;
this.setProperties(properties, constants); this.setProperties(properties, constants);
// mass, force, velocity // mass, force, velocity
@ -12509,6 +12514,17 @@ function Node(properties, imagelist, grouplist, constants) {
this.damping = 0.9; // damping factor this.damping = 0.9; // damping factor
}; };
/**
* (re)setting the clustering variables and objects
*/
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 = {};
};
/** /**
* Attach a edge to the node * Attach a edge to the node
* @param {Edge} edge * @param {Edge} edge
@ -12517,6 +12533,8 @@ Node.prototype.attachEdge = function(edge) {
if (this.edges.indexOf(edge) == -1) { if (this.edges.indexOf(edge) == -1) {
this.edges.push(edge); this.edges.push(edge);
} }
this.remaining_edges = this.edges.length;
this.remaining_edges_tmp = this.edges.length;
this._updateMass(); this._updateMass();
}; };
@ -12529,6 +12547,8 @@ Node.prototype.detachEdge = function(edge) {
if (index != -1) { if (index != -1) {
this.edges.splice(index, 1); this.edges.splice(index, 1);
} }
this.remaining_edges = this.edges.length;
this.remaining_edges_tmp = this.edges.length;
this._updateMass(); this._updateMass();
}; };
@ -12639,6 +12659,14 @@ Node.parseColor = function(color) {
highlight: { highlight: {
border: color, border: color,
background: color background: color
},
cluster: {
border: color,
background: color,
highlight: {
border: color,
background: color
}
} }
}; };
// TODO: automatically generate a nice highlight color // TODO: automatically generate a nice highlight color
@ -12647,6 +12675,7 @@ Node.parseColor = function(color) {
c = {}; c = {};
c.background = color.background || 'white'; c.background = color.background || 'white';
c.border = color.border || c.background; c.border = color.border || c.background;
if (util.isString(color.highlight)) { if (util.isString(color.highlight)) {
c.highlight = { c.highlight = {
border: color.highlight, border: color.highlight,
@ -12658,6 +12687,32 @@ Node.parseColor = function(color) {
c.highlight.background = color.highlight && color.highlight.background || c.background; c.highlight.background = color.highlight && color.highlight.background || c.background;
c.highlight.border = color.highlight && color.highlight.border || c.border; c.highlight.border = color.highlight && color.highlight.border || c.border;
} }
// check if cluster colorgroup has been defined
if (util.isString(color.cluster)) {
c.cluster = {
border: color.cluster,
background: color.cluster
}
}
else {
c.cluster = {};
c.cluster.background = color.cluster && color.cluster.background || c.background;
c.cluster.border = color.cluster && color.cluster.border || c.border;
}
// check if cluster highlight colorgroup has been defined
if (util.isString(color.cluster.highlight)) {
c.cluster.highlight = {
border: color.cluster.highlight,
background: color.cluster.highlight
}
}
else {
c.cluster.highlight = {};
c.cluster.highlight.background = color.cluster.highlight && color.cluster.highlight.background || c.background;
c.cluster.highlight.border = color.cluster.highlight && color.cluster.highlight.border || c.border;
}
} }
return c; return c;
}; };
@ -12667,7 +12722,8 @@ Node.parseColor = function(color) {
*/ */
Node.prototype.select = function() { Node.prototype.select = function() {
this.selected = true; this.selected = true;
this._reset();
// why do this?
// this._reset();
}; };
/** /**
@ -12675,7 +12731,8 @@ Node.prototype.select = function() {
*/ */
Node.prototype.unselect = function() { Node.prototype.unselect = function() {
this.selected = false; this.selected = false;
this._reset();
// why do this?
// this._reset();
}; };
/** /**
@ -12937,9 +12994,15 @@ Node.prototype._drawBox = function (ctx) {
this.left = this.x - this.width / 2; this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2; this.top = this.y - this.height / 2;
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;
if (this.cluster_size > 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;
}
else {
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.roundRect(this.left, this.top, this.width, this.height, this.radius); ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
ctx.fill(); ctx.fill();
ctx.stroke(); ctx.stroke();
@ -12963,9 +13026,15 @@ Node.prototype._drawDatabase = function (ctx) {
this.left = this.x - this.width / 2; this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2; this.top = this.y - this.height / 2;
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;
if (this.cluster_size > 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;
}
else {
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.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height); ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
ctx.fill(); ctx.fill();
ctx.stroke(); ctx.stroke();
@ -12991,9 +13060,15 @@ Node.prototype._drawCircle = function (ctx) {
this.left = this.x - this.width / 2; this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2; this.top = this.y - this.height / 2;
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;
if (this.cluster_size > 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;
}
else {
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.circle(this.x, this.y, this.radius); ctx.circle(this.x, this.y, this.radius);
ctx.fill(); ctx.fill();
ctx.stroke(); ctx.stroke();
@ -13018,9 +13093,15 @@ Node.prototype._drawEllipse = function (ctx) {
this.left = this.x - this.width / 2; this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2; this.top = this.y - this.height / 2;
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;
if (this.cluster_size > 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;
}
else {
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.ellipse(this.left, this.top, this.width, this.height); ctx.ellipse(this.left, this.top, this.width, this.height);
ctx.fill(); ctx.fill();
ctx.stroke(); ctx.stroke();
@ -13062,9 +13143,15 @@ Node.prototype._drawShape = function (ctx, shape) {
this.left = this.x - this.width / 2; this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2; this.top = this.y - this.height / 2;
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;
if (this.cluster_size > 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;
}
else {
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[shape](this.x, this.y, this.radius); ctx[shape](this.x, this.y, this.radius);
ctx.fill(); ctx.fill();
@ -14005,6 +14092,14 @@ function Graph (container, data, options) {
highlight: { highlight: {
border: '#2B7CE9', border: '#2B7CE9',
background: '#D2E5FF' background: '#D2E5FF'
},
cluster: {
border: '#000000',
background: '#F0F0F0',
highlight: {
border: '#FF0FFF',
background: '#FF000F'
}
} }
}, },
borderColor: '#2B7CE9', borderColor: '#2B7CE9',
@ -14029,6 +14124,13 @@ function Graph (container, data, options) {
altLength: undefined altLength: undefined
} }
}, },
clustering: {
clusterLength: 30, // threshold length for clustering
zoomOffset: 0.1,
widthGrowth: 15, // growth factor = ((parent_size + child_size) / parent_size) * widthGrowthFactor
heightGrowth: 15, // growth factor = ((parent_size + child_size) / parent_size) * heightGrowthFactor
massTransferCoefficient: 0.1 // parent.mass += massTransferCoefficient * child.mass
},
minForce: 0.05, minForce: 0.05,
minVelocity: 0.02, // px/s minVelocity: 0.02, // px/s
maxIterations: 1000 // maximum number of iteration to stabilize maxIterations: 1000 // maximum number of iteration to stabilize
@ -14038,6 +14140,8 @@ function Graph (container, data, options) {
this.node_indices = []; // the node indices list is used to speed up the computation of the repulsion fields this.node_indices = []; // the node indices list is used to speed up the computation of the repulsion fields
this.nodes = {}; // object with Node objects this.nodes = {}; // object with Node objects
this.edges = {}; // object with Edge objects this.edges = {}; // object with Edge objects
this.scale = 1; // defining the global scale variable in the constructor
this.previous_scale = 1; // use this to check if the zoom operation is 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 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 nodes currently moving
// TODO: create a counter to keep track on the number of edges having values // TODO: create a counter to keep track on the number of edges having values
@ -14096,7 +14200,184 @@ function Graph (container, data, options) {
// draw data // draw data
this.setData(data); this.setData(data);
}
};
/**
* This function checks if the zoom action is in or out.
* If out, check if we can form clusters, if in, check if we can open clusters.
* This function is only called from _zoom()
*
* @private
*/
Graph.prototype._updateClusters = function() {
if (this.previous_scale > this.scale) { // zoom out
this._formClusters();
}
else if (this.previous_scale < this.scale) { // zoom out
this._openClusters();
}
this._updateNodeIndexList();
for (var nid in this.nodes) {
var node = this.nodes[nid];
node.label = String(node.remaining_edges);
}
this.previous_scale = this.scale;
};
/**
* This function loops over all nodes in the node_indices 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]];
this._expandClusterNode(node,true);
}
};
/**
* This function checks if a node has to be opened. This is done by checking the zoom level.
* If the node contains child nodes, this function is recursively called on the child nodes as well.
* This recursive behaviour is optional and can be set by the recursive argument.
*
* @param node | Node object: to check for cluster and expand
* @param recursive | Boolean: enable or disable recursive calling
* @private
*/
Graph.prototype._expandClusterNode = function(node, recursive) {
if (node.formation_scale != undefined) {
if (this.scale > node.formation_scale) {
for (var contained_node_id in node.contained_nodes) {
if (node.contained_nodes.hasOwnProperty(contained_node_id)) {
// put the child node back in the global nodes object
this.nodes[contained_node_id] = node.contained_nodes[contained_node_id];
var child_node = this.nodes[contained_node_id];
// remove mass from child node from parent node
node.mass -= this.constants.clustering.massTransferCoefficient * this.nodes[contained_node_id].mass;
// decrease the size again
node.cluster_size -= child_node.cluster_size;
var grow_coefficient = this.constants.clustering.zoomOffset + (node.cluster_size + child_node.cluster_size) / node.cluster_size;
node.width -= grow_coefficient * this.constants.clustering.widthGrowth;
node.height -= grow_coefficient * this.constants.clustering.heightGrowth;
node.fontSize -= 1 * child_node.cluster_size;
// check if a further expansion step is possible if recursivity is enabled
if (recursive == true) {
this._expandClusterNode(child_node,true);
}
}
}
// put the edges back in the global this.edges
for (var contained_edge_id in node.contained_edges) {
if (node.contained_edges.hasOwnProperty(contained_edge_id)) {
this.edges[contained_edge_id] = node.contained_edges[contained_edge_id];
node.remaining_edges += 1;
}
}
// reset the cluster settings of this node
node.resetCluster();
}
}
};
/**
* This function checks if any nodes at the end of their trees have edges below a threshold length
* This function is called only from _updateClusters()
*
* @private
*/
Graph.prototype._formClusters = function() {
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 = []
for (var id in edges) {
if (edges.hasOwnProperty(id)) {
edges_id_array.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 = edges[edges_id_array[i]];
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) {
// checking for clustering possiblities
// first check which node is larger
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
}
// 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);
delete this.edges[edges_id_array[i]];
}
else if (parent_node.remaining_edges == 1) {
this._addToCluster(child_node,parent_node,edge);
delete this.edges[edges_id_array[i]];
}
}
}
}
};
/**
* This function adds the childnode to the parentnode, creating a cluster if it is not already.
* This function is called only from _updateClusters()
*
* @param parent_node | Node object: this is the node that will house the child node
* @param child_node | Node object: this node will be deleted from the global this.nodes and stored in the parent node
* @param edge | Edge object: this edge will be deleted from the global this.edges and stored in the parent node
* @private
*/
Graph.prototype._addToCluster = function(parent_node, child_node, edge) {
// 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;
if (this.nodes.hasOwnProperty(child_node.id)) {
delete this.nodes[child_node.id];
}
var grow_coefficient = this.constants.clustering.zoomOffset + (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 += grow_coefficient * this.constants.clustering.widthGrowth;
parent_node.height += grow_coefficient * this.constants.clustering.heightGrowth;
parent_node.remaining_edges -= 1;
parent_node.formation_scale = this.scale;
parent_node.cluster_size += child_node.cluster_size;
parent_node.fontSize += 1 * child_node.cluster_size;
};
/** /**
* Update the this.node_indices with the most recent node index list * Update the this.node_indices with the most recent node index list
@ -14110,7 +14391,7 @@ Graph.prototype._updateNodeIndexList = function() {
this.node_indices.push(idx); this.node_indices.push(idx);
} }
} }
}
};
/** /**
* Set nodes and edges, and optionally options as well. * Set nodes and edges, and optionally options as well.
@ -14546,6 +14827,7 @@ Graph.prototype._zoom = function(scale, pointer) {
this._setScale(scale); this._setScale(scale);
this._setTranslation(tx, ty); this._setTranslation(tx, ty);
this._updateClusters();
this._redraw(); this._redraw();
return scale; return scale;
@ -15544,19 +15826,25 @@ Graph.prototype._calculateForces = function() {
dy = node2.y - node1.y; dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy); distance = Math.sqrt(dx * dx + dy * dy);
//if (distance < 10*minimumDistance) {
//
if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045
angle = Math.atan2(dy, dx); angle = Math.atan2(dy, dx);
// TODO: correct factor for repulsing force
//repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
//repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307
repulsingForce = 1.0;
}
else {
// TODO: correct factor for repulsing force
//repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
//repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
}
fx = Math.cos(angle) * repulsingForce; fx = Math.cos(angle) * repulsingForce;
fy = Math.sin(angle) * repulsingForce; fy = Math.sin(angle) * repulsingForce;
node1._addForce(-fx, -fy); node1._addForce(-fx, -fy);
node2._addForce(fx, fy); node2._addForce(fx, fy);
// }
}
} }
} }
@ -15611,7 +15899,8 @@ Graph.prototype._calculateForces = function() {
} }
} }
/* TODO: re-implement repulsion of edges
// TODO: re-implement repulsion of edges
// repulsing forces between edges // repulsing forces between edges
var minimumDistance = this.constants.edges.distance, var minimumDistance = this.constants.edges.distance,
steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
@ -15646,7 +15935,7 @@ Graph.prototype._calculateForces = function() {
edges[l2].to._addForce(fx, fy); edges[l2].to._addForce(fx, fy);
} }
} }
*/
}; };
@ -15687,16 +15976,9 @@ Graph.prototype._discreteStepNodes = function() {
*/ */
Graph.prototype.start = function() { Graph.prototype.start = function() {
if (this.moving) { if (this.moving) {
var start = window.performance.now();
this._calculateForces(); this._calculateForces();
this._discreteStepNodes(); this._discreteStepNodes();
var end = window.performance.now();
var time = end - start;
console.log('Execution time: ' + time);
var vmin = this.constants.minVelocity; var vmin = this.constants.minVelocity;
this.moving = this._isMoving(vmin); this.moving = this._isMoving(vmin);
} }
@ -15707,8 +15989,23 @@ Graph.prototype.start = function() {
var graph = this; var graph = this;
this.timer = window.setTimeout(function () { this.timer = window.setTimeout(function () {
graph.timer = undefined; graph.timer = undefined;
// benchmark the calculation
var start = window.performance.now();
graph.start(); graph.start();
// Optionally call this twice for faster convergence
// graph.start();
var end = window.performance.now();
var time = end - start;
// console.log('Simulation time: ' + time);
start = window.performance.now();
graph._redraw(); graph._redraw();
end = window.performance.now();
time = end - start;
// console.log('Drawing time: ' + time);
}, this.refreshRate); }, this.refreshRate);
} }
} }

Loading…
Cancel
Save