Browse Source

Merge branch 'develop' into alex_dev

Conflicts:
	dist/vis.js
	examples/graph/02_random_nodes.html
	examples/graph/18_fully_random_nodes_clustering.html
	examples/graph/19_scale_free_graph_clustering.html
	examples/graph/20_UI_example.html
	src/graph/Graph.js
	src/graph/SelectionMixin.js
css_transitions
Alex de Mulder 10 years ago
parent
commit
53016d574a
49 changed files with 1809 additions and 12831 deletions
  1. +27
    -8
      HISTORY.md
  2. +6
    -3
      Jakefile.js
  3. +1
    -1
      bower.json
  4. +0
    -0
      dist/img/downarrow.png
  5. +0
    -0
      dist/img/leftarrow.png
  6. +0
    -0
      dist/img/minus.png
  7. +0
    -0
      dist/img/plus.png
  8. +0
    -0
      dist/img/rightarrow.png
  9. +0
    -0
      dist/img/uparrow.png
  10. +0
    -0
      dist/img/zoomExtends.png
  11. +559
    -289
      dist/vis.js
  12. +9
    -9
      dist/vis.min.js
  13. +78
    -65
      docs/graph.html
  14. +124
    -7
      docs/timeline.html
  15. +5
    -0
      examples/graph/02_random_nodes.html
  16. +3
    -5
      examples/graph/07_selections.html
  17. +7
    -1
      examples/graph/18_fully_random_nodes_clustering.html
  18. +6
    -1
      examples/graph/19_scale_free_graph_clustering.html
  19. +181
    -0
      examples/graph/20_navigation.html
  20. +1
    -1
      examples/graph/index.html
  21. +0
    -2
      examples/timeline/01_basic.html
  22. +8
    -8
      examples/timeline/02_dataset.html
  23. +53
    -0
      examples/timeline/06_event_listeners.html
  24. +1
    -0
      examples/timeline/index.html
  25. +15
    -1
      misc/how_to_publish.md
  26. +2
    -1
      package.json
  27. +100
    -60
      src/graph/Graph.js
  28. +245
    -0
      src/graph/NavigationMixin.js
  29. +3
    -3
      src/graph/Node.js
  30. +8
    -8
      src/graph/SectorsMixin.js
  31. +69
    -14
      src/graph/SelectionMixin.js
  32. +0
    -244
      src/graph/UIMixin.js
  33. +0
    -0
      src/graph/img/downarrow.png
  34. +0
    -0
      src/graph/img/leftarrow.png
  35. +0
    -0
      src/graph/img/minus.png
  36. +0
    -0
      src/graph/img/plus.png
  37. +0
    -0
      src/graph/img/rightarrow.png
  38. +0
    -0
      src/graph/img/uparrow.png
  39. +0
    -0
      src/graph/img/zoomExtends.png
  40. +22
    -7
      src/timeline/Range.js
  41. +146
    -21
      src/timeline/Timeline.js
  42. +14
    -4
      src/timeline/component/Group.js
  43. +25
    -4
      src/timeline/component/GroupSet.js
  44. +82
    -72
      src/timeline/component/ItemSet.js
  45. +3
    -0
      src/timeline/component/item/ItemBox.js
  46. +3
    -0
      src/timeline/component/item/ItemPoint.js
  47. +3
    -0
      src/timeline/component/item/ItemRange.js
  48. +0
    -28
      src/util.js
  49. +0
    -11964
      vis.js.tmp

+ 27
- 8
HISTORY.md View File

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

+ 6
- 3
Jakefile.js View File

@ -3,7 +3,7 @@
*/ */
var jake = require('jake'), var jake = require('jake'),
browserify = require('browserify'), browserify = require('browserify'),
path = require('path'),
wrench = require('wrench'),
fs = require('fs'); fs = require('fs');
require('jake-utils'); require('jake-utils');
@ -86,16 +86,19 @@ task('build', {async: true}, function () {
'./src/graph/SectorsMixin.js', './src/graph/SectorsMixin.js',
'./src/graph/ClusterMixin.js', './src/graph/ClusterMixin.js',
'./src/graph/SelectionMixin.js', './src/graph/SelectionMixin.js',
'./src/graph/UIMixin.js',
'./src/graph/NavigationMixin.js',
'./src/graph/Graph.js', './src/graph/Graph.js',
'./src/module/exports.js' './src/module/exports.js'
], ],
separator: '\n' separator: '\n'
}); });
// copy images
wrench.copyDirSyncRecursive('./src/graph/img', DIST+ '/img', {
forceDelete: true
});
var timeStart = Date.now(); var timeStart = Date.now();
// bundle the concatenated script and dependencies into one file // bundle the concatenated script and dependencies into one file

+ 1
- 1
bower.json View File

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

dist/UI_icons/downarrow.png → dist/img/downarrow.png View File


dist/UI_icons/leftarrow.png → dist/img/leftarrow.png View File


dist/UI_icons/minus.png → dist/img/minus.png View File


dist/UI_icons/plus.png → dist/img/plus.png View File


dist/UI_icons/rightarrow.png → dist/img/rightarrow.png View File


dist/UI_icons/uparrow.png → dist/img/uparrow.png View File


dist/UI_icons/zoomExtends.png → dist/img/zoomExtends.png View File


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


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


+ 78
- 65
docs/graph.html View File

