Browse Source

Merge pull request #29 from almende/alex_dev

merge alex_dev with develop
css_transitions
Jos de Jong 10 years ago
parent
commit
18a835dffb
41 changed files with 23210 additions and 3566 deletions
  1. +5
    -0
      .gitignore
  2. +10
    -0
      HISTORY.md
  3. +10
    -4
      Jakefile.js
  4. BIN
      dist/UI_icons/downarrow.png
  5. BIN
      dist/UI_icons/leftarrow.png
  6. BIN
      dist/UI_icons/minus.png
  7. BIN
      dist/UI_icons/plus.png
  8. BIN
      dist/UI_icons/rightarrow.png
  9. BIN
      dist/UI_icons/uparrow.png
  10. BIN
      dist/UI_icons/zoomExtends.png
  11. +8
    -0
      dist/vis.css
  12. +6982
    -3068
      dist/vis.js
  13. +10
    -8
      dist/vis.min.js
  14. +259
    -2
      docs/graph.html
  15. +8
    -0
      examples/graph/02_random_nodes.html
  16. +1
    -1
      examples/graph/17_network_info.html
  17. +102
    -0
      examples/graph/18_fully_random_nodes_clustering.html
  18. +142
    -0
      examples/graph/19_scale_free_graph_clustering.html
  19. +186
    -0
      examples/graph/20_UI_example.html
  20. BIN
      examples/graph/img/UI_icons/downarrow.png
  21. BIN
      examples/graph/img/UI_icons/leftarrow.png
  22. BIN
      examples/graph/img/UI_icons/minus.png
  23. BIN
      examples/graph/img/UI_icons/plus.png
  24. BIN
      examples/graph/img/UI_icons/rightarrow.png
  25. BIN
      examples/graph/img/UI_icons/uparrow.png
  26. BIN
      examples/graph/img/UI_icons/zoomExtends.png
  27. +3
    -0
      examples/graph/index.html
  28. +4
    -2
      package.json
  29. +25
    -3
      src/DataSet.js
  30. +1019
    -0
      src/graph/ClusterMixin.js
  31. +41
    -23
      src/graph/Edge.js
  32. +741
    -408
      src/graph/Graph.js
  33. +337
    -46
      src/graph/Node.js
  34. +1
    -1
      src/graph/Popup.js
  35. +547
    -0
      src/graph/SectorsMixin.js
  36. +487
    -0
      src/graph/SelectionMixin.js
  37. +258
    -0
      src/graph/UIMixin.js
  38. +14
    -0
      src/module/imports.js
  39. +28
    -0
      src/util.js
  40. +18
    -0
      test/dataset.js
  41. +11964
    -0
      vis.js.tmp

+ 5
- 0
.gitignore View File

@ -1,2 +1,7 @@
.idea
node_modules
.project
.settings/.jsdtscope
.settings/org.eclipse.wst.jsdt.ui.superType.container
.settings/org.eclipse.wst.jsdt.ui.superType.name
npm-debug.log

+ 10
- 0
HISTORY.md View File

@ -1,6 +1,16 @@
vis.js history
http://visjs.org
## 2014-01-27, version 0.4.0
- Fixed longstanding bug in the force calculation, increasing simulation stability and fluidity.
- Reworked the calculation of the Graph, increasing performance for larger datasets (up to 10x!).
- Support for automatic clustering in Graph to handle large (>50000) datasets without losing performance.
- Added automatic intial zooming to Graph, to more easily view large amounts of data.
- Added local declustering to Graph, freezing the simulation of nodes outside of the cluster.
- Added support for key-bindings by including mouseTrap in Graph.
- Added node-based navigation GUI.
- Added keyboard navigation option.
## 2014-01-14, version 0.3.0

+ 10
- 4
Jakefile.js View File

