Browse Source

Bug fixes, changed clustering to mixin, added examples and documentation.

css_transitions
Alex de Mulder 10 years ago
parent
commit
ba1dbb468c
14 changed files with 2517 additions and 2169 deletions
  1. +3
    -3
      Jakefile.js
  2. +1010
    -970
      dist/vis.js
  3. +9
    -9
      dist/vis.min.js
  4. +142
    -2
      docs/graph.html
  5. +18
    -32
      examples/graph/18_fully_random_nodes_clustering.html
  6. +140
    -0
      examples/graph/19_scale_free_graph_clustering.html
  7. +2
    -0
      examples/graph/index.html
  8. +1021
    -0
      src/graph/ClusterMixin.js
  9. +24
    -24
      src/graph/Edge.js
  10. +71
    -61
      src/graph/Graph.js
  11. +1
    -1
      src/graph/Node.js
  12. +1
    -1
      src/graph/Popup.js
  13. +75
    -68
      src/graph/SectorsMixin.js
  14. +0
    -998
      src/graph/cluster.js

+ 3
- 3
Jakefile.js View File

@ -84,7 +84,7 @@ task('build', {async: true}, function () {
'./src/graph/Groups.js',
'./src/graph/Images.js',
'./src/graph/SectorsMixin.js',
'./src/graph/Cluster.js',
'./src/graph/ClusterMixin.js',
'./src/graph/Graph.js',
@ -109,7 +109,7 @@ task('build', {async: true}, function () {
// write bundled file
write(VIS, lib);
console.log('created ' + VIS);
console.log('created js' + VIS);
// remove temporary file
fs.unlinkSync(VIS_TMP);
@ -136,7 +136,7 @@ task('minify', function () {
// update version number and stuff in the javascript files
replacePlaceholders(VIS_MIN);
console.log('created ' + VIS_MIN);
console.log('created minified ' + VIS_MIN);
});
/**

+ 1010
- 970
dist/vis.js
File diff suppressed because it is too large
View File


+ 9
- 9
dist/vis.min.js
File diff suppressed because it is too large
View File


+ 142
- 2
docs/graph.html View File

@ -29,6 +29,7 @@
</ul>
</li>
<li><a href="#Configuration_Options">Configuration Options</a></li>
<li><a href="#Clustering">Clustering Options</a></li>
<li><a href="#Methods">Methods</a></li>
<li><a href="#Events">Events</a></li>
<li><a href="#Data_Policy">Data Policy</a></li>
@ -995,6 +996,143 @@ var nodes = [
</table>
<h2 id="Clustering">Clustering</h2>
<p>
The graph now supports dynamic clustering of nodes. This allows a user to view a very large dataset (> 50.000 nodes) without
sacrificing performance. When loading a large dataset, the nodes are clustered initially (this may take a small while) to have a
responsive visualization to work with. The clustering is both outside-in and inside-out. Outside-in means that nodes with only one
connection will be contained, or clustered, in the node it is connected to. Inside-out clustering first determines which nodes are hubs.
Hubs are defined as the nodes with the top 3% highest amount of connections (assuming normal distribution). These hubs then "grow", meaning
they contain the nodes they are connected to within themselves. The edges that were connected to the nodes that are absorbed will be reconnected to the cluster.
<br />
<br />
A cluster is just a node that has references to the nodes and edges it contains. It has an internal counter to keep track of its size, which is then used
to calculate the required forces. The contained nodes are removed from the global nodes index, greatly speeding up the system.
<br />
<br />
The clustering has the following user-configurable settings. The default values have been tested with the Graph examples and work well.
The default state for clustering is <b>off</b>.
</p>
<table>
<tr>
<th>Name</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
<tr>
<td>enabled</td>
<td>Boolean</td>
<td>false</td>
<td>On/off switch for clustering. It is assumed clustering is enabled in the descriptions below.</td>
</tr>
<tr>
<td>initialMaxNumberOfNodes</td>
<td>Number</td>
<td>100</td>
<td>If the initial amount of nodes is larger than this value, clustering starts until the total number of nodes is less than this value.</td>
</tr>
<tr>
<td>absoluteMaxNumberOfNodes</td>
<td>Number</td>
<td>500</td>
<td>While zooming in and out, clusters can open up. Once there are more than <code>absoluteMaxNumberOfNodes</code> nodes,
clustering starts until <code>reduceToMaxNumberOfNodes</code> nodes are left. This is done to ensure performance is continiously fluid.</td>
</tr>
<tr>
<td>reduceToMaxNumberOfNodes</td>
<td>Number</td>
<td>300</td>
<td>While zooming in and out, clusters can open up. Once there are more than <code>absoluteMaxNumberOfNodes</code> nodes,
clustering starts until <code>reduceToMaxNumberOfNodes</code> nodes are left. This is done to ensure performance is continiously fluid.</td>
</tr>
<tr>
<td>chainThreshold</td>
<td>Number</td>
<td>0.4</td>
<td>Because of the clustering methods used, long chains of nodes can be formed. To reduce these chains, this threshold is used.
A <code>chainThreshold</code> of 0.4 means that no more than 40% of all nodes are allowed to be a chain node (two connections).
If there are more, they are clustered together.</td>
</tr>
<tr>
<td>clusterEdgeThreshold</td>
<td>Number</td>
<td>20</td>
<td>This is the absolute edge length threshold in pixels. If the edge is smaller on screen (that means zooming out reduces this length)
the node will be clustered. This is triggered when zooming out.</td>
</tr>
<tr>
<td>sectorThreshold</td>
<td>Integer</td>
<td>50</td>
<td>If a cluster larger than <code>sectorThreshold</code> is opened, a seperate instance called a sector, will be created. All the simulation of
nodes outside of this instance will be paused. This is to maintain performance and clarity when examining large clusters.
A sector is collapsed when zooming out far enough. Also, when opening a cluster, if this cluster is smaller than this value, it is fully unpacked.</td>
</tr>
<tr>
<td>screenSizeThreshold</td>
<td>Number</td>
<td>0.2</td>
<td>When zooming in, the clusters become bigger. A <code>screenSizeThreshold</code> of 0.2 means that if the width or height of this cluster
becomes bigger than 20% of the width or height of the canvas, the cluster is opened. If a sector has been created, if the sector is smaller than
20%, we collapse this sector.</td>
</tr>
<tr>
<td>fontSizeMultiplier</td>
<td>Number</td>
<td>4.0</td>
<td>This parameter denotes the increase in fontSize of the cluster when a single node is added to it.</td>
</tr>
<tr>
<td>forceAmplification</td>
<td>Number</td>
<td>0.6</td>
<td>This factor is used to calculate the increase of the repulsive force of a cluster. It is calculated by the following
formula: <code>repulsingForce *= 1 + (clusterSize * forceAmplification)</code>.</td>
</tr>
<tr>
<td>distanceAmplification</td>
<td>Number</td>
<td>0.2</td>
<td>This factor is used to calculate the increase in effective range of the repulsive force of the cluster.
A larger cluster has a longer range. It is calculated by the following
formula: <code>minDistance *= 1 + (clusterSize * distanceAmplification)</code>.</td>
</tr>
<tr>
<td>edgeGrowth</td>
<td>Number</td>
<td>11</td>
<td>This factor determines the elongation of edges connected to a cluster.</td>
</tr>
<tr>
<td>clusterSizeWidthFactor</td>
<td>Number</td>
<td>10</td>
<td>This factor determines how much the width of a cluster increases in pixels per added node.</td>
</tr>
<tr>
<td>clusterSizeHeightFactor</td>
<td>Number</td>
<td>10</td>
<td>This factor determines how much the height of a cluster increases in pixels per added node.</td>
</tr>
<tr>
<td>clusterSizeRadiusFactor</td>
<td>Number</td>
<td>10</td>
<td>This factor determines how much the radius of a cluster increases in pixels per added node.</td>
</tr>
<tr>
<td>activeAreaBoxSize</td>
<td>Number</td>
<td>100</td>
<td>Imagine a square with an edge length of <code>activeAreaBoxSize</code> pixels around your cursor.
If a cluster is in this box as you zoom in, the cluster can be opened in a seperate sector.
This is regardless of the zoom level.</td>
</tr>
</table>
<h2 id="Methods">Methods</h2>
<p>
@ -1009,11 +1147,13 @@ var nodes = [
</tr>
<tr>
<td>setData(data)</td>
<td>setData(data,[disableStart])</td>
<td>none</td>
<td>Loads data. Parameter <code>data</code> is an object containing
nodes, edges, and options. Parameters nodes, edges are an Array.
Options is a name-value map and is optional.
Options is a name-value map and is optional. Parameter <code>disableStart</code> is
an optional Boolean and can disable the start of the simulation that would begin at the end
of this function by default.
</td>
</tr>

examples/graph/02.1_really_random_nodes.html → examples/graph/18_fully_random_nodes_clustering.html View File

@ -45,37 +45,8 @@
to: to
});
}
/*
// Loop:
for (var i = 0; i < 5; i++) {
nodes.push({
id: i,
label: String(i)
});
}
edges.push({
from: 1,
to: 0
});
edges.push({
from: 1,
to: 2
});
edges.push({
from: 4,
to: 0
});
edges.push({
from: 2,
to: 3
});
edges.push({
from: 3,
to: 4
});
*/
// create a graph
var clusteringOn = document.getElementById('clustering').checked;
var container = document.getElementById('mygraph');
var data = {
nodes: nodes,
@ -85,10 +56,12 @@
edges: {
length: 80
},
clustering: {
enabled: clusteringOn
},
stabilize: false
};
graph = new vis.Graph(container, data, options);
// add event listeners
vis.events.addListener(graph, 'select', function(params) {
document.getElementById('selection').innerHTML =
@ -99,10 +72,23 @@
</head>
<body onload="draw();">
<div style="width:800px; font-size:15px;">
This example shows a fully randomly generated set of nodes and connected edges.
By clicking the checkbox you can turn clustering on and off. If you increase the number of nodes to
a value higher than 100, automatic clustering is used before the initial draw (assuming the checkbox is checked).
<br />
<br />
Clustering is automatic when zooming out. When zooming in over the cluster, the cluster pops open. When the cluster is very big, a special instance
will be created and the cluster contents will only be simulated in there. Double click will also open a cluster.
<br />
<br />
Try values of 500 and 5000 with and without clustering. All thresholds can be changed to suit your dataset.
</div>
<form onsubmit="draw(); return false;">
<label for="nodeCount">Number of nodes:</label>
<input id="nodeCount" type="text" value="50" style="width: 50px;">
<label for="clustering">Enable Clustering:</label>
<input id="clustering" type="checkbox" onChange="draw()" checked="true">
<input type="submit" value="Go">
</form>
<br>

+ 140
- 0
examples/graph/19_scale_free_graph_clustering.html View File

@ -0,0 +1,140 @@
<!doctype html>
<html>
<head>
<title>Graph | Random nodes</title>
<style type="text/css">
body {
font: 10pt sans;
}
#mygraph {
width: 600px;
height: 600px;
border: 1px solid lightgray;
}
</style>
<script type="text/javascript" src="../../dist/vis.js"></script>
<script type="text/javascript">
var nodes = null;
var edges = null;
var graph = null;
function draw() {
nodes = [];
edges = [];
var connectionCount = [];
// randomly create some nodes and edges
var nodeCount = document.getElementById('nodeCount').value;
for (var i = 0; i < nodeCount; i++) {
nodes.push({
id: i,
label: String(i)
});
connectionCount[i] = 0;
// create edges in a scale-free-graph way
if (i == 1) {
var from = i;
var to = 0;
edges.push({
from: from,
to: to
});
connectionCount[from]++;
connectionCount[to]++;
}
else if (i > 1) {
var conn = edges.length * 2;
var rand = Math.floor(Math.random() * conn);
var cum = 0;
var j = 0;
while (j < connectionCount.length && cum < rand) {
cum += connectionCount[j];
j++;
}
var from = i;
var to = j;
edges.push({
from: from,
to: to
});
connectionCount[from]++;
connectionCount[to]++;
}
}
// create a graph
var clusteringOn = document.getElementById('clustering').checked;
var clusterEdgeThreshold = parseInt(document.getElementById('clusterEdgeThreshold').value);
var container = document.getElementById('mygraph');
var data = {
nodes: nodes,
edges: edges
};
/*
var options = {
nodes: {
shape: 'circle'
},
edges: {
length: 50
},
stabilize: false
};
*/
var options = {
edges: {
length: 50
},
clustering: {
enabled: clusteringOn,
clusterEdgeThreshold: clusterEdgeThreshold
},
stabilize: false
};
graph = new vis.Graph(container, data, options);
// add event listeners
vis.events.addListener(graph, 'select', function(params) {
document.getElementById('selection').innerHTML =
'Selection: ' + graph.getSelection();
});
}
</script>
</head>
<body onload="draw();">
<div style="width:800px; font-size:15px;">
This example shows a randomly generated <b>scale-free-graph</b> set of nodes and connected edges.
By clicking the checkbox you can turn clustering on and off. If you increase the number of nodes to
a value higher than 100, automatic clustering is used before the initial draw (assuming the checkbox is checked).
<br />
<br />
Clustering is automatic when zooming out. When zooming in over the cluster, the cluster pops open. When the cluster is very big, a special instance
will be created and the cluster contents will only be simulated in there. Double click will also open a cluster.
<br />
<br />
Try values of 500 and 5000 with and without clustering. All thresholds can be changed to suit your dataset.
Experiment with the clusterEdgeThreshold, which increases the formation of clusters when zoomed out (assuming the checkbox is checked).
</div>
<form onsubmit="draw(); return false;">
<label for="nodeCount">Number of nodes:</label>
<input id="nodeCount" type="text" value="125" style="width: 50px;">
<label for="clustering">Enable Clustering:</label>
<input id="clustering" type="checkbox" onChange="draw()" checked="true">
<label for="clusterEdgeThreshold">clusterEdgeThreshold:</label>
<input id="clusterEdgeThreshold" type="text" value="20" style="width: 50px;">
<input type="submit" value="Go">
</form>
<br>
<div id="mygraph"></div>
<p id="selection"></p>
</body>
</html>

+ 2
- 0
examples/graph/index.html View File

@ -29,6 +29,8 @@
<p><a href="15_dot_language_playground.html">15_dot_language_playground.html</a></p>
<p><a href="16_dynamic_data.html">16_dynamic_data.html</a></p>
<p><a href="17_network_info.html">17_network_info.html</a></p>
<p><a href="18_fully_random_nodes_clustering.html">18_fully_random_nodes_clustering.html</a></p>
<p><a href="19_scale_free_graph_clustering.html">19_scale_free_graph_clustering.html</a></p>
<p><a href="graphviz/graphviz_gallery.html">graphviz_gallery.html</a></p>
</div>

+ 1021
- 0
src/graph/ClusterMixin.js
File diff suppressed because it is too large
View File


+ 24
- 24
src/graph/Edge.js View File

@ -26,7 +26,7 @@ function Edge (properties, graph, constants) {
// initialize variables
this.id = undefined;
this.fromId = undefined;
this.toId = undefined;
this.toId = undefined;
this.style = constants.edges.style;
this.title = undefined;
this.width = constants.edges.width;
@ -38,8 +38,8 @@ function Edge (properties, graph, constants) {
// we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
// by storing the original information we can revert to the original connection when the cluser is opened.
this.originalFromID = [];
this.originalToID = [];
this.originalFromId = [];
this.originalToId = [];
this.connected = false;
@ -55,7 +55,7 @@ function Edge (properties, graph, constants) {
this.setProperties(properties, constants);
};
}
/**
* Set or overwrite properties for the edge
@ -67,41 +67,41 @@ Edge.prototype.setProperties = function(properties, constants) {
return;
}
if (properties.from != undefined) {this.fromId = properties.from;}
if (properties.to != undefined) {this.toId = properties.to;}
if (properties.from !== undefined) {this.fromId = properties.from;}
if (properties.to !== undefined) {this.toId = properties.to;}
if (properties.id != undefined) {this.id = properties.id;}
if (properties.style != undefined) {this.style = properties.style;}
if (properties.label != undefined) {this.label = properties.label;}
if (properties.id !== undefined) {this.id = properties.id;}
if (properties.style !== undefined) {this.style = properties.style;}
if (properties.label !== undefined) {this.label = properties.label;}
if (this.label) {
this.fontSize = constants.edges.fontSize;
this.fontFace = constants.edges.fontFace;
this.fontColor = constants.edges.fontColor;
if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;}
if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;}
if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;}
if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
}
if (properties.title != undefined) {this.title = properties.title;}
if (properties.width != undefined) {this.width = properties.width;}
if (properties.value != undefined) {this.value = properties.value;}
if (properties.length != undefined) {this.length = properties.length;}
if (properties.title !== undefined) {this.title = properties.title;}
if (properties.width !== undefined) {this.width = properties.width;}
if (properties.value !== undefined) {this.value = properties.value;}
if (properties.length !== undefined) {this.length = properties.length;}
// Added to support dashed lines
// David Jordan
// 2012-08-08
if (properties.dash) {
if (properties.dash.length != undefined) {this.dash.length = properties.dash.length;}
if (properties.dash.gap != undefined) {this.dash.gap = properties.dash.gap;}
if (properties.dash.altLength != undefined) {this.dash.altLength = properties.dash.altLength;}
if (properties.dash.length !== undefined) {this.dash.length = properties.dash.length;}
if (properties.dash.gap !== undefined) {this.dash.gap = properties.dash.gap;}
if (properties.dash.altLength !== undefined) {this.dash.altLength = properties.dash.altLength;}
}
if (properties.color != undefined) {this.color = properties.color;}
if (properties.color !== undefined) {this.color = properties.color;}
// A node is connected when it has a from and to node.
this.connect();
this.widthFixed = this.widthFixed || (properties.width != undefined);
this.lengthFixed = this.lengthFixed || (properties.length != undefined);
this.widthFixed = this.widthFixed || (properties.width !== undefined);
this.lengthFixed = this.lengthFixed || (properties.length !== undefined);
this.stiffness = 1 / this.length;
// set draw method based on style
@ -350,12 +350,12 @@ Edge.prototype._drawDashLine = function(ctx) {
// draw dashed line
ctx.beginPath();
ctx.lineCap = 'round';
if (this.dash.altLength != undefined) //If an alt dash value has been set add to the array this value
if (this.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
{
ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
[this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
}
else if (this.dash.length != undefined && this.dash.gap != undefined) //If a dash and gap value has been set add to the array this value
else if (this.dash.length !== undefined && this.dash.gap !== undefined) //If a dash and gap value has been set add to the array this value
{
ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
[this.dash.length,this.dash.gap]);

+ 71
- 61
src/graph/Graph.js View File

@ -63,22 +63,24 @@ function Graph (container, data, options) {
altLength: undefined
}
},
clustering: {
enableClustering: false, // global on/off switch for clustering.
maxNumberOfNodes: 100, // for automatic (initial) clustering
snakeThreshold: 0.7, // maximum percentage of allowed snakenodes (long strings of connected nodes) within all nodes
clusterEdgeThreshold: 15, // edge length threshold. if smaller, this node is clustered
sectorThreshold: 50, // cluster size threshold. If larger, expanding in own sector.
screenSizeThreshold: 0.2, // relative size threshold. If the width or height of a clusternode takes up this much of the screen, decluster node
fontSizeMultiplier: 4, // how much the cluster font size grows per node in cluster (in px)
forceAmplification: 0.6, // factor of increase fo the repulsion force of a cluster (per node in cluster)
distanceAmplification: 0.2, // factor how much the repulsion distance of a cluster increases (per node in cluster).
edgeGrowth: 11, // amount of clusterSize connected to the edge is multiplied with this and added to edgeLength
clusterSizeWidthFactor: 10, // growth of the width per node in cluster
clusterSizeHeightFactor: 10, // growth of the height per node in cluster
clusterSizeRadiusFactor: 10, // growth of the radius per node in cluster
activeAreaBoxSize: 100, // box area around the curser where clusters are popped open
massTransferCoefficient: 1 // parent.mass += massTransferCoefficient * child.mass
clustering: { // Per Node in Cluster = PNiC
enabled: false, // (Boolean) | global on/off switch for clustering.
initialMaxNumberOfNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold.
absoluteMaxNumberOfNodes:500, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than this. If it is, cluster until reduced to reduceToMaxNumberOfNodes
reduceToMaxNumberOfNodes:300, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than absoluteMaxNumberOfNodes. If it is, cluster until reduced to this
chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains).
clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered.
sectorThreshold: 50, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector.
screenSizeThreshold: 0.2, // (% of canvas) | relative size threshold. If the width or height of a clusternode takes up this much of the screen, decluster node.
fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px).
forceAmplification: 0.6, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
distanceAmplification: 0.2, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
edgeGrowth: 11, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
clusterSizeWidthFactor: 10, // (px PNiC) | growth of the width per node in cluster.
clusterSizeHeightFactor: 10, // (px PNiC) | growth of the height per node in cluster.
clusterSizeRadiusFactor: 10, // (px PNiC) | growth of the radius per node in cluster.
activeAreaBoxSize: 100, // (px) | box area around the curser where clusters are popped open.
massTransferCoefficient: 1 // (multiplier) | parent.mass += massTransferCoefficient * child.mass
},
minForce: 0.05,
minVelocity: 0.02, // px/s
@ -86,7 +88,7 @@ function Graph (container, data, options) {
};
// call the constructor of the cluster object
Cluster.call(this);
this._loadClusterSystem();
// call the sector constructor
this._loadSectorSystem(); // would be fantastic if multiple in heritance just worked!
@ -94,6 +96,7 @@ function Graph (container, data, options) {
var graph = this;
this.freezeSimulation = false;// freeze the simulation
this.tapTimer = 0; // timer to detect doubleclick or double tap
this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
this.nodes = {}; // object with Node objects
this.edges = {}; // object with Edge objects
@ -160,41 +163,25 @@ function Graph (container, data, options) {
// apply options
this.setOptions(options);
// load data (the disable start variable will be the same as the enable clustering)
this.setData(data,this.constants.clustering.enableClustering); //
// load data (the disable start variable will be the same as the enabled clustering)
this.setData(data,this.constants.clustering.enabled);
// zoom so all data will fit on the screen
this.zoomToFit();
if (this.constants.clustering.enableClustering) {
// cluster if the data set is big
this.clusterToFit(this.constants.clustering.maxNumberOfNodes, true);
// updates the lables after clustering
this.updateLabels();
// this is called here because if clusterin is disabled, the start and stabilize are called in
// the setData function.
if (this.stabilize) {
this._doStabilize();
}
this.start();
// if clustering is disabled, the simulation will have started in the setData function
if (this.constants.clustering.enabled) {
this.startWithClustering();
}
}
/**
* We add the functionality of the cluster object to the graph object
* @type {Cluster.prototype}
*/
Graph.prototype = Object.create(Cluster.prototype);
/**
* This function zooms out to fit all data on screen based on amount of nodes
*/
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.
var zoomLevel = 46.5 / (numberOfNodes + 20.622); // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
if (zoomLevel > 1.0) {
zoomLevel = 1.0;
}
@ -281,7 +268,7 @@ Graph.prototype.setOptions = function (options) {
if (options.selectable !== undefined) {this.selectable = options.selectable;}
if (options.clustering) {
for (var prop in optiones.clustering) {
for (var prop in options.clustering) {
if (options.clustering.hasOwnProperty(prop)) {
this.constants.clustering[prop] = options.clustering[prop];
}
@ -413,13 +400,14 @@ Graph.prototype._create = function () {
this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
this.mouseTrap = mouseTrap;
/*
this.mouseTrap.bind("=",this.decreaseClusterLevel.bind(me));
this.mouseTrap.bind("-",this.increaseClusterLevel.bind(me));
this.mouseTrap.bind("s",this.singleStep.bind(me));
this.mouseTrap.bind("h",this.updateClustersDefault.bind(me));
this.mouseTrap.bind("c",this._collapseSector.bind(me));
this.mouseTrap.bind("f",this.toggleFreeze.bind(me));
*/
// add the frame to the container element
this.containerElement.appendChild(this.frame);
};
@ -599,8 +587,10 @@ Graph.prototype._onTap = function (event) {
if (node) {
if (node.isSelected() && elapsedTime < 300) {
this.zoomCenter = {"x" : this._canvasToX(pointer.x),
"y" : this._canvasToY(pointer.y)};
this.openCluster(node);
}
}
// select this node
this._selectNodes([nodeId]);
@ -655,7 +645,7 @@ Graph.prototype._onPinch = function (event) {
this.pinch.scale = 1;
}
// TODO: enable moving while pinching?
// TODO: enabled moving while pinching?
var scale = this.pinch.scale * event.gesture.scale;
this._zoom(scale, pointer)
};
@ -669,8 +659,8 @@ Graph.prototype._onPinch = function (event) {
*/
Graph.prototype._zoom = function(scale, pointer) {
var scaleOld = this._getScale();
if (scale < 0.001) {
scale = 0.001;
if (scale < 0.00001) {
scale = 0.00001;
}
if (scale > 10) {
scale = 10;
@ -799,7 +789,7 @@ Graph.prototype._checkShowPopup = function (pointer) {
for (id in nodes) {
if (nodes.hasOwnProperty(id)) {
var node = nodes[id];
if (node.getTitle() != undefined && node.isOverlappingWith(obj)) {
if (node.getTitle() !== undefined && node.isOverlappingWith(obj)) {
this.popupNode = node;
break;
}
@ -807,13 +797,13 @@ Graph.prototype._checkShowPopup = function (pointer) {
}
}
if (this.popupNode == undefined) {
if (this.popupNode === undefined) {
// search the edges for overlap
var edges = this.edges;
for (id in edges) {
if (edges.hasOwnProperty(id)) {
var edge = edges[id];
if (edge.connected && (edge.getTitle() != undefined) &&
if (edge.connected && (edge.getTitle() !== undefined) &&
edge.isOverlappingWith(obj)) {
this.popupNode = edge;
break;
@ -1679,14 +1669,14 @@ Graph.prototype._doStabilize = function() {
* Forces are caused by: edges, repulsing forces between nodes, gravity
* @private
*/
Graph.prototype._calculateForces = function(nodes,edges) {
Graph.prototype._calculateForces = function() {
// stop calculation if there is only one node
if (this.nodeIndices.length == 1) {
this.nodes[this.nodeIndices[0]]._setForce(0,0);
}
// if there are too many nodes on screen, we cluster without repositioning
else if (this.nodeIndices.length > this.constants.clustering.maxNumberOfNodes * 4 && this.constants.clustering.enableClustering == true) {
this.clusterToFit(this.constants.clustering.maxNumberOfNodes * 2, false);
else if (this.nodeIndices.length > this.constants.clustering.absoluteMaxNumberOfNodes && this.constants.clustering.enabled == true) {
this.clusterToFit(this.constants.clustering.reduceToMaxNumberOfNodes, false);
this._calculateForces();
}
else {
@ -1700,7 +1690,7 @@ Graph.prototype._calculateForces = function(nodes,edges) {
// create a local edge to the nodes and edges, that is faster
var dx, dy, angle, distance, fx, fy,
repulsingForce, springForce, length, edgeLength,
node, node1, node2, edge, edgeID, i, j, nodeID, xCenter, yCenter;
node, node1, node2, edge, edgeId, i, j, nodeId, xCenter, yCenter;
var clusterSize;
var nodes = this.nodes;
var edges = this.edges;
@ -1777,12 +1767,12 @@ Graph.prototype._calculateForces = function(nodes,edges) {
/*
// repulsion of the edges on the nodes and
for (var nodeID in nodes) {
if (nodes.hasOwnProperty(nodeID)) {
node = nodes[nodeID];
for(var edgeID in edges) {
if (edges.hasOwnProperty(edgeID)) {
edge = edges[edgeID];
for (var nodeId in nodes) {
if (nodes.hasOwnProperty(nodeId)) {
node = nodes[nodeId];
for(var edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
edge = edges[edgeId];
// get the center of the edge
xCenter = edge.from.x+(edge.to.x - edge.from.x)/2;
@ -1817,9 +1807,9 @@ Graph.prototype._calculateForces = function(nodes,edges) {
*/
// forces caused by the edges, modelled as springs
for (edgeID in edges) {
if (edges.hasOwnProperty(edgeID)) {
edge = edges[edgeID];
for (edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
edge = edges[edgeId];
if (edge.connected) {
// only calculate forces if nodes are in the same sector
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
@ -1985,7 +1975,27 @@ Graph.prototype.toggleFreeze = function() {
}
};
/**
* Mixin the cluster system and initialize the parameters required.
*
* @private
*/
Graph.prototype._loadClusterSystem = function() {
this.clusterSession = 0;
this.hubThreshold = 5;
for (var mixinFunction in ClusterMixin) {
if (ClusterMixin.hasOwnProperty(mixinFunction)) {
Graph.prototype[mixinFunction] = ClusterMixin[mixinFunction];
}
}
}
/**
* Mixin the sector system and initialize the parameters required
*
* @private
*/
Graph.prototype._loadSectorSystem = function() {
this.sectors = {};
this.activeSector = ["default"];

+ 1
- 1
src/graph/Node.js View File

@ -931,7 +931,7 @@ Node.prototype.setScale = function(scale) {
/**
* This function updates the damping parameter for clusters, based ont he
*
* @param {Integer} numberOfNodes
* @param {Number} numberOfNodes
*/
Node.prototype.updateDamping = function(numberOfNodes) {
this.damping = 0.8 + 0.1*this.clusterSize * (1 + 2/Math.pow(numberOfNodes,2));

+ 1
- 1
src/graph/Popup.js View File

@ -38,7 +38,7 @@ function Popup(container, x, y, text) {
style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
style.whiteSpace = "nowrap";
this.container.appendChild(this.frame);
};
}
/**
* @param {number} x Horizontal position of the popup window

+ 75
- 68
src/graph/SectorsMixin.js View File

@ -1,4 +1,13 @@
/**
* Creation of the SectorMixin var.
*
* This contains all the functions the Graph object can use to employ the sector system.
* The sector system is always used by Graph, though the benefits only apply to the use of clustering.
* If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges.
*
* Alex de Mulder
* 21-01-2013
*/
var SectorMixin = {
/**
@ -19,16 +28,16 @@ var SectorMixin = {
* This function sets the global references to nodes, edges and nodeIndices back to
* those of the supplied (active) sector. If a type is defined, do the specific type
*
* @param {String} sectorID
* @param {String} sectorId
* @param {String} [sectorType] | "active" or "frozen"
* @private
*/
_switchToSector : function(sectorID, sectorType) {
_switchToSector : function(sectorId, sectorType) {
if (sectorType === undefined || sectorType == "active") {
this._switchToActiveSector(sectorID);
this._switchToActiveSector(sectorId);
}
else {
this._switchToFrozenSector(sectorID);
this._switchToFrozenSector(sectorId);
}
},
@ -37,13 +46,13 @@ var SectorMixin = {
* This function sets the global references to nodes, edges and nodeIndices back to
* those of the supplied active sector.
*
* @param sectorID
* @param sectorId
* @private
*/
_switchToActiveSector : function(sectorID) {
this.nodeIndices = this.sectors["active"][sectorID]["nodeIndices"];
this.nodes = this.sectors["active"][sectorID]["nodes"];
this.edges = this.sectors["active"][sectorID]["edges"];
_switchToActiveSector : function(sectorId) {
this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"];
this.nodes = this.sectors["active"][sectorId]["nodes"];
this.edges = this.sectors["active"][sectorId]["edges"];
},
@ -51,13 +60,13 @@ var SectorMixin = {
* This function sets the global references to nodes, edges and nodeIndices back to
* those of the supplied frozen sector.
*
* @param sectorID
* @param sectorId
* @private
*/
_switchToFrozenSector : function(sectorID) {
this.nodeIndices = this.sectors["frozen"][sectorID]["nodeIndices"];
this.nodes = this.sectors["frozen"][sectorID]["nodes"];
this.edges = this.sectors["frozen"][sectorID]["edges"];
_switchToFrozenSector : function(sectorId) {
this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"];
this.nodes = this.sectors["frozen"][sectorId]["nodes"];
this.edges = this.sectors["frozen"][sectorId]["edges"];
},
@ -73,7 +82,7 @@ var SectorMixin = {
/**
* This function returns the currently active sector ID
* This function returns the currently active sector Id
*
* @returns {String}
* @private
@ -84,7 +93,7 @@ var SectorMixin = {
/**
* This function returns the previously active sector ID
* This function returns the previously active sector Id
*
* @returns {String}
* @private
@ -95,7 +104,6 @@ var SectorMixin = {
}
else {
throw new TypeError('there are not enough sectors in the this.activeSector array.');
return "";
}
},
@ -105,11 +113,11 @@ var SectorMixin = {
* This ensures it is the currently active sector returned by _sector() and it reaches the top
* of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack.
*
* @param newID
* @param newId
* @private
*/
_setActiveSector : function(newID) {
this.activeSector.push(newID);
_setActiveSector : function(newId) {
this.activeSector.push(newId);
},
@ -125,29 +133,29 @@ var SectorMixin = {
/**
* This function creates a new active sector with the supplied newID. This newID
* This function creates a new active sector with the supplied newId. This newId
* is the expanding node id.
*
* @param {String} newID | ID of the new active sector
* @param {String} newId | Id of the new active sector
* @private
*/
_createNewSector : function(newID) {
_createNewSector : function(newId) {
// create the new sector
this.sectors["active"][newID] = {"nodes":{},
this.sectors["active"][newId] = {"nodes":{},
"edges":{},
"nodeIndices":[],
"formationScale": this.scale,
"drawingNode": undefined};
// create the new sector render node. This gives visual feedback that you are in a new sector.
this.sectors["active"][newID]['drawingNode'] = new Node(
{id:newID,
this.sectors["active"][newId]['drawingNode'] = new Node(
{id:newId,
color: {
background: "#eaefef",
border: "495c5e"
}
},{},{},this.constants);
this.sectors["active"][newID]['drawingNode'].clusterSize = 2;
this.sectors["active"][newId]['drawingNode'].clusterSize = 2;
},
@ -155,11 +163,11 @@ var SectorMixin = {
* This function removes the currently active sector. This is called when we create a new
* active sector.
*
* @param {String} sectorID | ID of the active sector that will be removed
* @param {String} sectorId | Id of the active sector that will be removed
* @private
*/
_deleteActiveSector : function(sectorID) {
delete this.sectors["active"][sectorID];
_deleteActiveSector : function(sectorId) {
delete this.sectors["active"][sectorId];
},
@ -167,11 +175,11 @@ var SectorMixin = {
* This function removes the currently active sector. This is called when we reactivate
* the previously active sector.
*
* @param {String} sectorID | ID of the active sector that will be removed
* @param {String} sectorId | Id of the active sector that will be removed
* @private
*/
_deleteFrozenSector : function(sectorID) {
delete this.sectors["frozen"][sectorID];
_deleteFrozenSector : function(sectorId) {
delete this.sectors["frozen"][sectorId];
},
@ -179,15 +187,15 @@ var SectorMixin = {
* Freezing an active sector means moving it from the "active" object to the "frozen" object.
* We copy the references, then delete the active entree.
*
* @param sectorID
* @param sectorId
* @private
*/
_freezeSector : function(sectorID) {
_freezeSector : function(sectorId) {
// we move the set references from the active to the frozen stack.
this.sectors["frozen"][sectorID] = this.sectors["active"][sectorID];
this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId];
// we have moved the sector data into the frozen set, we now remove it from the active set
this._deleteActiveSector(sectorID);
this._deleteActiveSector(sectorId);
},
@ -195,15 +203,15 @@ var SectorMixin = {
* This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen"
* object to the "active" object.
*
* @param sectorID
* @param sectorId
* @private
*/
_activateSector : function(sectorID) {
_activateSector : function(sectorId) {
// we move the set references from the frozen to the active stack.
this.sectors["active"][sectorID] = this.sectors["frozen"][sectorID];
this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId];
// we have moved the sector data into the active set, we now remove it from the frozen stack
this._deleteFrozenSector(sectorID);
this._deleteFrozenSector(sectorId);
},
@ -213,27 +221,27 @@ var SectorMixin = {
* The data that is placed in the frozen (the previously active) sector is the node that has been removed from it
* upon the creation of a new active sector.
*
* @param sectorID
* @param sectorId
* @private
*/
_mergeThisWithFrozen : function(sectorID) {
_mergeThisWithFrozen : function(sectorId) {
// copy all nodes
for (var nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) {
this.sectors["frozen"][sectorID]["nodes"][nodeID] = this.nodes[nodeID];
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId];
}
}
// copy all edges (if not fully clustered, else there are no edges)
for (var edgeID in this.edges) {
if (this.edges.hasOwnProperty(edgeID)) {
this.sectors["frozen"][sectorID]["edges"][edgeID] = this.edges[edgeID];
for (var edgeId in this.edges) {
if (this.edges.hasOwnProperty(edgeId)) {
this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId];
}
}
// merge the nodeIndices
for (var i = 0; i < this.nodeIndices.length; i++) {
this.sectors["frozen"][sectorID]["nodeIndices"].push(this.nodeIndices[i]);
this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]);
}
},
@ -259,14 +267,13 @@ var SectorMixin = {
// this is the currently active sector
var sector = this._sector();
// this should allow me to select nodes from a frozen set.
// TODO: after rewriting the selection function, have this working
if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) {
console.log("the node is part of the active sector");
}
else {
console.log("I dont know what the fuck happened!!");
}
// // this should allow me to select nodes from a frozen set.
// if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) {
// console.log("the node is part of the active sector");
// }
// else {
// console.log("I dont know what the fuck happened!!");
// }
// when we switch to a new sector, we remove the node that will be expanded from the current nodes list.
delete this.nodes[node.id];
@ -276,7 +283,7 @@ var SectorMixin = {
// we fully freeze the currently active sector
this._freezeSector(sector);
// we create a new active sector. This sector has the ID of the node to ensure uniqueness
// we create a new active sector. This sector has the Id of the node to ensure uniqueness
this._createNewSector(unqiueIdentifier);
// we add the active sector to the sectors array to be able to revert these steps later on
@ -405,7 +412,7 @@ var SectorMixin = {
* @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
* | we dont pass the function itself because then the "this" is the window object
* | instead of the Graph object
* @param {*} [args] | Optional: arguments to pass to the runFunction
* @param {*} [argument] | Optional: arguments to pass to the runFunction
* @private
*/
_doInAllSectors : function(runFunction,argument) {
@ -438,14 +445,14 @@ var SectorMixin = {
var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
for (var sector in this.sectors[sectorType]) {
if (this.sectors[sectorType].hasOwnProperty(sector)) {
minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9;
minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9;
if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) {
this._switchToSector(sector,sectorType);
for (var nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) {
node = this.nodes[nodeID];
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
node.resize(ctx);
if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;}
if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;}
@ -456,9 +463,9 @@ var SectorMixin = {
node = this.sectors[sectorType][sector]["drawingNode"];
node.x = 0.5 * (maxX + minX);
node.y = 0.5 * (maxY + minY);
node.width = node.x - minX;
node.height = node.y - minY;
node.radius = Math.sqrt(Math.pow(node.width,2) + Math.pow(node.height,2));
node.width = 2 * (node.x - minX);
node.height = 2 * (node.y - minY);
node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2));
node.setScale(this.scale);
node._drawCircle(ctx);
}

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

@ -1,998 +0,0 @@
/**
* @constructor Cluster
* Contains the cluster properties for the graph object
*/
function Cluster() {
this.clusterSession = 0;
this.hubThreshold = 5;
}
/**
* This function clusters until the maxNumberOfNodes has been reached
*
* @param {Number} maxNumberOfNodes
* @param {Boolean} reposition
*/
Cluster.prototype.clusterToFit = function(maxNumberOfNodes, reposition) {
var numberOfNodes = this.nodeIndices.length;
var maxLevels = 50;
var level = 0;
// we first cluster the hubs, then we pull in the outliers, repeat
while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
if (level % 3 == 0) {
console.log("Aggregating Hubs @ level: ",level,". Threshold:", this.hubThreshold,"clusterSession",this.clusterSession);
this.forceAggregateHubs();
}
else {
console.log("Pulling in Outliers @ level: ",level,"clusterSession",this.clusterSession);
this.increaseClusterLevel();
}
numberOfNodes = this.nodeIndices.length;
level += 1;
}
// after the clustering we reposition the nodes to reduce the initial chaos
if (level > 1 && reposition == true) {
this.repositionNodes();
}
};
/**
* This function can be called to open up a specific cluster. It is only called by
* It will unpack the cluster back one level.
*
* @param node | Node object: cluster to open.
*/
Cluster.prototype.openCluster = function(node) {
var isMovingBeforeClustering = this.moving;
if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node)) {
this._addSector(node);
var level = 0;
while ((this.nodeIndices.length < this.constants.clustering.maxNumberOfNodes) &&
(level < 5)) {
this.decreaseClusterLevel();
level += 1;
}
}
else {
this._expandClusterNode(node,false,true);
// housekeeping
this._updateNodeIndexList();
this._updateDynamicEdges();
this.updateLabels();
}
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
}
};
/**
* This calls the updateClustes with default arguments
*/
Cluster.prototype.updateClustersDefault = function() {
if (this.constants.clustering.enableClustering) {
this.updateClusters(0,false,false);
}
};
/**
* This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
* be clustered with their connected node. This can be repeated as many times as needed.
* This can be called externally (by a keybind for instance) to reduce the complexity of big datasets.
*/
Cluster.prototype.increaseClusterLevel = function() {
this.updateClusters(-1,false,true);
};
/**
* This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will
* be unpacked if they are a cluster. This can be repeated as many times as needed.
* This can be called externally (by a key-bind for instance) to look into clusters without zooming.
*/
Cluster.prototype.decreaseClusterLevel = function() {
this.updateClusters(1,false,true);
};
/**
* This function clusters on zoom, it can be called with a predefined zoom direction
* If out, check if we can form clusters, if in, check if we can open clusters.
* This function is only called from _zoom()
*
* @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn
* @param {Boolean} recursive | enable or disable recursive calling of the opening of clusters
* @param {Boolean} force | enable or disable forcing
*
*/
Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) {
var isMovingBeforeClustering = this.moving;
var amountOfNodes = this.nodeIndices.length;
// on zoom out collapse the sector back to default
if (this.previousScale > this.scale && zoomDirection == 0) {
this._collapseSector();
}
// check if we zoom in or out
if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
this._formClusters(force);
}
else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in
if (force == false) {
this._openClustersBySize();
}
else {
this._openClusters(recursive,force);
}
}
this._updateNodeIndexList();
// if a cluster was NOT formed and the user zoomed out, we try clustering by hubs
if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
this._aggregateHubs(force);
this._updateNodeIndexList();
}
// we now reduce snakes.
if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
this.handleSnakes();
this._updateNodeIndexList();
}
this.previousScale = this.scale;
// rest of the housekeeping
this._updateDynamicEdges();
this.updateLabels();
// if a cluster was formed, we increase the clusterSession
if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
this.clusterSession += 1;
}
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
}
};
/**
* This function handles the snakes. It is called on every updateClusters().
*/
Cluster.prototype.handleSnakes = function() {
// after clustering we check how many snakes there are
var snakePercentage = this._getSnakeFraction();
if (snakePercentage > this.constants.clustering.snakeThreshold) {
this._reduceAmountOfSnakes(1 - this.constants.clustering.snakeThreshold / snakePercentage)
}
};
/**
* this functions starts clustering by hubs
* The minimum hub threshold is set globally
*
* @private
*/
Cluster.prototype._aggregateHubs = function(force) {
this._getHubSize();
this._formClustersByHub(force,false);
};
/**
* This function is fired by keypress. It forces hubs to form.
*
*/
Cluster.prototype.forceAggregateHubs = function() {
var isMovingBeforeClustering = this.moving;
var amountOfNodes = this.nodeIndices.length;
this._aggregateHubs(true);
// housekeeping
this._updateNodeIndexList();
this._updateDynamicEdges();
this.updateLabels();
// if a cluster was formed, we increase the clusterSession
if (this.nodeIndices.length != amountOfNodes) {
this.clusterSession += 1;
}
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
}
};
/**
* If a cluster takes up more than a set percentage of the screen, open the cluster
*
* @private
*/
Cluster.prototype._openClustersBySize = function() {
for (nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) {
var node = this.nodes[nodeID];
if (node.inView() == true) {
if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
(node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
this.openCluster(node);
}
}
}
}
};
/**
* This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it
* has to be opened based on the current zoom level.
*
* @private
*/
Cluster.prototype._openClusters = function(recursive,force) {
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
this._expandClusterNode(node,recursive,force);
}
};
/**
* 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} parentNode | to check for cluster and expand
* @param {Boolean} recursive | enable or disable recursive calling
* @param {Boolean} force | enable or disable forcing
* @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
* @private
*/
Cluster.prototype._expandClusterNode = function(parentNode, recursive, force, openAll) {
// first check if node is a cluster
if (parentNode.clusterSize > 1) {
// this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20
if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) {
openAll = true;
}
recursive = openAll ? true : recursive;
// if the last child has been added on a smaller scale than current scale (@optimization)
if (parentNode.formationScale < this.scale || force == true) {
// we will check if any of the contained child nodes should be removed from the cluster
for (var containedNodeID in parentNode.containedNodes) {
if (parentNode.containedNodes.hasOwnProperty(containedNodeID)) {
var childNode = parentNode.containedNodes[containedNodeID];
// force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
// the largest cluster is the one that comes from outside
if (force == true) {
if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1]
|| openAll) {
this._expelChildFromParent(parentNode,containedNodeID,recursive,force,openAll);
}
}
else {
if (this._nodeInActiveArea(parentNode)) {
this._expelChildFromParent(parentNode,containedNodeID,recursive,force,openAll);
}
}
}
}
}
}
};
/**
* ONLY CALLED FROM _expandClusterNode
*
* This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove
* the child node from the parent contained_node object and put it back into the global nodes object.
* The same holds for the edge that was connected to the child node. It is moved back into the global edges object.
*
* @param {Node} parentNode | the parent node
* @param {String} containedNodeID | child_node id as it is contained in the containedNodes object of the parent node
* @param {Boolean} recursive | This will also check if the child needs to be expanded.
* With force and recursive both true, the entire cluster is unpacked
* @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent
* @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
* @private
*/
Cluster.prototype._expelChildFromParent = function(parentNode, containedNodeID, recursive, force, openAll) {
var childNode = parentNode.containedNodes[containedNodeID];
// if child node has been added on smaller scale than current, kick out
if (childNode.formationScale < this.scale || force == true) {
// put the child node back in the global nodes object
this.nodes[containedNodeID] = childNode;
// release the contained edges from this childNode back into the global edges
this._releaseContainedEdges(parentNode,childNode);
// reconnect rerouted edges to the childNode
this._connectEdgeBackToChild(parentNode,childNode);
// validate all edges in dynamicEdges
this._validateEdges(parentNode);
// undo the changes from the clustering operation on the parent node
parentNode.mass -= this.constants.clustering.massTransferCoefficient * childNode.mass;
parentNode.fontSize -= this.constants.clustering.fontSizeMultiplier * childNode.clusterSize;
parentNode.clusterSize -= childNode.clusterSize;
parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
// place the child node near the parent, not at the exact same location to avoid chaos in the system
childNode.x = parentNode.x + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize;
childNode.y = parentNode.y + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize;
// remove node from the list
delete parentNode.containedNodes[containedNodeID];
// check if there are other childs with this clusterSession in the parent.
var othersPresent = false;
for (var childNodeID in parentNode.containedNodes) {
if (parentNode.containedNodes.hasOwnProperty(childNodeID)) {
if (parentNode.containedNodes[childNodeID].clusterSession == childNode.clusterSession) {
othersPresent = true;
break;
}
}
}
// if there are no others, remove the cluster session from the list
if (othersPresent == false) {
parentNode.clusterSessions.pop();
}
// remove the clusterSession from the child node
childNode.clusterSession = 0;
// restart the simulation to reorganise all nodes
this.moving = true;
// recalculate the size of the node on the next time the node is rendered
parentNode.clearSizeCache();
}
// check if a further expansion step is possible if recursivity is enabled
if (recursive == true) {
this._expandClusterNode(childNode,recursive,force,openAll);
}
};
/**
* This function checks if any nodes at the end of their trees have edges below a threshold length
* This function is called only from updateClusters()
* forceLevelCollapse ignores the length of the edge and collapses one level
* This means that a node with only one edge will be clustered with its connected node
*
* @private
* @param {Boolean} force
*/
Cluster.prototype._formClusters = function(force) {
if (force == false) {
this._formClustersByZoom();
}
else {
this._forceClustersByZoom();
}
};
/**
* This function handles the clustering by zooming out, this is based on a minimum edge distance
*
* @private
*/
Cluster.prototype._formClustersByZoom = function() {
var dx,dy,length,
minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
// check if any edges are shorter than minLength and start the clustering
// the clustering favours the node with the larger mass
for (var edgeID in this.edges) {
if (this.edges.hasOwnProperty(edgeID)) {
var edge = this.edges[edgeID];
if (edge.connected) {
if (edge.toId != edge.fromId) {
dx = (edge.to.x - edge.from.x);
dy = (edge.to.y - edge.from.y);
length = Math.sqrt(dx * dx + dy * dy);
if (length < minLength) {
// first check which node is larger
var parentNode = edge.from;
var childNode = edge.to;
if (edge.to.mass > edge.from.mass) {
parentNode = edge.to;
childNode = edge.from;
}
if (childNode.dynamicEdgesLength == 1) {
this._addToCluster(parentNode,childNode,false);
}
else if (parentNode.dynamicEdgesLength == 1) {
this._addToCluster(childNode,parentNode,false);
}
}
}
}
}
}
};
/**
* This function forces the graph to cluster all nodes with only one connecting edge to their
* connected node.
*
* @private
*/
Cluster.prototype._forceClustersByZoom = function() {
for (var nodeID in this.nodes) {
// another node could have absorbed this child.
if (this.nodes.hasOwnProperty(nodeID)) {
var childNode = this.nodes[nodeID];
// the edges can be swallowed by another decrease
if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) {
var edge = childNode.dynamicEdges[0];
var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId];
// group to the largest node
if (childNode.id != parentNode.id) {
if (parentNode.mass > childNode.mass) {
this._addToCluster(parentNode,childNode,true);
}
else {
this._addToCluster(childNode,parentNode,true);
}
}
}
}
}
};
/**
* This function forms clusters from hubs, it loops over all nodes
*
* @param {Boolean} force | Disregard zoom level
* @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
* @private
*/
Cluster.prototype._formClustersByHub = function(force, onlyEqual) {
// we loop over all nodes in the list
for (var nodeID in this.nodes) {
// we check if it is still available since it can be used by the clustering in this loop
if (this.nodes.hasOwnProperty(nodeID)) {
this._formClusterFromHub(this.nodes[nodeID],force,onlyEqual);
}
}
};
/**
* This function forms a cluster from a specific preselected hub node
*
* @param {Node} hubNode | the node we will cluster as a hub
* @param {Boolean} force | Disregard zoom level
* @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
* @param {Number} [absorptionSizeOffset] |
* @private
*/
Cluster.prototype._formClusterFromHub = function(hubNode, force, onlyEqual, absorptionSizeOffset) {
if (absorptionSizeOffset === undefined) {
absorptionSizeOffset = 0;
}
// we decide if the node is a hub
if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) ||
(hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) {
// initialize variables
var dx,dy,length;
var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
var allowCluster = false;
// we create a list of edges because the dynamicEdges change over the course of this loop
var edgesIDarray = [];
var amountOfInitialEdges = hubNode.dynamicEdges.length;
for (var j = 0; j < amountOfInitialEdges; j++) {
edgesIDarray.push(hubNode.dynamicEdges[j].id);
}
// if the hub clustering is not forces, we check if one of the edges connected
// to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold
if (force == false) {
allowCluster = false;
for (j = 0; j < amountOfInitialEdges; j++) {
var edge = this.edges[edgesIDarray[j]];
if (edge !== undefined) {
if (edge.connected) {
if (edge.toId != edge.fromId) {
dx = (edge.to.x - edge.from.x);
dy = (edge.to.y - edge.from.y);
length = Math.sqrt(dx * dx + dy * dy);
if (length < minLength) {
allowCluster = true;
break;
}
}
}
}
}
}
// start the clustering if allowed
if ((!force && allowCluster) || force) {
// we loop over all edges INITIALLY connected to this hub
for (j = 0; j < amountOfInitialEdges; j++) {
edge = this.edges[edgesIDarray[j]];
// the edge can be clustered by this function in a previous loop
if (edge !== undefined) {
var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
// we do not want hubs to merge with other hubs nor do we want to cluster itself.
if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) &&
(childNode.id != hubNode.id)) {
this._addToCluster(hubNode,childNode,force);
}
}
}
}
}
};
/**
* This function adds the child node to the parent node, creating a cluster if it is not already.
*
* @param {Node} parentNode | this is the node that will house the child node
* @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node
* @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
* @private
*/
Cluster.prototype._addToCluster = function(parentNode, childNode, force) {
// join child node in the parent node
parentNode.containedNodes[childNode.id] = childNode;
// manage all the edges connected to the child and parent nodes
for (var i = 0; i < childNode.dynamicEdges.length; i++) {
var edge = childNode.dynamicEdges[i];
if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode
this._addToContainedEdges(parentNode,childNode,edge);
}
else {
this._connectEdgeToCluster(parentNode,childNode,edge);
}
}
// a contained node has no dynamic edges.
childNode.dynamicEdges = [];
// remove circular edges from clusters
this._containCircularEdgesFromNode(parentNode,childNode);
// remove the childNode from the global nodes object
delete this.nodes[childNode.id];
// update the properties of the child and parent
var massBefore = parentNode.mass;
childNode.clusterSession = this.clusterSession;
parentNode.mass += this.constants.clustering.massTransferCoefficient * childNode.mass;
parentNode.clusterSize += childNode.clusterSize;
parentNode.fontSize += this.constants.clustering.fontSizeMultiplier * childNode.clusterSize;
// keep track of the clustersessions so we can open the cluster up as it has been formed.
if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) {
parentNode.clusterSessions.push(this.clusterSession);
}
// forced clusters only open from screen size and double tap
if (force == true) {
// parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3);
parentNode.formationScale = 0;
}
else {
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();
// set the pop-out scale for the childnode
parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale;
// nullify the movement velocity of the child, this is to avoid hectic behaviour
childNode.clearVelocity();
// the mass has altered, preservation of energy dictates the velocity to be updated
parentNode.updateVelocity(massBefore);
// restart the simulation to reorganise all nodes
this.moving = true;
};
/**
* This function will apply the changes made to the remainingEdges during the formation of the clusters.
* This is a seperate function to allow for level-wise collapsing of the node tree.
* It has to be called if a level is collapsed. It is called by _formClusters().
* @private
*/
Cluster.prototype._updateDynamicEdges = function() {
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
node.dynamicEdgesLength = node.dynamicEdges.length;
// this corrects for multiple edges pointing at the same other node
var correction = 0;
if (node.dynamicEdgesLength > 1) {
for (var j = 0; j < node.dynamicEdgesLength - 1; j++) {
var edgeToId = node.dynamicEdges[j].toId;
var edgeFromId = node.dynamicEdges[j].fromId;
for (var k = j+1; k < node.dynamicEdgesLength; k++) {
if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) ||
(node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) {
correction += 1;
}
}
}
}
node.dynamicEdgesLength -= correction;
}
};
/**
* This adds an edge from the childNode to the contained edges of the parent node
*
* @param parentNode | Node object
* @param childNode | Node object
* @param edge | Edge object
* @private
*/
Cluster.prototype._addToContainedEdges = function(parentNode, childNode, edge) {
// create an array object if it does not yet exist for this childNode
if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) {
parentNode.containedEdges[childNode.id] = []
}
// add this edge to the list
parentNode.containedEdges[childNode.id].push(edge);
// remove the edge from the global edges object
delete this.edges[edge.id];
// remove the edge from the parent object
for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
if (parentNode.dynamicEdges[i].id == edge.id) {
parentNode.dynamicEdges.splice(i,1);
break;
}
}
};
/**
* This function connects an edge that was connected to a child node to the parent node.
* It keeps track of which nodes it has been connected to with the originalID array.
*
* @param parentNode | Node object
* @param childNode | Node object
* @param edge | Edge object
* @private
*/
Cluster.prototype._connectEdgeToCluster = function(parentNode, childNode, edge) {
// handle circular edges
if (edge.toId == edge.fromId) {
this._addToContainedEdges(parentNode, childNode, edge);
}
else {
if (edge.toId == childNode.id) { // edge connected to other node on the "to" side
edge.originalToID.push(childNode.id);
edge.to = parentNode;
edge.toId = parentNode.id;
}
else { // edge connected to other node with the "from" side
edge.originalFromID.push(childNode.id);
edge.from = parentNode;
edge.fromId = parentNode.id;
}
this._addToReroutedEdges(parentNode,childNode,edge);
}
};
Cluster.prototype._containCircularEdgesFromNode = function(parentNode, childNode) {
// manage all the edges connected to the child and parent nodes
for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
var edge = parentNode.dynamicEdges[i];
// handle circular edges
if (edge.toId == edge.fromId) {
this._addToContainedEdges(parentNode, childNode, edge);
}
}
}
/**
* This adds an edge from the childNode to the rerouted edges of the parent node
*
* @param parentNode | Node object
* @param childNode | Node object
* @param edge | Edge object
* @private
*/
Cluster.prototype._addToReroutedEdges = function(parentNode, childNode, edge) {
// create an array object if it does not yet exist for this childNode
// we store the edge in the rerouted edges so we can restore it when the cluster pops open
if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) {
parentNode.reroutedEdges[childNode.id] = [];
}
parentNode.reroutedEdges[childNode.id].push(edge);
// this edge becomes part of the dynamicEdges of the cluster node
parentNode.dynamicEdges.push(edge);
};
/**
* This function connects an edge that was connected to a cluster node back to the child node.
*
* @param parentNode | Node object
* @param childNode | Node object
* @private
*/
Cluster.prototype._connectEdgeBackToChild = function(parentNode, childNode) {
if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) {
for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) {
var edge = parentNode.reroutedEdges[childNode.id][i];
if (edge.originalFromID[edge.originalFromID.length-1] == childNode.id) {
edge.originalFromID.pop();
edge.fromId = childNode.id;
edge.from = childNode;
}
else {
edge.originalToID.pop();
edge.toId = childNode.id;
edge.to = childNode;
}
// append this edge to the list of edges connecting to the childnode
childNode.dynamicEdges.push(edge);
// remove the edge from the parent object
for (var j = 0; j < parentNode.dynamicEdges.length; j++) {
if (parentNode.dynamicEdges[j].id == edge.id) {
parentNode.dynamicEdges.splice(j,1);
break;
}
}
}
// remove the entry from the rerouted edges
delete parentNode.reroutedEdges[childNode.id];
}
};
/**
* When loops are clustered, an edge can be both in the rerouted array and the contained array.
* This function is called last to verify that all edges in dynamicEdges are in fact connected to the
* parentNode
*
* @param parentNode | Node object
* @private
*/
Cluster.prototype._validateEdges = function(parentNode) {
for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
var edge = parentNode.dynamicEdges[i];
if (parentNode.id != edge.toId && parentNode.id != edge.fromId) {
parentNode.dynamicEdges.splice(i,1);
}
}
};
/**
* This function released the contained edges back into the global domain and puts them back into the
* dynamic edges of both parent and child.
*
* @param {Node} parentNode |
* @param {Node} childNode |
* @private
*/
Cluster.prototype._releaseContainedEdges = function(parentNode, childNode) {
for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) {
var edge = parentNode.containedEdges[childNode.id][i];
// put the edge back in the global edges object
this.edges[edge.id] = edge;
// put the edge back in the dynamic edges of the child and parent
childNode.dynamicEdges.push(edge);
parentNode.dynamicEdges.push(edge);
}
// remove the entry from the contained edges
delete parentNode.containedEdges[childNode.id];
};
// ------------------- UTILITY FUNCTIONS ---------------------------- //
/**
* This updates the node labels for all nodes (for debugging purposes)
*/
Cluster.prototype.updateLabels = function() {
var nodeID;
// update node labels
for (nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) {
var node = this.nodes[nodeID];
if (node.clusterSize > 1) {
node.label = "[".concat(String(node.clusterSize),"]");
}
}
}
// update node labels
for (nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) {
node = this.nodes[nodeID];
if (node.clusterSize == 1) {
if (node.originalLabel !== undefined) {
node.label = node.originalLabel;
}
else {
node.label = String(node.id);
}
}
}
}
/* Debug Override */
// for (nodeID in this.nodes) {
// if (this.nodes.hasOwnProperty(nodeID)) {
// node = this.nodes[nodeID];
// node.label = String(Math.round(node.width)).concat(":",Math.round(node.width*this.scale));
// }
// }
};
/**
* This function determines if the cluster we want to decluster is in the active area
* this means around the zoom center
*
* @param {Node} node
* @returns {boolean}
* @private
*/
Cluster.prototype._nodeInActiveArea = function(node) {
return (
Math.abs(node.x - this.zoomCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale
&&
Math.abs(node.y - this.zoomCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
)
};
/**
* This is an adaptation of the original repositioning function. This is called if the system is clustered initially
* It puts large clusters away from the center and randomizes the order.
*
*/
Cluster.prototype.repositionNodes = function() {
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
if (!node.isFixed()) {
var radius = this.constants.edges.length * (1 + 0.6*node.clusterSize);
var angle = 2 * Math.PI * Math.random();
node.x = radius * Math.cos(angle);
node.y = radius * Math.sin(angle);
}
}
};
/**
* We determine how many connections denote an important hub.
* We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
*
* @private
*/
Cluster.prototype._getHubSize = function() {
var average = 0;
var averageSquared = 0;
var hubCounter = 0;
var largestHub = 0;
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
if (node.dynamicEdgesLength > largestHub) {
largestHub = node.dynamicEdgesLength;
}
average += node.dynamicEdgesLength;
averageSquared += Math.pow(node.dynamicEdgesLength,2);
hubCounter += 1;
}
average = average / hubCounter;
averageSquared = averageSquared / hubCounter;
var variance = averageSquared - Math.pow(average,2);
var standardDeviation = Math.sqrt(variance);
this.hubThreshold = Math.floor(average + 2*standardDeviation);
// always have at least one to cluster
if (this.hubThreshold > largestHub) {
this.hubThreshold = largestHub;
}
// console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
// console.log("hubThreshold:",this.hubThreshold);
};
/**
* We reduce the amount of "extension nodes" or snakes. These are not quickly clustered with the outliers and hubs methods
* with this amount we can cluster specifically on these snakes.
*
* @param {Number} fraction | between 0 and 1, the percentage of snakes to reduce
* @private
*/
Cluster.prototype._reduceAmountOfSnakes = function(fraction) {
this.hubThreshold = 2;
var reduceAmount = Math.floor(this.nodeIndices.length * fraction);
for (nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) {
if (this.nodes[nodeID].dynamicEdgesLength == 2 && this.nodes[nodeID].dynamicEdges.length >= 2) {
if (reduceAmount > 0) {
this._formClusterFromHub(this.nodes[nodeID],true,true,1);
reduceAmount -= 1;
}
}
}
}
};
/**
* We get the amount of "extension nodes" or snakes. These are not quickly clustered with the outliers and hubs methods
* with this amount we can cluster specifically on these snakes.
*
* @private
*/
Cluster.prototype._getSnakeFraction = function() {
var snakes = 0;
var total = 0;
for (nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) {
if (this.nodes[nodeID].dynamicEdgesLength == 2 && this.nodes[nodeID].dynamicEdges.length >= 2) {
snakes += 1;
}
total += 1;
}
}
return snakes/total;
};

Loading…
Cancel
Save