Browse Source

Merge pull request #2 from almende/develop

Merge in upstream changes
css_transitions
Eric Gillingham 10 years ago
parent
commit
2d6396594f
57 changed files with 12636 additions and 4175 deletions
  1. +5
    -0
      .gitignore
  2. +29
    -0
      HISTORY.md
  3. +14
    -5
      Jakefile.js
  4. +1
    -1
      bower.json
  5. BIN
      dist/img/downarrow.png
  6. BIN
      dist/img/leftarrow.png
  7. BIN
      dist/img/minus.png
  8. BIN
      dist/img/plus.png
  9. BIN
      dist/img/rightarrow.png
  10. BIN
      dist/img/uparrow.png
  11. BIN
      dist/img/zoomExtends.png
  12. +8
    -0
      dist/vis.css
  13. +7375
    -3244
      dist/vis.js
  14. +10
    -8
      dist/vis.min.js
  15. +684
    -280
      docs/graph.html
  16. +134
    -17
      docs/timeline.html
  17. +10
    -3
      examples/graph/02_random_nodes.html
  18. +3
    -5
      examples/graph/07_selections.html
  19. +1
    -1
      examples/graph/17_network_info.html
  20. +102
    -0
      examples/graph/18_fully_random_nodes_clustering.html
  21. +141
    -0
      examples/graph/19_scale_free_graph_clustering.html
  22. +181
    -0
      examples/graph/20_navigation.html
  23. +3
    -0
      examples/graph/index.html
  24. +8
    -8
      examples/timeline/02_dataset.html
  25. +53
    -0
      examples/timeline/06_event_listeners.html
  26. +1
    -0
      examples/timeline/index.html
  27. +15
    -1
      misc/how_to_publish.md
  28. +6
    -3
      package.json
  29. +25
    -3
      src/DataSet.js
  30. +1019
    -0
      src/graph/ClusterMixin.js
  31. +41
    -23
      src/graph/Edge.js
  32. +784
    -411
      src/graph/Graph.js
  33. +245
    -0
      src/graph/NavigationMixin.js
  34. +338
    -46
      src/graph/Node.js
  35. +1
    -1
      src/graph/Popup.js
  36. +547
    -0
      src/graph/SectorsMixin.js
  37. +515
    -0
      src/graph/SelectionMixin.js
  38. BIN
      src/graph/img/downarrow.png
  39. BIN
      src/graph/img/leftarrow.png
  40. BIN
      src/graph/img/minus.png
  41. BIN
      src/graph/img/plus.png
  42. BIN
      src/graph/img/rightarrow.png
  43. BIN
      src/graph/img/uparrow.png
  44. BIN
      src/graph/img/zoomExtends.png
  45. +14
    -0
      src/module/imports.js
  46. +22
    -7
      src/timeline/Range.js
  47. +146
    -21
      src/timeline/Timeline.js
  48. +14
    -4
      src/timeline/component/Group.js
  49. +26
    -5
      src/timeline/component/GroupSet.js
  50. +82
    -72
      src/timeline/component/ItemSet.js
  51. +1
    -1
      src/timeline/component/Panel.js
  52. +4
    -4
      src/timeline/component/css/groupset.css
  53. +1
    -1
      src/timeline/component/css/panel.css
  54. +3
    -0
      src/timeline/component/item/ItemBox.js
  55. +3
    -0
      src/timeline/component/item/ItemPoint.js
  56. +3
    -0
      src/timeline/component/item/ItemRange.js
  57. +18
    -0
      test/dataset.js

+ 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

+ 29
- 0
HISTORY.md View File

@ -2,6 +2,35 @@ vis.js history
http://visjs.org
## 2014-01-31, version 0.4.0
### Timeline
- Implemented functions `on` and `off` to create event listeners for events
`rangechange`, `rangechanged`, and `select`.
- Impelmented function `select` to get and set the selected items.
- Items can be selected by clicking them, muti-select by holding them.
- Fixed non working `start` and `end` options.
### Graph
- 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 navigation controls.
- Added keyboard navigation.
- Implemented functions `on` and `off` to create event listeners for event
`select`.
## 2014-01-14, version 0.3.0
- Moved the generated library to folder `./dist`

+ 14
- 5
Jakefile.js View File