@ -29,7 +29,6 @@ task('default', ['build', 'minify'], function () {
desc('Build the visualization library vis.js');
task('build', {async: true}, function () {
jake.mkdirP(DIST);
// concatenate and stringify the css files
concat({
src: [
@ -83,14 +82,21 @@ task('build', {async: true}, function () {
'./src/graph/Popup.js',
'./src/graph/Groups.js',
'./src/graph/Images.js',
'./src/graph/SectorsMixin.js',
'./src/graph/ClusterMixin.js',
'./src/graph/SelectionMixin.js',
'./src/graph/UIMixin.js',
'./src/graph/Graph.js',
'./src/module/exports.js'
],
separator: '\n'
});
var timeStart = Date.now();
// bundle the concatenated script and dependencies into one file
var b = browserify();
b.add(VIS_TMP);
@ -100,13 +106,13 @@ task('build', {async: true}, function () {
if(err) {
throw err;
}
console.log("browserify",Date.now() - timeStart); timeStart = Date.now();
// add header and footer
var lib = read('./src/module/header.js') + code;
// write bundled file
write(VIS, lib);
console.log('created ' + VIS);
console.log('created js' + VIS);
// remove temporary file
fs.unlinkSync(VIS_TMP);
@ -133,7 +139,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);
});
/**

BIN
dist/UI_icons/downarrow.png View File

Before After
Width: 30  |  Height: 30  |  Size: 4.4 KiB

BIN
dist/UI_icons/leftarrow.png View File

Before After
Width: 30  |  Height: 30  |  Size: 4.4 KiB

BIN
dist/UI_icons/minus.png View File

Before After
Width: 30  |  Height: 30  |  Size: 4.0 KiB

BIN
dist/UI_icons/plus.png View File

Before After
Width: 30  |  Height: 30  |  Size: 4.2 KiB

BIN
dist/UI_icons/rightarrow.png View File

Before After
Width: 30  |  Height: 30  |  Size: 4.4 KiB

BIN
dist/UI_icons/uparrow.png View File

Before After
Width: 30  |  Height: 30  |  Size: 4.4 KiB

BIN
dist/UI_icons/zoomExtends.png View File

Before After
Width: 30  |  Height: 30  |  Size: 4.4 KiB

+ 8
- 0
dist/vis.css View File

@ -109,6 +109,14 @@
z-index: 999;
}
.vis.timeline .item.point.selected {
background-color: #FFF785;
z-index: 999;
}
.vis.timeline .item.point.selected .dot {
border-color: #FFC200;
}
.vis.timeline .item.cluster {
/* TODO: use another color or pattern? */
background: #97B0F8 url('img/cluster_bg.png');

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


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


+ 259
- 2
docs/graph.html View File

@ -29,6 +29,9 @@
</ul>
</li>
<li><a href="#Configuration_Options">Configuration Options</a></li>
<li><a href="#Clustering">Clustering</a></li>
<li><a href="#NavigationUI">Navigation GUI</a></li>
<li><a href="#Keyboard">Keyboard Navigation</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 +998,258 @@ 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>
<pre class="prettyprint">
// These variables must be defined in an options object named clustering.
</pre>
<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="NavigationUI">Navigation GUI Elements</h2>
<p>
As of 0.4.0, a node-based navigation GUI system has been added to vis.js.
It can be configured with the following user-configurable settings.
The default state for the GUI navigation elements is <b>off</b>.
</p>
<pre class="prettyprint">
// These variables must be defined in an options object named navigationUI.
// If a variable is not supplied, the default value is used.
options: {
navigationUI: {
enabled: false,
initiallyVisible: true,
enableToggling: true,
iconPath: this._getIconURL() // automatic loading of the default icons
},
}
</pre>
<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 the navigation UI elements.</td>
</tr>
<tr>
<td>initiallyVisible</td>
<td>Boolean</td>
<td>true</td>
<td>The UI elements can be toggled by pressing the "u" key. If <code>initiallyVisible</code> is false, the UI is hidden
until "u" is pressed on the keyboard.
</td>
</tr>
<tr>
<td>enableToggling</td>
<td>Boolean</td>
<td>true</td>
<td>Enable or disable the option to press "u" to toggle the UI elements. If the UI is initially hidden and the toggling is off, the UI cannot be used!</td>
</tr>
<tr>
<td>iconPath</td>
<td>string</td>
<td>[path to vis.js]/UI_icons/</td>
<td>The path to the icon images can be defined here. If custom icons are used, they have to have the same filename as the ones originally packaged with vis.js.</td>
</tr>
</table>
<h2 id="Keyboard">Keyboard Navigation</h2>
<p>
As of 0.4.0, keyboard navigation has been added to vis.js.
It can be configured with the following user-configurable settings.
The default state for the keyboard navigation is <b>off</b>. The predefined keys can be found in the <a href="../examples/graph/20_UI_example.html">UI example.</a>
</p>
<pre class="prettyprint">
// These variables must be defined in an options object named keyboardNavigation.
// If a variable is not supplied, the default value is used
options: {
keyboardNavigation: {
enabled: false,
xMovementSpeed: 10,
yMovementSpeed: 10,
zoomMovementSpeed: 0.02
}
}
</pre>
<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 the keyboard navigation.</td>
</tr>
<tr>
<td>xMovementSpeed</td>
<td>Number</td>
<td>10</td>
<td>This defines the speed of the camera movement in the x direction when using the keyboard navigation.
</td>
</tr>
<tr>
<td>yMovementSpeed</td>
<td>Boolean</td>
<td>10</td>
<td>This defines the speed of the camera movement in the y direction when using the keyboard navigation.</td>
</tr>
<tr>
<td>zoomMovementSpeed</td>
<td>string</td>
<td>0.02</td>
<td>This defines the zoomspeed when using the keyboard navigation.</td>
</tr>
</table>
<h2 id="Methods">Methods</h2>
<p>
@ -1009,11 +1264,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>

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

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

+ 1
- 1
examples/graph/17_network_info.html View File

@ -17,7 +17,7 @@
}
</style>
<script type="text/javascript" src="../../dist/vis.min.js"></script>
<script type="text/javascript" src="../../dist/vis.js"></script>
<script type="text/javascript">
var nodes = null;

+ 102
- 0
examples/graph/18_fully_random_nodes_clustering.html View File

@ -0,0 +1,102 @@
<!doctype html>
<html>
<head>
<title>Graph | Really 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 = [];
// randomly create some nodes and edges
var nodeCount = parseInt(document.getElementById('nodeCount').value);
for (var i = 0; i < nodeCount; i++) {
nodes.push({
id: i,
label: String(i)
});
}
for (var i = 0; i < nodeCount; i++) {
var from = i;
var to = i;
to = i;
while (to == i) {
to = Math.floor(Math.random() * (nodeCount));
}
edges.push({
from: from,
to: to
});
}
// create a graph
var clusteringOn = document.getElementById('clustering').checked;
var container = document.getElementById('mygraph');
var data = {
nodes: nodes,
edges: edges
};
var options = {
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 =
'Selection: ' + graph.getSelection();
});
}
</script>
</head>
<body onload="draw();">
<h2>Clustering - Fully random graph</h2>
<div style="width:700px; font-size:14px;">
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 done automatically 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>
<br />
<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>
<div id="mygraph"></div>
<p id="selection"></p>
</body>
</html>

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

@ -0,0 +1,142 @@
<!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();">
<h2>Clustering - Scale-Free-Graph</h2>
<div style="width:700px; font-size:14px;">
This example shows therandomly generated <b>scale-free-graph</b> set of nodes and connected edges from example 2.
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 done automatically 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>
<br />
<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>

+ 186
- 0
examples/graph/20_UI_example.html View File

@ -0,0 +1,186 @@
<!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;
}
table.legend_table {
font-size: 11px;
border-width:1px;
border-color:#d3d3d3;
border-style:solid;
}
table.legend_table,td {
border-width:1px;
border-color:#d3d3d3;
border-style:solid;
padding: 2px;
}
div.table_content {
width:80px;
text-align:center;
}
div.table_description {
width:100px;
}
</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 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
},
stabilize: false,
navigationUI: {
enabled: true
},
keyboardNavigation: {
enabled: true
}
};
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();">
<h2>UI - User Interface and Keyboad Navigation</h2>
<div style="width: 700px; font-size:14px;">
This example is the same as example 2, except for the UI that has been activated. The UI icons are described below. <br /><br />
<table class="legend_table">
<tr>
<td>Icons: </td>
<td><div class="table_content"><img src="img/UI_icons/uparrow.png" /> </div></td>
<td><div class="table_content"><img src="img/UI_icons/downarrow.png" /> </div></td>
<td><div class="table_content"><img src="img/UI_icons/leftarrow.png" /> </div></td>
<td><div class="table_content"><img src="img/UI_icons/rightarrow.png" /> </div></td>
<td><div class="table_content"><img src="img/UI_icons/plus.png" /> </div></td>
<td><div class="table_content"><img src="img/UI_icons/minus.png" /> </div></td>
<td><div class="table_content"><img src="img/UI_icons/zoomExtends.png" /> </div></td>
</tr>
<tr>
<td><div class="table_description">Keyboard shortcuts:</div></td>
<td><div class="table_content">Up arrow</div></td>
<td><div class="table_content">Down arrow</div></td>
<td><div class="table_content">Left arrow</div></td>
<td><div class="table_content">Right arrow</div></td>
<td><div class="table_content">=<br />[<br />Page up</div></td>
<td><div class="table_content">-<br />]<br />Page down</div></td>
<td><div class="table_content">None</div></td>
</tr>
<td><div class="table_description">Description:</div></td>
<td><div class="table_content">Move up</div></td>
<td><div class="table_content">Move down</div></td>
<td><div class="table_content">Move left</div></td>
<td><div class="table_content">Move right</div></td>
<td><div class="table_content">Zoom in</div></td>
<td><div class="table_content">Zoom out</div></td>
<td><div class="table_content">Zoom extends</div></td>
</tr>
</table>
<br />
Apart from clicking the icons, you can also navigate using the keyboard. The buttons are in table above.
Zoom Extends changes the zoom and position of the camera to encompass all visible nodes. The UI buttons can be toggled on or off
by pressing the U button on the keyboard.
</div>
<br />
<form onsubmit="draw(); return false;">
<label for="nodeCount">Number of nodes:</label>
<input id="nodeCount" type="text" value="25" style="width: 50px;">
<input type="submit" value="Go">
</form>
<br>
<div id="mygraph"></div>
<p id="selection"></p>
</body>
</html>

BIN
examples/graph/img/UI_icons/downarrow.png View File

Before After
Width: 30  |  Height: 30  |  Size: 4.4 KiB

BIN
examples/graph/img/UI_icons/leftarrow.png View File

Before After
Width: 30  |  Height: 30  |  Size: 4.4 KiB

BIN
examples/graph/img/UI_icons/minus.png View File

Before After
Width: 30  |  Height: 30  |  Size: 4.0 KiB

BIN
examples/graph/img/UI_icons/plus.png View File

Before After
Width: 30  |  Height: 30  |  Size: 4.2 KiB

BIN
examples/graph/img/UI_icons/rightarrow.png View File

Before After
Width: 30  |  Height: 30  |  Size: 4.4 KiB

BIN
examples/graph/img/UI_icons/uparrow.png View File

Before After
Width: 30  |  Height: 30  |  Size: 4.4 KiB

BIN
examples/graph/img/UI_icons/zoomExtends.png View File

Before After
Width: 30  |  Height: 30  |  Size: 4.4 KiB

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

@ -29,6 +29,9 @@
<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="20_UI_example.html">20_UI_example.html</a></p>
<p><a href="graphviz/graphviz_gallery.html">graphviz_gallery.html</a></p>
</div>

+ 4
- 2
package.json View File

@ -28,8 +28,10 @@
"devDependencies": {
"jake": "latest",
"jake-utils": "latest",
"browserify": "latest",
"browserify": "3.22",
"moment": "latest",
"hammerjs": "1.0.5"
"hammerjs": "1.0.5",
"mousetrap": "latest",
"node-watch": "latest"
}
}

+ 25
- 3
src/DataSet.js View File

