Browse Source

Merge pull request #3 from almende/develop

Pull in upstream
css_transitions
Eric Gillingham 10 years ago
parent
commit
64ec756095
123 changed files with 17164 additions and 9702 deletions
  1. +1
    -0
      .gitignore
  2. +1
    -0
      .npmignore
  3. +133
    -4
      HISTORY.md
  4. +28
    -10
      Jakefile.js
  5. +1
    -1
      README.md
  6. +2
    -1
      bower.json
  7. BIN
      dist/img/graph/acceptDeleteIcon.png
  8. BIN
      dist/img/graph/addNodeIcon.png
  9. BIN
      dist/img/graph/backIcon.png
  10. BIN
      dist/img/graph/connectIcon.png
  11. BIN
      dist/img/graph/cross.png
  12. BIN
      dist/img/graph/cross2.png
  13. BIN
      dist/img/graph/deleteIcon.png
  14. +0
    -0
      dist/img/graph/downArrow.png
  15. BIN
      dist/img/graph/editIcon.png
  16. +0
    -0
      dist/img/graph/leftArrow.png
  17. +0
    -0
      dist/img/graph/minus.png
  18. +0
    -0
      dist/img/graph/plus.png
  19. +0
    -0
      dist/img/graph/rightArrow.png
  20. +0
    -0
      dist/img/graph/upArrow.png
  21. +0
    -0
      dist/img/graph/zoomExtends.png
  22. BIN
      dist/img/timeline/delete.png
  23. +311
    -67
      dist/vis.css
  24. +6989
    -4466
      dist/vis.js
  25. +1
    -0
      dist/vis.min.css
  26. +11
    -9
      dist/vis.min.js
  27. +5
    -0
      docs/css/style.css
  28. +21
    -16
      docs/dataset.html
  29. +4
    -4
      docs/dataview.html
  30. +716
    -73
      docs/graph.html
  31. +3
    -1
      docs/index.html
  32. +157
    -25
      docs/timeline.html
  33. +4
    -14
      examples/graph/02_random_nodes.html
  34. +2
    -2
      examples/graph/07_selections.html
  35. +1
    -4
      examples/graph/19_scale_free_graph_clustering.html
  36. +12
    -11
      examples/graph/20_navigation.html
  37. +224
    -0
      examples/graph/21_data_manipulation.html
  38. +373
    -0
      examples/graph/22_les_miserables.html
  39. +147
    -0
      examples/graph/23_hierarchical_layout.html
  40. +139
    -0
      examples/graph/24_hierarchical_layout_userdefined.html
  41. +110
    -0
      examples/graph/25_physics_configuration.html
  42. +1
    -1
      examples/graph/graphviz/graphviz_gallery.html
  43. +5
    -0
      examples/graph/index.html
  44. +1
    -2
      examples/timeline/03_much_data.html
  45. +1
    -1
      examples/timeline/05_groups.html
  46. +17
    -4
      examples/timeline/06_event_listeners.html
  47. +65
    -0
      examples/timeline/07_custom_time_bar.html
  48. +90
    -0
      examples/timeline/08_edit_items.html
  49. +65
    -0
      examples/timeline/09_order_groups.html
  50. +51
    -0
      examples/timeline/10_limit_move_and_zoom.html
  51. +6
    -0
      examples/timeline/index.html
  52. +2
    -12
      misc/how_to_publish.md
  53. +3
    -1
      package.json
  54. +22
    -6
      src/DataSet.js
  55. +8
    -4
      src/DataView.js
  56. +0
    -89
      src/EventBus.js
  57. +0
    -116
      src/events.js
  58. +250
    -97
      src/graph/Edge.js
  59. +631
    -639
      src/graph/Graph.js
  60. +1
    -1
      src/graph/Groups.js
  61. +0
    -245
      src/graph/NavigationMixin.js
  62. +105
    -118
      src/graph/Node.js
  63. +40
    -13
      src/graph/Popup.js
  64. +0
    -515
      src/graph/SelectionMixin.js
  65. +128
    -0
      src/graph/css/graph-manipulation.css
  66. +66
    -0
      src/graph/css/graph-navigation.css
  67. +203
    -81
      src/graph/graphMixins/ClusterMixin.js
  68. +311
    -0
      src/graph/graphMixins/HierarchicalLayoutMixin.js
  69. +435
    -0
      src/graph/graphMixins/ManipulationMixin.js
  70. +199
    -0
      src/graph/graphMixins/MixinLoader.js
  71. +205
    -0
      src/graph/graphMixins/NavigationMixin.js
  72. +44
    -39
      src/graph/graphMixins/SectorsMixin.js
  73. +570
    -0
      src/graph/graphMixins/SelectionMixin.js
  74. +375
    -0
      src/graph/graphMixins/physics/BarnesHut.js
  75. +64
    -0
      src/graph/graphMixins/physics/HierarchialRepulsion.js
  76. +665
    -0
      src/graph/graphMixins/physics/PhysicsMixin.js
  77. +66
    -0
      src/graph/graphMixins/physics/Repulsion.js
  78. BIN
      src/graph/img/acceptDeleteIcon.png
  79. BIN
      src/graph/img/addNodeIcon.png
  80. BIN
      src/graph/img/backIcon.png
  81. BIN
      src/graph/img/connectIcon.png
  82. BIN
      src/graph/img/cross.png
  83. BIN
      src/graph/img/cross2.png
  84. BIN
      src/graph/img/deleteIcon.png
  85. +0
    -0
      src/graph/img/downArrow.png
  86. BIN
      src/graph/img/editIcon.png
  87. +0
    -0
      src/graph/img/leftArrow.png
  88. +0
    -0
      src/graph/img/rightArrow.png
  89. +0
    -0
      src/graph/img/upArrow.png
  90. +0
    -3
      src/module/exports.js
  91. +1
    -2
      src/module/imports.js
  92. +0
    -172
      src/timeline/Controller.js
  93. +96
    -130
      src/timeline/Range.js
  94. +81
    -104
      src/timeline/Stack.js
  95. +43
    -38
      src/timeline/TimeStep.js
  96. +475
    -229
      src/timeline/Timeline.js
  97. +17
    -79
      src/timeline/component/Component.js
  98. +0
    -113
      src/timeline/component/ContentPanel.js
  99. +54
    -59
      src/timeline/component/CurrentTime.js
  100. +71
    -180
      src/timeline/component/CustomTime.js

+ 1
- 0
.gitignore View File

@ -1,4 +1,5 @@
.idea
*.iml
node_modules
.project
.settings/.jsdtscope

+ 1
- 0
.npmignore View File

@ -1,3 +1,4 @@
misc
node_modules
src
test

+ 133
- 4
HISTORY.md View File

@ -1,14 +1,143 @@
vis.js history
# vis.js history
http://visjs.org
## not yet released, version 0.8.0
### Timeline
- Large refactoring of the Timeline, simplifying the code.
- Performance improvements.
- Improved layout of box-items inside groups.
- Function `setWindow` now accepts an object with properties `start` and `end`.
- Fixed option `autoResize` forcing a repaint of the Timeline with every check
rather than when the Timeline is actually resized.
- Fixed `select` event fired repeatedly when clicking an empty place on the
Timeline, deselecting selected items).
- Fixed initial visible window in case items exceed `zoomMax`. Thanks @Remper.
- Option `order` is now deprecated. This was needed for performance improvements.
- Minor bug fixes.
- More examples added.
### DataSet
- A DataSet can now be constructed with initial data, like
`new DataSet(data, options)`.
## 2014-04-18, version 0.7.4
### Graph
- fixed IE9 bug.
- style fixes.
- minor bug fixes.
## 2014-04-16, version 0.7.3
### Graph
- fixed color bug.
- added pull requests from kannonboy and vierja: tooltip styling, label fill color
## 2014-04-09, version 0.7.2
### Graph
- fixed edge select bug.
- fixed zoom bug on empty initialization.
## 2014-03-27, version 0.7.1
### Graph
- fixed edge color bug.
- fixed select event bug.
- clarified docs, stressing importance of css inclusion for correct display of navigation an manipulation icons.
- improved and expanded playing with physics (configurePhysics option).
- added highlights to navigation icons if the corresponding key is pressed.
- added freezeForStabilization option to improve stabilization with cached positions.
## 2014-03-07, version 0.7.0
### Graph
- changed navigation CSS. Icons are now always correctly positioned.
- added stabilizationIterations option to graph.
- added storePosition() method to save the XY positions of nodes in the DataSet.
- separated allowedToMove into allowedToMoveX and allowedToMoveY. This is required for initializing nodes from hierarchical layouts after storePosition().
- added color options for the edges.
## 2014-03-06, version 0.6.1
### Graph
- Bugfix graphviz examples.
- Bugfix labels position for smooth curves.
- Tweaked graphviz example physics.
- Updated physics documentation to stress importance of configurePhysics.
### Timeline
- Fixed a bug with options `margin.axis` and `margin.item` being ignored when setting them to zero.
- Some clarifications in the documentation.
## 2014-03-05, version 0.6.0
### Graph
- Added Physics Configuration option. This makes tweaking the physics system to suit your needs easier.
- Click and doubleClick events.
- Initial zoom bugfix.
- Directions for Hierarchical layout.
- Refactoring and bugfixes.
## 2014-02-20, version 0.5.1
- Fixed broken bower module.
## 2014-02-20, version 0.5.0
### Timeline
- Editable Items: drag items, add new items, update items, and remove items.
- Implemented options `selectable`, `editable`.
- Added events `timechange` and `timechanged` when dragging the custom time bar.
- Multiple items can be selected using ctrl+click or shift+click.
- Implemented functions `setWindow(start, end)` and `getWindow()`.
- Fixed scroll to zoom not working on IE in standards mode.
### Graph
- Editable nodes and edges: create, update, and remove them.
- Support for smooth, curved edges (on by default).
- Performance improvements.
- Fixed scroll to zoom not working on IE in standards mode.
- Added hierarchical layout option.
- Overhauled physics system, now using Barnes-Hut simulation by default. Great performance gains.
- Modified clustering system to give better results.
- Adaptive performance system to increase visual performance (60fps target).
### DataSet
- Renamed functions `subscribe` and `unsubscribe` to `on` and `off` respectively.
## 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.
- Implemented 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.
@ -20,7 +149,7 @@ http://visjs.org
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
- Added automatic initial 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.
@ -35,7 +164,7 @@ http://visjs.org
- Moved the generated library to folder `./dist`
- Css stylesheet must be loaded explicitly now.
- Implemented options `showCurrentTime` and `showCustomTime`. Thanks fi0dor.
- Implemented options `showCurrentTime` and `showCustomTime`. Thanks @fi0dor.
- Implemented touch support for Timeline.
- Fixed broken Timeline options `min` and `max`.
- Fixed not being able to load vis.js in node.js.

+ 28
- 10
Jakefile.js View File