@ -3,7 +3,7 @@
*/
var jake = require('jake'),
browserify = require('browserify'),
path = require('path'),
wrench = require('wrench'),
fs = require('fs');
require('jake-utils');
@ -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,6 +82,10 @@ 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/NavigationMixin.js',
'./src/graph/Graph.js',
'./src/module/exports.js'
@ -91,6 +94,12 @@ task('build', {async: true}, function () {
separator: '\n'
});
// copy images
wrench.copyDirSyncRecursive('./src/graph/img', DIST+ '/img', {
forceDelete: true
});
var timeStart = Date.now();
// bundle the concatenated script and dependencies into one file
var b = browserify();
b.add(VIS_TMP);
@ -100,13 +109,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 +142,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);
});
/**

+ 1
- 1
bower.json View File

@ -1,6 +1,6 @@
{
"name": "vis",
"version": "0.4.0-SNAPSHOT",
"version": "0.5.0-SNAPSHOT",
"description": "A dynamic, browser-based visualization library.",
"homepage": "http://visjs.org/",
"repository": {

BIN
dist/img/downarrow.png View File

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

BIN
dist/img/leftarrow.png View File

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

BIN
dist/img/minus.png View File

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

BIN
dist/img/plus.png View File

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

BIN
dist/img/rightarrow.png View File

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

BIN
dist/img/uparrow.png View File

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

BIN
dist/img/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');

+ 7375
- 3244
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


+ 684
- 280
docs/graph.html
File diff suppressed because it is too large
View File


+ 134
- 17
docs/timeline.html View File

@ -14,6 +14,16 @@
<h1>Timeline documentation</h1>
<h2 id="Overview">Overview</h2>
<p>
The Timeline is an interactive visualization chart to visualize data in time.
The data items can take place on a single date, or have a start and end date (a range).
You can freely move and zoom in the timeline by dragging and scrolling in the
Timeline. Items can be created, edited, and deleted in the timeline.
The time scale on the axis is adjusted automatically, and supports scales ranging
from milliseconds to years.
</p>
<h2 id="Contents">Contents</h2>
<ul>
@ -28,20 +38,11 @@
</li>
<li><a href="#Configuration_Options">Configuration Options</a></li>
<li><a href="#Methods">Methods</a></li>
<li><a href="#Events">Events</a></li>
<li><a href="#Styles">Styles</a></li>
<li><a href="#Data_Policy">Data Policy</a></li>
</ul>
<h2 id="Overview">Overview</h2>
<p>
The Timeline is an interactive visualization chart to visualize data in time.
The data items can take place on a single date, or have a start and end date (a range).
You can freely move and zoom in the timeline by dragging and scrolling in the
Timeline. Items can be created, edited, and deleted in the timeline.
The time scale on the axis is adjusted automatically, and supports scales ranging
from milliseconds to years.
</p>
<h2 id="Example">Example</h2>
<p>
The following code shows how to create a Timeline and provide it with data.
@ -342,7 +343,7 @@ var options = {
<tr>
<td>end</td>
<td>Date</td>
<td>Date | Number | String</td>
<td>none</td>
<td>The initial end date for the axis of the timeline.
If not provided, the latest date present in the items set is taken as
@ -387,7 +388,7 @@ var options = {
<tr>
<td>max</td>
<td>Date</td>
<td>Date | Number | String</td>
<td>none</td>
<td>Set a maximum Date for the visible range.
It will not be possible to move beyond this maximum.
@ -404,7 +405,7 @@ var options = {
<tr>
<td>min</td>
<td>Date</td>
<td>Date | Number | String</td>
<td>none</td>
<td>Set a minimum Date for the visible range.
It will not be possible to move beyond this minimum.
@ -482,7 +483,7 @@ var options = {
<tr>
<td>start</td>
<td>Date</td>
<td>Date | Number | String</td>
<td>none</td>
<td>The initial start date for the axis of the timeline.
If not provided, the earliest date present in the events is taken as start date.</td>
@ -544,12 +545,32 @@ var options = {
<td>Retrieve the custom time. Only applicable when the option <code>showCustomTime</code> is true.
</td>
</tr>
<tr>
<td>setCustomTime(time)</td>
<td>none</td>
<td>Adjust the custom time bar. Only applicable when the option <code>showCustomTime</code> is true. <code>time</code> is a Date object.
</td>
</tr>
<tr>
<td>getSelection()</td>
<td>ids</td>
<td>Get an array with the ids of the currently selected items.</td>
</tr>
<tr>
<td>on(event, callback)</td>
<td>none</td>
<td>Create an event listener. The callback function is invoked every time the event is triggered. Avialable events: <code>rangechange</code>, <code>rangechanged</code>, <code>select</code>. The callback function is invoked as <code>callback(properties)</code>, where <code>properties</code> is an object containing event specific properties. See section <a href="#Events">Events for more information</a>.</td>
</tr>
<tr>
<td>off(event, callback)</td>
<td>none</td>
<td>Remove an event listener created before via function <code>on(event, callback)</code>. See section <a href="#Events">Events for more information</a>.</td>
</tr>
<tr>
<td>setGroups(groups)</td>
<td>none</td>
@ -560,6 +581,7 @@ var options = {
must correspond with the id of the group.
</td>
</tr>
<tr>
<td>setItems(items)</td>
<td>none</td>
@ -572,12 +594,107 @@ var options = {
<tr>
<td>setOptions(options)</td>
<td>none</td>
<td>Set or update options. It is possible to change any option
of the timeline at any time. You can for example switch orientation
on the fly.
<td>Set or update options. It is possible to change any option of the timeline at any time. You can for example switch orientation on the fly.
</td>
</tr>
<tr>
<td>setSelection([ids])</td>
<td>none</td>
<td>Select or deselect items. Currently selected items will be unselected.
</td>
</tr>
</table>
<h2 id="Events">Events</h2>
<p>
Timeline fires events when changing the visible window by dragging, or when
selecting items.
</p>
<p>
Here an example on how to listen for a <code>select</code> event.
</p>
<pre class="prettyprint lang-js">
timeline.on('select', function (properties) {
alert('selected items: ' + properties.nodes);
});
</pre>
<p>
A listener can be removed via the function <code>off</code>:
</p>
<pre class="prettyprint lang-js">
function onSelect (properties) {
alert('selected items: ' + properties.nodes);
}
// add event listener
timeline.on('select', onSelect);
// do stuff...
// remove event listener
timeline.off('select', onSelect);
</pre>
<p>
The following events are available.
</p>
<table>
<colgroup>
<col style="width: 20%;">
<col style="width: 40%;">
<col style="width: 40%;">
</colgroup>
<tr>
<th>name</th>
<th>Description</th>
<th>Properties</th>
</tr>
<tr>
<td>rangechange</td>
<td>Fired repeatedly when the user is dragging the timeline window.
</td>
<td>
<ul>
<li><code>start</code> (Number): timestamp of the current start of the window.</li>
<li><code>end</code> (Number): timestamp of the current end of the window.</li>
</ul>
</td>
</tr>
<tr>
<td>rangechanged</td>
<td>Fired once after the user has dragging the timeline window.
</td>
<td>
<ul>
<li><code>start</code> (Number): timestamp of the current start of the window.</li>
<li><code>end</code> (Number): timestamp of the current end of the window.</li>
</ul>
</td>
</tr>
<tr>
<td>select</td>
<td>Fired after the user selects or deselects items by tapping or holding them.
Not fired when the method <code>setSelection</code>is executed.
</td>
<td>
<ul>
<li><code>items</code>: an array with the ids of the selected items</li>
</ul>
</td>
</tr>
</table>

+ 10
- 3
examples/graph/02_random_nodes.html View File

@ -74,6 +74,7 @@
nodes: nodes,
edges: edges
};
/*
var options = {
nodes: {
shape: 'circle'
@ -83,12 +84,18 @@
},
stabilize: false
};
*/
var options = {
edges: {
length: 50
},
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();
graph.on('select', function(params) {
document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes;
});
}
</script>