@ -42,6 +42,7 @@ function DataSet (options) {
this.data = {}; // map with data indexed by id
this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
this.convert = {}; // field types by field name
this.showInternalIds = this.options.showInternalIds || false; // show internal ids with the get function
if (this.options.convert) {
for (var field in this.options.convert) {
@ -275,6 +276,7 @@ DataSet.prototype.update = function (data, senderId) {
*/
DataSet.prototype.get = function (args) {
var me = this;
var globalShowInternalIds = this.showInternalIds;
// parse the arguments
var id, ids, options, data;
@ -318,6 +320,13 @@ DataSet.prototype.get = function (args) {
type = 'Array';
}
// we allow the setting of this value for a single get request.
if (options != undefined) {
if (options.showInternalIds != undefined) {
this.showInternalIds = options.showInternalIds;
}
}
// build options
var convert = options && options.convert || this.options.convert;
var filter = options && options.filter;
@ -352,6 +361,9 @@ DataSet.prototype.get = function (args) {
}
}
// restore the global value of showInternalIds
this.showInternalIds = globalShowInternalIds;
// order the results
if (options && options.order && id == undefined) {
this._sort(items, options.order);
@ -831,7 +843,7 @@ DataSet.prototype._getItem = function (id, convert) {
if (raw.hasOwnProperty(field)) {
value = raw[field];
// output all fields, except internal ids
if ((field != fieldId) || !(value in internalIds)) {
if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
converted[field] = util.convert(value, convert[field]);
}
}
@ -843,13 +855,12 @@ DataSet.prototype._getItem = function (id, convert) {
if (raw.hasOwnProperty(field)) {
value = raw[field];
// output all fields, except internal ids
if ((field != fieldId) || !(value in internalIds)) {
if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
converted[field] = value;
}
}
}
}
return converted;
};
@ -883,6 +894,17 @@ DataSet.prototype._updateItem = function (item) {
return id;
};
/**
* check if an id is an internal or external id
* @param id
* @returns {boolean}
* @private
*/
DataSet.prototype.isInternalId = function(id) {
return (id in this.internalIds);
};
/**
* Get an array with the column names of a Google DataTable
* @param {DataTable} dataTable

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


+ 41
- 23
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;
@ -35,6 +35,12 @@ function Edge (properties, graph, constants) {
this.from = null; // a node
this.to = null; // a node
// 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.connected = false;
// Added to support dashed lines
@ -48,6 +54,7 @@ function Edge (properties, graph, constants) {
this.lengthFixed = false;
this.setProperties(properties, constants);
}
/**
@ -60,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
@ -262,10 +269,10 @@ Edge.prototype._drawLine = function(ctx) {
*/
Edge.prototype._getLineWidth = function() {
if (this.from.selected || this.to.selected) {
return Math.min(this.width * 2, this.widthMax);
return Math.min(this.width * 2, this.widthMax)*this.graphScaleInv;
}
else {
return this.width;
return this.width*this.graphScaleInv;
}
};
@ -343,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]);
@ -600,3 +607,14 @@ Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
return Math.sqrt(dx*dx + dy*dy);
};
/**
* This allows the zoom level of the graph to influence the rendering
*
* @param scale
*/
Edge.prototype.setScale = function(scale) {
this.graphScaleInv = 1.0/scale;
};

+ 741
- 408
src/graph/Graph.js
File diff suppressed because it is too large
View File


+ 337
- 46
src/graph/Node.js View File

@ -26,6 +26,8 @@ function Node(properties, imagelist, grouplist, constants) {
this.selected = false;
this.edges = []; // all edges connected to this node
this.dynamicEdges = [];
this.reroutedEdges = {};
this.group = constants.nodes.group;
this.fontSize = constants.nodes.fontSize;
@ -42,24 +44,54 @@ function Node(properties, imagelist, grouplist, constants) {
this.y = 0;
this.xFixed = false;
this.yFixed = false;
this.horizontalAlignLeft = true; // these are for the navigationUI
this.verticalAlignTop = true; // these are for the navigationUI
this.radius = constants.nodes.radius;
this.baseRadiusValue = constants.nodes.radius;
this.radiusFixed = false;
this.radiusMin = constants.nodes.radiusMin;
this.radiusMax = constants.nodes.radiusMax;
this.imagelist = imagelist;
this.grouplist = grouplist;
this.setProperties(properties, constants);
// creating the variables for clustering
this.resetCluster();
this.dynamicEdgesLength = 0;
this.clusterSession = 0;
this.clusterSizeWidthFactor = constants.clustering.clusterSizeWidthFactor;
this.clusterSizeHeightFactor = constants.clustering.clusterSizeHeightFactor;
this.clusterSizeRadiusFactor = constants.clustering.clusterSizeRadiusFactor;
// mass, force, velocity
this.mass = 50; // kg (mass is adjusted for the number of connected edges)
this.mass = 1; // kg (mass is adjusted for the number of connected edges)
this.fx = 0.0; // external force x
this.fy = 0.0; // external force y
this.vx = 0.0; // velocity x
this.vy = 0.0; // velocity y
this.minForce = constants.minForce;
this.damping = 0.9; // damping factor
this.damping = 0.9;
this.dampingFactor = 75;
this.graphScaleInv = 1;
this.canvasTopLeft = {"x": -300, "y": -300};
this.canvasBottomRight = {"x": 300, "y": 300};
}
/**
* (re)setting the clustering variables and objects
*/
Node.prototype.resetCluster = function() {
// clustering variables
this.formationScale = undefined; // this is used to determine when to open the cluster
this.clusterSize = 1; // this signifies the total amount of nodes in this cluster
this.containedNodes = {};
this.containedEdges = {};
this.clusterSessions = [];
};
/**
@ -70,6 +102,10 @@ Node.prototype.attachEdge = function(edge) {
if (this.edges.indexOf(edge) == -1) {
this.edges.push(edge);
}
if (this.dynamicEdges.indexOf(edge) == -1) {
this.dynamicEdges.push(edge);
}
this.dynamicEdgesLength = this.dynamicEdges.length;
this._updateMass();
};
@ -81,7 +117,9 @@ Node.prototype.detachEdge = function(edge) {
var index = this.edges.indexOf(edge);
if (index != -1) {
this.edges.splice(index, 1);
this.dynamicEdges.splice(index, 1);
}
this.dynamicEdgesLength = this.dynamicEdges.length;
this._updateMass();
};
@ -91,7 +129,7 @@ Node.prototype.detachEdge = function(edge) {
* @private
*/
Node.prototype._updateMass = function() {
this.mass = 50 + 20 * this.edges.length; // kg
this.mass = 1 + 0.6 * this.edges.length; // kg
};
/**
@ -103,15 +141,20 @@ Node.prototype.setProperties = function(properties, constants) {
if (!properties) {
return;
}
this.originalLabel = undefined;
// basic properties
if (properties.id != undefined) {this.id = properties.id;}
if (properties.label != undefined) {this.label = properties.label;}
if (properties.title != undefined) {this.title = properties.title;}
if (properties.group != undefined) {this.group = properties.group;}
if (properties.x != undefined) {this.x = properties.x;}
if (properties.y != undefined) {this.y = properties.y;}
if (properties.value != undefined) {this.value = properties.value;}
if (properties.id !== undefined) {this.id = properties.id;}
if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = properties.label;}
if (properties.title !== undefined) {this.title = properties.title;}
if (properties.group !== undefined) {this.group = properties.group;}
if (properties.x !== undefined) {this.x = properties.x;}
if (properties.y !== undefined) {this.y = properties.y;}
if (properties.value !== undefined) {this.value = properties.value;}
// navigationUI properties
if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;}
if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;}
if (this.id === undefined) {
throw "Node must have an id";
@ -128,17 +171,16 @@ Node.prototype.setProperties = function(properties, constants) {
}
// individual shape properties
if (properties.shape != undefined) {this.shape = properties.shape;}
if (properties.image != undefined) {this.image = properties.image;}
if (properties.radius != undefined) {this.radius = properties.radius;}
if (properties.color != undefined) {this.color = Node.parseColor(properties.color);}
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.shape !== undefined) {this.shape = properties.shape;}
if (properties.image !== undefined) {this.image = properties.image;}
if (properties.radius !== undefined) {this.radius = properties.radius;}
if (properties.color !== undefined) {this.color = Node.parseColor(properties.color);}
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 (this.image != undefined) {
if (this.image !== undefined) {
if (this.imagelist) {
this.imageObj = this.imagelist.load(this.image);
}
@ -147,9 +189,9 @@ Node.prototype.setProperties = function(properties, constants) {
}
}
this.xFixed = this.xFixed || (properties.x != undefined);
this.yFixed = this.yFixed || (properties.y != undefined);
this.radiusFixed = this.radiusFixed || (properties.radius != undefined);
this.xFixed = this.xFixed || (properties.x !== undefined);
this.yFixed = this.yFixed || (properties.y !== undefined);
this.radiusFixed = this.radiusFixed || (properties.radius !== undefined);
if (this.shape == 'image') {
this.radiusMin = constants.nodes.widthMin;
@ -172,7 +214,6 @@ Node.prototype.setProperties = function(properties, constants) {
case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
}
// reset the size of the node, this can be changed
this._reset();
};
@ -200,6 +241,7 @@ Node.parseColor = function(color) {
c = {};
c.background = color.background || 'white';
c.border = color.border || c.background;
if (util.isString(color.highlight)) {
c.highlight = {
border: color.highlight,
@ -212,6 +254,7 @@ Node.parseColor = function(color) {
c.highlight.border = color.highlight && color.highlight.border || c.border;
}
}
return c;
};
@ -231,6 +274,14 @@ Node.prototype.unselect = function() {
this._reset();
};
/**
* Reset the calculated size of the node, forces it to recalculate its size
*/
Node.prototype.clearSizeCache = function() {
this._reset();
};
/**
* Reset the calculated size of the node, forces it to recalculate its size
* @private
@ -327,15 +378,15 @@ Node.prototype.discreteStep = function(interval) {
if (!this.xFixed) {
var dx = -this.damping * this.vx; // damping force
var ax = (this.fx + dx) / this.mass; // acceleration
this.vx += ax / interval; // velocity
this.x += this.vx / interval; // position
this.vx += ax * interval; // velocity
this.x += this.vx * interval; // position
}
if (!this.yFixed) {
var dy = -this.damping * this.vy; // damping force
var ay = (this.fy + dy) / this.mass; // acceleration
this.vy += ay / interval; // velocity
this.y += this.vy / interval; // position
this.vy += ay * interval; // velocity
this.y += this.vy * interval; // position
}
};
@ -355,9 +406,14 @@ Node.prototype.isFixed = function() {
*/
// TODO: replace this method with calculating the kinetic energy
Node.prototype.isMoving = function(vmin) {
return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin ||
(!this.xFixed && Math.abs(this.fx) > this.minForce) ||
(!this.yFixed && Math.abs(this.fy) > this.minForce));
if (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin) {
return true;
}
else {
this.vx = 0; this.vy = 0;
return false;
}
//return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin);
};
/**
@ -405,6 +461,7 @@ Node.prototype.setValueRange = function(min, max) {
this.radius = (this.value - min) * scale + this.radiusMin;
}
}
this.baseRadiusValue = this.radius;
};
/**
@ -431,20 +488,28 @@ Node.prototype.resize = function(ctx) {
* @return {boolean} True if location is located on node
*/
Node.prototype.isOverlappingWith = function(obj) {
return (this.left < obj.right &&
this.left + this.width > obj.left &&
this.top < obj.bottom &&
this.top + this.height > obj.top);
return (this.left < obj.right &&
this.left + this.width > obj.left &&
this.top < obj.bottom &&
this.top + this.height > obj.top);
};
Node.prototype._resizeImage = function (ctx) {
// TODO: pre calculate the image size
if (!this.width) { // undefined or 0
if (!this.width || !this.height) { // undefined or 0
var width, height;
if (this.value) {
this.radius = this.baseRadiusValue;
var scale = this.imageObj.height / this.imageObj.width;
width = this.radius || this.imageObj.width;
height = this.radius * scale || this.imageObj.height;
if (scale !== undefined) {
width = this.radius || this.imageObj.width;
height = this.radius * scale || this.imageObj.height;
}
else {
width = 0;
height = 0;
}
}
else {
width = this.imageObj.width;
@ -452,7 +517,14 @@ Node.prototype._resizeImage = function (ctx) {
}
this.width = width;
this.height = height;
if (this.width > 0 && this.height > 0) {
this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor;
this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor;
this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor;
}
}
};
Node.prototype._drawImage = function (ctx) {
@ -462,7 +534,19 @@ Node.prototype._drawImage = function (ctx) {
this.top = this.y - this.height / 2;
var yLabel;
if (this.imageObj) {
if (this.imageObj.width != 0 ) {
// draw the shade
if (this.clusterSize > 1) {
var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0);
lineWidth *= this.graphScaleInv;
lineWidth = Math.min(0.2 * this.width,lineWidth);
ctx.globalAlpha = 0.5;
ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth);
}
// draw the image
ctx.globalAlpha = 1.0;
ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
yLabel = this.y + this.height / 2;
}
@ -481,6 +565,10 @@ Node.prototype._resizeBox = function (ctx) {
var textSize = this.getTextSize(ctx);
this.width = textSize.width + 2 * margin;
this.height = textSize.height + 2 * margin;
this.width += (this.clusterSize - 1) * 0.5 * this.clusterSizeWidthFactor;
this.height += (this.clusterSize - 1) * 0.5 * this.clusterSizeHeightFactor;
// this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor;
}
};
@ -490,9 +578,26 @@ Node.prototype._drawBox = function (ctx) {
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
var clusterLineWidth = 2.5;
var selectionLineWidth = 2;
ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
// draw the outer border
if (this.clusterSize > 1) {
ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
ctx.lineWidth *= this.graphScaleInv;
ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.radius);
ctx.stroke();
}
ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
ctx.lineWidth *= this.graphScaleInv;
ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
ctx.lineWidth = this.selected ? 2.0 : 1.0;
ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
ctx.fill();
ctx.stroke();
@ -508,6 +613,11 @@ Node.prototype._resizeDatabase = function (ctx) {
var size = textSize.width + 2 * margin;
this.width = size;
this.height = size;
// scaling used for clustering
this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor;
this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor;
this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor;
}
};
@ -516,9 +626,25 @@ Node.prototype._drawDatabase = function (ctx) {
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
var clusterLineWidth = 2.5;
var selectionLineWidth = 2;
ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
// draw the outer border
if (this.clusterSize > 1) {
ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
ctx.lineWidth *= this.graphScaleInv;
ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
ctx.database(this.x - this.width/2 - 2*ctx.lineWidth, this.y - this.height*0.5 - 2*ctx.lineWidth, this.width + 4*ctx.lineWidth, this.height + 4*ctx.lineWidth);
ctx.stroke();
}
ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
ctx.lineWidth *= this.graphScaleInv;
ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
ctx.lineWidth = this.selected ? 2.0 : 1.0;
ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
ctx.fill();
ctx.stroke();
@ -536,6 +662,11 @@ Node.prototype._resizeCircle = function (ctx) {
this.width = diameter;
this.height = diameter;
// scaling used for clustering
// this.width += (this.clusterSize - 1) * 0.5 * this.clusterSizeWidthFactor;
// this.height += (this.clusterSize - 1) * 0.5 * this.clusterSizeHeightFactor;
this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor;
}
};
@ -544,9 +675,25 @@ Node.prototype._drawCircle = function (ctx) {
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
var clusterLineWidth = 2.5;
var selectionLineWidth = 2;
ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
// draw the outer border
if (this.clusterSize > 1) {
ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
ctx.lineWidth *= this.graphScaleInv;
ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
ctx.circle(this.x, this.y, this.radius+2*ctx.lineWidth);
ctx.stroke();
}
ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
ctx.lineWidth *= this.graphScaleInv;
ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
ctx.lineWidth = this.selected ? 2.0 : 1.0;
ctx.circle(this.x, this.y, this.radius);
ctx.fill();
ctx.stroke();
@ -563,6 +710,11 @@ Node.prototype._resizeEllipse = function (ctx) {
if (this.width < this.height) {
this.width = this.height;
}
// scaling used for clustering
this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor;
this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor;
this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor;
}
};
@ -571,13 +723,29 @@ Node.prototype._drawEllipse = function (ctx) {
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
var clusterLineWidth = 2.5;
var selectionLineWidth = 2;
ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
// draw the outer border
if (this.clusterSize > 1) {
ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
ctx.lineWidth *= this.graphScaleInv;
ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth);
ctx.stroke();
}
ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
ctx.lineWidth *= this.graphScaleInv;
ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
ctx.lineWidth = this.selected ? 2.0 : 1.0;
ctx.ellipse(this.left, this.top, this.width, this.height);
ctx.fill();
ctx.stroke();
this._label(ctx, this.label, this.x, this.y);
};
@ -603,9 +771,15 @@ Node.prototype._drawStar = function (ctx) {
Node.prototype._resizeShape = function (ctx) {
if (!this.width) {
this.radius = this.baseRadiusValue;
var size = 2 * this.radius;
this.width = size;
this.height = size;
// scaling used for clustering
this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor;
this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor;
this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor;
}
};
@ -615,9 +789,35 @@ Node.prototype._drawShape = function (ctx, shape) {
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
var clusterLineWidth = 2.5;
var selectionLineWidth = 2;
var radiusMultiplier = 2;
// choose draw method depending on the shape
switch (shape) {
case 'dot': radiusMultiplier = 2; break;
case 'square': radiusMultiplier = 2; break;
case 'triangle': radiusMultiplier = 3; break;
case 'triangleDown': radiusMultiplier = 3; break;
case 'star': radiusMultiplier = 4; break;
}
ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
// draw the outer border
if (this.clusterSize > 1) {
ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
ctx.lineWidth *= this.graphScaleInv;
ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
ctx[shape](this.x, this.y, this.radius + radiusMultiplier * ctx.lineWidth);
ctx.stroke();
}
ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
ctx.lineWidth *= this.graphScaleInv;
ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
ctx.lineWidth = this.selected ? 2.0 : 1.0;
ctx[shape](this.x, this.y, this.radius);
ctx.fill();
@ -634,6 +834,11 @@ Node.prototype._resizeText = function (ctx) {
var textSize = this.getTextSize(ctx);
this.width = textSize.width + 2 * margin;
this.height = textSize.height + 2 * margin;
// scaling used for clustering
this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor;
this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor;
this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor;
}
};
@ -667,7 +872,7 @@ Node.prototype._label = function (ctx, text, x, y, align, baseline) {
Node.prototype.getTextSize = function(ctx) {
if (this.label != undefined) {
if (this.label !== undefined) {
ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
var lines = this.label.split('\n'),
@ -684,3 +889,89 @@ Node.prototype.getTextSize = function(ctx) {
return {"width": 0, "height": 0};
}
};
/**
* this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn.
* there is a safety margin of 0.3 * width;
*
* @returns {boolean}
*/
Node.prototype.inArea = function() {
if (this.width !== undefined) {
return (this.x + this.width*this.graphScaleInv >= this.canvasTopLeft.x &&
this.x - this.width*this.graphScaleInv < this.canvasBottomRight.x &&
this.y + this.height*this.graphScaleInv >= this.canvasTopLeft.y &&
this.y - this.height*this.graphScaleInv < this.canvasBottomRight.y);
}
else {
return true;
}
}
/**
* checks if the core of the node is in the display area, this is used for opening clusters around zoom
* @returns {boolean}
*/
Node.prototype.inView = function() {
return (this.x >= this.canvasTopLeft.x &&
this.x < this.canvasBottomRight.x &&
this.y >= this.canvasTopLeft.y &&
this.y < this.canvasBottomRight.y);
}
/**
* This allows the zoom level of the graph to influence the rendering
* We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas
*
* @param scale
* @param canvasTopLeft
* @param canvasBottomRight
*/
Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) {
this.graphScaleInv = 1.0/scale;
this.canvasTopLeft = canvasTopLeft;
this.canvasBottomRight = canvasBottomRight;
};
/**
* This allows the zoom level of the graph to influence the rendering
*
* @param scale
*/
Node.prototype.setScale = function(scale) {
this.graphScaleInv = 1.0/scale;
};
/**
* This function updates the damping parameter for clusters, based ont he
*
* @param {Number} numberOfNodes
*/
Node.prototype.updateDamping = function(numberOfNodes) {
this.damping = (0.8 + 0.1*this.clusterSize * (1 + Math.pow(numberOfNodes,-2)));
this.damping *= this.dampingFactor;
};
/**
* set the velocity at 0. Is called when this node is contained in another during clustering
*/
Node.prototype.clearVelocity = function() {
this.vx = 0;
this.vy = 0;
};
/**
* Basic preservation of (kinectic) energy
*
* @param massBeforeClustering
*/
Node.prototype.updateVelocity = function(massBeforeClustering) {
var energyBefore = this.vx * this.vx * massBeforeClustering;
this.vx = Math.sqrt(energyBefore/this.mass);
energyBefore = this.vy * this.vy * massBeforeClustering;
this.vy = Math.sqrt(energyBefore/this.mass);
};

+ 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

+ 547
- 0
src/graph/SectorsMixin.js View File

@ -0,0 +1,547 @@
/**
* 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 = {
/**
* This function is only called by the setData function of the Graph object.
* This loads the global references into the active sector. This initializes the sector.
*
* @private
*/
_putDataInSector : function() {
this.sectors["active"][this._sector()].nodes = this.nodes;
this.sectors["active"][this._sector()].edges = this.edges;
this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices;
},
/**
* /**
* 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} [sectorType] | "active" or "frozen"
* @private
*/
_switchToSector : function(sectorId, sectorType) {
if (sectorType === undefined || sectorType == "active") {
this._switchToActiveSector(sectorId);
}
else {
this._switchToFrozenSector(sectorId);
}
},
/**
* This function sets the global references to nodes, edges and nodeIndices back to
* those of the supplied active sector.
*
* @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"];
},
/**
* This function sets the global references to nodes, edges and nodeIndices back to
* those of the supplied frozen sector.
*
* @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"];
},
/**
* This function sets the global references to nodes, edges and nodeIndices to
* those of the navigationUI sector.
*
* @private
*/
_switchToUISector : function() {
this.nodeIndices = this.sectors["navigationUI"]["nodeIndices"];
this.nodes = this.sectors["navigationUI"]["nodes"];
this.edges = this.sectors["navigationUI"]["edges"];
},
/**
* This function sets the global references to nodes, edges and nodeIndices back to
* those of the currently active sector.
*
* @private
*/
_loadLatestSector : function() {
this._switchToSector(this._sector());
},
/**
* This function returns the currently active sector Id
*
* @returns {String}
* @private
*/
_sector : function() {
return this.activeSector[this.activeSector.length-1];
},
/**
* This function returns the previously active sector Id
*
* @returns {String}
* @private
*/
_previousSector : function() {
if (this.activeSector.length > 1) {
return this.activeSector[this.activeSector.length-2];
}
else {
throw new TypeError('there are not enough sectors in the this.activeSector array.');
}
},
/**
* We add the active sector at the end of the this.activeSector array
* 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
* @private
*/
_setActiveSector : function(newId) {
this.activeSector.push(newId);
},
/**
* We remove the currently active sector id from the active sector stack. This happens when
* we reactivate the previously active sector
*
* @private
*/
_forgetLastSector : function() {
this.activeSector.pop();
},
/**
* 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
* @private
*/
_createNewSector : function(newId) {
// create the new sector
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,
color: {
background: "#eaefef",
border: "495c5e"
}
},{},{},this.constants);
this.sectors["active"][newId]['drawingNode'].clusterSize = 2;
},
/**
* 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
* @private
*/
_deleteActiveSector : function(sectorId) {
delete this.sectors["active"][sectorId];
},
/**
* 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
* @private
*/
_deleteFrozenSector : function(sectorId) {
delete this.sectors["frozen"][sectorId];
},
/**
* 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
* @private
*/
_freezeSector : function(sectorId) {
// we move the set references from the active to the frozen stack.
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 is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen"
* object to the "active" object.
*
* @param sectorId
* @private
*/
_activateSector : function(sectorId) {
// we move the set references from the frozen to the active stack.
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 function merges the data from the currently active sector with a frozen sector. This is used
* in the process of reverting back to the previously active sector.
* 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
* @private
*/
_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];
}
}
// 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];
}
}
// merge the nodeIndices
for (var i = 0; i < this.nodeIndices.length; i++) {
this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]);
}
},
/**
* This clusters the sector to one cluster. It was a single cluster before this process started so
* we revert to that state. The clusterToFit function with a maximum size of 1 node does this.
*
* @private
*/
_collapseThisToSingleCluster : function() {
this.clusterToFit(1,false);
},
/**
* We create a new active sector from the node that we want to open.
*
* @param node
* @private
*/
_addSector : function(node) {
// this is the currently active sector
var sector = this._sector();
// // 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];
var unqiueIdentifier = util.randomUUID();
// 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
this._createNewSector(unqiueIdentifier);
// we add the active sector to the sectors array to be able to revert these steps later on
this._setActiveSector(unqiueIdentifier);
// we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier
this._switchToSector(this._sector());
// finally we add the node we removed from our previous active sector to the new active sector
this.nodes[node.id] = node;
},
/**
* We close the sector that is currently open and revert back to the one before.
* If the active sector is the "default" sector, nothing happens.
*
* @private
*/
_collapseSector : function() {
// the currently active sector
var sector = this._sector();
// we cannot collapse the default sector
if (sector != "default") {
if ((this.nodeIndices.length == 1) ||
(this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
(this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
var previousSector = this._previousSector();
// we collapse the sector back to a single cluster
this._collapseThisToSingleCluster();
// we move the remaining nodes, edges and nodeIndices to the previous sector.
// This previous sector is the one we will reactivate
this._mergeThisWithFrozen(previousSector);
// the previously active (frozen) sector now has all the data from the currently active sector.
// we can now delete the active sector.
this._deleteActiveSector(sector);
// we activate the previously active (and currently frozen) sector.
this._activateSector(previousSector);
// we load the references from the newly active sector into the global references
this._switchToSector(previousSector);
// we forget the previously active sector because we reverted to the one before
this._forgetLastSector();
// finally, we update the node index list.
this._updateNodeIndexList();
}
}
},
/**
* This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
*
* @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 {*} [argument] | Optional: arguments to pass to the runFunction
* @private
*/
_doInAllActiveSectors : function(runFunction,argument) {
if (argument === undefined) {
for (var sector in this.sectors["active"]) {
if (this.sectors["active"].hasOwnProperty(sector)) {
// switch the global references to those of this sector
this._switchToActiveSector(sector);
this[runFunction]();
}
}
}
else {
for (var sector in this.sectors["active"]) {
if (this.sectors["active"].hasOwnProperty(sector)) {
// switch the global references to those of this sector
this._switchToActiveSector(sector);
var args = Array.prototype.splice.call(arguments, 1);
if (args.length > 1) {
this[runFunction](args[0],args[1]);
}
else {
this[runFunction](argument);
}
}
}
}
// we revert the global references back to our active sector
this._loadLatestSector();
},
/**
* This runs a function in all frozen sectors. This is used in the _redraw().
*
* @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
* | we don't pass the function itself because then the "this" is the window object
* | instead of the Graph object
* @param {*} [argument] | Optional: arguments to pass to the runFunction
* @private
*/
_doInAllFrozenSectors : function(runFunction,argument) {
if (argument === undefined) {
for (var sector in this.sectors["frozen"]) {
if (this.sectors["frozen"].hasOwnProperty(sector)) {
// switch the global references to those of this sector
this._switchToFrozenSector(sector);
this[runFunction]();
}
}
}
else {
for (var sector in this.sectors["frozen"]) {
if (this.sectors["frozen"].hasOwnProperty(sector)) {
// switch the global references to those of this sector
this._switchToFrozenSector(sector);
var args = Array.prototype.splice.call(arguments, 1);
if (args.length > 1) {
this[runFunction](args[0],args[1]);
}
else {
this[runFunction](argument);
}
}
}
}
this._loadLatestSector();
},
/**
* This runs a function in the navigationUI sector.
*
* @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
* | we don't pass the function itself because then the "this" is the window object
* | instead of the Graph object
* @param {*} [argument] | Optional: arguments to pass to the runFunction
* @private
*/
_doInUISector : function(runFunction,argument) {
this._switchToUISector();
if (argument === undefined) {
this[runFunction]();
}
else {
var args = Array.prototype.splice.call(arguments, 1);
if (args.length > 1) {
this[runFunction](args[0],args[1]);
}
else {
this[runFunction](argument);
}
}
this._loadLatestSector();
},
/**
* This runs a function in all sectors. This is used in the _redraw().
*
* @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
* | we don't pass the function itself because then the "this" is the window object
* | instead of the Graph object
* @param {*} [argument] | Optional: arguments to pass to the runFunction
* @private
*/
_doInAllSectors : function(runFunction,argument) {
var args = Array.prototype.splice.call(arguments, 1);
if (argument === undefined) {
this._doInAllActiveSectors(runFunction);
this._doInAllFrozenSectors(runFunction);
}
else {
if (args.length > 1) {
this._doInAllActiveSectors(runFunction,args[0],args[1]);
this._doInAllFrozenSectors(runFunction,args[0],args[1]);
}
else {
this._doInAllActiveSectors(runFunction,argument);
this._doInAllFrozenSectors(runFunction,argument);
}
}
},
/**
* This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the
* active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it.
*
* @private
*/
_clearNodeIndexList : function() {
var sector = this._sector();
this.sectors["active"][sector]["nodeIndices"] = [];
this.nodeIndices = this.sectors["active"][sector]["nodeIndices"];
},
/**
* Draw the encompassing sector node
*
* @param ctx
* @param sectorType
* @private
*/
_drawSectorNodes : function(ctx,sectorType) {
var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
for (var sector in this.sectors[sectorType]) {
if (this.sectors[sectorType].hasOwnProperty(sector)) {
if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) {
this._switchToSector(sector,sectorType);
minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9;
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;}
if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;}
if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;}
}
}
node = this.sectors[sectorType][sector]["drawingNode"];
node.x = 0.5 * (maxX + minX);
node.y = 0.5 * (maxY + minY);
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);
}
}
}
},
_drawAllSectorNodes : function(ctx) {
this._drawSectorNodes(ctx,"frozen");
this._drawSectorNodes(ctx,"active");
this._loadLatestSector();
}
};