@ -684,7 +684,7 @@ var options = {
</tr> </tr>
<tr> <tr>
<td><a href="#Keyboard_navigation">keyboardNavigation</a></td>
<td><a href="#Keyboard_navigation">keyboard</a></td>
<td>Object</td> <td>Object</td>
<td>none</td> <td>none</td>
<td> <td>
@ -693,7 +693,7 @@ var options = {
</tr> </tr>
<tr> <tr>
<td><a href="#Navigation_controls">navigationUI</a></td>
<td><a href="#Navigation_controls">navigation</a></td>
<td>Object</td> <td>Object</td>
<td>none</td> <td>none</td>
<td> <td>
@ -1145,7 +1145,6 @@ var nodes = [
// If a variable is not supplied, the default value is used. // If a variable is not supplied, the default value is used.
var options = { var options = {
clustering: { clustering: {
enabled: false,
initialMaxNodes: 100, initialMaxNodes: 100,
clusterThreshold:500, clusterThreshold:500,
reduceToNodes:300, reduceToNodes:300,
@ -1177,12 +1176,6 @@ var options: {
<th>Description</th> <th>Description</th>
</tr> </tr>
<tr>
<td>enabled</td>
<td>Boolean</td>
<td>false</td>
<td>On/off switch for clustering. It is assumed clustering is enabled in the descriptions below.</td>
</tr>
<tr> <tr>
<td>initialMaxNodes</td> <td>initialMaxNodes</td>
<td>Number</td> <td>Number</td>
@ -1289,24 +1282,23 @@ var options: {
</tr> </tr>
</table> </table>
<h2 id="Navigation_controls">Navigation controls</h2>
<h3 id="Navigation_controls">Navigation controls</h3>
<p> <p>
Graph has a menu with navigation controls, which is disabled by default. Graph has a menu with navigation controls, which is disabled by default.
It can be configured with the following settings. It can be configured with the following settings.
</p> </p>
<pre class="prettyprint"> <pre class="prettyprint">
// These variables must be defined in an options object named navigationUI.
// If a variable is not supplied, the default value is used.
// simple use of navigation controls
var options: { var options: {
navigationUI: {
enabled: false,
iconPath: this._getIconURL() // automatic loading of the default icons
}
navigation: true
} }
// OR to just load the module with default values:
// advanced use of navigation controls
var options: { var options: {
navigationUI: true
navigation: {
iconPath: '/path/to/navigation/icons/'
}
} }
</pre> </pre>
@ -1318,16 +1310,10 @@ var options: {
<th>Description</th> <th>Description</th>
</tr> </tr>
<tr>
<td>enabled</td>
<td>Boolean</td>
<td>false</td>
<td>On/off switch for the navigation UI elements.</td>
</tr>
<tr> <tr>
<td>iconPath</td> <td>iconPath</td>
<td>string</td> <td>string</td>
<td>[path to vis.js]/UI_icons/</td>
<td>"/img"</td>
<td>The path to the icon images can be defined here. If custom icons are used, they have to have the same filename as the ones originally packaged with vis.js.</td> <td>The path to the icon images can be defined here. If custom icons are used, they have to have the same filename as the ones originally packaged with vis.js.</td>
</tr> </tr>
</table> </table>
@ -1336,21 +1322,24 @@ var options: {
<p> <p>
The graph can be navigated using shortcut keys. The graph can be navigated using shortcut keys.
It can be configured with the following user-configurable settings. It can be configured with the following user-configurable settings.
The default state for the keyboard navigation is <b>off</b>. The predefined keys can be found in the <a href="../examples/graph/20_UI_example.html">UI example.</a>
The default state for the keyboard navigation is <b>off</b>. The predefined keys can be found in the example <a href="../examples/graph/20_navigation.html">20_navigation.html</a>.
</p> </p>
<pre class="prettyprint"> <pre class="prettyprint">
// These variables must be defined in an options object named keyboardNavigation.
// If a variable is not supplied, the default value is used
// simple use of keyboard controls
var options: { var options: {
keyboardNavigation: {
enabled: false,
speed: {x: 10, y: 10, zoom: 0.02}
}
keyboard: true
} }
// OR to just load the module with default values:
// advanced configuration for keyboard controls
var options: { var options: {
keyboardNavigation: true
keyboard: {
speed: {
x: 10,
y: 10,
zoom: 0.02
}
}
} }
</pre> </pre>
@ -1362,12 +1351,6 @@ var options: {
<th>Description</th> <th>Description</th>
</tr> </tr>
<tr>
<td>enabled</td>
<td>Boolean</td>
<td>false</td>
<td>On/off switch for the keyboard navigation.</td>
</tr>
<tr> <tr>
<td>speed.x</td> <td>speed.x</td>
<td>Number</td> <td>Number</td>
@ -1401,6 +1384,33 @@ var options: {
<th>Description</th> <th>Description</th>
</tr> </tr>
<tr>
<td>getSelection()</td>
<td>Array of ids</td>
<td>Returns an array with the ids of the selected nodes.
Returns an empty array if no nodes are selected.
The selections are not ordered.
</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>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</a> for more information.</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</a> for more information.</td>
</tr>
<tr>
<td>redraw()</td>
<td>none</td>
<td>Redraw the graph. Useful when the layout of the webpage changed.</td>
</tr>
<tr> <tr>
<td>setData(data,[disableStart])</td> <td>setData(data,[disableStart])</td>
<td>none</td> <td>none</td>
@ -1420,21 +1430,6 @@ var options: {
</td> </td>
</tr> </tr>
<tr>
<td>getSelection()</td>
<td>Array of ids</td>
<td>Returns an array with the ids of the selected nodes.
Returns an empty array if no nodes are selected.
The selections are not ordered.
</td>
</tr>
<tr>
<td>redraw()</td>
<td>none</td>
<td>Redraw the graph. Useful when the layout of the webpage changed.</td>
</tr>
<tr> <tr>
<td>setSelection(selection)</td> <td>setSelection(selection)</td>
<td>none</td> <td>none</td>
@ -1458,7 +1453,7 @@ var options: {
<h2 id="Events">Events</h2> <h2 id="Events">Events</h2>
<p> <p>
Graph fires events after one or multiple nodes are selected.
Graph fires events after one or multiple nodes are selected or deselected.
The event can be catched by creating a listener. The event can be catched by creating a listener.
</p> </p>
@ -1467,13 +1462,30 @@ var options: {
</p> </p>
<pre class="prettyprint lang-js"> <pre class="prettyprint lang-js">
function onSelect() {
alert('selected nodes: ' + graph.getSelection());
graph.on('select', function (properties) {
alert('selected nodes: ' + 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 nodes: ' + properties.nodes);
} }
vis.events.addListener(graph, 'select', onSelect);
// add event listener
graph.on('select', onSelect);
// do stuff...
// remove event listener
graph.off('select', onSelect);
</pre> </pre>
<p> <p>
The following events are available. The following events are available.
</p> </p>
@ -1487,13 +1499,14 @@ vis.events.addListener(graph, 'select', onSelect);
<tr> <tr>
<td>select</td> <td>select</td>
<td>Fired after the user selects or unselects a node by clicking it,
or when selecting a number of nodes by dragging a selection area
around them. Not fired when the method <code>setSelection</code>
is executed. The ids of the selected nodes can be retrieved via the
method <code>getSelection</code>.
<td>Fired after the user selects or deselects a node by clicking it.
Not fired when the method <code>setSelection</code>is executed.
</td>
<td>
<ul>
<li><code>nodes</code>: an array with the ids of the selected nodes</li>
</ul>
</td> </td>
<td>none</td>
</tr> </tr>
</table> </table>

+ 124
- 7
docs/timeline.html View File

@ -38,6 +38,7 @@
</li> </li>
<li><a href="#Configuration_Options">Configuration Options</a></li> <li><a href="#Configuration_Options">Configuration Options</a></li>
<li><a href="#Methods">Methods</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="#Styles">Styles</a></li>
<li><a href="#Data_Policy">Data Policy</a></li> <li><a href="#Data_Policy">Data Policy</a></li>
</ul> </ul>
@ -342,7 +343,7 @@ var options = {
<tr> <tr>
<td>end</td> <td>end</td>
<td>Date</td>
<td>Date | Number | String</td>
<td>none</td> <td>none</td>
<td>The initial end date for the axis of the timeline. <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 If not provided, the latest date present in the items set is taken as
@ -387,7 +388,7 @@ var options = {
<tr> <tr>
<td>max</td> <td>max</td>
<td>Date</td>
<td>Date | Number | String</td>
<td>none</td> <td>none</td>
<td>Set a maximum Date for the visible range. <td>Set a maximum Date for the visible range.
It will not be possible to move beyond this maximum. It will not be possible to move beyond this maximum.
@ -404,7 +405,7 @@ var options = {
<tr> <tr>
<td>min</td> <td>min</td>
<td>Date</td>
<td>Date | Number | String</td>
<td>none</td> <td>none</td>
<td>Set a minimum Date for the visible range. <td>Set a minimum Date for the visible range.
It will not be possible to move beyond this minimum. It will not be possible to move beyond this minimum.
@ -482,7 +483,7 @@ var options = {
<tr> <tr>
<td>start</td> <td>start</td>
<td>Date</td>
<td>Date | Number | String</td>
<td>none</td> <td>none</td>
<td>The initial start date for the axis of the timeline. <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> 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>Retrieve the custom time. Only applicable when the option <code>showCustomTime</code> is true.
</td> </td>
</tr> </tr>
<tr> <tr>
<td>setCustomTime(time)</td> <td>setCustomTime(time)</td>
<td>none</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>Adjust the custom time bar. Only applicable when the option <code>showCustomTime</code> is true. <code>time</code> is a Date object.
</td> </td>
</tr> </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> <tr>
<td>setGroups(groups)</td> <td>setGroups(groups)</td>
<td>none</td> <td>none</td>
@ -560,6 +581,7 @@ var options = {
must correspond with the id of the group. must correspond with the id of the group.
</td> </td>
</tr> </tr>
<tr> <tr>
<td>setItems(items)</td> <td>setItems(items)</td>
<td>none</td> <td>none</td>
@ -572,12 +594,107 @@ var options = {
<tr> <tr>
<td>setOptions(options)</td> <td>setOptions(options)</td>
<td>none</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> </td>
</tr> </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> </table>

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

@ -94,9 +94,14 @@
graph = new vis.Graph(container, data, options); graph = new vis.Graph(container, data, options);
// add event listeners // add event listeners
<<<<<<< HEAD
vis.events.addListener(graph, 'select', function(params) { vis.events.addListener(graph, 'select', function(params) {
document.getElementById('selection').innerHTML = document.getElementById('selection').innerHTML =
'Selection: ' + JSON.stringify(graph.getSelection()); 'Selection: ' + JSON.stringify(graph.getSelection());
=======
graph.on('select', function(params) {
document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes;
>>>>>>> develop
}); });
} }
</script> </script>

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

@ -51,11 +51,9 @@
graph = new vis.Graph(container, data, options); graph = new vis.Graph(container, data, options);
// add event listener // 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) // set initial selection (id's of some nodes)
graph.setSelection([3, 4, 5]); graph.setSelection([3, 4, 5]);

+ 7
- 1
examples/graph/18_fully_random_nodes_clustering.html View File

@ -1,7 +1,7 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<title>Graph | Really Random nodes</title>
<title>Graph | Fully random nodes clustering</title>
<style type="text/css"> <style type="text/css">
body { body {
@ -62,10 +62,16 @@
stabilize: false stabilize: false
}; };
graph = new vis.Graph(container, data, options); graph = new vis.Graph(container, data, options);
// add event listeners // add event listeners
<<<<<<< HEAD
vis.events.addListener(graph, 'select', function(params) { vis.events.addListener(graph, 'select', function(params) {
document.getElementById('selection').innerHTML = document.getElementById('selection').innerHTML =
'Selection: ' + JSON.stringify(graph.getSelection()); 'Selection: ' + JSON.stringify(graph.getSelection());
=======
graph.on('select', function(params) {
document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes;
>>>>>>> develop
}); });
} }
</script> </script>

+ 6
- 1
examples/graph/19_scale_free_graph_clustering.html View File

@ -1,7 +1,7 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<title>Graph | Random nodes</title>
<title>Graph | Scale free graph clustering</title>
<style type="text/css"> <style type="text/css">
body { body {
@ -100,9 +100,14 @@
graph = new vis.Graph(container, data, options); graph = new vis.Graph(container, data, options);
// add event listeners // add event listeners
<<<<<<< HEAD
vis.events.addListener(graph, 'select', function(params) { vis.events.addListener(graph, 'select', function(params) {
document.getElementById('selection').innerHTML = document.getElementById('selection').innerHTML =
'Selection: ' + JSON.stringify(graph.getSelection()); 'Selection: ' + JSON.stringify(graph.getSelection());
=======
graph.on('select', function(params) {
document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes;
>>>>>>> develop
}); });
} }
</script> </script>

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

+ 1
- 1
examples/graph/index.html View File

@ -31,7 +31,7 @@
<p><a href="17_network_info.html">17_network_info.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="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="19_scale_free_graph_clustering.html">19_scale_free_graph_clustering.html</a></p>
<p><a href="20_UI_example.html">20_UI_example.html</a></p>
<p><a href="20_navigation.html">20_navigation.html</a></p>
<p><a href="graphviz/graphviz_gallery.html">graphviz_gallery.html</a></p> <p><a href="graphviz/graphviz_gallery.html">graphviz_gallery.html</a></p>
</div> </div>

+ 0
- 2
examples/timeline/01_basic.html View File

@ -7,8 +7,6 @@
body, html { body, html {
font-family: sans-serif; font-family: sans-serif;
} }
</style> </style>
<script src="../../dist/vis.js"></script> <script src="../../dist/vis.js"></script>

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

@ -39,18 +39,18 @@
} }
}); });
items.add([ 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 container = document.getElementById('visualization');
var options = { var options = {
start: now.clone().add('days', -3),
end: now.clone().add('days', 7),
start: '2014-01-10',
end: '2014-02-10',
orientation: 'top', orientation: 'top',
height: '100%', height: '100%',
showCurrentTime: true 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="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="04_html_data.html">04_html_data.html</a></p>
<p><a href="05_groups.html">05_groups.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> </div>
</body> </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. 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 bower install vis
Verify if it installs the just released version, and verify if it works. 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 ## Update website
- Copy the `dist` folder from the `master` branch to the `github-pages` branch. - 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 node updateversion.js
- Commit the changes in the `gh-pages` branch.
## Prepare next version ## Prepare next version

+ 2
- 1
package.json View File

@ -1,6 +1,6 @@
{ {
"name": "vis", "name": "vis",
"version": "0.4.0-SNAPSHOT",
"version": "0.5.0-SNAPSHOT",
"description": "A dynamic, browser-based visualization library.", "description": "A dynamic, browser-based visualization library.",
"homepage": "http://visjs.org/", "homepage": "http://visjs.org/",
"repository": { "repository": {
@ -29,6 +29,7 @@
"jake": "latest", "jake": "latest",
"jake-utils": "latest", "jake-utils": "latest",
"browserify": "3.22", "browserify": "3.22",
"wrench": "latest",
"moment": "latest", "moment": "latest",
"hammerjs": "1.0.5", "hammerjs": "1.0.5",
"mousetrap": "latest", "mousetrap": "latest",

+ 100
- 60
src/graph/Graph.js View File

@ -86,11 +86,11 @@ function Graph (container, data, options) {
activeAreaBoxSize: 100, // (px) | box area around the curser where clusters are popped open. activeAreaBoxSize: 100, // (px) | box area around the curser where clusters are popped open.
massTransferCoefficient: 1 // (multiplier) | parent.mass += massTransferCoefficient * child.mass massTransferCoefficient: 1 // (multiplier) | parent.mass += massTransferCoefficient * child.mass
}, },
navigationUI: {
navigation: {
enabled: false, enabled: false,
iconPath: this._getIconURL()
iconPath: this._getScriptPath() + '/img'
}, },
keyboardNavigation: {
keyboard: {
enabled: false, enabled: false,
speed: {x: 10, y: 10, zoom: 0.02} speed: {x: 10, y: 10, zoom: 0.02}
}, },
@ -105,7 +105,7 @@ function Graph (container, data, options) {
graph._redraw(); graph._redraw();
}); });
// navigationUI variables
// navigation variables
this.xIncrement = 0; this.xIncrement = 0;
this.yIncrement = 0; this.yIncrement = 0;
this.zoomIncrement = 0; this.zoomIncrement = 0;
@ -149,7 +149,6 @@ function Graph (container, data, options) {
this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action
this.scale = 1; // defining the global scale variable in the constructor this.scale = 1; // defining the global scale variable in the constructor
this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
this.lastPointerPosition = {"x": 0,"y": 0}; // this is used for keyboard navigation
// TODO: create a counter to keep track on the number of nodes having values // TODO: create a counter to keep track on the number of nodes having values
// TODO: create a counter to keep track on the number of nodes currently moving // TODO: create a counter to keep track on the number of nodes currently moving
// TODO: create a counter to keep track on the number of edges having values // TODO: create a counter to keep track on the number of edges having values
@ -205,25 +204,26 @@ function Graph (container, data, options) {
} }
/** /**
* get the URL where the UI icons are located
* Get the script path where the vis.js library is located
* *
* @returns {string}
* @returns {string | null} path Path or null when not found. Path does not
* end with a slash.
* @private * @private
*/ */
Graph.prototype._getIconURL = function() {
Graph.prototype._getScriptPath = function() {
var scripts = document.getElementsByTagName( 'script' ); var scripts = document.getElementsByTagName( 'script' );
var scriptNamePosition, srcPosition, imagePath;
// find script named vis.js or vis.min.js
for (var i = 0; i < scripts.length; i++) { for (var i = 0; i < scripts.length; i++) {
srcPosition = scripts[i].outerHTML.search("src");
if (srcPosition != -1) {
scriptNamePosition = util._getLowestPositiveNumber(scripts[i].outerHTML.search("vis.js"),
scripts[i].outerHTML.search("vis.min.js"));
if (scriptNamePosition != -1) {
imagePath = scripts[i].outerHTML.substring(srcPosition+5,scriptNamePosition).concat("UI_icons/");
return imagePath;
}
var src = scripts[i].src;
var match = src && /\/?vis(.min)?\.js$/.exec(src);
if (match) {
// return path without the script name
return src.substring(0, src.length - match[0].length);
} }
} }
return null;
}; };
@ -403,28 +403,28 @@ Graph.prototype.setOptions = function (options) {
this.constants.clustering.enabled = false; this.constants.clustering.enabled = false;
} }
if (options.navigationUI) {
this.constants.navigationUI.enabled = true;
for (var prop in options.navigationUI) {
if (options.navigationUI.hasOwnProperty(prop)) {
this.constants.navigationUI[prop] = options.navigationUI[prop];
if (options.navigation) {
this.constants.navigation.enabled = true;
for (var prop in options.navigation) {
if (options.navigation.hasOwnProperty(prop)) {
this.constants.navigation[prop] = options.navigation[prop];
} }
} }
} }
else if (options.navigationUI !== undefined) {
this.constants.navigationUI.enabled = false;
else if (options.navigation !== undefined) {
this.constants.navigation.enabled = false;
} }
if (options.keyboardNavigation) {
this.constants.keyboardNavigation.enabled = true;
for (var prop in options.keyboardNavigation) {
if (options.keyboardNavigation.hasOwnProperty(prop)) {
this.constants.keyboardNavigation[prop] = options.keyboardNavigation[prop];
if (options.keyboard) {
this.constants.keyboard.enabled = true;
for (var prop in options.keyboard) {
if (options.keyboard.hasOwnProperty(prop)) {
this.constants.keyboard[prop] = options.keyboard[prop];
} }
} }
} }
else if (options.keyboardNavigation !== undefined) {
this.constants.keyboardNavigation.enabled = false;
else if (options.keyboard !== undefined) {
this.constants.keyboard.enabled = false;
} }
@ -492,8 +492,8 @@ Graph.prototype.setOptions = function (options) {
this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2); this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
this._setScale(1); this._setScale(1);
// load the navigationUI system.
this._loadUISystem();
// load the navigation system.
this._loadNavigationControls();
// bind keys. If disabled, this will not do anything; // bind keys. If disabled, this will not do anything;
this._createKeyBinds(); this._createKeyBinds();
@ -501,6 +501,33 @@ Graph.prototype.setOptions = function (options) {
this._redraw(); this._redraw();
}; };
/**
* Add event listener
* @param {String} event Event name. Available events:
* 'select'
* @param {function} callback Callback function, invoked as callback(properties)
* where properties is an optional object containing
* event specific properties.
*/
Graph.prototype.on = function on (event, callback) {
var available = ['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
*/
Graph.prototype.off = function off (event, callback) {
events.removeListener(this, event, callback);
};
/** /**
* fire an event * fire an event
* @param {String} event The name of an event, for example 'select' * @param {String} event The name of an event, for example 'select'
@ -570,7 +597,7 @@ Graph.prototype._create = function () {
/** /**
* Binding the keys for keyboard navigation. These functions are defined in the UIMixin
* Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin
* @private * @private
*/ */
Graph.prototype._createKeyBinds = function() { Graph.prototype._createKeyBinds = function() {
@ -579,7 +606,7 @@ Graph.prototype._createKeyBinds = function() {
this.mousetrap.reset(); this.mousetrap.reset();
if (this.constants.keyboardNavigation.enabled == true) {
if (this.constants.keyboard.enabled == true) {
this.mousetrap.bind("up", this._moveUp.bind(me) , "keydown"); this.mousetrap.bind("up", this._moveUp.bind(me) , "keydown");
this.mousetrap.bind("up", this._yStopMoving.bind(me), "keyup"); this.mousetrap.bind("up", this._yStopMoving.bind(me), "keyup");
this.mousetrap.bind("down", this._moveDown.bind(me) , "keydown"); this.mousetrap.bind("down", this._moveDown.bind(me) , "keydown");
@ -638,6 +665,7 @@ Graph.prototype._onDragStart = function () {
var node = this._getNodeAt(drag.pointer); var node = this._getNodeAt(drag.pointer);
// note: drag.pointer is set in _onTouch to get the initial touch location // note: drag.pointer is set in _onTouch to get the initial touch location
drag.dragging = true;
drag.selection = []; drag.selection = [];
drag.translation = this._getTranslation(); drag.translation = this._getTranslation();
drag.nodeId = null; drag.nodeId = null;
@ -731,6 +759,7 @@ Graph.prototype._onDrag = function (event) {
* @private * @private
*/ */
Graph.prototype._onDragEnd = function () { Graph.prototype._onDragEnd = function () {
this.drag.dragging = false;
var selection = this.drag.selection; var selection = this.drag.selection;
if (selection) { if (selection) {
selection.forEach(function (s) { selection.forEach(function (s) {
@ -803,7 +832,7 @@ Graph.prototype._onPinch = function (event) {
/** /**
* Zoom the graph in or out * Zoom the graph in or out
* @param {Number} scale a number around 1, and between 0.01 and 10 * @param {Number} scale a number around 1, and between 0.01 and 10
* @param {{x: Number, y: Number}} pointer
* @param {{x: Number, y: Number}} pointer Position on screen
* @return {Number} appliedScale scale is limited within the boundaries * @return {Number} appliedScale scale is limited within the boundaries
* @private * @private
*/ */
@ -895,8 +924,6 @@ Graph.prototype._onMouseMoveTitle = function (event) {
var gesture = util.fakeGesture(this, event); var gesture = util.fakeGesture(this, event);
var pointer = this._getPointer(gesture.center); var pointer = this._getPointer(gesture.center);
this.lastPointerPosition = pointer;
// check if the previously selected node is still selected // check if the previously selected node is still selected
if (this.popupNode) { if (this.popupNode) {
this._checkHidePopup(pointer); this._checkHidePopup(pointer);
@ -911,7 +938,7 @@ Graph.prototype._onMouseMoveTitle = function (event) {
if (this.popupTimer) { if (this.popupTimer) {
clearInterval(this.popupTimer); // stop any running calculationTimer clearInterval(this.popupTimer); // stop any running calculationTimer
} }
if (!this.leftButtonDown) {
if (!this.drag.dragging) {
this.popupTimer = setTimeout(checkShow, 300); this.popupTimer = setTimeout(checkShow, 300);
} }
}; };
@ -1099,10 +1126,15 @@ Graph.prototype.setSize = function(width, height) {
this.frame.canvas.width = this.frame.canvas.clientWidth; this.frame.canvas.width = this.frame.canvas.clientWidth;
this.frame.canvas.height = this.frame.canvas.clientHeight; this.frame.canvas.height = this.frame.canvas.clientHeight;
<<<<<<< HEAD
this.manipulationDiv.style.width = this.frame.canvas.clientWidth; this.manipulationDiv.style.width = this.frame.canvas.clientWidth;
if (this.constants.navigationUI.enabled == true) { if (this.constants.navigationUI.enabled == true) {
this._relocateUI(); this._relocateUI();
=======
if (this.constants.navigation.enabled == true) {
this._relocateNavigation();
>>>>>>> develop
} }
}; };
@ -1439,10 +1471,14 @@ Graph.prototype._redraw = function() {
ctx.translate(this.translation.x, this.translation.y); ctx.translate(this.translation.x, this.translation.y);
ctx.scale(this.scale, this.scale); ctx.scale(this.scale, this.scale);
this.canvasTopLeft = {"x": this._canvasToX(0),
"y": this._canvasToY(0)};
this.canvasBottomRight = {"x": this._canvasToX(this.frame.canvas.clientWidth),
"y": this._canvasToY(this.frame.canvas.clientHeight)};
this.canvasTopLeft = {
"x": this._canvasToX(0),
"y": this._canvasToY(0)
};
this.canvasBottomRight = {
"x": this._canvasToX(this.frame.canvas.clientWidth),
"y": this._canvasToY(this.frame.canvas.clientHeight)
};
this._doInAllSectors("_drawAllSectorNodes",ctx); this._doInAllSectors("_drawAllSectorNodes",ctx);
this._doInAllSectors("_drawEdges",ctx); this._doInAllSectors("_drawEdges",ctx);
@ -1451,8 +1487,8 @@ Graph.prototype._redraw = function() {
// restore original scaling and translation // restore original scaling and translation
ctx.restore(); ctx.restore();
if (this.constants.navigationUI.enabled == true) {
this._doInUISector("_drawNodes",ctx,true);
if (this.constants.navigation.enabled == true) {
this._doInNavigationSector("_drawNodes",ctx,true);
} }
}; };
@ -1914,10 +1950,14 @@ Graph.prototype.start = function() {
// keyboad movement // keyboad movement
if (graph.xIncrement != 0 || graph.yIncrement != 0) { if (graph.xIncrement != 0 || graph.yIncrement != 0) {
var translation = graph._getTranslation(); var translation = graph._getTranslation();
graph._setTranslation(translation.x+graph.xIncrement,translation.y+graph.yIncrement);
graph._setTranslation(translation.x+graph.xIncrement, translation.y+graph.yIncrement);
} }
if (graph.zoomIncrement != 0) { if (graph.zoomIncrement != 0) {
graph._zoom(graph.scale*(1 + graph.zoomIncrement),graph.lastPointerPosition);
var center = {
x: graph.frame.canvas.clientWidth / 2,
y: graph.frame.canvas.clientHeight / 2
};
graph._zoom(graph.scale*(1 + graph.zoomIncrement), center);
} }
graph.start(); graph.start();
@ -1997,7 +2037,7 @@ Graph.prototype._loadSectorSystem = function() {
"formationScale": 1.0, "formationScale": 1.0,
"drawingNode": undefined}; "drawingNode": undefined};
this.sectors["frozen"] = {}; this.sectors["frozen"] = {};
this.sectors["navigationUI"] = {"nodes":{},
this.sectors["navigation"] = {"nodes":{},
"edges":{}, "edges":{},
"nodeIndices":[], "nodeIndices":[],
"formationScale": 1.0, "formationScale": 1.0,
@ -2053,34 +2093,34 @@ Graph.prototype._loadManipulationSystem = function() {
} }
/** /**
* Mixin the navigationUI (User Interface) system and initialize the parameters required
* Mixin the navigation (User Interface) system and initialize the parameters required
* *
* @private * @private
*/ */
Graph.prototype._loadUISystem = function() {
for (var mixinFunction in UIMixin) {
if (UIMixin.hasOwnProperty(mixinFunction)) {
Graph.prototype[mixinFunction] = UIMixin[mixinFunction];
Graph.prototype._loadNavigationControls = function() {
for (var mixinFunction in NavigationMixin) {
if (NavigationMixin.hasOwnProperty(mixinFunction)) {
Graph.prototype[mixinFunction] = NavigationMixin[mixinFunction];
} }
} }
if (this.constants.navigationUI.enabled == true) {
this._loadUIElements();
if (this.constants.navigation.enabled == true) {
this._loadNavigationElements();
} }
} }
/** /**
* this function exists to avoid errors when not loading the navigationUI system
* this function exists to avoid errors when not loading the navigation system
*/ */
Graph.prototype._relocateUI = function() {
// empty, is overloaded by navigationUI system
Graph.prototype._relocateNavigation = function() {
// empty, is overloaded by navigation system
} }
/** /**
* * this function exists to avoid errors when not loading the navigationUI system
* * this function exists to avoid errors when not loading the navigation system
*/ */
Graph.prototype._unHighlightAll = function() { Graph.prototype._unHighlightAll = function() {
// empty, is overloaded by the navigationUI system
// empty, is overloaded by the navigation system
} }

+ 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;
}
};

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

@ -44,8 +44,8 @@ function Node(properties, imagelist, grouplist, constants) {
this.y = 0; this.y = 0;
this.xFixed = false; this.xFixed = false;
this.yFixed = false; this.yFixed = false;
this.horizontalAlignLeft = true; // these are for the navigationUI
this.verticalAlignTop = true; // these are for the navigationUI
this.horizontalAlignLeft = true; // these are for the navigation controls
this.verticalAlignTop = true; // these are for the navigation controls
this.radius = constants.nodes.radius; this.radius = constants.nodes.radius;
this.baseRadiusValue = constants.nodes.radius; this.baseRadiusValue = constants.nodes.radius;
this.radiusFixed = false; this.radiusFixed = false;
@ -150,7 +150,7 @@ Node.prototype.setProperties = function(properties, constants) {
if (properties.y !== undefined) {this.y = properties.y;} if (properties.y !== undefined) {this.y = properties.y;}
if (properties.value !== undefined) {this.value = properties.value;} if (properties.value !== undefined) {this.value = properties.value;}
// navigationUI properties
// navigation controls properties
if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;} if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;} if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;}
if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;} if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;}

+ 8
- 8
src/graph/SectorsMixin.js View File

@ -72,14 +72,14 @@ var SectorMixin = {
/** /**
* This function sets the global references to nodes, edges and nodeIndices to * This function sets the global references to nodes, edges and nodeIndices to
* those of the navigationUI sector.
* those of the navigation controls sector.
* *
* @private * @private
*/ */
_switchToUISector : function() {
this.nodeIndices = this.sectors["navigationUI"]["nodeIndices"];
this.nodes = this.sectors["navigationUI"]["nodes"];
this.edges = this.sectors["navigationUI"]["edges"];
_switchToNavigationSector : function() {
this.nodeIndices = this.sectors["navigation"]["nodeIndices"];
this.nodes = this.sectors["navigation"]["nodes"];
this.edges = this.sectors["navigation"]["edges"];
}, },
@ -432,7 +432,7 @@ var SectorMixin = {
/** /**
* This runs a function in the navigationUI sector.
* 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 * @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 * | we don't pass the function itself because then the "this" is the window object
@ -440,8 +440,8 @@ var SectorMixin = {
* @param {*} [argument] | Optional: arguments to pass to the runFunction * @param {*} [argument] | Optional: arguments to pass to the runFunction
* @private * @private
*/ */
_doInUISector : function(runFunction,argument) {
this._switchToUISector();
_doInNavigationSector : function(runFunction,argument) {
this._switchToNavigationSector();
if (argument === undefined) { if (argument === undefined) {
this[runFunction](); this[runFunction]();
} }

+ 69
- 14
src/graph/SelectionMixin.js View File

@ -33,14 +33,14 @@ var SelectionMixin = {
/** /**
* retrieve all nodes in the navigationUI overlapping with given object
* retrieve all nodes in the navigation controls overlapping with given object
* @param {Object} object An object with parameters left, top, right, bottom * @param {Object} object An object with parameters left, top, right, bottom
* @return {Number[]} An array with id's of the overlapping nodes * @return {Number[]} An array with id's of the overlapping nodes
* @private * @private
*/ */
_getAllUINodesOverlappingWith : function (object) {
_getAllNavigationNodesOverlappingWith : function (object) {
var overlappingNodes = []; var overlappingNodes = [];
this._doInUISector("_getNodesOverlappingWith",object,overlappingNodes);
this._doInNavigationSector("_getNodesOverlappingWith",object,overlappingNodes);
return overlappingNodes; return overlappingNodes;
}, },
@ -80,17 +80,17 @@ var SelectionMixin = {
/** /**
* Get the top navigationUI node at the a specific point (like a click)
* Get the top navigation controls node at the a specific point (like a click)
* *
* @param {{x: Number, y: Number}} pointer * @param {{x: Number, y: Number}} pointer
* @return {Node | null} node * @return {Node | null} node
* @private * @private
*/ */
_getUINodeAt : function (pointer) {
_getNavigationNodeAt : function (pointer) {
var screenPositionObject = this._pointerToScreenPositionObject(pointer); var screenPositionObject = this._pointerToScreenPositionObject(pointer);
var overlappingNodes = this._getAllUINodesOverlappingWith(screenPositionObject);
var overlappingNodes = this._getAllNavigationNodesOverlappingWith(screenPositionObject);
if (overlappingNodes.length > 0) { if (overlappingNodes.length > 0) {
return this.sectors["navigationUI"]["nodes"][overlappingNodes[overlappingNodes.length - 1]];
return this.sectors["navigation"]["nodes"][overlappingNodes[overlappingNodes.length - 1]];
} }
else { else {
return null; return null;
@ -106,7 +106,7 @@ var SelectionMixin = {
* @private * @private
*/ */
_getNodeAt : function (pointer) { _getNodeAt : function (pointer) {
// we first check if this is an navigationUI element
// we first check if this is an navigation controls element
var positionObject = this._pointerToPositionObject(pointer); var positionObject = this._pointerToPositionObject(pointer);
var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); var overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
@ -213,7 +213,9 @@ var SelectionMixin = {
this.selectionObj = {}; this.selectionObj = {};
if (doNotTrigger == false) { if (doNotTrigger == false) {
this._trigger('select');
this._trigger('select', {
nodes: this.getSelection()
});
} }
}, },
@ -307,13 +309,15 @@ var SelectionMixin = {
this._removeFromSelection(object); this._removeFromSelection(object);
} }
if (doNotTrigger == false) { if (doNotTrigger == false) {
this._trigger('select');
this._trigger('select', {
nodes: this.getSelection()
});
} }
}, },
/** /**
* handles the selection part of the touch, only for navigationUI elements;
* 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. * Touch is triggered before tap, also before hold. Hold triggers after a while.
* This is the most responsive solution * This is the most responsive solution
* *
@ -321,8 +325,8 @@ var SelectionMixin = {
* @private * @private
*/ */
_handleTouch : function(pointer) { _handleTouch : function(pointer) {
if (this.constants.navigationUI.enabled == true) {
var node = this._getUINodeAt(pointer);
if (this.constants.navigation.enabled == true) {
var node = this._getNavigationNodeAt(pointer);
if (node != null) { if (node != null) {
if (this[node.triggerFunction] !== undefined) { if (this[node.triggerFunction] !== undefined) {
this[node.triggerFunction](); this[node.triggerFunction]();
@ -395,7 +399,7 @@ var SelectionMixin = {
/** /**
* handle the onRelease event. These functions are here for the navigationUI module.
* handle the onRelease event. These functions are here for the navigation controls module.
* *
* @private * @private
*/ */
@ -497,16 +501,67 @@ var SelectionMixin = {
delete this.selectionObj[objectId]; delete this.selectionObj[objectId];
} }
} }
<<<<<<< HEAD
else { // assuming only edges and nodes are selected else { // assuming only edges and nodes are selected
if (!this.edges.hasOwnProperty(objectId)) { if (!this.edges.hasOwnProperty(objectId)) {
delete this.selectionObj[objectId]; delete this.selectionObj[objectId];
} }
=======
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;
>>>>>>> develop
} }
} }
} }
} }
<<<<<<< HEAD
=======
if (changed) {
// fire the select event
this._trigger('select', {
nodes: this.getSelection()
});
}
>>>>>>> develop
}; };

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

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

examples/graph/img/UI_icons/downarrow.png → src/graph/img/downarrow.png View File


examples/graph/img/UI_icons/leftarrow.png → src/graph/img/leftarrow.png View File


examples/graph/img/UI_icons/minus.png → src/graph/img/minus.png View File


examples/graph/img/UI_icons/plus.png → src/graph/img/plus.png View File


examples/graph/img/UI_icons/rightarrow.png → src/graph/img/rightarrow.png View File


examples/graph/img/UI_icons/uparrow.png → src/graph/img/uparrow.png View File


examples/graph/img/UI_icons/zoomExtends.png → src/graph/img/zoomExtends.png View File


+ 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); 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 * Trigger an event
* @param {String} event name of the event, available events: 'rangechange', * @param {String} event name of the event, available events: 'rangechange',
@ -139,8 +154,8 @@ Range.prototype.setRange = function(start, end) {
* @private * @private
*/ */
Range.prototype._applyRange = function(start, end) { 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, 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, min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
diff; 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: 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, 'move', 'horizontal');
this.range.subscribe(this.rootPanel, 'zoom', 'horizontal'); this.range.subscribe(this.rootPanel, 'zoom', 'horizontal');
this.range.on('rangechange', function () {
this.range.on('rangechange', function (properties) {
var force = true; var force = true;
me.controller.requestReflow(force); me.controller.requestReflow(force);
me._trigger('rangechange', properties);
}); });
this.range.on('rangechanged', function () {
this.range.on('rangechanged', function (properties) {
var force = true; var force = true;
me.controller.requestReflow(force); 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 // time axis
var timeaxisOptions = Object.create(rootOptions); var timeaxisOptions = Object.create(rootOptions);
@ -139,10 +147,9 @@ function Timeline (container, items, options) {
Timeline.prototype.setOptions = function (options) { Timeline.prototype.setOptions = function (options) {
util.extend(this.options, 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.reflow();
this.controller.repaint(); this.controller.repaint();
@ -198,29 +205,29 @@ Timeline.prototype.setItems = function(items) {
var dataRange = this.getItemRange(); var dataRange = this.getItemRange();
// add 5% space on both sides // 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) { if (interval <= 0) {
// prevent an empty interval // prevent an empty interval
interval = 24 * 60 * 60 * 1000; // 1 day 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 // override specified start and/or end date
if (this.options.start != undefined) { if (this.options.start != undefined) {
min = util.convert(this.options.start, 'Date');
start = util.convert(this.options.start, 'Date');
} }
if (this.options.end != undefined) { 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 // 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 * @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 * @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() : [];
}; };
/** /**

+ 25
- 4
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 * @return {Array} ids The ids of the selected items
*/ */
GroupSet.prototype.select = function select(ids) {
GroupSet.prototype.getSelection = function getSelection() {
var selection = [], var selection = [],
groups = this.groups; groups = this.groups;
@ -162,7 +183,7 @@ GroupSet.prototype.select = function select(ids) {
for (var id in groups) { for (var id in groups) {
if (groups.hasOwnProperty(id)) { if (groups.hasOwnProperty(id)) {
var group = groups[id]; var group = groups[id];
selection = selection.concat(group.select(ids));
selection = selection.concat(group.getSelection());
} }
} }

+ 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; var i, ii, id, item, selection;
if (ids) { if (ids) {
@ -152,11 +154,14 @@ ItemSet.prototype.select = function select(ids) {
this.requestRepaint(); 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 // 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 // reposition all items. Show items only when in the visible area
util.forEach(this.items, function (item) { util.forEach(this.items, function (item) {
@ -546,7 +553,10 @@ ItemSet.prototype._onRemove = function _onRemove(ids) {
ItemSet.prototype._toQueue = function _toQueue(action, ids) { ItemSet.prototype._toQueue = function _toQueue(action, ids) {
var queue = this.queue; var queue = this.queue;
ids.forEach(function (id) { ids.forEach(function (id) {
queue[id] = action;
queue[id] = {
id: id,
action: action
};
}); });
if (this.controller) { if (this.controller) {

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

@ -260,6 +260,9 @@ ItemBox.prototype._create = function _create() {
// dot on axis // dot on axis
dom.dot = document.createElement('DIV'); dom.dot = document.createElement('DIV');
dom.dot.className = 'dot'; 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 = document.createElement('div');
dom.dot.className = 'dot'; dom.dot.className = 'dot';
dom.point.appendChild(dom.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 = document.createElement('div');
dom.content.className = 'content'; dom.content.className = 'content';
dom.box.appendChild(dom.content); dom.box.appendChild(dom.content);
// attach this item as attribute
dom.box['timeline-item'] = this;
} }
}; };

+ 0
- 28
src/util.js View File

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

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


Loading…
Cancel
Save