+ 3
- 5
examples/graph/07_selections.html View File

@ -51,11 +51,9 @@
graph = new vis.Graph(container, data, options);
// add event listener
function onSelect() {
document.getElementById('info').innerHTML +=
'selection: ' + graph.getSelection().join(', ') + '<br>';
}
vis.events.addListener(graph, 'select', onSelect);
graph.on('select', function(params) {
document.getElementById('info').innerHTML += 'selection: ' + params.nodes + '<br>';
});
// set initial selection (id's of some nodes)
graph.setSelection([3, 4, 5]);

+ 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 | Fully random nodes clustering</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
graph.on('select', function(params) {
document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes;
});
}
</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>

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

@ -0,0 +1,141 @@
<!doctype html>
<html>
<head>
<title>Graph | Scale free graph clustering</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
graph.on('select', function(params) {
document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes;
});
}
</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>

+ 181
- 0
examples/graph/20_navigation.html View File

@ -0,0 +1,181 @@
<!doctype html>
<html>
<head>
<title>Graph | Navigation</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,
navigation: true,
keyboard: true
};
graph = new vis.Graph(container, data, options);
// add event listeners
graph.on('select', function(params) {
document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes;
});
}
</script>
</head>
<body onload="draw();">
<h2>Navigation controls and keyboad navigation</h2>
<div style="width: 700px; font-size:14px;">
This example is the same as example 2, except for the navigation controls that has been activated. The navigation controls are described below. <br /><br />
<table class="legend_table">
<tr>
<td>Icons: </td>
<td><div class="table_content"><img src="../../dist/img/uparrow.png" /> </div></td>
<td><div class="table_content"><img src="../../dist/img/downarrow.png" /> </div></td>
<td><div class="table_content"><img src="../../dist/img/leftarrow.png" /> </div></td>
<td><div class="table_content"><img src="../../dist/img/rightarrow.png" /> </div></td>
<td><div class="table_content"><img src="../../dist/img/plus.png" /> </div></td>
<td><div class="table_content"><img src="../../dist/img/minus.png" /> </div></td>
<td><div class="table_content"><img src="../../dist/img/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>
<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.
</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>

+ 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_navigation.html">20_navigation.html</a></p>
<p><a href="graphviz/graphviz_gallery.html">graphviz_gallery.html</a></p>
</div>

+ 8
- 8
examples/timeline/02_dataset.html View File