+ 487
- 0
src/graph/SelectionMixin.js View File

@ -0,0 +1,487 @@
var SelectionMixin = {
/**
* This function can be called from the _doInAllSectors function
*
* @param object
* @param overlappingNodes
* @private
*/
_getNodesOverlappingWith : function(object, overlappingNodes) {
var nodes = this.nodes;
for (var nodeId in nodes) {
if (nodes.hasOwnProperty(nodeId)) {
if (nodes[nodeId].isOverlappingWith(object)) {
overlappingNodes.push(nodeId);
}
}
}
},
/**
* retrieve all nodes overlapping with given object
* @param {Object} object An object with parameters left, top, right, bottom
* @return {Number[]} An array with id's of the overlapping nodes
* @private
*/
_getAllNodesOverlappingWith : function (object) {
var overlappingNodes = [];
this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes);
return overlappingNodes;
},
/**
* retrieve all nodes in the navigationUI overlapping with given object
* @param {Object} object An object with parameters left, top, right, bottom
* @return {Number[]} An array with id's of the overlapping nodes
* @private
*/
_getAllUINodesOverlappingWith : function (object) {
var overlappingNodes = [];
this._doInUISector("_getNodesOverlappingWith",object,overlappingNodes);
return overlappingNodes;
},
/**
* Return a position object in canvasspace from a single point in screenspace
*
* @param pointer
* @returns {{left: number, top: number, right: number, bottom: number}}
* @private
*/
_pointerToPositionObject : function(pointer) {
var x = this._canvasToX(pointer.x);
var y = this._canvasToY(pointer.y);
return {left: x,
top: y,
right: x,
bottom: y};
},
/**
* Return a position object in canvasspace from a single point in screenspace
*
* @param pointer
* @returns {{left: number, top: number, right: number, bottom: number}}
* @private
*/
_pointerToScreenPositionObject : function(pointer) {
var x = pointer.x;
var y = pointer.y;
return {left: x,
top: y,
right: x,
bottom: y};
},
/**
* Get the top navigationUI node at the a specific point (like a click)
*
* @param {{x: Number, y: Number}} pointer
* @return {Node | null} node
* @private
*/
_getUINodeAt : function (pointer) {
var screenPositionObject = this._pointerToScreenPositionObject(pointer);
var overlappingNodes = this._getAllUINodesOverlappingWith(screenPositionObject);
if (this.UIvisible && overlappingNodes.length > 0) {
return this.sectors["navigationUI"]["nodes"][overlappingNodes[overlappingNodes.length - 1]];
}
else {
return null;
}
},
/**
* Get the top node at the a specific point (like a click)
*
* @param {{x: Number, y: Number}} pointer
* @return {Node | null} node
* @private
*/
_getNodeAt : function (pointer) {
// we first check if this is an navigationUI element
var positionObject = this._pointerToPositionObject(pointer);
overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
// if there are overlapping nodes, select the last one, this is the
// one which is drawn on top of the others
if (overlappingNodes.length > 0) {
return this.nodes[overlappingNodes[overlappingNodes.length - 1]];
}
else {
return null;
}
},
/**
* Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call
* _getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
*
* @param pointer
* @returns {null}
* @private
*/
_getEdgeAt : function(pointer) {
return null;
},
/**
* Add object to the selection array. The this.selection id array may not be needed.
*
* @param obj
* @private
*/
_addToSelection : function(obj) {
this.selection.push(obj.id);
this.selectionObj[obj.id] = obj;
},
/**
* Remove a single option from selection.
*
* @param obj
* @private
*/
_removeFromSelection : function(obj) {
for (var i = 0; i < this.selection.length; i++) {
if (obj.id == this.selection[i]) {
this.selection.splice(i,1);
break;
}
}
delete this.selectionObj[obj.id];
},
/**
* Unselect all. The selectionObj is useful for this.
*
* @private
*/
_unselectAll : function() {
this.selection = [];
for (var objId in this.selectionObj) {
if (this.selectionObj.hasOwnProperty(objId)) {
this.selectionObj[objId].unselect();
}
}
this.selectionObj = {};
this._trigger('select');
},
/**
* Check if anything is selected
*
* @returns {boolean}
* @private
*/
_selectionIsEmpty : function() {
if (this.selection.length == 0) {
return true;
}
else {
return false;
}
},
/**
* This is called when someone clicks on a node. either select or deselect it.
* If there is an existing selection and we don't want to append to it, clear the existing selection
*
* @param {Node} node
* @param {Boolean} append
* @private
*/
_selectNode : function(node, append) {
if (this._selectionIsEmpty() == false && append == false) {
this._unselectAll();
}
if (node.selected == false) {
node.select();
this._addToSelection(node);
}
else {
node.unselect();
this._removeFromSelection(node);
}
this._trigger('select');
},
/**
* handles the selection part of the touch, only for navigationUI elements;
* Touch is triggered before tap, also before hold. Hold triggers after a while.
* This is the most responsive solution
*
* @param {Object} pointer
* @private
*/
_handleTouch : function(pointer) {
var node = this._getUINodeAt(pointer);
if (node != null) {
if (this[node.triggerFunction] !== undefined) {
this[node.triggerFunction]();
}
}
},
/**
* handles the selection part of the tap;
*
* @param {Object} pointer
* @private
*/
_handleTap : function(pointer) {
var node = this._getNodeAt(pointer);
if (node != null) {
this._selectNode(node,false);
}
else {
this._unselectAll();
}
this._redraw();
},
/**
* handles the selection part of the double tap and opens a cluster if needed
*
* @param {Object} pointer
* @private
*/
_handleDoubleTap : function(pointer) {
var node = this._getNodeAt(pointer);
if (node != null && node !== undefined) {
// we reset the areaCenter here so the opening of the node will occur
this.areaCenter = {"x" : this._canvasToX(pointer.x),
"y" : this._canvasToY(pointer.y)};
this.openCluster(node);
}
},
/**
* Handle the onHold selection part
*
* @param pointer
* @private
*/
_handleOnHold : function(pointer) {
var node = this._getNodeAt(pointer);
if (node != null) {
this._selectNode(node,true);
}
this._redraw();
},
/**
* handle the onRelease event. These functions are here for the navigationUI module.
*
* @private
*/
_handleOnRelease : function() {
this.xIncrement = 0;
this.yIncrement = 0;
this.zoomIncrement = 0;
this._unHighlightAll();
},
/**
* * // TODO: rework this function, it is from the old system
*
* retrieve the currently selected nodes
* @return {Number[] | String[]} selection An array with the ids of the
* selected nodes.
*/
getSelection : function() {
return this.selection.concat([]);
},
/**
* // TODO: rework this function, it is from the old system
*
* select zero or more nodes
* @param {Number[] | String[]} selection An array with the ids of the
* selected nodes.
*/
setSelection : function(selection) {
var i, iMax, id;
if (!selection || (selection.length == undefined))
throw 'Selection must be an array with ids';
// first unselect any selected node
for (i = 0, iMax = this.selection.length; i < iMax; i++) {
id = this.selection[i];
this.nodes[id].unselect();
}
this.selection = [];
for (i = 0, iMax = selection.length; i < iMax; i++) {
id = selection[i];
var node = this.nodes[id];
if (!node) {
throw new RangeError('Node with id "' + id + '" not found');
}
node.select();
this.selection.push(id);
}
this.redraw();
},
/**
* TODO: rework this function, it is from the old system
*
* Validate the selection: remove ids of nodes which no longer exist
* @private
*/
_updateSelection : function () {
var i = 0;
while (i < this.selection.length) {
var nodeId = this.selection[i];
if (!this.nodes.hasOwnProperty(nodeId)) {
this.selection.splice(i, 1);
delete this.selectionObj[nodeId];
}
else {
i++;
}
}
}
/**
* Unselect selected nodes. If no selection array is provided, all nodes
* are unselected
* @param {Object[]} selection Array with selection objects, each selection
* object has a parameter row. Optional
* @param {Boolean} triggerSelect If true (default), the select event
* is triggered when nodes are unselected
* @return {Boolean} changed True if the selection is changed
* @private
*/
/* _unselectNodes : function(selection, triggerSelect) {
var changed = false;
var i, iMax, id;
if (selection) {
// remove provided selections
for (i = 0, iMax = selection.length; i < iMax; i++) {
id = selection[i];
if (this.nodes.hasOwnProperty(id)) {
this.nodes[id].unselect();
}
var j = 0;
while (j < this.selection.length) {
if (this.selection[j] == id) {
this.selection.splice(j, 1);
changed = true;
}
else {
j++;
}
}
}
}
else if (this.selection && this.selection.length) {
// remove all selections
for (i = 0, iMax = this.selection.length; i < iMax; i++) {
id = this.selection[i];
if (this.nodes.hasOwnProperty(id)) {
this.nodes[id].unselect();
}
changed = true;
}
this.selection = [];
}
if (changed && (triggerSelect == true || triggerSelect == undefined)) {
// fire the select event
this._trigger('select');
}
return changed;
},
*/
/**
* select all nodes on given location x, y
* @param {Array} selection an array with node ids
* @param {boolean} append If true, the new selection will be appended to the
* current selection (except for duplicate entries)
* @return {Boolean} changed True if the selection is changed
* @private
*/
/* _selectNodes : function(selection, append) {
var changed = false;
var i, iMax;
// TODO: the selectNodes method is a little messy, rework this
// check if the current selection equals the desired selection
var selectionAlreadyThere = true;
if (selection.length != this.selection.length) {
selectionAlreadyThere = false;
}
else {
for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) {
if (selection[i] != this.selection[i]) {
selectionAlreadyThere = false;
break;
}
}
}
if (selectionAlreadyThere) {
return changed;
}
if (append == undefined || append == false) {
// first deselect any selected node
var triggerSelect = false;
changed = this._unselectNodes(undefined, triggerSelect);
}
for (i = 0, iMax = selection.length; i < iMax; i++) {
// add each of the new selections, but only when they are not duplicate
var id = selection[i];
var isDuplicate = (this.selection.indexOf(id) != -1);
if (!isDuplicate) {
this.nodes[id].select();
this.selection.push(id);
changed = true;
}
}
if (changed) {
// fire the select event
this._trigger('select');
}
return changed;
},
*/
};