@ -4,6 +4,7 @@
var jake = require('jake'),
browserify = require('browserify'),
wrench = require('wrench'),
CleanCSS = require('clean-css'),
fs = require('fs');
require('jake-utils');
@ -14,6 +15,7 @@ var VIS = DIST + '/vis.js';
var VIS_CSS = DIST + '/vis.css';
var VIS_TMP = DIST + '/vis.js.tmp';
var VIS_MIN = DIST + '/vis.min.js';
var VIS_MIN_CSS = DIST + '/vis.min.css';
/**
* default task
@ -29,6 +31,8 @@ task('default', ['build', 'minify'], function () {
desc('Build the visualization library vis.js');
task('build', {async: true}, function () {
jake.mkdirP(DIST);
jake.mkdirP(DIST + '/img');
// concatenate and stringify the css files
concat({
src: [
@ -39,7 +43,10 @@ task('build', {async: true}, function () {
'./src/timeline/component/css/item.css',
'./src/timeline/component/css/timeaxis.css',
'./src/timeline/component/css/currenttime.css',
'./src/timeline/component/css/customtime.css'
'./src/timeline/component/css/customtime.css',
'./src/graph/css/graph-manipulation.css',
'./src/graph/css/graph-navigation.css'
],
dest: VIS_CSS,
separator: '\n'
@ -54,15 +61,12 @@ task('build', {async: true}, function () {
'./src/shim.js',
'./src/util.js',
'./src/events.js',
'./src/EventBus.js',
'./src/DataSet.js',
'./src/DataView.js',
'./src/timeline/TimeStep.js',
'./src/timeline/Stack.js',
'./src/timeline/Range.js',
'./src/timeline/Controller.js',
'./src/timeline/component/Component.js',
'./src/timeline/component/Panel.js',
'./src/timeline/component/RootPanel.js',
@ -82,10 +86,17 @@ task('build', {async: true}, function () {
'./src/graph/Popup.js',
'./src/graph/Groups.js',
'./src/graph/Images.js',
'./src/graph/SectorsMixin.js',
'./src/graph/ClusterMixin.js',
'./src/graph/SelectionMixin.js',
'./src/graph/NavigationMixin.js',
'./src/graph/graphMixins/physics/PhysicsMixin.js',
'./src/graph/graphMixins/physics/HierarchialRepulsion.js',
'./src/graph/graphMixins/physics/BarnesHut.js',
'./src/graph/graphMixins/physics/Repulsion.js',
'./src/graph/graphMixins/HierarchicalLayoutMixin.js',
'./src/graph/graphMixins/ManipulationMixin.js',
'./src/graph/graphMixins/SectorsMixin.js',
'./src/graph/graphMixins/ClusterMixin.js',
'./src/graph/graphMixins/SelectionMixin.js',
'./src/graph/graphMixins/NavigationMixin.js',
'./src/graph/graphMixins/MixinLoader.js',
'./src/graph/Graph.js',
'./src/module/exports.js'
@ -95,7 +106,10 @@ task('build', {async: true}, function () {
});
// copy images
wrench.copyDirSyncRecursive('./src/graph/img', DIST+ '/img', {
wrench.copyDirSyncRecursive('./src/graph/img', DIST + '/img/graph', {
forceDelete: true
});
wrench.copyDirSyncRecursive('./src/timeline/img', DIST + '/img/timeline', {
forceDelete: true
});
@ -131,7 +145,7 @@ task('build', {async: true}, function () {
* minify the visualization library vis.js
*/
desc('Minify the visualization library vis.js');
task('minify', function () {
task('minify', {async: true}, function () {
// minify javascript
minify({
src: VIS,
@ -143,6 +157,10 @@ task('minify', function () {
replacePlaceholders(VIS_MIN);
console.log('created minified ' + VIS_MIN);
var minified = new CleanCSS().minify(read(VIS_CSS));
write(VIS_MIN_CSS, minified);
console.log('created minified ' + VIS_MIN_CSS);
});
/**

+ 1
- 1
README.md View File

@ -143,7 +143,7 @@ Then, the project can be build running:
## Test
To test teh library, install the project dependencies once:
To test the library, install the project dependencies once:
npm install

+ 2
- 1
bower.json View File

@ -1,6 +1,6 @@
{
"name": "vis",
"version": "0.5.0-SNAPSHOT",
"version": "0.7.5-SNAPSHOT",
"description": "A dynamic, browser-based visualization library.",
"homepage": "http://visjs.org/",
"repository": {
@ -8,6 +8,7 @@
"url": "git://github.com/almende/vis.git"
},
"ignore": [
"misc",
"node_modules",
"src",
"test",

BIN
dist/img/graph/acceptDeleteIcon.png View File

Before After
Width: 24  |  Height: 24  |  Size: 20 KiB

BIN
dist/img/graph/addNodeIcon.png View File

Before After
Width: 24  |  Height: 24  |  Size: 20 KiB

BIN
dist/img/graph/backIcon.png View File

Before After
Width: 24  |  Height: 24  |  Size: 20 KiB

BIN
dist/img/graph/connectIcon.png View File

Before After
Width: 24  |  Height: 24  |  Size: 20 KiB

BIN
dist/img/graph/cross.png View File

Before After
Width: 7  |  Height: 7  |  Size: 18 KiB

BIN
dist/img/graph/cross2.png View File

Before After
Width: 5  |  Height: 5  |  Size: 17 KiB

BIN
dist/img/graph/deleteIcon.png View File

Before After
Width: 24  |  Height: 24  |  Size: 20 KiB

dist/img/downarrow.png → dist/img/graph/downArrow.png View File


BIN
dist/img/graph/editIcon.png View File

Before After
Width: 24  |  Height: 24  |  Size: 20 KiB

dist/img/leftarrow.png → dist/img/graph/leftArrow.png View File


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


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


dist/img/rightarrow.png → dist/img/graph/rightArrow.png View File


dist/img/uparrow.png → dist/img/graph/upArrow.png View File


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


BIN
dist/img/timeline/delete.png View File

Before After
Width: 16  |  Height: 16  |  Size: 665 B

+ 311
- 67
dist/vis.css View File

@ -9,79 +9,86 @@
border: 1px solid #bfbfbf;
-moz-box-sizing: border-box;
box-sizing: border-box;
/* FIXME: there is an issue with the height of the items when panel height is animated
-webkit-transition: height 4s ease-in-out;
transition: height 4s ease-in-out;
/**/
}
.vis.timeline .panel {
.vis.timeline .vpanel {
position: absolute;
overflow: hidden;
}
.vis.timeline .groupset {
position: absolute;
padding: 0;
margin: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.vis.timeline .labels {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
.vis.timeline .vpanel.side {
border-right: 1px solid #bfbfbf;
}
padding: 0;
margin: 0;
.vis.timeline .vpanel.side.hidden {
display: none;
}
border-right: 1px solid #bfbfbf;
box-sizing: border-box;
-moz-box-sizing: border-box;
.vis.timeline .groupset {
position: relative;
}
.vis.timeline .labels .label-set {
position: absolute;
top: 0;
left: 0;
.vis.timeline .labelset {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
border-top: none;
border-bottom: 1px solid #bfbfbf;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.vis.timeline .labels .label-set .label {
position: absolute;
.vis.timeline .labelset .vlabel {
position: relative;
left: 0;
top: 0;
width: 100%;
color: #4d4d4d;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.vis.timeline.top .labels .label-set .label,
.vis.timeline.top .groupset .itemset-axis {
.vis.timeline.bottom .labelset .vlabel,
.vis.timeline.top .vpanel.side-content,
.vis.timeline.top .groupset .itemset {
border-top: 1px solid #bfbfbf;
border-bottom: none;
}
.vis.timeline.bottom .labels .label-set .label,
.vis.timeline.bottom .groupset .itemset-axis {
.vis.timeline.top .labelset .vlabel,
.vis.timeline.bottom .vpanel.side-content,
.vis.timeline.bottom .groupset .itemset {
border-top: none;
border-bottom: 1px solid #bfbfbf;
}
.vis.timeline .labels .label-set .label .inner {
.vis.timeline .labelset .vlabel .inner {
display: inline-block;
padding: 5px;
}
.vis.timeline .itemset {
position: absolute;
position: relative;
padding: 0;
margin: 0;
overflow: hidden;
-moz-box-sizing: border-box;
box-sizing: border-box;
/* FIXME: get transition working for rootpanel and itemset
-webkit-transition: height 4s ease-in-out;
transition: height 4s ease-in-out;
/**/
}
.vis.timeline .background {
@ -90,8 +97,8 @@
.vis.timeline .foreground {
}
.vis.timeline .itemset-axis {
position: absolute;
.vis.timeline .axis {
overflow: visible;
}
@ -101,6 +108,12 @@
border-color: #97B0F8;
background-color: #D5DDF6;
display: inline-block;
padding: 5px;
/* TODO: enable css transitions
-webkit-transition: top .4s ease-in-out, bottom .4s ease-in-out;
transition: top .4s ease-in-out, bottom .4s ease-in-out;
/**/
}
.vis.timeline .item.selected {
@ -109,11 +122,16 @@
z-index: 999;
}
.vis.timeline.editable .item.selected {
cursor: move;
}
.vis.timeline .item.point.selected {
background-color: #FFF785;
z-index: 999;
}
.vis.timeline .item.point.selected .dot {
.vis.timeline .item.point.selected .dot,
.vis.timeline .item.dot.selected {
border-color: #FFC200;
}
@ -138,68 +156,100 @@
background: none;
}
.vis.timeline .dot {
.vis.timeline .dot,
.vis.timeline .item.dot {
padding: 0;
border: 5px solid #97B0F8;
position: absolute;
border-radius: 5px;
-moz-border-radius: 5px; /* For Firefox 3.6 and older */
}
.vis.timeline .item.range {
overflow: hidden;
border-style: solid;
border-width: 1px;
border-radius: 2px;
-moz-border-radius: 2px; /* For Firefox 3.6 and older */
}
.vis.timeline .item.rangeoverflow {
.vis.timeline .item.range,
.vis.timeline .item.rangeoverflow{
border-style: solid;
border-width: 1px;
border-radius: 2px;
-moz-border-radius: 2px; /* For Firefox 3.6 and older */
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.vis.timeline .item.range .drag-left, .vis.timeline .item.rangeoverflow .drag-left {
cursor: w-resize;
z-index: 1000;
}
.vis.timeline .item.range .drag-right, .vis.timeline .item.rangeoverflow .drag-right {
cursor: e-resize;
z-index: 1000;
}
.vis.timeline .item.range .content, .vis.timeline .item.rangeoverflow .content {
.vis.timeline .item.range .content,
.vis.timeline .item.rangeoverflow .content {
position: relative;
display: inline-block;
}
.vis.timeline .item.range .content {
overflow: hidden;
max-width: 100%;
}
.vis.timeline .item.line {
padding: 0;
position: absolute;
width: 0;
border-left-width: 1px;
border-left-style: solid;
/* TODO: enable css transitions
-webkit-transition: height .4s ease-in-out, top .4s ease-in-out;
transition: height .4s ease-in-out, top .4s ease-in-out;
/**/
}
.vis.timeline .item .content {
margin: 5px;
white-space: nowrap;
overflow: hidden;
}
.vis.timeline .axis {
position: relative;
.vis.timeline .item .delete {
background: url('img/timeline/delete.png') no-repeat top center;
position: absolute;
width: 24px;
height: 24px;
top: 0;
right: -24px;
cursor: pointer;
}
.vis.timeline .axis .text {
.vis.timeline .item.range .drag-left,
.vis.timeline .item.rangeoverflow .drag-left {
position: absolute;
width: 24px;
height: 100%;
top: 0;
left: -4px;
cursor: w-resize;
z-index: 10000;
}
.vis.timeline .item.range .drag-right,
.vis.timeline .item.rangeoverflow .drag-right {
position: absolute;
width: 24px;
height: 100%;
top: 0;
right: -4px;
cursor: e-resize;
z-index: 10001; /* a little higher z-index than .drag-left */
}
.vis.timeline .timeaxis {
position: absolute;
}
.vis.timeline .timeaxis .text {
position: absolute;
color: #4d4d4d;
padding: 3px;
white-space: nowrap;
}
.vis.timeline .axis .text.measure {
.vis.timeline .timeaxis .text.measure {
position: absolute;
padding-left: 0;
padding-right: 0;
@ -208,13 +258,13 @@
visibility: hidden;
}
.vis.timeline .axis .grid.vertical {
.vis.timeline .timeaxis .grid.vertical {
position: absolute;
width: 0;
border-right: 1px solid;
}
.vis.timeline .axis .grid.horizontal {
.vis.timeline .timeaxis .grid.horizontal {
position: absolute;
left: 0;
width: 100%;
@ -222,11 +272,11 @@
border-bottom: 1px solid;
}
.vis.timeline .axis .grid.minor {
.vis.timeline .timeaxis .grid.minor {
border-color: #e5e5e5;
}
.vis.timeline .axis .grid.major {
.vis.timeline .timeaxis .grid.major {
border-color: #bfbfbf;
}
@ -241,3 +291,197 @@
cursor: move;
z-index: 9;
}
div.graph-manipulationDiv {
border-width:0px;
border-bottom: 1px;
border-style:solid;
border-color: #d6d9d8;
background: #ffffff; /* Old browsers */
background: -moz-linear-gradient(top, #ffffff 0%, #fcfcfc 48%, #fafafa 50%, #fcfcfc 100%); /* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(48%,#fcfcfc), color-stop(50%,#fafafa), color-stop(100%,#fcfcfc)); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* IE10+ */
background: linear-gradient(to bottom, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#fcfcfc',GradientType=0 ); /* IE6-9 */
width: 600px;
height:30px;
z-index:10;
position:absolute;
}
div.graph-manipulation-editMode {
height:30px;
z-index:10;
position:absolute;
margin-top:20px;
}
div.graph-manipulation-closeDiv {
height:30px;
width:30px;
z-index:11;
position:absolute;
margin-top:3px;
margin-left:590px;
background-position: 0px 0px;
background-repeat:no-repeat;
background-image: url("img/graph/cross.png");
cursor: pointer;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
span.graph-manipulationUI {
font-family: verdana;
font-size: 12px;
-moz-border-radius: 15px;
border-radius: 15px;
display:inline-block;
background-position: 0px 0px;
background-repeat:no-repeat;
height:24px;
margin: -14px 0px 0px 10px;
vertical-align:middle;
cursor: pointer;
padding: 0px 8px 0px 8px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
span.graph-manipulationUI:hover {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.20);
}
span.graph-manipulationUI:active {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.50);
}
span.graph-manipulationUI.back {
background-image: url("img/graph/backIcon.png");
}
span.graph-manipulationUI.none:hover {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0);
cursor: default;
}
span.graph-manipulationUI.none:active {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0);
}
span.graph-manipulationUI.none {
padding: 0px 0px 0px 0px;
}
span.graph-manipulationUI.notification{
margin: 2px;
font-weight: bold;
}
span.graph-manipulationUI.add {
background-image: url("img/graph/addNodeIcon.png");
}
span.graph-manipulationUI.edit {
background-image: url("img/graph/editIcon.png");
}
span.graph-manipulationUI.edit.editmode {
background-color: #fcfcfc;
border-style:solid;
border-width:1px;
border-color: #cccccc;
}
span.graph-manipulationUI.connect {
background-image: url("img/graph/connectIcon.png");
}
span.graph-manipulationUI.delete {
background-image: url("img/graph/deleteIcon.png");
}
/* top right bottom left */
span.graph-manipulationLabel {
margin: 0px 0px 0px 23px;
line-height: 25px;
}
div.graph-seperatorLine {
display:inline-block;
width:1px;
height:20px;
background-color: #bdbdbd;
margin: 5px 7px 0px 15px;
}
div.graph-navigation {
width:34px;
height:34px;
z-index:10;
-moz-border-radius: 17px;
border-radius: 17px;
position:absolute;
display:inline-block;
background-position: 2px 2px;
background-repeat:no-repeat;
cursor: pointer;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
div.graph-navigation:hover {
box-shadow: 0px 0px 3px 3px rgba(56, 207, 21, 0.30);
}
div.graph-navigation:active {
box-shadow: 0px 0px 1px 3px rgba(56, 207, 21, 0.95);
}
div.graph-navigation.active {
box-shadow: 0px 0px 1px 3px rgba(56, 207, 21, 0.95);
}
div.graph-navigation.up {
background-image: url("img/graph/upArrow.png");
bottom:50px;
left:55px;
}
div.graph-navigation.down {
background-image: url("img/graph/downArrow.png");
bottom:10px;
left:55px;
}
div.graph-navigation.left {
background-image: url("img/graph/leftArrow.png");
bottom:10px;
left:15px;
}
div.graph-navigation.right {
background-image: url("img/graph/rightArrow.png");
bottom:10px;
left:95px;
}
div.graph-navigation.zoomIn {
background-image: url("img/graph/plus.png");
bottom:10px;
right:15px;
}
div.graph-navigation.zoomOut {
background-image: url("img/graph/minus.png");
bottom:10px;
right:55px;
}
div.graph-navigation.zoomExtends {
background-image: url("img/graph/zoomExtends.png");
bottom:50px;
right:15px;
}

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


+ 1
- 0
dist/vis.min.css
File diff suppressed because it is too large
View File


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


+ 5
- 0
docs/css/style.css View File

@ -75,3 +75,8 @@ td {
padding: 5px;
vertical-align: top;
}
p.important_note {
color: #3a6baa;
font-weight:bold;
}

+ 21
- 16
docs/dataset.html View File

@ -62,8 +62,8 @@ data.add([
]);
// subscribe to any change in the DataSet
data.subscribe('*', function (event, params, senderId) {
console.log('event', event, params);
data.on('*', function (event, properties, senderId) {
console.log('event', event, properties);
});
// update an existing item
@ -107,7 +107,7 @@ console.log('formatted items', items);
</p>
<pre class="prettyprint lang-js">
var data = new vis.DataSet(options)
var data = new vis.DataSet([data] [, options])
</pre>
<p>
@ -116,6 +116,11 @@ var data = new vis.DataSet(options)
<a href="#Data_Manipulation">Data Manipulation</a>.
</p>
<p>
The parameter <code>data</code>code> is optional and can be an Array or
Google DataTable with items.
</p>
<p>
The parameter <code>options</code> is optional and is an object which can
contain the following properties:
@ -545,8 +550,8 @@ var items = data.get({
<p>
One can subscribe on changes in a DataSet.
A subscription can be created using the method <code>subscribe</code>,
and removed with <code>unsubscribe</code>.
A subscription can be created using the method <code>on</code>,
and removed with <code>off</code>.
</p>
<pre class="prettyprint lang-js">
@ -554,8 +559,8 @@ var items = data.get({
var data = new vis.DataSet();
// subscribe to any change in the DataSet
data.subscribe('*', function (event, params, senderId) {
console.log('event:', event, 'params:', params, 'senderId:', senderId);
data.on('*', function (event, properties, senderId) {
console.log('event:', event, 'properties:', properties, 'senderId:', senderId);
});
// add an item
@ -565,14 +570,14 @@ data.remove(1); // triggers an 'remove' event
</pre>
<h3 id="Subscribe">Subscribe</h3>
<h3 id="On">On</h3>
<p>
Subscribe to an event.
</p>
Syntax:
<pre class="prettyprint lang-js">DataSet.subscribe(event, callback)</pre>
<pre class="prettyprint lang-js">DataSet.on(event, callback)</pre>
Where:
<ul>
@ -587,17 +592,17 @@ Where:
</li>
</ul>
<h3 id="Unsubscribe">Unsubscribe</h3>
<h3 id="Off">Off</h3>
<p>
Unsubscribe from an event.
</p>
Syntax:
<pre class="prettyprint lang-js">DataSet.unsubscribe(event, callback)</pre>
<pre class="prettyprint lang-js">DataSet.off(event, callback)</pre>
Where <code>event</code> and <code>callback</code> correspond with the
parameters used to <a href="#Subscribe">subscribe</a> to the event.
parameters used to <a href="#On">subscribe</a> to the event.
<h3 id="Events">Events</h3>
@ -650,7 +655,7 @@ parameters used to subscribe to the event.
</p>
<pre class="prettyprint lang-js">
function (event, params, senderId) {
function (event, properties, senderId) {
// handle the event
});
</pre>
@ -674,13 +679,13 @@ function (event, params, senderId) {
</td>
</tr>
<tr>
<td>params</td>
<td>properties</td>
<td>Object&nbsp;|&nbsp;null</td>
<td>
Optional parameters providing more information on the event.
Optional properties providing more information on the event.
In case of the events <code>add</code>,
<code>update</code>, and <code>remove</code>,
<code>params</code> is always an object containing a property
<code>properties</code> is always an object containing a property
items, which contains an array with the ids of the affected
items.
</td>

+ 4
- 4
docs/dataview.html View File

@ -63,8 +63,8 @@ var view = new vis.DataView(data, {
});
// subscribe to any change in the DataView
view.subscribe('*', function (event, params, senderId) {
console.log('event', event, params);
view.on('*', function (event, properties, senderId) {
console.log('event', event, properties);
});
// update an item in the data set
@ -201,8 +201,8 @@ var view = new vis.DataView({
});
// subscribe to any change in the DataView
view.subscribe('*', function (event, params, senderId) {
console.log('event:', event, 'params:', params, 'senderId:', senderId);
view.on('*', function (event, properties, senderId) {
console.log('event:', event, 'properties:', properties, 'senderId:', senderId);
});
// add, update, and remove data in the DataSet...

+ 716
- 73
docs/graph.html View File

@ -24,7 +24,14 @@
<p>
The graph visualization works smooth on any modern browser for up to a
few hundred nodes and edges.
few thousand nodes and edges. To handle a larger amount of nodes, Graph
has <a href="#Clustering">clustering</a> support.
</p>
<p>
Every dataset is different. Nodes can have different sizes based on content, interconnectivity can be high or low etc. Because of this, graph has a special option
that the user can use to explore which settings may be good for you. Use configurePhysics as described in the <u><a href="#PhysicsConfiguration">Physics</a></u> section or by
<u><a href="../examples/graph/25_physics_configuration.html">example 25</a></u>.
</p>
<p>
@ -53,9 +60,14 @@
<li><a href="#Nodes_configuration">Nodes</a></li>
<li><a href="#Edges_configuration">Edges</a></li>
<li><a href="#Groups_configuration">Groups</a></li>
<li><a href="#Physics">Physics</a></li>
<li><a href="#Data_manipulation">Data manipulation</a></li>
<li><a href="#Clustering">Clustering</a></li>
<li><a href="#Navigation_controls">Navigation controls</a></li>
<li><a href="#Keyboard_navigation">Keyboard navigation</a></li>
<li><a href="#Hierarchical_layout">Hierarchical layout</a></li>
<li><a href="#Localization">Localization</a></li>
<li><a href="#Tooltips">Tooltips</a></li>
</ul>
</li>
<li><a href="#Methods">Methods</a></li>
@ -288,6 +300,22 @@ var nodes = [
the same color schema.</td>
</tr>
<tr>
<td>allowedToMoveX</td>
<td>Boolean</td>
<td>false</td>
<td>If allowedToMoveX is false, then the node will not move from its supplied position.
If an X position has been supplied, it is fixed in the X-direction.
If no X value has been supplied, this argument will not do anything.</td>
</tr>
<tr>
<td>allowedToMoveY</td>
<td>Boolean</td>
<td>false</td>
<td>If allowedToMoveY is false, then the node will not move from its supplied position.
If an Y position has been supplied, it is fixed in the Y-direction.
If no Y value has been supplied, this argument will not do anything.</td>
</tr>
<tr>
<td>fontColor</td>
<td>String</td>
@ -324,8 +352,21 @@ var nodes = [
<td>string</td>
<td>no</td>
<td>Url of an image. Only applicable when the shape of the node is
<code>image</code>.</td>
<code>image</code>.</td>
</tr>
<tr>
<td>mass</td>
<td>number</td>
<td>1</td>
<td>When using the Barnes Hut simulation method (which is selected by default),
the mass of a node determines the gravitational repulsion during the simulation. Higher mass will push other nodes further away.</td>
</tr>
<tr>
<td>level</td>
<td>number</td>
<td>-1</td>
<td>This level is used in the hierarchical layout. If this is not selected, the level does not do anything.</td>
</tr>
<tr>
<td>radius</td>
@ -377,10 +418,11 @@ var nodes = [
<tr>
<td>title</td>
<td>string</td>
<td>string | function</td>
<td>no</td>
<td>Title to be displayed when the user hovers over the node.
The title can contain HTML code.</td>
The title can contain HTML code. If using a function, returning <code>undefined</code>
will prevent the tooltip from being displayed.</td>
</tr>
<tr>
@ -449,12 +491,26 @@ var edges = [
<th>Description</th>
</tr>
<tr>
<td>color</td>
<td>string</td>
<td>no</td>
<td>A HTML color for the edge.</td>
</tr>
<tr>
<td>color</td>
<td>String | Object</td>
<td>no</td>
<td>Color for the edge.</td>
</tr>
<tr>
<td>color.color</td>
<td>String</td>
<td>no</td>
<td>Color of the edge when not selected.</td>
</tr>
<tr>
<td>color.highlight</td>
<td>String</td>
<td>no</td>
<td>Color of the edge when selected.</td>
</tr>
<tr>
<td>dash</td>
@ -520,6 +576,14 @@ var edges = [
Only applicable when property <code>label</code> is defined.</td>
</tr>
<tr>
<td>fontFill</td>
<td>string</td>
<td>no</td>
<td>Font fill for the background color of the text label of the edge.
Only applicable when property <code>label</code> is defined.</td>
</tr>
<tr>
<td>from</td>
<td>Number | String</td>
@ -529,13 +593,6 @@ var edges = [
type.</td>
</tr>
<tr>
<td>length</td>
<td>number</td>
<td>no</td>
<td>The length of the edge in pixels.</td>
</tr>
<tr>
<td>style</td>
<td>string</td>
@ -552,13 +609,19 @@ var edges = [
<td>no</td>
<td>Text label to be displayed halfway the edge.</td>
</tr>
<tr>
<td>length</td>
<td>number</td>
<td>physics.[method].springLength</td>
<td>The resting length of the edge when modeled as a spring. By default the springLength determined by the physics is used. By using this setting you can make certain edges have different resting lengths.</td>
</tr>
<tr>
<td>title</td>
<td>string</td>
<td>string | function</td>
<td>no</td>
<td>Title to be displayed when the user hovers over the edge.
The title can contain HTML code.</td>
The title can contain HTML code. If using a function, returning <code>undefined</code>
will prevent the tooltip from being displayed.</td>
</tr>
<tr>
@ -647,6 +710,34 @@ var options = {
<th>Description</th>
</tr>
<tr>
<td><a href="#Physics">physics</a></td>
<td>Object</td>
<td>none</td>
<td>
Configuration of the physics system governing the simulation of the nodes and edges.
Barnes-Hut nBody simulation is used by default. See section <a href="#Physics">Physics</a> for an overview of the available options.
</td>
</tr>
<tr>
<td><a href="#Physics">configurePhysics</a></td>
<td>Boolean</td>
<td>false</td>
<td>
Enabling this setting will create a physics configuration div above the graph. You can use this to fine tune the physics system to suit your needs.
Because of the many possible configurations, there is not a one-size-fits-all setting. By using this tool, you can adapt the physics to your dataset.
</td>
</tr>
<tr>
<td><a href="#Data_manipulation">dataManipulation</a></td>
<td>Object</td>
<td>none</td>
<td>
Settings for manipulating the Dataset. See section <a href="#Data_manipulation">Data manipulation</a> for an overview of the available options.
</td>
</tr>
<tr>
<td><a href="#Clustering">clustering</a></td>
<td>Object</td>
@ -665,6 +756,17 @@ var options = {
</td>
</tr>
<tr>
<td>freezeForStabilization</a></td>
<td>Boolean</td>
<td>false</td>
<td>
With the advent of the storePosition() function, the positions of the nodes can be saved after they are stabilized. The smoothCurves require support nodes and those positions are not stored. In order
to speed up the initialization of the graph by using storePosition() and loading the nodes with the stored positions, the freezeForStabilization option freezes all nodes that have been supplied with
an x and y position in place during the stabilization. That way only the support nodes for the smooth curves have to stabilize, greatly speeding up the stabilization process with cached positions.
</td>
</tr>
<tr>
<td><a href="#Groups_configuration">groups</a></td>
<td>Object</td>
@ -710,6 +812,13 @@ var options = {
</td>
</tr>
<tr>
<td>smoothCurves</td>
<td>Boolean</td>
<td>true</td>
<td>If true, edges are drawn as smooth curves. This is more computationally intensive since the edge now is a quadratic Bezier curve with control points on both nodes and an invisible node in the center of the edge. This support node is also handed by the physics simulation.</td>
</tr>
<tr>
<td>selectable</td>
<td>Boolean</td>
@ -726,6 +835,14 @@ var options = {
the nodes move to a stabe position visibly in an animated way.</td>
</tr>
<tr>
<td>stabilizationIterations</td>
<td>Number</td>
<td>1000</td>
<td>If stabilize is set to true, this number is the (maximum) amount of physics steps the stabilization process takes
before showing the result. If your simulation takes too long to stabilize, this number can be reduced. On the other hand, if your graph is not stabilized after loading, this number can be increased.</td>
</tr>
<tr>
<td>width</td>
<td>String</td>
@ -806,6 +923,22 @@ var options = {
<td>"#2B7CE9"</td>
<td>Default border color of the node when selected.</td>
</tr>
<tr>
<td>allowedToMoveX</td>
<td>Boolean</td>
<td>false</td>
<td>If allowedToMoveX is false, then the node will not move from its supplied position.
If an X position has been supplied, it is fixed in the X-direction.
If no X value has been supplied, this argument will not do anything.</td>
</tr>
<tr>
<td>allowedToMoveY</td>
<td>Boolean</td>
<td>false</td>
<td>If allowedToMoveY is false, then the node will not move from its supplied position.
If an Y position has been supplied, it is fixed in the Y-direction.
If no Y value has been supplied, this argument will not do anything.</td>
</tr>
<tr>
<td>fontColor</td>
@ -837,7 +970,19 @@ var options = {
<td>none</td>
<td>Default image url for the nodes. only applicable to shape <code>image</code>.</td>
</tr>
<tr>
<td>mass</td>
<td>number</td>
<td>1</td>
<td>When using the Barnes Hut simulation method (which is selected by default),
the mass of a node determines the gravitational repulsion during the simulation. Higher mass will push other nodes further away.</td>
</tr>
<tr>
<td>level</td>
<td>number</td>
<td>-1</td>
<td>This level is used in the hierarchical layout. If this is not selected, the level does not do anything.</td>
</tr>
<tr>
<td>widthMin</td>
<td>Number</td>
@ -918,12 +1063,26 @@ var options = {
<th>Description</th>
</tr>
<tr>
<td>color</td>
<td>String</td>
<td>"#2B7CE9"</td>
<td>The default color of a edge.</td>
</tr>
<tr>
<td>color</td>
<td>String | Object</td>
<td>Object</td>
<td>Colors of the edge. This object contains both colors for the selected and unselected state.</td>
</tr>
<tr>
<td>color.color</td>
<td>String</td>
<td>"#848484"</td>
<td>Color of the edge when not selected.</td>
</tr>
<tr>
<td>color.highlight</td>
<td>String</td>
<td>"#848484"</td>
<td>Color of the edge when selected.</td>
</tr>
<tr>
<td>dash</td>
@ -963,13 +1122,13 @@ var options = {
<td>Default length of a gap in pixels on a dashed line.
Only applicable when the line style is <code>dash-line</code>.</td>
</tr>
<tr>
<td>length</td>
<td>number</td>
<td>physics.[method].springLength</td>
<td>The resting length of the edge when modeled as a spring. By default the springLength determined by the physics is used. By using this setting you can make certain edges have different resting lengths.</td>
</tr>
<tr>
<td>length</td>
<td>Number</td>
<td>100</td>
<td>The default length of a edge.</td>
</tr>
<tr>
<td>style</td>
<td>String</td>
@ -1122,6 +1281,248 @@ var nodes = [
</table>
<h3 id="Physics">Physics</h3>
<p>
The original simulation method was based on particel physics with a repulsion field (potential) around each node,
and the edges were modelled as springs. The new system employed the <a href="http://en.wikipedia.org/wiki/Barnes%E2%80%93Hut_simulation">Barnes-Hut</a> gravitational simulation model. The edges are still modelled as springs.
To unify the physics system, the damping, repulsion distance and edge length have been combined in an physics option. To retain good behaviour, both the old repulsion model and the Barnes-Hut model have their own parameters.
If no options for the physics system are supplied, the Barnes-Hut method will be used with the default parameters. If you want to customize the physics system easily, you can use the configurePhysics option.
<p class="important_note">Note: if the behaviour of your graph is not the way you want it, use configurePhysics as described <u><a href="#PhysicsConfiguration">below</a></u> or by <u><a href="../examples/graph/25_physics_configuration.html">example 25</a></u>.</p>
</p>
<pre class="prettyprint">
// These variables must be defined in an options object named physics.
// If a variable is not supplied, the default value is used.
var options = {
physics: {
barnesHut: {
enabled: true,
gravitationalConstant: -2000,
centralGravity: 0.1,
springLength: 95,
springConstant: 0.04,
damping: 0.09
},
repulsion: {
centralGravity: 0.1,
springLength: 50,
springConstant: 0.05,
nodeDistance: 100,
damping: 0.09
},
}
</pre>
<h5>barnesHut:</h5>
<table>
<tr>
<th>Name</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
<tr>
<td>enabled</td>
<td>Boolean</td>
<td>true</td>
<td>This switches the Barnes-Hut simulation on or off. If it is turned off, the old repulsion model is used. Barnes-Hut is generally faster and yields better results.</td>
</tr>
<tr>
<td>gravitationalConstant</td>
<td>Number</td>
<td>-2000</td>
<td>This is the gravitational constand used to calculate the gravity forces. More information is available <a href="http://en.wikipedia.org/wiki/Newton's_law_of_universal_gravitation" target="_blank">here</a>.</td>
</tr>
<tr>
<td>centralGravity</td>
<td>Number</td>
<td>0.1</td>
<td>The central gravity is a force that pulls all nodes to the center. This ensures independent groups do not float apart.</td>
</tr>
<tr>
<td>springLength</td>
<td>Number</td>
<td>95</td>
<td>In the previous versions this was a property of the edges, called length. This is the length of the springs when they are at rest. During the simulation they will be streched by the gravitational fields.
To greatly reduce the edge length, the gravitationalConstant has to be reduced as well.</td>
</tr>
<tr>
<td>springConstant</td>
<td>Number</td>
<td>0.04</td>
<td>This is the spring constant used to calculate the spring forces based on Hooke&prime;s Law. More information is available <a href="http://en.wikipedia.org/wiki/Hooke's_law" target="_blank">here</a>.</td>
</tr>
<tr>
<td>damping</td>
<td>Number</td>
<td>0.09</td>
<td>This is the damping constant. It is used to dissipate energy from the system to have it settle in an equilibrium. More information is available <a href="http://en.wikipedia.org/wiki/Damping" target="_blank">here</a>.</td>
</tr>
</table>
<h5>repulsion:</h5>
<table>
<tr>
<th>Name</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
<tr>
<td>centralGravity</td>
<td>Number</td>
<td>0.1</td>
<td>The central gravity is a force that pulls all nodes to the center. This ensures independent groups do not float apart.</td>
</tr>
<tr>
<td>springLength</td>
<td>Number</td>
<td>50</td>
<td>In the previous versions this was a property of the edges, called length. This is the length of the springs when they are at rest. During the simulation they will be streched by the gravitational fields.
To greatly reduce the edge length, the gravitationalConstant has to be reduced as well.</td>
</tr>
<tr>
<td>nodeDistance</td>
<td>Number</td>
<td>100</td>
<td>This parameter is used to define the distance of influence of the repulsion field of the nodes. Below half this distance, the repulsion is maximal and beyond twice this distance the repulsion is zero.</td>
</tr>
<tr>
<td>springConstant</td>
<td>Number</td>
<td>0.05</td>
<td>This is the spring constant used to calculate the spring forces based on Hooke&prime;s Law. More information is available <a href="http://en.wikipedia.org/wiki/Hooke's_law" target="_blank">here</a>.</td>
</tr>
<tr>
<td>damping</td>
<td>Number</td>
<td>0.09</td>
<td>This is the damping constant. It is used to dissipate energy from the system to have it settle in an equilibrium. More information is available <a href="http://en.wikipedia.org/wiki/Damping" target="_blank">here</a>.</td>
</tr>
</table>
<h4 id="PhysicsConfiguration">Configuration:</h4>
Every dataset is different. Nodes can have different sizes based on content, interconnectivity can be high or low etc. Because of this, graph has a special option
that the user can use to explore which settings may be good for him or her. This is ment to be used during the development phase when you are implementing vis.js. Once you have found
settings you are happy with, you can supply them to graph using the physics options as described above.
On start, the default settings will be loaded. Keep in mind that selecting the hierarchical simulation mode <b>disables</b> smooth curves. These will not be enabled again afterwards.
<pre class="prettyprint">
var options = {
configurePhysics:true
}
</pre>
<h3 id="Data_manipulation">Data manipulation</h3>
<p>
By using the data manipulation feature of the graph you can dynamically create nodes, connect nodes with edges, edit nodes or delete nodes and edges.
The toolbar is fully HTML and CSS so the user can style this to their preference. To control the behaviour of the data manipulation, users can insert custom functions
into the data manipulation process. For example, an injected function can show an detailed pop-up when a user wants to add a node. In <a href="../examples/graph/21_data_manipulation.html">example 21</a>,
two functions have been injected into the add and edit functionality. This is described in more detail in the next subsection. To correctly display the manipulation icons, the <b>vis.css</b> file must be included.
The user is free to alter or overload the CSS classes but without them the navigation icons are not visible.
</p>
<pre class="prettyprint">
// These variables must be defined in an options object named dataManipulation.
// If a variable is not supplied, the default value is used.
var options = {
dataManipulation: {
enabled: false,
initiallyVisible: false
}
}
// OR to just load the module with default values:
var options: {
dataManipulation: true
}
</pre>
<table>
<tr>
<th>Name</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
<tr>
<td>enabled</td>
<td>Boolean</td>
<td>false</td>
<td>Enabling or disabling of the data manipulation toolbar. If it is initially hidden, an edit button appears in the top left corner.</td>
</tr>
<tr>
<td>initiallyVisible</td>
<td>Boolean</td>
<td>false</td>
<td>Initially hide or show the data manipulation toolbar.</td>
</tr>
</table>
<h4 id="Data_manipulation_custom">Data manipulation: custom functionality</h4>
<p>
Users can insert custom functions into the add node, edit node, connect nodes, and delete selected operations. This is done by supplying them in the options.
If the callback is NOT called, nothing happens. <a href="../examples/graph/21_data_manipulation.html">Example 21</a> has two working examples
for the add and edit functions. The data the user is supplied with in these functions has been described in the code below.
For the add data, you can add any and all options that are accepted for node creation as described above. The same goes for edit, however only the fields described
in the code below contain information on the selected node. The callback for connect accepts any options that are used for edge creation. Only the callback for delete selected
requires the same data structure that is supplied to the user. <br /><br />
<b>If there is no injected function supplied for the edit operation, the button will not be shown in the toolbar.</b>
</p>
<pre class="prettyprint">
// If a variable is not supplied, the default value is used.
var options: {
dataManipulation: true,
onAdd: function(data,callback) {
/** data = {id: random unique id,
* label: new,
* x: x position of click (canvas space),
* y: y position of click (canvas space),
* allowedToMoveX: true,
* allowedToMoveY: true
* };
*/
var newData = {..}; // alter the data as you want.
// all fields normally accepted by a node can be used.
callback(newData); // call the callback to add a node.
},
onEdit: function(data,callback) {
/** data = {id:...,
* label: ...,
* group: ...,
* shape: ...,
* color: {
* background:...,
* border:...,
* highlight: {
* background:...,
* border:...
* }
* }
* };
*/
var newData = {..}; // alter the data as you want.
// all fields normally accepted by a node can be used.
callback(newData); // call the callback with the new data to edit the node.
}
onConnect: function(data,callback) {
// data = {from: nodeId1, to: nodeId2};
var newData = {..}; // check or alter data as you see fit.
callback(newData); // call the callback to connect the nodes.
},
onDelete: function(data,callback) {
// data = {nodes: [selectedNodeIds], edges: [selectedEdgeIds]};
var newData = {..}; // alter the data as you want.
// the same data structure is required.
callback(newData); // call the callback to delete the objects.
}
};
</pre>
<p>
Because the interface elements are CSS and HTML, the user will have to correct for size changes of the canvas. To facilitate this, a new event has been added called resize.
A function can be bound to this event. This function is supplied with the new widht and height of the canvas. The CSS can then be updated accordingly.
An code snippet from example 21 is shown below.
</p>
<pre class="prettyprint">
graph.on("resize", function(params) {console.log(params.width,params.height)});
</pre>
<h3 id="Clustering">Clustering</h3>
<p>
The graph now supports dynamic clustering of nodes. This allows a user to view a very large dataset (> 50.000 nodes) without
@ -1150,16 +1551,19 @@ var options = {
reduceToNodes:300,
chainThreshold: 0.4,
clusterEdgeThreshold: 20,
sectorThreshold: 50,
sectorThreshold: 100,
screenSizeThreshold: 0.2,
fontSizeMultiplier: 4.0,
forceAmplification: 0.6,
distanceAmplification: 0.2,
edgeGrowth: 11,
nodeScaling: {width: 10,
height: 10,
radius: 10},
activeAreaBoxSize: 100
maxFontSize: 1000,
forceAmplification: 0.1,
distanceAmplification: 0.1,
edgeGrowth: 20,
nodeScaling: {width: 1,
height: 1,
radius: 1},
maxNodeSizeIncrements: 600,
activeAreaBoxSize: 100,
clusterLevelDifference: 2
}
}
// OR to just load the module with default values:
@ -1233,6 +1637,12 @@ var options: {
<td>4.0</td>
<td>This parameter denotes the increase in fontSize of the cluster when a single node is added to it.</td>
</tr>
<tr>
<td>maxFontSize</td>
<td>Number</td>
<td>1000</td>
<td>This parameter denotes the largest allowed font size. If the font becomes too large, some browsers experience problems displaying this.</td>
</tr>
<tr>
<td>forceAmplification</td>
<td>Number</td>
@ -1251,7 +1661,7 @@ var options: {
<tr>
<td>edgeGrowth</td>
<td>Number</td>
<td>11</td>
<td>20</td>
<td>This factor determines the elongation of edges connected to a cluster.</td>
</tr>
<tr>
@ -1272,56 +1682,50 @@ var options: {
<td>10</td>
<td>This factor determines how much the radius of a cluster increases in pixels per added node.</td>
</tr>
<tr>
<td>activeAreaBoxSize</td>
<tr>
<td>maxNodeSizeIncrements</td>
<td>Number</td>
<td>600</td>
<td>This limits the size clusters can grow to. The default value, 600, implies that if a cluster contains more than 600 nodes, it will no longer grow.</td>
</tr>
<tr>
<td>activeAreaBoxSize</td>
<td>Number</td>
<td>100</td>
<td>Imagine a square with an edge length of <code>activeAreaBoxSize</code> pixels around your cursor.
If a cluster is in this box as you zoom in, the cluster can be opened in a seperate sector.
This is regardless of the zoom level.</td>
</tr>
<tr>
<td>clusterLevelDifference</td>
<td>Number</td>
<td>100</td>
<td>Imagine a square with an edge length of <code>activeAreaBoxSize</code> pixels around your cursor.
If a cluster is in this box as you zoom in, the cluster can be opened in a seperate sector.
This is regardless of the zoom level.</td>
<td>2</td>
<td>At every clustering session, Graph will check if the difference between cluster levels is
acceptable. When a cluster is formed when zooming out, that is one cluster level.
If you zoom out further and it encompasses more nodes, that is another level. For example:
If the highest level of your graph at any given time is 3, nodes that have not clustered or
have clustered only once will join their neighbour with the lowest cluster level.</td>
</tr>
</table>
<h3 id="Navigation_controls">Navigation controls</h3>
<p>
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. To correctly display the navigation icons, the <b>vis.css</b> file must be included.
The user is free to alter or overload the CSS classes but without them the navigation icons are not visible.
</p>
<pre class="prettyprint">
// simple use of navigation controls
// use of navigation controls
var options: {
navigation: true
}
// advanced use of navigation controls
var options: {
navigation: {
iconPath: '/path/to/navigation/icons/'
}
}
</pre>
<table>
<tr>
<th>Name</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
<tr>
<td>iconPath</td>
<td>string</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>
</tr>
</table>
<h3 id="Keyboard_navigation">Keyboard navigation</h3>
<p>
The graph can be navigated using shortcut keys.
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 example <a href="../examples/graph/20_navigation.html">20_navigation.html</a>.
</p>
@ -1372,6 +1776,192 @@ var options: {
</tr>
</table>
<h3 id="Hierarchical_layout">Hierarchical layout</h3>
<p>
The graph can be used to display nodes in a hierarchical way. This can be determined automatically, based on the amount of edges connected to each node, or defined by the user.
If the user wants to manually determine the hierarchy, each node has to be supplied with a level (from 0 being heighest to n). The automatic method
is shown in <a href="../examples/graph/23_hierarchical_layout.html">example 23</a> and the user-defined method is shown in <a href="../examples/graph/24_hierarchical_layout_userdefined.html">example 24</a>.
This layout method does not support smooth curves or clustering. It automatically turns these features off.
</p>
<pre class="prettyprint">
// simple use of the hierarchical layout
var options: {
hierarchicalLayout: true
}
// advanced configuration for hierarchical layout
var options: {
hierarchicalLayout: {
enabled:false,
levelSeparation: 150,
nodeSpacing: 100,
direction: "UD"
}
}
// partial configuration automatically sets enabled to true
var options: {
hierarchicalLayout: {
nodeSpacing: 100,
direction: "UD"
}
}
</pre>
<table>
<tr>
<th>Name</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
<tr>
<td>enabled</td>
<td>Boolean</td>
<td>false</td>
<td>Enable or disable the hierarchical layout.
</td>
</tr>
<tr>
<td>levelSeparation</td>
<td>Number</td>
<td>150</td>
<td>This defines the space between levels (in the Y-direction, considering UP-DOWN direction).</td>
</tr>
<tr>
<td>nodeSpacing</td>
<td>Number</td>
<td>100</td>
<td>This defines the space between nodes in the same level (in the X-direction, considering UP-DOWN direction).
This is only relevant during the initial placing of nodes.</td>
</tr>
<tr>
<td>direction</td>
<td>String</td>
<td>UD</td>
<td>This defines the direction the graph is drawn in. The supported directions are: Up-Down (UD), Down-Up (DU), Left-Right (LR) and Right-Left (RL).
These need to be supplied by the acronyms in parentheses.</td>
</tr>
</table>
<h3 id="Localization">Localization</h3>
<p>
When using vis.js in other languages, one can use the localization option to overwrite the labels used in the data manipulation interface.
</p>
<pre class="prettyprint">
var options: {
labels:{
add:"Add Node",
edit:"Edit",
link:"Add Link",
del:"Delete selected",
editNode:"Edit Node",
back:"Back",
addDescription:"Click in an empty space to place a new node.",
linkDescription:"Click on a node and drag the edge to another
node to connect them.",
addError:"The function for add does not support two arguments
(data,callback).",
linkError:"The function for connect does not support two arguments
(data,callback).",
editError:"The function for edit does not support two arguments
(data, callback).",
editBoundError:"No edit function has been bound to this button.",
deleteError:"The function for delete does not support two arguments
(data, callback).",
deleteClusterError:"Clusters cannot be deleted."
}
}
</pre>
<table>
<tr>
<th>Name</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
<tr>
<td>labels</td>
<td>object</td>
<td>(shown above)</td>
<td>Overwrite one or all labels used in the datamanipulation interface with localized strings.
</td>
</tr>
</table>
<h3 id="Tooltips">Tooltips</h3>
<p>
The behaviour and style of the tooltips used to display node and edge title attributes can be customized.
</p>
<pre class="prettyprint">
// tooltip behaviour and style options
var options: {
tooltip: {
delay: 300,
fontColor: "black",
fontSize: 14, // px
fontFace: "verdana",
color: {
border: "#666",
background: "#FFFFC6"
}
}
}
</pre>
<table>
<tr>
<th>Name</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
<tr>
<td>delay</td>
<td>Number</td>
<td>300</td>
<td>Time in milliseconds a user must hover over a node or edge before a tooltip appears.</td>
</tr>
<tr>
<td>fontColor</td>
<td>String</td>
<td>"black"</td>
<td>Default color for tooltip text.</td>
</tr>
<tr>
<td>fontSize</td>
<td>Number</td>
<td>14</td>
<td>Size in pixels of tooltip text.</td>
</tr>
<tr>
<td>fontFace</td>
<td>String</td>
<td>"verdana"</td>
<td>Font family to used for tooltip text.</td>
</tr>
<tr>
<td>color.background</td>
<td>String</td>
<td>"#FFFFC6"</td>
<td>Background color for the node.</td>
</tr>
<tr>
<td>color.border</td>
<td>String</td>
<td>"#666"</td>
<td>Border color for the node.</td>
</tr>
</table>
<h2 id="Methods">Methods</h2>
<p>
Graph supports the following methods.
@ -1392,6 +1982,13 @@ var options: {
The selections are not ordered.
</td>
</tr>
<tr>
<td>storePosition()</td>
<td>none</td>
<td>This will put the X and Y positions of all nodes in the dataset. It will also include allowedToMoveX and allowedToMoveY with the correct values.
You can use this to stablize your graph once, then save the positions in a database so the next time you load the nodes, stabilization will be near instantaneous.
</td>
</tr>
<tr>
<td>on(event, callback)</td>
@ -1449,6 +2046,12 @@ var options: {
or in percentages.</td>
</tr>
<tr>
<td>zoomExtent()</td>
<td>none</td>
<td>Scales the graph so all the nodes are in center view.</td>
</tr>
</table>
<h2 id="Events">Events</h2>
@ -1505,9 +2108,49 @@ graph.off('select', onSelect);
<td>
<ul>
<li><code>nodes</code>: an array with the ids of the selected nodes</li>
<li><code>edges</code>: an array with the ids of the selected edges</li>
</ul>
</td>
</tr>
<tr>
<td>click</td>
<td>Fired after the user clicks or taps on a touchscreen.</td>
<td>
<ul>
<li><code>nodes</code>: an array with the ids of the selected nodes</li>
<li><code>edges</code>: an array with the ids of the selected edges</li>
</ul>
</td>
</tr>
<tr>
<td>doubleClick</td>
<td>Fired after the user double clicks or double taps on a touchscreen.</td>
<td>
<ul>
<li><code>nodes</code>: an array with the ids of the selected nodes</li>
<li><code>edges</code>: an array with the ids of the selected edges</li>
</ul>
</td>
</tr>
<tr>
<td>resize</td>
<td>Fired when the size of the canvas has been updated (not neccecarily changed) by the setSize() function or by the setOptions() function.</td>
<td>
<ul>
<li><code>width</code>: the new width of the canvas</li>
<li><code>height</code>: the new height of the canvas</li>
</ul>
</td>
</tr>
<tr>
<td>stabilized</td>
<td>Fired when the graph has been stabilized after initialization. This event can be used to trigger the .storePosition() function after stabilization.</td>
<td>
<ul>
<li><code>iterations</code>: number of iterations used to stabilize</li>
</ul>
</td>
</tr>
</table>

+ 3
- 1
docs/index.html View File

@ -23,7 +23,9 @@
<p>
The library is developed by
<a href="http://almende.com" target="_blank">Almende B.V.</a>
<a href="http://almende.com" target="_blank">Almende B.V.</a>.
Vis.js runs fine on Chrome, Firefox, Opera, Safari, IE9+, and most mobile
browsers (with full touch support).
</p>
<h2 id="Components">Components</h2>

+ 157
- 25
docs/timeline.html View File

@ -39,6 +39,7 @@
<li><a href="#Configuration_Options">Configuration Options</a></li>
<li><a href="#Methods">Methods</a></li>
<li><a href="#Events">Events</a></li>
<li><a href="#Editing_Items">Editing Items</a></li>
<li><a href="#Styles">Styles</a></li>
<li><a href="#Data_Policy">Data Policy</a></li>
</ul>
@ -194,8 +195,8 @@ var items = [
<td>type</td>
<td>String</td>
<td>'box'</td>
<td>The type of the item. Can be 'box' (default), 'range', or 'point'.
<!-- TODO: describe rangeoverflow -->
<td>The type of the item. Can be 'box' (default), 'point', 'range', or 'rangeoverflow'.
Types 'box' and 'point' need a start date, and types 'range' and 'rangeoverflow' need both a start and end date. Types 'range' and rangeoverflow are equal, except that overflowing text in 'range' is hidden, while visible in 'rangeoverflow'.
</td>
</tr>
<tr>
@ -215,13 +216,13 @@ var items = [
<td>no</td>
<td>This field is optional. A className can be used to give items
an individual css style. For example, when an item has className
'red', one can define a css style
<code>
.red {
background-color: red;
border-color: dark-red;
}
</code>.
'red', one can define a css style like:
<pre class="prettyprint lang-css">
.vis.timeline .red {
color: white;
background-color: red;
border-color: darkred;
}</pre>
More details on how to style items can be found in the section
<a href="#Styles">Styles</a>.
</td>
@ -309,7 +310,10 @@ var groups = [
<pre class="prettyprint lang-js">
var options = {
width: '100%',
height: '30px'
height: '30px',
margin: {
item: 20
}
};
</pre>
@ -336,11 +340,19 @@ var options = {
<tr>
<td>autoResize</td>
<td>boolean</td>
<td>false</td>
<td>true</td>
<td>If true, the Timeline will automatically detect when its
container is resized, and redraw itself accordingly.</td>
</tr>
<tr>
<td>editable</td>
<td>Boolean</td>
<td>false</td>
<td>If true, the items on the timeline can be dragged. Only applicable when option <code>selectable</code> is <code>true</code>. See also the callbacks <code>onAdd</code>, <code>onUpdate</code>, <code>onMove</code>, and <code>onRemove</code>, described in detail in section <a href="#Editing_Items">Editing Items</a>.
</td>
</tr>
<tr>
<td>end</td>
<td>Date | Number | String</td>
@ -412,6 +424,39 @@ var options = {
</td>
</tr>
<tr>
<td>onAdd</td>
<td>Function</td>
<td>none</td>
<td>Callback function triggered when an item is about to be added: when the user double taps an empty space in the Timeline. See section <a href="#Editing_Items">Editing Items</a> for more information. Only applicable when both options <code>selectable</code> and <code>editable</code> are set <code>true</code>.
</td>
</tr>
<tr>
<td>onUpdate</td>
<td>Function</td>
<td>none</td>
<td>Callback function triggered when an item is about to be updated, when the user double taps an item in the Timeline. See section <a href="#Editing_Items">Editing Items</a> for more information. Only applicable when both options <code>selectable</code> and <code>editable</code> are set <code>true</code>.
</td>
</tr>
<tr>
<td>onMove</td>
<td>Function</td>
<td>none</td>
<td>Callback function triggered when an item has been moved: after the user has dragged the item to an other position. See section <a href="#Editing_Items">Editing Items</a> for more information. Only applicable when both options <code>selectable</code> and <code>editable</code> are set <code>true</code>.
</td>
</tr>
<tr>
<td>onRemove</td>
<td>Function</td>
<td>none</td>
<td>Callback function triggered when an item is about to be removed: when the user tapped the delete button on the top right of a selected item. See section <a href="#Editing_Items">Editing Items</a> for more information. Only applicable when both options <code>selectable</code> and <code>editable</code> are set <code>true</code>.
</td>
</tr>
<!-- TODO: cleanup option order
<tr>
<td>order</td>
<td>Function</td>
@ -422,14 +467,13 @@ var options = {
`vis.components.items.Item`.
</td>
</tr>
-->
<tr>
<td>orientation</td>
<td>String</td>
<td>'bottom'</td>
<td>Orientation of the timeline: 'top' or 'bottom' (default).
If orientation is 'bottom', the time axis is drawn at the bottom,
and if 'top', the axis is drawn on top.</td>
<td>Orientation of the timeline: 'top' or 'bottom' (default). If orientation is 'bottom', the time axis is drawn at the bottom, and if 'top', the axis is drawn on top.</td>
</tr>
<tr>
@ -437,7 +481,19 @@ var options = {
<td>Number</td>
<td>5</td>
<td>The padding of items, needed to correctly calculate the size
of item ranges. Must correspond with the css of item ranges.</td>
of item ranges. Must correspond with the css of items, for example when setting <code>options.padding=10</code>, corresponding css is:
<pre class="prettyprint lang-css">
.vis.timeline .item {
padding: 10px;
}</pre>
</td>
</tr>
<tr>
<td>selectable</td>
<td>Boolean</td>
<td>true</td>
<td>If true, the items on the timeline can be selected. Multiple items can be selected by long pressing them, or by using ctrl+click or shift+click. The event <code>select</code> is fired each time the selection has changed (see section <a href="#Events">Events</a>).</td>
</tr>
<tr>
@ -451,9 +507,7 @@ var options = {
<td>showCustomTime</td>
<td>boolean</td>
<td>false</td>
<td>Show a vertical bar displaying a custom time. This line can be dragged by the user. The custom time can be utilized to show a state in the past or in the future.
<!-- TODO: more docs on showCustomTime
When the custom time bar is dragged by the user, an event is triggered, on which the contents of the timeline can be changed in to the state at that moment in time.--></td>
<td>Show a vertical bar displaying a custom time. This line can be dragged by the user. The custom time can be utilized to show a state in the past or in the future. When the custom time bar is dragged by the user, the event <code>timechange</code> is fired repeatedly. After the bar is dragged, the event <code>timechanged</code> is fired once.</td>
</tr>
<tr>
@ -493,8 +547,7 @@ var options = {
<td>type</td>
<td>String</td>
<td>'box'</td>
<td>Specifies the type for the timeline items. Choose from 'dot' or 'point'.
Note that individual items can override this global type.
<td>Specifies the type for the timeline items. Choose from 'box', 'point', 'range', and 'rangeoverflow'. Note that individual items can override this global type.
</td>
</tr>
@ -555,10 +608,16 @@ var options = {
<tr>
<td>getSelection()</td>
<td>ids</td>
<td>Number[]</td>
<td>Get an array with the ids of the currently selected items.</td>
</tr>
<tr>
<td>getWindow()</td>
<td>Object</td>
<td>Get the current visible window. Returns an object with properties <code>start: Date</code> and <code>end: Date</code>.</td>
</tr>
<tr>
<td>on(event, callback)</td>
<td>none</td>
@ -605,13 +664,19 @@ var options = {
</td>
</tr>
<tr>
<td>setWindow(start, end)</td>
<td>none</td>
<td>Set the current visible window. The parameters <code>start</code> and <code>end</code> can be a <code>Date</code>, <code>Number</code>, or <code>String</code>. If the parameter value of <code>start</code> or <code>end</code> is null, the parameter will be left unchanged.</td>
</tr>
</table>
<h2 id="Events">Events</h2>
<p>
Timeline fires events when changing the visible window by dragging, or when
selecting items.
Timeline fires events when changing the visible window by dragging, when
selecting items, and when dragging the custom time bar.
</p>
<p>
@ -674,7 +739,7 @@ timeline.off('select', onSelect);
<tr>
<td>rangechanged</td>
<td>Fired once after the user has dragging the timeline window.
<td>Fired once after the user has dragged the timeline window.
</td>
<td>
<ul>
@ -696,8 +761,75 @@ timeline.off('select', onSelect);
</td>
</tr>
<tr>
<td>timechange</td>
<td>Fired repeatedly when the user is dragging the custom time bar.
Only available when the custom time bar is enabled.
</td>
<td>
<ul>
<li><code>time</code> (Date): the current time.</li>
</ul>
</td>
</tr>
<tr>
<td>timechanged</td>
<td>Fired once after the user has dragged the custom time bar.
Only available when the custom time bar is enabled.
</td>
<td>
<ul>
<li><code>time</code> (Date): the current time.</li>
</ul>
</td>
</tr>
</table>
<h2 id="Editing_Items">Editing Items</h2>
<p>
When the Timeline is configured to be editable (both options <code>selectable</code> and <code>editable</code> are <code>true</code>), the user can move items by dragging them, can create a new item by double tapping on an empty space, can update an item by double tapping it, and can delete a selected item by clicking the delete button on the top right.
</p>
<p>
One can specify callback functions to validate changes made by the user. There are a number of callback functions for this purpose:
</p>
<ul>
<li><code>onAdd(item, callback)</code> Fired when a new item is about to be added. If not implemented, the item will be added with default text contents.</li>
<li><code>onUpdate(item, callback)</code> Fired when an item is about to be updated. This function typically has to show a dialog where the user change the item. If not implemented, nothing happens.</li>
<li><code>onMove(item, callback)</code> Fired when an item has been moved. If not implemented, the move action will be accepted.</li>
<li><code>onRemove(item, callback)</code> Fired when an item is about to be deleted. If not implemented, the item will be always removed.</li>
</ul>
<p>
Each of the callbacks is invoked with two arguments:
</p>
<ul>
<li><code>item</code>: the item being manipulated</li>
<li><code>callback</code>: a callback function which must be invoked to report back. The callback must be invoked as <code>callback(item | null)</code>. Here, <code>item</code> can contain changes to the passed item. When invoked as <code>callback(null)</code>, the action will be cancelled.</li>
</ul>
<p>
Example code:
</p>
<pre class="prettyprint lang-js">var options = {
onUpdate: function (item, callback) {
item.content = prompt('Edit items text:', item.content);
if (item.content != null) {
callback(item); // send back adjusted item
}
else {
callback(null); // cancel updating the item
}
}
}
</pre>
A full example is available here: <a href="../examples/timeline/08_edit_items.html">08_edit_items.html</a>.
<h2 id="Styles">Styles</h2>
<p>
@ -709,7 +841,7 @@ timeline.off('select', onSelect);
<p>For example, to change the border and background color of all items, include the
following code inside the head of your html code or in a separate stylesheet.</p>
<pre class="prettyprint lang-html">&lt;style&gt;
.graph .item {
.vis.timeline .item {
border-color: orange;
background-color: yellow;
}

+ 4
- 14
examples/graph/02_random_nodes.html View File

@ -57,6 +57,7 @@
j++;
}
var from = i;
var to = j;
edges.push({
@ -74,22 +75,11 @@
nodes: nodes,
edges: edges
};
/*
var options = {
nodes: {
shape: 'circle'
},
edges: {
length: 50
},
stabilize: false
};
*/
var options = {
edges: {
length: 50
},
stabilize: false
stabilize: false,
};
graph = new vis.Graph(container, data, options);
@ -102,12 +92,12 @@
</head>
<body onload="draw();">
<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>

+ 2
- 2
examples/graph/07_selections.html View File

@ -51,8 +51,8 @@
graph = new vis.Graph(container, data, options);
// add event listener
graph.on('select', function(params) {
document.getElementById('info').innerHTML += 'selection: ' + params.nodes + '<br>';
graph.on('select', function(properties) {
document.getElementById('info').innerHTML += 'selection: ' + JSON.stringify(properties) + '<br>';
});
// set initial selection (id's of some nodes)

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

@ -88,9 +88,6 @@
};
*/
var options = {
edges: {
length: 50
},
clustering: {
enabled: clusteringOn,
clusterEdgeThreshold: clusterEdgeThreshold
@ -110,7 +107,7 @@
<body onload="draw();">
<h2>Clustering - Scale-Free-Graph</h2>
<div style="width:700px; font-size:14px;">
This example shows therandomly generated <b>scale-free-graph</b> set of nodes and connected edges from example 2.
This example shows the randomly generated <b>scale-free-graph</b> set of nodes and connected edges from example 2.
By clicking the checkbox you can turn clustering on and off. If you increase the number of nodes to
a value higher than 100, automatic clustering is used before the initial draw (assuming the checkbox is checked).
<br />

+ 12
- 11
examples/graph/20_navigation.html View File

@ -31,10 +31,10 @@
div.table_description {
width:100px;
}
</style>
<script type="text/javascript" src="../../dist/vis.js"></script>
<link type="text/css" rel="stylesheet" href="../../dist/vis.css">
<script type="text/javascript">
var nodes = null;
@ -126,17 +126,17 @@
<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 />
This example is the same as example 2, except for the navigation controls that have 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>
<td><div class="table_content"><img src="../../dist/img/graph/upArrow.png" /> </div></td>
<td><div class="table_content"><img src="../../dist/img/graph/downArrow.png" /> </div></td>
<td><div class="table_content"><img src="../../dist/img/graph/leftArrow.png" /> </div></td>
<td><div class="table_content"><img src="../../dist/img/graph/rightArrow.png" /> </div></td>
<td><div class="table_content"><img src="../../dist/img/graph/plus.png" /> </div></td>
<td><div class="table_content"><img src="../../dist/img/graph/minus.png" /> </div></td>
<td><div class="table_content"><img src="../../dist/img/graph/zoomExtends.png" /> </div></td>
</tr>
<tr>
<td><div class="table_description">Keyboard shortcuts:</div></td>
@ -156,12 +156,13 @@
<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>
<td><div class="table_content">Zoom extent</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.
Zoom Extends changes the zoom and position of the camera to encompass all visible nodes. <u>To correctly display the navigation icons, the <b>vis.css</b> file must be included.</u>
The user is free to alter or overload the CSS classes but without them the navigation icons are not visible.
</div>

+ 224
- 0
examples/graph/21_data_manipulation.html View File

@ -0,0 +1,224 @@
<!doctype html>
<html>
<head>
<title>Graph | Navigation</title>
<style type="text/css">
body {
font: 10pt sans;
}
#mygraph {
position:relative;
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;
}
#operation {
font-size:28px;
}
#graph-popUp {
display:none;
position:absolute;
top:350px;
left:170px;
z-index:299;
width:250px;
height:120px;
background-color: #f9f9f9;
border-style:solid;
border-width:3px;
border-color: #5394ed;
padding:10px;
text-align: center;
}
</style>
<script type="text/javascript" src="../../dist/vis.js"></script>
<link type="text/css" rel="stylesheet" href="../../dist/vis.css">
<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 = 25;
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 = {
edges: {
length: 50
},
stabilize: false,
dataManipulation: true,
onAdd: function(data,callback) {
var span = document.getElementById('operation');
var idInput = document.getElementById('node-id');
var labelInput = document.getElementById('node-label');
var saveButton = document.getElementById('saveButton');
var cancelButton = document.getElementById('cancelButton');
var div = document.getElementById('graph-popUp');
span.innerHTML = "Add Node";
idInput.value = data.id;
labelInput.value = data.label;
saveButton.onclick = saveData.bind(this,data,callback);
cancelButton.onclick = clearPopUp.bind();
div.style.display = 'block';
},
onEdit: function(data,callback) {
var span = document.getElementById('operation');
var idInput = document.getElementById('node-id');
var labelInput = document.getElementById('node-label');
var saveButton = document.getElementById('saveButton');
var cancelButton = document.getElementById('cancelButton');
var div = document.getElementById('graph-popUp');
span.innerHTML = "Edit Node";
idInput.value = data.id;
labelInput.value = data.label;
saveButton.onclick = saveData.bind(this,data,callback);
cancelButton.onclick = clearPopUp.bind();
div.style.display = 'block';
},
onConnect: function(data,callback) {
if (data.from == data.to) {
var r=confirm("Do you want to connect the node to itself?");
if (r==true) {
callback(data);
}
}
else {
callback(data);
}
}
};
graph = new vis.Graph(container, data, options);
// add event listeners
graph.on('select', function(params) {
document.getElementById('selection').innerHTML = 'Selection: ' + params.nodes;
});
graph.on("resize", function(params) {console.log(params.width,params.height)});
function clearPopUp() {
var saveButton = document.getElementById('saveButton');
var cancelButton = document.getElementById('cancelButton');
saveButton.onclick = null;
cancelButton.onclick = null;
var div = document.getElementById('graph-popUp');
div.style.display = 'none';
}
function saveData(data,callback) {
var idInput = document.getElementById('node-id');
var labelInput = document.getElementById('node-label');
var div = document.getElementById('graph-popUp');
data.id = idInput.value;
data.label = labelInput.value;
clearPopUp();
callback(data);
}
}
</script>
</head>
<body onload="draw();">
<h2>Editing the dataset</h2>
<div style="width: 700px; font-size:14px;">
In this example we have enabled the data manipulation setting. If the dataManipulation option is set to true, the edit button will appear.
If you prefer to have the toolbar visible initially, you can set the initiallyVisible option to true. The exact method is described in the docs.
<br /><br />
The data manipulation allows the user to add nodes, connect them, edit them and delete any selected items. In this example we have created trigger functions
for the add and edit operations. By settings these trigger functions the user can direct the way the data is manipulated. In this example we have created a simple
pop-up that allows us to edit some of the properties.
</div>
<br />
<div id="graph-popUp">
<span id="operation">node</span> <br>
<table style="margin:auto;"><tr>
<td>id</td><td><input id="node-id" value="new value"></td>
</tr>
<tr>
<td>label</td><td><input id="node-label" value="new value"> </td>
</tr></table>
<input type="button" value="save" id="saveButton"></button>
<input type="button" value="cancel" id="cancelButton"></button>
</div>
<br />
<div id="mygraph"></div>
<p id="selection"></p>
</body>
</html>

+ 373
- 0
examples/graph/22_les_miserables.html View File

@ -0,0 +1,373 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Graph | Multiline text</title>
<style type="text/css">
#mygraph {
width: 900px;
height: 900px;
border: 1px solid lightgray;
}
</style>
<script type="text/javascript" src="../../dist/vis.js"></script>
<script type="text/javascript">
function draw() {
// create some nodes
var nodes = [
{id:0,"labelHidden":"Myriel","group":1},
{id:1,"labelHidden":"Napoleon","group":1},
{id:2,"labelHidden":"Mlle.Baptistine","group":1},
{id:3,"labelHidden":"Mme.Magloire","group":1},
{id:4,"labelHidden":"CountessdeLo","group":1},
{id:5,"labelHidden":"Geborand","group":1},
{id:6,"labelHidden":"Champtercier","group":1},
{id:7,"labelHidden":"Cravatte","group":1},
{id:8,"labelHidden":"Count","group":1},
{id:9,"labelHidden":"OldMan","group":1},
{id:10,"labelHidden":"Labarre","group":2},
{id:11,"labelHidden":"Valjean","group":2},
{id:12,"labelHidden":"Marguerite","group":3},
{id:13,"labelHidden":"Mme.deR","group":2},
{id:14,"labelHidden":"Isabeau","group":2},
{id:15,"labelHidden":"Gervais","group":2},
{id:16,"labelHidden":"Tholomyes","group":3},
{id:17,"labelHidden":"Listolier","group":3},
{id:18,"labelHidden":"Fameuil","group":3},
{id:19,"labelHidden":"Blacheville","group":3},
{id:20,"labelHidden":"Favourite","group":3},
{id:21,"labelHidden":"Dahlia","group":3},
{id:22,"labelHidden":"Zephine","group":3},
{id:23,"labelHidden":"Fantine","group":3},
{id:24,"labelHidden":"Mme.Thenardier","group":4},
{id:25,"labelHidden":"Thenardier","group":4},
{id:26,"labelHidden":"Cosette","group":5},
{id:27,"labelHidden":"Javert","group":4},
{id:28,"labelHidden":"Fauchelevent","group":0},
{id:29,"labelHidden":"Bamatabois","group":2},
{id:30,"labelHidden":"Perpetue","group":3},
{id:31,"labelHidden":"Simplice","group":2},
{id:32,"labelHidden":"Scaufflaire","group":2},
{id:33,"labelHidden":"Woman1","group":2},
{id:34,"labelHidden":"Judge","group":2},
{id:35,"labelHidden":"Champmathieu","group":2},
{id:36,"labelHidden":"Brevet","group":2},
{id:37,"labelHidden":"Chenildieu","group":2},
{id:38,"labelHidden":"Cochepaille","group":2},
{id:39,"labelHidden":"Pontmercy","group":4},
{id:40,"labelHidden":"Boulatruelle","group":6},
{id:41,"labelHidden":"Eponine","group":4},
{id:42,"labelHidden":"Anzelma","group":4},
{id:43,"labelHidden":"Woman2","group":5},
{id:44,"labelHidden":"MotherInnocent","group":0},
{id:45,"labelHidden":"Gribier","group":0},
{id:46,"labelHidden":"Jondrette","group":7},
{id:47,"labelHidden":"Mme.Burgon","group":7},
{id:48,"labelHidden":"Gavroche","group":8},
{id:49,"labelHidden":"Gillenormand","group":5},
{id:50,"labelHidden":"Magnon","group":5},
{id:51,"labelHidden":"Mlle.Gillenormand","group":5},
{id:52,"labelHidden":"Mme.Pontmercy","group":5},
{id:53,"labelHidden":"Mlle.Vaubois","group":5},
{id:54,"labelHidden":"Lt.Gillenormand","group":5},
{id:55,"labelHidden":"Marius","group":8},
{id:56,"labelHidden":"BaronessT","group":5},
{id:57,"labelHidden":"Mabeuf","group":8},
{id:58,"labelHidden":"Enjolras","group":8},
{id:59,"labelHidden":"Combeferre","group":8},
{id:60,"labelHidden":"Prouvaire","group":8},
{id:61,"labelHidden":"Feuilly","group":8},
{id:62,"labelHidden":"Courfeyrac","group":8},
{id:63,"labelHidden":"Bahorel","group":8},
{id:64,"labelHidden":"Bossuet","group":8},
{id:65,"labelHidden":"Joly","group":8},
{id:66,"labelHidden":"Grantaire","group":8},
{id:67,"labelHidden":"MotherPlutarch","group":9},
{id:68,"labelHidden":"Gueulemer","group":4},
{id:69,"labelHidden":"Babet","group":4},
{id:70,"labelHidden":"Claquesous","group":4},
{id:71,"labelHidden":"Montparnasse","group":4},
{id:72,"labelHidden":"Toussaint","group":5},
{id:73,"labelHidden":"Child1","group":10},
{id:74,"labelHidden":"Child2","group":10},
{id:75,"labelHidden":"Brujon","group":4},
{id:76,"labelHidden":"Mme.Hucheloup","group":8}
];
// create some edges
var edges = [
{"from":1,"to":0},
{"from":2,"to":0},
{"from":3,"to":0},
{"from":3,"to":2},
{"from":4,"to":0},
{"from":5,"to":0},
{"from":6,"to":0},
{"from":7,"to":0},
{"from":8,"to":0},
{"from":9,"to":0},
{"from":11,"to":10},
{"from":11,"to":3},
{"from":11,"to":2},
{"from":11,"to":0},
{"from":12,"to":11},
{"from":13,"to":11},
{"from":14,"to":11},
{"from":15,"to":11},
{"from":17,"to":16},
{"from":18,"to":16},
{"from":18,"to":17},
{"from":19,"to":16},
{"from":19,"to":17},
{"from":19,"to":18},
{"from":20,"to":16},
{"from":20,"to":17},
{"from":20,"to":18},
{"from":20,"to":19},
{"from":21,"to":16},
{"from":21,"to":17},
{"from":21,"to":18},
{"from":21,"to":19},
{"from":21,"to":20},
{"from":22,"to":16},
{"from":22,"to":17},
{"from":22,"to":18},
{"from":22,"to":19},
{"from":22,"to":20},
{"from":22,"to":21},
{"from":23,"to":16},
{"from":23,"to":17},
{"from":23,"to":18},
{"from":23,"to":19},
{"from":23,"to":20},
{"from":23,"to":21},
{"from":23,"to":22},
{"from":23,"to":12},
{"from":23,"to":11},
{"from":24,"to":23},
{"from":24,"to":11},
{"from":25,"to":24},
{"from":25,"to":23},
{"from":25,"to":11},
{"from":26,"to":24},
{"from":26,"to":11},
{"from":26,"to":16},
{"from":26,"to":25},
{"from":27,"to":11},
{"from":27,"to":23},
{"from":27,"to":25},
{"from":27,"to":24},
{"from":27,"to":26},
{"from":28,"to":11},
{"from":28,"to":27},
{"from":29,"to":23},
{"from":29,"to":27},
{"from":29,"to":11},
{"from":30,"to":23},
{"from":31,"to":30},
{"from":31,"to":11},
{"from":31,"to":23},
{"from":31,"to":27},
{"from":32,"to":11},
{"from":33,"to":11},
{"from":33,"to":27},
{"from":34,"to":11},
{"from":34,"to":29},
{"from":35,"to":11},
{"from":35,"to":34},
{"from":35,"to":29},
{"from":36,"to":34},
{"from":36,"to":35},
{"from":36,"to":11},
{"from":36,"to":29},
{"from":37,"to":34},
{"from":37,"to":35},
{"from":37,"to":36},
{"from":37,"to":11},
{"from":37,"to":29},
{"from":38,"to":34},
{"from":38,"to":35},
{"from":38,"to":36},
{"from":38,"to":37},
{"from":38,"to":11},
{"from":38,"to":29},
{"from":39,"to":25},
{"from":40,"to":25},
{"from":41,"to":24},
{"from":41,"to":25},
{"from":42,"to":41},
{"from":42,"to":25},
{"from":42,"to":24},
{"from":43,"to":11},
{"from":43,"to":26},
{"from":43,"to":27},
{"from":44,"to":28},
{"from":44,"to":11},
{"from":45,"to":28},
{"from":47,"to":46},
{"from":48,"to":47},
{"from":48,"to":25},
{"from":48,"to":27},
{"from":48,"to":11},
{"from":49,"to":26},
{"from":49,"to":11},
{"from":50,"to":49},
{"from":50,"to":24},
{"from":51,"to":49},
{"from":51,"to":26},
{"from":51,"to":11},
{"from":52,"to":51},
{"from":52,"to":39},
{"from":53,"to":51},
{"from":54,"to":51},
{"from":54,"to":49},
{"from":54,"to":26},
{"from":55,"to":51},
{"from":55,"to":49},
{"from":55,"to":39},
{"from":55,"to":54},
{"from":55,"to":26},
{"from":55,"to":11},
{"from":55,"to":16},
{"from":55,"to":25},
{"from":55,"to":41},
{"from":55,"to":48},
{"from":56,"to":49},
{"from":56,"to":55},
{"from":57,"to":55},
{"from":57,"to":41},
{"from":57,"to":48},
{"from":58,"to":55},
{"from":58,"to":48},
{"from":58,"to":27},
{"from":58,"to":57},
{"from":58,"to":11},
{"from":59,"to":58},
{"from":59,"to":55},
{"from":59,"to":48},
{"from":59,"to":57},
{"from":60,"to":48},
{"from":60,"to":58},
{"from":60,"to":59},
{"from":61,"to":48},
{"from":61,"to":58},
{"from":61,"to":60},
{"from":61,"to":59},
{"from":61,"to":57},
{"from":61,"to":55},
{"from":62,"to":55},
{"from":62,"to":58},
{"from":62,"to":59},
{"from":62,"to":48},
{"from":62,"to":57},
{"from":62,"to":41},
{"from":62,"to":61},
{"from":62,"to":60},
{"from":63,"to":59},
{"from":63,"to":48},
{"from":63,"to":62},
{"from":63,"to":57},
{"from":63,"to":58},
{"from":63,"to":61},
{"from":63,"to":60},
{"from":63,"to":55},
{"from":64,"to":55},
{"from":64,"to":62},
{"from":64,"to":48},
{"from":64,"to":63},
{"from":64,"to":58},
{"from":64,"to":61},
{"from":64,"to":60},
{"from":64,"to":59},
{"from":64,"to":57},
{"from":64,"to":11},
{"from":65,"to":63},
{"from":65,"to":64},
{"from":65,"to":48},
{"from":65,"to":62},
{"from":65,"to":58},
{"from":65,"to":61},
{"from":65,"to":60},
{"from":65,"to":59},
{"from":65,"to":57},
{"from":65,"to":55},
{"from":66,"to":64},
{"from":66,"to":58},
{"from":66,"to":59},
{"from":66,"to":62},
{"from":66,"to":65},
{"from":66,"to":48},
{"from":66,"to":63},
{"from":66,"to":61},
{"from":66,"to":60},
{"from":67,"to":57},
{"from":68,"to":25},
{"from":68,"to":11},
{"from":68,"to":24},
{"from":68,"to":27},
{"from":68,"to":48},
{"from":68,"to":41},
{"from":69,"to":25},
{"from":69,"to":68},
{"from":69,"to":11},
{"from":69,"to":24},
{"from":69,"to":27},
{"from":69,"to":48},
{"from":69,"to":41},
{"from":70,"to":25},
{"from":70,"to":69},
{"from":70,"to":68},
{"from":70,"to":11},
{"from":70,"to":24},
{"from":70,"to":27},
{"from":70,"to":41},
{"from":70,"to":58},
{"from":71,"to":27},
{"from":71,"to":69},
{"from":71,"to":68},
{"from":71,"to":70},
{"from":71,"to":11},
{"from":71,"to":48},
{"from":71,"to":41},
{"from":71,"to":25},
{"from":72,"to":26},
{"from":72,"to":27},
{"from":72,"to":11},
{"from":73,"to":48},
{"from":74,"to":48},
{"from":74,"to":73},
{"from":75,"to":69},
{"from":75,"to":68},
{"from":75,"to":25},
{"from":75,"to":48},
{"from":75,"to":41},
{"from":75,"to":70},
{"from":75,"to":71},
{"from":76,"to":64},
{"from":76,"to":65},
{"from":76,"to":66},
{"from":76,"to":63},
{"from":76,"to":62},
{"from":76,"to":48},
{"from":76,"to":58}
];
// create a graph
var container = document.getElementById('mygraph');
var data = {
nodes: nodes,
edges: edges
};
var options = {nodes: {shape:'circle'},stabilize: false};
var graph = new vis.Graph(container, data, options);
}
</script>
</head>
<body onload="draw()">
<div id="mygraph"></div>
</body>
</html>

+ 147
- 0
examples/graph/23_hierarchical_layout.html View File

@ -0,0 +1,147 @@
<!doctype html>
<html>
<head>
<title>Graph | Random nodes</title>
<style type="text/css">
body {
font: 10pt sans;
}
#mygraph {
width: 600px;
height: 600px;
border: 1px solid lightgray;
}
</style>
<script type="text/javascript" src="../../dist/vis.js"></script>
<script type="text/javascript">
var nodes = null;
var edges = null;
var graph = null;
function draw() {
nodes = [];
edges = [];
var connectionCount = [];
// randomly create some nodes and edges
var nodeCount = document.getElementById('nodeCount').value;
for (var i = 0; i < nodeCount; i++) {
nodes.push({
id: i,
label: String(i)
});
connectionCount[i] = 0;
// create edges in a scale-free-graph way
if (i == 1) {
var from = i;
var to = 0;
edges.push({
from: from,
to: to
});
connectionCount[from]++;
connectionCount[to]++;
}
else if (i > 1) {
var conn = edges.length * 2;
var rand = Math.floor(Math.random() * conn);
var cum = 0;
var j = 0;
while (j < connectionCount.length && cum < rand) {
cum += connectionCount[j];
j++;
}
var from = i;
var to = j;
edges.push({
from: from,
to: to
});
connectionCount[from]++;
connectionCount[to]++;
}
}
// create a graph
var container = document.getElementById('mygraph');
var data = {
nodes: nodes,
edges: edges
};
var directionInput = document.getElementById("direction");
var options = {
edges: {
},
stabilize: false,
hierarchicalLayout: {
direction: directionInput.value
}
};
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>Hierarchical Layout - Scale-Free-Graph</h2>
<div style="width:700px; font-size:14px;">
This example shows the randomly generated <b>scale-free-graph</b> set of nodes and connected edges from example 2.
In this example, hierarchical layout has been enabled and the vertical levels are determined automatically.
</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>
<input type="button" id="btn-UD" value="Up-Down">
<input type="button" id="btn-DU" value="Down-Up">
<input type="button" id="btn-LR" value="Left-Right">
<input type="button" id="btn-RL" value="Right-Left">
<input type="hidden" id='direction' value="UD">
<script language="javascript">
var directionInput = document.getElementById("direction");
var btnUD = document.getElementById("btn-UD");
btnUD.onclick = function() {
directionInput.value = "UD";
draw();
}
var btnDU = document.getElementById("btn-DU");
btnDU.onclick = function() {
directionInput.value = "DU";
draw();
};
var btnLR = document.getElementById("btn-LR");
btnLR.onclick = function() {
directionInput.value = "LR";
draw();
};
var btnRL = document.getElementById("btn-RL");
btnRL.onclick = function() {
directionInput.value = "RL";
draw();
};
</script>
<br>
<div id="mygraph"></div>
<p id="selection"></p>
</body>
</html>

+ 139
- 0
examples/graph/24_hierarchical_layout_userdefined.html View File

@ -0,0 +1,139 @@
<!doctype html>
<html>
<head>
<title>Graph | Random nodes</title>
<style type="text/css">
body {
font: 10pt sans;
}
#mygraph {
width: 600px;
height: 600px;
border: 1px solid lightgray;
}
</style>
<script type="text/javascript" src="../../dist/vis.js"></script>
<script type="text/javascript">
var nodes = null;
var edges = null;
var graph = null;
function draw() {
nodes = [];
edges = [];
var connectionCount = [];
// randomly create some nodes and edges
for (var i = 0; i < 15; i++) {
nodes.push({
id: i,
label: String(i)
});
}
edges.push({
from: 0,
to: 1
});
edges.push({
from: 0,
to: 6
});
edges.push({
from: 0,
to: 13
});edges.push({
from: 0,
to: 11
});
edges.push({
from: 1,
to: 2
});
edges.push({
from: 2,
to: 3
});
edges.push({
from: 2,
to: 4
});
edges.push({
from: 3,
to: 5
});
edges.push({
from: 1,
to: 10
});
edges.push({
from: 1,
to: 7
});
edges.push({
from: 2,
to: 8
});
edges.push({
from: 2,
to: 9
});
edges.push({
from: 3,
to: 14
});
edges.push({
from: 1,
to: 12
});
nodes[0]["level"] = 0;
nodes[1]["level"] = 1;
nodes[2]["level"] = 3;
nodes[3]["level"] = 4;
nodes[4]["level"] = 4;
nodes[5]["level"] = 5;
nodes[6]["level"] = 1;
nodes[7]["level"] = 2;
nodes[8]["level"] = 4;
nodes[9]["level"] = 4;
nodes[10]["level"] = 2;
nodes[11]["level"] = 1;
nodes[12]["level"] = 2;
nodes[13]["level"] = 1;
nodes[14]["level"] = 5;
// create a graph
var container = document.getElementById('mygraph');
var data = {
nodes: nodes,
edges: edges
};
var options = {
hierarchicalLayout: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>Hierarchical Layout - User-defined</h2>
<div style="width:700px; font-size:14px;">
This example shows a user-defined hierarchical layout. If the user defines levels for nodes but does not do so for all nodes, an alert will show up and hierarchical layout will be disabled. Either all or none can be defined.
</div>
<br />
<div id="mygraph"></div>
<p id="selection"></p>
</body>
</html>

+ 110
- 0
examples/graph/25_physics_configuration.html View File

@ -0,0 +1,110 @@
<!doctype html>
<html>
<head>
<title>Graph | Playing with Physics</title>
<style type="text/css">
body {
font: 10pt sans;
}
#mygraph {
width: 600px;
height: 600px;
border: 1px solid lightgray;
}
</style>
<script type="text/javascript" src="../../dist/vis.js"></script>
<script type="text/javascript">
var nodes = null;
var edges = null;
var graph = null;
function draw() {
nodes = [];
edges = [];
var connectionCount = [];
// randomly create some nodes and edges
var nodeCount = 60;
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 = {
edges: {
},
stabilize: false,
configurePhysics: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>Playing with Physics</h2>
<div style="width: 700px; font-size:14px;">
Every dataset is different. Nodes can have different sizes based on content, interconnectivity can be high or low etc. Because of this, graph has a special option
that the user can use to explore which settings may be good for him or her. This is ment to be used during the development phase when you are implementing vis.js. Once you have found
settings you are happy with, you can supply them to graph using the documented physics options.
On start, the default settings will be loaded. Keep in mind that selecting the hierarchical simulation mode <b>disables</b> smooth curves. These will not be enabled again afterwards.
</div>
<br />
<div id="mygraph"></div>
<p id="selection"></p>
</body>
</html>

+ 1
- 1
examples/graph/graphviz/graphviz_gallery.html View File

@ -65,7 +65,7 @@
<script type="text/javascript">
var container = document.getElementById('mygraph');
var url = document.getElementById('url');
var graph = new vis.Graph(container);
var graph = new vis.Graph(container,{},{physics:{barnesHut:{springLength:75,springConstant:0.015}}});
function loadData () {
$.ajax({

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

@ -32,6 +32,11 @@
<p><a href="18_fully_random_nodes_clustering.html">18_fully_random_nodes_clustering.html</a></p>
<p><a href="19_scale_free_graph_clustering.html">19_scale_free_graph_clustering.html</a></p>
<p><a href="20_navigation.html">20_navigation.html</a></p>
<p><a href="21_data_manipulation.html">21_data_manipulation.html</a></p>
<p><a href="22_les_miserables.html">22_les_miserables.html</a></p>
<p><a href="23_hierarchical_layout.html">23_hierarchical_layout.html</a></p>
<p><a href="24_hierarchical_layout_userdefined.html">24_hierarchical_layout_userdefined.html</a></p>
<p><a href="25_physics_configuration.html">25_physics_configuration.html</a></p>
<p><a href="graphviz/graphviz_gallery.html">graphviz_gallery.html</a></p>
</div>

+ 1
- 2
examples/timeline/03_much_data.html View File

@ -22,7 +22,7 @@
</h1>
<p>
<label for="count">Number of items</label>
<input id="count" value="100">
<input id="count" value="1000">
<input id="draw" type="button" value="draw">
</p>
<div id="visualization"></div>
@ -63,7 +63,6 @@
};
var timeline = new vis.Timeline(container, items, options);
</script>
</body>
</html>

+ 1
- 1
examples/timeline/05_groups.html View File

@ -60,7 +60,7 @@
// create visualization
var container = document.getElementById('visualization');
var options = {
groupOrder: 'content'
groupOrder: 'content' // groupOrder can be a property name or a sorting function
};
var timeline = new vis.Timeline(container);

+ 17
- 4
examples/timeline/06_event_listeners.html View File

@ -18,16 +18,25 @@
<div id="log"></div>
<script type="text/javascript">
var container = document.getElementById('visualization');
var items = [
var items = new vis.DataSet({
convert: {
start: 'Date',
end: 'Date'
}
});
items.add([
{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 container = document.getElementById('visualization');
var options = {
editable: true
};
var timeline = new vis.Timeline(container, items, options);
timeline.on('rangechange', function (properties) {
@ -40,6 +49,10 @@
logEvent('select', properties);
});
items.on('*', function (event, properties) {
logEvent(event, properties);
});
function logEvent(event, properties) {
var log = document.getElementById('log');
var msg = document.createElement('div');

+ 65
- 0
examples/timeline/07_custom_time_bar.html View File

@ -0,0 +1,65 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Timeline | Show current and custom time bars</title>
<style type="text/css">
body, html {
font-family: sans-serif;
font-size: 11pt;
}
</style>
<script src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
</head>
<body>
<p>
<input type="text" id="setTime" value="2014-02-07" />
<input type="button" id="set" value="Set custom time" />
</p>
<p>
<input type="button" id="get" value="Get custom time" />
<span id="getTime"></span>
</p>
<p>
<code>timechange</code> event: <span id="timechangeEvent"></span>
</p>
<p>
<code>timechanged</code> event: <span id="timechangedEvent"></span>
</p>
<div id="visualization"></div>
<script type="text/javascript">
var container = document.getElementById('visualization');
var items = [];
var options = {
showCurrentTime: true,
showCustomTime: true,
start: new Date(Date.now() - 1000 * 60 * 60 * 24),
end: new Date(Date.now() + 1000 * 60 * 60 * 24 * 6)
};
var timeline = new vis.Timeline(container, items, options);
document.getElementById('set').onclick = function () {
var time = document.getElementById('setTime').value;
timeline.setCustomTime(time);
};
document.getElementById('setTime').value = new Date().toISOString().substring(0, 10);
document.getElementById('get').onclick = function () {
document.getElementById('getTime').innerHTML = timeline.getCustomTime();
};
timeline.on('timechange', function (properties) {
document.getElementById('timechangeEvent').innerHTML = properties.time;
});
timeline.on('timechanged', function (properties) {
document.getElementById('timechangedEvent').innerHTML = properties.time;
});
</script>
</body>
</html>

+ 90
- 0
examples/timeline/08_edit_items.html View File

@ -0,0 +1,90 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Timeline | Edit items</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 items = new vis.DataSet([
{id: 1, content: 'item 1', start: new Date(2013, 3, 20)},
{id: 2, content: 'item 2', start: new Date(2013, 3, 14)},
{id: 3, content: 'item 3', start: new Date(2013, 3, 18)},
{id: 4, content: 'item 4', start: new Date(2013, 3, 16), end: new Date(2013, 3, 19)},
{id: 5, content: 'item 5', start: new Date(2013, 3, 25)},
{id: 6, content: 'item 6', start: new Date(2013, 3, 27)}
]);
var container = document.getElementById('visualization');
var options = {
editable: true,
onAdd: function (item, callback) {
item.content = prompt('Enter text content for new item:', item.content);
if (item.content != null) {
callback(item); // send back adjusted new item
}
else {
callback(null); // cancel item creation
}
},
onMove: function (item, callback) {
if (confirm('Do you really want to move the item to\n' +
'start: ' + item.start + '\n' +
'end: ' + item.end + '?')) {
callback(item); // send back item as confirmation (can be changed
}
else {
callback(null); // cancel editing item
}
},
onUpdate: function (item, callback) {
item.content = prompt('Edit items text:', item.content);
if (item.content != null) {
callback(item); // send back adjusted item
}
else {
callback(null); // cancel updating the item
}
},
onRemove: function (item, callback) {
if (confirm('Remove item ' + item.content + '?')) {
callback(item); // confirm deletion
}
else {
callback(null); // cancel deletion
}
}
};
var timeline = new vis.Timeline(container, items, options);
items.on('*', function (event, properties) {
logEvent(event, 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>

+ 65
- 0
examples/timeline/09_order_groups.html View File

@ -0,0 +1,65 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Timeline | Order groups</title>
<style>
body, html {
font-family: arial, sans-serif;
font-size: 11pt;
}
#visualization {
box-sizing: border-box;
width: 100%;
height: 300px;
}
</style>
<script src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
</head>
<body>
<p>
This example demonstrate custom ordering of groups.
</p>
<div id="visualization"></div>
<script>
var groups = new vis.DataSet([
{id: 0, content: 'First', value: 1},
{id: 1, content: 'Third', value: 3},
{id: 2, content: 'Second', value: 2}
]);
// create a dataset with items
var items = new vis.DataSet([
{id: 0, group: 0, content: 'item 0', start: new Date(2014, 3, 17)},
{id: 1, group: 0, content: 'item 1', start: new Date(2014, 3, 19)},
{id: 2, group: 1, content: 'item 2', start: new Date(2014, 3, 16)},
{id: 3, group: 1, content: 'item 3', start: new Date(2014, 3, 23)},
{id: 4, group: 1, content: 'item 4', start: new Date(2014, 3, 22)},
{id: 5, group: 2, content: 'item 5', start: new Date(2014, 3, 24)}
]);
// create visualization
var container = document.getElementById('visualization');
var options = {
// option groupOrder can be a property name or a sort function
// the sort function must compare two groups and return a value
// > 0 when a > b
// < 0 when a < b
// 0 when a == b
groupOrder: function (a, b) {
return a.value - b.value;
}
};
var timeline = new vis.Timeline(container);
timeline.setOptions(options);
timeline.setGroups(groups);
timeline.setItems(items);
</script>
</body>
</html>

+ 51
- 0
examples/timeline/10_limit_move_and_zoom.html View File

@ -0,0 +1,51 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Timeline | Limit move and zoom</title>
<style>
body, html {
font-family: arial, sans-serif;
font-size: 11pt;
}
</style>
<script src="../../dist/vis.js"></script>
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
</head>
<body>
<p>
The visible range is limited in this demo:
</p>
<ul>
<li>minimum visible date is limited to 2012-01-01 using option <code>min</code></li>
<li>maximum visible date is limited to 2013-01-01 (excluded) using option <code>max</code></li>
<li>visible zoom interval is limited to a minimum of 24 hours using option <code>zoomMin</code></li>
<li>visible zoom interval is limited to a maximum of about 3 months using option <code>zoomMax</code></li>
</ul>
<div id="visualization"></div>
<script>
// create some items
var items = [
{'start': new Date(2012, 4, 25), 'content': 'First'},
{'start': new Date(2012, 4, 26), 'content': 'Last'}
];
// create visualization
var container = document.getElementById('visualization');
var options = {
height: '300px',
min: new Date(2012, 0, 1), // lower limit of visible range
max: new Date(2013, 0, 1), // upper limit of visible range
zoomMin: 1000 * 60 * 60 * 24, // one day in milliseconds
zoomMax: 1000 * 60 * 60 * 24 * 31 * 3 // about three months in milliseconds
};
// create the timeline
var timeline = new vis.Timeline(container);
timeline.setOptions(options);
timeline.setItems(items);
</script>
</body>
</html>

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

@ -18,6 +18,12 @@
<p><a href="04_html_data.html">04_html_data.html</a></p>
<p><a href="05_groups.html">05_groups.html</a></p>
<p><a href="06_event_listeners.html">06_event_listeners.html</a></p>
<p><a href="07_custom_time_bar.html">07_custom_time_bar.html</a></p>
<p><a href="08_edit_items.html">08_edit_items.html</a></p>
<p><a href="09_order_groups.html">09_order_groups.html</a></p>
<p><a href="10_limit_move_and_zoom.html">10_limit_range_and_zoom.html</a></p>
<p><a href="requirejs/requirejs_example.html">requirejs_example.html</a></p>
</div>
</body>

+ 2
- 12
misc/how_to_publish.md View File

@ -28,7 +28,7 @@ This generates the vis.js library in the folder `./dist`.
- Commit the changes to the `develop` branch.
- Merge the `develop` branch into the `master` branch.
- Push the brances to github
- Push the branches to github
- Create a version tag (with the new version number) and push it to github:
git tag v0.3.0
@ -55,17 +55,7 @@ This generates the vis.js library in the folder `./dist`.
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).
- Verify within an hour whether vis.js is updated on http://cdnjs.com/
## Update website

+ 3
- 1
package.json View File

@ -1,6 +1,6 @@
{
"name": "vis",
"version": "0.5.0-SNAPSHOT",
"version": "0.7.5-SNAPSHOT",
"description": "A dynamic, browser-based visualization library.",
"homepage": "http://visjs.org/",
"repository": {
@ -28,11 +28,13 @@
"devDependencies": {
"jake": "latest",
"jake-utils": "latest",
"clean-css": "latest",
"browserify": "3.22",
"wrench": "latest",
"moment": "latest",
"hammerjs": "1.0.5",
"mousetrap": "latest",
"emitter-component": "latest",
"node-watch": "latest"
}
}

+ 22
- 6
src/DataSet.js View File

@ -26,6 +26,7 @@
* - gives triggers upon changes in the data
* - can import/export data in various data formats
*
* @param {Array | DataTable} [data] Optional array with initial data
* @param {Object} [options] Available options:
* {String} fieldId Field name of the id in the
* items, 'id' by default.
@ -35,9 +36,15 @@
* @constructor DataSet
*/
// TODO: add a DataSet constructor DataSet(data, options)
function DataSet (options) {
function DataSet (data, options) {
this.id = util.randomUUID();
// correctly read optional arguments
if (data && !Array.isArray(data) && !util.isDataTable(data)) {
options = data;
data = null;
}
this.options = options || {};
this.data = {}; // map with data indexed by id
this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
@ -58,10 +65,13 @@ function DataSet (options) {
}
}
// event subscribers
this.subscribers = {};
this.subscribers = {}; // event subscribers
this.internalIds = {}; // internally generated id's
this.internalIds = {}; // internally generated id's
// add initial data when provided
if (data) {
this.add(data);
}
}
/**
@ -73,7 +83,7 @@ function DataSet (options) {
* {Object | null} params
* {String | Number} senderId
*/
DataSet.prototype.subscribe = function (event, callback) {
DataSet.prototype.on = function on (event, callback) {
var subscribers = this.subscribers[event];
if (!subscribers) {
subscribers = [];
@ -85,12 +95,15 @@ DataSet.prototype.subscribe = function (event, callback) {
});
};
// TODO: make this function deprecated (replaced with `on` since version 0.5)
DataSet.prototype.subscribe = DataSet.prototype.on;
/**
* Unsubscribe from an event, remove an event listener
* @param {String} event
* @param {function} callback
*/
DataSet.prototype.unsubscribe = function (event, callback) {
DataSet.prototype.off = function off(event, callback) {
var subscribers = this.subscribers[event];
if (subscribers) {
this.subscribers[event] = subscribers.filter(function (listener) {
@ -99,6 +112,9 @@ DataSet.prototype.unsubscribe = function (event, callback) {
}
};
// TODO: make this function deprecated (replaced with `on` since version 0.5)
DataSet.prototype.unsubscribe = DataSet.prototype.off;
/**
* Trigger an event
* @param {String} event

+ 8
- 4
src/DataView.js View File

@ -69,8 +69,8 @@ DataView.prototype.setData = function (data) {
this._trigger('add', {items: ids});
// subscribe to new dataset
if (this.data.subscribe) {
this.data.subscribe('*', this.listener);
if (this.data.on) {
this.data.on('*', this.listener);
}
}
};
@ -276,6 +276,10 @@ DataView.prototype._onEvent = function (event, params, senderId) {
};
// copy subscription functionality from DataSet
DataView.prototype.subscribe = DataSet.prototype.subscribe;
DataView.prototype.unsubscribe = DataSet.prototype.unsubscribe;
DataView.prototype.on = DataSet.prototype.on;
DataView.prototype.off = DataSet.prototype.off;
DataView.prototype._trigger = DataSet.prototype._trigger;
// TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5)
DataView.prototype.subscribe = DataView.prototype.on;
DataView.prototype.unsubscribe = DataView.prototype.off;

+ 0
- 89
src/EventBus.js View File

@ -1,89 +0,0 @@
/**
* An event bus can be used to emit events, and to subscribe to events
* @constructor EventBus
*/
function EventBus() {
this.subscriptions = [];
}
/**
* Subscribe to an event
* @param {String | RegExp} event The event can be a regular expression, or
* a string with wildcards, like 'server.*'.
* @param {function} callback. Callback are called with three parameters:
* {String} event, {*} [data], {*} [source]
* @param {*} [target]
* @returns {String} id A subscription id
*/
EventBus.prototype.on = function (event, callback, target) {
var regexp = (event instanceof RegExp) ?
event :
new RegExp(event.replace('*', '\\w+'));
var subscription = {
id: util.randomUUID(),
event: event,
regexp: regexp,
callback: (typeof callback === 'function') ? callback : null,
target: target
};
this.subscriptions.push(subscription);
return subscription.id;
};
/**
* Unsubscribe from an event
* @param {String | Object} filter Filter for subscriptions to be removed
* Filter can be a string containing a
* subscription id, or an object containing
* one or more of the fields id, event,
* callback, and target.
*/
EventBus.prototype.off = function (filter) {
var i = 0;
while (i < this.subscriptions.length) {
var subscription = this.subscriptions[i];
var match = true;
if (filter instanceof Object) {
// filter is an object. All fields must match
for (var prop in filter) {
if (filter.hasOwnProperty(prop)) {
if (filter[prop] !== subscription[prop]) {
match = false;
}
}
}
}
else {
// filter is a string, filter on id
match = (subscription.id == filter);
}
if (match) {
this.subscriptions.splice(i, 1);
}
else {
i++;
}
}
};
/**
* Emit an event
* @param {String} event
* @param {*} [data]
* @param {*} [source]
*/
EventBus.prototype.emit = function (event, data, source) {
for (var i =0; i < this.subscriptions.length; i++) {
var subscription = this.subscriptions[i];
if (subscription.regexp.test(event)) {
if (subscription.callback) {
subscription.callback(event, data, source);
}
}
}
};

+ 0
- 116
src/events.js View File

@ -1,116 +0,0 @@
/**
* Event listener (singleton)
*/
// TODO: replace usage of the event listener for the EventBus
var events = {
'listeners': [],
/**
* Find a single listener by its object
* @param {Object} object
* @return {Number} index -1 when not found
*/
'indexOf': function (object) {
var listeners = this.listeners;
for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
var listener = listeners[i];
if (listener && listener.object == object) {
return i;
}
}
return -1;
},
/**
* Add an event listener
* @param {Object} object
* @param {String} event The name of an event, for example 'select'
* @param {function} callback The callback method, called when the
* event takes place
*/
'addListener': function (object, event, callback) {
var index = this.indexOf(object);
var listener = this.listeners[index];
if (!listener) {
listener = {
'object': object,
'events': {}
};
this.listeners.push(listener);
}
var callbacks = listener.events[event];
if (!callbacks) {
callbacks = [];
listener.events[event] = callbacks;
}
// add the callback if it does not yet exist
if (callbacks.indexOf(callback) == -1) {
callbacks.push(callback);
}
},
/**
* Remove an event listener
* @param {Object} object
* @param {String} event The name of an event, for example 'select'
* @param {function} callback The registered callback method
*/
'removeListener': function (object, event, callback) {
var index = this.indexOf(object);
var listener = this.listeners[index];
if (listener) {
var callbacks = listener.events[event];
if (callbacks) {
index = callbacks.indexOf(callback);
if (index != -1) {
callbacks.splice(index, 1);
}
// remove the array when empty
if (callbacks.length == 0) {
delete listener.events[event];
}
}
// count the number of registered events. remove listener when empty
var count = 0;
var events = listener.events;
for (var e in events) {
if (events.hasOwnProperty(e)) {
count++;
}
}
if (count == 0) {
delete this.listeners[index];
}
}
},
/**
* Remove all registered event listeners
*/
'removeAllListeners': function () {
this.listeners = [];
},
/**
* Trigger an event. All registered event handlers will be called
* @param {Object} object
* @param {String} event
* @param {Object} properties (optional)
*/
'trigger': function (object, event, properties) {
var index = this.indexOf(object);
var listener = this.listeners[index];
if (listener) {
var callbacks = listener.events[event];
if (callbacks) {
for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
callbacks[i](properties);
}
}
}
}
};

+ 250
- 97
src/graph/Edge.js View File

@ -31,10 +31,14 @@ function Edge (properties, graph, constants) {
this.title = undefined;
this.width = constants.edges.width;
this.value = undefined;
this.length = constants.edges.length;
this.length = constants.physics.springLength;
this.customLength = false;
this.selected = false;
this.smooth = constants.smoothCurves;
this.from = null; // a node
this.to = null; // a node
this.via = null; // a temp node
// we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
// by storing the original information we can revert to the original connection when the cluser is opened.
@ -48,13 +52,12 @@ function Edge (properties, graph, constants) {
// 2012-08-08
this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
this.stiffness = undefined; // depends on the length of the edge
this.color = constants.edges.color;
this.color = {color:constants.edges.color.color,
highlight:constants.edges.color.highlight};
this.widthFixed = false;
this.lengthFixed = false;
this.setProperties(properties, constants);
}
/**
@ -73,18 +76,24 @@ Edge.prototype.setProperties = function(properties, constants) {
if (properties.id !== undefined) {this.id = properties.id;}
if (properties.style !== undefined) {this.style = properties.style;}
if (properties.label !== undefined) {this.label = properties.label;}
if (this.label) {
this.fontSize = constants.edges.fontSize;
this.fontFace = constants.edges.fontFace;
this.fontColor = constants.edges.fontColor;
this.fontFill = constants.edges.fontFill;
if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
if (properties.fontFill !== undefined) {this.fontFill = properties.fontFill;}
}
if (properties.title !== undefined) {this.title = properties.title;}
if (properties.width !== undefined) {this.width = properties.width;}
if (properties.value !== undefined) {this.value = properties.value;}
if (properties.length !== undefined) {this.length = properties.length;}
if (properties.title !== undefined) {this.title = properties.title;}
if (properties.width !== undefined) {this.width = properties.width;}
if (properties.value !== undefined) {this.value = properties.value;}
if (properties.length !== undefined) {this.length = properties.length;
this.customLength = true;}
// Added to support dashed lines
// David Jordan
@ -95,14 +104,22 @@ Edge.prototype.setProperties = function(properties, constants) {
if (properties.dash.altLength !== undefined) {this.dash.altLength = properties.dash.altLength;}
}
if (properties.color !== undefined) {this.color = properties.color;}
if (properties.color !== undefined) {
if (util.isString(properties.color)) {
this.color.color = properties.color;
this.color.highlight = properties.color;
}
else {
if (properties.color.color !== undefined) {this.color.color = properties.color.color;}
if (properties.color.highlight !== undefined) {this.color.highlight = properties.color.highlight;}
}
}
// A node is connected when it has a from and to node.
this.connect();
this.widthFixed = this.widthFixed || (properties.width !== undefined);
this.lengthFixed = this.lengthFixed || (properties.length !== undefined);
this.stiffness = 1 / this.length;
// set draw method based on style
switch (this.style) {
@ -160,7 +177,7 @@ Edge.prototype.disconnect = function () {
* has been set.
*/
Edge.prototype.getTitle = function() {
return this.title;
return typeof this.title === "function" ? this.title() : this.title;
};
@ -201,19 +218,22 @@ Edge.prototype.draw = function(ctx) {
* @return {boolean} True if location is located on the edge
*/
Edge.prototype.isOverlappingWith = function(obj) {
var distMax = 10;
var xFrom = this.from.x;
var yFrom = this.from.y;
var xTo = this.to.x;
var yTo = this.to.y;
var xObj = obj.left;
var yObj = obj.top;
if (this.connected) {
var distMax = 10;
var xFrom = this.from.x;
var yFrom = this.from.y;
var xTo = this.to.x;
var yTo = this.to.y;
var xObj = obj.left;
var yObj = obj.top;
var dist = Edge._dist(xFrom, yFrom, xTo, yTo, xObj, yObj);
var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
return (dist < distMax);
return (dist < distMax);
}
else {
return false
}
};
@ -226,17 +246,25 @@ Edge.prototype.isOverlappingWith = function(obj) {
*/
Edge.prototype._drawLine = function(ctx) {
// set style
ctx.strokeStyle = this.color;
if (this.selected == true) {ctx.strokeStyle = this.color.highlight;}
else {ctx.strokeStyle = this.color.color;}
ctx.lineWidth = this._getLineWidth();
var point;
if (this.from != this.to) {
// draw line
this._line(ctx);
// draw label
var point;
if (this.label) {
point = this._pointOnLine(0.5);
if (this.smooth == true) {
var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
point = {x:midpointX, y:midpointY};
}
else {
point = this._pointOnLine(0.5);
}
this._label(ctx, this.label, point.x, point.y);
}
}
@ -268,7 +296,7 @@ Edge.prototype._drawLine = function(ctx) {
* @private
*/
Edge.prototype._getLineWidth = function() {
if (this.from.selected || this.to.selected) {
if (this.selected == true) {
return Math.min(this.width * 2, this.widthMax)*this.graphScaleInv;
}
else {
@ -285,7 +313,12 @@ Edge.prototype._line = function (ctx) {
// draw a straight line
ctx.beginPath();
ctx.moveTo(this.from.x, this.from.y);
ctx.lineTo(this.to.x, this.to.y);
if (this.smooth == true) {
ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
}
else {
ctx.lineTo(this.to.x, this.to.y);
}
ctx.stroke();
};
@ -317,7 +350,7 @@ Edge.prototype._label = function (ctx, text, x, y) {
// TODO: cache the calculated size
ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
this.fontSize + "px " + this.fontFace;
ctx.fillStyle = 'white';
ctx.fillStyle = this.fontFill;
var width = ctx.measureText(text).width;
var height = this.fontSize;
var left = x - width / 2;
@ -344,32 +377,87 @@ Edge.prototype._label = function (ctx, text, x, y) {
*/
Edge.prototype._drawDashLine = function(ctx) {
// set style
ctx.strokeStyle = this.color;
if (this.selected == true) {ctx.strokeStyle = this.color.highlight;}
else {ctx.strokeStyle = this.color.color;}
ctx.lineWidth = this._getLineWidth();
// draw dashed line
ctx.beginPath();
ctx.lineCap = 'round';
if (this.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
{
ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
[this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
}
else if (this.dash.length !== undefined && this.dash.gap !== undefined) //If a dash and gap value has been set add to the array this value
{
ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
[this.dash.length,this.dash.gap]);
}
else //If all else fails draw a line
{
// only firefox and chrome support this method, else we use the legacy one.
if (ctx.mozDash !== undefined || ctx.setLineDash !== undefined) {
ctx.beginPath();
ctx.moveTo(this.from.x, this.from.y);
ctx.lineTo(this.to.x, this.to.y);
// configure the dash pattern
var pattern = [0];
if (this.dash.length !== undefined && this.dash.gap !== undefined) {
pattern = [this.dash.length,this.dash.gap];
}
else {
pattern = [5,5];
}
// set dash settings for chrome or firefox
if (typeof ctx.setLineDash !== 'undefined') { //Chrome
ctx.setLineDash(pattern);
ctx.lineDashOffset = 0;
} else { //Firefox
ctx.mozDash = pattern;
ctx.mozDashOffset = 0;
}
// draw the line
if (this.smooth == true) {
ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
}
else {
ctx.lineTo(this.to.x, this.to.y);
}
ctx.stroke();
// restore the dash settings.
if (typeof ctx.setLineDash !== 'undefined') { //Chrome
ctx.setLineDash([0]);
ctx.lineDashOffset = 0;
} else { //Firefox
ctx.mozDash = [0];
ctx.mozDashOffset = 0;
}
}
else { // unsupporting smooth lines
// draw dashed line
ctx.beginPath();
ctx.lineCap = 'round';
if (this.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
{
ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
[this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
}
else if (this.dash.length !== undefined && this.dash.gap !== undefined) //If a dash and gap value has been set add to the array this value
{
ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
[this.dash.length,this.dash.gap]);
}
else //If all else fails draw a line
{
ctx.moveTo(this.from.x, this.from.y);
ctx.lineTo(this.to.x, this.to.y);
}
ctx.stroke();
}
ctx.stroke();
// draw label
if (this.label) {
var point = this._pointOnLine(0.5);
var point;
if (this.smooth == true) {
var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
point = {x:midpointX, y:midpointY};
}
else {
point = this._pointOnLine(0.5);
}
this._label(ctx, this.label, point.x, point.y);
}
};
@ -414,43 +502,50 @@ Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
Edge.prototype._drawArrowCenter = function(ctx) {
var point;
// set style
ctx.strokeStyle = this.color;
ctx.fillStyle = this.color;
if (this.selected == true) {ctx.strokeStyle = this.color.highlight; ctx.fillStyle = this.color.highlight;}
else {ctx.strokeStyle = this.color.color; ctx.fillStyle = this.color.color;}
ctx.lineWidth = this._getLineWidth();
if (this.from != this.to) {
// draw line
this._line(ctx);
// draw an arrow halfway the line
var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
var length = 10 + 5 * this.width; // TODO: make customizable?
point = this._pointOnLine(0.5);
// draw an arrow halfway the line
if (this.smooth == true) {
var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
point = {x:midpointX, y:midpointY};
}
else {
point = this._pointOnLine(0.5);
}
ctx.arrow(point.x, point.y, angle, length);
ctx.fill();
ctx.stroke();
// draw label
if (this.label) {
point = this._pointOnLine(0.5);
this._label(ctx, this.label, point.x, point.y);
}
}
else {
// draw circle
var x, y;
var radius = this.length / 4;
var radius = 0.25 * Math.max(100,this.length);
var node = this.from;
if (!node.width) {
node.resize(ctx);
}
if (node.width > node.height) {
x = node.x + node.width / 2;
x = node.x + node.width * 0.5;
y = node.y - radius;
}
else {
x = node.x + radius;
y = node.y - node.height / 2;
y = node.y - node.height * 0.5;
}
this._circle(ctx, x, y, radius);
@ -481,43 +576,71 @@ Edge.prototype._drawArrowCenter = function(ctx) {
*/
Edge.prototype._drawArrow = function(ctx) {
// set style
ctx.strokeStyle = this.color;
ctx.fillStyle = this.color;
if (this.selected == true) {ctx.strokeStyle = this.color.highlight; ctx.fillStyle = this.color.highlight;}
else {ctx.strokeStyle = this.color.color; ctx.fillStyle = this.color.color;}
ctx.lineWidth = this._getLineWidth();
// draw line
var angle, length;
//draw a line
if (this.from != this.to) {
// calculate length and angle of the line
angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
var dx = (this.to.x - this.from.x);
var dy = (this.to.y - this.from.y);
var lEdge = Math.sqrt(dx * dx + dy * dy);
var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
var lFrom = this.from.distanceToBorder(ctx, angle + Math.PI);
var pFrom = (lEdge - lFrom) / lEdge;
var xFrom = (pFrom) * this.from.x + (1 - pFrom) * this.to.x;
var yFrom = (pFrom) * this.from.y + (1 - pFrom) * this.to.y;
var lTo = this.to.distanceToBorder(ctx, angle);
var pTo = (lEdge - lTo) / lEdge;
var xTo = (1 - pTo) * this.from.x + pTo * this.to.x;
var yTo = (1 - pTo) * this.from.y + pTo * this.to.y;
if (this.smooth == true) {
angle = Math.atan2((this.to.y - this.via.y), (this.to.x - this.via.x));
dx = (this.to.x - this.via.x);
dy = (this.to.y - this.via.y);
edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
}
var toBorderDist = this.to.distanceToBorder(ctx, angle);
var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
var xTo,yTo;
if (this.smooth == true) {
xTo = (1 - toBorderPoint) * this.via.x + toBorderPoint * this.to.x;
yTo = (1 - toBorderPoint) * this.via.y + toBorderPoint * this.to.y;
}
else {
xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
}
ctx.beginPath();
ctx.moveTo(xFrom, yFrom);
ctx.lineTo(xTo, yTo);
ctx.moveTo(xFrom,yFrom);
if (this.smooth == true) {
ctx.quadraticCurveTo(this.via.x,this.via.y,xTo, yTo);
}
else {
ctx.lineTo(xTo, yTo);
}
ctx.stroke();
// draw arrow at the end of the line
length = 10 + 5 * this.width; // TODO: make customizable?
length = 10 + 5 * this.width;
ctx.arrow(xTo, yTo, angle, length);
ctx.fill();
ctx.stroke();
// draw label
if (this.label) {
var point = this._pointOnLine(0.5);
var point;
if (this.smooth == true) {
var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
point = {x:midpointX, y:midpointY};
}
else {
point = this._pointOnLine(0.5);
}
this._label(ctx, this.label, point.x, point.y);
}
}
@ -525,12 +648,12 @@ Edge.prototype._drawArrow = function(ctx) {
// draw circle
var node = this.from;
var x, y, arrow;
var radius = this.length / 4;
var radius = 0.25 * Math.max(100,this.length);
if (!node.width) {
node.resize(ctx);
}
if (node.width > node.height) {
x = node.x + node.width / 2;
x = node.x + node.width * 0.5;
y = node.y - radius;
arrow = {
x: x,
@ -540,7 +663,7 @@ Edge.prototype._drawArrow = function(ctx) {
}
else {
x = node.x + radius;
y = node.y - node.height / 2;
y = node.y - node.height * 0.5;
arrow = {
x: node.x,
y: y,
@ -548,7 +671,6 @@ Edge.prototype._drawArrow = function(ctx) {
};
}
ctx.beginPath();
// TODO: do not draw a circle, but an arc
// TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
ctx.stroke();
@ -581,31 +703,46 @@ Edge.prototype._drawArrow = function(ctx) {
* @param {number} y3
* @private
*/
Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
var px = x2-x1,
py = y2-y1,
something = px*px + py*py,
u = ((x3 - x1) * px + (y3 - y1) * py) / something;
if (u > 1) {
u = 1;
}
else if (u < 0) {
u = 0;
Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
if (this.smooth == true) {
var minDistance = 1e9;
var i,t,x,y,dx,dy;
for (i = 0; i < 10; i++) {
t = 0.1*i;
x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*this.via.x + Math.pow(t,2)*x2;
y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*this.via.y + Math.pow(t,2)*y2;
dx = Math.abs(x3-x);
dy = Math.abs(y3-y);
minDistance = Math.min(minDistance,Math.sqrt(dx*dx + dy*dy));
}
return minDistance
}
else {
var px = x2-x1,
py = y2-y1,
something = px*px + py*py,
u = ((x3 - x1) * px + (y3 - y1) * py) / something;
if (u > 1) {
u = 1;
}
else if (u < 0) {
u = 0;
}
var x = x1 + u * px,
y = y1 + u * py,
dx = x - x3,
dy = y - y3;
var x = x1 + u * px,
y = y1 + u * py,
dx = x - x3,
dy = y - y3;
//# Note: If the actual distance does not matter,
//# if you only want to compare what this function
//# returns to other results of this function, you
//# can just return the squared distance instead
//# (i.e. remove the sqrt) to gain a little performance
//# Note: If the actual distance does not matter,
//# if you only want to compare what this function
//# returns to other results of this function, you
//# can just return the squared distance instead
//# (i.e. remove the sqrt) to gain a little performance
return Math.sqrt(dx*dx + dy*dy);
return Math.sqrt(dx*dx + dy*dy);
}
};
@ -617,4 +754,20 @@ Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
*/
Edge.prototype.setScale = function(scale) {
this.graphScaleInv = 1.0/scale;
};
Edge.prototype.select = function() {
this.selected = true;
};
Edge.prototype.unselect = function() {
this.selected = false;
};
Edge.prototype.positionBezierNode = function() {
if (this.via !== null) {
this.via.x = 0.5 * (this.from.x + this.to.x);
this.via.y = 0.5 * (this.from.y + this.to.y);
}
};

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


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

@ -74,7 +74,7 @@ Groups.prototype.get = function (groupname) {
Groups.prototype.add = function (groupname, style) {
this.groups[groupname] = style;
if (style.color) {
style.color = Node.parseColor(style.color);
style.color = util.parseColor(style.color);
}
return style;
};

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

@ -1,245 +0,0 @@
/**
* 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;
}
};

+ 105
- 118
src/graph/Node.js View File

@ -21,6 +21,7 @@
* retrieving group properties
* @param {Object} constants An object with default values for
* example for the color
*
*/
function Node(properties, imagelist, grouplist, constants) {
this.selected = false;
@ -33,6 +34,7 @@ function Node(properties, imagelist, grouplist, constants) {
this.fontSize = constants.nodes.fontSize;
this.fontFace = constants.nodes.fontFace;
this.fontColor = constants.nodes.fontColor;
this.fontDrawThreshold = 3;
this.color = constants.nodes.color;
@ -40,8 +42,8 @@ function Node(properties, imagelist, grouplist, constants) {
this.id = undefined;
this.shape = constants.nodes.shape;
this.image = constants.nodes.image;
this.x = 0;
this.y = 0;
this.x = null;
this.y = null;
this.xFixed = false;
this.yFixed = false;
this.horizontalAlignLeft = true; // these are for the navigation controls
@ -51,11 +53,23 @@ function Node(properties, imagelist, grouplist, constants) {
this.radiusFixed = false;
this.radiusMin = constants.nodes.radiusMin;
this.radiusMax = constants.nodes.radiusMax;
this.level = -1;
this.preassignedLevel = false;
this.imagelist = imagelist;
this.imagelist = imagelist;
this.grouplist = grouplist;
// physics properties
this.fx = 0.0; // external force x
this.fy = 0.0; // external force y
this.vx = 0.0; // velocity x
this.vy = 0.0; // velocity y
this.minForce = constants.minForce;
this.damping = constants.physics.damping;
this.mass = 1; // kg
this.fixedData = {x:null,y:null};
this.setProperties(properties, constants);
// creating the variables for clustering
@ -65,20 +79,15 @@ function Node(properties, imagelist, grouplist, constants) {
this.clusterSizeWidthFactor = constants.clustering.nodeScaling.width;
this.clusterSizeHeightFactor = constants.clustering.nodeScaling.height;
this.clusterSizeRadiusFactor = constants.clustering.nodeScaling.radius;
this.maxNodeSizeIncrements = constants.clustering.maxNodeSizeIncrements;
this.growthIndicator = 0;
// mass, force, velocity
this.mass = 1; // kg (mass is adjusted for the number of connected edges)
this.fx = 0.0; // external force x
this.fy = 0.0; // external force y
this.vx = 0.0; // velocity x
this.vy = 0.0; // velocity y
this.minForce = constants.minForce;
this.damping = 0.9;
this.dampingFactor = 75;
// variables to tell the node about the graph.
this.graphScaleInv = 1;
this.graphScale = 1;
this.canvasTopLeft = {"x": -300, "y": -300};
this.canvasBottomRight = {"x": 300, "y": 300};
this.parentEdgeId = null;
}
/**
@ -105,7 +114,6 @@ Node.prototype.attachEdge = function(edge) {
this.dynamicEdges.push(edge);
}
this.dynamicEdgesLength = this.dynamicEdges.length;
this._updateMass();
};
/**
@ -119,17 +127,8 @@ Node.prototype.detachEdge = function(edge) {
this.dynamicEdges.splice(index, 1);
}
this.dynamicEdgesLength = this.dynamicEdges.length;
this._updateMass();
};
/**
* Update the nodes mass, which is determined by the number of edges connecting
* to it (more edges -> heavier node).
* @private
*/
Node.prototype._updateMass = function() {
this.mass = 1 + 0.6 * this.edges.length; // kg
};
/**
* Set or overwrite properties for the node
@ -149,6 +148,11 @@ Node.prototype.setProperties = function(properties, constants) {
if (properties.x !== undefined) {this.x = properties.x;}
if (properties.y !== undefined) {this.y = properties.y;}
if (properties.value !== undefined) {this.value = properties.value;}
if (properties.level !== undefined) {this.level = properties.level; this.preassignedLevel = true;}
// physics
if (properties.mass !== undefined) {this.mass = properties.mass;}
// navigation controls properties
if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
@ -173,13 +177,13 @@ Node.prototype.setProperties = function(properties, constants) {
if (properties.shape !== undefined) {this.shape = properties.shape;}
if (properties.image !== undefined) {this.image = properties.image;}
if (properties.radius !== undefined) {this.radius = properties.radius;}
if (properties.color !== undefined) {this.color = Node.parseColor(properties.color);}
if (properties.color !== undefined) {this.color = util.parseColor(properties.color);}
if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
if (this.image !== undefined) {
if (this.image !== undefined && this.image != "") {
if (this.imagelist) {
this.imageObj = this.imagelist.load(this.image);
}
@ -188,8 +192,8 @@ Node.prototype.setProperties = function(properties, constants) {
}
}
this.xFixed = this.xFixed || (properties.x !== undefined);
this.yFixed = this.yFixed || (properties.y !== undefined);
this.xFixed = this.xFixed || (properties.x !== undefined && !properties.allowedToMoveX);
this.yFixed = this.yFixed || (properties.y !== undefined && !properties.allowedToMoveY);
this.radiusFixed = this.radiusFixed || (properties.radius !== undefined);
if (this.shape == 'image') {
@ -217,46 +221,6 @@ Node.prototype.setProperties = function(properties, constants) {
this._reset();
};
/**
* Parse a color property into an object with border, background, and
* hightlight colors
* @param {Object | String} color
* @return {Object} colorObject
*/
Node.parseColor = function(color) {
var c;
if (util.isString(color)) {
c = {
border: color,
background: color,
highlight: {
border: color,
background: color
}
};
// TODO: automatically generate a nice highlight color
}
else {
c = {};
c.background = color.background || 'white';
c.border = color.border || c.background;
if (util.isString(color.highlight)) {
c.highlight = {
border: color.highlight,
background: color.highlight
}
}
else {
c.highlight = {};
c.highlight.background = color.highlight && color.highlight.background || c.background;
c.highlight.border = color.highlight && color.highlight.border || c.border;
}
}
return c;
};
/**
* select this node
*/
@ -296,7 +260,7 @@ Node.prototype._reset = function() {
* has been set.
*/
Node.prototype.getTitle = function() {
return this.title;
return typeof this.title === "function" ? this.title() : this.title;
};
/**
@ -312,7 +276,6 @@ Node.prototype.distanceToBorder = function (ctx, angle) {
this.resize(ctx);
}
//noinspection FallthroughInSwitchStatementJS
switch (this.shape) {
case 'circle':
case 'dot':
@ -344,7 +307,6 @@ Node.prototype.distanceToBorder = function (ctx, angle) {
}
}
// TODO: implement calculation of distance to border for all shapes
};
@ -375,21 +337,50 @@ Node.prototype._addForce = function(fx, fy) {
*/
Node.prototype.discreteStep = function(interval) {
if (!this.xFixed) {
var dx = -this.damping * this.vx; // damping force
var ax = (this.fx + dx) / this.mass; // acceleration
var dx = this.damping * this.vx; // damping force
var ax = (this.fx - dx) / this.mass; // acceleration
this.vx += ax * interval; // velocity
this.x += this.vx * interval; // position
}
if (!this.yFixed) {
var dy = -this.damping * this.vy; // damping force
var ay = (this.fy + dy) / this.mass; // acceleration
var dy = this.damping * this.vy; // damping force
var ay = (this.fy - dy) / this.mass; // acceleration
this.vy += ay * interval; // velocity
this.y += this.vy * interval; // position
}
};
/**
* Perform one discrete step for the node
* @param {number} interval Time interval in seconds
*/
Node.prototype.discreteStepLimited = function(interval, maxVelocity) {
if (!this.xFixed) {
var dx = this.damping * this.vx; // damping force
var ax = (this.fx - dx) / this.mass; // acceleration
this.vx += ax * interval; // velocity
this.vx = (Math.abs(this.vx) > maxVelocity) ? ((this.vx > 0) ? maxVelocity : -maxVelocity) : this.vx;
this.x += this.vx * interval; // position
}
else {
this.fx = 0;
}
if (!this.yFixed) {
var dy = this.damping * this.vy; // damping force
var ay = (this.fy - dy) / this.mass; // acceleration
this.vy += ay * interval; // velocity
this.vy = (Math.abs(this.vy) > maxVelocity) ? ((this.vy > 0) ? maxVelocity : -maxVelocity) : this.vy;
this.y += this.vy * interval; // position
}
else {
this.fy = 0;
}
};
/**
* Check if this node has a fixed x and y position
* @return {boolean} true if fixed, false if not
@ -405,16 +396,7 @@ Node.prototype.isFixed = function() {
*/
// TODO: replace this method with calculating the kinetic energy
Node.prototype.isMoving = function(vmin) {
if (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin) {
// console.log(vmin,this.vx,this.vy);
return true;
}
else {
this.vx = 0; this.vy = 0;
return false;
}
//return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin);
return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin);
};
/**
@ -519,10 +501,12 @@ Node.prototype._resizeImage = function (ctx) {
this.width = width;
this.height = height;
this.growthIndicator = 0;
if (this.width > 0 && this.height > 0) {
this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor;
this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor;
this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor;
this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
this.growthIndicator = this.width - width;
}
}
@ -567,9 +551,11 @@ Node.prototype._resizeBox = function (ctx) {
this.width = textSize.width + 2 * margin;
this.height = textSize.height + 2 * margin;
this.width += (this.clusterSize - 1) * 0.5 * this.clusterSizeWidthFactor;
this.height += (this.clusterSize - 1) * 0.5 * this.clusterSizeHeightFactor;
// this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor;
this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
this.growthIndicator = this.width - (textSize.width + 2 * margin);
// this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
}
};
@ -616,9 +602,10 @@ Node.prototype._resizeDatabase = function (ctx) {
this.height = size;
// scaling used for clustering
this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor;
this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor;
this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor;
this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
this.growthIndicator = this.width - size;
}
};
@ -665,9 +652,10 @@ Node.prototype._resizeCircle = function (ctx) {
this.height = diameter;
// scaling used for clustering
// this.width += (this.clusterSize - 1) * 0.5 * this.clusterSizeWidthFactor;
// this.height += (this.clusterSize - 1) * 0.5 * this.clusterSizeHeightFactor;
this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor;
// this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
// this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
this.growthIndicator = this.radius - 0.5*diameter;
}
};
@ -711,11 +699,13 @@ Node.prototype._resizeEllipse = function (ctx) {
if (this.width < this.height) {
this.width = this.height;
}
var defaultSize = this.width;
// scaling used for clustering
this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor;
this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor;
this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor;
// scaling used for clustering
this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
this.growthIndicator = this.width - defaultSize;
}
};
@ -778,9 +768,10 @@ Node.prototype._resizeShape = function (ctx) {
this.height = size;
// scaling used for clustering
this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor;
this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor;
this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor;
this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
this.growthIndicator = this.width - size;
}
};
@ -837,9 +828,10 @@ Node.prototype._resizeText = function (ctx) {
this.height = textSize.height + 2 * margin;
// scaling used for clustering
this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor;
this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor;
this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor;
this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
this.growthIndicator = this.width - (textSize.width + 2 * margin);
}
};
@ -853,7 +845,7 @@ Node.prototype._drawText = function (ctx) {
Node.prototype._label = function (ctx, text, x, y, align, baseline) {
if (text) {
if (text && this.fontSize * this.graphScale > this.fontDrawThreshold) {
ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
ctx.fillStyle = this.fontColor || "black";
ctx.textAlign = align || "center";
@ -907,7 +899,7 @@ Node.prototype.inArea = function() {
else {
return true;
}
}
};
/**
* checks if the core of the node is in the display area, this is used for opening clusters around zoom
@ -918,7 +910,7 @@ Node.prototype.inView = function() {
this.x < this.canvasBottomRight.x &&
this.y >= this.canvasTopLeft.y &&
this.y < this.canvasBottomRight.y);
}
};
/**
* This allows the zoom level of the graph to influence the rendering
@ -930,6 +922,7 @@ Node.prototype.inView = function() {
*/
Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) {
this.graphScaleInv = 1.0/scale;
this.graphScale = scale;
this.canvasTopLeft = canvasTopLeft;
this.canvasBottomRight = canvasBottomRight;
};
@ -942,17 +935,9 @@ Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight)
*/
Node.prototype.setScale = function(scale) {
this.graphScaleInv = 1.0/scale;
this.graphScale = scale;
};
/**
* This function updates the damping parameter for clusters, based ont he
*
* @param {Number} numberOfNodes
*/
Node.prototype.updateDamping = function(numberOfNodes) {
this.damping = (0.8 + 0.1*this.clusterSize * (1 + Math.pow(numberOfNodes,-2)));
this.damping *= this.dampingFactor;
};
/**
@ -971,8 +956,10 @@ Node.prototype.clearVelocity = function() {
*/
Node.prototype.updateVelocity = function(massBeforeClustering) {
var energyBefore = this.vx * this.vx * massBeforeClustering;
//this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
this.vx = Math.sqrt(energyBefore/this.mass);
energyBefore = this.vy * this.vy * massBeforeClustering;
//this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
this.vy = Math.sqrt(energyBefore/this.mass);
};

+ 40
- 13
src/graph/Popup.js View File

@ -4,14 +4,39 @@
* @param {Number} [x]
* @param {Number} [y]
* @param {String} [text]
* @param {Object} [style] An object containing borderColor,
* backgroundColor, etc.
*/
function Popup(container, x, y, text) {
function Popup(container, x, y, text, style) {
if (container) {
this.container = container;
}
else {
this.container = document.body;
}
// x, y and text are optional, see if a style object was passed in their place
if (style === undefined) {
if (typeof x === "object") {
style = x;
x = undefined;
} else if (typeof text === "object") {
style = text;
text = undefined;
} else {
// for backwards compatibility, in case clients other than Graph are creating Popup directly
style = {
fontColor: 'black',
fontSize: 14, // px
fontFace: 'verdana',
color: {
border: '#666',
background: '#FFFFC6'
}
}
}
}
this.x = 0;
this.y = 0;
this.padding = 5;
@ -25,18 +50,20 @@ function Popup(container, x, y, text) {
// create the frame
this.frame = document.createElement("div");
var style = this.frame.style;
style.position = "absolute";
style.visibility = "hidden";
style.border = "1px solid #666";
style.color = "black";
style.padding = this.padding + "px";
style.backgroundColor = "#FFFFC6";
style.borderRadius = "3px";
style.MozBorderRadius = "3px";
style.WebkitBorderRadius = "3px";
style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
style.whiteSpace = "nowrap";
var styleAttr = this.frame.style;
styleAttr.position = "absolute";
styleAttr.visibility = "hidden";
styleAttr.border = "1px solid " + style.color.border;
styleAttr.color = style.fontColor;
styleAttr.fontSize = style.fontSize + "px";
styleAttr.fontFamily = style.fontFace;
styleAttr.padding = this.padding + "px";
styleAttr.backgroundColor = style.color.background;
styleAttr.borderRadius = "3px";
styleAttr.MozBorderRadius = "3px";
styleAttr.WebkitBorderRadius = "3px";
styleAttr.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
styleAttr.whiteSpace = "nowrap";
this.container.appendChild(this.frame);
}

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

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

+ 128
- 0
src/graph/css/graph-manipulation.css View File

@ -0,0 +1,128 @@
div.graph-manipulationDiv {
border-width:0px;
border-bottom: 1px;
border-style:solid;
border-color: #d6d9d8;
background: #ffffff; /* Old browsers */
background: -moz-linear-gradient(top, #ffffff 0%, #fcfcfc 48%, #fafafa 50%, #fcfcfc 100%); /* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(48%,#fcfcfc), color-stop(50%,#fafafa), color-stop(100%,#fcfcfc)); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Opera 11.10+ */
background: -ms-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* IE10+ */
background: linear-gradient(to bottom, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* W3C */
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#fcfcfc',GradientType=0 ); /* IE6-9 */
width: 600px;
height:30px;
z-index:10;
position:absolute;
}
div.graph-manipulation-editMode {
height:30px;
z-index:10;
position:absolute;
margin-top:20px;
}
div.graph-manipulation-closeDiv {
height:30px;
width:30px;
z-index:11;
position:absolute;
margin-top:3px;
margin-left:590px;
background-position: 0px 0px;
background-repeat:no-repeat;
background-image: url("img/graph/cross.png");
cursor: pointer;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
span.graph-manipulationUI {
font-family: verdana;
font-size: 12px;
-moz-border-radius: 15px;
border-radius: 15px;
display:inline-block;
background-position: 0px 0px;
background-repeat:no-repeat;
height:24px;
margin: -14px 0px 0px 10px;
vertical-align:middle;
cursor: pointer;
padding: 0px 8px 0px 8px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
span.graph-manipulationUI:hover {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.20);
}
span.graph-manipulationUI:active {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.50);
}
span.graph-manipulationUI.back {
background-image: url("img/graph/backIcon.png");
}
span.graph-manipulationUI.none:hover {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0);
cursor: default;
}
span.graph-manipulationUI.none:active {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0);
}
span.graph-manipulationUI.none {
padding: 0px 0px 0px 0px;
}
span.graph-manipulationUI.notification{
margin: 2px;
font-weight: bold;
}
span.graph-manipulationUI.add {
background-image: url("img/graph/addNodeIcon.png");
}
span.graph-manipulationUI.edit {
background-image: url("img/graph/editIcon.png");
}
span.graph-manipulationUI.edit.editmode {
background-color: #fcfcfc;
border-style:solid;
border-width:1px;
border-color: #cccccc;
}
span.graph-manipulationUI.connect {
background-image: url("img/graph/connectIcon.png");
}
span.graph-manipulationUI.delete {
background-image: url("img/graph/deleteIcon.png");
}
/* top right bottom left */
span.graph-manipulationLabel {
margin: 0px 0px 0px 23px;
line-height: 25px;
}
div.graph-seperatorLine {
display:inline-block;
width:1px;
height:20px;
background-color: #bdbdbd;
margin: 5px 7px 0px 15px;
}

+ 66
- 0
src/graph/css/graph-navigation.css View File

@ -0,0 +1,66 @@
div.graph-navigation {
width:34px;
height:34px;
z-index:10;
-moz-border-radius: 17px;
border-radius: 17px;
position:absolute;
display:inline-block;
background-position: 2px 2px;
background-repeat:no-repeat;
cursor: pointer;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
div.graph-navigation:hover {
box-shadow: 0px 0px 3px 3px rgba(56, 207, 21, 0.30);
}
div.graph-navigation:active {
box-shadow: 0px 0px 1px 3px rgba(56, 207, 21, 0.95);
}
div.graph-navigation.active {
box-shadow: 0px 0px 1px 3px rgba(56, 207, 21, 0.95);
}
div.graph-navigation.up {
background-image: url("img/graph/upArrow.png");
bottom:50px;
left:55px;
}
div.graph-navigation.down {
background-image: url("img/graph/downArrow.png");
bottom:10px;
left:55px;
}
div.graph-navigation.left {
background-image: url("img/graph/leftArrow.png");
bottom:10px;
left:15px;
}
div.graph-navigation.right {
background-image: url("img/graph/rightArrow.png");
bottom:10px;
left:95px;
}
div.graph-navigation.zoomIn {
background-image: url("img/graph/plus.png");
bottom:10px;
right:15px;
}
div.graph-navigation.zoomOut {
background-image: url("img/graph/minus.png");
bottom:10px;
right:55px;
}
div.graph-navigation.zoomExtends {
background-image: url("img/graph/zoomExtends.png");
bottom:50px;
right:15px;
}

src/graph/ClusterMixin.js → src/graph/graphMixins/ClusterMixin.js View File

@ -8,23 +8,24 @@
*/
var ClusterMixin = {
/**
* This is only called in the constructor of the graph object
* */
/**
* This is only called in the constructor of the graph object
*
*/
startWithClustering : function() {
// cluster if the data set is big
this.clusterToFit(this.constants.clustering.initialMaxNodes, true);
// cluster if the data set is big
this.clusterToFit(this.constants.clustering.initialMaxNodes, true);
// updates the lables after clustering
this.updateLabels();
// updates the lables after clustering
this.updateLabels();
// this is called here because if clusterin is disabled, the start and stabilize are called in
// the setData function.
if (this.stabilize) {
this._doStabilize();
}
this.start();
},
// this is called here because if clusterin is disabled, the start and stabilize are called in
// the setData function.
if (this.stabilize) {
this._stabilize();
}
this.start();
},
/**
* This function clusters until the initialMaxNodes has been reached
@ -41,20 +42,23 @@ var ClusterMixin = {
// we first cluster the hubs, then we pull in the outliers, repeat
while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
if (level % 3 == 0) {
this.forceAggregateHubs();
this.forceAggregateHubs(true);
this.normalizeClusterLevels();
}
else {
this.increaseClusterLevel();
this.increaseClusterLevel(); // this also includes a cluster normalization
}
numberOfNodes = this.nodeIndices.length;
level += 1;
}
// after the clustering we reposition the nodes to reduce the initial chaos
if (level > 1 && reposition == true) {
if (level > 0 && reposition == true) {
this.repositionNodes();
}
},
this._updateCalculationNodes();
},
/**
* This function can be called to open up a specific cluster. It is only called by
@ -66,12 +70,16 @@ var ClusterMixin = {
var isMovingBeforeClustering = this.moving;
if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) &&
!(this._sector() == "default" && this.nodeIndices.length == 1)) {
// this loads a new sector, loads the nodes and edges and nodeIndices of it.
this._addSector(node);
var level = 0;
// we decluster until we reach a decent number of nodes
while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) {
this.decreaseClusterLevel();
level += 1;
}
}
else {
this._expandClusterNode(node,false,true);
@ -79,6 +87,7 @@ var ClusterMixin = {
// update the index list, dynamic edges and labels
this._updateNodeIndexList();
this._updateDynamicEdges();
this._updateCalculationNodes();
this.updateLabels();
}
@ -86,7 +95,8 @@ var ClusterMixin = {
if (this.moving != isMovingBeforeClustering) {
this.start();
}
},
},
/**
* This calls the updateClustes with default arguments
@ -97,6 +107,7 @@ var ClusterMixin = {
}
},
/**
* This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
* be clustered with their connected node. This can be repeated as many times as needed.
@ -104,8 +115,7 @@ var ClusterMixin = {
*/
increaseClusterLevel : function() {
this.updateClusters(-1,false,true);
},
},
/**
@ -115,7 +125,7 @@ var ClusterMixin = {
*/
decreaseClusterLevel : function() {
this.updateClusters(1,false,true);
},
},
/**
@ -129,7 +139,7 @@ var ClusterMixin = {
* @param {Boolean} force | enabled or disable forcing
*
*/
updateClusters : function(zoomDirection,recursive,force) {
updateClusters : function(zoomDirection,recursive,force,doNotStart) {
var isMovingBeforeClustering = this.moving;
var amountOfNodes = this.nodeIndices.length;
@ -178,13 +188,19 @@ var ClusterMixin = {
// if a cluster was formed, we increase the clusterSession
if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
this.clusterSession += 1;
// if clusters have been made, we normalize the cluster level
this.normalizeClusterLevels();
}
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
if (doNotStart == false || doNotStart === undefined) {
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
}
}
},
this._updateCalculationNodes();
},
/**
* This function handles the chains. It is called on every updateClusters().
@ -196,7 +212,7 @@ var ClusterMixin = {
this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage)
}
},
},
/**
* this functions starts clustering by hubs
@ -207,14 +223,14 @@ var ClusterMixin = {
_aggregateHubs : function(force) {
this._getHubSize();
this._formClustersByHub(force,false);
},
},
/**
* This function is fired by keypress. It forces hubs to form.
*
*/
forceAggregateHubs : function() {
forceAggregateHubs : function(doNotStart) {
var isMovingBeforeClustering = this.moving;
var amountOfNodes = this.nodeIndices.length;
@ -230,11 +246,13 @@ var ClusterMixin = {
this.clusterSession += 1;
}
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
if (doNotStart == false || doNotStart === undefined) {
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
}
}
},
},
/**
* If a cluster takes up more than a set percentage of the screen, open the cluster
@ -253,7 +271,7 @@ var ClusterMixin = {
}
}
}
},
},
/**
@ -266,8 +284,9 @@ var ClusterMixin = {
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
this._expandClusterNode(node,recursive,force);
this._updateCalculationNodes();
}
},
},
/**
* This function checks if a node has to be opened. This is done by checking the zoom level.
@ -313,7 +332,7 @@ var ClusterMixin = {
}
}
}
},
},
/**
* ONLY CALLED FROM _expandClusterNode
@ -330,11 +349,14 @@ var ClusterMixin = {
* @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
* @private
*/
_expelChildFromParent : function(parentNode, containedNodeId, recursive, force, openAll) {
_expelChildFromParent : function(parentNode, containedNodeId, recursive, force, openAll) {
var childNode = parentNode.containedNodes[containedNodeId];
// if child node has been added on smaller scale than current, kick out
if (childNode.formationScale < this.scale || force == true) {
// unselect all selected items
this._unselectAll();
// put the child node back in the global nodes object
this.nodes[containedNodeId] = childNode;
@ -348,14 +370,14 @@ var ClusterMixin = {
this._validateEdges(parentNode);
// undo the changes from the clustering operation on the parent node
parentNode.mass -= this.constants.clustering.massTransferCoefficient * childNode.mass;
parentNode.fontSize -= this.constants.clustering.fontSizeMultiplier * childNode.clusterSize;
parentNode.mass -= childNode.mass;
parentNode.clusterSize -= childNode.clusterSize;
parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
// place the child node near the parent, not at the exact same location to avoid chaos in the system
childNode.x = parentNode.x + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize;
childNode.y = parentNode.y + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize;
childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random());
childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random());
// remove node from the list
delete parentNode.containedNodes[containedNodeId];
@ -375,21 +397,37 @@ var ClusterMixin = {
parentNode.clusterSessions.pop();
}
this._repositionBezierNodes(childNode);
// this._repositionBezierNodes(parentNode);
// remove the clusterSession from the child node
childNode.clusterSession = 0;
// restart the simulation to reorganise all nodes
this.moving = true;
// recalculate the size of the node on the next time the node is rendered
parentNode.clearSizeCache();
// restart the simulation to reorganise all nodes
this.moving = true;
}
// check if a further expansion step is possible if recursivity is enabled
if (recursive == true) {
this._expandClusterNode(childNode,recursive,force,openAll);
}
},
},
/**
* position the bezier nodes at the center of the edges
*
* @param node
* @private
*/
_repositionBezierNodes : function(node) {
for (var i = 0; i < node.dynamicEdges.length; i++) {
node.dynamicEdges[i].positionBezierNode();
}
},
/**
@ -408,7 +446,8 @@ var ClusterMixin = {
else {
this._forceClustersByZoom();
}
},
},
/**
* This function handles the clustering by zooming out, this is based on a minimum edge distance
@ -451,7 +490,7 @@ var ClusterMixin = {
}
}
}
},
},
/**
* This function forces the graph to cluster all nodes with only one connecting edge to their
@ -482,8 +521,41 @@ var ClusterMixin = {
}
}
}
},
},
/**
* To keep the nodes of roughly equal size we normalize the cluster levels.
* This function clusters a node to its smallest connected neighbour.
*
* @param node
* @private
*/
_clusterToSmallestNeighbour : function(node) {
var smallestNeighbour = -1;
var smallestNeighbourNode = null;
for (var i = 0; i < node.dynamicEdges.length; i++) {
if (node.dynamicEdges[i] !== undefined) {
var neighbour = null;
if (node.dynamicEdges[i].fromId != node.id) {
neighbour = node.dynamicEdges[i].from;
}
else if (node.dynamicEdges[i].toId != node.id) {
neighbour = node.dynamicEdges[i].to;
}
if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) {
smallestNeighbour = neighbour.clusterSessions.length;
smallestNeighbourNode = neighbour;
}
}
}
if (neighbour != null && this.nodes[neighbour.id] !== undefined) {
this._addToCluster(neighbour, node, true);
}
},
/**
@ -501,7 +573,7 @@ var ClusterMixin = {
this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual);
}
}
},
},
/**
* This function forms a cluster from a specific preselected hub node
@ -571,7 +643,7 @@ var ClusterMixin = {
}
}
}
},
},
@ -610,9 +682,9 @@ var ClusterMixin = {
// update the properties of the child and parent
var massBefore = parentNode.mass;
childNode.clusterSession = this.clusterSession;
parentNode.mass += this.constants.clustering.massTransferCoefficient * childNode.mass;
parentNode.mass += childNode.mass;
parentNode.clusterSize += childNode.clusterSize;
parentNode.fontSize += this.constants.clustering.fontSizeMultiplier * childNode.clusterSize;
parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
// keep track of the clustersessions so we can open the cluster up as it has been formed.
if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) {
@ -642,12 +714,12 @@ var ClusterMixin = {
// restart the simulation to reorganise all nodes
this.moving = true;
},
},
/**
* This function will apply the changes made to the remainingEdges during the formation of the clusters.
* This is a seperate function to allow for level-wise collapsing of the node tree.
* This is a seperate function to allow for level-wise collapsing of the node barnesHutTree.
* It has to be called if a level is collapsed. It is called by _formClusters().
* @private
*/
@ -672,7 +744,7 @@ var ClusterMixin = {
}
node.dynamicEdgesLength -= correction;
}
},
},
/**
@ -701,7 +773,7 @@ var ClusterMixin = {
break;
}
}
},
},
/**
* This function connects an edge that was connected to a child node to the parent node.
@ -732,9 +804,17 @@ var ClusterMixin = {
this._addToReroutedEdges(parentNode,childNode,edge);
}
},
},
/**
* If a node is connected to itself, a circular edge is drawn. When clustering we want to contain
* these edges inside of the cluster.
*
* @param parentNode
* @param childNode
* @private
*/
_containCircularEdgesFromNode : function(parentNode, childNode) {
// manage all the edges connected to the child and parent nodes
for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
@ -805,7 +885,7 @@ var ClusterMixin = {
// remove the entry from the rerouted edges
delete parentNode.reroutedEdges[childNode.id];
}
},
},
/**
@ -823,7 +903,7 @@ var ClusterMixin = {
parentNode.dynamicEdges.splice(i,1);
}
}
},
},
/**
@ -848,7 +928,7 @@ var ClusterMixin = {
// remove the entry from the contained edges
delete parentNode.containedEdges[childNode.id];
},
},
@ -886,15 +966,57 @@ var ClusterMixin = {
}
}
/* Debug Override */
// for (nodeId in this.nodes) {
// if (this.nodes.hasOwnProperty(nodeId)) {
// node = this.nodes[nodeId];
// node.label = String(Math.round(node.width)).concat(":",Math.round(node.width*this.scale));
// }
// }
// /* Debug Override */
// for (nodeId in this.nodes) {
// if (this.nodes.hasOwnProperty(nodeId)) {
// node = this.nodes[nodeId];
// node.label = String(node.level);
// }
// }
},
/**
* We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes
* if the rest of the nodes are already a few cluster levels in.
* To fix this we use this function. It determines the min and max cluster level and sends nodes that have not
* clustered enough to the clusterToSmallestNeighbours function.
*/
normalizeClusterLevels : function() {
var maxLevel = 0;
var minLevel = 1e9;
var clusterLevel = 0;
// we loop over all nodes in the list
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
clusterLevel = this.nodes[nodeId].clusterSessions.length;
if (maxLevel < clusterLevel) {maxLevel = clusterLevel;}
if (minLevel > clusterLevel) {minLevel = clusterLevel;}
}
}
if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) {
var amountOfNodes = this.nodeIndices.length;
var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference;
// we loop over all nodes in the list
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
if (this.nodes[nodeId].clusterSessions.length < targetLevel) {
this._clusterToSmallestNeighbour(this.nodes[nodeId]);
}
}
}
this._updateNodeIndexList();
this._updateDynamicEdges();
// if a cluster was formed, we increase the clusterSession
if (this.nodeIndices.length != amountOfNodes) {
this.clusterSession += 1;
}
}
},
},
/**
@ -911,7 +1033,7 @@ var ClusterMixin = {
&&
Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
)
},
},
/**
@ -922,17 +1044,15 @@ var ClusterMixin = {
repositionNodes : function() {
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
if (!node.isFixed()) {
var radius = this.constants.edges.length * (1 + 0.6*node.clusterSize);
if ((node.xFixed == false || node.yFixed == false)) {
var radius = 10 * 0.1*this.nodeIndices.length * Math.min(100,node.mass);
var angle = 2 * Math.PI * Math.random();
node.x = radius * Math.cos(angle);
node.y = radius * Math.sin(angle);
if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
this._repositionBezierNodes(node);
}
}
},
},
/**
@ -948,6 +1068,7 @@ var ClusterMixin = {
var largestHub = 0;
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
if (node.dynamicEdgesLength > largestHub) {
largestHub = node.dynamicEdgesLength;
@ -972,7 +1093,7 @@ var ClusterMixin = {
// console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
// console.log("hubThreshold:",this.hubThreshold);
},
},
/**
@ -995,7 +1116,7 @@ var ClusterMixin = {
}
}
}
},
},
/**
* We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
@ -1015,5 +1136,6 @@ var ClusterMixin = {
}
}
return chains/total;
}
}
};

+ 311
- 0
src/graph/graphMixins/HierarchicalLayoutMixin.js View File

@ -0,0 +1,311 @@
var HierarchicalLayoutMixin = {
_resetLevels : function() {
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
var node = this.nodes[nodeId];
if (node.preassignedLevel == false) {
node.level = -1;
}
}
}
},
/**
* This is the main function to layout the nodes in a hierarchical way.
* It checks if the node details are supplied correctly
*
* @private
*/
_setupHierarchicalLayout : function() {
if (this.constants.hierarchicalLayout.enabled == true) {
if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") {
this.constants.hierarchicalLayout.levelSeparation *= -1;
}
else {
this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation);
}
// get the size of the largest hubs and check if the user has defined a level for a node.
var hubsize = 0;
var node, nodeId;
var definedLevel = false;
var undefinedLevel = false;
for (nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
if (node.level != -1) {
definedLevel = true;
}
else {
undefinedLevel = true;
}
if (hubsize < node.edges.length) {
hubsize = node.edges.length;
}
}
}
// if the user defined some levels but not all, alert and run without hierarchical layout
if (undefinedLevel == true && definedLevel == true) {
alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.");
this.zoomExtent(true,this.constants.clustering.enabled);
if (!this.constants.clustering.enabled) {
this.start();
}
}
else {
// setup the system to use hierarchical method.
this._changeConstants();
// define levels if undefined by the users. Based on hubsize
if (undefinedLevel == true) {
this._determineLevels(hubsize);
}
// check the distribution of the nodes per level.
var distribution = this._getDistribution();
// place the nodes on the canvas. This also stablilizes the system.
this._placeNodesByHierarchy(distribution);
// start the simulation.
this.start();
}
}
},
/**
* This function places the nodes on the canvas based on the hierarchial distribution.
*
* @param {Object} distribution | obtained by the function this._getDistribution()
* @private
*/
_placeNodesByHierarchy : function(distribution) {
var nodeId, node;
// start placing all the level 0 nodes first. Then recursively position their branches.
for (nodeId in distribution[0].nodes) {
if (distribution[0].nodes.hasOwnProperty(nodeId)) {
node = distribution[0].nodes[nodeId];
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
if (node.xFixed) {
node.x = distribution[0].minPos;
node.xFixed = false;
distribution[0].minPos += distribution[0].nodeSpacing;
}
}
else {
if (node.yFixed) {
node.y = distribution[0].minPos;
node.yFixed = false;
distribution[0].minPos += distribution[0].nodeSpacing;
}
}
this._placeBranchNodes(node.edges,node.id,distribution,node.level);
}
}
// stabilize the system after positioning. This function calls zoomExtent.
this._stabilize();
},
/**
* This function get the distribution of levels based on hubsize
*
* @returns {Object}
* @private
*/
_getDistribution : function() {
var distribution = {};
var nodeId, node;
// we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time.
// the fix of X is removed after the x value has been set.
for (nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
node.xFixed = true;
node.yFixed = true;
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
node.y = this.constants.hierarchicalLayout.levelSeparation*node.level;
}
else {
node.x = this.constants.hierarchicalLayout.levelSeparation*node.level;
}
if (!distribution.hasOwnProperty(node.level)) {
distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0};
}
distribution[node.level].amount += 1;
distribution[node.level].nodes[node.id] = node;
}
}
// determine the largest amount of nodes of all levels
var maxCount = 0;
for (var level in distribution) {
if (distribution.hasOwnProperty(level)) {
if (maxCount < distribution[level].amount) {
maxCount = distribution[level].amount;
}
}
}
// set the initial position and spacing of each nodes accordingly
for (var level in distribution) {
if (distribution.hasOwnProperty(level)) {
distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing;
distribution[level].nodeSpacing /= (distribution[level].amount + 1);
distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing);
}
}
return distribution;
},
/**
* this function allocates nodes in levels based on the recursive branching from the largest hubs.
*
* @param hubsize
* @private
*/
_determineLevels : function(hubsize) {
var nodeId, node;
// determine hubs
for (nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
if (node.edges.length == hubsize) {
node.level = 0;
}
}
}
// branch from hubs
for (nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
node = this.nodes[nodeId];
if (node.level == 0) {
this._setLevel(1,node.edges,node.id);
}
}
}
},
/**
* Since hierarchical layout does not support:
* - smooth curves (based on the physics),
* - clustering (based on dynamic node counts)
*
* We disable both features so there will be no problems.
*
* @private
*/
_changeConstants : function() {
this.constants.clustering.enabled = false;
this.constants.physics.barnesHut.enabled = false;
this.constants.physics.hierarchicalRepulsion.enabled = true;
this._loadSelectedForceSolver();
this.constants.smoothCurves = false;
this._configureSmoothCurves();
},
/**
* This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
* on a X position that ensures there will be no overlap.
*
* @param edges
* @param parentId
* @param distribution
* @param parentLevel
* @private
*/
_placeBranchNodes : function(edges, parentId, distribution, parentLevel) {
for (var i = 0; i < edges.length; i++) {
var childNode = null;
if (edges[i].toId == parentId) {
childNode = edges[i].from;
}
else {
childNode = edges[i].to;
}
// if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
var nodeMoved = false;
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
if (childNode.xFixed && childNode.level > parentLevel) {
childNode.xFixed = false;
childNode.x = distribution[childNode.level].minPos;
nodeMoved = true;
}
}
else {
if (childNode.yFixed && childNode.level > parentLevel) {
childNode.yFixed = false;
childNode.y = distribution[childNode.level].minPos;
nodeMoved = true;
}
}
if (nodeMoved == true) {
distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing;
if (childNode.edges.length > 1) {
this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level);
}
}
}
},
/**
* this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
*
* @param level
* @param edges
* @param parentId
* @private
*/
_setLevel : function(level, edges, parentId) {
for (var i = 0; i < edges.length; i++) {
var childNode = null;
if (edges[i].toId == parentId) {
childNode = edges[i].from;
}
else {
childNode = edges[i].to;
}
if (childNode.level == -1 || childNode.level > level) {
childNode.level = level;
if (edges.length > 1) {
this._setLevel(level+1, childNode.edges, childNode.id);
}
}
}
},
/**
* Unfix nodes
*
* @private
*/
_restoreNodes : function() {
for (nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
this.nodes[nodeId].xFixed = false;
this.nodes[nodeId].yFixed = false;
}
}
}
};

+ 435
- 0
src/graph/graphMixins/ManipulationMixin.js View File

@ -0,0 +1,435 @@
/**
* Created by Alex on 2/4/14.
*/
var manipulationMixin = {
/**
* clears the toolbar div element of children
*
* @private
*/
_clearManipulatorBar : function() {
while (this.manipulationDiv.hasChildNodes()) {
this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
}
},
/**
* Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore
* these functions to their original functionality, we saved them in this.cachedFunctions.
* This function restores these functions to their original function.
*
* @private
*/
_restoreOverloadedFunctions : function() {
for (var functionName in this.cachedFunctions) {
if (this.cachedFunctions.hasOwnProperty(functionName)) {
this[functionName] = this.cachedFunctions[functionName];
}
}
},
/**
* Enable or disable edit-mode.
*
* @private
*/
_toggleEditMode : function() {
this.editMode = !this.editMode;
var toolbar = document.getElementById("graph-manipulationDiv");
var closeDiv = document.getElementById("graph-manipulation-closeDiv");
var editModeDiv = document.getElementById("graph-manipulation-editMode");
if (this.editMode == true) {
toolbar.style.display="block";
closeDiv.style.display="block";
editModeDiv.style.display="none";
closeDiv.onclick = this._toggleEditMode.bind(this);
}
else {
toolbar.style.display="none";
closeDiv.style.display="none";
editModeDiv.style.display="block";
closeDiv.onclick = null;
}
this._createManipulatorBar()
},
/**
* main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar.
*
* @private
*/
_createManipulatorBar : function() {
// remove bound functions
if (this.boundFunction) {
this.off('select', this.boundFunction);
}
// restore overloaded functions
this._restoreOverloadedFunctions();
// resume calculation
this.freezeSimulation = false;
// reset global variables
this.blockConnectingEdgeSelection = false;
this.forceAppendSelection = false;
if (this.editMode == true) {
while (this.manipulationDiv.hasChildNodes()) {
this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
}
// add the icons to the manipulator div
this.manipulationDiv.innerHTML = "" +
"<span class='graph-manipulationUI add' id='graph-manipulate-addNode'>" +
"<span class='graph-manipulationLabel'>"+this.constants.labels['add'] +"</span></span>" +
"<div class='graph-seperatorLine'></div>" +
"<span class='graph-manipulationUI connect' id='graph-manipulate-connectNode'>" +
"<span class='graph-manipulationLabel'>"+this.constants.labels['link'] +"</span></span>";
if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
this.manipulationDiv.innerHTML += "" +
"<div class='graph-seperatorLine'></div>" +
"<span class='graph-manipulationUI edit' id='graph-manipulate-editNode'>" +
"<span class='graph-manipulationLabel'>"+this.constants.labels['editNode'] +"</span></span>";
}
if (this._selectionIsEmpty() == false) {
this.manipulationDiv.innerHTML += "" +
"<div class='graph-seperatorLine'></div>" +
"<span class='graph-manipulationUI delete' id='graph-manipulate-delete'>" +
"<span class='graph-manipulationLabel'>"+this.constants.labels['del'] +"</span></span>";
}
// bind the icons
var addNodeButton = document.getElementById("graph-manipulate-addNode");
addNodeButton.onclick = this._createAddNodeToolbar.bind(this);
var addEdgeButton = document.getElementById("graph-manipulate-connectNode");
addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this);
if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
var editButton = document.getElementById("graph-manipulate-editNode");
editButton.onclick = this._editNode.bind(this);
}
if (this._selectionIsEmpty() == false) {
var deleteButton = document.getElementById("graph-manipulate-delete");
deleteButton.onclick = this._deleteSelected.bind(this);
}
var closeDiv = document.getElementById("graph-manipulation-closeDiv");
closeDiv.onclick = this._toggleEditMode.bind(this);
this.boundFunction = this._createManipulatorBar.bind(this);
this.on('select', this.boundFunction);
}
else {
this.editModeDiv.innerHTML = "" +
"<span class='graph-manipulationUI edit editmode' id='graph-manipulate-editModeButton'>" +
"<span class='graph-manipulationLabel'>" + this.constants.labels['edit'] + "</span></span>";
var editModeButton = document.getElementById("graph-manipulate-editModeButton");
editModeButton.onclick = this._toggleEditMode.bind(this);
}
},
/**
* Create the toolbar for adding Nodes
*
* @private
*/
_createAddNodeToolbar : function() {
// clear the toolbar
this._clearManipulatorBar();
if (this.boundFunction) {
this.off('select', this.boundFunction);
}
// create the toolbar contents
this.manipulationDiv.innerHTML = "" +
"<span class='graph-manipulationUI back' id='graph-manipulate-back'>" +
"<span class='graph-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
"<div class='graph-seperatorLine'></div>" +
"<span class='graph-manipulationUI none' id='graph-manipulate-back'>" +
"<span id='graph-manipulatorLabel' class='graph-manipulationLabel'>" + this.constants.labels['addDescription'] + "</span></span>";
// bind the icon
var backButton = document.getElementById("graph-manipulate-back");
backButton.onclick = this._createManipulatorBar.bind(this);
// we use the boundFunction so we can reference it when we unbind it from the "select" event.
this.boundFunction = this._addNode.bind(this);
this.on('select', this.boundFunction);
},
/**
* create the toolbar to connect nodes
*
* @private
*/
_createAddEdgeToolbar : function() {
// clear the toolbar
this._clearManipulatorBar();
this._unselectAll(true);
this.freezeSimulation = true;
if (this.boundFunction) {
this.off('select', this.boundFunction);
}
this._unselectAll();
this.forceAppendSelection = false;
this.blockConnectingEdgeSelection = true;
this.manipulationDiv.innerHTML = "" +
"<span class='graph-manipulationUI back' id='graph-manipulate-back'>" +
"<span class='graph-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
"<div class='graph-seperatorLine'></div>" +
"<span class='graph-manipulationUI none' id='graph-manipulate-back'>" +
"<span id='graph-manipulatorLabel' class='graph-manipulationLabel'>" + this.constants.labels['linkDescription'] + "</span></span>";
// bind the icon
var backButton = document.getElementById("graph-manipulate-back");
backButton.onclick = this._createManipulatorBar.bind(this);
// we use the boundFunction so we can reference it when we unbind it from the "select" event.
this.boundFunction = this._handleConnect.bind(this);
this.on('select', this.boundFunction);
// temporarily overload functions
this.cachedFunctions["_handleTouch"] = this._handleTouch;
this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
this._handleTouch = this._handleConnect;
this._handleOnRelease = this._finishConnect;
// redraw to show the unselect
this._redraw();
},
/**
* the function bound to the selection event. It checks if you want to connect a cluster and changes the description
* to walk the user through the process.
*
* @private
*/
_handleConnect : function(pointer) {
if (this._getSelectedNodeCount() == 0) {
var node = this._getNodeAt(pointer);
if (node != null) {
if (node.clusterSize > 1) {
alert("Cannot create edges to a cluster.")
}
else {
this._selectObject(node,false);
// create a node the temporary line can look at
this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants);
this.sectors['support']['nodes']['targetNode'].x = node.x;
this.sectors['support']['nodes']['targetNode'].y = node.y;
this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants);
this.sectors['support']['nodes']['targetViaNode'].x = node.x;
this.sectors['support']['nodes']['targetViaNode'].y = node.y;
this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge";
// create a temporary edge
this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants);
this.edges['connectionEdge'].from = node;
this.edges['connectionEdge'].connected = true;
this.edges['connectionEdge'].smooth = true;
this.edges['connectionEdge'].selected = true;
this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode'];
this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode'];
this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
this._handleOnDrag = function(event) {
var pointer = this._getPointer(event.gesture.center);
this.sectors['support']['nodes']['targetNode'].x = this._canvasToX(pointer.x);
this.sectors['support']['nodes']['targetNode'].y = this._canvasToY(pointer.y);
this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._canvasToX(pointer.x) + this.edges['connectionEdge'].from.x);
this.sectors['support']['nodes']['targetViaNode'].y = this._canvasToY(pointer.y);
};
this.moving = true;
this.start();
}
}
}
},
_finishConnect : function(pointer) {
if (this._getSelectedNodeCount() == 1) {
// restore the drag function
this._handleOnDrag = this.cachedFunctions["_handleOnDrag"];
delete this.cachedFunctions["_handleOnDrag"];
// remember the edge id
var connectFromId = this.edges['connectionEdge'].fromId;
// remove the temporary nodes and edge
delete this.edges['connectionEdge'];
delete this.sectors['support']['nodes']['targetNode'];
delete this.sectors['support']['nodes']['targetViaNode'];
var node = this._getNodeAt(pointer);
if (node != null) {
if (node.clusterSize > 1) {
alert("Cannot create edges to a cluster.")
}
else {
this._createEdge(connectFromId,node.id);
this._createManipulatorBar();
}
}
this._unselectAll();
}
},
/**
* Adds a node on the specified location
*
* @param {Object} pointer
*/
_addNode : function() {
if (this._selectionIsEmpty() && this.editMode == true) {
var positionObject = this._pointerToPositionObject(this.pointerPosition);
var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true};
if (this.triggerFunctions.add) {
if (this.triggerFunctions.add.length == 2) {
var me = this;
this.triggerFunctions.add(defaultData, function(finalizedData) {
me.nodesData.add(finalizedData);
me._createManipulatorBar();
me.moving = true;
me.start();
});
}
else {
alert(this.constants.labels['addError']);
this._createManipulatorBar();
this.moving = true;
this.start();
}
}
else {
this.nodesData.add(defaultData);
this._createManipulatorBar();
this.moving = true;
this.start();
}
}
},
/**
* connect two nodes with a new edge.
*
* @private
*/
_createEdge : function(sourceNodeId,targetNodeId) {
if (this.editMode == true) {
var defaultData = {from:sourceNodeId, to:targetNodeId};
if (this.triggerFunctions.connect) {
if (this.triggerFunctions.connect.length == 2) {
var me = this;
this.triggerFunctions.connect(defaultData, function(finalizedData) {
me.edgesData.add(finalizedData);
me.moving = true;
me.start();
});
}
else {
alert(this.constants.labels["linkError"]);
this.moving = true;
this.start();
}
}
else {
this.edgesData.add(defaultData);
this.moving = true;
this.start();
}
}
},
/**
* Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
*
* @private
*/
_editNode : function() {
if (this.triggerFunctions.edit && this.editMode == true) {
var node = this._getSelectedNode();
var data = {id:node.id,
label: node.label,
group: node.group,
shape: node.shape,
color: {
background:node.color.background,
border:node.color.border,
highlight: {
background:node.color.highlight.background,
border:node.color.highlight.border
}
}};
if (this.triggerFunctions.edit.length == 2) {
var me = this;
this.triggerFunctions.edit(data, function (finalizedData) {
me.nodesData.update(finalizedData);
me._createManipulatorBar();
me.moving = true;
me.start();
});
}
else {
alert(this.constants.labels["editError"]);
}
}
else {
alert(this.constants.labels["editBoundError"]);
}
},
/**
* delete everything in the selection
*
* @private
*/
_deleteSelected : function() {
if (!this._selectionIsEmpty() && this.editMode == true) {
if (!this._clusterInSelection()) {
var selectedNodes = this.getSelectedNodes();
var selectedEdges = this.getSelectedEdges();
if (this.triggerFunctions.del) {
var me = this;
var data = {nodes: selectedNodes, edges: selectedEdges};
if (this.triggerFunctions.del.length = 2) {
this.triggerFunctions.del(data, function (finalizedData) {
me.edgesData.remove(finalizedData.edges);
me.nodesData.remove(finalizedData.nodes);
me._unselectAll();
me.moving = true;
me.start();
});
}
else {
alert(this.constants.labels["deleteError"])
}
}
else {
this.edgesData.remove(selectedEdges);
this.nodesData.remove(selectedNodes);
this._unselectAll();
this.moving = true;
this.start();
}
}
else {
alert(this.constants.labels["deleteClusterError"]);
}
}
}
};

+ 199
- 0
src/graph/graphMixins/MixinLoader.js View File

@ -0,0 +1,199 @@
/**
* Created by Alex on 2/10/14.
*/
var graphMixinLoaders = {
/**
* Load a mixin into the graph object
*
* @param {Object} sourceVariable | this object has to contain functions.
* @private
*/
_loadMixin: function (sourceVariable) {
for (var mixinFunction in sourceVariable) {
if (sourceVariable.hasOwnProperty(mixinFunction)) {
Graph.prototype[mixinFunction] = sourceVariable[mixinFunction];
}
}
},
/**
* removes a mixin from the graph object.
*
* @param {Object} sourceVariable | this object has to contain functions.
* @private
*/
_clearMixin: function (sourceVariable) {
for (var mixinFunction in sourceVariable) {
if (sourceVariable.hasOwnProperty(mixinFunction)) {
Graph.prototype[mixinFunction] = undefined;
}
}
},
/**
* Mixin the physics system and initialize the parameters required.
*
* @private
*/
_loadPhysicsSystem: function () {
this._loadMixin(physicsMixin);
this._loadSelectedForceSolver();
if (this.constants.configurePhysics == true) {
this._loadPhysicsConfiguration();
}
},
/**
* Mixin the cluster system and initialize the parameters required.
*
* @private
*/
_loadClusterSystem: function () {
this.clusterSession = 0;
this.hubThreshold = 5;
this._loadMixin(ClusterMixin);
},
/**
* Mixin the sector system and initialize the parameters required
*
* @private
*/
_loadSectorSystem: function () {
this.sectors = { },
this.activeSector = ["default"];
this.sectors["active"] = { },
this.sectors["active"]["default"] = {"nodes": {},
"edges": {},
"nodeIndices": [],
"formationScale": 1.0,
"drawingNode": undefined };
this.sectors["frozen"] = {},
this.sectors["support"] = {"nodes": {},
"edges": {},
"nodeIndices": [],
"formationScale": 1.0,
"drawingNode": undefined };
this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
this._loadMixin(SectorMixin);
},
/**
* Mixin the selection system and initialize the parameters required
*
* @private
*/
_loadSelectionSystem: function () {
this.selectionObj = {nodes: {}, edges: {}};
this._loadMixin(SelectionMixin);
},
/**
* Mixin the navigationUI (User Interface) system and initialize the parameters required
*
* @private
*/
_loadManipulationSystem: function () {
// reset global variables -- these are used by the selection of nodes and edges.
this.blockConnectingEdgeSelection = false;
this.forceAppendSelection = false
if (this.constants.dataManipulation.enabled == true) {
// load the manipulator HTML elements. All styling done in css.
if (this.manipulationDiv === undefined) {
this.manipulationDiv = document.createElement('div');
this.manipulationDiv.className = 'graph-manipulationDiv';
this.manipulationDiv.id = 'graph-manipulationDiv';
if (this.editMode == true) {
this.manipulationDiv.style.display = "block";
}
else {
this.manipulationDiv.style.display = "none";
}
this.containerElement.insertBefore(this.manipulationDiv, this.frame);
}
if (this.editModeDiv === undefined) {
this.editModeDiv = document.createElement('div');
this.editModeDiv.className = 'graph-manipulation-editMode';
this.editModeDiv.id = 'graph-manipulation-editMode';
if (this.editMode == true) {
this.editModeDiv.style.display = "none";
}
else {
this.editModeDiv.style.display = "block";
}
this.containerElement.insertBefore(this.editModeDiv, this.frame);
}
if (this.closeDiv === undefined) {
this.closeDiv = document.createElement('div');
this.closeDiv.className = 'graph-manipulation-closeDiv';
this.closeDiv.id = 'graph-manipulation-closeDiv';
this.closeDiv.style.display = this.manipulationDiv.style.display;
this.containerElement.insertBefore(this.closeDiv, this.frame);
}
// load the manipulation functions
this._loadMixin(manipulationMixin);
// create the manipulator toolbar
this._createManipulatorBar();
}
else {
if (this.manipulationDiv !== undefined) {
// removes all the bindings and overloads
this._createManipulatorBar();
// remove the manipulation divs
this.containerElement.removeChild(this.manipulationDiv);
this.containerElement.removeChild(this.editModeDiv);
this.containerElement.removeChild(this.closeDiv);
this.manipulationDiv = undefined;
this.editModeDiv = undefined;
this.closeDiv = undefined;
// remove the mixin functions
this._clearMixin(manipulationMixin);
}
}
},
/**
* Mixin the navigation (User Interface) system and initialize the parameters required
*
* @private
*/
_loadNavigationControls: function () {
this._loadMixin(NavigationMixin);
// the clean function removes the button divs, this is done to remove the bindings.
this._cleanNavigation();
if (this.constants.navigation.enabled == true) {
this._loadNavigationElements();
}
},
/**
* Mixin the hierarchical layout system.
*
* @private
*/
_loadHierarchySystem: function () {
this._loadMixin(HierarchicalLayoutMixin);
}
};

+ 205
- 0
src/graph/graphMixins/NavigationMixin.js View File

@ -0,0 +1,205 @@
/**
* Created by Alex on 1/22/14.
*/
var NavigationMixin = {
_cleanNavigation : function() {
// clean up previosu navigation items
var wrapper = document.getElementById('graph-navigation_wrapper');
if (wrapper != null) {
this.containerElement.removeChild(wrapper);
}
document.onmouseup = null;
},
/**
* 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() {
this._cleanNavigation();
this.navigationDivs = {};
var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends'];
var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent'];
this.navigationDivs['wrapper'] = document.createElement('div');
this.navigationDivs['wrapper'].id = "graph-navigation_wrapper";
this.navigationDivs['wrapper'].style.position = "absolute";
this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame);
for (var i = 0; i < navigationDivs.length; i++) {
this.navigationDivs[navigationDivs[i]] = document.createElement('div');
this.navigationDivs[navigationDivs[i]].id = "graph-navigation_" + navigationDivs[i];
this.navigationDivs[navigationDivs[i]].className = "graph-navigation " + navigationDivs[i];
this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]);
this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this);
}
document.onmouseup = this._stopMovement.bind(this);
},
/**
* this stops all movement induced by the navigation buttons
*
* @private
*/
_stopMovement : function() {
this._xStopMoving();
this._yStopMoving();
this._stopZoom();
},
/**
* stops the actions performed by page up and down etc.
*
* @param event
* @private
*/
_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.yIncrement = this.constants.keyboard.speed.y;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
if (this.navigationDivs) {
this.navigationDivs['up'].className += " active";
}
},
/**
* move the screen down
* @private
*/
_moveDown : function(event) {
this.yIncrement = -this.constants.keyboard.speed.y;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
if (this.navigationDivs) {
this.navigationDivs['down'].className += " active";
}
},
/**
* move the screen left
* @private
*/
_moveLeft : function(event) {
this.xIncrement = this.constants.keyboard.speed.x;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
if (this.navigationDivs) {
this.navigationDivs['left'].className += " active";
}
},
/**
* move the screen right
* @private
*/
_moveRight : function(event) {
this.xIncrement = -this.constants.keyboard.speed.y;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
if (this.navigationDivs) {
this.navigationDivs['right'].className += " active";
}
},
/**
* Zoom in, using the same method as the movement.
* @private
*/
_zoomIn : function(event) {
this.zoomIncrement = this.constants.keyboard.speed.zoom;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
if (this.navigationDivs) {
this.navigationDivs['zoomIn'].className += " active";
}
},
/**
* Zoom out
* @private
*/
_zoomOut : function() {
this.zoomIncrement = -this.constants.keyboard.speed.zoom;
this.start(); // if there is no node movement, the calculation wont be done
this._preventDefault(event);
if (this.navigationDivs) {
this.navigationDivs['zoomOut'].className += " active";
}
},
/**
* Stop zooming and unhighlight the zoom controls
* @private
*/
_stopZoom : function() {
this.zoomIncrement = 0;
if (this.navigationDivs) {
this.navigationDivs['zoomIn'].className = this.navigationDivs['zoomIn'].className.replace(" active","");
this.navigationDivs['zoomOut'].className = this.navigationDivs['zoomOut'].className.replace(" active","");
}
},
/**
* Stop moving in the Y direction and unHighlight the up and down
* @private
*/
_yStopMoving : function() {
this.yIncrement = 0;
if (this.navigationDivs) {
this.navigationDivs['up'].className = this.navigationDivs['up'].className.replace(" active","");
this.navigationDivs['down'].className = this.navigationDivs['down'].className.replace(" active","");
}
},
/**
* Stop moving in the X direction and unHighlight left and right.
* @private
*/
_xStopMoving : function() {
this.xIncrement = 0;
if (this.navigationDivs) {
this.navigationDivs['left'].className = this.navigationDivs['left'].className.replace(" active","");
this.navigationDivs['right'].className = this.navigationDivs['right'].className.replace(" active","");
}
}
};

src/graph/SectorsMixin.js → src/graph/graphMixins/SectorsMixin.js View File

@ -58,28 +58,29 @@ var SectorMixin = {
/**
* This function sets the global references to nodes, edges and nodeIndices back to
* those of the supplied frozen sector.
* those of the supplied active sector.
*
* @param sectorId
* @private
*/
_switchToFrozenSector : function(sectorId) {
this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"];
this.nodes = this.sectors["frozen"][sectorId]["nodes"];
this.edges = this.sectors["frozen"][sectorId]["edges"];
_switchToSupportSector : function() {
this.nodeIndices = this.sectors["support"]["nodeIndices"];
this.nodes = this.sectors["support"]["nodes"];
this.edges = this.sectors["support"]["edges"];
},
/**
* This function sets the global references to nodes, edges and nodeIndices to
* those of the navigation controls sector.
* This function sets the global references to nodes, edges and nodeIndices back to
* those of the supplied frozen sector.
*
* @param sectorId
* @private
*/
_switchToNavigationSector : function() {
this.nodeIndices = this.sectors["navigation"]["nodeIndices"];
this.nodes = this.sectors["navigation"]["nodes"];
this.edges = this.sectors["navigation"]["edges"];
_switchToFrozenSector : function(sectorId) {
this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"];
this.nodes = this.sectors["frozen"][sectorId]["nodes"];
this.edges = this.sectors["frozen"][sectorId]["edges"];
},
@ -349,6 +350,9 @@ var SectorMixin = {
// finally, we update the node index list.
this._updateNodeIndexList();
// we refresh the list with calulation nodes and calculation node indices.
this._updateCalculationNodes();
}
}
},
@ -393,6 +397,35 @@ var SectorMixin = {
},
/**
* This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
*
* @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
* | we dont pass the function itself because then the "this" is the window object
* | instead of the Graph object
* @param {*} [argument] | Optional: arguments to pass to the runFunction
* @private
*/
_doInSupportSector : function(runFunction,argument) {
if (argument === undefined) {
this._switchToSupportSector();
this[runFunction]();
}
else {
this._switchToSupportSector();
var args = Array.prototype.splice.call(arguments, 1);
if (args.length > 1) {
this[runFunction](args[0],args[1]);
}
else {
this[runFunction](argument);
}
}
// we revert the global references back to our active sector
this._loadLatestSector();
},
/**
* This runs a function in all frozen sectors. This is used in the _redraw().
*
@ -431,33 +464,6 @@ var SectorMixin = {
},
/**
* This runs a function in the navigation controls sector.
*
* @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
* | we don't pass the function itself because then the "this" is the window object
* | instead of the Graph object
* @param {*} [argument] | Optional: arguments to pass to the runFunction
* @private
*/
_doInNavigationSector : function(runFunction,argument) {
this._switchToNavigationSector();
if (argument === undefined) {
this[runFunction]();
}
else {
var args = Array.prototype.splice.call(arguments, 1);
if (args.length > 1) {
this[runFunction](args[0],args[1]);
}
else {
this[runFunction](argument);
}
}
this._loadLatestSector();
},
/**
* This runs a function in all sectors. This is used in the _redraw().
*
@ -483,7 +489,6 @@ var SectorMixin = {
this._doInAllFrozenSectors(runFunction,argument);
}
}
},

+ 570
- 0
src/graph/graphMixins/SelectionMixin.js View File

@ -0,0 +1,570 @@
var SelectionMixin = {
/**
* This function can be called from the _doInAllSectors function
*
* @param object
* @param overlappingNodes
* @private
*/
_getNodesOverlappingWith : function(object, overlappingNodes) {
var nodes = this.nodes;
for (var nodeId in nodes) {
if (nodes.hasOwnProperty(nodeId)) {
if (nodes[nodeId].isOverlappingWith(object)) {
overlappingNodes.push(nodeId);
}
}
}
},
/**
* retrieve all nodes overlapping with given object
* @param {Object} object An object with parameters left, top, right, bottom
* @return {Number[]} An array with id's of the overlapping nodes
* @private
*/
_getAllNodesOverlappingWith : function (object) {
var overlappingNodes = [];
this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes);
return overlappingNodes;
},
/**
* Return a position object in canvasspace from a single point in screenspace
*
* @param pointer
* @returns {{left: number, top: number, right: number, bottom: number}}
* @private
*/
_pointerToPositionObject : function(pointer) {
var x = this._canvasToX(pointer.x);
var y = this._canvasToY(pointer.y);
return {left: x,
top: y,
right: x,
bottom: y};
},
/**
* Get the top node at the a specific point (like a click)
*
* @param {{x: Number, y: Number}} pointer
* @return {Node | null} node
* @private
*/
_getNodeAt : function (pointer) {
// we first check if this is an navigation controls element
var positionObject = this._pointerToPositionObject(pointer);
var overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
// if there are overlapping nodes, select the last one, this is the
// one which is drawn on top of the others
if (overlappingNodes.length > 0) {
return this.nodes[overlappingNodes[overlappingNodes.length - 1]];
}
else {
return null;
}
},
/**
* retrieve all edges overlapping with given object, selector is around center
* @param {Object} object An object with parameters left, top, right, bottom
* @return {Number[]} An array with id's of the overlapping nodes
* @private
*/
_getEdgesOverlappingWith : function (object, overlappingEdges) {
var edges = this.edges;
for (var edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
if (edges[edgeId].isOverlappingWith(object)) {
overlappingEdges.push(edgeId);
}
}
}
},
/**
* retrieve all nodes overlapping with given object
* @param {Object} object An object with parameters left, top, right, bottom
* @return {Number[]} An array with id's of the overlapping nodes
* @private
*/
_getAllEdgesOverlappingWith : function (object) {
var overlappingEdges = [];
this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges);
return overlappingEdges;
},
/**
* Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call
* _getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
*
* @param pointer
* @returns {null}
* @private
*/
_getEdgeAt : function(pointer) {
var positionObject = this._pointerToPositionObject(pointer);
var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject);
if (overlappingEdges.length > 0) {
return this.edges[overlappingEdges[overlappingEdges.length - 1]];
}
else {
return null;
}
},
/**
* Add object to the selection array.
*
* @param obj
* @private
*/
_addToSelection : function(obj) {
if (obj instanceof Node) {
this.selectionObj.nodes[obj.id] = obj;
}
else {
this.selectionObj.edges[obj.id] = obj;
}
},
/**
* Remove a single option from selection.
*
* @param {Object} obj
* @private
*/
_removeFromSelection : function(obj) {
if (obj instanceof Node) {
delete this.selectionObj.nodes[obj.id];
}
else {
delete this.selectionObj.edges[obj.id];
}
},
/**
* Unselect all. The selectionObj is useful for this.
*
* @param {Boolean} [doNotTrigger] | ignore trigger
* @private
*/
_unselectAll : function(doNotTrigger) {
if (doNotTrigger === undefined) {
doNotTrigger = false;
}
for(var nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
this.selectionObj.nodes[nodeId].unselect();
}
}
for(var edgeId in this.selectionObj.edges) {
if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
this.selectionObj.edges[edgeId].unselect();;
}
}
this.selectionObj = {nodes:{},edges:{}};
if (doNotTrigger == false) {
this.emit('select', this.getSelection());
}
},
/**
* Unselect all clusters. The selectionObj is useful for this.
*
* @param {Boolean} [doNotTrigger] | ignore trigger
* @private
*/
_unselectClusters : function(doNotTrigger) {
if (doNotTrigger === undefined) {
doNotTrigger = false;
}
for (var nodeId in this.selectionObj.nodes) {
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
this.selectionObj.nodes[nodeId].unselect();
this._removeFromSelection(this.selectionObj.nodes[nodeId]);
}
}
}
if (doNotTrigger == false) {
this.emit('select', this.getSelection());
}
},
/**
* return the number of selected nodes
*
* @returns {number}
* @private
*/
_getSelectedNodeCount : function() {
var count = 0;
for (var nodeId in this.selectionObj.nodes) {
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
count += 1;
}
}
return count;
},
/**
* return the number of selected nodes
*
* @returns {number}
* @private
*/
_getSelectedNode : function() {
for (var nodeId in this.selectionObj.nodes) {
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
return this.selectionObj.nodes[nodeId];
}
}
return null;
},
/**
* return the number of selected edges
*
* @returns {number}
* @private
*/
_getSelectedEdgeCount : function() {
var count = 0;
for (var edgeId in this.selectionObj.edges) {
if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
count += 1;
}
}
return count;
},
/**
* return the number of selected objects.
*
* @returns {number}
* @private
*/
_getSelectedObjectCount : function() {
var count = 0;
for(var nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
count += 1;
}
}
for(var edgeId in this.selectionObj.edges) {
if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
count += 1;
}
}
return count;
},
/**
* Check if anything is selected
*
* @returns {boolean}
* @private
*/
_selectionIsEmpty : function() {
for(var nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
return false;
}
}
for(var edgeId in this.selectionObj.edges) {
if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
return false;
}
}
return true;
},
/**
* check if one of the selected nodes is a cluster.
*
* @returns {boolean}
* @private
*/
_clusterInSelection : function() {
for(var nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
return true;
}
}
}
return false;
},
/**
* select the edges connected to the node that is being selected
*
* @param {Node} node
* @private
*/
_selectConnectedEdges : function(node) {
for (var i = 0; i < node.dynamicEdges.length; i++) {
var edge = node.dynamicEdges[i];
edge.select();
this._addToSelection(edge);
}
},
/**
* unselect the edges connected to the node that is being selected
*
* @param {Node} node
* @private
*/
_unselectConnectedEdges : function(node) {
for (var i = 0; i < node.dynamicEdges.length; i++) {
var edge = node.dynamicEdges[i];
edge.unselect();
this._removeFromSelection(edge);
}
},
/**
* This is called when someone clicks on a node. either select or deselect it.
* If there is an existing selection and we don't want to append to it, clear the existing selection
*
* @param {Node || Edge} object
* @param {Boolean} append
* @param {Boolean} [doNotTrigger] | ignore trigger
* @private
*/
_selectObject : function(object, append, doNotTrigger) {
if (doNotTrigger === undefined) {
doNotTrigger = false;
}
if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) {
this._unselectAll(true);
}
if (object.selected == false) {
object.select();
this._addToSelection(object);
if (object instanceof Node && this.blockConnectingEdgeSelection == false) {
this._selectConnectedEdges(object);
}
}
else {
object.unselect();
this._removeFromSelection(object);
}
if (doNotTrigger == false) {
this.emit('select', this.getSelection());
}
},
/**
* handles the selection part of the touch, only for navigation controls elements;
* Touch is triggered before tap, also before hold. Hold triggers after a while.
* This is the most responsive solution
*
* @param {Object} pointer
* @private
*/
_handleTouch : function(pointer) {
},
/**
* handles the selection part of the tap;
*
* @param {Object} pointer
* @private
*/
_handleTap : function(pointer) {
var node = this._getNodeAt(pointer);
if (node != null) {
this._selectObject(node,false);
}
else {
var edge = this._getEdgeAt(pointer);
if (edge != null) {
this._selectObject(edge,false);
}
else {
this._unselectAll();
}
}
this.emit("click", this.getSelection());
this._redraw();
},
/**
* handles the selection part of the double tap and opens a cluster if needed
*
* @param {Object} pointer
* @private
*/
_handleDoubleTap : function(pointer) {
var node = this._getNodeAt(pointer);
if (node != null && node !== undefined) {
// we reset the areaCenter here so the opening of the node will occur
this.areaCenter = {"x" : this._canvasToX(pointer.x),
"y" : this._canvasToY(pointer.y)};
this.openCluster(node);
}
this.emit("doubleClick", this.getSelection());
},
/**
* Handle the onHold selection part
*
* @param pointer
* @private
*/
_handleOnHold : function(pointer) {
var node = this._getNodeAt(pointer);
if (node != null) {
this._selectObject(node,true);
}
else {
var edge = this._getEdgeAt(pointer);
if (edge != null) {
this._selectObject(edge,true);
}
}
this._redraw();
},
/**
* handle the onRelease event. These functions are here for the navigation controls module.
*
* @private
*/
_handleOnRelease : function(pointer) {
},
/**
*
* retrieve the currently selected objects
* @return {Number[] | String[]} selection An array with the ids of the
* selected nodes.
*/
getSelection : function() {
var nodeIds = this.getSelectedNodes();
var edgeIds = this.getSelectedEdges();
return {nodes:nodeIds, edges:edgeIds};
},
/**
*
* retrieve the currently selected nodes
* @return {String} selection An array with the ids of the
* selected nodes.
*/
getSelectedNodes : function() {
var idArray = [];
for(var nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
idArray.push(nodeId);
}
}
return idArray
},
/**
*
* retrieve the currently selected edges
* @return {Array} selection An array with the ids of the
* selected nodes.
*/
getSelectedEdges : function() {
var idArray = [];
for(var edgeId in this.selectionObj.edges) {
if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
idArray.push(edgeId);
}
}
return idArray;
},
/**
* select zero or more nodes
* @param {Number[] | String[]} selection An array with the ids of the
* selected nodes.
*/
setSelection : function(selection) {
var i, iMax, id;
if (!selection || (selection.length == undefined))
throw 'Selection must be an array with ids';
// first unselect any selected node
this._unselectAll(true);
for (i = 0, iMax = selection.length; i < iMax; i++) {
id = selection[i];
var node = this.nodes[id];
if (!node) {
throw new RangeError('Node with id "' + id + '" not found');
}
this._selectObject(node,true,true);
}
this.redraw();
},
/**
* Validate the selection: remove ids of nodes which no longer exist
* @private
*/
_updateSelection : function () {
for(var nodeId in this.selectionObj.nodes) {
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
if (!this.nodes.hasOwnProperty(nodeId)) {
delete this.selectionObj.nodes[nodeId];
}
}
}
for(var edgeId in this.selectionObj.edges) {
if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
if (!this.edges.hasOwnProperty(edgeId)) {
delete this.selectionObj.edges[edgeId];
}
}
}
}
};

+ 375
- 0
src/graph/graphMixins/physics/BarnesHut.js View File

@ -0,0 +1,375 @@
/**
* Created by Alex on 2/10/14.
*/
var barnesHutMixin = {
/**
* This function calculates the forces the nodes apply on eachother based on a gravitational model.
* The Barnes Hut method is used to speed up this N-body simulation.
*
* @private
*/
_calculateNodeForces : function() {
if (this.constants.physics.barnesHut.gravitationalConstant != 0) {
var node;
var nodes = this.calculationNodes;
var nodeIndices = this.calculationNodeIndices;
var nodeCount = nodeIndices.length;
this._formBarnesHutTree(nodes,nodeIndices);
var barnesHutTree = this.barnesHutTree;
// place the nodes one by one recursively
for (var i = 0; i < nodeCount; i++) {
node = nodes[nodeIndices[i]];
// starting with root is irrelevant, it never passes the BarnesHut condition
this._getForceContribution(barnesHutTree.root.children.NW,node);
this._getForceContribution(barnesHutTree.root.children.NE,node);
this._getForceContribution(barnesHutTree.root.children.SW,node);
this._getForceContribution(barnesHutTree.root.children.SE,node);
}
}
},
/**
* This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
* If a region contains a single node, we check if it is not itself, then we apply the force.
*
* @param parentBranch
* @param node
* @private
*/
_getForceContribution : function(parentBranch,node) {
// we get no force contribution from an empty region
if (parentBranch.childrenCount > 0) {
var dx,dy,distance;
// get the distance from the center of mass to the node.
dx = parentBranch.centerOfMass.x - node.x;
dy = parentBranch.centerOfMass.y - node.y;
distance = Math.sqrt(dx * dx + dy * dy);
// BarnesHut condition
// original condition : s/d < theta = passed === d/s > 1/theta = passed
// calcSize = 1/s --> d * 1/s > 1/theta = passed
if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.theta) {
// duplicate code to reduce function calls to speed up program
if (distance == 0) {
distance = 0.1*Math.random();
dx = distance;
}
var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
var fx = dx * gravityForce;
var fy = dy * gravityForce;
node.fx += fx;
node.fy += fy;
}
else {
// Did not pass the condition, go into children if available
if (parentBranch.childrenCount == 4) {
this._getForceContribution(parentBranch.children.NW,node);
this._getForceContribution(parentBranch.children.NE,node);
this._getForceContribution(parentBranch.children.SW,node);
this._getForceContribution(parentBranch.children.SE,node);
}
else { // parentBranch must have only one node, if it was empty we wouldnt be here
if (parentBranch.children.data.id != node.id) { // if it is not self
// duplicate code to reduce function calls to speed up program
if (distance == 0) {
distance = 0.5*Math.random();
dx = distance;
}
var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
var fx = dx * gravityForce;
var fy = dy * gravityForce;
node.fx += fx;
node.fy += fy;
}
}
}
}
},
/**
* This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
*
* @param nodes
* @param nodeIndices
* @private
*/
_formBarnesHutTree : function(nodes,nodeIndices) {
var node;
var nodeCount = nodeIndices.length;
var minX = Number.MAX_VALUE,
minY = Number.MAX_VALUE,
maxX =-Number.MAX_VALUE,
maxY =-Number.MAX_VALUE;
// get the range of the nodes
for (var i = 0; i < nodeCount; i++) {
var x = nodes[nodeIndices[i]].x;
var y = nodes[nodeIndices[i]].y;
if (x < minX) { minX = x; }
if (x > maxX) { maxX = x; }
if (y < minY) { minY = y; }
if (y > maxY) { maxY = y; }
}
// make the range a square
var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y
if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize
else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize
var minimumTreeSize = 1e-5;
var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX));
var halfRootSize = 0.5 * rootSize;
var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY);
// construct the barnesHutTree
var barnesHutTree = {root:{
centerOfMass:{x:0,y:0}, // Center of Mass
mass:0,
range: {minX:centerX-halfRootSize,maxX:centerX+halfRootSize,
minY:centerY-halfRootSize,maxY:centerY+halfRootSize},
size: rootSize,
calcSize: 1 / rootSize,
children: {data:null},
maxWidth: 0,
level: 0,
childrenCount: 4
}};
this._splitBranch(barnesHutTree.root);
// place the nodes one by one recursively
for (i = 0; i < nodeCount; i++) {
node = nodes[nodeIndices[i]];
this._placeInTree(barnesHutTree.root,node);
}
// make global
this.barnesHutTree = barnesHutTree
},
_updateBranchMass : function(parentBranch, node) {
var totalMass = parentBranch.mass + node.mass;
var totalMassInv = 1/totalMass;
parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.mass;
parentBranch.centerOfMass.x *= totalMassInv;
parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.mass;
parentBranch.centerOfMass.y *= totalMassInv;
parentBranch.mass = totalMass;
var biggestSize = Math.max(Math.max(node.height,node.radius),node.width);
parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth;
},
_placeInTree : function(parentBranch,node,skipMassUpdate) {
if (skipMassUpdate != true || skipMassUpdate === undefined) {
// update the mass of the branch.
this._updateBranchMass(parentBranch,node);
}
if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW
if (parentBranch.children.NW.range.maxY > node.y) { // in NW
this._placeInRegion(parentBranch,node,"NW");
}
else { // in SW
this._placeInRegion(parentBranch,node,"SW");
}
}
else { // in NE or SE
if (parentBranch.children.NW.range.maxY > node.y) { // in NE
this._placeInRegion(parentBranch,node,"NE");
}
else { // in SE
this._placeInRegion(parentBranch,node,"SE");
}
}
},
_placeInRegion : function(parentBranch,node,region) {
switch (parentBranch.children[region].childrenCount) {
case 0: // place node here
parentBranch.children[region].children.data = node;
parentBranch.children[region].childrenCount = 1;
this._updateBranchMass(parentBranch.children[region],node);
break;
case 1: // convert into children
// if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
// we move one node a pixel and we do not put it in the tree.
if (parentBranch.children[region].children.data.x == node.x &&
parentBranch.children[region].children.data.y == node.y) {
node.x += Math.random();
node.y += Math.random();
}
else {
this._splitBranch(parentBranch.children[region]);
this._placeInTree(parentBranch.children[region],node);
}
break;
case 4: // place in branch
this._placeInTree(parentBranch.children[region],node);
break;
}
},
/**
* this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch
* after the split is complete.
*
* @param parentBranch
* @private
*/
_splitBranch : function(parentBranch) {
// if the branch is filled with a node, replace the node in the new subset.
var containedNode = null;
if (parentBranch.childrenCount == 1) {
containedNode = parentBranch.children.data;
parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0;
}
parentBranch.childrenCount = 4;
parentBranch.children.data = null;
this._insertRegion(parentBranch,"NW");
this._insertRegion(parentBranch,"NE");
this._insertRegion(parentBranch,"SW");
this._insertRegion(parentBranch,"SE");
if (containedNode != null) {
this._placeInTree(parentBranch,containedNode);
}
},
/**
* This function subdivides the region into four new segments.
* Specifically, this inserts a single new segment.
* It fills the children section of the parentBranch
*
* @param parentBranch
* @param region
* @param parentRange
* @private
*/
_insertRegion : function(parentBranch, region) {
var minX,maxX,minY,maxY;
var childSize = 0.5 * parentBranch.size;
switch (region) {
case "NW":
minX = parentBranch.range.minX;
maxX = parentBranch.range.minX + childSize;
minY = parentBranch.range.minY;
maxY = parentBranch.range.minY + childSize;
break;
case "NE":
minX = parentBranch.range.minX + childSize;
maxX = parentBranch.range.maxX;
minY = parentBranch.range.minY;
maxY = parentBranch.range.minY + childSize;
break;
case "SW":
minX = parentBranch.range.minX;
maxX = parentBranch.range.minX + childSize;
minY = parentBranch.range.minY + childSize;
maxY = parentBranch.range.maxY;
break;
case "SE":
minX = parentBranch.range.minX + childSize;
maxX = parentBranch.range.maxX;
minY = parentBranch.range.minY + childSize;
maxY = parentBranch.range.maxY;
break;
}
parentBranch.children[region] = {
centerOfMass:{x:0,y:0},
mass:0,
range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
size: 0.5 * parentBranch.size,
calcSize: 2 * parentBranch.calcSize,
children: {data:null},
maxWidth: 0,
level: parentBranch.level+1,
childrenCount: 0
};
},
/**
* This function is for debugging purposed, it draws the tree.
*
* @param ctx
* @param color
* @private
*/
_drawTree : function(ctx,color) {
if (this.barnesHutTree !== undefined) {
ctx.lineWidth = 1;
this._drawBranch(this.barnesHutTree.root,ctx,color);
}
},
/**
* This function is for debugging purposes. It draws the branches recursively.
*
* @param branch
* @param ctx
* @param color
* @private
*/
_drawBranch : function(branch,ctx,color) {
if (color === undefined) {
color = "#FF0000";
}
if (branch.childrenCount == 4) {
this._drawBranch(branch.children.NW,ctx);
this._drawBranch(branch.children.NE,ctx);
this._drawBranch(branch.children.SE,ctx);
this._drawBranch(branch.children.SW,ctx);
}
ctx.strokeStyle = color;
ctx.beginPath();
ctx.moveTo(branch.range.minX,branch.range.minY);
ctx.lineTo(branch.range.maxX,branch.range.minY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(branch.range.maxX,branch.range.minY);
ctx.lineTo(branch.range.maxX,branch.range.maxY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(branch.range.maxX,branch.range.maxY);
ctx.lineTo(branch.range.minX,branch.range.maxY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(branch.range.minX,branch.range.maxY);
ctx.lineTo(branch.range.minX,branch.range.minY);
ctx.stroke();
/*
if (branch.mass > 0) {
ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
ctx.stroke();
}
*/
}
};

+ 64
- 0
src/graph/graphMixins/physics/HierarchialRepulsion.js View File

@ -0,0 +1,64 @@
/**
* Created by Alex on 2/10/14.
*/
var hierarchalRepulsionMixin = {
/**
* Calculate the forces the nodes apply on eachother based on a repulsion field.
* This field is linearly approximated.
*
* @private
*/
_calculateNodeForces: function () {
var dx, dy, distance, fx, fy, combinedClusterSize,
repulsingForce, node1, node2, i, j;
var nodes = this.calculationNodes;
var nodeIndices = this.calculationNodeIndices;
// approximation constants
var b = 5;
var a_base = 0.5 * -b;
// repulsing forces between nodes
var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance;
var minimumDistance = nodeDistance;
// we loop from i over all but the last entree in the array
// j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
for (i = 0; i < nodeIndices.length - 1; i++) {
node1 = nodes[nodeIndices[i]];
for (j = i + 1; j < nodeIndices.length; j++) {
node2 = nodes[nodeIndices[j]];
dx = node2.x - node1.x;
dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy);
var a = a_base / minimumDistance;
if (distance < 2 * minimumDistance) {
repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
// normalize force with
if (distance == 0) {
distance = 0.01;
}
else {
repulsingForce = repulsingForce / distance;
}
fx = dx * repulsingForce;
fy = dy * repulsingForce;
node1.fx -= fx;
node1.fy -= fy;
node2.fx += fx;
node2.fy += fy;
}
}
}
}
};

+ 665
- 0
src/graph/graphMixins/physics/PhysicsMixin.js View File

@ -0,0 +1,665 @@
/**
* Created by Alex on 2/6/14.
*/
var physicsMixin = {
/**
* Toggling barnes Hut calculation on and off.
*
* @private
*/
_toggleBarnesHut: function () {
this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled;
this._loadSelectedForceSolver();
this.moving = true;
this.start();
},
/**
* This loads the node force solver based on the barnes hut or repulsion algorithm
*
* @private
*/
_loadSelectedForceSolver: function () {
// this overloads the this._calculateNodeForces
if (this.constants.physics.barnesHut.enabled == true) {
this._clearMixin(repulsionMixin);
this._clearMixin(hierarchalRepulsionMixin);
this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity;
this.constants.physics.springLength = this.constants.physics.barnesHut.springLength;
this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant;
this.constants.physics.damping = this.constants.physics.barnesHut.damping;
this._loadMixin(barnesHutMixin);
}
else if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
this._clearMixin(barnesHutMixin);
this._clearMixin(repulsionMixin);
this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity;
this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength;
this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant;
this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping;
this._loadMixin(hierarchalRepulsionMixin);
}
else {
this._clearMixin(barnesHutMixin);
this._clearMixin(hierarchalRepulsionMixin);
this.barnesHutTree = undefined;
this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity;
this.constants.physics.springLength = this.constants.physics.repulsion.springLength;
this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant;
this.constants.physics.damping = this.constants.physics.repulsion.damping;
this._loadMixin(repulsionMixin);
}
},
/**
* Before calculating the forces, we check if we need to cluster to keep up performance and we check
* if there is more than one node. If it is just one node, we dont calculate anything.
*
* @private
*/
_initializeForceCalculation: function () {
// stop calculation if there is only one node
if (this.nodeIndices.length == 1) {
this.nodes[this.nodeIndices[0]]._setForce(0, 0);
}
else {
// if there are too many nodes on screen, we cluster without repositioning
if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) {
this.clusterToFit(this.constants.clustering.reduceToNodes, false);
}
// we now start the force calculation
this._calculateForces();
}
},
/**
* Calculate the external forces acting on the nodes
* Forces are caused by: edges, repulsing forces between nodes, gravity
* @private
*/
_calculateForces: function () {
// Gravity is required to keep separated groups from floating off
// the forces are reset to zero in this loop by using _setForce instead
// of _addForce
this._calculateGravitationalForces();
this._calculateNodeForces();
if (this.constants.smoothCurves == true) {
this._calculateSpringForcesWithSupport();
}
else {
this._calculateSpringForces();
}
},
/**
* Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also
* handled in the calculateForces function. We then use a quadratic curve with the center node as control.
* This function joins the datanodes and invisible (called support) nodes into one object.
* We do this so we do not contaminate this.nodes with the support nodes.
*
* @private
*/
_updateCalculationNodes: function () {
if (this.constants.smoothCurves == true) {
this.calculationNodes = {};
this.calculationNodeIndices = [];
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
this.calculationNodes[nodeId] = this.nodes[nodeId];
}
}
var supportNodes = this.sectors['support']['nodes'];
for (var supportNodeId in supportNodes) {
if (supportNodes.hasOwnProperty(supportNodeId)) {
if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) {
this.calculationNodes[supportNodeId] = supportNodes[supportNodeId];
}
else {
supportNodes[supportNodeId]._setForce(0, 0);
}
}
}
for (var idx in this.calculationNodes) {
if (this.calculationNodes.hasOwnProperty(idx)) {
this.calculationNodeIndices.push(idx);
}
}
}
else {
this.calculationNodes = this.nodes;
this.calculationNodeIndices = this.nodeIndices;
}
},
/**
* this function applies the central gravity effect to keep groups from floating off
*
* @private
*/
_calculateGravitationalForces: function () {
var dx, dy, distance, node, i;
var nodes = this.calculationNodes;
var gravity = this.constants.physics.centralGravity;
var gravityForce = 0;
for (i = 0; i < this.calculationNodeIndices.length; i++) {
node = nodes[this.calculationNodeIndices[i]];
node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters.
// gravity does not apply when we are in a pocket sector
if (this._sector() == "default" && gravity != 0) {
dx = -node.x;
dy = -node.y;
distance = Math.sqrt(dx * dx + dy * dy);
gravityForce = (distance == 0) ? 0 : (gravity / distance);
node.fx = dx * gravityForce;
node.fy = dy * gravityForce;
}
else {
node.fx = 0;
node.fy = 0;
}
}
},
/**
* this function calculates the effects of the springs in the case of unsmooth curves.
*
* @private
*/
_calculateSpringForces: function () {
var edgeLength, edge, edgeId;
var dx, dy, fx, fy, springForce, length;
var edges = this.edges;
// forces caused by the edges, modelled as springs
for (edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
edge = edges[edgeId];
if (edge.connected) {
// only calculate forces if nodes are in the same sector
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
// this implies that the edges between big clusters are longer
edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
dx = (edge.from.x - edge.to.x);
dy = (edge.from.y - edge.to.y);
length = Math.sqrt(dx * dx + dy * dy);
if (length == 0) {
length = 0.01;
}
springForce = this.constants.physics.springConstant * (edgeLength - length) / length;
fx = dx * springForce;
fy = dy * springForce;
edge.from.fx += fx;
edge.from.fy += fy;
edge.to.fx -= fx;
edge.to.fy -= fy;
}
}
}
}
},
/**
* This function calculates the springforces on the nodes, accounting for the support nodes.
*
* @private
*/
_calculateSpringForcesWithSupport: function () {
var edgeLength, edge, edgeId, combinedClusterSize;
var edges = this.edges;
// forces caused by the edges, modelled as springs
for (edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
edge = edges[edgeId];
if (edge.connected) {
// only calculate forces if nodes are in the same sector
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
if (edge.via != null) {
var node1 = edge.to;
var node2 = edge.via;
var node3 = edge.from;
edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
combinedClusterSize = node1.clusterSize + node3.clusterSize - 2;
// this implies that the edges between big clusters are longer
edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth;
this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
}
}
}
}
}
},
/**
* This is the code actually performing the calculation for the function above. It is split out to avoid repetition.
*
* @param node1
* @param node2
* @param edgeLength
* @private
*/
_calculateSpringForce: function (node1, node2, edgeLength) {
var dx, dy, fx, fy, springForce, length;
dx = (node1.x - node2.x);
dy = (node1.y - node2.y);
length = Math.sqrt(dx * dx + dy * dy);
if (length == 0) {
length = 0.01;
}
springForce = this.constants.physics.springConstant * (edgeLength - length) / length;
fx = dx * springForce;
fy = dy * springForce;
node1.fx += fx;
node1.fy += fy;
node2.fx -= fx;
node2.fy -= fy;
},
/**
* Load the HTML for the physics config and bind it
* @private
*/
_loadPhysicsConfiguration: function () {
if (this.physicsConfiguration === undefined) {
this.backupConstants = {};
util.copyObject(this.constants, this.backupConstants);
var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"];
this.physicsConfiguration = document.createElement('div');
this.physicsConfiguration.className = "PhysicsConfiguration";
this.physicsConfiguration.innerHTML = '' +
'<table><tr><td><b>Simulation Mode:</b></td></tr>' +
'<tr>' +
'<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod1" value="BH" checked="checked">Barnes Hut</td>' +
'<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod2" value="R">Repulsion</td>' +
'<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod3" value="H">Hierarchical</td>' +
'</tr>' +
'</table>' +
'<table id="graph_BH_table" style="display:none">' +
'<tr><td><b>Barnes Hut</b></td></tr>' +
'<tr>' +
'<td width="150px">gravitationalConstant</td><td>0</td><td><input type="range" min="500" max="20000" value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" step="25" style="width:300px" id="graph_BH_gc"></td><td width="50px">-20000</td><td><input value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" id="graph_BH_gc_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.barnesHut.centralGravity + '" step="0.05" style="width:300px" id="graph_BH_cg"></td><td>3</td><td><input value="' + this.constants.physics.barnesHut.centralGravity + '" id="graph_BH_cg_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.barnesHut.springLength + '" step="1" style="width:300px" id="graph_BH_sl"></td><td>500</td><td><input value="' + this.constants.physics.barnesHut.springLength + '" id="graph_BH_sl_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.barnesHut.springConstant + '" step="0.001" style="width:300px" id="graph_BH_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.barnesHut.springConstant + '" id="graph_BH_sc_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.barnesHut.damping + '" step="0.005" style="width:300px" id="graph_BH_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.barnesHut.damping + '" id="graph_BH_damp_value" style="width:60px"></td>' +
'</tr>' +
'</table>' +
'<table id="graph_R_table" style="display:none">' +
'<tr><td><b>Repulsion</b></td></tr>' +
'<tr>' +
'<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.repulsion.nodeDistance + '" step="1" style="width:300px" id="graph_R_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.repulsion.nodeDistance + '" id="graph_R_nd_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.repulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_R_cg"></td><td>3</td><td><input value="' + this.constants.physics.repulsion.centralGravity + '" id="graph_R_cg_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.repulsion.springLength + '" step="1" style="width:300px" id="graph_R_sl"></td><td>500</td><td><input value="' + this.constants.physics.repulsion.springLength + '" id="graph_R_sl_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.repulsion.springConstant + '" step="0.001" style="width:300px" id="graph_R_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.repulsion.springConstant + '" id="graph_R_sc_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.repulsion.damping + '" step="0.005" style="width:300px" id="graph_R_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.repulsion.damping + '" id="graph_R_damp_value" style="width:60px"></td>' +
'</tr>' +
'</table>' +
'<table id="graph_H_table" style="display:none">' +
'<tr><td width="150"><b>Hierarchical</b></td></tr>' +
'<tr>' +
'<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" step="1" style="width:300px" id="graph_H_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" id="graph_H_nd_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_H_cg"></td><td>3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" id="graph_H_cg_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" step="1" style="width:300px" id="graph_H_sl"></td><td>500</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" id="graph_H_sl_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" step="0.001" style="width:300px" id="graph_H_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" id="graph_H_sc_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.hierarchicalRepulsion.damping + '" step="0.005" style="width:300px" id="graph_H_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.damping + '" id="graph_H_damp_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">direction</td><td>1</td><td><input type="range" min="0" max="3" value="' + hierarchicalLayoutDirections.indexOf(this.constants.hierarchicalLayout.direction) + '" step="1" style="width:300px" id="graph_H_direction"></td><td>4</td><td><input value="' + this.constants.hierarchicalLayout.direction + '" id="graph_H_direction_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">levelSeparation</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.levelSeparation + '" step="1" style="width:300px" id="graph_H_levsep"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.levelSeparation + '" id="graph_H_levsep_value" style="width:60px"></td>' +
'</tr>' +
'<tr>' +
'<td width="150px">nodeSpacing</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.nodeSpacing + '" step="1" style="width:300px" id="graph_H_nspac"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.nodeSpacing + '" id="graph_H_nspac_value" style="width:60px"></td>' +
'</tr>' +
'</table>' +
'<table><tr><td><b>Options:</b></td></tr>' +
'<tr>' +
'<td width="180px"><input type="button" id="graph_toggleSmooth" value="Toggle smoothCurves" style="width:150px"></td>' +
'<td width="180px"><input type="button" id="graph_repositionNodes" value="Reinitialize" style="width:150px"></td>' +
'<td width="180px"><input type="button" id="graph_generateOptions" value="Generate Options" style="width:150px"></td>' +
'</tr>' +
'</table>'
this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement);
this.optionsDiv = document.createElement("div");
this.optionsDiv.style.fontSize = "14px";
this.optionsDiv.style.fontFamily = "verdana";
this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement);
var rangeElement;
rangeElement = document.getElementById('graph_BH_gc');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant");
rangeElement = document.getElementById('graph_BH_cg');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity");
rangeElement = document.getElementById('graph_BH_sc');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant");
rangeElement = document.getElementById('graph_BH_sl');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength");
rangeElement = document.getElementById('graph_BH_damp');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping");
rangeElement = document.getElementById('graph_R_nd');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance");
rangeElement = document.getElementById('graph_R_cg');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity");
rangeElement = document.getElementById('graph_R_sc');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant");
rangeElement = document.getElementById('graph_R_sl');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength");
rangeElement = document.getElementById('graph_R_damp');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping");
rangeElement = document.getElementById('graph_H_nd');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance");
rangeElement = document.getElementById('graph_H_cg');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity");
rangeElement = document.getElementById('graph_H_sc');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant");
rangeElement = document.getElementById('graph_H_sl');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength");
rangeElement = document.getElementById('graph_H_damp');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping");
rangeElement = document.getElementById('graph_H_direction');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction");
rangeElement = document.getElementById('graph_H_levsep');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation");
rangeElement = document.getElementById('graph_H_nspac');
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing");
var radioButton1 = document.getElementById("graph_physicsMethod1");
var radioButton2 = document.getElementById("graph_physicsMethod2");
var radioButton3 = document.getElementById("graph_physicsMethod3");
radioButton2.checked = true;
if (this.constants.physics.barnesHut.enabled) {
radioButton1.checked = true;
}
if (this.constants.hierarchicalLayout.enabled) {
radioButton3.checked = true;
}
var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
var graph_repositionNodes = document.getElementById("graph_repositionNodes");
var graph_generateOptions = document.getElementById("graph_generateOptions");
graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this);
graph_repositionNodes.onclick = graphRepositionNodes.bind(this);
graph_generateOptions.onclick = graphGenerateOptions.bind(this);
if (this.constants.smoothCurves == true) {
graph_toggleSmooth.style.background = "#A4FF56";
}
else {
graph_toggleSmooth.style.background = "#FF8532";
}
switchConfigurations.apply(this);
radioButton1.onchange = switchConfigurations.bind(this);
radioButton2.onchange = switchConfigurations.bind(this);
radioButton3.onchange = switchConfigurations.bind(this);
}
},
_overWriteGraphConstants: function (constantsVariableName, value) {
var nameArray = constantsVariableName.split("_");
if (nameArray.length == 1) {
this.constants[nameArray[0]] = value;
}
else if (nameArray.length == 2) {
this.constants[nameArray[0]][nameArray[1]] = value;
}
else if (nameArray.length == 3) {
this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value;
}
}
};
function graphToggleSmoothCurves () {
this.constants.smoothCurves = !this.constants.smoothCurves;
var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
else {graph_toggleSmooth.style.background = "#FF8532";}
this._configureSmoothCurves(false);
};
function graphRepositionNodes () {
for (var nodeId in this.calculationNodes) {
if (this.calculationNodes.hasOwnProperty(nodeId)) {
this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0;
this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0;
}
}
if (this.constants.hierarchicalLayout.enabled == true) {
this._setupHierarchicalLayout();
}
else {
this.repositionNodes();
}
this.moving = true;
this.start();
};
function graphGenerateOptions () {
var options = "No options are required, default values used.";
var optionsSpecific = [];
var radioButton1 = document.getElementById("graph_physicsMethod1");
var radioButton2 = document.getElementById("graph_physicsMethod2");
if (radioButton1.checked == true) {
if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);}
if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
if (optionsSpecific.length != 0) {
options = "var options = {";
options += "physics: {barnesHut: {";
for (var i = 0; i < optionsSpecific.length; i++) {
options += optionsSpecific[i];
if (i < optionsSpecific.length - 1) {
options += ", "
}
}
options += '}}'
}
if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
if (optionsSpecific.length == 0) {options = "var options = {";}
else {options += ", "}
options += "smoothCurves: " + this.constants.smoothCurves;
}
if (options != "No options are required, default values used.") {
options += '};'
}
}
else if (radioButton2.checked == true) {
options = "var options = {";
options += "physics: {barnesHut: {enabled: false}";
if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);}
if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
if (optionsSpecific.length != 0) {
options += ", repulsion: {";
for (var i = 0; i < optionsSpecific.length; i++) {
options += optionsSpecific[i];
if (i < optionsSpecific.length - 1) {
options += ", "
}
}
options += '}}'
}
if (optionsSpecific.length == 0) {options += "}"}
if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
options += ", smoothCurves: " + this.constants.smoothCurves;
}
options += '};'
}
else {
options = "var options = {";
if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);}
if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
if (optionsSpecific.length != 0) {
options += "physics: {hierarchicalRepulsion: {";
for (var i = 0; i < optionsSpecific.length; i++) {
options += optionsSpecific[i];
if (i < optionsSpecific.length - 1) {
options += ", ";
}
}
options += '}},';
}
options += 'hierarchicalLayout: {';
optionsSpecific = [];
if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);}
if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);}
if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);}
if (optionsSpecific.length != 0) {
for (var i = 0; i < optionsSpecific.length; i++) {
options += optionsSpecific[i];
if (i < optionsSpecific.length - 1) {
options += ", "
}
}
options += '}'
}
else {
options += "enabled:true}";
}
options += '};'
}
this.optionsDiv.innerHTML = options;
};
function switchConfigurations () {
var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"];
var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value;
var tableId = "graph_" + radioButton + "_table";
var table = document.getElementById(tableId);
table.style.display = "block";
for (var i = 0; i < ids.length; i++) {
if (ids[i] != tableId) {
table = document.getElementById(ids[i]);
table.style.display = "none";
}
}
this._restoreNodes();
if (radioButton == "R") {
this.constants.hierarchicalLayout.enabled = false;
this.constants.physics.hierarchicalRepulsion.enabled = false;
this.constants.physics.barnesHut.enabled = false;
}
else if (radioButton == "H") {
this.constants.hierarchicalLayout.enabled = true;
this.constants.physics.hierarchicalRepulsion.enabled = true;
this.constants.physics.barnesHut.enabled = false;
this._setupHierarchicalLayout();
}
else {
this.constants.hierarchicalLayout.enabled = false;
this.constants.physics.hierarchicalRepulsion.enabled = false;
this.constants.physics.barnesHut.enabled = true;
}
this._loadSelectedForceSolver();
var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
else {graph_toggleSmooth.style.background = "#FF8532";}
this.moving = true;
this.start();
}
function showValueOfRange (id,map,constantsVariableName) {
var valueId = id + "_value";
var rangeValue = document.getElementById(id).value;
if (map instanceof Array) {
document.getElementById(valueId).value = map[parseInt(rangeValue)];
this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]);
}
else {
document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue);
this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue));
}
if (constantsVariableName == "hierarchicalLayout_direction" ||
constantsVariableName == "hierarchicalLayout_levelSeparation" ||
constantsVariableName == "hierarchicalLayout_nodeSpacing") {
this._setupHierarchicalLayout();
}
this.moving = true;
this.start();
};

+ 66
- 0
src/graph/graphMixins/physics/Repulsion.js View File

@ -0,0 +1,66 @@
/**
* Created by Alex on 2/10/14.
*/
var repulsionMixin = {
/**
* Calculate the forces the nodes apply on eachother based on a repulsion field.
* This field is linearly approximated.
*
* @private
*/
_calculateNodeForces: function () {
var dx, dy, angle, distance, fx, fy, combinedClusterSize,
repulsingForce, node1, node2, i, j;
var nodes = this.calculationNodes;
var nodeIndices = this.calculationNodeIndices;
// approximation constants
var a_base = -2 / 3;
var b = 4 / 3;
// repulsing forces between nodes
var nodeDistance = this.constants.physics.repulsion.nodeDistance;
var minimumDistance = nodeDistance;
// we loop from i over all but the last entree in the array
// j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
for (i = 0; i < nodeIndices.length - 1; i++) {
node1 = nodes[nodeIndices[i]];
for (j = i + 1; j < nodeIndices.length; j++) {
node2 = nodes[nodeIndices[j]];
combinedClusterSize = node1.clusterSize + node2.clusterSize - 2;
dx = node2.x - node1.x;
dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy);
minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification));
var a = a_base / minimumDistance;
if (distance < 2 * minimumDistance) {
if (distance < 0.5 * minimumDistance) {
repulsingForce = 1.0;
}
else {
repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
}
// amplify the repulsion for clusters.
repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification;
repulsingForce = repulsingForce / distance;
fx = dx * repulsingForce;
fy = dy * repulsingForce;
node1.fx -= fx;
node1.fy -= fy;
node2.fx += fx;
node2.fy += fy;
}
}
}
}
};

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

Before After
Width: 24  |  Height: 24  |  Size: 20 KiB

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

Before After
Width: 24  |  Height: 24  |  Size: 20 KiB

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

Before After
Width: 24  |  Height: 24  |  Size: 20 KiB

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

Before After
Width: 24  |  Height: 24  |  Size: 20 KiB

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

Before After
Width: 7  |  Height: 7  |  Size: 18 KiB

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

Before After
Width: 5  |  Height: 5  |  Size: 17 KiB

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

Before After
Width: 24  |  Height: 24  |  Size: 20 KiB

src/graph/img/downarrow.png → src/graph/img/downArrow.png View File


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

Before After
Width: 24  |  Height: 24  |  Size: 20 KiB

src/graph/img/leftarrow.png → src/graph/img/leftArrow.png View File


src/graph/img/rightarrow.png → src/graph/img/rightArrow.png View File


src/graph/img/uparrow.png → src/graph/img/upArrow.png View File


+ 0
- 3
src/module/exports.js View File

@ -3,15 +3,12 @@
*/
var vis = {
util: util,
events: events,
Controller: Controller,
DataSet: DataSet,
DataView: DataView,
Range: Range,
Stack: Stack,
TimeStep: TimeStep,
EventBus: EventBus,
components: {
items: {

+ 1
- 2
src/module/imports.js View File

@ -6,6 +6,7 @@
// If not available there, load via require.
var moment = (typeof window !== 'undefined') && window['moment'] || require('moment');
var Emitter = require('emitter-component');
var Hammer;
if (typeof window !== 'undefined') {
@ -28,5 +29,3 @@ else {
throw Error('mouseTrap is only available in a browser, not in node.js.');
}
}

+ 0
- 172
src/timeline/Controller.js View File

@ -1,172 +0,0 @@
/**
* @constructor Controller
*
* A Controller controls the reflows and repaints of all visual components
*/
function Controller () {
this.id = util.randomUUID();
this.components = {};
this.repaintTimer = undefined;
this.reflowTimer = undefined;
}
/**
* Add a component to the controller
* @param {Component} component
*/
Controller.prototype.add = function add(component) {
// validate the component
if (component.id == undefined) {
throw new Error('Component has no field id');
}
if (!(component instanceof Component) && !(component instanceof Controller)) {
throw new TypeError('Component must be an instance of ' +
'prototype Component or Controller');
}
// add the component
component.controller = this;
this.components[component.id] = component;
};
/**
* Remove a component from the controller
* @param {Component | String} component
*/
Controller.prototype.remove = function remove(component) {
var id;
for (id in this.components) {
if (this.components.hasOwnProperty(id)) {
if (id == component || this.components[id] == component) {
break;
}
}
}
if (id) {
delete this.components[id];
}
};
/**
* Request a reflow. The controller will schedule a reflow
* @param {Boolean} [force] If true, an immediate reflow is forced. Default
* is false.
*/
Controller.prototype.requestReflow = function requestReflow(force) {
if (force) {
this.reflow();
}
else {
if (!this.reflowTimer) {
var me = this;
this.reflowTimer = setTimeout(function () {
me.reflowTimer = undefined;
me.reflow();
}, 0);
}
}
};
/**
* Request a repaint. The controller will schedule a repaint
* @param {Boolean} [force] If true, an immediate repaint is forced. Default
* is false.
*/
Controller.prototype.requestRepaint = function requestRepaint(force) {
if (force) {
this.repaint();
}
else {
if (!this.repaintTimer) {
var me = this;
this.repaintTimer = setTimeout(function () {
me.repaintTimer = undefined;
me.repaint();
}, 0);
}
}
};
/**
* Repaint all components
*/
Controller.prototype.repaint = function repaint() {
var changed = false;
// cancel any running repaint request
if (this.repaintTimer) {
clearTimeout(this.repaintTimer);
this.repaintTimer = undefined;
}
var done = {};
function repaint(component, id) {
if (!(id in done)) {
// first repaint the components on which this component is dependent
if (component.depends) {
component.depends.forEach(function (dep) {
repaint(dep, dep.id);
});
}
if (component.parent) {
repaint(component.parent, component.parent.id);
}
// repaint the component itself and mark as done
changed = component.repaint() || changed;
done[id] = true;
}
}
util.forEach(this.components, repaint);
// immediately reflow when needed
if (changed) {
this.reflow();
}
// TODO: limit the number of nested reflows/repaints, prevent loop
};
/**
* Reflow all components
*/
Controller.prototype.reflow = function reflow() {
var resized = false;
// cancel any running repaint request
if (this.reflowTimer) {
clearTimeout(this.reflowTimer);
this.reflowTimer = undefined;
}
var done = {};
function reflow(component, id) {
if (!(id in done)) {
// first reflow the components on which this component is dependent
if (component.depends) {
component.depends.forEach(function (dep) {
reflow(dep, dep.id);
});
}
if (component.parent) {
reflow(component.parent, component.parent.id);
}
// reflow the component itself and mark as done
resized = component.reflow() || resized;
done[id] = true;
}
}
util.forEach(this.components, reflow);
// immediately repaint when needed
if (resized) {
this.repaint();
}
// TODO: limit the number of nested reflows/repaints, prevent loop
};

+ 96
- 130
src/timeline/Range.js View File

@ -3,19 +3,41 @@
* A Range controls a numeric range with a start and end value.
* The Range adjusts the range based on mouse events or programmatic changes,
* and triggers events when the range is changing or has been changed.
* @param {Object} [options] See description at Range.setOptions
* @extends Controller
* @param {RootPanel} root Root panel, used to subscribe to events
* @param {Panel} parent Parent panel, used to attach to the DOM
* @param {Object} [options] See description at Range.setOptions
*/
function Range(options) {
function Range(root, parent, options) {
this.id = util.randomUUID();
this.start = null; // Number
this.end = null; // Number
this.root = root;
this.parent = parent;
this.options = options || {};
// drag listeners for dragging
this.root.on('dragstart', this._onDragStart.bind(this));
this.root.on('drag', this._onDrag.bind(this));
this.root.on('dragend', this._onDragEnd.bind(this));
// ignore dragging when holding
this.root.on('hold', this._onHold.bind(this));
// mouse wheel for zooming
this.root.on('mousewheel', this._onMouseWheel.bind(this));
this.root.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF
// pinch to zoom
this.root.on('touch', this._onTouch.bind(this));
this.root.on('pinch', this._onPinch.bind(this));
this.setOptions(options);
}
// turn Range into an event emitter
Emitter(Range.prototype);
/**
* Set options for the range controller
* @param {Object} options Available options:
@ -46,91 +68,6 @@ function validateDirection (direction) {
}
}
/**
* Add listeners for mouse and touch events to the component
* @param {Component} component
* @param {String} event Available events: 'move', 'zoom'
* @param {String} direction Available directions: 'horizontal', 'vertical'
*/
Range.prototype.subscribe = function (component, event, direction) {
var me = this;
if (event == 'move') {
// drag start listener
component.on('dragstart', function (event) {
me._onDragStart(event, component);
});
// drag listener
component.on('drag', function (event) {
me._onDrag(event, component, direction);
});
// drag end listener
component.on('dragend', function (event) {
me._onDragEnd(event, component);
});
}
else if (event == 'zoom') {
// mouse wheel
function mousewheel (event) {
me._onMouseWheel(event, component, direction);
}
component.on('mousewheel', mousewheel);
component.on('DOMMouseScroll', mousewheel); // For FF
// pinch
component.on('touch', function (event) {
me._onTouch();
});
component.on('pinch', function (event) {
me._onPinch(event, component, direction);
});
}
else {
throw new TypeError('Unknown event "' + event + '". ' +
'Choose "move" or "zoom".');
}
};
/**
* 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 on (event, callback) {
var available = ['rangechange', 'rangechanged'];
if (available.indexOf(event) == -1) {
throw new Error('Unknown event "' + event + '". Choose from ' + available.join());
}
events.addListener(this, event, callback);
};
/**
* Remove an event listener
* @param {String} event name of the event
* @param {function} callback callback handler
*/
Range.prototype.off = function off (event, callback) {
events.removeListener(this, event, callback);
};
/**
* Trigger an event
* @param {String} event name of the event, available events: 'rangechange',
* 'rangechanged'
* @private
*/
Range.prototype._trigger = function (event) {
events.trigger(this, event, {
start: this.start,
end: this.end
});
};
/**
* Set a new start and end range
* @param {Number} [start]
@ -139,8 +76,12 @@ Range.prototype._trigger = function (event) {
Range.prototype.setRange = function(start, end) {
var changed = this._applyRange(start, end);
if (changed) {
this._trigger('rangechange');
this._trigger('rangechanged');
var params = {
start: new Date(this.start),
end: new Date(this.end)
};
this.emit('rangechange', params);
this.emit('rangechanged', params);
}
};
@ -305,18 +246,19 @@ var touchParams = {};
/**
* Start dragging horizontally or vertically
* @param {Event} event
* @param {Object} component
* @private
*/
Range.prototype._onDragStart = function(event, component) {
Range.prototype._onDragStart = function(event) {
// refuse to drag when we where pinching to prevent the timeline make a jump
// when releasing the fingers in opposite order from the touch screen
if (touchParams.pinching) return;
if (touchParams.ignore) return;
// TODO: reckon with option movable
touchParams.start = this.start;
touchParams.end = this.end;
var frame = component.frame;
var frame = this.parent.frame;
if (frame) {
frame.style.cursor = 'move';
}
@ -325,57 +267,63 @@ Range.prototype._onDragStart = function(event, component) {
/**
* Perform dragging operating.
* @param {Event} event
* @param {Component} component
* @param {String} direction 'horizontal' or 'vertical'
* @private
*/
Range.prototype._onDrag = function (event, component, direction) {
Range.prototype._onDrag = function (event) {
var direction = this.options.direction;
validateDirection(direction);
// TODO: reckon with option movable
// refuse to drag when we where pinching to prevent the timeline make a jump
// when releasing the fingers in opposite order from the touch screen
if (touchParams.pinching) return;
if (touchParams.ignore) return;
var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
interval = (touchParams.end - touchParams.start),
width = (direction == 'horizontal') ? component.width : component.height,
width = (direction == 'horizontal') ? this.parent.width : this.parent.height,
diffRange = -delta / width * interval;
this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange);
// fire a rangechange event
this._trigger('rangechange');
this.emit('rangechange', {
start: new Date(this.start),
end: new Date(this.end)
});
};
/**
* Stop dragging operating.
* @param {event} event
* @param {Component} component
* @private
*/
Range.prototype._onDragEnd = function (event, component) {
Range.prototype._onDragEnd = function (event) {
// refuse to drag when we where pinching to prevent the timeline make a jump
// when releasing the fingers in opposite order from the touch screen
if (touchParams.pinching) return;
if (touchParams.ignore) return;
if (component.frame) {
component.frame.style.cursor = 'auto';
// TODO: reckon with option movable
if (this.parent.frame) {
this.parent.frame.style.cursor = 'auto';
}
// fire a rangechanged event
this._trigger('rangechanged');
this.emit('rangechanged', {
start: new Date(this.start),
end: new Date(this.end)
});
};
/**
* Event handler for mouse wheel event, used to zoom
* Code from http://adomas.org/javascript-mouse-wheel/
* @param {Event} event
* @param {Component} component
* @param {String} direction 'horizontal' or 'vertical'
* @private
*/
Range.prototype._onMouseWheel = function(event, component, direction) {
validateDirection(direction);
Range.prototype._onMouseWheel = function(event) {
// TODO: reckon with option zoomable
// retrieve delta
var delta = 0;
@ -405,47 +353,63 @@ Range.prototype._onMouseWheel = function(event, component, direction) {
// calculate center, the date to zoom around
var gesture = util.fakeGesture(this, event),
pointer = getPointer(gesture.touches[0], component.frame),
pointerDate = this._pointerToDate(component, direction, pointer);
pointer = getPointer(gesture.center, this.parent.frame),
pointerDate = this._pointerToDate(pointer);
this.zoom(scale, pointerDate);
}
// Prevent default actions caused by mouse wheel
// (else the page and timeline both zoom and scroll)
util.preventDefault(event);
event.preventDefault();
};
/**
* On start of a touch gesture, initialize scale to 1
* Start of a touch gesture
* @private
*/
Range.prototype._onTouch = function () {
Range.prototype._onTouch = function (event) {
touchParams.start = this.start;
touchParams.end = this.end;
touchParams.pinching = false;
touchParams.ignore = false;
touchParams.center = null;
// don't move the range when dragging a selected event
// TODO: it's not so neat to have to know about the state of the ItemSet
var item = ItemSet.itemFromTarget(event);
if (item && item.selected && this.options.editable) {
touchParams.ignore = true;
}
};
/**
* On start of a hold gesture
* @private
*/
Range.prototype._onHold = function () {
touchParams.ignore = true;
};
/**
* Handle pinch event
* @param {Event} event
* @param {Component} component
* @param {String} direction 'horizontal' or 'vertical'
* @private
*/
Range.prototype._onPinch = function (event, component, direction) {
touchParams.pinching = true;
Range.prototype._onPinch = function (event) {
var direction = this.options.direction;
touchParams.ignore = true;
// TODO: reckon with option zoomable
if (event.gesture.touches.length > 1) {
if (!touchParams.center) {
touchParams.center = getPointer(event.gesture.center, component.frame);
touchParams.center = getPointer(event.gesture.center, this.parent.frame);
}
var scale = 1 / event.gesture.scale,
initDate = this._pointerToDate(component, direction, touchParams.center),
center = getPointer(event.gesture.center, component.frame),
date = this._pointerToDate(component, direction, center),
initDate = this._pointerToDate(touchParams.center),
center = getPointer(event.gesture.center, this.parent.frame),
date = this._pointerToDate(this.parent, center),
delta = date - initDate; // TODO: utilize delta
// calculate new start and end
@ -459,21 +423,23 @@ Range.prototype._onPinch = function (event, component, direction) {
/**
* Helper function to calculate the center date for zooming
* @param {Component} component
* @param {{x: Number, y: Number}} pointer
* @param {String} direction 'horizontal' or 'vertical'
* @return {number} date
* @private
*/
Range.prototype._pointerToDate = function (component, direction, pointer) {
Range.prototype._pointerToDate = function (pointer) {
var conversion;
var direction = this.options.direction;
validateDirection(direction);
if (direction == 'horizontal') {
var width = component.width;
var width = this.parent.width;
conversion = this.conversion(width);
return pointer.x / conversion.scale + conversion.offset;
}
else {
var height = component.height;
var height = this.parent.height;
conversion = this.conversion(height);
return pointer.y / conversion.scale + conversion.offset;
}

+ 81
- 104
src/timeline/Stack.js View File

@ -1,18 +1,16 @@
// TODO: turn Stack into a Mixin?
/**
* @constructor Stack
* Stacks items on top of each other.
* @param {ItemSet} parent
* @param {Object} [options]
*/
function Stack (parent, options) {
this.parent = parent;
function Stack (options) {
this.options = options || {};
this.defaultOptions = {
order: function (a, b) {
//return (b.width - a.width) || (a.left - b.left); // TODO: cleanup
// Order: ranges over non-ranges, ranged ordered by width, and
// lastly ordered by start.
// Order: ranges over non-ranges, ranged ordered by width,
// and non-ranges ordered by start.
if (a instanceof ItemRange) {
if (b instanceof ItemRange) {
var aInt = (a.data.end - a.data.start);
@ -33,143 +31,122 @@ function Stack (parent, options) {
}
},
margin: {
item: 10
item: 10,
axis: 20
}
};
this.ordered = []; // ordered items
}
/**
* Set options for the stack
* @param {Object} options Available options:
* {ItemSet} parent
* {Number} margin
* {function} order Stacking order
* {Number} [margin.item=10]
* {Number} [margin.axis=20]
* {function} [order] Stacking order
*/
Stack.prototype.setOptions = function setOptions (options) {
util.extend(this.options, options);
// TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately
};
/**
* Stack the items such that they don't overlap. The items will have a minimal
* distance equal to options.margin.item.
* Order an array with items using a predefined order function for items
* @param {Item[]} items
*/
Stack.prototype.update = function update() {
this._order();
this._stack();
Stack.prototype.order = function order(items) {
//order the items
var order = this.options.order || this.defaultOptions.order;
if (!(typeof order === 'function')) {
throw new Error('Option order must be a function');
}
items.sort(order);
};
/**
* Order the items. The items are ordered by width first, and by left position
* second.
* If a custom order function has been provided via the options, then this will
* be used.
* @private
* Order items by their start data
* @param {Item[]} items
*/
Stack.prototype._order = function _order () {
var items = this.parent.items;
if (!items) {
throw new Error('Cannot stack items: parent does not contain items');
}
// TODO: store the sorted items, to have less work later on
var ordered = [];
var index = 0;
// items is a map (no array)
util.forEach(items, function (item) {
if (item.visible) {
ordered[index] = item;
index++;
}
Stack.prototype.orderByStart = function orderByStart(items) {
items.sort(function (a, b) {
return a.data.start - b.data.start;
});
};
//if a customer stack order function exists, use it.
var order = this.options.order || this.defaultOptions.order;
if (!(typeof order === 'function')) {
throw new Error('Option order must be a function');
}
ordered.sort(order);
/**
* Order items by their end date. If they have no end date, their start date
* is used.
* @param {Item[]} items
*/
Stack.prototype.orderByEnd = function orderByEnd(items) {
items.sort(function (a, b) {
var aTime = ('end' in a.data) ? a.data.end : a.data.start,
bTime = ('end' in b.data) ? b.data.end : b.data.start;
this.ordered = ordered;
return aTime - bTime;
});
};
/**
* Adjust vertical positions of the events such that they don't overlap each
* other.
* @param {Item[]} items All visible items
* @param {boolean} [force=false] If true, all items will be re-stacked.
* If false (default), only items having a
* top===null will be re-stacked
* @private
*/
Stack.prototype._stack = function _stack () {
Stack.prototype.stack = function stack (items, force) {
var i,
iMax,
ordered = this.ordered,
options = this.options,
orientation = options.orientation || this.defaultOptions.orientation,
axisOnTop = (orientation == 'top'),
margin;
marginItem,
marginAxis;
if (options.margin && options.margin.item !== undefined) {
margin = options.margin.item;
marginItem = options.margin.item;
}
else {
margin = this.defaultOptions.margin.item
marginItem = this.defaultOptions.margin.item
}
if (options.margin && options.margin.axis !== undefined) {
marginAxis = options.margin.axis;
}
else {
marginAxis = this.defaultOptions.margin.axis
}
// calculate new, non-overlapping positions
for (i = 0, iMax = ordered.length; i < iMax; i++) {
var item = ordered[i];
var collidingItem = null;
do {
// TODO: optimize checking for overlap. when there is a gap without items,
// you only need to check for items from the next item on, not from zero
collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin);
if (collidingItem != null) {
// There is a collision. Reposition the event above the colliding element
if (axisOnTop) {
item.top = collidingItem.top + collidingItem.height + margin;
}
else {
item.top = collidingItem.top - item.height - margin;
}
}
} while (collidingItem);
if (force) {
// reset top position of all items
for (i = 0, iMax = items.length; i < iMax; i++) {
items[i].top = null;
}
}
};
/**
* Check if the destiny position of given item overlaps with any
* of the other items from index itemStart to itemEnd.
* @param {Array} items Array with items
* @param {int} itemIndex Number of the item to be checked for overlap
* @param {int} itemStart First item to be checked.
* @param {int} itemEnd Last item to be checked.
* @return {Object | null} colliding item, or undefined when no collisions
* @param {Number} margin A minimum required margin.
* If margin is provided, the two items will be
* marked colliding when they overlap or
* when the margin between the two is smaller than
* the requested margin.
*/
Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex,
itemStart, itemEnd, margin) {
var collision = this.collision;
// calculate new, non-overlapping positions
for (i = 0, iMax = items.length; i < iMax; i++) {
var item = items[i];
if (item.top === null) {
// initialize top position
item.top = marginAxis;
do {
// TODO: optimize checking for overlap. when there is a gap without items,
// you only need to check for items from the next item on, not from zero
var collidingItem = null;
for (var j = 0, jj = items.length; j < jj; j++) {
var other = items[j];
if (other.top !== null && other !== item && this.collision(item, other, marginItem)) {
collidingItem = other;
break;
}
}
// we loop from end to start, as we suppose that the chance of a
// collision is larger for items at the end, so check these first.
var a = items[itemIndex];
for (var i = itemEnd; i >= itemStart; i--) {
var b = items[i];
if (collision(a, b, margin)) {
if (i != itemIndex) {
return b;
}
if (collidingItem != null) {
// There is a collision. Reposition the event above the colliding element
item.top = collidingItem.top + collidingItem.height + marginItem;
}
} while (collidingItem);
}
}
return null;
};
/**
@ -185,8 +162,8 @@ Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex,
* @return {boolean} true if a and b collide, else false
*/
Stack.prototype.collision = function collision (a, b, margin) {
return ((a.left - margin) < (b.left + b.getWidth()) &&
(a.left + a.getWidth() + margin) > b.left &&
return ((a.left - margin) < (b.left + b.width) &&
(a.left + a.width + margin) > b.left &&
(a.top - margin) < (b.top + b.height) &&
(a.top + a.height + margin) > b.top);
};

+ 43
- 38
src/timeline/TimeStep.js View File

@ -281,35 +281,38 @@ TimeStep.prototype.setMinimumStep = function(minimumStep) {
};
/**
* Snap a date to a rounded value. The snap intervals are dependent on the
* current scale and step.
* @param {Date} date the date to be snapped
* Snap a date to a rounded value.
* The snap intervals are dependent on the current scale and step.
* @param {Date} date the date to be snapped.
* @return {Date} snappedDate
*/
TimeStep.prototype.snap = function(date) {
var clone = new Date(date.valueOf());
if (this.scale == TimeStep.SCALE.YEAR) {
var year = date.getFullYear() + Math.round(date.getMonth() / 12);
date.setFullYear(Math.round(year / this.step) * this.step);
date.setMonth(0);
date.setDate(0);
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
var year = clone.getFullYear() + Math.round(clone.getMonth() / 12);
clone.setFullYear(Math.round(year / this.step) * this.step);
clone.setMonth(0);
clone.setDate(0);
clone.setHours(0);
clone.setMinutes(0);
clone.setSeconds(0);
clone.setMilliseconds(0);
}
else if (this.scale == TimeStep.SCALE.MONTH) {
if (date.getDate() > 15) {
date.setDate(1);
date.setMonth(date.getMonth() + 1);
if (clone.getDate() > 15) {
clone.setDate(1);
clone.setMonth(clone.getMonth() + 1);
// important: first set Date to 1, after that change the month.
}
else {
date.setDate(1);
clone.setDate(1);
}
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
clone.setHours(0);
clone.setMinutes(0);
clone.setSeconds(0);
clone.setMilliseconds(0);
}
else if (this.scale == TimeStep.SCALE.DAY ||
this.scale == TimeStep.SCALE.WEEKDAY) {
@ -317,56 +320,58 @@ TimeStep.prototype.snap = function(date) {
switch (this.step) {
case 5:
case 2:
date.setHours(Math.round(date.getHours() / 24) * 24); break;
clone.setHours(Math.round(clone.getHours() / 24) * 24); break;
default:
date.setHours(Math.round(date.getHours() / 12) * 12); break;
clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
}
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
clone.setMinutes(0);
clone.setSeconds(0);
clone.setMilliseconds(0);
}
else if (this.scale == TimeStep.SCALE.HOUR) {
switch (this.step) {
case 4:
date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break;
default:
date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break;
}
date.setSeconds(0);
date.setMilliseconds(0);
clone.setSeconds(0);
clone.setMilliseconds(0);
} else if (this.scale == TimeStep.SCALE.MINUTE) {
//noinspection FallthroughInSwitchStatementJS
switch (this.step) {
case 15:
case 10:
date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
date.setSeconds(0);
clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5);
clone.setSeconds(0);
break;
case 5:
date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break;
default:
date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break;
}
date.setMilliseconds(0);
clone.setMilliseconds(0);
}
else if (this.scale == TimeStep.SCALE.SECOND) {
//noinspection FallthroughInSwitchStatementJS
switch (this.step) {
case 15:
case 10:
date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
date.setMilliseconds(0);
clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5);
clone.setMilliseconds(0);
break;
case 5:
date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break;
default:
date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break;
}
}
else if (this.scale == TimeStep.SCALE.MILLISECOND) {
var step = this.step > 5 ? this.step / 2 : 1;
date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step);
}
return clone;
};
/**

+ 475
- 229
src/timeline/Timeline.js View File

@ -1,127 +1,243 @@
/**
* Create a timeline visualization
* @param {HTMLElement} container
* @param {vis.DataSet | Array | DataTable} [items]
* @param {vis.DataSet | Array | google.visualization.DataTable} [items]
* @param {Object} [options] See Timeline.setOptions for the available options.
* @constructor
*/
function Timeline (container, items, options) {
// validate arguments
if (!container) throw new Error('No container element provided');
var me = this;
var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
this.options = {
orientation: 'bottom',
direction: 'horizontal', // 'horizontal' or 'vertical'
autoResize: true,
editable: false,
selectable: true,
snap: null, // will be specified after timeaxis is created
min: null,
max: null,
zoomMin: 10, // milliseconds
zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
// moveable: true, // TODO: option moveable
// zoomable: true, // TODO: option zoomable
showMinorLabels: true,
showMajorLabels: true,
showCurrentTime: false,
showCustomTime: false,
autoResize: false
};
// controller
this.controller = new Controller();
type: 'box',
align: 'center',
margin: {
axis: 20,
item: 10
},
padding: 5,
onAdd: function (item, callback) {
callback(item);
},
onUpdate: function (item, callback) {
callback(item);
},
onMove: function (item, callback) {
callback(item);
},
onRemove: function (item, callback) {
callback(item);
},
toScreen: me._toScreen.bind(me),
toTime: me._toTime.bind(me)
};
// root panel
if (!container) {
throw new Error('No container element provided');
}
var rootOptions = Object.create(this.options);
rootOptions.height = function () {
// TODO: change to height
if (me.options.height) {
// fixed height
return me.options.height;
}
else {
// auto height
return (me.timeaxis.height + me.content.height) + 'px';
var rootOptions = util.extend(Object.create(this.options), {
height: function () {
if (me.options.height) {
// fixed height
return me.options.height;
}
else {
// auto height
// TODO: implement a css based solution to automatically have the right hight
return (me.timeAxis.height + me.contentPanel.height) + 'px';
}
}
};
});
this.rootPanel = new RootPanel(container, rootOptions);
this.controller.add(this.rootPanel);
// item panel
var itemOptions = Object.create(this.options);
itemOptions.left = function () {
return me.labelPanel.width;
};
itemOptions.width = function () {
return me.rootPanel.width - me.labelPanel.width;
};
itemOptions.top = null;
itemOptions.height = null;
this.itemPanel = new Panel(this.rootPanel, [], itemOptions);
this.controller.add(this.itemPanel);
// label panel
var labelOptions = Object.create(this.options);
labelOptions.top = null;
labelOptions.left = null;
labelOptions.height = null;
labelOptions.width = function () {
if (me.content && typeof me.content.getLabelsWidth === 'function') {
return me.content.getLabelsWidth();
}
else {
return 0;
// single select (or unselect) when tapping an item
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));
// add item on doubletap
this.rootPanel.on('doubletap', this._onAddItem.bind(this));
// side panel
var sideOptions = util.extend(Object.create(this.options), {
top: function () {
return (sideOptions.orientation == 'top') ? '0' : '';
},
bottom: function () {
return (sideOptions.orientation == 'top') ? '' : '0';
},
left: '0',
right: null,
height: '100%',
width: function () {
if (me.groupSet) {
return me.groupSet.getLabelsWidth();
}
else {
return 0;
}
},
className: function () {
return 'side' + (me.groupsData ? '' : ' hidden');
}
};
this.labelPanel = new Panel(this.rootPanel, [], labelOptions);
this.controller.add(this.labelPanel);
});
this.sidePanel = new Panel(sideOptions);
this.rootPanel.appendChild(this.sidePanel);
// main panel (contains time axis and itemsets)
var mainOptions = util.extend(Object.create(this.options), {
left: function () {
// we align left to enable a smooth resizing of the window
return me.sidePanel.width;
},
right: null,
height: '100%',
width: function () {
return me.rootPanel.width - me.sidePanel.width;
},
className: 'main'
});
this.mainPanel = new Panel(mainOptions);
this.rootPanel.appendChild(this.mainPanel);
// range
// TODO: move range inside rootPanel?
var rangeOptions = Object.create(this.options);
this.range = new Range(rangeOptions);
this.range = new Range(this.rootPanel, this.mainPanel, rangeOptions);
this.range.setRange(
now.clone().add('days', -3).valueOf(),
now.clone().add('days', 4).valueOf()
);
// TODO: reckon with options moveable and zoomable
// TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable
this.range.subscribe(this.rootPanel, 'move', 'horizontal');
this.range.subscribe(this.rootPanel, 'zoom', 'horizontal');
this.range.on('rangechange', function (properties) {
var force = true;
me.controller.requestReflow(force);
me._trigger('rangechange', properties);
me.rootPanel.repaint();
me.emit('rangechange', properties);
});
this.range.on('rangechanged', function (properties) {
var force = true;
me.controller.requestReflow(force);
me._trigger('rangechanged', properties);
me.rootPanel.repaint();
me.emit('rangechanged', properties);
});
// single select (or unselect) when tapping an item
// TODO: implement ctrl+click
this.rootPanel.on('tap', this._onSelectItem.bind(this));
// multi select when holding mouse/touch, or on ctrl+click
this.rootPanel.on('hold', this._onMultiSelectItem.bind(this));
// time axis
var timeaxisOptions = Object.create(rootOptions);
timeaxisOptions.range = this.range;
timeaxisOptions.left = null;
timeaxisOptions.top = null;
timeaxisOptions.width = '100%';
timeaxisOptions.height = null;
this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions);
this.timeaxis.setRange(this.range);
this.controller.add(this.timeaxis);
// panel with time axis
var timeAxisOptions = util.extend(Object.create(rootOptions), {
range: this.range,
left: null,
top: null,
width: null,
height: null
});
this.timeAxis = new TimeAxis(timeAxisOptions);
this.timeAxis.setRange(this.range);
this.options.snap = this.timeAxis.snap.bind(this.timeAxis);
this.mainPanel.appendChild(this.timeAxis);
// content panel (contains itemset(s))
var contentOptions = util.extend(Object.create(this.options), {
top: function () {
return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
},
bottom: function () {
return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
},
left: null,
right: null,
height: null,
width: null,
className: 'content'
});
this.contentPanel = new Panel(contentOptions);
this.mainPanel.appendChild(this.contentPanel);
// content panel (contains the vertical lines of box items)
var backgroundOptions = util.extend(Object.create(this.options), {
top: function () {
return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
},
bottom: function () {
return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
},
left: null,
right: null,
height: function () {
return me.contentPanel.height;
},
width: null,
className: 'background'
});
this.backgroundPanel = new Panel(backgroundOptions);
this.mainPanel.insertBefore(this.backgroundPanel, this.contentPanel);
// panel with axis holding the dots of item boxes
var axisPanelOptions = util.extend(Object.create(rootOptions), {
left: 0,
top: function () {
return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
},
bottom: function () {
return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
},
width: '100%',
height: 0,
className: 'axis'
});
this.axisPanel = new Panel(axisPanelOptions);
this.mainPanel.appendChild(this.axisPanel);
// content panel (contains itemset(s))
var sideContentOptions = util.extend(Object.create(this.options), {
top: function () {
return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
},
bottom: function () {
return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
},
left: null,
right: null,
height: null,
width: null,
className: 'side-content'
});
this.sideContentPanel = new Panel(sideContentOptions);
this.sidePanel.appendChild(this.sideContentPanel);
// current time bar
this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions);
this.controller.add(this.currenttime);
// Note: time bar will be attached in this.setOptions when selected
this.currentTime = new CurrentTime(this.range, rootOptions);
// custom time bar
this.customtime = new CustomTime(this.timeaxis, [], rootOptions);
this.controller.add(this.customtime);
// Note: time bar will be attached in this.setOptions when selected
this.customTime = new CustomTime(rootOptions);
this.customTime.on('timechange', function (time) {
me.emit('timechange', time);
});
this.customTime.on('timechanged', function (time) {
me.emit('timechanged', time);
});
this.itemSet = null;
this.groupSet = null;
// create groupset
this.setGroups(null);
@ -140,6 +256,9 @@ function Timeline (container, items, options) {
}
}
// turn Timeline into an event emitter
Emitter(Timeline.prototype);
/**
* Set options
* @param {Object} options TODO: describe the available options
@ -151,8 +270,58 @@ Timeline.prototype.setOptions = function (options) {
// both start and end are optional
this.range.setRange(options.start, options.end);
this.controller.reflow();
this.controller.repaint();
if ('editable' in options || 'selectable' in options) {
if (this.options.selectable) {
// force update of selection
this.setSelection(this.getSelection());
}
else {
// remove selection
this.setSelection([]);
}
}
// validate the callback functions
var validateCallback = (function (fn) {
if (!(this.options[fn] instanceof Function) || this.options[fn].length != 2) {
throw new Error('option ' + fn + ' must be a function ' + fn + '(item, callback)');
}
}).bind(this);
['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(validateCallback);
// add/remove the current time bar
if (this.options.showCurrentTime) {
if (!this.mainPanel.hasChild(this.currentTime)) {
this.mainPanel.appendChild(this.currentTime);
this.currentTime.start();
}
}
else {
if (this.mainPanel.hasChild(this.currentTime)) {
this.currentTime.stop();
this.mainPanel.removeChild(this.currentTime);
}
}
// add/remove the custom time bar
if (this.options.showCustomTime) {
if (!this.mainPanel.hasChild(this.customTime)) {
this.mainPanel.appendChild(this.customTime);
}
}
else {
if (this.mainPanel.hasChild(this.customTime)) {
this.mainPanel.removeChild(this.customTime);
}
}
// TODO: remove deprecation error one day (deprecated since version 0.8.0)
if (options && options.order) {
throw new Error('Option order is deprecated. There is no replacement for this feature.');
}
// repaint everything
this.rootPanel.repaint();
};
/**
@ -160,7 +329,11 @@ Timeline.prototype.setOptions = function (options) {
* @param {Date} time
*/
Timeline.prototype.setCustomTime = function (time) {
this.customtime._setCustomTime(time);
if (!this.customTime) {
throw new Error('Cannot get custom time: Custom time bar is not enabled');
}
this.customTime.setCustomTime(time);
};
/**
@ -168,37 +341,41 @@ Timeline.prototype.setCustomTime = function (time) {
* @return {Date} customTime
*/
Timeline.prototype.getCustomTime = function() {
return new Date(this.customtime.customTime.valueOf());
if (!this.customTime) {
throw new Error('Cannot get custom time: Custom time bar is not enabled');
}
return this.customTime.getCustomTime();
};
/**
* Set items
* @param {vis.DataSet | Array | DataTable | null} items
* @param {vis.DataSet | Array | google.visualization.DataTable | null} items
*/
Timeline.prototype.setItems = function(items) {
var initialLoad = (this.itemsData == null);
// convert to type DataSet when needed
var newItemSet;
var newDataSet;
if (!items) {
newItemSet = null;
newDataSet = null;
}
else if (items instanceof DataSet) {
newItemSet = items;
newDataSet = items;
}
if (!(items instanceof DataSet)) {
newItemSet = new DataSet({
newDataSet = new DataSet({
convert: {
start: 'Date',
end: 'Date'
}
});
newItemSet.add(items);
newDataSet.add(items);
}
// set items
this.itemsData = newItemSet;
this.content.setItems(newItemSet);
this.itemsData = newDataSet;
(this.itemSet || this.groupSet).setItems(newDataSet);
if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
// apply the data range as range
@ -225,87 +402,84 @@ Timeline.prototype.setItems = function(items) {
end = util.convert(this.options.end, 'Date');
}
// apply range if there is a min or max available
if (start != null || end != null) {
this.range.setRange(start, end);
// skip range set if there is no start and end date
if (start === null && end === null) {
return;
}
// if start and end dates are set but cannot be satisfyed due to zoom restrictions — correct end date
if (start != null && end != null) {
var diff = end.valueOf() - start.valueOf();
if (this.options.zoomMax != undefined && this.options.zoomMax < diff) {
end = new Date(start.valueOf() + this.options.zoomMax);
}
if (this.options.zoomMin != undefined && this.options.zoomMin > diff) {
end = new Date(start.valueOf() + this.options.zoomMin);
}
}
this.range.setRange(start, end);
}
};
/**
* Set groups
* @param {vis.DataSet | Array | DataTable} groups
* @param {vis.DataSet | Array | google.visualization.DataTable} groupSet
*/
Timeline.prototype.setGroups = function(groups) {
Timeline.prototype.setGroups = function(groupSet) {
var me = this;
this.groupsData = groups;
// switch content type between ItemSet or GroupSet when needed
var Type = this.groupsData ? GroupSet : ItemSet;
if (!(this.content instanceof Type)) {
// remove old content set
if (this.content) {
this.content.hide();
if (this.content.setItems) {
this.content.setItems(); // disconnect from items
}
if (this.content.setGroups) {
this.content.setGroups(); // disconnect from groups
}
this.controller.remove(this.content);
}
this.groupsData = groupSet;
// create options for the itemset or groupset
var options = util.extend(Object.create(this.options), {
top: null,
bottom: null,
right: null,
left: null,
width: null,
height: null
});
// create new content set
var options = Object.create(this.options);
util.extend(options, {
top: function () {
if (me.options.orientation == 'top') {
return me.timeaxis.height;
}
else {
return me.itemPanel.height - me.timeaxis.height - me.content.height;
}
},
left: null,
width: '100%',
height: function () {
if (me.options.height) {
// fixed height
return me.itemPanel.height - me.timeaxis.height;
}
else {
// auto height
return null;
}
},
maxHeight: function () {
// TODO: change maxHeight to be a css string like '100%' or '300px'
if (me.options.maxHeight) {
if (!util.isNumber(me.options.maxHeight)) {
throw new TypeError('Number expected for property maxHeight');
}
return me.options.maxHeight - me.timeaxis.height;
}
else {
return null;
}
},
labelContainer: function () {
return me.labelPanel.getContainer();
}
});
if (this.groupsData) {
// Create a GroupSet
// remove itemset if existing
if (this.itemSet) {
this.itemSet.hide(); // TODO: not so nice having to hide here
this.contentPanel.removeChild(this.itemSet);
this.itemSet.setItems(); // disconnect from itemset
this.itemSet = null;
}
this.content = new Type(this.itemPanel, [this.timeaxis], options);
if (this.content.setRange) {
this.content.setRange(this.range);
// create new GroupSet when needed
if (!this.groupSet) {
this.groupSet = new GroupSet(this.contentPanel, this.sideContentPanel, this.backgroundPanel, this.axisPanel, options);
this.groupSet.on('change', this.rootPanel.repaint.bind(this.rootPanel));
this.groupSet.setRange(this.range);
this.groupSet.setItems(this.itemsData);
this.groupSet.setGroups(this.groupsData);
this.contentPanel.appendChild(this.groupSet);
}
if (this.content.setItems) {
this.content.setItems(this.itemsData);
else {
this.groupSet.setGroups(this.groupsData);
}
if (this.content.setGroups) {
this.content.setGroups(this.groupsData);
}
else {
// ItemSet
if (this.groupSet) {
this.groupSet.hide(); // TODO: not so nice having to hide here
//this.groupSet.setGroups(); // disconnect from groupset
this.groupSet.setItems(); // disconnect from itemset
this.contentPanel.removeChild(this.groupSet);
this.groupSet = null;
}
this.controller.add(this.content);
// create new items
this.itemSet = new ItemSet(this.backgroundPanel, this.axisPanel, options);
this.itemSet.setRange(this.range);
this.itemSet.setItems(this.itemsData);
this.itemSet.on('change', me.rootPanel.repaint.bind(me.rootPanel));
this.contentPanel.appendChild(this.itemSet);
}
};
@ -356,7 +530,9 @@ Timeline.prototype.getItemRange = function getItemRange() {
* unselected.
*/
Timeline.prototype.setSelection = function setSelection (ids) {
if (this.content) this.content.setSelection(ids);
var itemOrGroupSet = (this.itemSet || this.groupSet);
if (itemOrGroupSet) itemOrGroupSet.setSelection(ids);
};
/**
@ -364,45 +540,44 @@ Timeline.prototype.setSelection = function setSelection (ids) {
* @return {Array} ids The ids of the selected items
*/
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());
}
var itemOrGroupSet = (this.itemSet || this.groupSet);
events.addListener(this, event, callback);
return itemOrGroupSet ? itemOrGroupSet.getSelection() : [];
};
/**
* Remove an event listener
* @param {String} event Event name
* @param {function} callback Callback function
* Set the visible window. Both parameters are optional, you can change only
* start or only end. Syntax:
*
* TimeLine.setWindow(start, end)
* TimeLine.setWindow(range)
*
* Where start and end can be a Date, number, or string, and range is an
* object with properties start and end.
*
* @param {Date | Number | String} [start] Start date of visible window
* @param {Date | Number | String} [end] End date of visible window
*/
Timeline.prototype.off = function off (event, callback) {
events.removeListener(this, event, callback);
Timeline.prototype.setWindow = function setWindow(start, end) {
if (arguments.length == 1) {
var range = arguments[0];
this.range.setRange(range.start, range.end);
}
else {
this.range.setRange(start, end);
}
};
/**
* Trigger an event
* @param {String} event Event name, available events: 'rangechange',
* 'rangechanged', 'select'
* @param {Object} [properties] Event specific properties
* @private
* Get the visible window
* @return {{start: Date, end: Date}} Visible range
*/
Timeline.prototype._trigger = function _trigger(event, properties) {
events.trigger(this, event, properties || {});
Timeline.prototype.getWindow = function setWindow() {
var range = this.range.getRange();
return {
start: new Date(range.start),
end: new Date(range.end)
};
};
/**
@ -410,67 +585,138 @@ Timeline.prototype._trigger = function _trigger(event, properties) {
* @param {Event} event
* @private
*/
// TODO: move this function to ItemSet
Timeline.prototype._onSelectItem = function (event) {
var item = this._itemFromTarget(event);
if (!this.options.selectable) return;
var ctrlKey = event.gesture.srcEvent && event.gesture.srcEvent.ctrlKey;
var shiftKey = event.gesture.srcEvent && event.gesture.srcEvent.shiftKey;
if (ctrlKey || shiftKey) {
this._onMultiSelectItem(event);
return;
}
var oldSelection = this.getSelection();
var item = ItemSet.itemFromTarget(event);
var selection = item ? [item.id] : [];
this.setSelection(selection);
this._trigger('select', {
items: this.getSelection()
});
var newSelection = this.getSelection();
// if selection is changed, emit a select event
if (!util.equalArray(oldSelection, newSelection)) {
this.emit('select', {
items: this.getSelection()
});
}
event.stopPropagation();
};
/**
* Handle selecting/deselecting multiple items when holding an item
* @param {Event} event
* Handle creation and updates of an item on double tap
* @param event
* @private
*/
Timeline.prototype._onMultiSelectItem = function (event) {
var selection,
item = this._itemFromTarget(event);
Timeline.prototype._onAddItem = function (event) {
if (!this.options.selectable) return;
if (!this.options.editable) return;
if (!item) {
// do nothing...
return;
}
var me = this,
item = ItemSet.itemFromTarget(event);
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);
if (item) {
// update item
// execute async handler to update the item (or cancel it)
var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset
this.options.onUpdate(itemData, function (itemData) {
if (itemData) {
me.itemsData.update(itemData);
}
});
}
else {
// item is already selected -> deselect it
selection.splice(index, 1);
}
this.setSelection(selection);
this._trigger('select', {
items: this.getSelection()
});
// add item
var xAbs = vis.util.getAbsoluteLeft(this.rootPanel.frame);
var x = event.gesture.center.pageX - xAbs;
var newItem = {
start: this.timeAxis.snap(this._toTime(x)),
content: 'new item'
};
var id = util.randomUUID();
newItem[this.itemsData.fieldId] = id;
var group = GroupSet.groupFromTarget(event);
if (group) {
newItem.group = group.groupId;
}
event.stopPropagation();
// execute async handler to customize (or cancel) adding an item
this.options.onAdd(newItem, function (item) {
if (item) {
me.itemsData.add(newItem);
// TODO: need to trigger a repaint?
}
});
}
};
/**
* Find an item from an event target:
* searches for the attribute 'timeline-item' in the event target's element tree
* Handle selecting/deselecting multiple items when holding an item
* @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'];
// TODO: move this function to ItemSet
Timeline.prototype._onMultiSelectItem = function (event) {
if (!this.options.selectable) return;
var selection,
item = ItemSet.itemFromTarget(event);
if (item) {
// multi select items
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);
}
target = target.parentNode;
this.setSelection(selection);
this.emit('select', {
items: this.getSelection()
});
event.stopPropagation();
}
};
return null;
};
/**
* Convert a position on screen (pixels) to a datetime
* @param {int} x Position on the screen in pixels
* @return {Date} time The datetime the corresponds with given position x
* @private
*/
Timeline.prototype._toTime = function _toTime(x) {
var conversion = this.range.conversion(this.mainPanel.width);
return new Date(x / conversion.scale + conversion.offset);
};
/**
* Convert a datetime (Date object) into a position on the screen
* @param {Date} time A date
* @return {int} x The position on the screen in pixels which corresponds
* with the given date.
* @private
*/
Timeline.prototype._toScreen = function _toScreen(time) {
var conversion = this.range.conversion(this.mainPanel.width);
return (time.valueOf() - conversion.offset) * conversion.scale;
};

+ 17
- 79
src/timeline/component/Component.js View File

@ -4,23 +4,23 @@
function Component () {
this.id = null;
this.parent = null;
this.depends = null;
this.controller = null;
this.childs = null;
this.options = null;
this.frame = null; // main DOM element
this.top = 0;
this.left = 0;
this.width = 0;
this.height = 0;
}
// Turn the Component into an event emitter
Emitter(Component.prototype);
/**
* Set parameters for the frame. Parameters will be merged in current parameter
* set.
* @param {Object} options Available parameters:
* {String | function} [className]
* {EventBus} [eventBus]
* {String | Number | function} [left]
* {String | Number | function} [top]
* {String | Number | function} [width]
@ -30,10 +30,7 @@ Component.prototype.setOptions = function setOptions(options) {
if (options) {
util.extend(this.options, options);
if (this.controller) {
this.requestRepaint();
this.requestReflow();
}
this.repaint();
}
};
@ -55,29 +52,18 @@ Component.prototype.getOption = function getOption(name) {
return value;
};
/**
* Get the container element of the component, which can be used by a child to
* add its own widgets. Not all components do have a container for childs, in
* that case null is returned.
* @returns {HTMLElement | null} container
*/
// TODO: get rid of the getContainer and getFrame methods, provide these via the options
Component.prototype.getContainer = function getContainer() {
// should be implemented by the component
return null;
};
/**
* Get the frame element of the component, the outer HTML DOM element.
* @returns {HTMLElement | null} frame
*/
Component.prototype.getFrame = function getFrame() {
return this.frame;
// should be implemented by the component
return null;
};
/**
* Repaint the component
* @return {Boolean} changed
* @return {boolean} Returns true if the component is resized
*/
Component.prototype.repaint = function repaint() {
// should be implemented by the component
@ -85,64 +71,16 @@ Component.prototype.repaint = function repaint() {
};
/**
* Reflow the component
* @return {Boolean} resized
* Test whether the component is resized since the last time _isResized() was
* called.
* @return {Boolean} Returns true if the component is resized
* @private
*/
Component.prototype.reflow = function reflow() {
// should be implemented by the component
return false;
};
Component.prototype._isResized = function _isResized() {
var resized = (this._previousWidth !== this.width || this._previousHeight !== this.height);
/**
* Hide the component from the DOM
* @return {Boolean} changed
*/
Component.prototype.hide = function hide() {
if (this.frame && this.frame.parentNode) {
this.frame.parentNode.removeChild(this.frame);
return true;
}
else {
return false;
}
};
this._previousWidth = this.width;
this._previousHeight = this.height;
/**
* Show the component in the DOM (when not already visible).
* A repaint will be executed when the component is not visible
* @return {Boolean} changed
*/
Component.prototype.show = function show() {
if (!this.frame || !this.frame.parentNode) {
return this.repaint();
}
else {
return false;
}
};
/**
* Request a repaint. The controller will schedule a repaint
*/
Component.prototype.requestRepaint = function requestRepaint() {
if (this.controller) {
this.controller.requestRepaint();
}
else {
throw new Error('Cannot request a repaint: no controller configured');
// TODO: just do a repaint when no parent is configured?
}
};
/**
* Request a reflow. The controller will schedule a reflow
*/
Component.prototype.requestReflow = function requestReflow() {
if (this.controller) {
this.controller.requestReflow();
}
else {
throw new Error('Cannot request a reflow: no controller configured');
// TODO: just do a reflow when no parent is configured?
}
return resized;
};

+ 0
- 113
src/timeline/component/ContentPanel.js View File

@ -1,113 +0,0 @@
/**
* A content panel can contain a groupset or an itemset, and can handle
* vertical scrolling
* @param {Component} [parent]
* @param {Component[]} [depends] Components on which this components depends
* (except for the parent)
* @param {Object} [options] Available parameters:
* {String | Number | function} [left]
* {String | Number | function} [top]
* {String | Number | function} [width]
* {String | Number | function} [height]
* {String | function} [className]
* @constructor ContentPanel
* @extends Panel
*/
function ContentPanel(parent, depends, options) {
this.id = util.randomUUID();
this.parent = parent;
this.depends = depends;
this.options = options || {};
}
ContentPanel.prototype = new Component();
/**
* Set options. Will extend the current options.
* @param {Object} [options] Available parameters:
* {String | function} [className]
* {String | Number | function} [left]
* {String | Number | function} [top]
* {String | Number | function} [width]
* {String | Number | function} [height]
*/
ContentPanel.prototype.setOptions = Component.prototype.setOptions;
/**
* Get the container element of the panel, which can be used by a child to
* add its own widgets.
* @returns {HTMLElement} container
*/
ContentPanel.prototype.getContainer = function () {
return this.frame;
};
/**
* Repaint the component
* @return {Boolean} changed
*/
ContentPanel.prototype.repaint = function () {
var changed = 0,
update = util.updateProperty,
asSize = util.option.asSize,
options = this.options,
frame = this.frame;
if (!frame) {
frame = document.createElement('div');
frame.className = 'content-panel';
var className = options.className;
if (className) {
if (typeof className == 'function') {
util.addClassName(frame, String(className()));
}
else {
util.addClassName(frame, String(className));
}
}
this.frame = frame;
changed += 1;
}
if (!frame.parentNode) {
if (!this.parent) {
throw new Error('Cannot repaint panel: no parent attached');
}
var parentContainer = this.parent.getContainer();
if (!parentContainer) {
throw new Error('Cannot repaint panel: parent has no container element');
}
parentContainer.appendChild(frame);
changed += 1;
}
changed += update(frame.style, 'top', asSize(options.top, '0px'));
changed += update(frame.style, 'left', asSize(options.left, '0px'));
changed += update(frame.style, 'width', asSize(options.width, '100%'));
changed += update(frame.style, 'height', asSize(options.height, '100%'));
return (changed > 0);
};
/**
* Reflow the component
* @return {Boolean} resized
*/
ContentPanel.prototype.reflow = function () {
var changed = 0,
update = util.updateProperty,
frame = this.frame;
if (frame) {
changed += update(this, 'top', frame.offsetTop);
changed += update(this, 'left', frame.offsetLeft);
changed += update(this, 'width', frame.offsetWidth);
changed += update(this, 'height', frame.offsetHeight);
}
else {
changed += 1;
}
return (changed > 0);
};

+ 54
- 59
src/timeline/component/CurrentTime.js View File

@ -1,23 +1,22 @@
/**
* A current time bar
* @param {Component} parent
* @param {Component[]} [depends] Components on which this components depends
* (except for the parent)
* @param {Range} range
* @param {Object} [options] Available parameters:
* {Boolean} [showCurrentTime]
* @constructor CurrentTime
* @extends Component
*/
function CurrentTime (parent, depends, options) {
function CurrentTime (range, options) {
this.id = util.randomUUID();
this.parent = parent;
this.depends = depends;
this.range = range;
this.options = options || {};
this.defaultOptions = {
showCurrentTime: false
};
this._create();
}
CurrentTime.prototype = new Component();
@ -25,77 +24,73 @@ CurrentTime.prototype = new Component();
CurrentTime.prototype.setOptions = Component.prototype.setOptions;
/**
* Get the container element of the bar, which can be used by a child to
* add its own widgets.
* @returns {HTMLElement} container
* Create the HTML DOM for the current time bar
* @private
*/
CurrentTime.prototype.getContainer = function () {
return this.frame;
CurrentTime.prototype._create = function _create () {
var bar = document.createElement('div');
bar.className = 'currenttime';
bar.style.position = 'absolute';
bar.style.top = '0px';
bar.style.height = '100%';
this.bar = bar;
};
/**
* Get the frame element of the current time bar
* @returns {HTMLElement} frame
*/
CurrentTime.prototype.getFrame = function getFrame() {
return this.bar;
};
/**
* Repaint the component
* @return {Boolean} changed
* @return {boolean} Returns true if the component is resized
*/
CurrentTime.prototype.repaint = function () {
var bar = this.frame,
parent = this.parent,
parentContainer = parent.parent.getContainer();
CurrentTime.prototype.repaint = function repaint() {
var parent = this.parent;
if (!parent) {
throw new Error('Cannot repaint bar: no parent attached');
}
var now = new Date();
var x = this.options.toScreen(now);
if (!parentContainer) {
throw new Error('Cannot repaint bar: parent has no container element');
}
this.bar.style.left = x + 'px';
this.bar.title = 'Current time: ' + now;
if (!this.getOption('showCurrentTime')) {
if (bar) {
parentContainer.removeChild(bar);
delete this.frame;
}
return false;
};
return;
}
/**
* Start auto refreshing the current time bar
*/
CurrentTime.prototype.start = function start() {
var me = this;
if (!bar) {
bar = document.createElement('div');
bar.className = 'currenttime';
bar.style.position = 'absolute';
bar.style.top = '0px';
bar.style.height = '100%';
function update () {
me.stop();
parentContainer.appendChild(bar);
this.frame = bar;
}
// determine interval to refresh
var scale = me.range.conversion(me.parent.width).scale;
var interval = 1 / scale / 10;
if (interval < 30) interval = 30;
if (interval > 1000) interval = 1000;
if (!parent.conversion) {
parent._updateConversion();
}
me.repaint();
var now = new Date();
var x = parent.toScreen(now);
// start a timer to adjust for the new time
me.currentTimeTimer = setTimeout(update, interval);
}
bar.style.left = x + 'px';
bar.title = 'Current time: ' + now;
update();
};
// start a timer to adjust for the new time
/**
* Stop auto refreshing the current time bar
*/
CurrentTime.prototype.stop = function stop() {
if (this.currentTimeTimer !== undefined) {
clearTimeout(this.currentTimeTimer);
delete this.currentTimeTimer;
}
var timeline = this;
var interval = 1 / parent.conversion.scale / 2;
if (interval < 30) {
interval = 30;
}
this.currentTimeTimer = setTimeout(function() {
timeline.repaint();
}, interval);
return false;
};

+ 71
- 180
src/timeline/component/CustomTime.js View File

@ -1,26 +1,24 @@
/**
* A custom time bar
* @param {Component} parent
* @param {Component[]} [depends] Components on which this components depends
* (except for the parent)
* @param {Object} [options] Available parameters:
* {Boolean} [showCustomTime]
* @constructor CustomTime
* @extends Component
*/
function CustomTime (parent, depends, options) {
function CustomTime (options) {
this.id = util.randomUUID();
this.parent = parent;
this.depends = depends;
this.options = options || {};
this.defaultOptions = {
showCustomTime: false
};
this.listeners = [];
this.customTime = new Date();
this.eventParams = {}; // stores state parameters while dragging the bar
// create the DOM
this._create();
}
CustomTime.prototype = new Component();
@ -28,70 +26,51 @@ CustomTime.prototype = new Component();
CustomTime.prototype.setOptions = Component.prototype.setOptions;
/**
* Get the container element of the bar, which can be used by a child to
* add its own widgets.
* @returns {HTMLElement} container
* Create the DOM for the custom time
* @private
*/
CustomTime.prototype.getContainer = function () {
return this.frame;
CustomTime.prototype._create = function _create () {
var bar = document.createElement('div');
bar.className = 'customtime';
bar.style.position = 'absolute';
bar.style.top = '0px';
bar.style.height = '100%';
this.bar = bar;
var drag = document.createElement('div');
drag.style.position = 'relative';
drag.style.top = '0px';
drag.style.left = '-10px';
drag.style.height = '100%';
drag.style.width = '20px';
bar.appendChild(drag);
// attach event listeners
this.hammer = Hammer(bar, {
prevent_default: true
});
this.hammer.on('dragstart', this._onDragStart.bind(this));
this.hammer.on('drag', this._onDrag.bind(this));
this.hammer.on('dragend', this._onDragEnd.bind(this));
};
/**
* Get the frame element of the custom time bar
* @returns {HTMLElement} frame
*/
CustomTime.prototype.getFrame = function getFrame() {
return this.bar;
};
/**
* Repaint the component
* @return {Boolean} changed
* @return {boolean} Returns true if the component is resized
*/
CustomTime.prototype.repaint = function () {
var bar = this.frame,
parent = this.parent,
parentContainer = parent.parent.getContainer();
if (!parent) {
throw new Error('Cannot repaint bar: no parent attached');
}
if (!parentContainer) {
throw new Error('Cannot repaint bar: parent has no container element');
}
if (!this.getOption('showCustomTime')) {
if (bar) {
parentContainer.removeChild(bar);
delete this.frame;
}
return;
}
if (!bar) {
bar = document.createElement('div');
bar.className = 'customtime';
bar.style.position = 'absolute';
bar.style.top = '0px';
bar.style.height = '100%';
parentContainer.appendChild(bar);
var drag = document.createElement('div');
drag.style.position = 'relative';
drag.style.top = '0px';
drag.style.left = '-10px';
drag.style.height = '100%';
drag.style.width = '20px';
bar.appendChild(drag);
this.frame = bar;
this.subscribe(this, 'movetime');
}
if (!parent.conversion) {
parent._updateConversion();
}
var x = parent.toScreen(this.customTime);
var x = this.options.toScreen(this.customTime);
bar.style.left = x + 'px';
bar.title = 'Time: ' + this.customTime;
this.bar.style.left = x + 'px';
this.bar.title = 'Time: ' + this.customTime;
return false;
};
@ -100,7 +79,7 @@ CustomTime.prototype.repaint = function () {
* Set custom time.
* @param {Date} time
*/
CustomTime.prototype._setCustomTime = function(time) {
CustomTime.prototype.setCustomTime = function(time) {
this.customTime = new Date(time.valueOf());
this.repaint();
};
@ -109,147 +88,59 @@ CustomTime.prototype._setCustomTime = function(time) {
* Retrieve the current custom time.
* @return {Date} customTime
*/
CustomTime.prototype._getCustomTime = function() {
CustomTime.prototype.getCustomTime = function() {
return new Date(this.customTime.valueOf());
};
/**
* Add listeners for mouse and touch events to the component
* @param {Component} component
*/
CustomTime.prototype.subscribe = function (component, event) {
var me = this;
var listener = {
component: component,
event: event,
callback: function (event) {
me._onMouseDown(event, listener);
},
params: {}
};
component.on('mousedown', listener.callback);
me.listeners.push(listener);
};
/**
* 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.
*/
CustomTime.prototype.on = function (event, callback) {
var bar = this.frame;
if (!bar) {
throw new Error('Cannot add event listener: no parent attached');
}
events.addListener(this, event, callback);
util.addEventListener(bar, event, callback);
};
/**
* Start moving horizontally
* @param {Event} event
* @param {Object} listener Listener containing the component and params
* @private
*/
CustomTime.prototype._onMouseDown = function(event, listener) {
event = event || window.event;
var params = listener.params;
// only react on left mouse button down
var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
if (!leftButtonDown) {
return;
}
// get mouse position
params.mouseX = util.getPageX(event);
params.moved = false;
params.customTime = this.customTime;
// add event listeners to handle moving the custom time bar
var me = this;
if (!params.onMouseMove) {
params.onMouseMove = function (event) {
me._onMouseMove(event, listener);
};
util.addEventListener(document, 'mousemove', params.onMouseMove);
}
if (!params.onMouseUp) {
params.onMouseUp = function (event) {
me._onMouseUp(event, listener);
};
util.addEventListener(document, 'mouseup', params.onMouseUp);
}
util.stopPropagation(event);
util.preventDefault(event);
CustomTime.prototype._onDragStart = function(event) {
this.eventParams.dragging = true;
this.eventParams.customTime = this.customTime;
event.stopPropagation();
event.preventDefault();
};
/**
* Perform moving operating.
* This function activated from within the funcion CustomTime._onMouseDown().
* @param {Event} event
* @param {Object} listener
* @private
*/
CustomTime.prototype._onMouseMove = function (event, listener) {
event = event || window.event;
var params = listener.params;
var parent = this.parent;
// calculate change in mouse position
var mouseX = util.getPageX(event);
CustomTime.prototype._onDrag = function (event) {
if (!this.eventParams.dragging) return;
if (params.mouseX === undefined) {
params.mouseX = mouseX;
}
var deltaX = event.gesture.deltaX,
x = this.options.toScreen(this.eventParams.customTime) + deltaX,
time = this.options.toTime(x);
var diff = mouseX - params.mouseX;
// if mouse movement is big enough, register it as a "moved" event
if (Math.abs(diff) >= 1) {
params.moved = true;
}
var x = parent.toScreen(params.customTime);
var xnew = x + diff;
var time = parent.toTime(xnew);
this._setCustomTime(time);
this.setCustomTime(time);
// fire a timechange event
events.trigger(this, 'timechange', {customTime: this.customTime});
this.emit('timechange', {
time: new Date(this.customTime.valueOf())
});
util.preventDefault(event);
event.stopPropagation();
event.preventDefault();
};
/**
* Stop moving operating.
* This function activated from within the function CustomTime._onMouseDown().
* @param {event} event
* @param {Object} listener
* @private
*/
CustomTime.prototype._onMouseUp = function (event, listener) {
event = event || window.event;
var params = listener.params;
// remove event listeners here, important for Safari
if (params.onMouseMove) {
util.removeEventListener(document, 'mousemove', params.onMouseMove);
params.onMouseMove = null;
}
if (params.onMouseUp) {
util.removeEventListener(document, 'mouseup', params.onMouseUp);
params.onMouseUp = null;
}
if (params.moved) {
// fire a timechanged event
events.trigger(this, 'timechanged', {customTime: this.customTime});
}
CustomTime.prototype._onDragEnd = function (event) {
if (!this.eventParams.dragging) return;
// fire a timechanged event
this.emit('timechanged', {
time: new Date(this.customTime.valueOf())
});
event.stopPropagation();
event.preventDefault();
};

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save