@ -39,18 +39,18 @@
}
});
items.add([
{id: 1, content: 'item 1<br>start', start: now.clone().add('days', 4)},
{id: 2, content: 'item 2', start: now.clone().add('days', -2)},
{id: 3, content: 'item 3', start: now.clone().add('days', 2)},
{id: 4, content: 'item 4', start: now.clone().add('days', 0), end: now.clone().add('days', 3).toDate()},
{id: 5, content: 'item 5', start: now.clone().add('days', 9), type:'point'},
{id: 6, content: 'item 6', start: now.clone().add('days', 11)}
{id: 1, content: 'item 1<br>start', start: '2014-01-23'},
{id: 2, content: 'item 2', start: '2014-01-18'},
{id: 3, content: 'item 3', start: '2014-01-21'},
{id: 4, content: 'item 4', start: '2014-01-19', end: '2014-01-24'},
{id: 5, content: 'item 5', start: '2014-01-28', type:'point'},
{id: 6, content: 'item 6', start: '2014-01-26'}
]);
var container = document.getElementById('visualization');
var options = {
start: now.clone().add('days', -3),
end: now.clone().add('days', 7),
start: '2014-01-10',
end: '2014-02-10',
orientation: 'top',
height: '100%',
showCurrentTime: true

+ 53
- 0
examples/timeline/06_event_listeners.html View File

@ -0,0 +1,53 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Timeline | Event listeners</title>
<style type="text/css">
body, html {
font-family: sans-serif;
}
</style>
<script src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div id="visualization"></div>
<p></p>
<div id="log"></div>
<script type="text/javascript">
var container = document.getElementById('visualization');
var items = [
{id: 1, content: 'item 1', start: '2013-04-20'},
{id: 2, content: 'item 2', start: '2013-04-14'},
{id: 3, content: 'item 3', start: '2013-04-18'},
{id: 4, content: 'item 4', start: '2013-04-16', end: '2013-04-19'},
{id: 5, content: 'item 5', start: '2013-04-25'},
{id: 6, content: 'item 6', start: '2013-04-27'}
];
var options = {};
var timeline = new vis.Timeline(container, items, options);
timeline.on('rangechange', function (properties) {
logEvent('rangechange', properties);
});
timeline.on('rangechanged', function (properties) {
logEvent('rangechanged', properties);
});
timeline.on('select', function (properties) {
logEvent('select', properties);
});
function logEvent(event, properties) {
var log = document.getElementById('log');
var msg = document.createElement('div');
msg.innerHTML = 'event=' + JSON.stringify(event) + ', ' +
'properties=' + JSON.stringify(properties);
log.firstChild ? log.insertBefore(msg, log.firstChild) : log.appendChild(msg);
}
</script>
</body>
</html>

+ 1
- 0
examples/timeline/index.html View File

@ -17,6 +17,7 @@
<p><a href="03_much_data.html">03_much_data.html</a></p>
<p><a href="04_html_data.html">04_html_data.html</a></p>
<p><a href="05_groups.html">05_groups.html</a></p>
<p><a href="06_event_listeners.html">06_event_listeners.html</a></p>
</div>
</body>

+ 15
- 1
misc/how_to_publish.md View File

@ -49,13 +49,25 @@ This generates the vis.js library in the folder `./dist`.
Verify if it installs the just released version, and verify if it works.
- Install the libarry via bower:
- Install the library via bower:
bower install vis
Verify if it installs the just released version, and verify if it works.
- Publish the library at cdnjs.org
- clone the cdnjs project
- pull changes: `git pull upstream`
- add the new version of the library under /ajax/libs/vis/
- add new folder /x.y.z/ with the new library
- update the version number in package.json
- test the library by running `npm test`
- then do a pull request with as title "[author] Update vis.js to x.y.z"
(with correct version).
## Update website
- Copy the `dist` folder from the `master` branch to the `github-pages` branch.
@ -72,6 +84,8 @@ This generates the vis.js library in the folder `./dist`.
node updateversion.js
- Commit the changes in the `gh-pages` branch.
## Prepare next version

+ 6
- 3
package.json View File

@ -1,6 +1,6 @@
{
"name": "vis",
"version": "0.4.0-SNAPSHOT",
"version": "0.5.0-SNAPSHOT",
"description": "A dynamic, browser-based visualization library.",
"homepage": "http://visjs.org/",
"repository": {
@ -28,8 +28,11 @@
"devDependencies": {
"jake": "latest",
"jake-utils": "latest",
"browserify": "latest",
"browserify": "3.22",
"wrench": "latest",
"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;
};

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


+ 245
- 0
src/graph/NavigationMixin.js View File

@ -0,0 +1,245 @@
/**
* Created by Alex on 1/22/14.
*/
var NavigationMixin = {
/**
* This function moves the navigation controls if the canvas size has been changed. If the arugments
* verticaAlignTop and horizontalAlignLeft are false, the correction will be made
*
* @private
*/
_relocateNavigation : function() {
if (this.sectors !== undefined) {
var xOffset = this.navigationClientWidth - this.frame.canvas.clientWidth;
var yOffset = this.navigationClientHeight - this.frame.canvas.clientHeight;
this.navigationClientWidth = this.frame.canvas.clientWidth;
this.navigationClientHeight = this.frame.canvas.clientHeight;
var node = null;
for (var nodeId in this.sectors["navigation"]["nodes"]) {
if (this.sectors["navigation"]["nodes"].hasOwnProperty(nodeId)) {
node = this.sectors["navigation"]["nodes"][nodeId];
if (!node.horizontalAlignLeft) {
node.x -= xOffset;
}
if (!node.verticalAlignTop) {
node.y -= yOffset;
}
}
}
}
},
/**
* Creation of the navigation controls 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 navigation controls 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 _relocateNavigation function on a size change of the canvas.
*
* @private
*/
_loadNavigationElements : function() {
var DIR = this.constants.navigation.iconPath;
this.navigationClientWidth = this.frame.canvas.clientWidth;
this.navigationClientHeight = this.frame.canvas.clientHeight;
if (this.navigationClientWidth === undefined) {
this.navigationClientWidth = 0;
this.navigationClientHeight = 0;
}
var offset = 15;
var intermediateOffset = 7;
var navigationNodes = [
{id: 'navigation_up', shape: 'image', image: DIR + '/uparrow.png', triggerFunction: "_moveUp",
verticalAlignTop: false, x: 45 + offset + intermediateOffset, y: this.navigationClientHeight - 45 - offset - intermediateOffset},
{id: 'navigation_down', shape: 'image', image: DIR + '/downarrow.png', triggerFunction: "_moveDown",
verticalAlignTop: false, x: 45 + offset + intermediateOffset, y: this.navigationClientHeight - 15 - offset},
{id: 'navigation_left', shape: 'image', image: DIR + '/leftarrow.png', triggerFunction: "_moveLeft",
verticalAlignTop: false, x: 15 + offset, y: this.navigationClientHeight - 15 - offset},
{id: 'navigation_right', shape: 'image', image: DIR + '/rightarrow.png',triggerFunction: "_moveRight",
verticalAlignTop: false, x: 75 + offset + 2 * intermediateOffset, y: this.navigationClientHeight - 15 - offset},
{id: 'navigation_plus', shape: 'image', image: DIR + '/plus.png', triggerFunction: "_zoomIn",
verticalAlignTop: false, horizontalAlignLeft: false,
x: this.navigationClientWidth - 45 - offset - intermediateOffset, y: this.navigationClientHeight - 15 - offset},
{id: 'navigation_min', shape: 'image', image: DIR + '/minus.png', triggerFunction: "_zoomOut",
verticalAlignTop: false, horizontalAlignLeft: false,
x: this.navigationClientWidth - 15 - offset, y: this.navigationClientHeight - 15 - offset},
{id: 'navigation_zoomExtends', shape: 'image', image: DIR + '/zoomExtends.png', triggerFunction: "zoomToFit",
verticalAlignTop: false, horizontalAlignLeft: false,
x: this.navigationClientWidth - 15 - offset, y: this.navigationClientHeight - 45 - offset - intermediateOffset}
];
var nodeObj = null;
for (var i = 0; i < navigationNodes.length; i++) {
nodeObj = this.sectors["navigation"]['nodes'];
nodeObj[navigationNodes[i]['id']] = new Node(navigationNodes[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
*/
_highlightNavigationElement : function(elementId) {
if (this.sectors["navigation"]["nodes"].hasOwnProperty(elementId)) {
this.sectors["navigation"]["nodes"][elementId].clusterSize = 2;
}
},
/**
* Reverting back to a normal button
*
* @param {String} elementId
* @private
*/
_unHighlightNavigationElement : function(elementId) {
if (this.sectors["navigation"]["nodes"].hasOwnProperty(elementId)) {
this.sectors["navigation"]["nodes"][elementId].clusterSize = 1;
}
},
/**
* un-highlight (for lack of a better term) all navigation controls elements
* @private
*/
_unHighlightAll : function() {
for (var nodeId in this.sectors['navigation']['nodes']) {
if (this.sectors['navigation']['nodes'].hasOwnProperty(nodeId)) {
this._unHighlightNavigationElement(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._highlightNavigationElement("navigation_up");
this.yIncrement = this.constants.keyboard.speed.y;
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._highlightNavigationElement("navigation_down");
this.yIncrement = -this.constants.keyboard.speed.y;
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._highlightNavigationElement("navigation_left");
this.xIncrement = this.constants.keyboard.speed.x;
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._highlightNavigationElement("navigation_right");
this.xIncrement = -this.constants.keyboard.speed.y;
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._highlightNavigationElement("navigation_plus");
this.zoomIncrement = this.constants.keyboard.speed.zoom;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
},
/**
* Zoom out
* @private
*/
_zoomOut : function() {
this._highlightNavigationElement("navigation_min");
this.zoomIncrement = -this.constants.keyboard.speed.zoom;
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._unHighlightNavigationElement("navigation_plus");
this._unHighlightNavigationElement("navigation_min");
this.zoomIncrement = 0;
},
/**
* Stop moving in the Y direction and unHighlight the up and down
* @private
*/
_yStopMoving : function() {
this._unHighlightNavigationElement("navigation_up");
this._unHighlightNavigationElement("navigation_down");
this.yIncrement = 0;
},
/**
* Stop moving in the X direction and unHighlight left and right.
* @private
*/
_xStopMoving : function() {
this._unHighlightNavigationElement("navigation_left");
this._unHighlightNavigationElement("navigation_right");
this.xIncrement = 0;
}
};

+ 338
- 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,53 @@ function Node(properties, imagelist, grouplist, constants) {
this.y = 0;
this.xFixed = false;
this.yFixed = false;
this.horizontalAlignLeft = true; // these are for the navigation controls
this.verticalAlignTop = true; // these are for the navigation controls
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.nodeScaling.width;
this.clusterSizeHeightFactor = constants.clustering.nodeScaling.height;
this.clusterSizeRadiusFactor = constants.clustering.nodeScaling.radius;
// 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 +101,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 +116,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 +128,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 +140,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;}
// navigation controls 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 +170,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 +188,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 +213,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 +240,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 +253,7 @@ Node.parseColor = function(color) {
c.highlight.border = color.highlight && color.highlight.border || c.border;
}
}
return c;
};
@ -231,6 +273,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 +377,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 +405,16 @@ 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) {
// console.log(vmin,this.vx,this.vy);
return true;
}
else {
this.vx = 0; this.vy = 0;
return false;
}
//return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin);
};
/**
@ -405,6 +462,7 @@ Node.prototype.setValueRange = function(min, max) {
this.radius = (this.value - min) * scale + this.radiusMin;
}
}
this.baseRadiusValue = this.radius;
};
/**
@ -431,20 +489,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 +518,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 +535,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 +566,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 +579,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 +614,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 +627,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 +663,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 +676,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 +711,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 +724,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 +772,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 +790,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 +835,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 +873,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 +890,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 navigation controls sector.
*
* @private
*/
_switchToNavigationSector : function() {
this.nodeIndices = this.sectors["navigation"]["nodeIndices"];
this.nodes = this.sectors["navigation"]["nodes"];
this.edges = this.sectors["navigation"]["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 navigation controls 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
*/
_doInNavigationSector : function(runFunction,argument) {
this._switchToNavigationSector();
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();
}
};

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

@ -0,0 +1,515 @@
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 navigation controls 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
*/
_getAllNavigationNodesOverlappingWith : function (object) {
var overlappingNodes = [];
this._doInNavigationSector("_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 navigation controls node at the a specific point (like a click)
*
* @param {{x: Number, y: Number}} pointer
* @return {Node | null} node
* @private
*/
_getNavigationNodeAt : function (pointer) {
var screenPositionObject = this._pointerToScreenPositionObject(pointer);
var overlappingNodes = this._getAllNavigationNodesOverlappingWith(screenPositionObject);
if (overlappingNodes.length > 0) {
return this.sectors["navigation"]["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 navigation controls 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.
*
* @param {Boolean} [doNotTrigger] | ignore trigger
* @private
*/
_unselectAll : function(doNotTrigger) {
if (doNotTrigger === undefined) {
doNotTrigger = false;
}
this.selection = [];
for (var objId in this.selectionObj) {
if (this.selectionObj.hasOwnProperty(objId)) {
this.selectionObj[objId].unselect();
}
}
this.selectionObj = {};
if (doNotTrigger == false) {
this._trigger('select', {
nodes: this.getSelection()
});
}
},
/**
* 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
* @param {Boolean} [doNotTrigger] | ignore trigger
* @private
*/
_selectNode : function(node, append, doNotTrigger) {
if (doNotTrigger === undefined) {
doNotTrigger = false;
}
if (this._selectionIsEmpty() == false && append == false) {
this._unselectAll(true);
}
if (node.selected == false) {
node.select();
this._addToSelection(node);
}
else {
node.unselect();
this._removeFromSelection(node);
}
if (doNotTrigger == false) {
this._trigger('select', {
nodes: this.getSelection()
});
}
},
/**
* handles the selection part of the touch, only for navigation controls 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) {
if (this.constants.navigation.enabled == true) {
var node = this._getNavigationNodeAt(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 navigation controls module.
*
* @private
*/
_handleOnRelease : function() {
this.xIncrement = 0;
this.yIncrement = 0;
this.zoomIncrement = 0;
this._unHighlightAll();
},
/**
*
* retrieve the currently selected nodes
* @return {Number[] | String[]} selection An array with the ids of the
* selected nodes.
*/
getSelection : function() {
return this.selection.concat([]);
},
/**
*
* retrieve the currently selected nodes as objects
* @return {Objects} selection An array with the ids of the
* selected nodes.
*/
getSelectionObjects : function() {
return this.selectionObj;
},
/**
* // 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
this._unselectAll(true);
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');
}
this._selectNode(node,true,true);
}
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', {
nodes: this.getSelection()
});
}
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', {
nodes: this.getSelection()
});
}
return changed;
},
*/
};

BIN
src/graph/img/downarrow.png View File

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

BIN
src/graph/img/leftarrow.png View File

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

BIN
src/graph/img/minus.png View File

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

BIN
src/graph/img/plus.png View File

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

BIN
src/graph/img/rightarrow.png View File

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

BIN
src/graph/img/uparrow.png View File

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

BIN
src/graph/img/zoomExtends.png View File

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

+ 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.');
}
}

+ 22
- 7
src/timeline/Range.js View File

@ -94,15 +94,30 @@ Range.prototype.subscribe = function (component, event, direction) {
};
/**
* Event handler
* @param {String} event name of the event, for example 'click', 'mousemove'
* @param {function} callback callback handler, invoked with the raw HTML Event
* as parameter.
* Add event listener
* @param {String} event Name of the event.
* Available events: 'rangechange', 'rangechanged'
* @param {function} callback Callback function, invoked as callback({start: Date, end: Date})
*/
Range.prototype.on = function (event, callback) {
Range.prototype.on = function on (event, callback) {
var available = ['rangechange', 'rangechanged'];
if (available.indexOf(event) == -1) {
throw new Error('Unknown event "' + event + '". Choose from ' + available.join());
}
events.addListener(this, event, callback);
};
/**
* Remove an event listener
* @param {String} event name of the event
* @param {function} callback callback handler
*/
Range.prototype.off = function off (event, callback) {
events.removeListener(this, event, callback);
};
/**
* Trigger an event
* @param {String} event name of the event, available events: 'rangechange',
@ -139,8 +154,8 @@ Range.prototype.setRange = function(start, end) {
* @private
*/
Range.prototype._applyRange = function(start, end) {
var newStart = (start != null) ? util.convert(start, 'Number') : this.start,
newEnd = (end != null) ? util.convert(end, 'Number') : this.end,
var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start,
newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end,
max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
diff;

+ 146
- 21
src/timeline/Timeline.js View File

@ -83,18 +83,26 @@ function Timeline (container, items, options) {
);
// TODO: reckon with options moveable and zoomable
// TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable
this.range.subscribe(this.rootPanel, 'move', 'horizontal');
this.range.subscribe(this.rootPanel, 'zoom', 'horizontal');
this.range.on('rangechange', function () {
this.range.on('rangechange', function (properties) {
var force = true;
me.controller.requestReflow(force);
me._trigger('rangechange', properties);
});
this.range.on('rangechanged', function () {
this.range.on('rangechanged', function (properties) {
var force = true;
me.controller.requestReflow(force);
me._trigger('rangechanged', properties);
});
// TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable
// single select (or unselect) when tapping an item
// TODO: implement ctrl+click
this.rootPanel.on('tap', this._onSelectItem.bind(this));
// multi select when holding mouse/touch, or on ctrl+click
this.rootPanel.on('hold', this._onMultiSelectItem.bind(this));
// time axis
var timeaxisOptions = Object.create(rootOptions);
@ -139,10 +147,9 @@ function Timeline (container, items, options) {
Timeline.prototype.setOptions = function (options) {
util.extend(this.options, options);
// force update of range
// options.start and options.end can be undefined
//this.range.setRange(options.start, options.end);
this.range.setRange();
// force update of range (apply new min/max etc.)
// both start and end are optional
this.range.setRange(options.start, options.end);
this.controller.reflow();
this.controller.repaint();
@ -198,29 +205,29 @@ Timeline.prototype.setItems = function(items) {
var dataRange = this.getItemRange();
// add 5% space on both sides
var min = dataRange.min;
var max = dataRange.max;
if (min != null && max != null) {
var interval = (max.valueOf() - min.valueOf());
var start = dataRange.min;
var end = dataRange.max;
if (start != null && end != null) {
var interval = (end.valueOf() - start.valueOf());
if (interval <= 0) {
// prevent an empty interval
interval = 24 * 60 * 60 * 1000; // 1 day
}
min = new Date(min.valueOf() - interval * 0.05);
max = new Date(max.valueOf() + interval * 0.05);
start = new Date(start.valueOf() - interval * 0.05);
end = new Date(end.valueOf() + interval * 0.05);
}
// override specified start and/or end date
if (this.options.start != undefined) {
min = util.convert(this.options.start, 'Date');
start = util.convert(this.options.start, 'Date');
}
if (this.options.end != undefined) {
max = util.convert(this.options.end, 'Date');
end = util.convert(this.options.end, 'Date');
}
// apply range if there is a min or max available
if (min != null || max != null) {
this.range.setRange(min, max);
if (start != null || end != null) {
this.range.setRange(start, end);
}
}
};
@ -342,10 +349,128 @@ Timeline.prototype.getItemRange = function getItemRange() {
};
/**
* Change the item selection, and/or get currently selected items
* @param {Array} [ids] An array with zero or more ids of the items to be selected.
* Set selected items by their id. Replaces the current selection
* Unknown id's are silently ignored.
* @param {Array} [ids] An array with zero or more id's of the items to be
* selected. If ids is an empty array, all items will be
* unselected.
*/
Timeline.prototype.setSelection = function setSelection (ids) {
if (this.content) this.content.setSelection(ids);
};
/**
* Get the selected items by their id
* @return {Array} ids The ids of the selected items
*/
Timeline.prototype.select = function select(ids) {
return this.content ? this.content.select(ids) : [];
Timeline.prototype.getSelection = function getSelection() {
return this.content ? this.content.getSelection() : [];
};
/**
* Add event listener
* @param {String} event Event name. Available events:
* 'rangechange', 'rangechanged', 'select'
* @param {function} callback Callback function, invoked as callback(properties)
* where properties is an optional object containing
* event specific properties.
*/
Timeline.prototype.on = function on (event, callback) {
var available = ['rangechange', 'rangechanged', 'select'];
if (available.indexOf(event) == -1) {
throw new Error('Unknown event "' + event + '". Choose from ' + available.join());
}
events.addListener(this, event, callback);
};
/**
* Remove an event listener
* @param {String} event Event name
* @param {function} callback Callback function
*/
Timeline.prototype.off = function off (event, callback) {
events.removeListener(this, event, callback);
};
/**
* Trigger an event
* @param {String} event Event name, available events: 'rangechange',
* 'rangechanged', 'select'
* @param {Object} [properties] Event specific properties
* @private
*/
Timeline.prototype._trigger = function _trigger(event, properties) {
events.trigger(this, event, properties || {});
};
/**
* Handle selecting/deselecting an item when tapping it
* @param {Event} event
* @private
*/
Timeline.prototype._onSelectItem = function (event) {
var item = this._itemFromTarget(event);
var selection = item ? [item.id] : [];
this.setSelection(selection);
this._trigger('select', {
items: this.getSelection()
});
event.stopPropagation();
};
/**
* Handle selecting/deselecting multiple items when holding an item
* @param {Event} event
* @private
*/
Timeline.prototype._onMultiSelectItem = function (event) {
var selection,
item = this._itemFromTarget(event);
if (!item) {
// do nothing...
return;
}
selection = this.getSelection(); // current selection
var index = selection.indexOf(item.id);
if (index == -1) {
// item is not yet selected -> select it
selection.push(item.id);
}
else {
// item is already selected -> deselect it
selection.splice(index, 1);
}
this.setSelection(selection);
this._trigger('select', {
items: this.getSelection()
});
event.stopPropagation();
};
/**
* Find an item from an event target:
* searches for the attribute 'timeline-item' in the event target's element tree
* @param {Event} event
* @return {Item | null| item
* @private
*/
Timeline.prototype._itemFromTarget = function _itemFromTarget (event) {
var target = event.target;
while (target) {
if (target.hasOwnProperty('timeline-item')) {
return target['timeline-item'];
}
target = target.parentNode;
}
return null;
};

+ 14
- 4
src/timeline/component/Group.js View File

@ -76,12 +76,22 @@ Group.prototype.setItems = function setItems(items) {
};
/**
* Change the item selection, and/or get currently selected items
* @param {Array} [ids] An array with zero or more ids of the items to be selected.
* Set selected items by their id. Replaces the current selection.
* Unknown id's are silently ignored.
* @param {Array} [ids] An array with zero or more id's of the items to be
* selected. If ids is an empty array, all items will be
* unselected.
*/
Group.prototype.setSelection = function setSelection(ids) {
if (this.itemset) this.itemset.setSelection(ids);
};
/**
* Get the selected items by their id
* @return {Array} ids The ids of the selected items
*/
Group.prototype.select = function select(ids) {
return this.itemset ? this.itemset.select(ids) : [];
Group.prototype.getSelection = function getSelection() {
return this.itemset ? this.itemset.getSelection() : [];
};
/**

+ 26
- 5
src/timeline/component/GroupSet.js View File

@ -150,11 +150,32 @@ GroupSet.prototype.getGroups = function getGroups() {
};
/**
* Change the item selection, and/or get currently selected items
* @param {Array} [ids] An array with zero or more ids of the items to be selected.
* Set selected items by their id. Replaces the current selection.
* Unknown id's are silently ignored.
* @param {Array} [ids] An array with zero or more id's of the items to be
* selected. If ids is an empty array, all items will be
* unselected.
*/
GroupSet.prototype.setSelection = function setSelection(ids) {
var selection = [],
groups = this.groups;
// iterate over each of the groups
for (var id in groups) {
if (groups.hasOwnProperty(id)) {
var group = groups[id];
group.setSelection(ids);
}
}
return selection;
};
/**
* Get the selected items by their id
* @return {Array} ids The ids of the selected items
*/
GroupSet.prototype.select = function select(ids) {
GroupSet.prototype.getSelection = function getSelection() {
var selection = [],
groups = this.groups;
@ -162,7 +183,7 @@ GroupSet.prototype.select = function select(ids) {
for (var id in groups) {
if (groups.hasOwnProperty(id)) {
var group = groups[id];
selection = selection.concat(group.select(ids));
selection = selection.concat(group.getSelection());
}
}
@ -358,7 +379,7 @@ GroupSet.prototype.repaint = function repaint() {
GroupSet.prototype._createLabel = function(id) {
var group = this.groups[id];
var label = document.createElement('div');
label.className = 'label';
label.className = 'vlabel';
var inner = document.createElement('div');
inner.className = 'inner';
label.appendChild(inner);

+ 82
- 72
src/timeline/component/ItemSet.js View File

@ -112,11 +112,13 @@ ItemSet.prototype.setRange = function setRange(range) {
};
/**
* Change the item selection, and/or get currently selected items
* @param {Array} [ids] An array with zero or more ids of the items to be selected.
* @return {Array} ids The ids of the selected items
* Set selected items by their id. Replaces the current selection
* Unknown id's are silently ignored.
* @param {Array} [ids] An array with zero or more id's of the items to be
* selected. If ids is an empty array, all items will be
* unselected.
*/
ItemSet.prototype.select = function select(ids) {
ItemSet.prototype.setSelection = function setSelection(ids) {
var i, ii, id, item, selection;
if (ids) {
@ -152,11 +154,14 @@ ItemSet.prototype.select = function select(ids) {
this.requestRepaint();
}
}
else {
selection = this.selection.concat([]);
}
};
return selection;
/**
* Get the selected items by their id
* @return {Array} ids The ids of the selected items
*/
ItemSet.prototype.getSelection = function getSelection() {
return this.selection.concat([]);
};
/**
@ -262,80 +267,82 @@ ItemSet.prototype.repaint = function repaint() {
};
// show/hide added/changed/removed items
Object.keys(queue).forEach(function (id) {
//var entry = queue[id];
var action = queue[id];
var item = items[id];
//var item = entry.item;
//noinspection FallthroughInSwitchStatementJS
switch (action) {
case 'add':
case 'update':
var itemData = itemsData && itemsData.get(id, dataOptions);
if (itemData) {
var type = itemData.type ||
(itemData.start && itemData.end && 'range') ||
options.type ||
'box';
var constructor = ItemSet.types[type];
// TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
if (item) {
// update item
if (!constructor || !(item instanceof constructor)) {
// item type has changed, hide and delete the item
changed += item.hide();
item = null;
for (var id in queue) {
if (queue.hasOwnProperty(id)) {
var entry = queue[id],
item = items[id],
action = entry.action;
//noinspection FallthroughInSwitchStatementJS
switch (action) {
case 'add':
case 'update':
var itemData = itemsData && itemsData.get(id, dataOptions);
if (itemData) {
var type = itemData.type ||
(itemData.start && itemData.end && 'range') ||
options.type ||
'box';
var constructor = ItemSet.types[type];
// TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
if (item) {
// update item
if (!constructor || !(item instanceof constructor)) {
// item type has changed, hide and delete the item
changed += item.hide();
item = null;
}
else {
item.data = itemData; // TODO: create a method item.setData ?
changed++;
}
}
else {
item.data = itemData; // TODO: create a method item.setData ?
changed++;
}
}
if (!item) {
// create item
if (constructor) {
item = new constructor(me, itemData, options, defaultOptions);
item.id = id;
changed++;
if (!item) {
// create item
if (constructor) {
item = new constructor(me, itemData, options, defaultOptions);
item.id = entry.id; // we take entry.id, as id itself is stringified
changed++;
}
else {
throw new TypeError('Unknown item type "' + type + '"');
}
}
else {
throw new TypeError('Unknown item type "' + type + '"');
}
}
// force a repaint (not only a reposition)
item.repaint();
// force a repaint (not only a reposition)
item.repaint();
items[id] = item;
}
items[id] = item;
}
// update queue
delete queue[id];
break;
// update queue
delete queue[id];
break;
case 'remove':
if (item) {
// remove the item from the set selected items
if (item.selected) {
me._deselect(id);
}
case 'remove':
if (item) {
// remove the item from the set selected items
if (item.selected) {
me._deselect(id);
}
// remove DOM of the item
changed += item.hide();
}
// remove DOM of the item
changed += item.hide();
}
// update lists
delete items[id];
delete queue[id];
break;
// update lists
delete items[id];
delete queue[id];
break;
default:
console.log('Error: unknown action "' + action + '"');
default:
console.log('Error: unknown action "' + action + '"');
}
}
});
}
// reposition all items. Show items only when in the visible area
util.forEach(this.items, function (item) {
@ -546,7 +553,10 @@ ItemSet.prototype._onRemove = function _onRemove(ids) {
ItemSet.prototype._toQueue = function _toQueue(action, ids) {
var queue = this.queue;
ids.forEach(function (id) {
queue[id] = action;
queue[id] = {
id: id,
action: action
};
});
if (this.controller) {

+ 1
- 1
src/timeline/component/Panel.js View File

@ -54,7 +54,7 @@ Panel.prototype.repaint = function () {
frame = this.frame;
if (!frame) {
frame = document.createElement('div');
frame.className = 'panel';
frame.className = 'vpanel';
var className = options.className;
if (className) {

+ 4
- 4
src/timeline/component/css/groupset.css View File

@ -33,7 +33,7 @@
border-bottom: 1px solid #bfbfbf;
}
.vis.timeline .labels .label-set .label {
.vis.timeline .labels .label-set .vlabel {
position: absolute;
left: 0;
top: 0;
@ -41,19 +41,19 @@
color: #4d4d4d;
}
.vis.timeline.top .labels .label-set .label,
.vis.timeline.top .labels .label-set .vlabel,
.vis.timeline.top .groupset .itemset-axis {
border-top: 1px solid #bfbfbf;
border-bottom: none;
}
.vis.timeline.bottom .labels .label-set .label,
.vis.timeline.bottom .labels .label-set .vlabel,
.vis.timeline.bottom .groupset .itemset-axis {
border-top: none;
border-bottom: 1px solid #bfbfbf;
}
.vis.timeline .labels .label-set .label .inner {
.vis.timeline .labels .label-set .vlabel .inner {
display: inline-block;
padding: 5px;
}

+ 1
- 1
src/timeline/component/css/panel.css View File

@ -8,7 +8,7 @@
box-sizing: border-box;
}
.vis.timeline .panel {
.vis.timeline .vpanel {
position: absolute;
overflow: hidden;
}

+ 3
- 0
src/timeline/component/item/ItemBox.js View File

@ -260,6 +260,9 @@ ItemBox.prototype._create = function _create() {
// dot on axis
dom.dot = document.createElement('DIV');
dom.dot.className = 'dot';
// attach this item as attribute
dom.box['timeline-item'] = this;
}
};

+ 3
- 0
src/timeline/component/item/ItemPoint.js View File

@ -210,6 +210,9 @@ ItemPoint.prototype._create = function _create() {
dom.dot = document.createElement('div');
dom.dot.className = 'dot';
dom.point.appendChild(dom.dot);
// attach this item as attribute
dom.point['timeline-item'] = this;
}
};

+ 3
- 0
src/timeline/component/item/ItemRange.js View File

@ -226,6 +226,9 @@ ItemRange.prototype._create = function _create() {
dom.content = document.createElement('div');
dom.content.className = 'content';
dom.box.appendChild(dom.content);
// attach this item as attribute
dom.box['timeline-item'] = this;
}
};

+ 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

Loading…
Cancel
Save