+ 258
- 0
src/graph/UIMixin.js View File

@ -0,0 +1,258 @@
/**
* Created by Alex on 1/22/14.
*/
var UIMixin = {
/**
* This function moves the navigationUI if the canvas size has been changed. If the arugments
* verticaAlignTop and horizontalAlignLeft are false, the correction will be made
*
* @private
*/
_relocateUI : function() {
if (this.sectors !== undefined) {
var xOffset = this.UIclientWidth - this.frame.canvas.clientWidth;
var yOffset = this.UIclientHeight - this.frame.canvas.clientHeight;
this.UIclientWidth = this.frame.canvas.clientWidth;
this.UIclientHeight = this.frame.canvas.clientHeight;
var node = null;
for (var nodeId in this.sectors["navigationUI"]["nodes"]) {
if (this.sectors["navigationUI"]["nodes"].hasOwnProperty(nodeId)) {
node = this.sectors["navigationUI"]["nodes"][nodeId];
if (!node.horizontalAlignLeft) {
node.x -= xOffset;
}
if (!node.verticalAlignTop) {
node.y -= yOffset;
}
}
}
}
},
/**
* Creation of the navigationUI nodes. They are drawn over the rest of the nodes and are not affected by scale and translation
* they have a triggerFunction which is called on click. If the position of the navigationUI is dependent
* on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false.
* This means that the location will be corrected by the _relocateUI function on a size change of the canvas.
*
* @private
*/
_loadUIElements : function() {
var DIR = this.constants.navigationUI.iconPath;
this.UIclientWidth = this.frame.canvas.clientWidth;
this.UIclientHeight = this.frame.canvas.clientHeight;
if (this.UIclientWidth === undefined) {
this.UIclientWidth = 0;
this.UIclientHeight = 0;
}
var offset = 15;
var intermediateOffset = 7;
var UINodes = [
{id: 'UI_up', shape: 'image', image: DIR + 'uparrow.png', triggerFunction: "_moveUp",
verticalAlignTop: false, x: 45 + offset + intermediateOffset, y: this.UIclientHeight - 45 - offset - intermediateOffset},
{id: 'UI_down', shape: 'image', image: DIR + 'downarrow.png', triggerFunction: "_moveDown",
verticalAlignTop: false, x: 45 + offset + intermediateOffset, y: this.UIclientHeight - 15 - offset},
{id: 'UI_left', shape: 'image', image: DIR + 'leftarrow.png', triggerFunction: "_moveLeft",
verticalAlignTop: false, x: 15 + offset, y: this.UIclientHeight - 15 - offset},
{id: 'UI_right', shape: 'image', image: DIR + 'rightarrow.png',triggerFunction: "_moveRight",
verticalAlignTop: false, x: 75 + offset + 2 * intermediateOffset, y: this.UIclientHeight - 15 - offset},
{id: 'UI_plus', shape: 'image', image: DIR + 'plus.png', triggerFunction: "_zoomIn",
verticalAlignTop: false, horizontalAlignLeft: false,
x: this.UIclientWidth - 45 - offset - intermediateOffset, y: this.UIclientHeight - 15 - offset},
{id: 'UI_min', shape: 'image', image: DIR + 'minus.png', triggerFunction: "_zoomOut",
verticalAlignTop: false, horizontalAlignLeft: false,
x: this.UIclientWidth - 15 - offset, y: this.UIclientHeight - 15 - offset},
{id: 'UI_zoomExtends', shape: 'image', image: DIR + 'zoomExtends.png', triggerFunction: "zoomToFit",
verticalAlignTop: false, horizontalAlignLeft: false,
x: this.UIclientWidth - 15 - offset, y: this.UIclientHeight - 45 - offset - intermediateOffset}
];
var nodeObj = null;
for (var i = 0; i < UINodes.length; i++) {
nodeObj = this.sectors["navigationUI"]['nodes'];
nodeObj[UINodes[i]['id']] = new Node(UINodes[i], this.images, this.groups, this.constants);
}
},
/**
* By setting the clustersize to be larger than 1, we use the clustering drawing method
* to illustrate the buttons are presed. We call this highlighting.
*
* @param {String} elementId
* @private
*/
_highlightUIElement : function(elementId) {
if (this.sectors["navigationUI"]["nodes"].hasOwnProperty(elementId)) {
this.sectors["navigationUI"]["nodes"][elementId].clusterSize = 2;
}
},
/**
* Reverting back to a normal button
*
* @param {String} elementId
* @private
*/
_unHighlightUIElement : function(elementId) {
if (this.sectors["navigationUI"]["nodes"].hasOwnProperty(elementId)) {
this.sectors["navigationUI"]["nodes"][elementId].clusterSize = 1;
}
},
/**
* toggle the visibility of the navigationUI
*
* @private
*/
_toggleUI : function() {
if (this.UIvisible === undefined) {
this.UIvisible = false;
}
this.UIvisible = !this.UIvisible;
this._redraw();
},
/**
* un-highlight (for lack of a better term) all navigationUI elements
* @private
*/
_unHighlightAll : function() {
for (var nodeId in this.sectors['navigationUI']['nodes']) {
this._unHighlightUIElement(nodeId);
}
},
_preventDefault : function(event) {
if (event !== undefined) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
}
},
/**
* move the screen up
* By using the increments, instead of adding a fixed number to the translation, we keep fluent and
* instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently
* To avoid this behaviour, we do the translation in the start loop.
*
* @private
*/
_moveUp : function(event) {
this._highlightUIElement("UI_up");
this.yIncrement = this.constants.keyboardNavigation.yMovementSpeed;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
},
/**
* move the screen down
* @private
*/
_moveDown : function(event) {
this._highlightUIElement("UI_down");
this.yIncrement = -this.constants.keyboardNavigation.yMovementSpeed;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
},
/**
* move the screen left
* @private
*/
_moveLeft : function(event) {
this._highlightUIElement("UI_left");
this.xIncrement = this.constants.keyboardNavigation.xMovementSpeed;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
},
/**
* move the screen right
* @private
*/
_moveRight : function(event) {
this._highlightUIElement("UI_right");
this.xIncrement = -this.constants.keyboardNavigation.xMovementSpeed;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
},
/**
* Zoom in, using the same method as the movement.
* @private
*/
_zoomIn : function(event) {
this._highlightUIElement("UI_plus");
this.zoomIncrement = this.constants.keyboardNavigation.zoomMovementSpeed;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
},
/**
* Zoom out
* @private
*/
_zoomOut : function() {
this._highlightUIElement("UI_min");
this.zoomIncrement = -this.constants.keyboardNavigation.zoomMovementSpeed;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
},
/**
* Stop zooming and unhighlight the zoom controls
* @private
*/
_stopZoom : function() {
this._unHighlightUIElement("UI_plus");
this._unHighlightUIElement("UI_min");
this.zoomIncrement = 0;
},
/**
* Stop moving in the Y direction and unHighlight the up and down
* @private
*/
_yStopMoving : function() {
this._unHighlightUIElement("UI_up");
this._unHighlightUIElement("UI_down");
this.yIncrement = 0;
},
/**
* Stop moving in the X direction and unHighlight left and right.
* @private
*/
_xStopMoving : function() {
this._unHighlightUIElement("UI_left");
this._unHighlightUIElement("UI_right");
this.xIncrement = 0;
}
};

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

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

