|
@ -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); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|