+ 28
- 0
src/util.js View File

@ -671,3 +671,31 @@ util.option.asElement = function (value, defaultValue) {
return value || defaultValue || null;
};
/**
* Compare two numbers and return the lowest, non-negative number.
*
* @param {number} number1
* @param {number} number2
* @returns {number} | number1 or number2, the lowest positive number. If both negative, return -1
* @private
*/
util._getLowestPositiveNumber = function(number1,number2) {
if (number1 >= 0) {
if (number2 >= 0) {
return (number1 < number2) ? number1 : number2;
}
else {
return number1;
}
}
else {
if (number2 >= 0) {
return number2;
}
else {
return -1;
}
}
}

+ 18
- 0
test/dataset.js View File

@ -143,5 +143,23 @@ assert.deepEqual(data.getIds({
}), [3,1]);
data.clear();
// test if the setting of the showInternalIds works locally for a single get request
data.add({content: 'Item 1'});
data.add({content: 'Item 2'});
assert.strictEqual(data.get()[0].id, undefined);
assert.deepEqual((data.get({"showInternalIds": true})[0].id == undefined),false);
assert.deepEqual(data.isInternalId(data.get({"showInternalIds": true})[0].id), true);
assert.deepEqual((data.get()[0].id == undefined), true);
// check if the global setting is applied correctly
var data = new DataSet({showInternalIds: true});
data.add({content: 'Item 1'});
assert.deepEqual((data.get()[0].id == undefined), false);
assert.deepEqual(data.isInternalId(data.get()[0].id), true);
assert.deepEqual((data.get({"showInternalIds": false})[0].id == undefined),true);
// TODO: extensively test DataSet

+ 11964
- 0
vis.js.tmp
File diff suppressed because it is too large
View File


Loading…
Cancel
Save