diff --git a/.gitignore b/.gitignore
index b6a487c8..7bd1502b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
.idea
+*.iml
node_modules
.project
.settings/.jsdtscope
diff --git a/.npmignore b/.npmignore
index 648b4367..b453f43f 100644
--- a/.npmignore
+++ b/.npmignore
@@ -1,3 +1,4 @@
+misc
node_modules
src
test
diff --git a/HISTORY.md b/HISTORY.md
index 7b9763cc..94b2007f 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -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.
diff --git a/Jakefile.js b/Jakefile.js
index 4d273152..c85d1b40 100644
--- a/Jakefile.js
+++ b/Jakefile.js
@@ -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);
});
/**
diff --git a/README.md b/README.md
index b6b80643..5aaac946 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/bower.json b/bower.json
index 9ccabd0a..45abe51a 100644
--- a/bower.json
+++ b/bower.json
@@ -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",
diff --git a/dist/img/graph/acceptDeleteIcon.png b/dist/img/graph/acceptDeleteIcon.png
new file mode 100644
index 00000000..02a06285
Binary files /dev/null and b/dist/img/graph/acceptDeleteIcon.png differ
diff --git a/dist/img/graph/addNodeIcon.png b/dist/img/graph/addNodeIcon.png
new file mode 100644
index 00000000..6fa30613
Binary files /dev/null and b/dist/img/graph/addNodeIcon.png differ
diff --git a/dist/img/graph/backIcon.png b/dist/img/graph/backIcon.png
new file mode 100644
index 00000000..e2f99126
Binary files /dev/null and b/dist/img/graph/backIcon.png differ
diff --git a/dist/img/graph/connectIcon.png b/dist/img/graph/connectIcon.png
new file mode 100644
index 00000000..4164da1f
Binary files /dev/null and b/dist/img/graph/connectIcon.png differ
diff --git a/dist/img/graph/cross.png b/dist/img/graph/cross.png
new file mode 100644
index 00000000..9cbd189a
Binary files /dev/null and b/dist/img/graph/cross.png differ
diff --git a/dist/img/graph/cross2.png b/dist/img/graph/cross2.png
new file mode 100644
index 00000000..9fc4b95c
Binary files /dev/null and b/dist/img/graph/cross2.png differ
diff --git a/dist/img/graph/deleteIcon.png b/dist/img/graph/deleteIcon.png
new file mode 100644
index 00000000..54025647
Binary files /dev/null and b/dist/img/graph/deleteIcon.png differ
diff --git a/dist/img/downarrow.png b/dist/img/graph/downArrow.png
similarity index 100%
rename from dist/img/downarrow.png
rename to dist/img/graph/downArrow.png
diff --git a/dist/img/graph/editIcon.png b/dist/img/graph/editIcon.png
new file mode 100644
index 00000000..494d0f00
Binary files /dev/null and b/dist/img/graph/editIcon.png differ
diff --git a/dist/img/leftarrow.png b/dist/img/graph/leftArrow.png
similarity index 100%
rename from dist/img/leftarrow.png
rename to dist/img/graph/leftArrow.png
diff --git a/dist/img/minus.png b/dist/img/graph/minus.png
similarity index 100%
rename from dist/img/minus.png
rename to dist/img/graph/minus.png
diff --git a/dist/img/plus.png b/dist/img/graph/plus.png
similarity index 100%
rename from dist/img/plus.png
rename to dist/img/graph/plus.png
diff --git a/dist/img/rightarrow.png b/dist/img/graph/rightArrow.png
similarity index 100%
rename from dist/img/rightarrow.png
rename to dist/img/graph/rightArrow.png
diff --git a/dist/img/uparrow.png b/dist/img/graph/upArrow.png
similarity index 100%
rename from dist/img/uparrow.png
rename to dist/img/graph/upArrow.png
diff --git a/dist/img/zoomExtends.png b/dist/img/graph/zoomExtends.png
similarity index 100%
rename from dist/img/zoomExtends.png
rename to dist/img/graph/zoomExtends.png
diff --git a/dist/img/timeline/delete.png b/dist/img/timeline/delete.png
new file mode 100644
index 00000000..d54d0e06
Binary files /dev/null and b/dist/img/timeline/delete.png differ
diff --git a/dist/vis.css b/dist/vis.css
index 32bfe22c..46d6b19c 100644
--- a/dist/vis.css
+++ b/dist/vis.css
@@ -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;
+}
diff --git a/dist/vis.js b/dist/vis.js
index 794f3f67..696a8166 100644
--- a/dist/vis.js
+++ b/dist/vis.js
@@ -4,8 +4,8 @@
*
* A dynamic, browser-based visualization library.
*
- * @version 0.4.0
- * @date 2014-01-31
+ * @version 0.7.5-SNAPSHOT
+ * @date 2014-04-22
*
* @license
* Copyright (C) 2011-2014 Almende B.V, http://almende.com
@@ -31,6 +31,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') {
@@ -55,8 +56,6 @@ else {
}
-
-
// Internet Explorer 8 and older does not support Array.indexOf, so we define
// it here in that case.
// http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
@@ -408,6 +407,23 @@ util.extend = function (a, b) {
return a;
};
+/**
+ * Test whether all elements in two arrays are equal.
+ * @param {Array} a
+ * @param {Array} b
+ * @return {boolean} Returns true if both arrays have the same length and same
+ * elements.
+ */
+util.equalArray = function (a, b) {
+ if (a.length != b.length) return false;
+
+ for (var i = 1, len = a.length; i < len; i++) {
+ if (a[i] != b[i]) return false;
+ }
+
+ return true;
+};
+
/**
* Convert an object to another type
* @param {Boolean | Number | String | Date | Moment | Null | undefined} object
@@ -751,6 +767,22 @@ util.forEach = function forEach (object, callback) {
}
};
+/**
+ * Convert an object into an array: all objects properties are put into the
+ * array. The resulting array is unordered.
+ * @param {Object} object
+ * @param {Array} array
+ */
+util.toArray = function toArray(object) {
+ var array = [];
+
+ for (var prop in object) {
+ if (object.hasOwnProperty(prop)) array.push(object[prop]);
+ }
+
+ return array;
+}
+
/**
* Update a property in an object
* @param {Object} object
@@ -758,7 +790,7 @@ util.forEach = function forEach (object, callback) {
* @param {*} value
* @return {Boolean} changed
*/
-util.updateProperty = function updateProp (object, key, value) {
+util.updateProperty = function updateProperty (object, key, value) {
if (object[key] !== value) {
object[key] = value;
return true;
@@ -844,21 +876,6 @@ util.getTarget = function getTarget(event) {
return target;
};
-/**
- * Stop event propagation
- */
-util.stopPropagation = function stopPropagation(event) {
- if (!event)
- event = window.event;
-
- if (event.stopPropagation) {
- event.stopPropagation(); // non-IE browsers
- }
- else {
- event.cancelBubble = true; // IE browsers
- }
-};
-
/**
* Fake a hammer.js gesture. Event can be a ScrollEvent or MouseMoveEvent
* @param {Element} element
@@ -868,28 +885,23 @@ util.fakeGesture = function fakeGesture (element, event) {
var eventType = null;
// for hammer.js 1.0.5
- return Hammer.event.collectEventData(this, eventType, event);
+ var gesture = Hammer.event.collectEventData(this, eventType, event);
// for hammer.js 1.0.6
//var touches = Hammer.event.getTouchList(event, eventType);
- //return Hammer.event.collectEventData(this, eventType, touches, event);
-};
-
-/**
- * Cancels the event if it is cancelable, without stopping further propagation of the event.
- */
-util.preventDefault = function preventDefault (event) {
- if (!event)
- event = window.event;
+ // var gesture = Hammer.event.collectEventData(this, eventType, touches, event);
- if (event.preventDefault) {
- event.preventDefault(); // non-IE browsers
+ // on IE in standards mode, no touches are recognized by hammer.js,
+ // resulting in NaN values for center.pageX and center.pageY
+ if (isNaN(gesture.center.pageX)) {
+ gesture.center.pageX = event.pageX;
}
- else {
- event.returnValue = false; // IE browsers
+ if (isNaN(gesture.center.pageY)) {
+ gesture.center.pageY = event.pageY;
}
-};
+ return gesture;
+};
util.option = {};
@@ -983,208 +995,225 @@ util.option.asElement = function (value, defaultValue) {
return value || defaultValue || null;
};
-/**
- * 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;
- }
+util.GiveDec = function GiveDec(Hex) {
+ var Value;
- // add the callback if it does not yet exist
- if (callbacks.indexOf(callback) == -1) {
- callbacks.push(callback);
- }
- },
+ if (Hex == "A")
+ Value = 10;
+ else if (Hex == "B")
+ Value = 11;
+ else if (Hex == "C")
+ Value = 12;
+ else if (Hex == "D")
+ Value = 13;
+ else if (Hex == "E")
+ Value = 14;
+ else if (Hex == "F")
+ Value = 15;
+ else
+ Value = eval(Hex);
+
+ return Value;
+};
+
+util.GiveHex = function GiveHex(Dec) {
+ var Value;
+
+ if(Dec == 10)
+ Value = "A";
+ else if (Dec == 11)
+ Value = "B";
+ else if (Dec == 12)
+ Value = "C";
+ else if (Dec == 13)
+ Value = "D";
+ else if (Dec == 14)
+ Value = "E";
+ else if (Dec == 15)
+ Value = "F";
+ else
+ Value = "" + Dec;
- /**
- * 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);
- }
+ return Value;
+};
- // remove the array when empty
- if (callbacks.length == 0) {
- delete listener.events[event];
+/**
+ * Parse a color property into an object with border, background, and
+ * highlight colors
+ * @param {Object | String} color
+ * @return {Object} colorObject
+ */
+util.parseColor = function(color) {
+ var c;
+ if (util.isString(color)) {
+ if (util.isValidHex(color)) {
+ var hsv = util.hexToHSV(color);
+ var lighterColorHSV = {h:hsv.h,s:hsv.s * 0.45,v:Math.min(1,hsv.v * 1.05)};
+ var darkerColorHSV = {h:hsv.h,s:Math.min(1,hsv.v * 1.25),v:hsv.v*0.6};
+ var darkerColorHex = util.HSVToHex(darkerColorHSV.h ,darkerColorHSV.h ,darkerColorHSV.v);
+ var lighterColorHex = util.HSVToHex(lighterColorHSV.h,lighterColorHSV.s,lighterColorHSV.v);
+
+ c = {
+ background: color,
+ border:darkerColorHex,
+ highlight: {
+ background:lighterColorHex,
+ border:darkerColorHex
}
- }
-
- // 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++;
+ };
+ }
+ else {
+ c = {
+ background:color,
+ border:color,
+ highlight: {
+ background:color,
+ border:color
}
- }
- if (count == 0) {
- delete this.listeners[index];
- }
+ };
}
- },
-
- /**
- * Remove all registered event listeners
- */
- 'removeAllListeners': function () {
- this.listeners = [];
- },
+ }
+ else {
+ c = {};
+ c.background = color.background || 'white';
+ c.border = color.border || c.background;
- /**
- * 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);
- }
+ 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;
};
/**
- * An event bus can be used to emit events, and to subscribe to events
- * @constructor EventBus
+ * http://www.yellowpipe.com/yis/tools/hex-to-rgb/color-converter.php
+ *
+ * @param {String} hex
+ * @returns {{r: *, g: *, b: *}}
*/
-function EventBus() {
- this.subscriptions = [];
-}
+util.hexToRGB = function hexToRGB(hex) {
+ hex = hex.replace("#","").toUpperCase();
-/**
- * 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
- };
+ var a = util.GiveDec(hex.substring(0, 1));
+ var b = util.GiveDec(hex.substring(1, 2));
+ var c = util.GiveDec(hex.substring(2, 3));
+ var d = util.GiveDec(hex.substring(3, 4));
+ var e = util.GiveDec(hex.substring(4, 5));
+ var f = util.GiveDec(hex.substring(5, 6));
+
+ var r = (a * 16) + b;
+ var g = (c * 16) + d;
+ var b = (e * 16) + f;
+
+ return {r:r,g:g,b:b};
+};
- this.subscriptions.push(subscription);
+util.RGBToHex = function RGBToHex(red,green,blue) {
+ var a = util.GiveHex(Math.floor(red / 16));
+ var b = util.GiveHex(red % 16);
+ var c = util.GiveHex(Math.floor(green / 16));
+ var d = util.GiveHex(green % 16);
+ var e = util.GiveHex(Math.floor(blue / 16));
+ var f = util.GiveHex(blue % 16);
- return subscription.id;
+ var hex = a + b + c + d + e + f;
+ return "#" + hex;
};
+
/**
- * 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.
+ * http://www.javascripter.net/faq/rgb2hsv.htm
+ *
+ * @param red
+ * @param green
+ * @param blue
+ * @returns {*}
+ * @constructor
*/
-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);
- }
+util.RGBToHSV = function RGBToHSV (red,green,blue) {
+ red=red/255; green=green/255; blue=blue/255;
+ var minRGB = Math.min(red,Math.min(green,blue));
+ var maxRGB = Math.max(red,Math.max(green,blue));
- if (match) {
- this.subscriptions.splice(i, 1);
- }
- else {
- i++;
- }
+ // Black-gray-white
+ if (minRGB == maxRGB) {
+ return {h:0,s:0,v:minRGB};
}
+
+ // Colors other than black-gray-white:
+ var d = (red==minRGB) ? green-blue : ((blue==minRGB) ? red-green : blue-red);
+ var h = (red==minRGB) ? 3 : ((blue==minRGB) ? 1 : 5);
+ var hue = 60*(h - d/(maxRGB - minRGB))/360;
+ var saturation = (maxRGB - minRGB)/maxRGB;
+ var value = maxRGB;
+ return {h:hue,s:saturation,v:value};
};
+
/**
- * Emit an event
- * @param {String} event
- * @param {*} [data]
- * @param {*} [source]
+ * https://gist.github.com/mjijackson/5311256
+ * @param hue
+ * @param saturation
+ * @param value
+ * @returns {{r: number, g: number, b: number}}
+ * @constructor
*/
-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);
+util.HSVToRGB = function HSVToRGB(h, s, v) {
+ var r, g, b;
+
+ var i = Math.floor(h * 6);
+ var f = h * 6 - i;
+ var p = v * (1 - s);
+ var q = v * (1 - f * s);
+ var t = v * (1 - (1 - f) * s);
+
+ switch (i % 6) {
+ case 0: r = v, g = t, b = p; break;
+ case 1: r = q, g = v, b = p; break;
+ case 2: r = p, g = v, b = t; break;
+ case 3: r = p, g = q, b = v; break;
+ case 4: r = t, g = p, b = v; break;
+ case 5: r = v, g = p, b = q; break;
+ }
+
+ return {r:Math.floor(r * 255), g:Math.floor(g * 255), b:Math.floor(b * 255) };
+};
+
+util.HSVToHex = function HSVToHex(h, s, v) {
+ var rgb = util.HSVToRGB(h, s, v);
+ return util.RGBToHex(rgb.r, rgb.g, rgb.b);
+};
+
+util.hexToHSV = function hexToHSV(hex) {
+ var rgb = util.hexToRGB(hex);
+ return util.RGBToHSV(rgb.r, rgb.g, rgb.b);
+};
+
+util.isValidHex = function isValidHex(hex) {
+ var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex);
+ return isOk;
+};
+
+util.copyObject = function copyObject(objectFrom, objectTo) {
+ for (var i in objectFrom) {
+ if (objectFrom.hasOwnProperty(i)) {
+ if (typeof objectFrom[i] == "object") {
+ objectTo[i] = {};
+ util.copyObject(objectFrom[i], objectTo[i]);
+ }
+ else {
+ objectTo[i] = objectFrom[i];
}
}
}
@@ -1265,7 +1294,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 = [];
@@ -1277,12 +1306,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) {
@@ -1291,6 +1323,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
@@ -2198,8 +2233,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);
}
}
};
@@ -2405,10 +2440,14 @@ 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;
+
/**
* @constructor TimeStep
* The class TimeStep is an iterator for dates. You provide a start date and an
@@ -2692,35 +2731,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) {
@@ -2728,56 +2770,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;
};
/**
@@ -2859,21 +2903,19 @@ TimeStep.prototype.getLabelMajor = function(date) {
}
};
+// 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);
@@ -2894,143 +2936,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 {
+ marginItem = this.defaultOptions.margin.item
+ }
+ if (options.margin && options.margin.axis !== undefined) {
+ marginAxis = options.margin.axis;
}
else {
- margin = this.defaultOptions.margin.item
+ 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;
};
/**
@@ -3046,8 +3067,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);
};
@@ -3057,19 +3078,41 @@ Stack.prototype.collision = function collision (a, b, margin) {
* 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:
@@ -3100,91 +3143,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]
@@ -3193,8 +3151,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);
}
};
@@ -3359,18 +3321,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';
}
@@ -3379,57 +3342,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;
+
+ // TODO: reckon with option movable
- if (component.frame) {
- component.frame.style.cursor = 'auto';
+ 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;
@@ -3459,47 +3428,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;
+ }
};
/**
- * Handle pinch event
- * @param {Event} event
- * @param {Component} component
- * @param {String} direction 'horizontal' or 'vertical'
+ * On start of a hold gesture
* @private
*/
-Range.prototype._onPinch = function (event, component, direction) {
- touchParams.pinching = true;
+Range.prototype._onHold = function () {
+ touchParams.ignore = true;
+};
+
+/**
+ * Handle pinch event
+ * @param {Event} event
+ * @private
+ */
+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
@@ -3513,21 +3498,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;
}
@@ -3606,202 +3593,29 @@ Range.prototype.moveTo = function(moveTo) {
this.setRange(newStart, newEnd);
};
-/**
- * @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
-};
-
/**
* Prototype for visual components
*/
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]
@@ -3811,10 +3625,7 @@ Component.prototype.setOptions = function setOptions(options) {
if (options) {
util.extend(this.options, options);
- if (this.controller) {
- this.requestRepaint();
- this.requestReflow();
- }
+ this.repaint();
}
};
@@ -3836,29 +3647,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
@@ -3866,73 +3666,22 @@ Component.prototype.repaint = function repaint() {
};
/**
- * Reflow the component
- * @return {Boolean} resized
- */
-Component.prototype.reflow = function reflow() {
- // should be implemented by the component
- return false;
-};
-
-/**
- * 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;
- }
-};
-
-/**
- * Show the component in the DOM (when not already visible).
- * A repaint will be executed when the component is not visible
- * @return {Boolean} changed
+ * 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.show = function show() {
- if (!this.frame || !this.frame.parentNode) {
- return this.repaint();
- }
- else {
- return false;
- }
-};
+Component.prototype._isResized = function _isResized() {
+ var resized = (this._previousWidth !== this.width || this._previousHeight !== this.height);
-/**
- * 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?
- }
-};
+ this._previousWidth = this.width;
+ this._previousHeight = this.height;
-/**
- * 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;
};
/**
* A panel can contain components
- * @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]
@@ -3942,12 +3691,15 @@ Component.prototype.requestReflow = function requestReflow() {
* @constructor Panel
* @extends Component
*/
-function Panel(parent, depends, options) {
+function Panel(options) {
this.id = util.randomUUID();
- this.parent = parent;
- this.depends = depends;
+ this.parent = null;
+ this.childs = [];
this.options = options || {};
+
+ // create frame
+ this.frame = document.createElement('div');
}
Panel.prototype = new Component();
@@ -3964,81 +3716,139 @@ Panel.prototype = new Component();
Panel.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
+ * Get the outer frame of the panel
+ * @returns {HTMLElement} frame
*/
-Panel.prototype.getContainer = function () {
+Panel.prototype.getFrame = function () {
return this.frame;
};
/**
- * Repaint the component
- * @return {Boolean} changed
+ * Append a child to the panel
+ * @param {Component} child
*/
-Panel.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 = 'panel';
+Panel.prototype.appendChild = function (child) {
+ this.childs.push(child);
+ child.parent = this;
+
+ // attach to the DOM
+ var frame = child.getFrame();
+ if (frame) {
+ if (frame.parentNode) {
+ frame.parentNode.removeChild(frame);
+ }
+ this.frame.appendChild(frame);
+ }
+};
+
+/**
+ * Insert a child to the panel
+ * @param {Component} child
+ * @param {Component} beforeChild
+ */
+Panel.prototype.insertBefore = function (child, beforeChild) {
+ var index = this.childs.indexOf(beforeChild);
+ if (index != -1) {
+ this.childs.splice(index, 0, child);
+ child.parent = this;
+
+ // attach to the DOM
+ var frame = child.getFrame();
+ if (frame) {
+ if (frame.parentNode) {
+ frame.parentNode.removeChild(frame);
+ }
- var className = options.className;
- if (className) {
- if (typeof className == 'function') {
- util.addClassName(frame, String(className()));
+ var beforeFrame = beforeChild.getFrame();
+ if (beforeFrame) {
+ this.frame.insertBefore(frame, beforeFrame);
}
else {
- util.addClassName(frame, String(className));
+ this.frame.appendChild(frame);
}
}
-
- 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');
+};
+
+/**
+ * Remove a child from the panel
+ * @param {Component} child
+ */
+Panel.prototype.removeChild = function (child) {
+ var index = this.childs.indexOf(child);
+ if (index != -1) {
+ this.childs.splice(index, 1);
+ child.parent = null;
+
+ // remove from the DOM
+ var frame = child.getFrame();
+ if (frame && frame.parentNode) {
+ this.frame.removeChild(frame);
}
- 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);
+/**
+ * Test whether the panel contains given child
+ * @param {Component} child
+ */
+Panel.prototype.hasChild = function (child) {
+ var index = this.childs.indexOf(child);
+ return (index != -1);
};
/**
- * Reflow the component
- * @return {Boolean} resized
+ * Repaint the component
+ * @return {boolean} Returns true if the component was resized since previous repaint
*/
-Panel.prototype.reflow = function () {
- var changed = 0,
- update = util.updateProperty,
- frame = this.frame;
+Panel.prototype.repaint = function () {
+ var asString = util.option.asString,
+ options = this.options,
+ frame = this.getFrame();
- 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;
+ // update className
+ frame.className = 'vpanel' + (options.className ? (' ' + asString(options.className)) : '');
+
+ // repaint the child components
+ var childsResized = this._repaintChilds();
+
+ // update frame size
+ this._updateSize();
+
+ return this._isResized() || childsResized;
+};
+
+/**
+ * Repaint all childs of the panel
+ * @return {boolean} Returns true if the component is resized
+ * @private
+ */
+Panel.prototype._repaintChilds = function () {
+ var resized = false;
+ for (var i = 0, ii = this.childs.length; i < ii; i++) {
+ resized = this.childs[i].repaint() || resized;
}
+ return resized;
+};
+
+/**
+ * Apply the size from options to the panel, and recalculate it's actual size.
+ * @private
+ */
+Panel.prototype._updateSize = function () {
+ // apply size
+ this.frame.style.top = util.option.asSize(this.options.top);
+ this.frame.style.bottom = util.option.asSize(this.options.bottom);
+ this.frame.style.left = util.option.asSize(this.options.left);
+ this.frame.style.right = util.option.asSize(this.options.right);
+ this.frame.style.width = util.option.asSize(this.options.width, '100%');
+ this.frame.style.height = util.option.asSize(this.options.height, '');
- return (changed > 0);
+ // get actual size
+ this.top = this.frame.offsetTop;
+ this.left = this.frame.offsetLeft;
+ this.width = this.frame.offsetWidth;
+ this.height = this.frame.offsetHeight;
};
/**
@@ -4058,11 +3868,49 @@ function RootPanel(container, options) {
autoResize: true
};
- this.listeners = {}; // event listeners
+ // create the HTML DOM
+ this._create();
+
+ // attach the root panel to the provided container
+ if (!this.container) throw new Error('Cannot repaint root panel: no container attached');
+ this.container.appendChild(this.getFrame());
+
+
+ this._initWatch();
}
RootPanel.prototype = new Panel();
+/**
+ * Create the HTML DOM for the root panel
+ */
+RootPanel.prototype._create = function _create() {
+ // create frame
+ this.frame = document.createElement('div');
+
+ // create event listeners for all interesting events, these events will be
+ // emitted via emitter
+ this.hammer = Hammer(this.frame, {
+ prevent_default: true
+ });
+ this.listeners = {};
+
+ var me = this;
+ var events = [
+ 'touch', 'pinch', 'tap', 'doubletap', 'hold',
+ 'dragstart', 'drag', 'dragend',
+ 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is for Firefox
+ ];
+ events.forEach(function (event) {
+ var listener = function () {
+ var args = [event].concat(Array.prototype.slice.call(arguments, 0));
+ me.emit.apply(me, args);
+ };
+ me.hammer.on(event, listener);
+ me.listeners[event] = listener;
+ });
+};
+
/**
* Set options. Will extend the current options.
* @param {Object} [options] Available parameters:
@@ -4073,78 +3921,53 @@ RootPanel.prototype = new Panel();
* {String | Number | function} [height]
* {Boolean | function} [autoResize]
*/
-RootPanel.prototype.setOptions = Component.prototype.setOptions;
-
-/**
- * Repaint the component
- * @return {Boolean} changed
- */
-RootPanel.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');
-
- this.frame = frame;
+RootPanel.prototype.setOptions = function setOptions(options) {
+ if (options) {
+ util.extend(this.options, options);
- changed += 1;
- }
- if (!frame.parentNode) {
- if (!this.container) {
- throw new Error('Cannot repaint root panel: no container attached');
- }
- this.container.appendChild(frame);
- changed += 1;
- }
+ this.repaint();
- frame.className = 'vis timeline rootpanel ' + options.orientation;
- var className = options.className;
- if (className) {
- util.addClassName(frame, util.option.asString(className));
+ this._initWatch();
}
+};
- 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%'));
-
- this._updateEventEmitters();
- this._updateWatch();
-
- return (changed > 0);
+/**
+ * Get the frame of the root panel
+ */
+RootPanel.prototype.getFrame = function getFrame() {
+ return this.frame;
};
/**
- * Reflow the component
- * @return {Boolean} resized
+ * Repaint the root panel
*/
-RootPanel.prototype.reflow = function () {
- var changed = 0,
- update = util.updateProperty,
- frame = this.frame;
+RootPanel.prototype.repaint = function repaint() {
+ // update class name
+ var options = this.options;
+ var className = 'vis timeline rootpanel ' + options.orientation + (options.editable ? ' editable' : '');
+ if (options.className) className += ' ' + util.option.asString(className);
+ this.frame.className = className;
- 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;
- }
+ // repaint the child components
+ var childsResized = this._repaintChilds();
+
+ // update frame size
+ this.frame.style.maxHeight = util.option.asSize(this.options.maxHeight, '');
+ this._updateSize();
- return (changed > 0);
+ // if the root panel or any of its childs is resized, repaint again,
+ // as other components may need to be resized accordingly
+ var resized = this._isResized() || childsResized;
+ if (resized) {
+ setTimeout(this.repaint.bind(this), 0);
+ }
};
/**
- * Update watching for resize, depending on the current option
+ * Initialize watching when option autoResize is true
* @private
*/
-RootPanel.prototype._updateWatch = function () {
+RootPanel.prototype._initWatch = function _initWatch() {
var autoResize = this.getOption('autoResize');
if (autoResize) {
this._watch();
@@ -4159,12 +3982,12 @@ RootPanel.prototype._updateWatch = function () {
* automatically redraw itself.
* @private
*/
-RootPanel.prototype._watch = function () {
+RootPanel.prototype._watch = function _watch() {
var me = this;
this._unwatch();
- var checkSize = function () {
+ var checkSize = function checkSize() {
var autoResize = me.getOption('autoResize');
if (!autoResize) {
// stop watching when the option autoResize is changed to false
@@ -4174,9 +3997,12 @@ RootPanel.prototype._watch = function () {
if (me.frame) {
// check whether the frame is resized
- if ((me.frame.clientWidth != me.width) ||
- (me.frame.clientHeight != me.height)) {
- me.requestReflow();
+ if ((me.frame.clientWidth != me.lastWidth) ||
+ (me.frame.clientHeight != me.lastHeight)) {
+ me.lastWidth = me.frame.clientWidth;
+ me.lastHeight = me.frame.clientHeight;
+ me.repaint();
+ // TODO: emit a resize event instead?
}
}
};
@@ -4191,7 +4017,7 @@ RootPanel.prototype._watch = function () {
* Stop watching for a resize of the frame.
* @private
*/
-RootPanel.prototype._unwatch = function () {
+RootPanel.prototype._unwatch = function _unwatch() {
if (this.watchTimer) {
clearInterval(this.watchTimer);
this.watchTimer = undefined;
@@ -4201,76 +4027,14 @@ RootPanel.prototype._unwatch = function () {
};
/**
- * 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.
+ * A horizontal time axis
+ * @param {Object} [options] See TimeAxis.setOptions for the available
+ * options.
+ * @constructor TimeAxis
+ * @extends Component
*/
-RootPanel.prototype.on = function (event, callback) {
- // register the listener at this component
- var arr = this.listeners[event];
- if (!arr) {
- arr = [];
- this.listeners[event] = arr;
- }
- arr.push(callback);
-
- this._updateEventEmitters();
-};
-
-/**
- * Update the event listeners for all event emitters
- * @private
- */
-RootPanel.prototype._updateEventEmitters = function () {
- if (this.listeners) {
- var me = this;
- util.forEach(this.listeners, function (listeners, event) {
- if (!me.emitters) {
- me.emitters = {};
- }
- if (!(event in me.emitters)) {
- // create event
- var frame = me.frame;
- if (frame) {
- //console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging
- var callback = function(event) {
- listeners.forEach(function (listener) {
- // TODO: filter on event target!
- listener(event);
- });
- };
- me.emitters[event] = callback;
-
- if (!me.hammer) {
- me.hammer = Hammer(frame, {
- prevent_default: true
- });
- }
- me.hammer.on(event, callback);
- }
- }
- });
-
- // TODO: be able to delete event listeners
- // TODO: be able to move event listeners to a parent when available
- }
-};
-
-/**
- * A horizontal time axis
- * @param {Component} parent
- * @param {Component[]} [depends] Components on which this components depends
- * (except for the parent)
- * @param {Object} [options] See TimeAxis.setOptions for the available
- * options.
- * @constructor TimeAxis
- * @extends Component
- */
-function TimeAxis (parent, depends, options) {
- this.id = util.randomUUID();
- this.parent = parent;
- this.depends = depends;
+function TimeAxis (options) {
+ this.id = util.randomUUID();
this.dom = {
majorLines: [],
@@ -4301,8 +4065,10 @@ function TimeAxis (parent, depends, options) {
showMajorLabels: true
};
- this.conversion = null;
this.range = null;
+
+ // create the HTML DOM
+ this._create();
}
TimeAxis.prototype = new Component();
@@ -4310,6 +4076,13 @@ TimeAxis.prototype = new Component();
// TODO: comment options
TimeAxis.prototype.setOptions = Component.prototype.setOptions;
+/**
+ * Create the HTML DOM for the TimeAxis
+ */
+TimeAxis.prototype._create = function _create() {
+ this.frame = document.createElement('div');
+};
+
/**
* Set a range (start and end)
* @param {Range | Object} range A Range or an object containing start and end.
@@ -4323,126 +4096,70 @@ TimeAxis.prototype.setRange = function (range) {
};
/**
- * 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
- */
-TimeAxis.prototype.toTime = function(x) {
- var conversion = this.conversion;
- 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
+ * Get the outer frame of the time axis
+ * @return {HTMLElement} frame
*/
-TimeAxis.prototype.toScreen = function(time) {
- var conversion = this.conversion;
- return (time.valueOf() - conversion.offset) * conversion.scale;
+TimeAxis.prototype.getFrame = function getFrame() {
+ return this.frame;
};
/**
* Repaint the component
- * @return {Boolean} changed
+ * @return {boolean} Returns true if the component is resized
*/
TimeAxis.prototype.repaint = function () {
- var changed = 0,
- update = util.updateProperty,
- asSize = util.option.asSize,
+ var asSize = util.option.asSize,
options = this.options,
- orientation = this.getOption('orientation'),
props = this.props,
- step = this.step;
-
- var frame = this.frame;
- if (!frame) {
- frame = document.createElement('div');
- this.frame = frame;
- changed += 1;
- }
- frame.className = 'axis';
- // TODO: custom className?
-
- if (!frame.parentNode) {
- if (!this.parent) {
- throw new Error('Cannot repaint time axis: no parent attached');
- }
- var parentContainer = this.parent.getContainer();
- if (!parentContainer) {
- throw new Error('Cannot repaint time axis: parent has no container element');
- }
- parentContainer.appendChild(frame);
+ frame = this.frame;
- changed += 1;
- }
+ // update classname
+ frame.className = 'timeaxis'; // TODO: add className from options if defined
var parent = frame.parentNode;
if (parent) {
- var beforeChild = frame.nextSibling;
- parent.removeChild(frame); // take frame offline while updating (is almost twice as fast)
-
- var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ?
- (this.props.parentHeight - this.height) + 'px' :
- '0px';
- changed += update(frame.style, 'top', asSize(options.top, defaultTop));
- 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, this.height + 'px'));
-
- // get characters width and height
- this._repaintMeasureChars();
-
- if (this.step) {
- this._repaintStart();
+ // calculate character width and height
+ this._calculateCharSize();
- step.first();
- var xFirstMajorLabel = undefined;
- var max = 0;
- while (step.hasNext() && max < 1000) {
- max++;
- var cur = step.getCurrent(),
- x = this.toScreen(cur),
- isMajor = step.isMajor();
-
- // TODO: lines must have a width, such that we can create css backgrounds
-
- if (this.getOption('showMinorLabels')) {
- this._repaintMinorText(x, step.getLabelMinor());
- }
-
- if (isMajor && this.getOption('showMajorLabels')) {
- if (x > 0) {
- if (xFirstMajorLabel == undefined) {
- xFirstMajorLabel = x;
- }
- this._repaintMajorText(x, step.getLabelMajor());
- }
- this._repaintMajorLine(x);
- }
- else {
- this._repaintMinorLine(x);
- }
+ // TODO: recalculate sizes only needed when parent is resized or options is changed
+ var orientation = this.getOption('orientation'),
+ showMinorLabels = this.getOption('showMinorLabels'),
+ showMajorLabels = this.getOption('showMajorLabels');
- step.next();
- }
+ // determine the width and height of the elemens for the axis
+ var parentHeight = this.parent.height;
+ props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
+ props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
+ this.height = props.minorLabelHeight + props.majorLabelHeight;
+ this.width = frame.offsetWidth; // TODO: only update the width when the frame is resized?
- // create a major label on the left when needed
- if (this.getOption('showMajorLabels')) {
- var leftTime = this.toTime(0),
- leftText = step.getLabelMajor(leftTime),
- widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation
+ props.minorLineHeight = parentHeight + props.minorLabelHeight;
+ props.minorLineWidth = 1; // TODO: really calculate width
+ props.majorLineHeight = parentHeight + this.height;
+ props.majorLineWidth = 1; // TODO: really calculate width
- if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
- this._repaintMajorText(0, leftText);
- }
- }
+ // take frame offline while updating (is almost twice as fast)
+ var beforeChild = frame.nextSibling;
+ parent.removeChild(frame);
- this._repaintEnd();
+ // TODO: top/bottom positioning should be determined by options set in the Timeline, not here
+ if (orientation == 'top') {
+ frame.style.top = '0';
+ frame.style.left = '0';
+ frame.style.bottom = '';
+ frame.style.width = asSize(options.width, '100%');
+ frame.style.height = this.height + 'px';
+ }
+ else { // bottom
+ frame.style.top = '';
+ frame.style.bottom = '0';
+ frame.style.left = '0';
+ frame.style.width = asSize(options.width, '100%');
+ frame.style.height = this.height + 'px';
}
+ this._repaintLabels();
+
this._repaintLine();
// put frame online again
@@ -4454,34 +4171,80 @@ TimeAxis.prototype.repaint = function () {
}
}
- return (changed > 0);
+ return this._isResized();
};
/**
- * Start a repaint. Move all DOM elements to a redundant list, where they
- * can be picked for re-use, or can be cleaned up in the end
+ * Repaint major and minor text labels and vertical grid lines
* @private
*/
-TimeAxis.prototype._repaintStart = function () {
- var dom = this.dom,
- redundant = dom.redundant;
+TimeAxis.prototype._repaintLabels = function () {
+ var orientation = this.getOption('orientation');
- redundant.majorLines = dom.majorLines;
- redundant.majorTexts = dom.majorTexts;
- redundant.minorLines = dom.minorLines;
- redundant.minorTexts = dom.minorTexts;
+ // calculate range and step
+ var start = util.convert(this.range.start, 'Number'),
+ end = util.convert(this.range.end, 'Number'),
+ minimumStep = this.options.toTime((this.props.minorCharWidth || 10) * 5).valueOf()
+ -this.options.toTime(0).valueOf();
+ var step = new TimeStep(new Date(start), new Date(end), minimumStep);
+ this.step = step;
+ // Move all DOM elements to a "redundant" list, where they
+ // can be picked for re-use, and clear the lists with lines and texts.
+ // At the end of the function _repaintLabels, left over elements will be cleaned up
+ var dom = this.dom;
+ dom.redundant.majorLines = dom.majorLines;
+ dom.redundant.majorTexts = dom.majorTexts;
+ dom.redundant.minorLines = dom.minorLines;
+ dom.redundant.minorTexts = dom.minorTexts;
dom.majorLines = [];
dom.majorTexts = [];
dom.minorLines = [];
dom.minorTexts = [];
-};
-/**
- * End a repaint. Cleanup leftover DOM elements in the redundant list
- * @private
- */
-TimeAxis.prototype._repaintEnd = function () {
+ step.first();
+ var xFirstMajorLabel = undefined;
+ var max = 0;
+ while (step.hasNext() && max < 1000) {
+ max++;
+ var cur = step.getCurrent(),
+ x = this.options.toScreen(cur),
+ isMajor = step.isMajor();
+
+ // TODO: lines must have a width, such that we can create css backgrounds
+
+ if (this.getOption('showMinorLabels')) {
+ this._repaintMinorText(x, step.getLabelMinor(), orientation);
+ }
+
+ if (isMajor && this.getOption('showMajorLabels')) {
+ if (x > 0) {
+ if (xFirstMajorLabel == undefined) {
+ xFirstMajorLabel = x;
+ }
+ this._repaintMajorText(x, step.getLabelMajor(), orientation);
+ }
+ this._repaintMajorLine(x, orientation);
+ }
+ else {
+ this._repaintMinorLine(x, orientation);
+ }
+
+ step.next();
+ }
+
+ // create a major label on the left when needed
+ if (this.getOption('showMajorLabels')) {
+ var leftTime = this.options.toTime(0),
+ leftText = step.getLabelMajor(leftTime),
+ widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation
+
+ if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
+ this._repaintMajorText(0, leftText, orientation);
+ }
+ }
+
+ // Cleanup leftover DOM elements from the redundant list
util.forEach(this.dom.redundant, function (arr) {
while (arr.length) {
var elem = arr.pop();
@@ -4492,14 +4255,14 @@ TimeAxis.prototype._repaintEnd = function () {
});
};
-
/**
* Create a minor label for the axis at position x
* @param {Number} x
* @param {String} text
+ * @param {String} orientation "top" or "bottom" (default)
* @private
*/
-TimeAxis.prototype._repaintMinorText = function (x, text) {
+TimeAxis.prototype._repaintMinorText = function (x, text, orientation) {
// reuse redundant label
var label = this.dom.redundant.minorTexts.shift();
@@ -4514,8 +4277,16 @@ TimeAxis.prototype._repaintMinorText = function (x, text) {
this.dom.minorTexts.push(label);
label.childNodes[0].nodeValue = text;
+
+ if (orientation == 'top') {
+ label.style.top = this.props.majorLabelHeight + 'px';
+ label.style.bottom = '';
+ }
+ else {
+ label.style.top = '';
+ label.style.bottom = this.props.majorLabelHeight + 'px';
+ }
label.style.left = x + 'px';
- label.style.top = this.props.minorLabelTop + 'px';
//label.title = title; // TODO: this is a heavy operation
};
@@ -4523,9 +4294,10 @@ TimeAxis.prototype._repaintMinorText = function (x, text) {
* Create a Major label for the axis at position x
* @param {Number} x
* @param {String} text
+ * @param {String} orientation "top" or "bottom" (default)
* @private
*/
-TimeAxis.prototype._repaintMajorText = function (x, text) {
+TimeAxis.prototype._repaintMajorText = function (x, text, orientation) {
// reuse redundant label
var label = this.dom.redundant.majorTexts.shift();
@@ -4540,17 +4312,26 @@ TimeAxis.prototype._repaintMajorText = function (x, text) {
this.dom.majorTexts.push(label);
label.childNodes[0].nodeValue = text;
- label.style.top = this.props.majorLabelTop + 'px';
- label.style.left = x + 'px';
//label.title = title; // TODO: this is a heavy operation
+
+ if (orientation == 'top') {
+ label.style.top = '0px';
+ label.style.bottom = '';
+ }
+ else {
+ label.style.top = '';
+ label.style.bottom = '0px';
+ }
+ label.style.left = x + 'px';
};
/**
* Create a minor line for the axis at position x
* @param {Number} x
+ * @param {String} orientation "top" or "bottom" (default)
* @private
*/
-TimeAxis.prototype._repaintMinorLine = function (x) {
+TimeAxis.prototype._repaintMinorLine = function (x, orientation) {
// reuse redundant line
var line = this.dom.redundant.minorLines.shift();
@@ -4563,7 +4344,14 @@ TimeAxis.prototype._repaintMinorLine = function (x) {
this.dom.minorLines.push(line);
var props = this.props;
- line.style.top = props.minorLineTop + 'px';
+ if (orientation == 'top') {
+ line.style.top = this.props.majorLabelHeight + 'px';
+ line.style.bottom = '';
+ }
+ else {
+ line.style.top = '';
+ line.style.bottom = this.props.majorLabelHeight + 'px';
+ }
line.style.height = props.minorLineHeight + 'px';
line.style.left = (x - props.minorLineWidth / 2) + 'px';
};
@@ -4571,9 +4359,10 @@ TimeAxis.prototype._repaintMinorLine = function (x) {
/**
* Create a Major line for the axis at position x
* @param {Number} x
+ * @param {String} orientation "top" or "bottom" (default)
* @private
*/
-TimeAxis.prototype._repaintMajorLine = function (x) {
+TimeAxis.prototype._repaintMajorLine = function (x, orientation) {
// reuse redundant line
var line = this.dom.redundant.majorLines.shift();
@@ -4586,7 +4375,14 @@ TimeAxis.prototype._repaintMajorLine = function (x) {
this.dom.majorLines.push(line);
var props = this.props;
- line.style.top = props.majorLineTop + 'px';
+ if (orientation == 'top') {
+ line.style.top = '0px';
+ line.style.bottom = '';
+ }
+ else {
+ line.style.top = '';
+ line.style.bottom = '0px';
+ }
line.style.left = (x - props.majorLineWidth / 2) + 'px';
line.style.height = props.majorLineHeight + 'px';
};
@@ -4599,7 +4395,7 @@ TimeAxis.prototype._repaintMajorLine = function (x) {
TimeAxis.prototype._repaintLine = function() {
var line = this.dom.line,
frame = this.frame,
- options = this.options;
+ orientation = this.getOption('orientation');
// line before all axis elements
if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
@@ -4616,190 +4412,86 @@ TimeAxis.prototype._repaintLine = function() {
this.dom.line = line;
}
- line.style.top = this.props.lineTop + 'px';
+ if (orientation == 'top') {
+ line.style.top = this.height + 'px';
+ line.style.bottom = '';
+ }
+ else {
+ line.style.top = '';
+ line.style.bottom = this.height + 'px';
+ }
}
else {
- if (line && line.parentElement) {
- frame.removeChild(line.line);
+ if (line && line.parentNode) {
+ line.parentNode.removeChild(line);
delete this.dom.line;
}
}
};
/**
- * Create characters used to determine the size of text on the axis
+ * Determine the size of text on the axis (both major and minor axis).
+ * The size is calculated only once and then cached in this.props.
* @private
*/
-TimeAxis.prototype._repaintMeasureChars = function () {
- // calculate the width and height of a single character
- // this is used to calculate the step size, and also the positioning of the
- // axis
- var dom = this.dom,
- text;
-
- if (!dom.measureCharMinor) {
- text = document.createTextNode('0');
+TimeAxis.prototype._calculateCharSize = function () {
+ // determine the char width and height on the minor axis
+ if (!('minorCharHeight' in this.props)) {
+ var textMinor = document.createTextNode('0');
var measureCharMinor = document.createElement('DIV');
measureCharMinor.className = 'text minor measure';
- measureCharMinor.appendChild(text);
+ measureCharMinor.appendChild(textMinor);
this.frame.appendChild(measureCharMinor);
- dom.measureCharMinor = measureCharMinor;
+ this.props.minorCharHeight = measureCharMinor.clientHeight;
+ this.props.minorCharWidth = measureCharMinor.clientWidth;
+
+ this.frame.removeChild(measureCharMinor);
}
- if (!dom.measureCharMajor) {
- text = document.createTextNode('0');
+ if (!('majorCharHeight' in this.props)) {
+ var textMajor = document.createTextNode('0');
var measureCharMajor = document.createElement('DIV');
measureCharMajor.className = 'text major measure';
- measureCharMajor.appendChild(text);
+ measureCharMajor.appendChild(textMajor);
this.frame.appendChild(measureCharMajor);
- dom.measureCharMajor = measureCharMajor;
- }
-};
-
-/**
- * Reflow the component
- * @return {Boolean} resized
- */
-TimeAxis.prototype.reflow = function () {
- var changed = 0,
- update = util.updateProperty,
- frame = this.frame,
- range = this.range;
-
- if (!range) {
- throw new Error('Cannot repaint time axis: no range configured');
- }
-
- if (frame) {
- changed += update(this, 'top', frame.offsetTop);
- changed += update(this, 'left', frame.offsetLeft);
-
- // calculate size of a character
- var props = this.props,
- showMinorLabels = this.getOption('showMinorLabels'),
- showMajorLabels = this.getOption('showMajorLabels'),
- measureCharMinor = this.dom.measureCharMinor,
- measureCharMajor = this.dom.measureCharMajor;
- if (measureCharMinor) {
- props.minorCharHeight = measureCharMinor.clientHeight;
- props.minorCharWidth = measureCharMinor.clientWidth;
- }
- if (measureCharMajor) {
- props.majorCharHeight = measureCharMajor.clientHeight;
- props.majorCharWidth = measureCharMajor.clientWidth;
- }
-
- var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0;
- if (parentHeight != props.parentHeight) {
- props.parentHeight = parentHeight;
- changed += 1;
- }
- switch (this.getOption('orientation')) {
- case 'bottom':
- props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
- props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
-
- props.minorLabelTop = 0;
- props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight;
-
- props.minorLineTop = -this.top;
- props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0);
- props.minorLineWidth = 1; // TODO: really calculate width
-
- props.majorLineTop = -this.top;
- props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0);
- props.majorLineWidth = 1; // TODO: really calculate width
+ this.props.majorCharHeight = measureCharMajor.clientHeight;
+ this.props.majorCharWidth = measureCharMajor.clientWidth;
- props.lineTop = 0;
-
- break;
-
- case 'top':
- props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
- props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
-
- props.majorLabelTop = 0;
- props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight;
-
- props.minorLineTop = props.minorLabelTop;
- props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top);
- props.minorLineWidth = 1; // TODO: really calculate width
-
- props.majorLineTop = 0;
- props.majorLineHeight = Math.max(parentHeight - this.top);
- props.majorLineWidth = 1; // TODO: really calculate width
-
- props.lineTop = props.majorLabelHeight + props.minorLabelHeight;
-
- break;
-
- default:
- throw new Error('Unkown orientation "' + this.getOption('orientation') + '"');
- }
-
- var height = props.minorLabelHeight + props.majorLabelHeight;
- changed += update(this, 'width', frame.offsetWidth);
- changed += update(this, 'height', height);
-
- // calculate range and step
- this._updateConversion();
-
- var start = util.convert(range.start, 'Number'),
- end = util.convert(range.end, 'Number'),
- minimumStep = this.toTime((props.minorCharWidth || 10) * 5).valueOf()
- -this.toTime(0).valueOf();
- this.step = new TimeStep(new Date(start), new Date(end), minimumStep);
- changed += update(props.range, 'start', start);
- changed += update(props.range, 'end', end);
- changed += update(props.range, 'minimumStep', minimumStep.valueOf());
+ this.frame.removeChild(measureCharMajor);
}
-
- return (changed > 0);
};
/**
- * Calculate the scale and offset to convert a position on screen to the
- * corresponding date and vice versa.
- * After the method _updateConversion is executed once, the methods toTime
- * and toScreen can be used.
- * @private
+ * 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
*/
-TimeAxis.prototype._updateConversion = function() {
- var range = this.range;
- if (!range) {
- throw new Error('No range configured');
- }
-
- if (range.conversion) {
- this.conversion = range.conversion(this.width);
- }
- else {
- this.conversion = Range.conversion(range.start, range.end, this.width);
- }
+TimeAxis.prototype.snap = function snap (date) {
+ return this.step.snap(date);
};
/**
* 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();
@@ -4807,104 +4499,98 @@ 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;
};
/**
* 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();
@@ -4912,70 +4598,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;
- }
+ var x = this.options.toScreen(this.customTime);
- 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);
-
- bar.style.left = x + 'px';
- bar.title = 'Time: ' + this.customTime;
+ this.bar.style.left = x + 'px';
+ this.bar.title = 'Time: ' + this.customTime;
return false;
};
@@ -4984,7 +4651,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();
};
@@ -4993,213 +4660,120 @@ 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.
+ * Start moving horizontally
+ * @param {Event} event
+ * @private
*/
-CustomTime.prototype.on = function (event, callback) {
- var bar = this.frame;
- if (!bar) {
- throw new Error('Cannot add event listener: no parent attached');
- }
+CustomTime.prototype._onDragStart = function(event) {
+ this.eventParams.dragging = true;
+ this.eventParams.customTime = this.customTime;
- events.addListener(this, event, callback);
- util.addEventListener(bar, event, callback);
+ event.stopPropagation();
+ event.preventDefault();
};
/**
- * Start moving horizontally
+ * Perform moving operating.
* @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;
+CustomTime.prototype._onDrag = function (event) {
+ if (!this.eventParams.dragging) return;
- // only react on left mouse button down
- var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
- if (!leftButtonDown) {
- return;
- }
+ var deltaX = event.gesture.deltaX,
+ x = this.options.toScreen(this.eventParams.customTime) + deltaX,
+ time = this.options.toTime(x);
- // get mouse position
- params.mouseX = util.getPageX(event);
- params.moved = false;
+ this.setCustomTime(time);
- params.customTime = this.customTime;
+ // fire a timechange event
+ this.emit('timechange', {
+ time: new Date(this.customTime.valueOf())
+ });
- // 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);
-};
-
-/**
- * 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);
-
- if (params.mouseX === undefined) {
- params.mouseX = mouseX;
- }
-
- 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);
-
- // fire a timechange event
- events.trigger(this, 'timechange', {customTime: this.customTime});
-
- 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;
+CustomTime.prototype._onDragEnd = function (event) {
+ if (!this.eventParams.dragging) return;
- // 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;
- }
+ // fire a timechanged event
+ this.emit('timechanged', {
+ time: new Date(this.customTime.valueOf())
+ });
- if (params.moved) {
- // fire a timechanged event
- events.trigger(this, 'timechanged', {customTime: this.customTime});
- }
+ event.stopPropagation();
+ event.preventDefault();
};
/**
* An ItemSet holds a set of items and ranges which can be displayed in a
* range. The width is determined by the parent of the ItemSet, and the height
* is determined by the size of the items.
- * @param {Component} parent
- * @param {Component[]} [depends] Components on which this components depends
- * (except for the parent)
- * @param {Object} [options] See ItemSet.setOptions for the available
- * options.
+ * @param {Panel} backgroundPanel Panel which can be used to display the
+ * vertical lines of box items.
+ * @param {Panel} axisPanel Panel on the axis where the dots of box-items
+ * can be displayed.
+ * @param {Object} [options] See ItemSet.setOptions for the available options.
* @constructor ItemSet
* @extends Panel
*/
-// TODO: improve performance by replacing all Array.forEach with a for loop
-function ItemSet(parent, depends, options) {
+function ItemSet(backgroundPanel, axisPanel, options) {
this.id = util.randomUUID();
- this.parent = parent;
- this.depends = depends;
// one options object is shared by this itemset and all its items
this.options = options || {};
- this.defaultOptions = {
- type: 'box',
- align: 'center',
- orientation: 'bottom',
- margin: {
- axis: 20,
- item: 10
- },
- padding: 5
- };
-
+ this.backgroundPanel = backgroundPanel;
+ this.axisPanel = axisPanel;
+ this.itemOptions = Object.create(this.options);
this.dom = {};
+ this.hammer = null;
var me = this;
this.itemsData = null; // DataSet
this.range = null; // Range or Object {start: number, end: number}
+ // data change listeners
this.listeners = {
'add': function (event, params, senderId) {
- if (senderId != me.id) {
- me._onAdd(params.items);
- }
+ if (senderId != me.id) me._onAdd(params.items);
},
'update': function (event, params, senderId) {
- if (senderId != me.id) {
- me._onUpdate(params.items);
- }
+ if (senderId != me.id) me._onUpdate(params.items);
},
'remove': function (event, params, senderId) {
- if (senderId != me.id) {
- me._onRemove(params.items);
- }
+ if (senderId != me.id) me._onRemove(params.items);
}
};
- this.items = {}; // object with an Item for every data item
+ this.items = {}; // object with an Item for every data item
+ this.orderedItems = {
+ byStart: [],
+ byEnd: []
+ };
+ this.visibleItems = []; // visible, ordered items
+ this.visibleItemsStart = 0; // start index of visible items in this.orderedItems // TODO: cleanup
+ this.visibleItemsEnd = 0; // start index of visible items in this.orderedItems // TODO: cleanup
this.selection = []; // list with the ids of all selected nodes
this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
- this.stack = new Stack(this, Object.create(this.options));
- this.conversion = null;
+ this.stack = new Stack(Object.create(this.options));
+ this.stackDirty = true; // if true, all items will be restacked on next repaint
- // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis
+ this.touchParams = {}; // stores properties while dragging
+
+ // create the HTML DOM
+ this._create();
}
ItemSet.prototype = new Panel();
@@ -5212,6 +4786,42 @@ ItemSet.types = {
point: ItemPoint
};
+/**
+ * Create the HTML DOM for the ItemSet
+ */
+ItemSet.prototype._create = function _create(){
+ var frame = document.createElement('div');
+ frame['timeline-itemset'] = this;
+ this.frame = frame;
+
+ // create background panel
+ var background = document.createElement('div');
+ background.className = 'background';
+ this.backgroundPanel.frame.appendChild(background);
+ this.dom.background = background;
+
+ // create foreground panel
+ var foreground = document.createElement('div');
+ foreground.className = 'foreground';
+ frame.appendChild(foreground);
+ this.dom.foreground = foreground;
+
+ // create axis panel
+ var axis = document.createElement('div');
+ axis.className = 'axis';
+ this.dom.axis = axis;
+ this.axisPanel.frame.appendChild(axis);
+
+ // attach event listeners
+ // TODO: use event listeners from the rootpanel to improve performance?
+ this.hammer = Hammer(frame, {
+ 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));
+};
+
/**
* Set options for the ItemSet. Existing options will be extended/overwritten.
* @param {Object} [options] The following options are available:
@@ -5236,9 +4846,43 @@ ItemSet.types = {
* {Number} padding
* Padding of the contents of an item in pixels.
* Must correspond with the items css. Default is 5.
+ * {Function} snap
+ * Function to let items snap to nice dates when
+ * dragging items.
*/
ItemSet.prototype.setOptions = Component.prototype.setOptions;
+/**
+ * Hide the component from the DOM
+ */
+ItemSet.prototype.hide = function hide() {
+ // remove the axis with dots
+ if (this.dom.axis.parentNode) {
+ this.dom.axis.parentNode.removeChild(this.dom.axis);
+ }
+
+ // remove the background with vertical lines
+ if (this.dom.background.parentNode) {
+ this.dom.background.parentNode.removeChild(this.dom.background);
+ }
+};
+
+/**
+ * Show the component in the DOM (when not already visible).
+ * @return {Boolean} changed
+ */
+ItemSet.prototype.show = function show() {
+ // show axis with dots
+ if (!this.dom.axis.parentNode) {
+ this.axisPanel.frame.appendChild(this.dom.axis);
+ }
+
+ // show background with vertical lines
+ if (!this.dom.background.parentNode) {
+ this.backgroundPanel.frame.appendChild(this.dom.background);
+ }
+};
+
/**
* Set range (start and end).
* @param {Range | Object} range A Range or an object containing start and end.
@@ -5259,7 +4903,7 @@ ItemSet.prototype.setRange = function setRange(range) {
* unselected.
*/
ItemSet.prototype.setSelection = function setSelection(ids) {
- var i, ii, id, item, selection;
+ var i, ii, id, item;
if (ids) {
if (!Array.isArray(ids)) {
@@ -5283,16 +4927,6 @@ ItemSet.prototype.setSelection = function setSelection(ids) {
item.select();
}
}
-
- // trigger a select event
- selection = this.selection.concat([]);
- events.trigger(this, 'select', {
- ids: selection
- });
-
- if (this.controller) {
- this.requestRepaint();
- }
}
};
@@ -5319,183 +4953,151 @@ ItemSet.prototype._deselect = function _deselect(id) {
}
};
+/**
+ * Return the item sets frame
+ * @returns {HTMLElement} frame
+ */
+ItemSet.prototype.getFrame = function getFrame() {
+ return this.frame;
+};
+
/**
* Repaint the component
- * @return {Boolean} changed
+ * @return {boolean} Returns true if the component is resized
*/
ItemSet.prototype.repaint = function repaint() {
- var changed = 0,
- update = util.updateProperty,
- asSize = util.option.asSize,
+ var asSize = util.option.asSize,
+ asString = util.option.asString,
options = this.options,
orientation = this.getOption('orientation'),
- defaultOptions = this.defaultOptions,
frame = this.frame;
- if (!frame) {
- frame = document.createElement('div');
- frame.className = 'itemset';
-
- var className = options.className;
- if (className) {
- util.addClassName(frame, util.option.asString(className));
- }
-
- // create background panel
- var background = document.createElement('div');
- background.className = 'background';
- frame.appendChild(background);
- this.dom.background = background;
+ // update className
+ frame.className = 'itemset' + (options.className ? (' ' + asString(options.className)) : '');
- // create foreground panel
- var foreground = document.createElement('div');
- foreground.className = 'foreground';
- frame.appendChild(foreground);
- this.dom.foreground = foreground;
+ // check whether zoomed (in that case we need to re-stack everything)
+ var visibleInterval = this.range.end - this.range.start;
+ var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
+ this.lastVisibleInterval = visibleInterval;
+ this.lastWidth = this.width;
- // create axis panel
- var axis = document.createElement('div');
- axis.className = 'itemset-axis';
- //frame.appendChild(axis);
- this.dom.axis = axis;
-
- this.frame = frame;
- changed += 1;
+ /* TODO: implement+fix smarter way to update visible items
+ // find the first visible item
+ // TODO: use faster search, not linear
+ var byEnd = this.orderedItems.byEnd;
+ var start = 0;
+ var item = null;
+ while ((item = byEnd[start]) &&
+ (('end' in item.data) ? item.data.end : item.data.start) < this.range.start) {
+ start++;
}
- if (!this.parent) {
- throw new Error('Cannot repaint itemset: no parent attached');
- }
- var parentContainer = this.parent.getContainer();
- if (!parentContainer) {
- throw new Error('Cannot repaint itemset: parent has no container element');
- }
- if (!frame.parentNode) {
- parentContainer.appendChild(frame);
- changed += 1;
- }
- if (!this.dom.axis.parentNode) {
- parentContainer.appendChild(this.dom.axis);
- changed += 1;
+ // find the last visible item
+ // TODO: use faster search, not linear
+ var byStart = this.orderedItems.byStart;
+ var end = 0;
+ while ((item = byStart[end]) && item.data.start < this.range.end) {
+ end++;
}
- // reposition frame
- changed += update(frame.style, 'left', asSize(options.left, '0px'));
- changed += update(frame.style, 'top', asSize(options.top, '0px'));
- changed += update(frame.style, 'width', asSize(options.width, '100%'));
- changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
+ console.log('visible items', start, end); // TODO: cleanup
+ console.log('visible item ids', byStart[start] && byStart[start].id, byEnd[end-1] && byEnd[end-1].id); // TODO: cleanup
- // reposition axis
- changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px'));
- changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%'));
- if (orientation == 'bottom') {
- changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px');
+ this.visibleItems = [];
+ var i = start;
+ item = byStart[i];
+ var lastItem = byEnd[end];
+ while (item && item !== lastItem) {
+ this.visibleItems.push(item);
+ item = byStart[++i];
}
- else { // orientation == 'top'
- changed += update(this.dom.axis.style, 'top', this.top + 'px');
- }
-
- this._updateConversion();
-
- var me = this,
- queue = this.queue,
- itemsData = this.itemsData,
- items = this.items,
- dataOptions = {
- // TODO: cleanup
- // fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type', 'className']
- };
+ this.stack.order(this.visibleItems);
- // show/hide added/changed/removed items
- for (var id in queue) {
- if (queue.hasOwnProperty(id)) {
- var entry = queue[id],
- item = items[id],
- action = entry.action;
-
- //noinspection FallthroughInSwitchStatementJS
- switch (action) {
- case 'add':
- case 'update':
- var itemData = itemsData && itemsData.get(id, dataOptions);
-
- if (itemData) {
- var type = itemData.type ||
- (itemData.start && itemData.end && 'range') ||
- options.type ||
- 'box';
- var constructor = ItemSet.types[type];
-
- // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
- if (item) {
- // update item
- if (!constructor || !(item instanceof constructor)) {
- // item type has changed, hide and delete the item
- changed += item.hide();
- item = null;
- }
- else {
- item.data = itemData; // TODO: create a method item.setData ?
- changed++;
- }
- }
+ // show visible items
+ for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
+ item = this.visibleItems[i];
- if (!item) {
- // create item
- if (constructor) {
- item = new constructor(me, itemData, options, defaultOptions);
- item.id = entry.id; // we take entry.id, as id itself is stringified
- changed++;
- }
- else {
- throw new TypeError('Unknown item type "' + type + '"');
- }
- }
+ if (!item.displayed) item.show();
+ item.top = null; // reset stacking position
- // force a repaint (not only a reposition)
- item.repaint();
+ // reposition item horizontally
+ item.repositionX();
+ }
+ */
- items[id] = item;
- }
+ // simple, brute force calculation of visible items
+ // TODO: replace with a faster, more sophisticated solution
+ this.visibleItems = [];
+ for (var id in this.items) {
+ if (this.items.hasOwnProperty(id)) {
+ var item = this.items[id];
+ if (item.isVisible(this.range)) {
+ if (!item.displayed) item.show();
- // update queue
- delete queue[id];
- break;
+ // reposition item horizontally
+ item.repositionX();
- case 'remove':
- if (item) {
- // remove the item from the set selected items
- if (item.selected) {
- me._deselect(id);
- }
+ this.visibleItems.push(item);
+ }
+ else {
+ if (item.displayed) item.hide();
+ }
+ }
+ }
- // remove DOM of the item
- changed += item.hide();
- }
+ // reposition visible items vertically
+ //this.stack.order(this.visibleItems); // TODO: improve ordering
+ var force = this.stackDirty || zoomed; // force re-stacking of all items if true
+ this.stack.stack(this.visibleItems, force);
+ this.stackDirty = false;
+ for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
+ this.visibleItems[i].repositionY();
+ }
- // update lists
- delete items[id];
- delete queue[id];
- break;
+ // recalculate the height of the itemset
+ var marginAxis = (options.margin && 'axis' in options.margin) ? options.margin.axis : this.itemOptions.margin.axis,
+ marginItem = (options.margin && 'item' in options.margin) ? options.margin.item : this.itemOptions.margin.item,
+ height;
- default:
- console.log('Error: unknown action "' + action + '"');
- }
- }
+ // determine the height from the stacked items
+ var visibleItems = this.visibleItems;
+ if (visibleItems.length) {
+ var min = visibleItems[0].top;
+ var max = visibleItems[0].top + visibleItems[0].height;
+ util.forEach(visibleItems, function (item) {
+ min = Math.min(min, item.top);
+ max = Math.max(max, (item.top + item.height));
+ });
+ height = (max - min) + marginAxis + marginItem;
+ }
+ else {
+ height = marginAxis + marginItem;
}
- // reposition all items. Show items only when in the visible area
- util.forEach(this.items, function (item) {
- if (item.visible) {
- changed += item.show();
- item.reposition();
- }
- else {
- changed += item.hide();
- }
- });
+ // reposition frame
+ frame.style.left = asSize(options.left, '');
+ frame.style.right = asSize(options.right, '');
+ frame.style.top = asSize((orientation == 'top') ? '0' : '');
+ frame.style.bottom = asSize((orientation == 'top') ? '' : '0');
+ frame.style.width = asSize(options.width, '100%');
+ frame.style.height = asSize(height);
+ //frame.style.height = asSize('height' in options ? options.height : height); // TODO: reckon with height
+
+ // calculate actual size and position
+ this.top = frame.offsetTop;
+ this.left = frame.offsetLeft;
+ this.width = frame.offsetWidth;
+ this.height = height;
- return (changed > 0);
+ // reposition axis
+ this.dom.axis.style.left = asSize(options.left, '0');
+ this.dom.axis.style.right = asSize(options.right, '');
+ this.dom.axis.style.width = asSize(options.width, '100%');
+ this.dom.axis.style.height = asSize(0);
+ this.dom.axis.style.top = asSize((orientation == 'top') ? '0' : '');
+ this.dom.axis.style.bottom = asSize((orientation == 'top') ? '' : '0');
+
+ return this._isResized();
};
/**
@@ -5522,90 +5124,6 @@ ItemSet.prototype.getAxis = function getAxis() {
return this.dom.axis;
};
-/**
- * Reflow the component
- * @return {Boolean} resized
- */
-ItemSet.prototype.reflow = function reflow () {
- var changed = 0,
- options = this.options,
- marginAxis = options.margin && options.margin.axis || this.defaultOptions.margin.axis,
- marginItem = options.margin && options.margin.item || this.defaultOptions.margin.item,
- update = util.updateProperty,
- asNumber = util.option.asNumber,
- asSize = util.option.asSize,
- frame = this.frame;
-
- if (frame) {
- this._updateConversion();
-
- util.forEach(this.items, function (item) {
- changed += item.reflow();
- });
-
- // TODO: stack.update should be triggered via an event, in stack itself
- // TODO: only update the stack when there are changed items
- this.stack.update();
-
- var maxHeight = asNumber(options.maxHeight);
- var fixedHeight = (asSize(options.height) != null);
- var height;
- if (fixedHeight) {
- height = frame.offsetHeight;
- }
- else {
- // height is not specified, determine the height from the height and positioned items
- var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items
- if (visibleItems.length) {
- var min = visibleItems[0].top;
- var max = visibleItems[0].top + visibleItems[0].height;
- util.forEach(visibleItems, function (item) {
- min = Math.min(min, item.top);
- max = Math.max(max, (item.top + item.height));
- });
- height = (max - min) + marginAxis + marginItem;
- }
- else {
- height = marginAxis + marginItem;
- }
- }
- if (maxHeight != null) {
- height = Math.min(height, maxHeight);
- }
- changed += update(this, 'height', height);
-
- // calculate height from items
- changed += update(this, 'top', frame.offsetTop);
- changed += update(this, 'left', frame.offsetLeft);
- changed += update(this, 'width', frame.offsetWidth);
- }
- else {
- changed += 1;
- }
-
- return (changed > 0);
-};
-
-/**
- * Hide this component from the DOM
- * @return {Boolean} changed
- */
-ItemSet.prototype.hide = function hide() {
- var changed = false;
-
- // remove the DOM
- if (this.frame && this.frame.parentNode) {
- this.frame.parentNode.removeChild(this.frame);
- changed = true;
- }
- if (this.dom.axis && this.dom.axis.parentNode) {
- this.dom.axis.parentNode.removeChild(this.dom.axis);
- changed = true;
- }
-
- return changed;
-};
-
/**
* Set items
* @param {vis.DataSet | null} items
@@ -5641,7 +5159,7 @@ ItemSet.prototype.setItems = function setItems(items) {
// subscribe to new dataset
var id = this.id;
util.forEach(this.listeners, function (callback, event) {
- me.itemsData.subscribe(event, callback, id);
+ me.itemsData.on(event, callback, id);
});
// draw all new items
@@ -5658,23 +5176,84 @@ ItemSet.prototype.getItems = function getItems() {
return this.itemsData;
};
+/**
+ * Remove an item by its id
+ * @param {String | Number} id
+ */
+ItemSet.prototype.removeItem = function removeItem (id) {
+ var item = this.itemsData.get(id),
+ dataset = this._myDataSet();
+
+ if (item) {
+ // confirm deletion
+ this.options.onRemove(item, function (item) {
+ if (item) {
+ // remove by id here, it is possible that an item has no id defined
+ // itself, so better not delete by the item itself
+ dataset.remove(id);
+ }
+ });
+ }
+};
+
/**
* Handle updated items
* @param {Number[]} ids
* @private
*/
ItemSet.prototype._onUpdate = function _onUpdate(ids) {
- this._toQueue('update', ids);
+ var me = this,
+ items = this.items,
+ itemOptions = this.itemOptions;
+
+ ids.forEach(function (id) {
+ var itemData = me.itemsData.get(id),
+ item = items[id],
+ type = itemData.type ||
+ (itemData.start && itemData.end && 'range') ||
+ me.options.type ||
+ 'box';
+
+ var constructor = ItemSet.types[type];
+
+ if (item) {
+ // update item
+ if (!constructor || !(item instanceof constructor)) {
+ // item type has changed, hide and delete the item
+ item.hide();
+ item = null;
+ }
+ else {
+ item.data = itemData; // TODO: create a method item.setData ?
+ }
+ }
+
+ if (!item) {
+ // create item
+ if (constructor) {
+ item = new constructor(me, itemData, me.options, itemOptions);
+ item.id = id;
+ }
+ else {
+ throw new TypeError('Unknown item type "' + type + '"');
+ }
+ }
+
+ me.items[id] = item;
+ });
+
+ this._order();
+
+ this.stackDirty = true; // force re-stacking of all items next repaint
+ this.emit('change');
};
/**
- * Handle changed items
+ * Handle added items
* @param {Number[]} ids
* @private
*/
-ItemSet.prototype._onAdd = function _onAdd(ids) {
- this._toQueue('add', ids);
-};
+ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate;
/**
* Handle removed items
@@ -5682,75 +5261,235 @@ ItemSet.prototype._onAdd = function _onAdd(ids) {
* @private
*/
ItemSet.prototype._onRemove = function _onRemove(ids) {
- this._toQueue('remove', ids);
+ var count = 0;
+ var me = this;
+ ids.forEach(function (id) {
+ var item = me.items[id];
+ if (item) {
+ count++;
+ item.hide();
+ delete me.items[id];
+ delete me.visibleItems[id];
+
+ // remove from selection
+ var index = me.selection.indexOf(id);
+ if (index != -1) me.selection.splice(index, 1);
+ }
+ });
+
+ if (count) {
+ // update order
+ this._order();
+ this.stackDirty = true; // force re-stacking of all items next repaint
+ this.emit('change');
+ }
};
/**
- * Put items in the queue to be added/updated/remove
- * @param {String} action can be 'add', 'update', 'remove'
- * @param {Number[]} ids
+ * Order the items
+ * @private
*/
-ItemSet.prototype._toQueue = function _toQueue(action, ids) {
- var queue = this.queue;
- ids.forEach(function (id) {
- queue[id] = {
- id: id,
- action: action
- };
- });
+ItemSet.prototype._order = function _order() {
+ var array = util.toArray(this.items);
+ this.orderedItems.byStart = array;
+ this.orderedItems.byEnd = [].concat(array);
- if (this.controller) {
- //this.requestReflow();
- this.requestRepaint();
- }
+ // reorder the items
+ this.stack.orderByStart(this.orderedItems.byStart);
+ this.stack.orderByEnd(this.orderedItems.byEnd);
};
/**
- * Calculate the scale and offset to convert a position on screen to the
- * corresponding date and vice versa.
- * After the method _updateConversion is executed once, the methods toTime
- * and toScreen can be used.
+ * Start dragging the selected events
+ * @param {Event} event
* @private
*/
-ItemSet.prototype._updateConversion = function _updateConversion() {
- var range = this.range;
- if (!range) {
- throw new Error('No range configured');
+ItemSet.prototype._onDragStart = function (event) {
+ if (!this.options.editable) {
+ return;
}
- if (range.conversion) {
- this.conversion = range.conversion(this.width);
- }
- else {
- this.conversion = Range.conversion(range.start, range.end, this.width);
+ var item = ItemSet.itemFromTarget(event),
+ me = this;
+
+ if (item && item.selected) {
+ var dragLeftItem = event.target.dragLeftItem;
+ var dragRightItem = event.target.dragRightItem;
+
+ if (dragLeftItem) {
+ this.touchParams.itemProps = [{
+ item: dragLeftItem,
+ start: item.data.start.valueOf()
+ }];
+ }
+ else if (dragRightItem) {
+ this.touchParams.itemProps = [{
+ item: dragRightItem,
+ end: item.data.end.valueOf()
+ }];
+ }
+ else {
+ this.touchParams.itemProps = this.getSelection().map(function (id) {
+ var item = me.items[id];
+ var props = {
+ item: item
+ };
+
+ if ('start' in item.data) {
+ props.start = item.data.start.valueOf()
+ }
+ if ('end' in item.data) {
+ props.end = item.data.end.valueOf()
+ }
+
+ return props;
+ });
+ }
+
+ event.stopPropagation();
}
};
/**
- * Convert a position on screen (pixels) to a datetime
- * Before this method can be used, the method _updateConversion must be
- * executed once.
- * @param {int} x Position on the screen in pixels
- * @return {Date} time The datetime the corresponds with given position x
+ * Drag selected items
+ * @param {Event} event
+ * @private
*/
-ItemSet.prototype.toTime = function toTime(x) {
- var conversion = this.conversion;
- return new Date(x / conversion.scale + conversion.offset);
+ItemSet.prototype._onDrag = function (event) {
+ if (this.touchParams.itemProps) {
+ var snap = this.options.snap || null,
+ deltaX = event.gesture.deltaX,
+ scale = (this.width / (this.range.end - this.range.start)),
+ offset = deltaX / scale;
+
+ // move
+ this.touchParams.itemProps.forEach(function (props) {
+ if ('start' in props) {
+ var start = new Date(props.start + offset);
+ props.item.data.start = snap ? snap(start) : start;
+ }
+ if ('end' in props) {
+ var end = new Date(props.end + offset);
+ props.item.data.end = snap ? snap(end) : end;
+ }
+ });
+
+ // TODO: implement onMoving handler
+
+ // TODO: implement dragging from one group to another
+
+ this.stackDirty = true; // force re-stacking of all items next repaint
+ this.emit('change');
+
+ event.stopPropagation();
+ }
};
/**
- * Convert a datetime (Date object) into a position on the screen
- * Before this method can be used, the method _updateConversion must be
- * executed once.
- * @param {Date} time A date
- * @return {int} x The position on the screen in pixels which corresponds
- * with the given date.
+ * End of dragging selected items
+ * @param {Event} event
+ * @private
*/
-ItemSet.prototype.toScreen = function toScreen(time) {
- var conversion = this.conversion;
- return (time.valueOf() - conversion.offset) * conversion.scale;
+ItemSet.prototype._onDragEnd = function (event) {
+ if (this.touchParams.itemProps) {
+ // prepare a change set for the changed items
+ var changes = [],
+ me = this,
+ dataset = this._myDataSet();
+
+ this.touchParams.itemProps.forEach(function (props) {
+ var id = props.item.id,
+ item = me.itemsData.get(id);
+
+ var changed = false;
+ if ('start' in props.item.data) {
+ changed = (props.start != props.item.data.start.valueOf());
+ item.start = util.convert(props.item.data.start, dataset.convert['start']);
+ }
+ if ('end' in props.item.data) {
+ changed = changed || (props.end != props.item.data.end.valueOf());
+ item.end = util.convert(props.item.data.end, dataset.convert['end']);
+ }
+
+ // only apply changes when start or end is actually changed
+ if (changed) {
+ me.options.onMove(item, function (item) {
+ if (item) {
+ // apply changes
+ item[dataset.fieldId] = id; // ensure the item contains its id (can be undefined)
+ changes.push(item);
+ }
+ else {
+ // restore original values
+ if ('start' in props) props.item.data.start = props.start;
+ if ('end' in props) props.item.data.end = props.end;
+
+ this.stackDirty = true; // force re-stacking of all items next repaint
+ this.emit('change');
+ }
+ });
+ }
+ });
+ this.touchParams.itemProps = null;
+
+ // apply the changes to the data (if there are changes)
+ if (changes.length) {
+ dataset.update(changes);
+ }
+
+ event.stopPropagation();
+ }
+};
+
+/**
+ * Find an item from an event target:
+ * searches for the attribute 'timeline-item' in the event target's element tree
+ * @param {Event} event
+ * @return {Item | null} item
+ */
+ItemSet.itemFromTarget = function itemFromTarget (event) {
+ var target = event.target;
+ while (target) {
+ if (target.hasOwnProperty('timeline-item')) {
+ return target['timeline-item'];
+ }
+ target = target.parentNode;
+ }
+
+ return null;
+};
+
+/**
+ * Find the ItemSet from an event target:
+ * searches for the attribute 'timeline-itemset' in the event target's element tree
+ * @param {Event} event
+ * @return {ItemSet | null} item
+ */
+ItemSet.itemSetFromTarget = function itemSetFromTarget (event) {
+ var target = event.target;
+ while (target) {
+ if (target.hasOwnProperty('timeline-itemset')) {
+ return target['timeline-itemset'];
+ }
+ target = target.parentNode;
+ }
+
+ return null;
};
+/**
+ * Find the DataSet to which this ItemSet is connected
+ * @returns {null | DataSet} dataset
+ * @private
+ */
+ItemSet.prototype._myDataSet = function _myDataSet() {
+ // find the root DataSet
+ var dataset = this.itemsData;
+ while (dataset instanceof DataView) {
+ dataset = dataset.data;
+ }
+ return dataset;
+};
/**
* @constructor Item
* @param {ItemSet} parent
@@ -5768,11 +5507,13 @@ function Item (parent, data, options, defaultOptions) {
this.defaultOptions = defaultOptions || {};
this.selected = false;
- this.visible = false;
- this.top = 0;
- this.left = 0;
- this.width = 0;
- this.height = 0;
+ this.displayed = false;
+ this.dirty = true;
+
+ this.top = null;
+ this.left = null;
+ this.width = null;
+ this.height = null;
}
/**
@@ -5780,7 +5521,7 @@ function Item (parent, data, options, defaultOptions) {
*/
Item.prototype.select = function select() {
this.selected = true;
- if (this.visible) this.repaint();
+ if (this.displayed) this.repaint();
};
/**
@@ -5788,7 +5529,7 @@ Item.prototype.select = function select() {
*/
Item.prototype.unselect = function unselect() {
this.selected = false;
- if (this.visible) this.repaint();
+ if (this.displayed) this.repaint();
};
/**
@@ -5809,29 +5550,58 @@ Item.prototype.hide = function hide() {
/**
* Repaint the item
- * @return {Boolean} changed
*/
Item.prototype.repaint = function repaint() {
// should be implemented by the item
- return false;
};
/**
- * Reflow the item
- * @return {Boolean} resized
+ * Reposition the Item horizontally
*/
-Item.prototype.reflow = function reflow() {
+Item.prototype.repositionX = function repositionX() {
// should be implemented by the item
- return false;
};
/**
- * Return the items width
- * @return {Integer} width
+ * Reposition the Item vertically
*/
-Item.prototype.getWidth = function getWidth() {
- return this.width;
-}
+Item.prototype.repositionY = function repositionY() {
+ // should be implemented by the item
+};
+
+/**
+ * Repaint a delete button on the top right of the item when the item is selected
+ * @param {HTMLElement} anchor
+ * @private
+ */
+Item.prototype._repaintDeleteButton = function (anchor) {
+ if (this.selected && this.options.editable && !this.dom.deleteButton) {
+ // create and show button
+ var parent = this.parent;
+ var id = this.id;
+
+ var deleteButton = document.createElement('div');
+ deleteButton.className = 'delete';
+ deleteButton.title = 'Delete this item';
+
+ Hammer(deleteButton, {
+ preventDefault: true
+ }).on('tap', function (event) {
+ parent.removeItem(id);
+ event.stopPropagation();
+ });
+
+ anchor.appendChild(deleteButton);
+ this.dom.deleteButton = deleteButton;
+ }
+ else if (!this.selected && this.dom.deleteButton) {
+ // remove button
+ if (this.dom.deleteButton.parentNode) {
+ this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton);
+ }
+ this.dom.deleteButton = null;
+ }
+};
/**
* @constructor ItemBox
@@ -5846,294 +5616,223 @@ Item.prototype.getWidth = function getWidth() {
function ItemBox (parent, data, options, defaultOptions) {
this.props = {
dot: {
- left: 0,
- top: 0,
width: 0,
height: 0
},
line: {
- top: 0,
- left: 0,
width: 0,
height: 0
}
};
+ // validate data
+ if (data) {
+ if (data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + data);
+ }
+ }
+
Item.call(this, parent, data, options, defaultOptions);
}
ItemBox.prototype = new Item (null, null);
+/**
+ * Check whether this item is visible inside given range
+ * @returns {{start: Number, end: Number}} range with a timestamp for start and end
+ * @returns {boolean} True if visible
+ */
+ItemBox.prototype.isVisible = function isVisible (range) {
+ // determine visibility
+ // TODO: account for the real width of the item. Right now we just add 1/4 to the window
+ var interval = (range.end - range.start) / 4;
+ return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
+};
+
/**
* Repaint the item
- * @return {Boolean} changed
*/
ItemBox.prototype.repaint = function repaint() {
- // TODO: make an efficient repaint
- var changed = false;
var dom = this.dom;
-
if (!dom) {
- this._create();
+ // create DOM
+ this.dom = {};
dom = this.dom;
- changed = true;
+
+ // create main box
+ dom.box = document.createElement('DIV');
+
+ // contents box (inside the background box). used for making margins
+ dom.content = document.createElement('DIV');
+ dom.content.className = 'content';
+ dom.box.appendChild(dom.content);
+
+ // line to axis
+ dom.line = document.createElement('DIV');
+ dom.line.className = 'line';
+
+ // dot on axis
+ dom.dot = document.createElement('DIV');
+ dom.dot.className = 'dot';
+
+ // attach this item as attribute
+ dom.box['timeline-item'] = this;
}
- if (dom) {
- if (!this.parent) {
- throw new Error('Cannot repaint item: no parent attached');
- }
+ // append DOM to parent DOM
+ if (!this.parent) {
+ throw new Error('Cannot repaint item: no parent attached');
+ }
+ if (!dom.box.parentNode) {
+ var foreground = this.parent.getForeground();
+ if (!foreground) throw new Error('Cannot repaint time axis: parent has no foreground container element');
+ foreground.appendChild(dom.box);
+ }
+ if (!dom.line.parentNode) {
+ var background = this.parent.getBackground();
+ if (!background) throw new Error('Cannot repaint time axis: parent has no background container element');
+ background.appendChild(dom.line);
+ }
+ if (!dom.dot.parentNode) {
+ var axis = this.parent.getAxis();
+ if (!background) throw new Error('Cannot repaint time axis: parent has no axis container element');
+ axis.appendChild(dom.dot);
+ }
+ this.displayed = true;
- if (!dom.box.parentNode) {
- var foreground = this.parent.getForeground();
- if (!foreground) {
- throw new Error('Cannot repaint time axis: ' +
- 'parent has no foreground container element');
- }
- foreground.appendChild(dom.box);
- changed = true;
+ // update contents
+ if (this.data.content != this.content) {
+ this.content = this.data.content;
+ if (this.content instanceof Element) {
+ dom.content.innerHTML = '';
+ dom.content.appendChild(this.content);
}
-
- if (!dom.line.parentNode) {
- var background = this.parent.getBackground();
- if (!background) {
- throw new Error('Cannot repaint time axis: ' +
- 'parent has no background container element');
- }
- background.appendChild(dom.line);
- changed = true;
+ else if (this.data.content != undefined) {
+ dom.content.innerHTML = this.content;
}
-
- if (!dom.dot.parentNode) {
- var axis = this.parent.getAxis();
- if (!background) {
- throw new Error('Cannot repaint time axis: ' +
- 'parent has no axis container element');
- }
- axis.appendChild(dom.dot);
- changed = true;
+ else {
+ throw new Error('Property "content" missing in item ' + this.data.id);
}
- // update contents
- if (this.data.content != this.content) {
- this.content = this.data.content;
- if (this.content instanceof Element) {
- dom.content.innerHTML = '';
- dom.content.appendChild(this.content);
- }
- else if (this.data.content != undefined) {
- dom.content.innerHTML = this.content;
- }
- else {
- throw new Error('Property "content" missing in item ' + this.data.id);
- }
- changed = true;
- }
+ this.dirty = true;
+ }
- // update class
- var className = (this.data.className? ' ' + this.data.className : '') +
- (this.selected ? ' selected' : '');
- if (this.className != className) {
- this.className = className;
- dom.box.className = 'item box' + className;
- dom.line.className = 'item line' + className;
- dom.dot.className = 'item dot' + className;
- changed = true;
- }
+ // update class
+ var className = (this.data.className? ' ' + this.data.className : '') +
+ (this.selected ? ' selected' : '');
+ if (this.className != className) {
+ this.className = className;
+ dom.box.className = 'item box' + className;
+ dom.line.className = 'item line' + className;
+ dom.dot.className = 'item dot' + className;
+
+ this.dirty = true;
}
- return changed;
+ // recalculate size
+ if (this.dirty) {
+ this.props.dot.height = dom.dot.offsetHeight;
+ this.props.dot.width = dom.dot.offsetWidth;
+ this.props.line.width = dom.line.offsetWidth;
+ this.width = dom.box.offsetWidth;
+ this.height = dom.box.offsetHeight;
+
+ this.dirty = false;
+ }
+
+ this._repaintDeleteButton(dom.box);
};
/**
- * Show the item in the DOM (when not already visible). The items DOM will
+ * Show the item in the DOM (when not already displayed). The items DOM will
* be created when needed.
- * @return {Boolean} changed
*/
ItemBox.prototype.show = function show() {
- if (!this.dom || !this.dom.box.parentNode) {
- return this.repaint();
- }
- else {
- return false;
+ if (!this.displayed) {
+ this.repaint();
}
};
/**
* Hide the item from the DOM (when visible)
- * @return {Boolean} changed
*/
ItemBox.prototype.hide = function hide() {
- var changed = false,
- dom = this.dom;
- if (dom) {
- if (dom.box.parentNode) {
- dom.box.parentNode.removeChild(dom.box);
- changed = true;
- }
- if (dom.line.parentNode) {
- dom.line.parentNode.removeChild(dom.line);
- }
- if (dom.dot.parentNode) {
- dom.dot.parentNode.removeChild(dom.dot);
- }
+ if (this.displayed) {
+ var dom = this.dom;
+
+ if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box);
+ if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line);
+ if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot);
+
+ this.top = null;
+ this.left = null;
+
+ this.displayed = false;
}
- return changed;
};
/**
- * Reflow the item: calculate its actual size and position from the DOM
- * @return {boolean} resized returns true if the axis is resized
- * @override
+ * Reposition the item horizontally
+ * @Override
*/
-ItemBox.prototype.reflow = function reflow() {
- var changed = 0,
- update,
- dom,
- props,
- options,
- margin,
- start,
- align,
- orientation,
- top,
+ItemBox.prototype.repositionX = function repositionX() {
+ var start = this.defaultOptions.toScreen(this.data.start),
+ align = this.options.align || this.defaultOptions.align,
left,
- data,
- range;
+ box = this.dom.box,
+ line = this.dom.line,
+ dot = this.dom.dot;
- if (this.data.start == undefined) {
- throw new Error('Property "start" missing in item ' + this.data.id);
+ // calculate left position of the box
+ if (align == 'right') {
+ this.left = start - this.width;
}
-
- data = this.data;
- range = this.parent && this.parent.range;
- if (data && range) {
- // TODO: account for the width of the item
- var interval = (range.end - range.start);
- this.visible = (data.start > range.start - interval) && (data.start < range.end + interval);
+ else if (align == 'left') {
+ this.left = start;
}
else {
- this.visible = false;
+ // default or 'center'
+ this.left = start - this.width / 2;
}
- if (this.visible) {
- dom = this.dom;
- if (dom) {
- update = util.updateProperty;
- props = this.props;
- options = this.options;
- start = this.parent.toScreen(this.data.start);
- align = options.align || this.defaultOptions.align;
- margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
- orientation = options.orientation || this.defaultOptions.orientation;
-
- changed += update(props.dot, 'height', dom.dot.offsetHeight);
- changed += update(props.dot, 'width', dom.dot.offsetWidth);
- changed += update(props.line, 'width', dom.line.offsetWidth);
- changed += update(props.line, 'height', dom.line.offsetHeight);
- changed += update(props.line, 'top', dom.line.offsetTop);
- changed += update(this, 'width', dom.box.offsetWidth);
- changed += update(this, 'height', dom.box.offsetHeight);
- if (align == 'right') {
- left = start - this.width;
- }
- else if (align == 'left') {
- left = start;
- }
- else {
- // default or 'center'
- left = start - this.width / 2;
- }
- changed += update(this, 'left', left);
-
- changed += update(props.line, 'left', start - props.line.width / 2);
- changed += update(props.dot, 'left', start - props.dot.width / 2);
- changed += update(props.dot, 'top', -props.dot.height / 2);
- if (orientation == 'top') {
- top = margin;
-
- changed += update(this, 'top', top);
- }
- else {
- // default or 'bottom'
- var parentHeight = this.parent.height;
- top = parentHeight - this.height - margin;
+ // reposition box
+ box.style.left = this.left + 'px';
- changed += update(this, 'top', top);
- }
- }
- else {
- changed += 1;
- }
- }
+ // reposition line
+ line.style.left = (start - this.props.line.width / 2) + 'px';
- return (changed > 0);
+ // reposition dot
+ dot.style.left = (start - this.props.dot.width / 2) + 'px';
};
/**
- * Create an items DOM
- * @private
+ * Reposition the item vertically
+ * @Override
*/
-ItemBox.prototype._create = function _create() {
- var dom = this.dom;
- if (!dom) {
- this.dom = dom = {};
-
- // create the box
- dom.box = document.createElement('DIV');
- // className is updated in repaint()
-
- // contents box (inside the background box). used for making margins
- dom.content = document.createElement('DIV');
- dom.content.className = 'content';
- dom.box.appendChild(dom.content);
+ItemBox.prototype.repositionY = function repositionY () {
+ var orientation = this.options.orientation || this.defaultOptions.orientation,
+ box = this.dom.box,
+ line = this.dom.line,
+ dot = this.dom.dot;
- // line to axis
- dom.line = document.createElement('DIV');
- dom.line.className = 'line';
-
- // dot on axis
- dom.dot = document.createElement('DIV');
- dom.dot.className = 'dot';
+ if (orientation == 'top') {
+ box.style.top = (this.top || 0) + 'px';
+ box.style.bottom = '';
- // attach this item as attribute
- dom.box['timeline-item'] = this;
+ line.style.top = '0';
+ line.style.bottom = '';
+ line.style.height = (this.parent.top + this.top + 1) + 'px';
}
-};
-
-/**
- * Reposition the item, recalculate its left, top, and width, using the current
- * range and size of the items itemset
- * @override
- */
-ItemBox.prototype.reposition = function reposition() {
- var dom = this.dom,
- props = this.props,
- orientation = this.options.orientation || this.defaultOptions.orientation;
-
- if (dom) {
- var box = dom.box,
- line = dom.line,
- dot = dom.dot;
-
- box.style.left = this.left + 'px';
- box.style.top = this.top + 'px';
-
- line.style.left = props.line.left + 'px';
- if (orientation == 'top') {
- line.style.top = 0 + 'px';
- line.style.height = this.top + 'px';
- }
- else {
- // orientation 'bottom'
- line.style.top = (this.top + this.height) + 'px';
- line.style.height = Math.max(this.parent.height - this.top - this.height +
- this.props.dot.height / 2, 0) + 'px';
- }
+ else { // orientation 'bottom'
+ box.style.top = '';
+ box.style.bottom = (this.top || 0) + 'px';
- dot.style.left = props.dot.left + 'px';
- dot.style.top = props.dot.top + 'px';
+ line.style.top = (this.parent.top + this.parent.height - this.top - 1) + 'px';
+ line.style.bottom = '0';
+ line.style.height = '';
}
+
+ dot.style.top = (-this.props.dot.height / 2) + 'px';
};
/**
@@ -6159,220 +5858,173 @@ function ItemPoint (parent, data, options, defaultOptions) {
}
};
+ // validate data
+ if (data) {
+ if (data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + data);
+ }
+ }
+
Item.call(this, parent, data, options, defaultOptions);
}
ItemPoint.prototype = new Item (null, null);
+/**
+ * Check whether this item is visible inside given range
+ * @returns {{start: Number, end: Number}} range with a timestamp for start and end
+ * @returns {boolean} True if visible
+ */
+ItemPoint.prototype.isVisible = function isVisible (range) {
+ // determine visibility
+ var interval = (range.end - range.start);
+ return (this.data.start > range.start - interval) && (this.data.start < range.end);
+}
+
/**
* Repaint the item
- * @return {Boolean} changed
*/
ItemPoint.prototype.repaint = function repaint() {
- // TODO: make an efficient repaint
- var changed = false;
var dom = this.dom;
-
if (!dom) {
- this._create();
+ // create DOM
+ this.dom = {};
dom = this.dom;
- changed = true;
+
+ // background box
+ dom.point = document.createElement('div');
+ // className is updated in repaint()
+
+ // contents box, right from the dot
+ dom.content = document.createElement('div');
+ dom.content.className = 'content';
+ dom.point.appendChild(dom.content);
+
+ // dot at start
+ dom.dot = document.createElement('div');
+ dom.dot.className = 'dot';
+ dom.point.appendChild(dom.dot);
+
+ // attach this item as attribute
+ dom.point['timeline-item'] = this;
}
- if (dom) {
- if (!this.parent) {
- throw new Error('Cannot repaint item: no parent attached');
- }
+ // append DOM to parent DOM
+ if (!this.parent) {
+ throw new Error('Cannot repaint item: no parent attached');
+ }
+ if (!dom.point.parentNode) {
var foreground = this.parent.getForeground();
if (!foreground) {
- throw new Error('Cannot repaint time axis: ' +
- 'parent has no foreground container element');
+ throw new Error('Cannot repaint time axis: parent has no foreground container element');
}
+ foreground.appendChild(dom.point);
+ }
+ this.displayed = true;
- if (!dom.point.parentNode) {
- foreground.appendChild(dom.point);
- foreground.appendChild(dom.point);
- changed = true;
+ // update contents
+ if (this.data.content != this.content) {
+ this.content = this.data.content;
+ if (this.content instanceof Element) {
+ dom.content.innerHTML = '';
+ dom.content.appendChild(this.content);
}
-
- // update contents
- if (this.data.content != this.content) {
- this.content = this.data.content;
- if (this.content instanceof Element) {
- dom.content.innerHTML = '';
- dom.content.appendChild(this.content);
- }
- else if (this.data.content != undefined) {
- dom.content.innerHTML = this.content;
- }
- else {
- throw new Error('Property "content" missing in item ' + this.data.id);
- }
- changed = true;
+ else if (this.data.content != undefined) {
+ dom.content.innerHTML = this.content;
}
-
- // update class
- var className = (this.data.className? ' ' + this.data.className : '') +
- (this.selected ? ' selected' : '');
- if (this.className != className) {
- this.className = className;
- dom.point.className = 'item point' + className;
- changed = true;
+ else {
+ throw new Error('Property "content" missing in item ' + this.data.id);
}
+
+ this.dirty = true;
}
- return changed;
+ // update class
+ var className = (this.data.className? ' ' + this.data.className : '') +
+ (this.selected ? ' selected' : '');
+ if (this.className != className) {
+ this.className = className;
+ dom.point.className = 'item point' + className;
+
+ this.dirty = true;
+ }
+
+ // recalculate size
+ if (this.dirty) {
+ this.width = dom.point.offsetWidth;
+ this.height = dom.point.offsetHeight;
+ this.props.dot.width = dom.dot.offsetWidth;
+ this.props.dot.height = dom.dot.offsetHeight;
+ this.props.content.height = dom.content.offsetHeight;
+
+ // resize contents
+ dom.content.style.marginLeft = 1.5 * this.props.dot.width + 'px';
+ //dom.content.style.marginRight = ... + 'px'; // TODO: margin right
+
+ dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px';
+
+ this.dirty = false;
+ }
+
+ this._repaintDeleteButton(dom.point);
};
/**
* Show the item in the DOM (when not already visible). The items DOM will
* be created when needed.
- * @return {Boolean} changed
*/
ItemPoint.prototype.show = function show() {
- if (!this.dom || !this.dom.point.parentNode) {
- return this.repaint();
- }
- else {
- return false;
+ if (!this.displayed) {
+ this.repaint();
}
};
/**
* Hide the item from the DOM (when visible)
- * @return {Boolean} changed
*/
ItemPoint.prototype.hide = function hide() {
- var changed = false,
- dom = this.dom;
- if (dom) {
- if (dom.point.parentNode) {
- dom.point.parentNode.removeChild(dom.point);
- changed = true;
+ if (this.displayed) {
+ if (this.dom.point.parentNode) {
+ this.dom.point.parentNode.removeChild(this.dom.point);
}
+
+ this.top = null;
+ this.left = null;
+
+ this.displayed = false;
}
- return changed;
};
/**
- * Reflow the item: calculate its actual size from the DOM
- * @return {boolean} resized returns true if the axis is resized
- * @override
+ * Reposition the item horizontally
+ * @Override
*/
-ItemPoint.prototype.reflow = function reflow() {
- var changed = 0,
- update,
- dom,
- props,
- options,
- margin,
- orientation,
- start,
- top,
- data,
- range;
+ItemPoint.prototype.repositionX = function repositionX() {
+ var start = this.defaultOptions.toScreen(this.data.start);
- if (this.data.start == undefined) {
- throw new Error('Property "start" missing in item ' + this.data.id);
- }
+ this.left = start - this.props.dot.width / 2;
- data = this.data;
- range = this.parent && this.parent.range;
- if (data && range) {
- // TODO: account for the width of the item
- var interval = (range.end - range.start);
- this.visible = (data.start > range.start - interval) && (data.start < range.end);
- }
- else {
- this.visible = false;
- }
-
- if (this.visible) {
- dom = this.dom;
- if (dom) {
- update = util.updateProperty;
- props = this.props;
- options = this.options;
- orientation = options.orientation || this.defaultOptions.orientation;
- margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
- start = this.parent.toScreen(this.data.start);
-
- changed += update(this, 'width', dom.point.offsetWidth);
- changed += update(this, 'height', dom.point.offsetHeight);
- changed += update(props.dot, 'width', dom.dot.offsetWidth);
- changed += update(props.dot, 'height', dom.dot.offsetHeight);
- changed += update(props.content, 'height', dom.content.offsetHeight);
-
- if (orientation == 'top') {
- top = margin;
- }
- else {
- // default or 'bottom'
- var parentHeight = this.parent.height;
- top = Math.max(parentHeight - this.height - margin, 0);
- }
- changed += update(this, 'top', top);
- changed += update(this, 'left', start - props.dot.width / 2);
- changed += update(props.content, 'marginLeft', 1.5 * props.dot.width);
- //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO
-
- changed += update(props.dot, 'top', (this.height - props.dot.height) / 2);
- }
- else {
- changed += 1;
- }
- }
-
- return (changed > 0);
+ // reposition point
+ this.dom.point.style.left = this.left + 'px';
};
/**
- * Create an items DOM
- * @private
+ * Reposition the item vertically
+ * @Override
*/
-ItemPoint.prototype._create = function _create() {
- var dom = this.dom;
- if (!dom) {
- this.dom = dom = {};
-
- // background box
- dom.point = document.createElement('div');
- // className is updated in repaint()
-
- // contents box, right from the dot
- dom.content = document.createElement('div');
- dom.content.className = 'content';
- dom.point.appendChild(dom.content);
-
- // dot at start
- dom.dot = document.createElement('div');
- dom.dot.className = 'dot';
- dom.point.appendChild(dom.dot);
+ItemPoint.prototype.repositionY = function repositionY () {
+ var orientation = this.options.orientation || this.defaultOptions.orientation,
+ point = this.dom.point;
- // attach this item as attribute
- dom.point['timeline-item'] = this;
+ if (orientation == 'top') {
+ point.style.top = this.top + 'px';
+ point.style.bottom = '';
}
-};
-
-/**
- * Reposition the item, recalculate its left, top, and width, using the current
- * range and size of the items itemset
- * @override
- */
-ItemPoint.prototype.reposition = function reposition() {
- var dom = this.dom,
- props = this.props;
-
- if (dom) {
- dom.point.style.top = this.top + 'px';
- dom.point.style.left = this.left + 'px';
-
- dom.content.style.marginLeft = props.content.marginLeft + 'px';
- //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO
-
- dom.dot.style.top = props.dot.top + 'px';
+ else {
+ point.style.top = '';
+ point.style.bottom = this.top + 'px';
}
-};
+}
/**
* @constructor ItemRange
@@ -6387,86 +6039,120 @@ ItemPoint.prototype.reposition = function reposition() {
function ItemRange (parent, data, options, defaultOptions) {
this.props = {
content: {
- left: 0,
width: 0
}
};
+ // validate data
+ if (data) {
+ if (data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + data.id);
+ }
+ if (data.end == undefined) {
+ throw new Error('Property "end" missing in item ' + data.id);
+ }
+ }
+
Item.call(this, parent, data, options, defaultOptions);
}
ItemRange.prototype = new Item (null, null);
+ItemRange.prototype.baseClassName = 'item range';
+
+/**
+ * Check whether this item is visible inside given range
+ * @returns {{start: Number, end: Number}} range with a timestamp for start and end
+ * @returns {boolean} True if visible
+ */
+ItemRange.prototype.isVisible = function isVisible (range) {
+ // determine visibility
+ return (this.data.start < range.end) && (this.data.end > range.start);
+};
+
/**
* Repaint the item
- * @return {Boolean} changed
*/
ItemRange.prototype.repaint = function repaint() {
- // TODO: make an efficient repaint
- var changed = false;
var dom = this.dom;
-
if (!dom) {
- this._create();
+ // create DOM
+ this.dom = {};
dom = this.dom;
- changed = true;
+
+ // background box
+ dom.box = document.createElement('div');
+ // className is updated in repaint()
+
+ // contents box
+ dom.content = document.createElement('div');
+ dom.content.className = 'content';
+ dom.box.appendChild(dom.content);
+
+ // attach this item as attribute
+ dom.box['timeline-item'] = this;
}
- if (dom) {
- if (!this.parent) {
- throw new Error('Cannot repaint item: no parent attached');
- }
+ // append DOM to parent DOM
+ if (!this.parent) {
+ throw new Error('Cannot repaint item: no parent attached');
+ }
+ if (!dom.box.parentNode) {
var foreground = this.parent.getForeground();
if (!foreground) {
- throw new Error('Cannot repaint time axis: ' +
- 'parent has no foreground container element');
+ throw new Error('Cannot repaint time axis: parent has no foreground container element');
}
+ foreground.appendChild(dom.box);
+ }
+ this.displayed = true;
- if (!dom.box.parentNode) {
- foreground.appendChild(dom.box);
- changed = true;
+ // update contents
+ if (this.data.content != this.content) {
+ this.content = this.data.content;
+ if (this.content instanceof Element) {
+ dom.content.innerHTML = '';
+ dom.content.appendChild(this.content);
}
-
- // update content
- if (this.data.content != this.content) {
- this.content = this.data.content;
- if (this.content instanceof Element) {
- dom.content.innerHTML = '';
- dom.content.appendChild(this.content);
- }
- else if (this.data.content != undefined) {
- dom.content.innerHTML = this.content;
- }
- else {
- throw new Error('Property "content" missing in item ' + this.data.id);
- }
- changed = true;
+ else if (this.data.content != undefined) {
+ dom.content.innerHTML = this.content;
}
-
- // update class
- var className = (this.data.className? ' ' + this.data.className : '') +
- (this.selected ? ' selected' : '');
- if (this.className != className) {
- this.className = className;
- dom.box.className = 'item range' + className;
- changed = true;
+ else {
+ throw new Error('Property "content" missing in item ' + this.data.id);
}
+
+ this.dirty = true;
}
- return changed;
+ // update class
+ var className = (this.data.className ? (' ' + this.data.className) : '') +
+ (this.selected ? ' selected' : '');
+ if (this.className != className) {
+ this.className = className;
+ dom.box.className = this.baseClassName + className;
+
+ this.dirty = true;
+ }
+
+ // recalculate size
+ if (this.dirty) {
+ this.props.content.width = this.dom.content.offsetWidth;
+ this.height = this.dom.box.offsetHeight;
+
+ this.dirty = false;
+ }
+
+ this._repaintDeleteButton(dom.box);
+ this._repaintDragLeft();
+ this._repaintDragRight();
};
/**
* Show the item in the DOM (when not already visible). The items DOM will
* be created when needed.
- * @return {Boolean} changed
*/
ItemRange.prototype.show = function show() {
- if (!this.dom || !this.dom.box.parentNode) {
- return this.repaint();
- }
- else {
- return false;
+ if (!this.displayed) {
+ this.repaint();
}
};
@@ -6475,154 +6161,133 @@ ItemRange.prototype.show = function show() {
* @return {Boolean} changed
*/
ItemRange.prototype.hide = function hide() {
- var changed = false,
- dom = this.dom;
- if (dom) {
- if (dom.box.parentNode) {
- dom.box.parentNode.removeChild(dom.box);
- changed = true;
+ if (this.displayed) {
+ var box = this.dom.box;
+
+ if (box.parentNode) {
+ box.parentNode.removeChild(box);
}
+
+ this.top = null;
+ this.left = null;
+
+ this.displayed = false;
}
- return changed;
};
/**
- * Reflow the item: calculate its actual size from the DOM
- * @return {boolean} resized returns true if the axis is resized
- * @override
+ * Reposition the item horizontally
+ * @Override
*/
-ItemRange.prototype.reflow = function reflow() {
- var changed = 0,
- dom,
- props,
- options,
- margin,
- padding,
- parent,
- start,
- end,
- data,
- range,
- update,
- box,
- parentWidth,
- contentLeft,
- orientation,
- top;
+ItemRange.prototype.repositionX = function repositionX() {
+ var props = this.props,
+ parentWidth = this.parent.width,
+ start = this.defaultOptions.toScreen(this.data.start),
+ end = this.defaultOptions.toScreen(this.data.end),
+ padding = 'padding' in this.options ? this.options.padding : this.defaultOptions.padding,
+ contentLeft;
- if (this.data.start == undefined) {
- throw new Error('Property "start" missing in item ' + this.data.id);
+ // limit the width of the this, as browsers cannot draw very wide divs
+ if (start < -parentWidth) {
+ start = -parentWidth;
}
- if (this.data.end == undefined) {
- throw new Error('Property "end" missing in item ' + this.data.id);
+ if (end > 2 * parentWidth) {
+ end = 2 * parentWidth;
}
- data = this.data;
- range = this.parent && this.parent.range;
- if (data && range) {
- // TODO: account for the width of the item. Take some margin
- this.visible = (data.start < range.end) && (data.end > range.start);
+ // when range exceeds left of the window, position the contents at the left of the visible area
+ if (start < 0) {
+ contentLeft = Math.min(-start,
+ (end - start - props.content.width - 2 * padding));
+ // TODO: remove the need for options.padding. it's terrible.
}
else {
- this.visible = false;
+ contentLeft = 0;
}
- if (this.visible) {
- dom = this.dom;
- if (dom) {
- props = this.props;
- options = this.options;
- parent = this.parent;
- start = parent.toScreen(this.data.start);
- end = parent.toScreen(this.data.end);
- update = util.updateProperty;
- box = dom.box;
- parentWidth = parent.width;
- orientation = options.orientation || this.defaultOptions.orientation;
- margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
- padding = options.padding || this.defaultOptions.padding;
-
- changed += update(props.content, 'width', dom.content.offsetWidth);
+ this.left = start;
+ this.width = Math.max(end - start, 1);
- changed += update(this, 'height', box.offsetHeight);
-
- // limit the width of the this, as browsers cannot draw very wide divs
- if (start < -parentWidth) {
- start = -parentWidth;
- }
- if (end > 2 * parentWidth) {
- end = 2 * parentWidth;
- }
-
- // when range exceeds left of the window, position the contents at the left of the visible area
- if (start < 0) {
- contentLeft = Math.min(-start,
- (end - start - props.content.width - 2 * padding));
- // TODO: remove the need for options.padding. it's terrible.
- }
- else {
- contentLeft = 0;
- }
- changed += update(props.content, 'left', contentLeft);
+ this.dom.box.style.left = this.left + 'px';
+ this.dom.box.style.width = this.width + 'px';
+ this.dom.content.style.left = contentLeft + 'px';
+};
- if (orientation == 'top') {
- top = margin;
- changed += update(this, 'top', top);
- }
- else {
- // default or 'bottom'
- top = parent.height - this.height - margin;
- changed += update(this, 'top', top);
- }
+/**
+ * Reposition the item vertically
+ * @Override
+ */
+ItemRange.prototype.repositionY = function repositionY() {
+ var orientation = this.options.orientation || this.defaultOptions.orientation,
+ box = this.dom.box;
- changed += update(this, 'left', start);
- changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width;
- }
- else {
- changed += 1;
- }
+ if (orientation == 'top') {
+ box.style.top = this.top + 'px';
+ box.style.bottom = '';
+ }
+ else {
+ box.style.top = '';
+ box.style.bottom = this.top + 'px';
}
-
- return (changed > 0);
};
/**
- * Create an items DOM
+ * Repaint a drag area on the left side of the range when the range is selected
* @private
*/
-ItemRange.prototype._create = function _create() {
- var dom = this.dom;
- if (!dom) {
- this.dom = dom = {};
- // background box
- dom.box = document.createElement('div');
- // className is updated in repaint()
-
- // contents box
- dom.content = document.createElement('div');
- dom.content.className = 'content';
- dom.box.appendChild(dom.content);
+ItemRange.prototype._repaintDragLeft = function () {
+ if (this.selected && this.options.editable && !this.dom.dragLeft) {
+ // create and show drag area
+ var dragLeft = document.createElement('div');
+ dragLeft.className = 'drag-left';
+ dragLeft.dragLeftItem = this;
+
+ // TODO: this should be redundant?
+ Hammer(dragLeft, {
+ preventDefault: true
+ }).on('drag', function () {
+ //console.log('drag left')
+ });
- // attach this item as attribute
- dom.box['timeline-item'] = this;
+ this.dom.box.appendChild(dragLeft);
+ this.dom.dragLeft = dragLeft;
+ }
+ else if (!this.selected && this.dom.dragLeft) {
+ // delete drag area
+ if (this.dom.dragLeft.parentNode) {
+ this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft);
+ }
+ this.dom.dragLeft = null;
}
};
/**
- * Reposition the item, recalculate its left, top, and width, using the current
- * range and size of the items itemset
- * @override
+ * Repaint a drag area on the right side of the range when the range is selected
+ * @private
*/
-ItemRange.prototype.reposition = function reposition() {
- var dom = this.dom,
- props = this.props;
-
- if (dom) {
- dom.box.style.top = this.top + 'px';
- dom.box.style.left = this.left + 'px';
- dom.box.style.width = this.width + 'px';
+ItemRange.prototype._repaintDragRight = function () {
+ if (this.selected && this.options.editable && !this.dom.dragRight) {
+ // create and show drag area
+ var dragRight = document.createElement('div');
+ dragRight.className = 'drag-right';
+ dragRight.dragRightItem = this;
+
+ // TODO: this should be redundant?
+ Hammer(dragRight, {
+ preventDefault: true
+ }).on('drag', function () {
+ //console.log('drag right')
+ });
- dom.content.style.left = props.content.left + 'px';
+ this.dom.box.appendChild(dragRight);
+ this.dom.dragRight = dragRight;
+ }
+ else if (!this.selected && this.dom.dragRight) {
+ // delete drag area
+ if (this.dom.dragRight.parentNode) {
+ this.dom.dragRight.parentNode.removeChild(this.dom.dragRight);
+ }
+ this.dom.dragRight = null;
}
};
@@ -6649,89 +6314,61 @@ function ItemRangeOverflow (parent, data, options, defaultOptions) {
ItemRangeOverflow.prototype = new ItemRange (null, null);
+ItemRangeOverflow.prototype.baseClassName = 'item rangeoverflow';
+
/**
- * Repaint the item
- * @return {Boolean} changed
+ * Reposition the item horizontally
+ * @Override
*/
-ItemRangeOverflow.prototype.repaint = function repaint() {
- // TODO: make an efficient repaint
- var changed = false;
- var dom = this.dom;
+ItemRangeOverflow.prototype.repositionX = function repositionX() {
+ var parentWidth = this.parent.width,
+ start = this.defaultOptions.toScreen(this.data.start),
+ end = this.defaultOptions.toScreen(this.data.end),
+ padding = 'padding' in this.options ? this.options.padding : this.defaultOptions.padding,
+ contentLeft;
- if (!dom) {
- this._create();
- dom = this.dom;
- changed = true;
+ // limit the width of the this, as browsers cannot draw very wide divs
+ if (start < -parentWidth) {
+ start = -parentWidth;
}
-
- if (dom) {
- if (!this.parent) {
- throw new Error('Cannot repaint item: no parent attached');
- }
- var foreground = this.parent.getForeground();
- if (!foreground) {
- throw new Error('Cannot repaint time axis: ' +
- 'parent has no foreground container element');
- }
-
- if (!dom.box.parentNode) {
- foreground.appendChild(dom.box);
- changed = true;
- }
-
- // update content
- if (this.data.content != this.content) {
- this.content = this.data.content;
- if (this.content instanceof Element) {
- dom.content.innerHTML = '';
- dom.content.appendChild(this.content);
- }
- else if (this.data.content != undefined) {
- dom.content.innerHTML = this.content;
- }
- else {
- throw new Error('Property "content" missing in item ' + this.data.id);
- }
- changed = true;
- }
-
- // update class
- var className = this.data.className ? (' ' + this.data.className) : '';
- if (this.className != className) {
- this.className = className;
- dom.box.className = 'item rangeoverflow' + className;
- changed = true;
- }
+ if (end > 2 * parentWidth) {
+ end = 2 * parentWidth;
}
- return changed;
-};
+ // when range exceeds left of the window, position the contents at the left of the visible area
+ contentLeft = Math.max(-start, 0);
-/**
- * Return the items width
- * @return {Number} width
- */
-ItemRangeOverflow.prototype.getWidth = function getWidth() {
- if (this.props.content !== undefined && this.width < this.props.content.width)
- return this.props.content.width;
- else
- return this.width;
+ this.left = start;
+ var boxWidth = Math.max(end - start, 1);
+ this.width = (this.props.content.width < boxWidth) ?
+ boxWidth :
+ start + contentLeft + this.props.content.width;
+
+ this.dom.box.style.left = this.left + 'px';
+ this.dom.box.style.width = boxWidth + 'px';
+ this.dom.content.style.left = contentLeft + 'px';
};
/**
* @constructor Group
- * @param {GroupSet} parent
+ * @param {Panel} groupPanel
+ * @param {Panel} labelPanel
+ * @param {Panel} backgroundPanel
+ * @param {Panel} axisPanel
* @param {Number | String} groupId
* @param {Object} [options] Options to set initial property values
* // TODO: describe available options
* @extends Component
*/
-function Group (parent, groupId, options) {
+function Group (groupPanel, labelPanel, backgroundPanel, axisPanel, groupId, options) {
this.id = util.randomUUID();
- this.parent = parent;
+ this.groupPanel = groupPanel;
+ this.labelPanel = labelPanel;
+ this.backgroundPanel = backgroundPanel;
+ this.axisPanel = axisPanel;
this.groupId = groupId;
- this.itemset = null; // ItemSet
+ this.itemSet = null; // ItemSet
this.options = options || {};
this.options.top = 0;
@@ -6742,10 +6379,14 @@ function Group (parent, groupId, options) {
}
};
+ this.dom = {};
+
this.top = 0;
this.left = 0;
this.width = 0;
this.height = 0;
+
+ this._create();
}
Group.prototype = new Component();
@@ -6754,47 +6395,131 @@ Group.prototype = new Component();
Group.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
+ * Create DOM elements for the group
+ * @private
+ */
+Group.prototype._create = function() {
+ var label = document.createElement('div');
+ label.className = 'vlabel';
+ this.dom.label = label;
+
+ var inner = document.createElement('div');
+ inner.className = 'inner';
+ label.appendChild(inner);
+ this.dom.inner = inner;
+};
+
+/**
+ * Set the group data for this group
+ * @param {Object} data Group data, can contain properties content and className
*/
-Group.prototype.getContainer = function () {
- return this.parent.getContainer();
+Group.prototype.setData = function setData(data) {
+ // update contents
+ var content = data && data.content;
+ if (content instanceof Element) {
+ this.dom.inner.appendChild(content);
+ }
+ else if (content != undefined) {
+ this.dom.inner.innerHTML = content;
+ }
+ else {
+ this.dom.inner.innerHTML = this.groupId;
+ }
+
+ // update className
+ var className = data && data.className;
+ if (className) {
+ util.addClassName(this.dom.label, className);
+ }
};
/**
- * Set item set for the group. The group will create a view on the itemset,
+ * Set item set for the group. The group will create a view on the itemSet,
* filtered by the groups id.
- * @param {DataSet | DataView} items
+ * @param {DataSet | DataView} itemsData
*/
-Group.prototype.setItems = function setItems(items) {
- if (this.itemset) {
+Group.prototype.setItems = function setItems(itemsData) {
+ if (this.itemSet) {
// remove current item set
- this.itemset.hide();
- this.itemset.setItems();
-
- this.parent.controller.remove(this.itemset);
- this.itemset = null;
+ this.itemSet.setItems();
+ this.itemSet.hide();
+ this.groupPanel.frame.removeChild(this.itemSet.getFrame());
+ this.itemSet = null;
}
- if (items) {
+ if (itemsData) {
var groupId = this.groupId;
- var itemsetOptions = Object.create(this.options);
- this.itemset = new ItemSet(this, null, itemsetOptions);
- this.itemset.setRange(this.parent.range);
+ var me = this;
+ var itemSetOptions = util.extend(this.options, {
+ height: function () {
+ // FIXME: setting height doesn't yet work
+ return Math.max(me.props.label.height, me.itemSet.height);
+ }
+ });
+ this.itemSet = new ItemSet(this.backgroundPanel, this.axisPanel, itemSetOptions);
+ this.itemSet.on('change', this.emit.bind(this, 'change')); // propagate change event
+ this.itemSet.parent = this;
+ this.groupPanel.frame.appendChild(this.itemSet.getFrame());
+
+ if (this.range) this.itemSet.setRange(this.range);
- this.view = new DataView(items, {
+ this.view = new DataView(itemsData, {
filter: function (item) {
return item.group == groupId;
}
});
- this.itemset.setItems(this.view);
+ this.itemSet.setItems(this.view);
+ }
+};
+
+/**
+ * hide the group, detach from DOM if needed
+ */
+Group.prototype.show = function show() {
+ if (!this.dom.label.parentNode) {
+ this.labelPanel.frame.appendChild(this.dom.label);
+ }
+
+ var itemSetFrame = this.itemSet && this.itemSet.getFrame();
+ if (itemSetFrame) {
+ if (itemSetFrame.parentNode) {
+ itemSetFrame.parentNode.removeChild(itemSetFrame);
+ }
+ this.groupPanel.frame.appendChild(itemSetFrame);
+
+ this.itemSet.show();
+ }
+};
+
+/**
+ * hide the group, detach from DOM if needed
+ */
+Group.prototype.hide = function hide() {
+ if (this.dom.label.parentNode) {
+ this.dom.label.parentNode.removeChild(this.dom.label);
+ }
+
+ if (this.itemSet) {
+ this.itemSet.hide();
+ }
- this.parent.controller.add(this.itemset);
+ var itemSetFrame = this.itemset && this.itemSet.getFrame();
+ if (itemSetFrame && itemSetFrame.parentNode) {
+ itemSetFrame.parentNode.removeChild(itemSetFrame);
}
};
+/**
+ * Set range (start and end).
+ * @param {Range | Object} range A Range or an object containing start and end.
+ */
+Group.prototype.setRange = function (range) {
+ this.range = range;
+
+ if (this.itemSet) this.itemSet.setRange(range);
+};
+
/**
* Set selected items by their id. Replaces the current selection.
* Unknown id's are silently ignored.
@@ -6803,7 +6528,7 @@ Group.prototype.setItems = function setItems(items) {
* unselected.
*/
Group.prototype.setSelection = function setSelection(ids) {
- if (this.itemset) this.itemset.setSelection(ids);
+ if (this.itemSet) this.itemSet.setSelection(ids);
};
/**
@@ -6811,58 +6536,53 @@ Group.prototype.setSelection = function setSelection(ids) {
* @return {Array} ids The ids of the selected items
*/
Group.prototype.getSelection = function getSelection() {
- return this.itemset ? this.itemset.getSelection() : [];
+ return this.itemSet ? this.itemSet.getSelection() : [];
};
/**
- * Repaint the item
- * @return {Boolean} changed
+ * Repaint the group
+ * @return {boolean} Returns true if the component is resized
*/
Group.prototype.repaint = function repaint() {
- return false;
-};
+ var resized = false;
-/**
- * Reflow the item
- * @return {Boolean} resized
- */
-Group.prototype.reflow = function reflow() {
- var changed = 0,
- update = util.updateProperty;
+ this.show();
- changed += update(this, 'top', this.itemset ? this.itemset.top : 0);
- changed += update(this, 'height', this.itemset ? this.itemset.height : 0);
+ if (this.itemSet) {
+ resized = this.itemSet.repaint() || resized;
+ }
- // TODO: reckon with the height of the group label
+ // calculate inner size of the label
+ resized = util.updateProperty(this.props.label, 'width', this.dom.inner.clientWidth) || resized;
+ resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized;
- if (this.label) {
- var inner = this.label.firstChild;
- changed += update(this.props.label, 'width', inner.clientWidth);
- changed += update(this.props.label, 'height', inner.clientHeight);
- }
- else {
- changed += update(this.props.label, 'width', 0);
- changed += update(this.props.label, 'height', 0);
- }
+ this.height = this.itemSet ? this.itemSet.height : 0;
+
+ this.dom.label.style.height = this.height + 'px';
- return (changed > 0);
+ return resized;
};
/**
* An GroupSet holds a set of groups
- * @param {Component} parent
- * @param {Component[]} [depends] Components on which this components depends
- * (except for the parent)
+ * @param {Panel} contentPanel Panel where the ItemSets will be created
+ * @param {Panel} labelPanel Panel where the labels will be created
+ * @param {Panel} backgroundPanel Panel where the vertical lines of box
+ * items are created
+ * @param {Panel} axisPanel Panel on the axis where the dots of box
+ * items will be created
* @param {Object} [options] See GroupSet.setOptions for the available
* options.
* @constructor GroupSet
* @extends Panel
*/
-function GroupSet(parent, depends, options) {
+function GroupSet(contentPanel, labelPanel, backgroundPanel, axisPanel, options) {
this.id = util.randomUUID();
- this.parent = parent;
- this.depends = depends;
+ this.contentPanel = contentPanel;
+ this.labelPanel = labelPanel;
+ this.backgroundPanel = backgroundPanel;
+ this.axisPanel = axisPanel;
this.options = options || {};
this.range = null; // Range or Object {start: number, end: number}
@@ -6870,6 +6590,7 @@ function GroupSet(parent, depends, options) {
this.groupsData = null; // DataSet with groups
this.groups = {}; // map with groups
+ this.groupIds = []; // list with ordered group ids
this.dom = {};
this.props = {
@@ -6878,10 +6599,7 @@ function GroupSet(parent, depends, options) {
}
};
- // TODO: implement right orientation of the labels
-
- // changes in groups are queued key/value map containing id/action
- this.queue = {};
+ // TODO: implement right orientation of the labels (left/right)
var me = this;
this.listeners = {
@@ -6895,10 +6613,40 @@ function GroupSet(parent, depends, options) {
me._onRemove(params.items);
}
};
+
+ // create HTML DOM
+ this._create();
}
GroupSet.prototype = new Panel();
+/**
+ * Create the HTML DOM elements for the GroupSet
+ * @private
+ */
+GroupSet.prototype._create = function _create () {
+ // TODO: reimplement groupSet DOM elements
+ var frame = document.createElement('div');
+ frame.className = 'groupset';
+ frame['timeline-groupset'] = this;
+ this.frame = frame;
+
+ this.labelSet = new Panel({
+ className: 'labelset',
+ width: '100%',
+ height: '100%'
+ });
+ this.labelPanel.appendChild(this.labelSet);
+};
+
+/**
+ * Get the frame element of component
+ * @returns {null} Get frame is not supported by GroupSet
+ */
+GroupSet.prototype.getFrame = function getFrame() {
+ return this.frame;
+};
+
/**
* Set options for the GroupSet. Existing options will be extended/overwritten.
* @param {Object} [options] The following options are available:
@@ -6907,13 +6655,23 @@ GroupSet.prototype = new Panel();
*/
GroupSet.prototype.setOptions = Component.prototype.setOptions;
-GroupSet.prototype.setRange = function (range) {
- // TODO: implement setRange
-};
-
/**
- * Set items
- * @param {vis.DataSet | null} items
+ * Set range (start and end).
+ * @param {Range | Object} range A Range or an object containing start and end.
+ */
+GroupSet.prototype.setRange = function (range) {
+ this.range = range;
+
+ for (var id in this.groups) {
+ if (this.groups.hasOwnProperty(id)) {
+ this.groups[id].setRange(range);
+ }
+ }
+};
+
+/**
+ * Set items
+ * @param {vis.DataSet | null} items
*/
GroupSet.prototype.setItems = function setItems(items) {
this.itemsData = items;
@@ -6921,6 +6679,7 @@ GroupSet.prototype.setItems = function setItems(items) {
for (var id in this.groups) {
if (this.groups.hasOwnProperty(id)) {
var group = this.groups[id];
+ // TODO: every group will emit a change event, causing a lot of unnecessary repaints. improve this.
group.setItems(items);
}
}
@@ -6982,13 +6741,15 @@ GroupSet.prototype.setGroups = function setGroups(groups) {
// subscribe to new dataset
var id = this.id;
util.forEach(this.listeners, function (callback, event) {
- me.groupsData.subscribe(event, callback, id);
+ me.groupsData.on(event, callback, id);
});
// draw all new groups
ids = this.groupsData.getIds();
this._onAdd(ids);
}
+
+ this.emit('change');
};
/**
@@ -7042,312 +6803,117 @@ GroupSet.prototype.getSelection = function getSelection() {
/**
* Repaint the component
- * @return {Boolean} changed
+ * @return {boolean} Returns true if the component was resized since previous repaint
*/
GroupSet.prototype.repaint = function repaint() {
- var changed = 0,
- i, id, group, label,
- update = util.updateProperty,
+ var i, id, group,
asSize = util.option.asSize,
- asElement = util.option.asElement,
+ asString = util.option.asString,
options = this.options,
- frame = this.dom.frame,
- labels = this.dom.labels,
- labelSet = this.dom.labelSet;
-
- // create frame
- if (!this.parent) {
- throw new Error('Cannot repaint groupset: no parent attached');
- }
- var parentContainer = this.parent.getContainer();
- if (!parentContainer) {
- throw new Error('Cannot repaint groupset: parent has no container element');
- }
- if (!frame) {
- frame = document.createElement('div');
- frame.className = 'groupset';
- this.dom.frame = frame;
-
- var className = options.className;
- if (className) {
- util.addClassName(frame, util.option.asString(className));
- }
-
- changed += 1;
- }
- if (!frame.parentNode) {
- parentContainer.appendChild(frame);
- changed += 1;
- }
-
- // create labels
- var labelContainer = asElement(options.labelContainer);
- if (!labelContainer) {
- throw new Error('Cannot repaint groupset: option "labelContainer" not defined');
- }
- if (!labels) {
- labels = document.createElement('div');
- labels.className = 'labels';
- this.dom.labels = labels;
- }
- if (!labelSet) {
- labelSet = document.createElement('div');
- labelSet.className = 'label-set';
- labels.appendChild(labelSet);
- this.dom.labelSet = labelSet;
- }
- if (!labels.parentNode || labels.parentNode != labelContainer) {
- if (labels.parentNode) {
- labels.parentNode.removeChild(labels.parentNode);
- }
- labelContainer.appendChild(labels);
- }
-
- // reposition frame
- changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
- 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%'));
-
- // reposition labels
- changed += update(labelSet.style, 'top', asSize(options.top, '0px'));
- changed += update(labelSet.style, 'height', asSize(options.height, this.height + 'px'));
-
- var me = this,
- queue = this.queue,
- groups = this.groups,
- groupsData = this.groupsData;
-
- // show/hide added/changed/removed groups
- var ids = Object.keys(queue);
- if (ids.length) {
- ids.forEach(function (id) {
- var action = queue[id];
- var group = groups[id];
-
- //noinspection FallthroughInSwitchStatementJS
- switch (action) {
- case 'add':
- case 'update':
- if (!group) {
- var groupOptions = Object.create(me.options);
- util.extend(groupOptions, {
- height: null,
- maxHeight: null
- });
-
- group = new Group(me, id, groupOptions);
- group.setItems(me.itemsData); // attach items data
- groups[id] = group;
-
- me.controller.add(group);
- }
-
- // TODO: update group data
- group.data = groupsData.get(id);
-
- delete queue[id];
- break;
-
- case 'remove':
- if (group) {
- group.setItems(); // detach items data
- delete groups[id];
-
- me.controller.remove(group);
- }
-
- // update lists
- delete queue[id];
- break;
-
- default:
- console.log('Error: unknown action "' + action + '"');
- }
- });
-
- // the groupset depends on each of the groups
- //this.depends = this.groups; // TODO: gives a circular reference through the parent
-
- // TODO: apply dependencies of the groupset
-
- // update the top positions of the groups in the correct order
- var orderedGroups = this.groupsData.getIds({
- order: this.options.groupOrder
- });
- for (i = 0; i < orderedGroups.length; i++) {
- (function (group, prevGroup) {
- var top = 0;
- if (prevGroup) {
- top = function () {
- // TODO: top must reckon with options.maxHeight
- return prevGroup.top + prevGroup.height;
- }
- }
- group.setOptions({
- top: top
- });
- })(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]);
- }
-
- // (re)create the labels
- while (labelSet.firstChild) {
- labelSet.removeChild(labelSet.firstChild);
- }
- for (i = 0; i < orderedGroups.length; i++) {
- id = orderedGroups[i];
- label = this._createLabel(id);
- labelSet.appendChild(label);
- }
+ orientation = this.getOption('orientation'),
+ frame = this.frame,
+ resized = false,
+ groups = this.groups;
- changed++;
- }
+ // repaint all groups in order
+ this.groupIds.forEach(function (id) {
+ var groupResized = groups[id].repaint();
+ resized = resized || groupResized;
+ });
- // reposition the labels
- // TODO: labels are not displayed correctly when orientation=='top'
- // TODO: width of labelPanel is not immediately updated on a change in groups
+ // reposition the labels and calculate the maximum label width
+ var maxWidth = 0;
for (id in groups) {
if (groups.hasOwnProperty(id)) {
group = groups[id];
- label = group.label;
- if (label) {
- label.style.top = group.top + 'px';
- label.style.height = group.height + 'px';
- }
+ maxWidth = Math.max(maxWidth, group.props.label.width);
}
}
+ resized = util.updateProperty(this.props.labels, 'width', maxWidth) || resized;
- return (changed > 0);
-};
-
-/**
- * Create a label for group with given id
- * @param {Number} id
- * @return {Element} label
- * @private
- */
-GroupSet.prototype._createLabel = function(id) {
- var group = this.groups[id];
- var label = document.createElement('div');
- label.className = 'label';
- var inner = document.createElement('div');
- inner.className = 'inner';
- label.appendChild(inner);
+ // recalculate the height of the groupset, and recalculate top positions of the groups
+ var fixedHeight = (asSize(options.height) != null);
+ var height;
+ if (!fixedHeight) {
+ // height is not specified, calculate the sum of the height of all groups
+ height = 0;
- var content = group.data && group.data.content;
- if (content instanceof Element) {
- inner.appendChild(content);
- }
- else if (content != undefined) {
- inner.innerHTML = content;
+ this.groupIds.forEach(function (id) {
+ var group = groups[id];
+ group.top = height;
+ if (group.itemSet) group.itemSet.top = group.top; // TODO: this is an ugly hack
+ height += group.height;
+ });
}
- var className = group.data && group.data.className;
- if (className) {
- util.addClassName(label, className);
- }
+ // update classname
+ frame.className = 'groupset' + (options.className ? (' ' + asString(options.className)) : '');
- group.label = label; // TODO: not so nice, parking labels in the group this way!!!
+ // calculate actual size and position
+ this.top = frame.offsetTop;
+ this.left = frame.offsetLeft;
+ this.width = frame.offsetWidth;
+ this.height = height;
- return label;
+ return resized;
};
/**
- * Get container element
- * @return {HTMLElement} container
+ * Update the groupIds. Requires a repaint afterwards
+ * @private
*/
-GroupSet.prototype.getContainer = function getContainer() {
- return this.dom.frame;
+GroupSet.prototype._updateGroupIds = function () {
+ // reorder the groups
+ this.groupIds = this.groupsData.getIds({
+ order: this.options.groupOrder
+ });
+
+ // hide the groups now, they will be shown again in the next repaint
+ // in correct order
+ var groups = this.groups;
+ this.groupIds.forEach(function (id) {
+ groups[id].hide();
+ });
};
/**
* Get the width of the group labels
* @return {Number} width
*/
-GroupSet.prototype.getLabelsWidth = function getContainer() {
+GroupSet.prototype.getLabelsWidth = function getLabelsWidth() {
return this.props.labels.width;
};
-/**
- * Reflow the component
- * @return {Boolean} resized
- */
-GroupSet.prototype.reflow = function reflow() {
- var changed = 0,
- id, group,
- options = this.options,
- update = util.updateProperty,
- asNumber = util.option.asNumber,
- asSize = util.option.asSize,
- frame = this.dom.frame;
-
- if (frame) {
- var maxHeight = asNumber(options.maxHeight);
- var fixedHeight = (asSize(options.height) != null);
- var height;
- if (fixedHeight) {
- height = frame.offsetHeight;
- }
- else {
- // height is not specified, calculate the sum of the height of all groups
- height = 0;
-
- for (id in this.groups) {
- if (this.groups.hasOwnProperty(id)) {
- group = this.groups[id];
- height += group.height;
- }
- }
- }
- if (maxHeight != null) {
- height = Math.min(height, maxHeight);
- }
- changed += update(this, 'height', height);
-
- changed += update(this, 'top', frame.offsetTop);
- changed += update(this, 'left', frame.offsetLeft);
- changed += update(this, 'width', frame.offsetWidth);
- }
-
- // calculate the maximum width of the labels
- var width = 0;
- for (id in this.groups) {
- if (this.groups.hasOwnProperty(id)) {
- group = this.groups[id];
- var labelWidth = group.props && group.props.label && group.props.label.width || 0;
- width = Math.max(width, labelWidth);
- }
- }
- changed += update(this.props.labels, 'width', width);
-
- return (changed > 0);
-};
-
/**
* Hide the component from the DOM
- * @return {Boolean} changed
*/
GroupSet.prototype.hide = function hide() {
- if (this.dom.frame && this.dom.frame.parentNode) {
- this.dom.frame.parentNode.removeChild(this.dom.frame);
- return true;
- }
- else {
- return false;
+ // hide labelset
+ this.labelPanel.removeChild(this.labelSet);
+
+ // hide each of the groups
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ this.groups[groupId].hide();
+ }
}
};
/**
* Show the component in the DOM (when not already visible).
- * A repaint will be executed when the component is not visible
* @return {Boolean} changed
*/
GroupSet.prototype.show = function show() {
- if (!this.dom.frame || !this.dom.frame.parentNode) {
- return this.repaint();
+ // show label set
+ if (!this.labelPanel.hasChild(this.labelSet)) {
+ this.labelPanel.removeChild(this.labelSet);
}
- else {
- return false;
+
+ // show each of the groups
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ this.groups[groupId].show();
+ }
}
};
@@ -7357,7 +6923,7 @@ GroupSet.prototype.show = function show() {
* @private
*/
GroupSet.prototype._onUpdate = function _onUpdate(ids) {
- this._toQueue(ids, 'update');
+ this._onAdd(ids);
};
/**
@@ -7366,7 +6932,31 @@ GroupSet.prototype._onUpdate = function _onUpdate(ids) {
* @private
*/
GroupSet.prototype._onAdd = function _onAdd(ids) {
- this._toQueue(ids, 'add');
+ var me = this;
+
+ ids.forEach(function (id) {
+ var group = me.groups[id];
+ if (!group) {
+ var groupOptions = Object.create(me.options);
+ util.extend(groupOptions, {
+ height: null
+ });
+
+ group = new Group(me, me.labelSet, me.backgroundPanel, me.axisPanel, id, groupOptions);
+ group.on('change', me.emit.bind(me, 'change')); // propagate change event
+ group.setRange(me.range);
+ group.setItems(me.itemsData); // attach items data
+ me.groups[id] = group;
+ group.parent = me;
+ }
+
+ // update group data
+ group.setData(me.groupsData.get(id));
+ });
+
+ this._updateGroupIds();
+
+ this.emit('change');
};
/**
@@ -7375,150 +6965,310 @@ GroupSet.prototype._onAdd = function _onAdd(ids) {
* @private
*/
GroupSet.prototype._onRemove = function _onRemove(ids) {
- this._toQueue(ids, 'remove');
+ var groups = this.groups;
+ ids.forEach(function (id) {
+ var group = groups[id];
+
+ if (group) {
+ group.setItems(); // detach items data
+ group.hide(); // FIXME: for some reason when doing setItems after hide, setItems again makes the label visible
+ delete groups[id];
+ }
+ });
+
+ this._updateGroupIds();
+
+ this.emit('change');
};
/**
- * Put groups in the queue to be added/updated/remove
- * @param {Number[]} ids
- * @param {String} action can be 'add', 'update', 'remove'
+ * Find the GroupSet from an event target:
+ * searches for the attribute 'timeline-groupset' in the event target's element
+ * tree, then finds the right group in this groupset
+ * @param {Event} event
+ * @return {Group | null} group
*/
-GroupSet.prototype._toQueue = function _toQueue(ids, action) {
- var queue = this.queue;
- ids.forEach(function (id) {
- queue[id] = action;
- });
+GroupSet.groupSetFromTarget = function groupSetFromTarget (event) {
+ var target = event.target;
+ while (target) {
+ if (target.hasOwnProperty('timeline-groupset')) {
+ return target['timeline-groupset'];
+ }
+ target = target.parentNode;
+ }
- if (this.controller) {
- //this.requestReflow();
- this.requestRepaint();
+ return null;
+};
+
+/**
+ * Find the Group from an event target:
+ * searches for the two elements having attributes 'timeline-groupset' and
+ * 'timeline-itemset' in the event target's element, then finds the right group.
+ * @param {Event} event
+ * @return {Group | null} group
+ */
+GroupSet.groupFromTarget = function groupFromTarget (event) {
+ // find the groupSet
+ var groupSet = GroupSet.groupSetFromTarget(event);
+
+ // find the ItemSet
+ var itemSet = ItemSet.itemSetFromTarget(event);
+
+ // find the right group
+ if (groupSet && itemSet) {
+ for (var groupId in groupSet.groups) {
+ if (groupSet.groups.hasOwnProperty(groupId)) {
+ var group = groupSet.groups[groupId];
+ if (group.itemSet == itemSet) {
+ return group;
+ }
+ }
+ }
}
+
+ return null;
};
/**
* 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));
+ // 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);
- // multi select when holding mouse/touch, or on ctrl+click
- this.rootPanel.on('hold', this._onMultiSelectItem.bind(this));
+ // 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);
- // 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);
+ // 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);
@@ -7537,6 +7287,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
@@ -7548,8 +7301,53 @@ 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);
+ }
+ }
+
+ // repaint everything
+ this.rootPanel.repaint();
};
/**
@@ -7557,7 +7355,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);
};
/**
@@ -7565,37 +7367,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
@@ -7631,78 +7437,62 @@ Timeline.prototype.setItems = function(items) {
/**
* 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;
+ 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
+ });
- // 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);
- }
+ if (this.groupsData) {
+ // Create a GroupSet
- // 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();
- }
- });
+ // 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);
}
};
@@ -7753,7 +7543,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);
};
/**
@@ -7761,45 +7553,34 @@ 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() : [];
-};
+ var itemOrGroupSet = (this.itemSet || this.groupSet);
-/**
- * Add event listener
- * @param {String} event Event name. Available events:
- * 'rangechange', 'rangechanged', 'select'
- * @param {function} callback Callback function, invoked as callback(properties)
- * where properties is an optional object containing
- * event specific properties.
- */
-Timeline.prototype.on = function on (event, callback) {
- var available = ['rangechange', 'rangechanged', 'select'];
-
- if (available.indexOf(event) == -1) {
- throw new Error('Unknown event "' + event + '". Choose from ' + available.join());
- }
-
- events.addListener(this, event, callback);
+ 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.
+ * @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);
+// TODO: implement support for setWindow({start: ..., end: ...})
+// TODO: rename setWindow to setRange?
+Timeline.prototype.setWindow = function setWindow(start, end) {
+ 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 || {});
+// TODO: rename getWindow to getRange?
+Timeline.prototype.getWindow = function setWindow() {
+ var range = this.range.getRange();
+ return {
+ start: new Date(range.start),
+ end: new Date(range.end)
+ };
};
/**
@@ -7807,70 +7588,142 @@ 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);
+
+ if (item) {
+ // update item
- 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);
+ // 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);
+ // 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'
+ };
- this._trigger('select', {
- items: this.getSelection()
- });
+ var id = util.randomUUID();
+ newItem[this.itemsData.fieldId] = id;
- event.stopPropagation();
+ var group = GroupSet.groupFromTarget(event);
+ if (group) {
+ newItem.group = group.groupId;
+ }
+
+ // 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);
}
- target = target.parentNode;
+ else {
+ // item is already selected -> deselect it
+ selection.splice(index, 1);
+ }
+ 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;
};
+
(function(exports) {
/**
* Parse a text source containing data in DOT language into a JSON object.
@@ -8950,6 +8803,7 @@ if (typeof CanvasRenderingContext2D !== 'undefined') {
* 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;
@@ -8962,6 +8816,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;
@@ -8969,8 +8824,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
@@ -8980,11 +8835,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
@@ -8994,20 +8861,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;
}
/**
@@ -9034,7 +8896,6 @@ Node.prototype.attachEdge = function(edge) {
this.dynamicEdges.push(edge);
}
this.dynamicEdgesLength = this.dynamicEdges.length;
- this._updateMass();
};
/**
@@ -9048,17 +8909,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
@@ -9078,6 +8930,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;}
@@ -9102,13 +8959,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);
}
@@ -9117,8 +8974,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') {
@@ -9146,46 +9003,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
*/
@@ -9225,7 +9042,7 @@ Node.prototype._reset = function() {
* has been set.
*/
Node.prototype.getTitle = function() {
- return this.title;
+ return typeof this.title === "function" ? this.title() : this.title;
};
/**
@@ -9241,7 +9058,6 @@ Node.prototype.distanceToBorder = function (ctx, angle) {
this.resize(ctx);
}
- //noinspection FallthroughInSwitchStatementJS
switch (this.shape) {
case 'circle':
case 'dot':
@@ -9273,7 +9089,6 @@ Node.prototype.distanceToBorder = function (ctx, angle) {
}
}
-
// TODO: implement calculation of distance to border for all shapes
};
@@ -9304,21 +9119,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
@@ -9334,16 +9178,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);
};
/**
@@ -9448,10 +9283,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;
}
}
@@ -9496,9 +9333,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;
+
}
};
@@ -9545,9 +9384,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;
}
};
@@ -9594,9 +9434,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;
}
};
@@ -9640,11 +9481,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;
}
};
@@ -9707,9 +9550,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;
}
};
@@ -9766,9 +9610,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);
}
};
@@ -9782,7 +9627,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";
@@ -9836,7 +9681,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
@@ -9847,7 +9692,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
@@ -9859,6 +9704,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;
};
@@ -9871,17 +9717,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;
-};
/**
@@ -9900,8 +9738,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);
};
@@ -9939,10 +9779,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.
@@ -9956,13 +9800,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);
-
}
/**
@@ -9981,18 +9824,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
@@ -10003,14 +9852,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) {
@@ -10068,7 +9925,7 @@ Edge.prototype.disconnect = function () {
* has been set.
*/
Edge.prototype.getTitle = function() {
- return this.title;
+ return typeof this.title === "function" ? this.title() : this.title;
};
@@ -10109,19 +9966,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
+ }
};
@@ -10134,17 +9994,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);
}
}
@@ -10176,7 +10044,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 {
@@ -10193,7 +10061,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();
};
@@ -10225,7 +10098,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;
@@ -10252,32 +10125,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);
}
};
@@ -10322,43 +10250,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);
- ctx.arrow(point.x, point.y, angle, length);
- ctx.fill();
- ctx.stroke();
-
- // draw label
- if (this.label) {
+ // 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) {
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);
@@ -10389,43 +10324,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 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 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 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);
}
}
@@ -10433,12 +10396,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,
@@ -10448,7 +10411,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,
@@ -10456,7 +10419,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();
@@ -10489,31 +10451,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;
- var x = x1 + u * px,
- y = y1 + u * py,
- dx = x - x3,
- dy = y - y3;
+ if (u > 1) {
+ u = 1;
+ }
+ else if (u < 0) {
+ u = 0;
+ }
- //# 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
+ var x = x1 + u * px,
+ y = y1 + u * py,
+ dx = x - x3,
+ dy = y - y3;
- return Math.sqrt(dx*dx + dy*dy);
+ //# 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);
+ }
};
@@ -10526,20 +10503,61 @@ 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);
+ }
+};
/**
* Popup is a class to create a popup window with some text
* @param {Element} container The container object.
* @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;
@@ -10553,18 +10571,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);
}
@@ -10672,89 +10692,2006 @@ Groups.prototype.clear = function () {
i++;
}
}
- return i;
- }
-};
-
+ return i;
+ }
+};
+
+
+/**
+ * get group properties of a groupname. If groupname is not found, a new group
+ * is added.
+ * @param {*} groupname Can be a number, string, Date, etc.
+ * @return {Object} group The created group, containing all group properties
+ */
+Groups.prototype.get = function (groupname) {
+ var group = this.groups[groupname];
+
+ if (group == undefined) {
+ // create new group
+ var index = this.defaultIndex % Groups.DEFAULT.length;
+ this.defaultIndex++;
+ group = {};
+ group.color = Groups.DEFAULT[index];
+ this.groups[groupname] = group;
+ }
+
+ return group;
+};
+
+/**
+ * Add a custom group style
+ * @param {String} groupname
+ * @param {Object} style An object containing borderColor,
+ * backgroundColor, etc.
+ * @return {Object} group The created group object
+ */
+Groups.prototype.add = function (groupname, style) {
+ this.groups[groupname] = style;
+ if (style.color) {
+ style.color = util.parseColor(style.color);
+ }
+ return style;
+};
+
+/**
+ * @class Images
+ * This class loads images and keeps them stored.
+ */
+Images = function () {
+ this.images = {};
+
+ this.callback = undefined;
+};
+
+/**
+ * Set an onload callback function. This will be called each time an image
+ * is loaded
+ * @param {function} callback
+ */
+Images.prototype.setOnloadCallback = function(callback) {
+ this.callback = callback;
+};
+
+/**
+ *
+ * @param {string} url Url of the image
+ * @return {Image} img The image object
+ */
+Images.prototype.load = function(url) {
+ var img = this.images[url];
+ if (img == undefined) {
+ // create the image
+ var images = this;
+ img = new Image();
+ this.images[url] = img;
+ img.onload = function() {
+ if (images.callback) {
+ images.callback(this);
+ }
+ };
+ img.src = url;
+ }
+
+ return img;
+};
+
+/**
+ * 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 = '' +
+ '
' +
+ '' +
+ '' +
+ '' +
+ ''
+ 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();
+};
+
+
+
+/**
+ * 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;
+ }
+ }
+ }
+ }
+};
+/**
+ * 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();
+ }
+ */
+ }
+
+};
+/**
+ * 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;
+ }
+ }
+ }
+ }
+};
+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;
+ }
+ }
+ }
+
+
+};
+/**
+ * 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 = "" +
+ "" +
+ ""+this.constants.labels['add'] +"" +
+ "" +
+ "" +
+ ""+this.constants.labels['link'] +"";
+ if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
+ this.manipulationDiv.innerHTML += "" +
+ "" +
+ "" +
+ ""+this.constants.labels['editNode'] +"";
+ }
+ if (this._selectionIsEmpty() == false) {
+ this.manipulationDiv.innerHTML += "" +
+ "" +
+ "" +
+ ""+this.constants.labels['del'] +"";
+ }
+
+
+ // 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 = "" +
+ "" +
+ "" + this.constants.labels['edit'] + "";
+ 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 = "" +
+ "" +
+ "" + this.constants.labels['back'] + " " +
+ "" +
+ "" +
+ "" + this.constants.labels['addDescription'] + "";
+
+ // 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 = "" +
+ "" +
+ "" + this.constants.labels['back'] + " " +
+ "" +
+ "" +
+ "" + this.constants.labels['linkDescription'] + "";
+
+ // 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();
+ }
+ },
-/**
- * get group properties of a groupname. If groupname is not found, a new group
- * is added.
- * @param {*} groupname Can be a number, string, Date, etc.
- * @return {Object} group The created group, containing all group properties
- */
-Groups.prototype.get = function (groupname) {
- var group = this.groups[groupname];
- if (group == undefined) {
- // create new group
- var index = this.defaultIndex % Groups.DEFAULT.length;
- this.defaultIndex++;
- group = {};
- group.color = Groups.DEFAULT[index];
- this.groups[groupname] = group;
- }
+ /**
+ * 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();
+ }
+ }
+ },
- return group;
-};
-/**
- * Add a custom group style
- * @param {String} groupname
- * @param {Object} style An object containing borderColor,
- * backgroundColor, etc.
- * @return {Object} group The created group object
- */
-Groups.prototype.add = function (groupname, style) {
- this.groups[groupname] = style;
- if (style.color) {
- style.color = Node.parseColor(style.color);
- }
- return style;
-};
+ /**
+ * 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();
+ }
+ }
+ },
-/**
- * @class Images
- * This class loads images and keeps them stored.
- */
-Images = function () {
- this.images = {};
- this.callback = undefined;
-};
+ /**
+ * 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"]);
+ }
+ },
-/**
- * Set an onload callback function. This will be called each time an image
- * is loaded
- * @param {function} callback
- */
-Images.prototype.setOnloadCallback = function(callback) {
- this.callback = callback;
-};
-/**
- *
- * @param {string} url Url of the image
- * @return {Image} img The image object
- */
-Images.prototype.load = function(url) {
- var img = this.images[url];
- if (img == undefined) {
- // create the image
- var images = this;
- img = new Image();
- this.images[url] = img;
- img.onload = function() {
- if (images.callback) {
- images.callback(this);
+ /**
+ * 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();
+ }
}
- };
- img.src = url;
+ else {
+ alert(this.constants.labels["deleteClusterError"]);
+ }
+ }
}
-
- return img;
};
-
/**
* Creation of the SectorMixin var.
*
@@ -10815,28 +12752,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"];
},
@@ -11106,6 +13044,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();
}
}
},
@@ -11150,6 +13091,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().
*
@@ -11188,33 +13158,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().
*
@@ -11240,7 +13183,6 @@ var SectorMixin = {
this._doInAllFrozenSectors(runFunction,argument);
}
}
-
},
@@ -11312,23 +13254,24 @@ var SectorMixin = {
*/
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
@@ -11345,20 +13288,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
@@ -11370,12 +13316,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);
@@ -11383,6 +13333,7 @@ var ClusterMixin = {
// update the index list, dynamic edges and labels
this._updateNodeIndexList();
this._updateDynamicEdges();
+ this._updateCalculationNodes();
this.updateLabels();
}
@@ -11390,7 +13341,8 @@ var ClusterMixin = {
if (this.moving != isMovingBeforeClustering) {
this.start();
}
- },
+ },
+
/**
* This calls the updateClustes with default arguments
@@ -11401,6 +13353,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.
@@ -11408,8 +13361,7 @@ var ClusterMixin = {
*/
increaseClusterLevel : function() {
this.updateClusters(-1,false,true);
- },
-
+ },
/**
@@ -11419,7 +13371,7 @@ var ClusterMixin = {
*/
decreaseClusterLevel : function() {
this.updateClusters(1,false,true);
- },
+ },
/**
@@ -11433,7 +13385,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;
@@ -11482,13 +13434,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().
@@ -11500,7 +13458,7 @@ var ClusterMixin = {
this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage)
}
- },
+ },
/**
* this functions starts clustering by hubs
@@ -11511,14 +13469,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;
@@ -11534,11 +13492,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
@@ -11557,7 +13517,7 @@ var ClusterMixin = {
}
}
}
- },
+ },
/**
@@ -11570,8 +13530,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.
@@ -11617,7 +13578,7 @@ var ClusterMixin = {
}
}
}
- },
+ },
/**
* ONLY CALLED FROM _expandClusterNode
@@ -11634,11 +13595,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;
@@ -11652,14 +13616,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];
@@ -11679,21 +13643,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();
+ }
+ },
/**
@@ -11712,7 +13692,8 @@ var ClusterMixin = {
else {
this._forceClustersByZoom();
}
- },
+ },
+
/**
* This function handles the clustering by zooming out, this is based on a minimum edge distance
@@ -11755,7 +13736,7 @@ var ClusterMixin = {
}
}
}
- },
+ },
/**
* This function forces the graph to cluster all nodes with only one connecting edge to their
@@ -11786,8 +13767,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);
+ }
+ },
/**
@@ -11805,7 +13819,7 @@ var ClusterMixin = {
this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual);
}
}
- },
+ },
/**
* This function forms a cluster from a specific preselected hub node
@@ -11875,7 +13889,7 @@ var ClusterMixin = {
}
}
}
- },
+ },
@@ -11914,9 +13928,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) {
@@ -11946,12 +13960,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
*/
@@ -11976,7 +13990,7 @@ var ClusterMixin = {
}
node.dynamicEdgesLength -= correction;
}
- },
+ },
/**
@@ -12005,7 +14019,7 @@ var ClusterMixin = {
break;
}
}
- },
+ },
/**
* This function connects an edge that was connected to a child node to the parent node.
@@ -12036,9 +14050,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++) {
@@ -12109,7 +14131,7 @@ var ClusterMixin = {
// remove the entry from the rerouted edges
delete parentNode.reroutedEdges[childNode.id];
}
- },
+ },
/**
@@ -12127,7 +14149,7 @@ var ClusterMixin = {
parentNode.dynamicEdges.splice(i,1);
}
}
- },
+ },
/**
@@ -12152,7 +14174,7 @@ var ClusterMixin = {
// remove the entry from the contained edges
delete parentNode.containedEdges[childNode.id];
- },
+ },
@@ -12190,15 +14212,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;
+ }
+ }
+ },
- },
/**
@@ -12215,7 +14279,7 @@ var ClusterMixin = {
&&
Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
)
- },
+ },
/**
@@ -12226,17 +14290,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);
}
}
- },
-
-
-
+ },
/**
@@ -12252,6 +14314,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;
@@ -12276,7 +14339,7 @@ var ClusterMixin = {
// console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
// console.log("hubThreshold:",this.hubThreshold);
- },
+ },
/**
@@ -12299,7 +14362,7 @@ var ClusterMixin = {
}
}
}
- },
+ },
/**
* We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
@@ -12319,7 +14382,8 @@ var ClusterMixin = {
}
}
return chains/total;
- }
+ }
+
};
@@ -12356,18 +14420,6 @@ var SelectionMixin = {
},
- /**
- * 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
*
@@ -12385,42 +14437,6 @@ var SelectionMixin = {
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)
@@ -12432,7 +14448,7 @@ var SelectionMixin = {
_getNodeAt : function (pointer) {
// we first check if this is an navigation controls element
var positionObject = this._pointerToPositionObject(pointer);
- overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
+ 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
@@ -12445,6 +14461,36 @@ var SelectionMixin = {
},
+ /**
+ * 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.
@@ -12454,36 +14500,48 @@ var SelectionMixin = {
* @private
*/
_getEdgeAt : function(pointer) {
- return null;
+ 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. The this.selection id array may not be needed.
+ * Add object to the selection array.
*
* @param obj
* @private
*/
_addToSelection : function(obj) {
- this.selection.push(obj.id);
- this.selectionObj[obj.id] = 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 obj
+ * @param {Object} 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;
- }
+ if (obj instanceof Node) {
+ delete this.selectionObj.nodes[obj.id];
+ }
+ else {
+ delete this.selectionObj.edges[obj.id];
}
- delete this.selectionObj[obj.id];
},
@@ -12497,70 +14555,220 @@ var SelectionMixin = {
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.selection = [];
- for (var objId in this.selectionObj) {
- if (this.selectionObj.hasOwnProperty(objId)) {
- this.selectionObj[objId].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]);
+ }
}
}
- this.selectionObj = {};
if (doNotTrigger == false) {
- this._trigger('select', {
- nodes: this.getSelection()
- });
+ 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);
}
},
/**
- * Check if anything is selected
+ * unselect the edges connected to the node that is being selected
*
- * @returns {boolean}
+ * @param {Node} node
* @private
*/
- _selectionIsEmpty : function() {
- if (this.selection.length == 0) {
- return true;
- }
- else {
- return false;
+ _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} node
+ * @param {Node || Edge} object
* @param {Boolean} append
* @param {Boolean} [doNotTrigger] | ignore trigger
* @private
*/
- _selectNode : function(node, append, doNotTrigger) {
+ _selectObject : function(object, append, doNotTrigger) {
if (doNotTrigger === undefined) {
doNotTrigger = false;
}
- if (this._selectionIsEmpty() == false && append == false) {
+ if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) {
this._unselectAll(true);
}
-
- if (node.selected == false) {
- node.select();
- this._addToSelection(node);
+ if (object.selected == false) {
+ object.select();
+ this._addToSelection(object);
+ if (object instanceof Node && this.blockConnectingEdgeSelection == false) {
+ this._selectConnectedEdges(object);
+ }
}
else {
- node.unselect();
- this._removeFromSelection(node);
+ object.unselect();
+ this._removeFromSelection(object);
}
if (doNotTrigger == false) {
- this._trigger('select', {
- nodes: this.getSelection()
- });
+ this.emit('select', this.getSelection());
}
},
@@ -12574,14 +14782,7 @@ var SelectionMixin = {
* @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]();
- }
- }
- }
+
},
@@ -12594,11 +14795,18 @@ var SelectionMixin = {
_handleTap : function(pointer) {
var node = this._getNodeAt(pointer);
if (node != null) {
- this._selectNode(node,false);
+ this._selectObject(node,false);
}
else {
- this._unselectAll();
+ var edge = this._getEdgeAt(pointer);
+ if (edge != null) {
+ this._selectObject(edge,false);
+ }
+ else {
+ this._unselectAll();
+ }
}
+ this.emit("click", this.getSelection());
this._redraw();
},
@@ -12617,6 +14825,7 @@ var SelectionMixin = {
"y" : this._canvasToY(pointer.y)};
this.openCluster(node);
}
+ this.emit("doubleClick", this.getSelection());
},
@@ -12629,7 +14838,13 @@ var SelectionMixin = {
_handleOnHold : function(pointer) {
var node = this._getNodeAt(pointer);
if (node != null) {
- this._selectNode(node,true);
+ this._selectObject(node,true);
+ }
+ else {
+ var edge = this._getEdgeAt(pointer);
+ if (edge != null) {
+ this._selectObject(edge,true);
+ }
}
this._redraw();
},
@@ -12640,38 +14855,58 @@ var SelectionMixin = {
*
* @private
*/
- _handleOnRelease : function() {
- this.xIncrement = 0;
- this.yIncrement = 0;
- this.zoomIncrement = 0;
- this._unHighlightAll();
+ _handleOnRelease : function(pointer) {
+
},
/**
*
- * retrieve the currently selected nodes
+ * retrieve the currently selected objects
* @return {Number[] | String[]} selection An array with the ids of the
* selected nodes.
*/
getSelection : function() {
- return this.selection.concat([]);
+ var nodeIds = this.getSelectedNodes();
+ var edgeIds = this.getSelectedEdges();
+ return {nodes:nodeIds, edges:edgeIds};
},
/**
*
- * retrieve the currently selected nodes as objects
- * @return {Objects} selection An array with the ids of the
+ * retrieve the currently selected nodes
+ * @return {String} selection An array with the ids of the
* selected nodes.
*/
- getSelectionObjects : function() {
- return this.selectionObj;
+ getSelectedNodes : function() {
+ var idArray = [];
+ for(var nodeId in this.selectionObj.nodes) {
+ if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ idArray.push(nodeId);
+ }
+ }
+ return idArray
},
/**
- * // TODO: rework this function, it is from the old system
*
+ * 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.
@@ -12692,187 +14927,51 @@ var SelectionMixin = {
if (!node) {
throw new RangeError('Node with id "' + id + '" not found');
}
- this._selectNode(node,true,true);
+ this._selectObject(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();
+ for(var nodeId in this.selectionObj.nodes) {
+ if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
+ if (!this.nodes.hasOwnProperty(nodeId)) {
+ delete this.selectionObj.nodes[nodeId];
}
- 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;
+ for(var edgeId in this.selectionObj.edges) {
+ if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
+ if (!this.edges.hasOwnProperty(edgeId)) {
+ delete this.selectionObj.edges[edgeId];
}
}
}
- 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;
- },
- */
+ }
};
-
/**
* 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;
- }
- }
- }
+ _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
@@ -12882,83 +14981,48 @@ var NavigationMixin = {
* @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);
- }
- },
+ this._cleanNavigation();
+ this.navigationDivs = {};
+ var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends'];
+ var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent'];
- /**
- * 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;
+ 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);
+ },
/**
- * Reverting back to a normal button
+ * this stops all movement induced by the navigation buttons
*
- * @param {String} elementId
* @private
*/
- _unHighlightNavigationElement : function(elementId) {
- if (this.sectors["navigation"]["nodes"].hasOwnProperty(elementId)) {
- this.sectors["navigation"]["nodes"][elementId].clusterSize = 1;
- }
+ _stopMovement : function() {
+ this._xStopMoving();
+ this._yStopMoving();
+ this._stopZoom();
},
+
/**
- * un-highlight (for lack of a better term) all navigation controls elements
+ * stops the actions performed by page up and down etc.
+ *
+ * @param event
* @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) {
@@ -12979,10 +15043,12 @@ var NavigationMixin = {
* @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);
+ if (this.navigationDivs) {
+ this.navigationDivs['up'].className += " active";
+ }
},
@@ -12991,10 +15057,12 @@ var NavigationMixin = {
* @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);
+ if (this.navigationDivs) {
+ this.navigationDivs['down'].className += " active";
+ }
},
@@ -13003,10 +15071,12 @@ var NavigationMixin = {
* @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);
+ if (this.navigationDivs) {
+ this.navigationDivs['left'].className += " active";
+ }
},
@@ -13015,10 +15085,12 @@ var NavigationMixin = {
* @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);
+ if (this.navigationDivs) {
+ this.navigationDivs['right'].className += " active";
+ }
},
@@ -13027,10 +15099,12 @@ var NavigationMixin = {
* @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);
+ if (this.navigationDivs) {
+ this.navigationDivs['zoomIn'].className += " active";
+ }
},
@@ -13039,10 +15113,12 @@ var NavigationMixin = {
* @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);
+ if (this.navigationDivs) {
+ this.navigationDivs['zoomOut'].className += " active";
+ }
},
@@ -13051,10 +15127,11 @@ var NavigationMixin = {
* @private
*/
_stopZoom : function() {
- this._unHighlightNavigationElement("navigation_plus");
- this._unHighlightNavigationElement("navigation_min");
-
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","");
+ }
},
@@ -13063,10 +15140,11 @@ var NavigationMixin = {
* @private
*/
_yStopMoving : function() {
- this._unHighlightNavigationElement("navigation_up");
- this._unHighlightNavigationElement("navigation_down");
-
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","");
+ }
},
@@ -13075,15 +15153,216 @@ var NavigationMixin = {
* @private
*/
_xStopMoving : function() {
- this._unHighlightNavigationElement("navigation_left");
- this._unHighlightNavigationElement("navigation_right");
-
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","");
+ }
}
};
+/**
+ * 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);
+ }
+
+};
+
/**
* @constructor Graph
* Create a graph visualization, displaying nodes and edges.
@@ -13096,17 +15375,27 @@ var NavigationMixin = {
* @param {Object} options Options
*/
function Graph (container, data, options) {
+
+ this._initializeMixinLoaders();
+
// create variables and set default values
this.containerElement = container;
this.width = '100%';
this.height = '100%';
- // to give everything a nice fluidity, we seperate the rendering and calculating of the forces
- this.renderRefreshRate = 60; // hz (fps)
+
+ // render and calculation settings
+ this.renderRefreshRate = 60; // hz (fps)
this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
- this.stabilize = true; // stabilize before displaying the graph
+ this.renderTime = 0.5 * this.renderTimestep; // measured time it takes to render a frame
+ this.maxPhysicsTicksPerRender = 3; // max amount of physics ticks per render step.
+ this.physicsDiscreteStepsize = 0.65; // discrete stepsize of the simulation
+
+ this.stabilize = true; // stabilize before displaying the graph
this.selectable = true;
+ this.initializing = true;
- this.forceFactor = 50000;
+ // these functions are triggered when the dataset is edited
+ this.triggerFunctions = {add:null,edit:null,connect:null,del:null};
// set constant values
this.constants = {
@@ -13114,15 +15403,15 @@ function Graph (container, data, options) {
radiusMin: 5,
radiusMax: 20,
radius: 5,
- distance: 100, // px
shape: 'ellipse',
image: undefined,
widthMin: 16, // px
widthMax: 64, // px
+ fixed: false,
fontColor: 'black',
fontSize: 14, // px
- //fontFace: verdana,
- fontFace: 'arial',
+ fontFace: 'verdana',
+ level: -1,
color: {
border: '#2B7CE9',
background: '#97C2FC',
@@ -13141,18 +15430,51 @@ function Graph (container, data, options) {
widthMax: 15,
width: 1,
style: 'line',
- color: '#343434',
+ color: {
+ color:'#848484',
+ highlight:'#848484'
+ },
fontColor: '#343434',
fontSize: 14, // px
fontFace: 'arial',
- //distance: 100, //px
- length: 100, // px
+ fontFill: 'white',
dash: {
length: 10,
gap: 5,
altLength: undefined
}
},
+ configurePhysics:false,
+ physics: {
+ barnesHut: {
+ enabled: true,
+ theta: 1 / 0.6, // inverted to save time during calculation
+ gravitationalConstant: -2000,
+ centralGravity: 0.3,
+ springLength: 95,
+ springConstant: 0.04,
+ damping: 0.09
+ },
+ repulsion: {
+ centralGravity: 0.1,
+ springLength: 200,
+ springConstant: 0.05,
+ nodeDistance: 100,
+ damping: 0.09
+ },
+ hierarchicalRepulsion: {
+ enabled: false,
+ centralGravity: 0.0,
+ springLength: 100,
+ springConstant: 0.01,
+ nodeDistance: 60,
+ damping: 0.09
+ },
+ damping: null,
+ centralGravity: null,
+ springLength: null,
+ springConstant: null
+ },
clustering: { // Per Node in Cluster = PNiC
enabled: false, // (Boolean) | global on/off switch for clustering.
initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold.
@@ -13160,118 +15482,172 @@ function Graph (container, data, options) {
reduceToNodes:300, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than clusterThreshold. If it is, cluster until reduced to this
chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains).
clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered.
- sectorThreshold: 50, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector.
+ sectorThreshold: 100, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector.
screenSizeThreshold: 0.2, // (% of canvas) | relative size threshold. If the width or height of a clusternode takes up this much of the screen, decluster node.
fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px).
- forceAmplification: 0.6, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
- distanceAmplification: 0.2, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
- edgeGrowth: 11, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
- nodeScaling: {width: 10, // (px PNiC) | growth of the width per node in cluster.
- height: 10, // (px PNiC) | growth of the height per node in cluster.
- radius: 10}, // (px PNiC) | growth of the radius per node in cluster.
- activeAreaBoxSize: 100, // (px) | box area around the curser where clusters are popped open.
- massTransferCoefficient: 1 // (multiplier) | parent.mass += massTransferCoefficient * child.mass
+ maxFontSize: 1000,
+ forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
+ distanceAmplification: 0.1, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
+ edgeGrowth: 20, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
+ nodeScaling: {width: 1, // (px PNiC) | growth of the width per node in cluster.
+ height: 1, // (px PNiC) | growth of the height per node in cluster.
+ radius: 1}, // (px PNiC) | growth of the radius per node in cluster.
+ maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster.
+ activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open.
+ clusterLevelDifference: 2
},
navigation: {
- enabled: false,
- iconPath: this._getScriptPath() + '/img'
+ enabled: false
},
keyboard: {
enabled: false,
speed: {x: 10, y: 10, zoom: 0.02}
},
- minVelocity: 2, // px/s
- maxIterations: 1000 // maximum number of iteration to stabilize
+ dataManipulation: {
+ enabled: false,
+ initiallyVisible: false
+ },
+ hierarchicalLayout: {
+ enabled:false,
+ levelSeparation: 150,
+ nodeSpacing: 100,
+ direction: "UD" // UD, DU, LR, RL
+ },
+ freezeForStabilization: false,
+ smoothCurves: true,
+ maxVelocity: 10,
+ minVelocity: 0.1, // px/s
+ stabilizationIterations: 1000, // maximum number of iteration to stabilize
+ 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."
+ },
+ tooltip: {
+ delay: 300,
+ fontColor: 'black',
+ fontSize: 14, // px
+ fontFace: 'verdana',
+ color: {
+ border: '#666',
+ background: '#FFFFC6'
+ }
+ }
};
+ this.editMode = this.constants.dataManipulation.initiallyVisible;
// Node variables
+ var graph = this;
this.groups = new Groups(); // object with groups
this.images = new Images(); // object with images
this.images.setOnloadCallback(function () {
graph._redraw();
});
- // navigation variables
+ // keyboard navigation variables
this.xIncrement = 0;
this.yIncrement = 0;
this.zoomIncrement = 0;
+ // loading all the mixins:
+ // load the force calculation functions, grouped under the physics system.
+ this._loadPhysicsSystem();
// create a frame and canvas
this._create();
-
// load the sector system. (mandatory, fully integrated with Graph)
this._loadSectorSystem();
-
- // apply options
- this.setOptions(options);
-
// load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it)
this._loadClusterSystem();
-
// load the selection system. (mandatory, required by Graph)
this._loadSelectionSystem();
+ // load the selection system. (mandatory, required by Graph)
+ this._loadHierarchySystem();
+
+ // apply options
+ this.setOptions(options);
// other vars
- var graph = this;
this.freezeSimulation = false;// freeze the simulation
+ this.cachedFunctions = {};
+ // containers for nodes and edges
+ this.calculationNodes = {};
+ this.calculationNodeIndices = [];
this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
this.nodes = {}; // object with Node objects
this.edges = {}; // object with Edge objects
+ // position and scale variables and objects
this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw.
this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
-
+ this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action
this.scale = 1; // defining the global scale variable in the constructor
this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
- // TODO: create a counter to keep track on the number of nodes having values
- // TODO: create a counter to keep track on the number of nodes currently moving
- // TODO: create a counter to keep track on the number of edges having values
+ // datasets or dataviews
this.nodesData = null; // A DataSet or DataView
this.edgesData = null; // A DataSet or DataView
// create event listeners used to subscribe on the DataSets of the nodes and edges
- var me = this;
this.nodesListeners = {
'add': function (event, params) {
- me._addNodes(params.items);
- me.start();
+ graph._addNodes(params.items);
+ graph.start();
},
'update': function (event, params) {
- me._updateNodes(params.items);
- me.start();
+ graph._updateNodes(params.items);
+ graph.start();
},
'remove': function (event, params) {
- me._removeNodes(params.items);
- me.start();
+ graph._removeNodes(params.items);
+ graph.start();
}
};
this.edgesListeners = {
'add': function (event, params) {
- me._addEdges(params.items);
- me.start();
+ graph._addEdges(params.items);
+ graph.start();
},
'update': function (event, params) {
- me._updateEdges(params.items);
- me.start();
+ graph._updateEdges(params.items);
+ graph.start();
},
'remove': function (event, params) {
- me._removeEdges(params.items);
- me.start();
+ graph._removeEdges(params.items);
+ graph.start();
}
};
- // properties of the data
- this.moving = false; // True if any of the nodes have an undefined position
- this.timer = undefined;
+ // properties for the animation
+ this.moving = true;
+ this.timer = undefined; // Scheduling function. Is definded in this.start();
// load data (the disable start variable will be the same as the enabled clustering)
- this.setData(data,this.constants.clustering.enabled);
+ this.setData(data,this.constants.clustering.enabled || this.constants.hierarchicalLayout.enabled);
- // zoom so all data will fit on the screen
- this.zoomToFit(true);
+ // hierarchical layout
+ this.initializing = false;
+ if (this.constants.hierarchicalLayout.enabled == true) {
+ this._setupHierarchicalLayout();
+ }
+ else {
+ // zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here.
+ if (this.stabilize == false) {
+ this.zoomExtent(true,this.constants.clustering.enabled);
+ }
+ }
// if clustering is disabled, the simulation will have started in the setData function
if (this.constants.clustering.enabled) {
@@ -13279,6 +15655,9 @@ function Graph (container, data, options) {
}
}
+// Extend Graph with an Emitter mixin
+Emitter(Graph.prototype);
+
/**
* Get the script path where the vis.js library is located
*
@@ -13309,12 +15688,17 @@ Graph.prototype._getScriptPath = function() {
*/
Graph.prototype._getRange = function() {
var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
- for (var i = 0; i < this.nodeIndices.length; i++) {
- node = this.nodes[this.nodeIndices[i]];
- if (minX > (node.x - node.width)) {minX = node.x - node.width;}
- if (maxX < (node.x + node.width)) {maxX = node.x + node.width;}
- if (minY > (node.y - node.height)) {minY = node.y - node.height;}
- if (maxY < (node.y + node.height)) {maxY = node.y + node.height;}
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ node = this.nodes[nodeId];
+ if (minX > (node.x)) {minX = node.x;}
+ if (maxX < (node.x)) {maxX = node.x;}
+ if (minY > (node.y)) {minY = node.y;}
+ if (maxY < (node.y)) {maxY = node.y;}
+ }
+ }
+ if (minX == 1e9 && maxX == -1e9 && minY == 1e9 && maxY == -1e9) {
+ minY = 0, maxY = 0, minX = 0, maxX = 0;
}
return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
};
@@ -13326,9 +15710,8 @@ Graph.prototype._getRange = function() {
* @private
*/
Graph.prototype._findCenter = function(range) {
- var center = {x: (0.5 * (range.maxX + range.minX)),
- y: (0.5 * (range.maxY + range.minY))};
- return center;
+ return {x: (0.5 * (range.maxX + range.minX)),
+ y: (0.5 * (range.maxY + range.minY))};
};
@@ -13354,22 +15737,41 @@ Graph.prototype._centerGraph = function(range) {
*
* @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
*/
-Graph.prototype.zoomToFit = function(initialZoom) {
+Graph.prototype.zoomExtent = function(initialZoom, disableStart) {
if (initialZoom === undefined) {
initialZoom = false;
}
+ if (disableStart === undefined) {
+ disableStart = false;
+ }
- var numberOfNodes = this.nodeIndices.length;
var range = this._getRange();
+ var zoomLevel;
if (initialZoom == true) {
- if (this.constants.clustering.enabled == true &&
+ var numberOfNodes = this.nodeIndices.length;
+ if (this.constants.smoothCurves == true) {
+ if (this.constants.clustering.enabled == true &&
numberOfNodes >= this.constants.clustering.initialMaxNodes) {
- var zoomLevel = 38.8467 / (numberOfNodes - 14.50184) + 0.0116; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
+ zoomLevel = 49.07548 / (numberOfNodes + 142.05338) + 9.1444e-04; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
+ }
+ else {
+ zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
+ }
}
else {
- var zoomLevel = 42.54117319 / (numberOfNodes + 39.31966387) + 0.1944405; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
+ if (this.constants.clustering.enabled == true &&
+ numberOfNodes >= this.constants.clustering.initialMaxNodes) {
+ zoomLevel = 77.5271985 / (numberOfNodes + 187.266146) + 4.76710517e-05; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
+ }
+ else {
+ zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
+ }
}
+
+ // correct for larger canvasses.
+ var factor = Math.min(this.frame.canvas.clientWidth / 600, this.frame.canvas.clientHeight / 600);
+ zoomLevel *= factor;
}
else {
var xDistance = (Math.abs(range.minX) + Math.abs(range.maxX)) * 1.1;
@@ -13385,10 +15787,13 @@ Graph.prototype.zoomToFit = function(initialZoom) {
zoomLevel = 1.0;
}
- this.pinch.mousewheelScale = zoomLevel;
+
this._setScale(zoomLevel);
this._centerGraph(range);
- this.start();
+ if (disableStart == false) {
+ this.moving = true;
+ this.start();
+ }
};
@@ -13448,9 +15853,8 @@ Graph.prototype.setData = function(data, disableStart) {
if (!disableStart) {
// find a stable position or start animating to a stable position
if (this.stabilize) {
- this._doStabilize();
+ this._stabilize();
}
- this.moving = true;
this.start();
}
};
@@ -13461,15 +15865,78 @@ Graph.prototype.setData = function(data, disableStart) {
*/
Graph.prototype.setOptions = function (options) {
if (options) {
+ var prop;
// retrieve parameter values
if (options.width !== undefined) {this.width = options.width;}
if (options.height !== undefined) {this.height = options.height;}
if (options.stabilize !== undefined) {this.stabilize = options.stabilize;}
if (options.selectable !== undefined) {this.selectable = options.selectable;}
+ if (options.smoothCurves !== undefined) {this.constants.smoothCurves = options.smoothCurves;}
+ if (options.freezeForStabilization !== undefined) {this.constants.freezeForStabilization = options.freezeForStabilization;}
+ if (options.configurePhysics !== undefined){this.constants.configurePhysics = options.configurePhysics;}
+ if (options.stabilizationIterations !== undefined) {this.constants.stabilizationIterations = options.stabilizationIterations;}
+
+
+
+ if (options.labels !== undefined) {
+ for (prop in options.labels) {
+ if (options.labels.hasOwnProperty(prop)) {
+ this.constants.labels[prop] = options.labels[prop];
+ }
+ }
+ }
+
+ if (options.onAdd) {
+ this.triggerFunctions.add = options.onAdd;
+ }
+
+ if (options.onEdit) {
+ this.triggerFunctions.edit = options.onEdit;
+ }
+
+ if (options.onConnect) {
+ this.triggerFunctions.connect = options.onConnect;
+ }
+
+ if (options.onDelete) {
+ this.triggerFunctions.del = options.onDelete;
+ }
+
+ if (options.physics) {
+ if (options.physics.barnesHut) {
+ this.constants.physics.barnesHut.enabled = true;
+ for (prop in options.physics.barnesHut) {
+ if (options.physics.barnesHut.hasOwnProperty(prop)) {
+ this.constants.physics.barnesHut[prop] = options.physics.barnesHut[prop];
+ }
+ }
+ }
+
+ if (options.physics.repulsion) {
+ this.constants.physics.barnesHut.enabled = false;
+ for (prop in options.physics.repulsion) {
+ if (options.physics.repulsion.hasOwnProperty(prop)) {
+ this.constants.physics.repulsion[prop] = options.physics.repulsion[prop];
+ }
+ }
+ }
+ }
+
+ if (options.hierarchicalLayout) {
+ this.constants.hierarchicalLayout.enabled = true;
+ for (prop in options.hierarchicalLayout) {
+ if (options.hierarchicalLayout.hasOwnProperty(prop)) {
+ this.constants.hierarchicalLayout[prop] = options.hierarchicalLayout[prop];
+ }
+ }
+ }
+ else if (options.hierarchicalLayout !== undefined) {
+ this.constants.hierarchicalLayout.enabled = false;
+ }
if (options.clustering) {
this.constants.clustering.enabled = true;
- for (var prop in options.clustering) {
+ for (prop in options.clustering) {
if (options.clustering.hasOwnProperty(prop)) {
this.constants.clustering[prop] = options.clustering[prop];
}
@@ -13481,7 +15948,7 @@ Graph.prototype.setOptions = function (options) {
if (options.navigation) {
this.constants.navigation.enabled = true;
- for (var prop in options.navigation) {
+ for (prop in options.navigation) {
if (options.navigation.hasOwnProperty(prop)) {
this.constants.navigation[prop] = options.navigation[prop];
}
@@ -13493,7 +15960,7 @@ Graph.prototype.setOptions = function (options) {
if (options.keyboard) {
this.constants.keyboard.enabled = true;
- for (var prop in options.keyboard) {
+ for (prop in options.keyboard) {
if (options.keyboard.hasOwnProperty(prop)) {
this.constants.keyboard[prop] = options.keyboard[prop];
}
@@ -13503,23 +15970,45 @@ Graph.prototype.setOptions = function (options) {
this.constants.keyboard.enabled = false;
}
+ if (options.dataManipulation) {
+ this.constants.dataManipulation.enabled = true;
+ for (prop in options.dataManipulation) {
+ if (options.dataManipulation.hasOwnProperty(prop)) {
+ this.constants.dataManipulation[prop] = options.dataManipulation[prop];
+ }
+ }
+ }
+ else if (options.dataManipulation !== undefined) {
+ this.constants.dataManipulation.enabled = false;
+ }
// TODO: work out these options and document them
if (options.edges) {
for (prop in options.edges) {
if (options.edges.hasOwnProperty(prop)) {
- this.constants.edges[prop] = options.edges[prop];
+ if (typeof options.edges[prop] != "object") {
+ this.constants.edges[prop] = options.edges[prop];
+ }
}
}
- if (options.edges.length !== undefined &&
- options.nodes && options.nodes.distance === undefined) {
- this.constants.edges.length = options.edges.length;
- this.constants.nodes.distance = options.edges.length * 1.25;
+ if (options.edges.color !== undefined) {
+ if (util.isString(options.edges.color)) {
+ this.constants.edges.color = {};
+ this.constants.edges.color.color = options.edges.color;
+ this.constants.edges.color.highlight = options.edges.color;
+ }
+ else {
+ if (options.edges.color.color !== undefined) {this.constants.edges.color.color = options.edges.color.color;}
+ if (options.edges.color.highlight !== undefined) {this.constants.edges.color.highlight = options.edges.color.highlight;}
+ }
}
if (!options.edges.fontColor) {
- this.constants.edges.fontColor = options.edges.color;
+ if (options.edges.color !== undefined) {
+ if (util.isString(options.edges.color)) {this.constants.edges.fontColor = options.edges.color;}
+ else if (options.edges.color.color !== undefined) {this.constants.edges.fontColor = options.edges.color.color;}
+ }
}
// Added to support dashed lines
@@ -13546,7 +16035,7 @@ Graph.prototype.setOptions = function (options) {
}
if (options.nodes.color) {
- this.constants.nodes.color = Node.parseColor(options.nodes.color);
+ this.constants.nodes.color = util.parseColor(options.nodes.color);
}
/*
@@ -13562,59 +16051,40 @@ Graph.prototype.setOptions = function (options) {
}
}
}
+
+ if (options.tooltip) {
+ for (prop in options.tooltip) {
+ if (options.tooltip.hasOwnProperty(prop)) {
+ this.constants.tooltip[prop] = options.tooltip[prop];
+ }
+ }
+ if (options.tooltip.color) {
+ this.constants.tooltip.color = util.parseColor(options.tooltip.color);
+ }
+ }
}
- this.setSize(this.width, this.height);
- this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
- this._setScale(1);
+ // (Re)loading the mixins that can be enabled or disabled in the options.
+ // load the force calculation functions, grouped under the physics system.
+ this._loadPhysicsSystem();
// load the navigation system.
this._loadNavigationControls();
+ // load the data manipulation system
+ this._loadManipulationSystem();
+ // configure the smooth curves
+ this._configureSmoothCurves();
+
// bind keys. If disabled, this will not do anything;
this._createKeyBinds();
+ this.setSize(this.width, this.height);
+ this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
+ this._setScale(1);
this._redraw();
};
-/**
- * Add event listener
- * @param {String} event Event name. Available events:
- * 'select'
- * @param {function} callback Callback function, invoked as callback(properties)
- * where properties is an optional object containing
- * event specific properties.
- */
-Graph.prototype.on = function on (event, callback) {
- var available = ['select'];
-
- if (available.indexOf(event) == -1) {
- throw new Error('Unknown event "' + event + '". Choose from ' + available.join());
- }
-
- events.addListener(this, event, callback);
-};
-
-/**
- * Remove an event listener
- * @param {String} event Event name
- * @param {function} callback Callback function
- */
-Graph.prototype.off = function off (event, callback) {
- events.removeListener(this, event, callback);
-};
-
-/**
- * fire an event
- * @param {String} event The name of an event, for example 'select'
- * @param {Object} params Optional object with event parameters
- * @private
- */
-Graph.prototype._trigger = function (event, params) {
- events.trigger(this, event, params);
-};
-
-
/**
* Create the main frame for the Graph.
* This function is executed once when a Graph object is created. The frame
@@ -13667,6 +16137,7 @@ Graph.prototype._create = function () {
// add the frame to the container element
this.containerElement.appendChild(this.frame);
+
};
@@ -13702,15 +16173,12 @@ Graph.prototype._createKeyBinds = function() {
this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown");
this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup");
}
- /*
- this.mousetrap.bind("=",this.decreaseClusterLevel.bind(me));
- this.mousetrap.bind("-",this.increaseClusterLevel.bind(me));
- this.mousetrap.bind("s",this.singleStep.bind(me));
- this.mousetrap.bind("h",this.updateClustersDefault.bind(me));
- this.mousetrap.bind("c",this._collapseSector.bind(me));
- this.mousetrap.bind("f",this.toggleFreeze.bind(me));
- */
-}
+
+ if (this.constants.dataManipulation.enabled == true) {
+ this.mousetrap.bind("escape",this._createManipulatorBar.bind(me));
+ this.mousetrap.bind("del",this._deleteSelected.bind(me));
+ }
+};
/**
* Get the pointer location from a touch location
@@ -13731,7 +16199,7 @@ Graph.prototype._getPointer = function (touch) {
* @private
*/
Graph.prototype._onTouch = function (event) {
- this.drag.pointer = this._getPointer(event.gesture.touches[0]);
+ this.drag.pointer = this._getPointer(event.gesture.center);
this.drag.pinched = false;
this.pinch.scale = this._getScale();
@@ -13743,6 +16211,17 @@ Graph.prototype._onTouch = function (event) {
* @private
*/
Graph.prototype._onDragStart = function () {
+ this._handleDragStart();
+};
+
+
+/**
+ * This function is called by _onDragStart.
+ * It is separated out because we can then overload it for the datamanipulation system.
+ *
+ * @private
+ */
+Graph.prototype._handleDragStart = function() {
var drag = this.drag;
var node = this._getNodeAt(drag.pointer);
// note: drag.pointer is set in _onTouch to get the initial touch location
@@ -13756,52 +16235,63 @@ Graph.prototype._onDragStart = function () {
drag.nodeId = node.id;
// select the clicked node if not yet selected
if (!node.isSelected()) {
- this._selectNode(node,false);
+ this._selectObject(node,false);
}
// create an array with the selected nodes and their original location and status
- var me = this;
- this.selection.forEach(function (id) {
- var node = me.nodes[id];
- if (node) {
+ for (var objectId in this.selectionObj.nodes) {
+ if (this.selectionObj.nodes.hasOwnProperty(objectId)) {
+ var object = this.selectionObj.nodes[objectId];
var s = {
- id: id,
- node: node,
+ id: object.id,
+ node: object,
// store original x, y, xFixed and yFixed, make the node temporarily Fixed
- x: node.x,
- y: node.y,
- xFixed: node.xFixed,
- yFixed: node.yFixed
+ x: object.x,
+ y: object.y,
+ xFixed: object.xFixed,
+ yFixed: object.yFixed
};
- node.xFixed = true;
- node.yFixed = true;
+ object.xFixed = true;
+ object.yFixed = true;
drag.selection.push(s);
}
- });
+ }
}
};
+
/**
* handle drag event
* @private
*/
Graph.prototype._onDrag = function (event) {
+ this._handleOnDrag(event)
+};
+
+
+/**
+ * This function is called by _onDrag.
+ * It is separated out because we can then overload it for the datamanipulation system.
+ *
+ * @private
+ */
+Graph.prototype._handleOnDrag = function(event) {
if (this.drag.pinched) {
return;
}
- var pointer = this._getPointer(event.gesture.touches[0]);
+ var pointer = this._getPointer(event.gesture.center);
var me = this,
- drag = this.drag,
- selection = drag.selection;
+ drag = this.drag,
+ selection = drag.selection;
if (selection && selection.length) {
// calculate delta's and new location
var deltaX = pointer.x - drag.pointer.x,
- deltaY = pointer.y - drag.pointer.y;
+ deltaY = pointer.y - drag.pointer.y;
// update position of all selected nodes
selection.forEach(function (s) {
@@ -13816,7 +16306,7 @@ Graph.prototype._onDrag = function (event) {
}
});
- // start animation if not yet running
+ // start _animationStep if not yet running
if (!this.moving) {
this.moving = true;
this.start();
@@ -13828,8 +16318,8 @@ Graph.prototype._onDrag = function (event) {
var diffY = pointer.y - this.drag.pointer.y;
this._setTranslation(
- this.drag.translation.x + diffX,
- this.drag.translation.y + diffY);
+ this.drag.translation.x + diffX,
+ this.drag.translation.y + diffY);
this._redraw();
this.moved = true;
}
@@ -13856,8 +16346,10 @@ Graph.prototype._onDragEnd = function () {
* @private
*/
Graph.prototype._onTap = function (event) {
- var pointer = this._getPointer(event.gesture.touches[0]);
+ var pointer = this._getPointer(event.gesture.center);
+ this.pointerPosition = pointer;
this._handleTap(pointer);
+
};
@@ -13866,9 +16358,8 @@ Graph.prototype._onTap = function (event) {
* @private
*/
Graph.prototype._onDoubleTap = function (event) {
- var pointer = this._getPointer(event.gesture.touches[0]);
+ var pointer = this._getPointer(event.gesture.center);
this._handleDoubleTap(pointer);
-
};
@@ -13877,18 +16368,19 @@ Graph.prototype._onDoubleTap = function (event) {
* @private
*/
Graph.prototype._onHold = function (event) {
- var pointer = this._getPointer(event.gesture.touches[0]);
+ var pointer = this._getPointer(event.gesture.center);
+ this.pointerPosition = pointer;
this._handleOnHold(pointer);
};
/**
* handle the release of the screen
*
- * @param event
* @private
*/
Graph.prototype._onRelease = function (event) {
- this._handleOnRelease();
+ var pointer = this._getPointer(event.gesture.center);
+ this._handleOnRelease(pointer);
};
/**
@@ -13934,17 +16426,16 @@ Graph.prototype._zoom = function(scale, pointer) {
this.areaCenter = {"x" : this._canvasToX(pointer.x),
"y" : this._canvasToY(pointer.y)};
- // this.areaCenter = {"x" : pointer.x,"y" : pointer.y };
-// console.log(translation.x,translation.y,pointer.x,pointer.y,scale);
- this.pinch.mousewheelScale = scale;
this._setScale(scale);
this._setTranslation(tx, ty);
this.updateClustersDefault();
this._redraw();
+
return scale;
};
+
/**
* Event handler for mouse wheel event, used to zoom the timeline
* See http://adomas.org/javascript-mouse-wheel/
@@ -13967,12 +16458,9 @@ Graph.prototype._onMouseWheel = function(event) {
// Basically, delta is now positive if wheel was scrolled up,
// and negative, if wheel was scrolled down.
if (delta) {
- if (!('mousewheelScale' in this.pinch)) {
- this.pinch.mousewheelScale = 1;
- }
// calculate the new scale
- var scale = this.pinch.mousewheelScale;
+ var scale = this._getScale();
var zoom = delta / 10;
if (delta < 0) {
zoom = zoom / (1 - zoom);
@@ -13984,10 +16472,7 @@ Graph.prototype._onMouseWheel = function(event) {
var pointer = this._getPointer(gesture.center);
// apply the new scale
- scale = this._zoom(scale, pointer);
-
- // store the new, applied scale -- this is now done in _zoom
-// this.pinch.mousewheelScale = scale;
+ this._zoom(scale, pointer);
}
// Prevent default actions caused by mouse wheel.
@@ -14019,7 +16504,7 @@ Graph.prototype._onMouseMoveTitle = function (event) {
clearInterval(this.popupTimer); // stop any running calculationTimer
}
if (!this.drag.dragging) {
- this.popupTimer = setTimeout(checkShow, 300);
+ this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay);
}
};
@@ -14076,7 +16561,7 @@ Graph.prototype._checkShowPopup = function (pointer) {
if (this.popupNode != lastPopupNode) {
var me = this;
if (!me.popup) {
- me.popup = new Popup(me.frame);
+ me.popup = new Popup(me.frame, me.constants.tooltip);
}
// adjust a small offset such that the mouse cursor is located in the
@@ -14094,6 +16579,7 @@ Graph.prototype._checkShowPopup = function (pointer) {
}
};
+
/**
* Check if the popup must be hided, which is the case when the mouse is no
* longer hovering on the object
@@ -14110,85 +16596,6 @@ Graph.prototype._checkHidePopup = function (pointer) {
};
-/**
- * Temporary method to test calculating a hub value for the nodes
- * @param {number} level Maximum number edges between two nodes in order
- * to call them connected. Optional, 1 by default
- * @return {Number[]} connectioncount array with the connection count
- * for each node
- * @private
- */
-Graph.prototype._getConnectionCount = function(level) {
- if (level == undefined) {
- level = 1;
- }
-
- // get the nodes connected to given nodes
- function getConnectedNodes(nodes) {
- var connectedNodes = [];
-
- for (var j = 0, jMax = nodes.length; j < jMax; j++) {
- var node = nodes[j];
-
- // find all nodes connected to this node
- var edges = node.edges;
- for (var i = 0, iMax = edges.length; i < iMax; i++) {
- var edge = edges[i];
- var other = null;
-
- // check if connected
- if (edge.from == node)
- other = edge.to;
- else if (edge.to == node)
- other = edge.from;
-
- // check if the other node is not already in the list with nodes
- var k, kMax;
- if (other) {
- for (k = 0, kMax = nodes.length; k < kMax; k++) {
- if (nodes[k] == other) {
- other = null;
- break;
- }
- }
- }
- if (other) {
- for (k = 0, kMax = connectedNodes.length; k < kMax; k++) {
- if (connectedNodes[k] == other) {
- other = null;
- break;
- }
- }
- }
-
- if (other)
- connectedNodes.push(other);
- }
- }
-
- return connectedNodes;
- }
-
- var connections = [];
- var nodes = this.nodes;
- for (var id in nodes) {
- if (nodes.hasOwnProperty(id)) {
- var c = [nodes[id]];
- for (var l = 0; l < level; l++) {
- c = c.concat(getConnectedNodes(c));
- }
- connections.push(c);
- }
- }
-
- var hubs = [];
- for (var i = 0, len = connections.length; i < len; i++) {
- hubs.push(connections[i].length);
- }
-
- return hubs;
-};
-
/**
* Set a new size for the graph
* @param {string} width Width in pixels or percentage (for example '800px'
@@ -14206,9 +16613,17 @@ Graph.prototype.setSize = function(width, height) {
this.frame.canvas.width = this.frame.canvas.clientWidth;
this.frame.canvas.height = this.frame.canvas.clientHeight;
- if (this.constants.navigation.enabled == true) {
- this._relocateNavigation();
+ if (this.manipulationDiv !== undefined) {
+ this.manipulationDiv.style.width = this.frame.canvas.clientWidth + "px";
+ }
+ if (this.navigationDivs !== undefined) {
+ if (this.navigationDivs['wrapper'] !== undefined) {
+ this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
+ this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
+ }
}
+
+ this.emit('resize', {width:this.frame.canvas.width,height:this.frame.canvas.height});
};
/**
@@ -14236,7 +16651,7 @@ Graph.prototype._setNodes = function(nodes) {
if (oldNodesData) {
// unsubscribe from old dataset
util.forEach(this.nodesListeners, function (callback, event) {
- oldNodesData.unsubscribe(event, callback);
+ oldNodesData.off(event, callback);
});
}
@@ -14247,7 +16662,7 @@ Graph.prototype._setNodes = function(nodes) {
// subscribe to new dataset
var me = this;
util.forEach(this.nodesListeners, function (callback, event) {
- me.nodesData.subscribe(event, callback);
+ me.nodesData.on(event, callback);
});
// draw all new nodes
@@ -14270,22 +16685,23 @@ Graph.prototype._addNodes = function(ids) {
var node = new Node(data, this.images, this.groups, this.constants);
this.nodes[id] = node; // note: this may replace an existing node
- if (!node.isFixed()) {
- // TODO: position new nodes in a smarter way!
- var radius = this.constants.edges.length * 2;
- var count = ids.length;
- var angle = 2 * Math.PI * (i / count);
- node.x = radius * Math.cos(angle);
- node.y = radius * Math.sin(angle);
-
- // note: no not use node.isMoving() here, as that gives the current
- // velocity of the node, which is zero after creation of the node.
- this.moving = true;
+ if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) {
+ var radius = 10 * 0.1*ids.length;
+ var angle = 2 * Math.PI * Math.random();
+ if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
+ if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
}
+ this.moving = true;
}
this._updateNodeIndexList();
+ if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
+ this._resetLevels();
+ this._setupHierarchicalLayout();
+ }
+ this._updateCalculationNodes();
this._reconnectEdges();
this._updateValueRange(this.nodes);
+ this.updateLabels();
};
/**
@@ -14331,6 +16747,11 @@ Graph.prototype._removeNodes = function(ids) {
delete nodes[id];
}
this._updateNodeIndexList();
+ if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
+ this._resetLevels();
+ this._setupHierarchicalLayout();
+ }
+ this._updateCalculationNodes();
this._reconnectEdges();
this._updateSelection();
this._updateValueRange(nodes);
@@ -14362,7 +16783,7 @@ Graph.prototype._setEdges = function(edges) {
if (oldEdgesData) {
// unsubscribe from old dataset
util.forEach(this.edgesListeners, function (callback, event) {
- oldEdgesData.unsubscribe(event, callback);
+ oldEdgesData.off(event, callback);
});
}
@@ -14373,7 +16794,7 @@ Graph.prototype._setEdges = function(edges) {
// subscribe to new dataset
var me = this;
util.forEach(this.edgesListeners, function (callback, event) {
- me.edgesData.subscribe(event, callback);
+ me.edgesData.on(event, callback);
});
// draw all new nodes
@@ -14407,6 +16828,12 @@ Graph.prototype._addEdges = function (ids) {
this.moving = true;
this._updateValueRange(edges);
+ this._createBezierNodes();
+ if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
+ this._resetLevels();
+ this._setupHierarchicalLayout();
+ }
+ this._updateCalculationNodes();
};
/**
@@ -14435,6 +16862,11 @@ Graph.prototype._updateEdges = function (ids) {
}
}
+ this._createBezierNodes();
+ if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
+ this._resetLevels();
+ this._setupHierarchicalLayout();
+ }
this.moving = true;
this._updateValueRange(edges);
};
@@ -14450,6 +16882,9 @@ Graph.prototype._removeEdges = function (ids) {
var id = ids[i];
var edge = edges[id];
if (edge) {
+ if (edge.via != null) {
+ delete this.sectors['support']['nodes'][edge.via.id];
+ }
edge.disconnect();
delete edges[id];
}
@@ -14457,6 +16892,11 @@ Graph.prototype._removeEdges = function (ids) {
this.moving = true;
this._updateValueRange(edges);
+ if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
+ this._resetLevels();
+ this._setupHierarchicalLayout();
+ }
+ this._updateCalculationNodes();
};
/**
@@ -14554,14 +16994,13 @@ Graph.prototype._redraw = function() {
this._doInAllSectors("_drawAllSectorNodes",ctx);
this._doInAllSectors("_drawEdges",ctx);
- this._doInAllSectors("_drawNodes",ctx);
+ this._doInAllSectors("_drawNodes",ctx,false);
+
+// this._doInSupportSector("_drawNodes",ctx,true);
+// this._drawTree(ctx,"#F00F0F");
// restore original scaling and translation
ctx.restore();
-
- if (this.constants.navigation.enabled == true) {
- this._doInNavigationSector("_drawNodes",ctx,true);
- }
};
/**
@@ -14717,244 +17156,49 @@ Graph.prototype._drawEdges = function(ctx) {
* Find a stable position for all nodes
* @private
*/
-Graph.prototype._doStabilize = function() {
- //var start = new Date();
+Graph.prototype._stabilize = function() {
+ if (this.constants.freezeForStabilization == true) {
+ this._freezeDefinedNodes();
+ }
// find stable position
var count = 0;
- var vmin = this.constants.minVelocity;
- var stable = false;
- while (!stable && count < this.constants.maxIterations) {
- this._initializeForceCalculation();
- this._discreteStepNodes();
- stable = !this._isMoving(vmin);
+ while (this.moving && count < this.constants.stabilizationIterations) {
+ this._physicsTick();
count++;
}
- this.zoomToFit();
-
- // var end = new Date();
-
- // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup
-};
-
-
-/**
- * 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
- */
-Graph.prototype._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();
+ this.zoomExtent(false,true);
+ if (this.constants.freezeForStabilization == true) {
+ this._restoreFrozenNodes();
}
+ this.emit("stabilized",{iterations:count});
};
-/**
- * Calculate the external forces acting on the nodes
- * Forces are caused by: edges, repulsing forces between nodes, gravity
- * @private
- */
-Graph.prototype._calculateForces = function() {
-// var screenCenterPos = {"x":(0.5*(this.canvasTopLeft.x + this.canvasBottomRight.x)),
-// "y":(0.5*(this.canvasTopLeft.y + this.canvasBottomRight.y))}
- // create a local edge to the nodes and edges, that is faster
- var dx, dy, angle, distance, fx, fy,
- repulsingForce, springForce, length, edgeLength,
- node, node1, node2, edge, edgeId, i, j, nodeId, xCenter, yCenter;
- var clusterSize;
+Graph.prototype._freezeDefinedNodes = function() {
var nodes = this.nodes;
- var edges = this.edges;
-
- // 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
- var gravity = 0.08 * this.forceFactor;
- for (i = 0; i < this.nodeIndices.length; i++) {
- node = nodes[this.nodeIndices[i]];
- // gravity does not apply when we are in a pocket sector
- if (this._sector() == "default") {
- dx = -node.x;// + screenCenterPos.x;
- dy = -node.y;// + screenCenterPos.y;
-
- angle = Math.atan2(dy, dx);
- fx = Math.cos(angle) * gravity;
- fy = Math.sin(angle) * gravity;
- }
- else {
- fx = 0;
- fy = 0;
- }
- node._setForce(fx, fy);
-
- node.updateDamping(this.nodeIndices.length);
- }
-
- // repulsing forces between nodes
- var minimumDistance = this.constants.nodes.distance,
- steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
-
-
- // 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 < this.nodeIndices.length-1; i++) {
- node1 = nodes[this.nodeIndices[i]];
- for (j = i+1; j < this.nodeIndices.length; j++) {
- node2 = nodes[this.nodeIndices[j]];
- clusterSize = (node1.clusterSize + node2.clusterSize - 2);
- dx = node2.x - node1.x;
- dy = node2.y - node1.y;
- distance = Math.sqrt(dx * dx + dy * dy);
-
-
- // clusters have a larger region of influence
- minimumDistance = (clusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + clusterSize * this.constants.clustering.distanceAmplification));
- if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045
- angle = Math.atan2(dy, dx);
-
- if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307
- repulsingForce = 1.0;
- }
- else {
- // TODO: correct factor for repulsing force
- //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
- //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
- repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
- }
- // amplify the repulsion for clusters.
- repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification;
- repulsingForce *= this.forceFactor;
-
-
- fx = Math.cos(angle) * repulsingForce;
- fy = Math.sin(angle) * repulsingForce ;
-
- node1._addForce(-fx, -fy);
- node2._addForce(fx, fy);
+ for (var id in nodes) {
+ if (nodes.hasOwnProperty(id)) {
+ if (nodes[id].x != null && nodes[id].y != null) {
+ nodes[id].fixedData.x = nodes[id].xFixed;
+ nodes[id].fixedData.y = nodes[id].yFixed;
+ nodes[id].xFixed = true;
+ nodes[id].yFixed = true;
}
}
}
+};
-/*
- // repulsion of the edges on the nodes and
- for (var nodeId in nodes) {
- if (nodes.hasOwnProperty(nodeId)) {
- node = nodes[nodeId];
- for(var edgeId in edges) {
- if (edges.hasOwnProperty(edgeId)) {
- edge = edges[edgeId];
-
- // get the center of the edge
- xCenter = edge.from.x+(edge.to.x - edge.from.x)/2;
- yCenter = edge.from.y+(edge.to.y - edge.from.y)/2;
-
- // calculate normally distributed force
- dx = node.x - xCenter;
- dy = node.y - yCenter;
- distance = Math.sqrt(dx * dx + dy * dy);
- if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045
- angle = Math.atan2(dy, dx);
-
- if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307
- repulsingForce = 1.0;
- }
- else {
- // TODO: correct factor for repulsing force
- //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
- //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
- repulsingForce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)); // TODO: customize the repulsing force
- }
- fx = Math.cos(angle) * repulsingForce;
- fy = Math.sin(angle) * repulsingForce;
- node._addForce(fx, fy);
- edge.from._addForce(-fx/2,-fy/2);
- edge.to._addForce(-fx/2,-fy/2);
- }
- }
+Graph.prototype._restoreFrozenNodes = function() {
+ var nodes = this.nodes;
+ for (var id in nodes) {
+ if (nodes.hasOwnProperty(id)) {
+ if (nodes[id].fixedData.x != null) {
+ nodes[id].xFixed = nodes[id].fixedData.x;
+ nodes[id].yFixed = nodes[id].fixedData.y;
}
}
}
-*/
-
- // 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)) {
- clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2);
- dx = (edge.to.x - edge.from.x);
- dy = (edge.to.y - edge.from.y);
- //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin
- //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin
- //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2;
- edgeLength = edge.length;
- // this implies that the edges between big clusters are longer
- edgeLength += clusterSize * this.constants.clustering.edgeGrowth;
- length = Math.sqrt(dx * dx + dy * dy);
- angle = Math.atan2(dy, dx);
-
- springForce = edge.stiffness * (edgeLength - length) * this.forceFactor;
-
- fx = Math.cos(angle) * springForce;
- fy = Math.sin(angle) * springForce;
-
- edge.from._addForce(-fx, -fy);
- edge.to._addForce(fx, fy);
- }
- }
- }
- }
-/*
- // TODO: re-implement repulsion of edges
-
- // repulsing forces between edges
- var minimumDistance = this.constants.edges.distance,
- steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
- for (var l = 0; l < edges.length; l++) {
- //Keep distance from other edge centers
- for (var l2 = l + 1; l2 < this.edges.length; l2++) {
- //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin
- //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin
- //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0),
- var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
- ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
- l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2,
- l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2,
-
- // calculate normally distributed force
- dx = l2x - lx,
- dy = l2y - ly,
- distance = Math.sqrt(dx * dx + dy * dy),
- angle = Math.atan2(dy, dx),
-
-
- // TODO: correct factor for repulsing force
- //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
- //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
- repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force
- fx = Math.cos(angle) * repulsingforce,
- fy = Math.sin(angle) * repulsingforce;
-
- edges[l].from._addForce(-fx, -fy);
- edges[l].to._addForce(-fx, -fy);
- edges[l2].from._addForce(fx, fy);
- edges[l2].to._addForce(fx, fy);
- }
- }
-*/
};
@@ -14965,10 +17209,9 @@ Graph.prototype._calculateForces = function() {
* @private
*/
Graph.prototype._isMoving = function(vmin) {
- var vminCorrected = vmin / this.scale;
var nodes = this.nodes;
for (var id in nodes) {
- if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vminCorrected)) {
+ if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
return true;
}
}
@@ -14980,93 +17223,149 @@ Graph.prototype._isMoving = function(vmin) {
* /**
* Perform one discrete step for all nodes
*
- * @param interval
* @private
*/
Graph.prototype._discreteStepNodes = function() {
- var interval = 0.01;
+ var interval = this.physicsDiscreteStepsize;
var nodes = this.nodes;
- for (var id in nodes) {
- if (nodes.hasOwnProperty(id)) {
- nodes[id].discreteStep(interval);
+ var nodeId;
+ var nodesPresent = false;
+
+ if (this.constants.maxVelocity > 0) {
+ for (nodeId in nodes) {
+ if (nodes.hasOwnProperty(nodeId)) {
+ nodes[nodeId].discreteStepLimited(interval, this.constants.maxVelocity);
+ nodesPresent = true;
+ }
+ }
+ }
+ else {
+ for (nodeId in nodes) {
+ if (nodes.hasOwnProperty(nodeId)) {
+ nodes[nodeId].discreteStep(interval);
+ nodesPresent = true;
+ }
+ }
+ }
+
+ if (nodesPresent == true) {
+ var vminCorrected = this.constants.minVelocity / Math.max(this.scale,0.05);
+ if (vminCorrected > 0.5*this.constants.maxVelocity) {
+ this.moving = true;
+ }
+ else {
+ this.moving = this._isMoving(vminCorrected);
}
}
-
- var vmin = this.constants.minVelocity;
- this.moving = this._isMoving(vmin);
};
-
-/**
- * Start animating nodes and edges
- *
- * @poram {Boolean} runCalculationStep
- */
-Graph.prototype.start = function() {
+Graph.prototype._physicsTick = function() {
if (!this.freezeSimulation) {
-
if (this.moving) {
this._doInAllActiveSectors("_initializeForceCalculation");
this._doInAllActiveSectors("_discreteStepNodes");
+ if (this.constants.smoothCurves) {
+ this._doInSupportSector("_discreteStepNodes");
+ }
this._findCenter(this._getRange())
}
+ }
+};
- if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) {
- // start animation. only start calculationTimer if it is not already running
- if (!this.timer) {
- var graph = this;
- this.timer = window.setTimeout(function () {
- graph.timer = undefined;
- // keyboad movement
- if (graph.xIncrement != 0 || graph.yIncrement != 0) {
- var translation = graph._getTranslation();
- graph._setTranslation(translation.x+graph.xIncrement, translation.y+graph.yIncrement);
- }
- if (graph.zoomIncrement != 0) {
- var center = {
- x: graph.frame.canvas.clientWidth / 2,
- y: graph.frame.canvas.clientHeight / 2
- };
- graph._zoom(graph.scale*(1 + graph.zoomIncrement), center);
- }
+/**
+ * This function runs one step of the animation. It calls an x amount of physics ticks and one render tick.
+ * It reschedules itself at the beginning of the function
+ *
+ * @private
+ */
+Graph.prototype._animationStep = function() {
+ // reset the timer so a new scheduled animation step can be set
+ this.timer = undefined;
+ // handle the keyboad movement
+ this._handleNavigation();
- graph.start();
- graph._redraw();
+ // this schedules a new animation step
+ this.start();
- //this.end = window.performance.now();
- //this.time = this.end - this.startTime;
- //console.log('refresh time: ' + this.time);
- //this.startTime = window.performance.now();
+ // start the physics simulation
+ var calculationTime = Date.now();
+ var maxSteps = 1;
+ this._physicsTick();
+ var timeRequired = Date.now() - calculationTime;
+ while (timeRequired < (this.renderTimestep - this.renderTime) && maxSteps < this.maxPhysicsTicksPerRender) {
+ this._physicsTick();
+ timeRequired = Date.now() - calculationTime;
+ maxSteps++;
- }, this.renderTimestep);
- }
- }
- else {
- this._redraw();
- }
}
-};
+ // start the rendering process
+ var renderTime = Date.now();
+ this._redraw();
+ this.renderTime = Date.now() - renderTime;
+};
+if (typeof window !== 'undefined') {
+ window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
+ window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
+}
+/**
+ * Schedule a animation step with the refreshrate interval.
+ */
+Graph.prototype.start = function() {
+ if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) {
+ if (!this.timer) {
+ var ua = navigator.userAgent.toLowerCase();
-Graph.prototype.singleStep = function() {
- if (this.moving) {
- this._initializeForceCalculation();
- this._discreteStepNodes();
+ var requiresTimeout = false;
+ if (ua.indexOf('msie 9.0') != -1) { // IE 9
+ requiresTimeout = true;
+ }
+ else if (ua.indexOf('safari') != -1) { // safari
+ if (ua.indexOf('chrome') <= -1) {
+ requiresTimeout = true;
+ }
+ }
- var vmin = this.constants.minVelocity;
- this.moving = this._isMoving(vmin);
+ if (requiresTimeout == true) {
+ this.timer = window.setTimeout(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
+ }
+ else{
+ this.timer = window.requestAnimationFrame(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
+ }
+ }
+ }
+ else {
this._redraw();
}
};
+/**
+ * Move the graph according to the keyboard presses.
+ *
+ * @private
+ */
+Graph.prototype._handleNavigation = function() {
+ if (this.xIncrement != 0 || this.yIncrement != 0) {
+ var translation = this._getTranslation();
+ this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement);
+ }
+ if (this.zoomIncrement != 0) {
+ var center = {
+ x: this.frame.canvas.clientWidth / 2,
+ y: this.frame.canvas.clientHeight / 2
+ };
+ this._zoom(this.scale*(1 + this.zoomIncrement), center);
+ }
+};
+
/**
- * Freeze the animation
+ * Freeze the _animationStep
*/
Graph.prototype.toggleFreeze = function() {
if (this.freezeSimulation == false) {
@@ -15078,111 +17377,83 @@ Graph.prototype.toggleFreeze = function() {
}
};
-/**
- * Mixin the cluster system and initialize the parameters required.
- *
- * @private
- */
-Graph.prototype._loadClusterSystem = function() {
- this.clusterSession = 0;
- this.hubThreshold = 5;
- for (var mixinFunction in ClusterMixin) {
- if (ClusterMixin.hasOwnProperty(mixinFunction)) {
- Graph.prototype[mixinFunction] = ClusterMixin[mixinFunction];
- }
- }
-}
-/**
- * Mixin the sector system and initialize the parameters required
- *
- * @private
- */
-Graph.prototype._loadSectorSystem = function() {
- this.sectors = {};
- this.activeSector = ["default"];
- this.sectors["active"] = {};
- this.sectors["active"]["default"] = {"nodes":{},
- "edges":{},
- "nodeIndices":[],
- "formationScale": 1.0,
- "drawingNode": undefined};
- this.sectors["frozen"] = {};
- this.sectors["navigation"] = {"nodes":{},
- "edges":{},
- "nodeIndices":[],
- "formationScale": 1.0,
- "drawingNode": undefined};
+Graph.prototype._configureSmoothCurves = function(disableStart) {
+ if (disableStart === undefined) {
+ disableStart = true;
+ }
- this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
- for (var mixinFunction in SectorMixin) {
- if (SectorMixin.hasOwnProperty(mixinFunction)) {
- Graph.prototype[mixinFunction] = SectorMixin[mixinFunction];
+ if (this.constants.smoothCurves == true) {
+ this._createBezierNodes();
+ }
+ else {
+ // delete the support nodes
+ this.sectors['support']['nodes'] = {};
+ for (var edgeId in this.edges) {
+ if (this.edges.hasOwnProperty(edgeId)) {
+ this.edges[edgeId].smooth = false;
+ this.edges[edgeId].via = null;
+ }
}
}
+ this._updateCalculationNodes();
+ if (!disableStart) {
+ this.moving = true;
+ this.start();
+ }
};
-
-/**
- * Mixin the selection system and initialize the parameters required
- *
- * @private
- */
-Graph.prototype._loadSelectionSystem = function() {
- this.selection = [];
- this.selectionObj = {};
-
- for (var mixinFunction in SelectionMixin) {
- if (SelectionMixin.hasOwnProperty(mixinFunction)) {
- Graph.prototype[mixinFunction] = SelectionMixin[mixinFunction];
+Graph.prototype._createBezierNodes = function() {
+ if (this.constants.smoothCurves == true) {
+ for (var edgeId in this.edges) {
+ if (this.edges.hasOwnProperty(edgeId)) {
+ var edge = this.edges[edgeId];
+ if (edge.via == null) {
+ edge.smooth = true;
+ var nodeId = "edgeId:".concat(edge.id);
+ this.sectors['support']['nodes'][nodeId] = new Node(
+ {id:nodeId,
+ mass:1,
+ shape:'circle',
+ image:"",
+ internalMultiplier:1
+ },{},{},this.constants);
+ edge.via = this.sectors['support']['nodes'][nodeId];
+ edge.via.parentEdgeId = edge.id;
+ edge.positionBezierNode();
+ }
+ }
}
}
-}
+};
-/**
- * Mixin the navigation (User Interface) system and initialize the parameters required
- *
- * @private
- */
-Graph.prototype._loadNavigationControls = function() {
- for (var mixinFunction in NavigationMixin) {
- if (NavigationMixin.hasOwnProperty(mixinFunction)) {
- Graph.prototype[mixinFunction] = NavigationMixin[mixinFunction];
+Graph.prototype._initializeMixinLoaders = function () {
+ for (var mixinFunction in graphMixinLoaders) {
+ if (graphMixinLoaders.hasOwnProperty(mixinFunction)) {
+ Graph.prototype[mixinFunction] = graphMixinLoaders[mixinFunction];
}
}
-
- if (this.constants.navigation.enabled == true) {
- this._loadNavigationElements();
- }
-}
-
-/**
- * this function exists to avoid errors when not loading the navigation system
- */
-Graph.prototype._relocateNavigation = function() {
- // empty, is overloaded by navigation system
-}
+};
/**
- * * this function exists to avoid errors when not loading the navigation system
+ * Load the XY positions of the nodes into the dataset.
*/
-Graph.prototype._unHighlightAll = function() {
- // empty, is overloaded by the navigation system
-}
-
-
-
-
-
-
-
-
-
-
-
-
+Graph.prototype.storePosition = function() {
+ var dataArray = [];
+ for (var nodeId in this.nodes) {
+ if (this.nodes.hasOwnProperty(nodeId)) {
+ var node = this.nodes[nodeId];
+ var allowedToMoveX = !this.nodes.xFixed;
+ var allowedToMoveY = !this.nodes.yFixed;
+ if (this.nodesData.data[nodeId].x != Math.round(node.x) || this.nodesData.data[nodeId].y != Math.round(node.y)) {
+ dataArray.push({id:nodeId,x:Math.round(node.x),y:Math.round(node.y),allowedToMoveX:allowedToMoveX,allowedToMoveY:allowedToMoveY});
+ }
+ }
+ }
+ this.nodesData.update(dataArray);
+};
@@ -15201,15 +17472,12 @@ Graph.prototype._unHighlightAll = function() {
*/
var vis = {
util: util,
- events: events,
- Controller: Controller,
DataSet: DataSet,
DataView: DataView,
Range: Range,
Stack: Stack,
TimeStep: TimeStep,
- EventBus: EventBus,
components: {
items: {
@@ -15266,7 +17534,173 @@ if (typeof window !== 'undefined') {
}
-},{"hammerjs":2,"moment":3,"mousetrap":4}],2:[function(require,module,exports){
+},{"emitter-component":2,"hammerjs":3,"moment":4,"mousetrap":5}],2:[function(require,module,exports){
+
+/**
+ * Expose `Emitter`.
+ */
+
+module.exports = Emitter;
+
+/**
+ * Initialize a new `Emitter`.
+ *
+ * @api public
+ */
+
+function Emitter(obj) {
+ if (obj) return mixin(obj);
+};
+
+/**
+ * Mixin the emitter properties.
+ *
+ * @param {Object} obj
+ * @return {Object}
+ * @api private
+ */
+
+function mixin(obj) {
+ for (var key in Emitter.prototype) {
+ obj[key] = Emitter.prototype[key];
+ }
+ return obj;
+}
+
+/**
+ * Listen on the given `event` with `fn`.
+ *
+ * @param {String} event
+ * @param {Function} fn
+ * @return {Emitter}
+ * @api public
+ */
+
+Emitter.prototype.on =
+Emitter.prototype.addEventListener = function(event, fn){
+ this._callbacks = this._callbacks || {};
+ (this._callbacks[event] = this._callbacks[event] || [])
+ .push(fn);
+ return this;
+};
+
+/**
+ * Adds an `event` listener that will be invoked a single
+ * time then automatically removed.
+ *
+ * @param {String} event
+ * @param {Function} fn
+ * @return {Emitter}
+ * @api public
+ */
+
+Emitter.prototype.once = function(event, fn){
+ var self = this;
+ this._callbacks = this._callbacks || {};
+
+ function on() {
+ self.off(event, on);
+ fn.apply(this, arguments);
+ }
+
+ on.fn = fn;
+ this.on(event, on);
+ return this;
+};
+
+/**
+ * Remove the given callback for `event` or all
+ * registered callbacks.
+ *
+ * @param {String} event
+ * @param {Function} fn
+ * @return {Emitter}
+ * @api public
+ */
+
+Emitter.prototype.off =
+Emitter.prototype.removeListener =
+Emitter.prototype.removeAllListeners =
+Emitter.prototype.removeEventListener = function(event, fn){
+ this._callbacks = this._callbacks || {};
+
+ // all
+ if (0 == arguments.length) {
+ this._callbacks = {};
+ return this;
+ }
+
+ // specific event
+ var callbacks = this._callbacks[event];
+ if (!callbacks) return this;
+
+ // remove all handlers
+ if (1 == arguments.length) {
+ delete this._callbacks[event];
+ return this;
+ }
+
+ // remove specific handler
+ var cb;
+ for (var i = 0; i < callbacks.length; i++) {
+ cb = callbacks[i];
+ if (cb === fn || cb.fn === fn) {
+ callbacks.splice(i, 1);
+ break;
+ }
+ }
+ return this;
+};
+
+/**
+ * Emit `event` with the given args.
+ *
+ * @param {String} event
+ * @param {Mixed} ...
+ * @return {Emitter}
+ */
+
+Emitter.prototype.emit = function(event){
+ this._callbacks = this._callbacks || {};
+ var args = [].slice.call(arguments, 1)
+ , callbacks = this._callbacks[event];
+
+ if (callbacks) {
+ callbacks = callbacks.slice(0);
+ for (var i = 0, len = callbacks.length; i < len; ++i) {
+ callbacks[i].apply(this, args);
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Return array of callbacks for `event`.
+ *
+ * @param {String} event
+ * @return {Array}
+ * @api public
+ */
+
+Emitter.prototype.listeners = function(event){
+ this._callbacks = this._callbacks || {};
+ return this._callbacks[event] || [];
+};
+
+/**
+ * Check if this emitter has `event` handlers.
+ *
+ * @param {String} event
+ * @return {Boolean}
+ * @api public
+ */
+
+Emitter.prototype.hasListeners = function(event){
+ return !! this.listeners(event).length;
+};
+
+},{}],3:[function(require,module,exports){
/*! Hammer.JS - v1.0.5 - 2013-04-07
* http://eightmedia.github.com/hammer.js
*
@@ -16688,9 +19122,9 @@ else {
}
}
})(this);
-},{}],3:[function(require,module,exports){
-//! moment.js
-//! version : 2.5.1
+},{}],4:[function(require,module,exports){
+var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {};//! moment.js
+//! version : 2.6.0
//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
//! license : MIT
//! momentjs.com
@@ -16702,8 +19136,10 @@ else {
************************************/
var moment,
- VERSION = "2.5.1",
- global = this,
+ VERSION = "2.6.0",
+ // the global-scope this is NOT the global object in Node.js
+ globalScope = typeof global !== 'undefined' ? global : this,
+ oldGlobalMoment,
round = Math.round,
i,
@@ -16732,7 +19168,7 @@ else {
},
// check for nodeJS
- hasModule = (typeof module !== 'undefined' && module.exports && typeof require !== 'undefined'),
+ hasModule = (typeof module !== 'undefined' && module.exports),
// ASP.NET json date format regex
aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
@@ -16743,7 +19179,7 @@ else {
isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,
// format tokens
- formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,
+ formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,
localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
// parsing token regexes
@@ -16756,6 +19192,7 @@ else {
parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z
parseTokenT = /T/i, // T (ISO separator)
parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
+ parseTokenOrdinal = /\d{1,2}/,
//strict parsing regexes
parseTokenOneDigit = /\d/, // 0 - 9
@@ -16781,7 +19218,7 @@ else {
// iso time formats and regexes
isoTimes = [
- ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/],
+ ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/],
['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
['HH:mm', /(T| )\d\d:\d\d/],
['HH', /(T| )\d\d/]
@@ -16812,6 +19249,7 @@ else {
w : 'week',
W : 'isoWeek',
M : 'month',
+ Q : 'quarter',
y : 'year',
DDD : 'dayOfYear',
e : 'weekday',
@@ -16987,6 +19425,23 @@ else {
};
}
+ function deprecate(msg, fn) {
+ var firstTime = true;
+ function printMsg() {
+ if (moment.suppressDeprecationWarnings === false &&
+ typeof console !== 'undefined' && console.warn) {
+ console.warn("Deprecation warning: " + msg);
+ }
+ }
+ return extend(function () {
+ if (firstTime) {
+ printMsg();
+ firstTime = false;
+ }
+ return fn.apply(this, arguments);
+ }, fn);
+ }
+
function padToken(func, count) {
return function (a) {
return leftZeroFill(func.call(this, a), count);
@@ -17027,6 +19482,7 @@ else {
function Duration(duration) {
var normalizedInput = normalizeObjectUnits(duration),
years = normalizedInput.year || 0,
+ quarters = normalizedInput.quarter || 0,
months = normalizedInput.month || 0,
weeks = normalizedInput.week || 0,
days = normalizedInput.day || 0,
@@ -17048,6 +19504,7 @@ else {
// which months you are are talking about, so we have to store
// it separately.
this._months = +months +
+ quarters * 3 +
years * 12;
this._data = {};
@@ -17110,34 +19567,23 @@ else {
}
// helper function for _.addTime and _.subtractTime
- function addOrSubtractDurationFromMoment(mom, duration, isAdding, ignoreUpdateOffset) {
+ function addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) {
var milliseconds = duration._milliseconds,
days = duration._days,
- months = duration._months,
- minutes,
- hours;
+ months = duration._months;
+ updateOffset = updateOffset == null ? true : updateOffset;
if (milliseconds) {
mom._d.setTime(+mom._d + milliseconds * isAdding);
}
- // store the minutes and hours so we can restore them
- if (days || months) {
- minutes = mom.minute();
- hours = mom.hour();
- }
if (days) {
- mom.date(mom.date() + days * isAdding);
+ rawSetter(mom, 'Date', rawGetter(mom, 'Date') + days * isAdding);
}
if (months) {
- mom.month(mom.month() + months * isAdding);
- }
- if (milliseconds && !ignoreUpdateOffset) {
- moment.updateOffset(mom);
+ rawMonthSetter(mom, rawGetter(mom, 'Month') + months * isAdding);
}
- // restore the minutes and hours after possibly changing dst
- if (days || months) {
- mom.minute(minutes);
- mom.hour(hours);
+ if (updateOffset) {
+ moment.updateOffset(mom, days || months);
}
}
@@ -17252,6 +19698,10 @@ else {
return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
}
+ function weeksInYear(year, dow, doy) {
+ return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week;
+ }
+
function daysInYear(year) {
return isLeapYear(year) ? 366 : 365;
}
@@ -17642,6 +20092,8 @@ else {
function getParseRegexForToken(token, config) {
var a, strict = config._strict;
switch (token) {
+ case 'Q':
+ return parseTokenOneDigit;
case 'DDDD':
return parseTokenThreeDigits;
case 'YYYY':
@@ -17710,6 +20162,8 @@ else {
case 'e':
case 'E':
return parseTokenOneOrTwoDigits;
+ case 'Do':
+ return parseTokenOrdinal;
default :
a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i"));
return a;
@@ -17731,6 +20185,12 @@ else {
var a, datePartArray = config._a;
switch (token) {
+ // QUARTER
+ case 'Q':
+ if (input != null) {
+ datePartArray[MONTH] = (toInt(input) - 1) * 3;
+ }
+ break;
// MONTH
case 'M' : // fall through to MM
case 'MM' :
@@ -17755,6 +20215,11 @@ else {
datePartArray[DATE] = toInt(input);
}
break;
+ case 'Do' :
+ if (input != null) {
+ datePartArray[DATE] = toInt(parseInt(input, 10));
+ }
+ break;
// DAY OF YEAR
case 'DDD' : // fall through to DDDD
case 'DDDD' :
@@ -17765,7 +20230,7 @@ else {
break;
// YEAR
case 'YY' :
- datePartArray[YEAR] = toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
+ datePartArray[YEAR] = moment.parseTwoDigitYear(input);
break;
case 'YYYY' :
case 'YYYYY' :
@@ -17854,9 +20319,9 @@ else {
//compute day of the year from weeks and weekdays
if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
fixYear = function (val) {
- var int_val = parseInt(val, 10);
+ var intVal = parseInt(val, 10);
return val ?
- (val.length < 3 ? (int_val > 68 ? 1900 + int_val : 2000 + int_val) : int_val) :
+ (val.length < 3 ? (intVal > 68 ? 1900 + intVal : 2000 + intVal) : intVal) :
(config._a[YEAR] == null ? moment().weekYear() : config._a[YEAR]);
};
@@ -18092,7 +20557,7 @@ else {
makeDateFromStringAndFormat(config);
}
else {
- config._d = new Date(string);
+ moment.createFromInputFallback(config);
}
}
@@ -18113,8 +20578,11 @@ else {
config._d = new Date(+input);
} else if (typeof(input) === 'object') {
dateFromObject(config);
- } else {
+ } else if (typeof(input) === 'number') {
+ // from milliseconds
config._d = new Date(input);
+ } else {
+ moment.createFromInputFallback(config);
}
}
@@ -18241,7 +20709,7 @@ else {
var input = config._i,
format = config._f;
- if (input === null) {
+ if (input === null || (format === undefined && input === '')) {
return moment.invalid({nullInput: true});
}
@@ -18287,6 +20755,17 @@ else {
return makeMoment(c);
};
+ moment.suppressDeprecationWarnings = false;
+
+ moment.createFromInputFallback = deprecate(
+ "moment construction falls back to js Date. This is " +
+ "discouraged and will be removed in upcoming major " +
+ "release. Please refer to " +
+ "https://github.com/moment/moment/issues/1407 for more info.",
+ function (config) {
+ config._d = new Date(config._i);
+ });
+
// creating with utc
moment.utc = function (input, format, lang, strict) {
var c;
@@ -18383,6 +20862,10 @@ else {
// default format
moment.defaultFormat = isoFormat;
+ // Plugins that add properties should also add the key here (null value),
+ // so we can properly clone ourselves.
+ moment.momentProperties = momentProperties;
+
// This function will be called whenever a moment is mutated.
// It is intended to keep the offset in sync with the timezone.
moment.updateOffset = function () {};
@@ -18446,8 +20929,12 @@ else {
return m;
};
- moment.parseZone = function (input) {
- return moment(input).parseZone();
+ moment.parseZone = function () {
+ return moment.apply(null, arguments).parseZone();
+ };
+
+ moment.parseTwoDigitYear = function (input) {
+ return toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
};
/************************************
@@ -18634,29 +21121,7 @@ else {
}
},
- month : function (input) {
- var utc = this._isUTC ? 'UTC' : '',
- dayOfMonth;
-
- if (input != null) {
- if (typeof input === 'string') {
- input = this.lang().monthsParse(input);
- if (typeof input !== 'number') {
- return this;
- }
- }
-
- dayOfMonth = this.date();
- this.date(1);
- this._d['set' + utc + 'Month'](input);
- this.date(Math.min(dayOfMonth, this.daysInMonth()));
-
- moment.updateOffset(this);
- return this;
- } else {
- return this._d['get' + utc + 'Month']();
- }
- },
+ month : makeAccessor('Month', true),
startOf: function (units) {
units = normalizeUnits(units);
@@ -18666,6 +21131,7 @@ else {
case 'year':
this.month(0);
/* falls through */
+ case 'quarter':
case 'month':
this.date(1);
/* falls through */
@@ -18692,6 +21158,11 @@ else {
this.isoWeekday(1);
}
+ // quarters are also special
+ if (units === 'quarter') {
+ this.month(Math.floor(this.month() / 3) * 3);
+ }
+
return this;
},
@@ -18725,7 +21196,17 @@ else {
return other > this ? this : other;
},
- zone : function (input) {
+ // keepTime = true means only change the timezone, without affecting
+ // the local hour. So 5:31:26 +0300 --[zone(2, true)]--> 5:31:26 +0200
+ // It is possible that 5:31:26 doesn't exist int zone +0200, so we
+ // adjust the time as needed, to be valid.
+ //
+ // Keeping the time actually adds/subtracts (one hour)
+ // from the actual represented time. That is why we call updateOffset
+ // a second time. In case it wants us to change the offset again
+ // _changeInProgress == true case, then we have to adjust, because
+ // there is no such time in the given timezone.
+ zone : function (input, keepTime) {
var offset = this._offset || 0;
if (input != null) {
if (typeof input === "string") {
@@ -18737,7 +21218,14 @@ else {
this._offset = input;
this._isUTC = true;
if (offset !== input) {
- addOrSubtractDurationFromMoment(this, moment.duration(offset - input, 'm'), 1, true);
+ if (!keepTime || this._changeInProgress) {
+ addOrSubtractDurationFromMoment(this,
+ moment.duration(offset - input, 'm'), 1, false);
+ } else if (!this._changeInProgress) {
+ this._changeInProgress = true;
+ moment.updateOffset(this, true);
+ this._changeInProgress = null;
+ }
}
} else {
return this._isUTC ? offset : this._d.getTimezoneOffset();
@@ -18782,8 +21270,8 @@ else {
return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
},
- quarter : function () {
- return Math.ceil((this.month() + 1.0) / 3.0);
+ quarter : function (input) {
+ return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3);
},
weekYear : function (input) {
@@ -18818,6 +21306,15 @@ else {
return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
},
+ isoWeeksInYear : function () {
+ return weeksInYear(this.year(), 1, 4);
+ },
+
+ weeksInYear : function () {
+ var weekInfo = this._lang._week;
+ return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy);
+ },
+
get : function (units) {
units = normalizeUnits(units);
return this[units]();
@@ -18844,33 +21341,68 @@ else {
}
});
- // helper for adding shortcuts
- function makeGetterAndSetter(name, key) {
- moment.fn[name] = moment.fn[name + 's'] = function (input) {
- var utc = this._isUTC ? 'UTC' : '';
- if (input != null) {
- this._d['set' + utc + key](input);
- moment.updateOffset(this);
+ function rawMonthSetter(mom, value) {
+ var dayOfMonth;
+
+ // TODO: Move this out of here!
+ if (typeof value === 'string') {
+ value = mom.lang().monthsParse(value);
+ // TODO: Another silent failure?
+ if (typeof value !== 'number') {
+ return mom;
+ }
+ }
+
+ dayOfMonth = Math.min(mom.date(),
+ daysInMonth(mom.year(), value));
+ mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth);
+ return mom;
+ }
+
+ function rawGetter(mom, unit) {
+ return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]();
+ }
+
+ function rawSetter(mom, unit, value) {
+ if (unit === 'Month') {
+ return rawMonthSetter(mom, value);
+ } else {
+ return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value);
+ }
+ }
+
+ function makeAccessor(unit, keepTime) {
+ return function (value) {
+ if (value != null) {
+ rawSetter(this, unit, value);
+ moment.updateOffset(this, keepTime);
return this;
} else {
- return this._d['get' + utc + key]();
+ return rawGetter(this, unit);
}
};
}
- // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds)
- for (i = 0; i < proxyGettersAndSetters.length; i ++) {
- makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]);
- }
-
- // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
- makeGetterAndSetter('year', 'FullYear');
+ moment.fn.millisecond = moment.fn.milliseconds = makeAccessor('Milliseconds', false);
+ moment.fn.second = moment.fn.seconds = makeAccessor('Seconds', false);
+ moment.fn.minute = moment.fn.minutes = makeAccessor('Minutes', false);
+ // Setting the hour should keep the time, because the user explicitly
+ // specified which hour he wants. So trying to maintain the same hour (in
+ // a new timezone) makes sense. Adding/subtracting hours does not follow
+ // this rule.
+ moment.fn.hour = moment.fn.hours = makeAccessor('Hours', true);
+ // moment.fn.month is defined separately
+ moment.fn.date = makeAccessor('Date', true);
+ moment.fn.dates = deprecate("dates accessor is deprecated. Use date instead.", makeAccessor('Date', true));
+ moment.fn.year = makeAccessor('FullYear', true);
+ moment.fn.years = deprecate("years accessor is deprecated. Use year instead.", makeAccessor('FullYear', true));
// add plural methods
moment.fn.days = moment.fn.day;
moment.fn.months = moment.fn.month;
moment.fn.weeks = moment.fn.week;
moment.fn.isoWeeks = moment.fn.isoWeek;
+ moment.fn.quarters = moment.fn.quarter;
// add aliased format methods
moment.fn.toJSON = moment.fn.toISOString;
@@ -19046,51 +21578,42 @@ else {
Exposing Moment
************************************/
- function makeGlobal(deprecate) {
- var warned = false, local_moment = moment;
+ function makeGlobal(shouldDeprecate) {
/*global ender:false */
if (typeof ender !== 'undefined') {
return;
}
- // here, `this` means `window` in the browser, or `global` on the server
- // add `moment` as a global object via a string identifier,
- // for Closure Compiler "advanced" mode
- if (deprecate) {
- global.moment = function () {
- if (!warned && console && console.warn) {
- warned = true;
- console.warn(
- "Accessing Moment through the global scope is " +
- "deprecated, and will be removed in an upcoming " +
- "release.");
- }
- return local_moment.apply(null, arguments);
- };
- extend(global.moment, local_moment);
+ oldGlobalMoment = globalScope.moment;
+ if (shouldDeprecate) {
+ globalScope.moment = deprecate(
+ "Accessing Moment through the global scope is " +
+ "deprecated, and will be removed in an upcoming " +
+ "release.",
+ moment);
} else {
- global['moment'] = moment;
+ globalScope.moment = moment;
}
}
// CommonJS module is defined
if (hasModule) {
module.exports = moment;
- makeGlobal(true);
} else if (typeof define === "function" && define.amd) {
define("moment", function (require, exports, module) {
- if (module.config && module.config() && module.config().noGlobal !== true) {
- // If user provided noGlobal, he is aware of global
- makeGlobal(module.config().noGlobal === undefined);
+ if (module.config && module.config() && module.config().noGlobal === true) {
+ // release the global variable
+ globalScope.moment = oldGlobalMoment;
}
return moment;
});
+ makeGlobal(true);
} else {
makeGlobal();
}
}).call(this);
-},{}],4:[function(require,module,exports){
+},{}],5:[function(require,module,exports){
/**
* Copyright 2012 Craig Campbell
*
diff --git a/dist/vis.min.css b/dist/vis.min.css
new file mode 100644
index 00000000..6b32bc70
--- /dev/null
+++ b/dist/vis.min.css
@@ -0,0 +1 @@
+.vis.timeline.rootpanel{position:relative;overflow:hidden;border:1px solid #bfbfbf;-moz-box-sizing:border-box;box-sizing:border-box}.vis.timeline .vpanel{position:absolute;overflow:hidden;-moz-box-sizing:border-box;box-sizing:border-box}.vis.timeline .vpanel.side{border-right:1px solid #bfbfbf}.vis.timeline .vpanel.side.hidden{display:none}.vis.timeline .groupset{position:relative}.vis.timeline .labelset{position:relative;width:100%;overflow:hidden;-moz-box-sizing:border-box;box-sizing:border-box}.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.bottom .labelset .vlabel,.vis.timeline.top .groupset .itemset,.vis.timeline.top .vpanel.side-content{border-top:1px solid #bfbfbf;border-bottom:none}.vis.timeline.bottom .groupset .itemset,.vis.timeline.bottom .vpanel.side-content,.vis.timeline.top .labelset .vlabel{border-top:none;border-bottom:1px solid #bfbfbf}.vis.timeline .labelset .vlabel .inner{display:inline-block;padding:5px}.vis.timeline .itemset{position:relative;padding:0;margin:0;-moz-box-sizing:border-box;box-sizing:border-box}.vis.timeline .axis{overflow:visible}.vis.timeline .item{position:absolute;color:#1A1A1A;border-color:#97B0F8;background-color:#D5DDF6;display:inline-block;padding:5px}.vis.timeline .item.selected{border-color:#FFC200;background-color:#FFF785;z-index:999}.vis.timeline.editable .item.selected{cursor:move}.vis.timeline .item.point.selected{background-color:#FFF785;z-index:999}.vis.timeline .item.dot.selected,.vis.timeline .item.point.selected .dot{border-color:#FFC200}.vis.timeline .item.cluster{background:#97B0F8 url(img/cluster_bg.png);color:#fff}.vis.timeline .item.cluster.point{border-color:#D5DDF6}.vis.timeline .item.box{text-align:center;border-style:solid;border-width:1px;border-radius:5px;-moz-border-radius:5px}.vis.timeline .item.point{background:0 0}.vis.timeline .dot,.vis.timeline .item.dot{padding:0;border:5px solid #97B0F8;position:absolute;border-radius:5px;-moz-border-radius:5px}.vis.timeline .item.range,.vis.timeline .item.rangeoverflow{border-style:solid;border-width:1px;border-radius:2px;-moz-border-radius:2px;-moz-box-sizing:border-box;box-sizing:border-box}.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}.vis.timeline .item .content{white-space:nowrap;overflow:hidden}.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 .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}.vis.timeline .timeaxis{position:absolute}.vis.timeline .timeaxis .text{position:absolute;color:#4d4d4d;padding:3px;white-space:nowrap}.vis.timeline .timeaxis .text.measure{position:absolute;padding-left:0;padding-right:0;margin-left:0;margin-right:0;visibility:hidden}.vis.timeline .timeaxis .grid.vertical{position:absolute;width:0;border-right:1px solid}.vis.timeline .timeaxis .grid.horizontal{position:absolute;left:0;width:100%;height:0;border-bottom:1px solid}.vis.timeline .timeaxis .grid.minor{border-color:#e5e5e5}.vis.timeline .timeaxis .grid.major{border-color:#bfbfbf}.vis.timeline .currenttime{background-color:#FF7F6E;width:2px;z-index:9}.vis.timeline .customtime{background-color:#6E94FF;width:2px;cursor:move;z-index:9}div.graph-manipulationDiv{border-width:0;border-bottom:1px;border-style:solid;border-color:#d6d9d8;background:#fff;background:-moz-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#fff),color-stop(48%,#fcfcfc),color-stop(50%,#fafafa),color-stop(100%,#fcfcfc));background:-webkit-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:-o-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:-ms-linear-gradient(top,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);background:linear-gradient(to bottom,#fff 0,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#fcfcfc', GradientType=0);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:0 0;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:0 0;background-repeat:no-repeat;height:24px;margin:-14px 0 0 10px;vertical-align:middle;cursor:pointer;padding:0 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,.2)}span.graph-manipulationUI:active{box-shadow:1px 1px 8px rgba(0,0,0,.5)}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);cursor:default}span.graph-manipulationUI.none:active{box-shadow:1px 1px 8px rgba(0,0,0,0)}span.graph-manipulationUI.none{padding:0}span.graph-manipulationUI.notification{margin:2px;font-weight:700}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:#ccc}span.graph-manipulationUI.connect{background-image:url(img/graph/connectIcon.png)}span.graph-manipulationUI.delete{background-image:url(img/graph/deleteIcon.png)}span.graph-manipulationLabel{margin:0 0 0 23px;line-height:25px}div.graph-seperatorLine{display:inline-block;width:1px;height:20px;background-color:#bdbdbd;margin:5px 7px 0 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:0 0 3px 3px rgba(56,207,21,.3)}div.graph-navigation.active,div.graph-navigation:active{box-shadow:0 0 1px 3px rgba(56,207,21,.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}
\ No newline at end of file
diff --git a/dist/vis.min.js b/dist/vis.min.js
index 083f0f1f..a7ae2cb0 100644
--- a/dist/vis.min.js
+++ b/dist/vis.min.js
@@ -4,8 +4,8 @@
*
* A dynamic, browser-based visualization library.
*
- * @version 0.4.0
- * @date 2014-01-31
+ * @version 0.7.5-SNAPSHOT
+ * @date 2014-04-22
*
* @license
* Copyright (C) 2011-2014 Almende B.V, http://almende.com
@@ -22,10 +22,12 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
-!function(t){if("object"==typeof exports)module.exports=t();else if("function"==typeof define&&define.amd)define(t);else{var e;"undefined"!=typeof window?e=window:"undefined"!=typeof global?e=global:"undefined"!=typeof self&&(e=self),e.vis=t()}}(function(){var t;return function e(t,i,n){function s(r,a){if(!i[r]){if(!t[r]){var h="function"==typeof require&&require;if(!a&&h)return h(r,!0);if(o)return o(r,!0);throw new Error("Cannot find module '"+r+"'")}var d=i[r]={exports:{}};t[r][0].call(d.exports,function(e){var i=t[r][1][e];return s(i?i:e)},d,d.exports,e,t,i,n)}return i[r].exports}for(var o="function"==typeof require&&require,r=0;ri;++i)t.call(e||this,this[i],i,this)}),Array.prototype.map||(Array.prototype.map=function(t,e){var i,n,s;if(null==this)throw new TypeError(" this is null or not defined");var o=Object(this),r=o.length>>>0;if("function"!=typeof t)throw new TypeError(t+" is not a function");for(e&&(i=e),n=new Array(r),s=0;r>s;){var a,h;s in o&&(a=o[s],h=t.call(i,a,s,o),n[s]=h),s++}return n}),Array.prototype.filter||(Array.prototype.filter=function(t){"use strict";if(null==this)throw new TypeError;var e=Object(this),i=e.length>>>0;if("function"!=typeof t)throw new TypeError;for(var n=[],s=arguments[1],o=0;i>o;o++)if(o in e){var r=e[o];t.call(s,r,o,e)&&n.push(r)}return n}),Object.keys||(Object.keys=function(){var t=Object.prototype.hasOwnProperty,e=!{toString:null}.propertyIsEnumerable("toString"),i=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],n=i.length;return function(s){if("object"!=typeof s&&"function"!=typeof s||null===s)throw new TypeError("Object.keys called on non-object");var o=[];for(var r in s)t.call(s,r)&&o.push(r);if(e)for(var a=0;n>a;a++)t.call(s,i[a])&&o.push(i[a]);return o}}()),Array.isArray||(Array.isArray=function(t){return"[object Array]"===Object.prototype.toString.call(t)}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,n=function(){},s=function(){return i.apply(this instanceof n&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return n.prototype=this.prototype,s.prototype=new n,s}),Object.create||(Object.create=function(t){function e(){}if(arguments.length>1)throw new Error("Object.create implementation only accepts the first parameter.");return e.prototype=t,new e}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,n=function(){},s=function(){return i.apply(this instanceof n&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return n.prototype=this.prototype,s.prototype=new n,s});var z={};z.isNumber=function(t){return t instanceof Number||"number"==typeof t},z.isString=function(t){return t instanceof String||"string"==typeof t},z.isDate=function(t){if(t instanceof Date)return!0;if(z.isString(t)){var e=P.exec(t);if(e)return!0;if(!isNaN(Date.parse(t)))return!0}return!1},z.isDataTable=function(t){return"undefined"!=typeof google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable},z.randomUUID=function(){var t=function(){return Math.floor(65536*Math.random()).toString(16)};return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()},z.extend=function(t){for(var e=1,i=arguments.length;i>e;e++){var n=arguments[e];for(var s in n)n.hasOwnProperty(s)&&void 0!==n[s]&&(t[s]=n[s])}return t},z.convert=function(t,e){var i;if(void 0===t)return void 0;if(null===t)return null;if(!e)return t;if("string"!=typeof e&&!(e instanceof String))throw new Error("Type must be a string");switch(e){case"boolean":case"Boolean":return Boolean(t);case"number":case"Number":return Number(t.valueOf());case"string":case"String":return String(t);case"Date":if(z.isNumber(t))return new Date(t);if(t instanceof Date)return new Date(t.valueOf());if(L.isMoment(t))return new Date(t.valueOf());if(z.isString(t))return i=P.exec(t),i?new Date(Number(i[1])):L(t).toDate();throw new Error("Cannot convert object of type "+z.getType(t)+" to type Date");case"Moment":if(z.isNumber(t))return L(t);if(t instanceof Date)return L(t.valueOf());if(L.isMoment(t))return L(t);if(z.isString(t))return i=P.exec(t),L(i?Number(i[1]):t);throw new Error("Cannot convert object of type "+z.getType(t)+" to type Date");case"ISODate":if(z.isNumber(t))return new Date(t);if(t instanceof Date)return t.toISOString();if(L.isMoment(t))return t.toDate().toISOString();if(z.isString(t))return i=P.exec(t),i?new Date(Number(i[1])).toISOString():new Date(t).toISOString();throw new Error("Cannot convert object of type "+z.getType(t)+" to type ISODate");case"ASPDate":if(z.isNumber(t))return"/Date("+t+")/";if(t instanceof Date)return"/Date("+t.valueOf()+")/";if(z.isString(t)){i=P.exec(t);var n;return n=i?new Date(Number(i[1])).valueOf():new Date(t).valueOf(),"/Date("+n+")/"}throw new Error("Cannot convert object of type "+z.getType(t)+" to type ASPDate");default:throw new Error("Cannot convert object of type "+z.getType(t)+' to type "'+e+'"')}};var P=/^\/?Date\((\-?\d+)/i;z.getType=function(t){var e=typeof t;return"object"==e?null==t?"null":t instanceof Boolean?"Boolean":t instanceof Number?"Number":t instanceof String?"String":t instanceof Array?"Array":t instanceof Date?"Date":"Object":"number"==e?"Number":"boolean"==e?"Boolean":"string"==e?"String":e},z.getAbsoluteLeft=function(t){for(var e=document.documentElement,i=document.body,n=t.offsetLeft,s=t.offsetParent;null!=s&&s!=i&&s!=e;)n+=s.offsetLeft,n-=s.scrollLeft,s=s.offsetParent;return n},z.getAbsoluteTop=function(t){for(var e=document.documentElement,i=document.body,n=t.offsetTop,s=t.offsetParent;null!=s&&s!=i&&s!=e;)n+=s.offsetTop,n-=s.scrollTop,s=s.offsetParent;return n},z.getPageY=function(t){if("pageY"in t)return t.pageY;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientY:t.clientY;var i=document.documentElement,n=document.body;return e+(i&&i.scrollTop||n&&n.scrollTop||0)-(i&&i.clientTop||n&&n.clientTop||0)},z.getPageX=function(t){if("pageY"in t)return t.pageX;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientX:t.clientX;var i=document.documentElement,n=document.body;return e+(i&&i.scrollLeft||n&&n.scrollLeft||0)-(i&&i.clientLeft||n&&n.clientLeft||0)},z.addClassName=function(t,e){var i=t.className.split(" ");-1==i.indexOf(e)&&(i.push(e),t.className=i.join(" "))},z.removeClassName=function(t,e){var i=t.className.split(" "),n=i.indexOf(e);-1!=n&&(i.splice(n,1),t.className=i.join(" "))},z.forEach=function(t,e){var i,n;if(t instanceof Array)for(i=0,n=t.length;n>i;i++)e(t[i],i,t);else for(i in t)t.hasOwnProperty(i)&&e(t[i],i,t)},z.updateProperty=function(t,e,i){return t[e]!==i?(t[e]=i,!0):!1},z.addEventListener=function(t,e,i,n){t.addEventListener?(void 0===n&&(n=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.addEventListener(e,i,n)):t.attachEvent("on"+e,i)},z.removeEventListener=function(t,e,i,n){t.removeEventListener?(void 0===n&&(n=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.removeEventListener(e,i,n)):t.detachEvent("on"+e,i)},z.getTarget=function(t){t||(t=window.event);var e;return t.target?e=t.target:t.srcElement&&(e=t.srcElement),void 0!=e.nodeType&&3==e.nodeType&&(e=e.parentNode),e},z.stopPropagation=function(t){t||(t=window.event),t.stopPropagation?t.stopPropagation():t.cancelBubble=!0},z.fakeGesture=function(t,e){var i=null;return N.event.collectEventData(this,i,e)},z.preventDefault=function(t){t||(t=window.event),t.preventDefault?t.preventDefault():t.returnValue=!1},z.option={},z.option.asBoolean=function(t,e){return"function"==typeof t&&(t=t()),null!=t?0!=t:e||null},z.option.asNumber=function(t,e){return"function"==typeof t&&(t=t()),null!=t?Number(t)||e||null:e||null},z.option.asString=function(t,e){return"function"==typeof t&&(t=t()),null!=t?String(t):e||null},z.option.asSize=function(t,e){return"function"==typeof t&&(t=t()),z.isString(t)?t:z.isNumber(t)?t+"px":e||null},z.option.asElement=function(t,e){return"function"==typeof t&&(t=t()),t||e||null};var F={listeners:[],indexOf:function(t){for(var e=this.listeners,i=0,n=this.listeners.length;n>i;i++){var s=e[i];if(s&&s.object==t)return i}return-1},addListener:function(t,e,i){var n=this.indexOf(t),s=this.listeners[n];s||(s={object:t,events:{}},this.listeners.push(s));var o=s.events[e];o||(o=[],s.events[e]=o),-1==o.indexOf(i)&&o.push(i)},removeListener:function(t,e,i){var n=this.indexOf(t),s=this.listeners[n];if(s){var o=s.events[e];o&&(n=o.indexOf(i),-1!=n&&o.splice(n,1),0==o.length&&delete s.events[e]);var r=0,a=s.events;for(var h in a)a.hasOwnProperty(h)&&r++;0==r&&delete this.listeners[n]}},removeAllListeners:function(){this.listeners=[]},trigger:function(t,e,i){var n=this.indexOf(t),s=this.listeners[n];if(s){var o=s.events[e];if(o)for(var r=0,a=o.length;a>r;r++)o[r](i)}}};s.prototype.on=function(t,e,i){var n=t instanceof RegExp?t:new RegExp(t.replace("*","\\w+")),s={id:z.randomUUID(),event:t,regexp:n,callback:"function"==typeof e?e:null,target:i};return this.subscriptions.push(s),s.id},s.prototype.off=function(t){for(var e=0;eo;o++)i=s._addItem(t[o]),n.push(i);else if(z.isDataTable(t))for(var a=this._getColumnNames(t),h=0,d=t.getNumberOfRows();d>h;h++){for(var c={},l=0,u=a.length;u>l;l++){var p=a[l];c[p]=t.getValue(h,l)}i=s._addItem(c),n.push(i)}else{if(!(t instanceof Object))throw new Error("Unknown dataType");i=s._addItem(t),n.push(i)}return n.length&&this._trigger("add",{items:n},e),n},o.prototype.update=function(t,e){var i=[],n=[],s=this,o=s.fieldId,r=function(t){var e=t[o];s.data[e]?(e=s._updateItem(t),n.push(e)):(e=s._addItem(t),i.push(e))};if(t instanceof Array)for(var a=0,h=t.length;h>a;a++)r(t[a]);else if(z.isDataTable(t))for(var d=this._getColumnNames(t),c=0,l=t.getNumberOfRows();l>c;c++){for(var u={},p=0,f=d.length;f>p;p++){var g=d[p];u[g]=t.getValue(c,p)}r(u)}else{if(!(t instanceof Object))throw new Error("Unknown dataType");r(t)}return i.length&&this._trigger("add",{items:i},e),n.length&&this._trigger("update",{items:n},e),i.concat(n)},o.prototype.get=function(){var t,e,i,n,s=this,o=this.showInternalIds,r=z.getType(arguments[0]);"String"==r||"Number"==r?(t=arguments[0],i=arguments[1],n=arguments[2]):"Array"==r?(e=arguments[0],i=arguments[1],n=arguments[2]):(i=arguments[0],n=arguments[1]);var a;if(i&&i.type){if(a="DataTable"==i.type?"DataTable":"Array",n&&a!=z.getType(n))throw new Error('Type of parameter "data" ('+z.getType(n)+") does not correspond with specified options.type ("+i.type+")");if("DataTable"==a&&!z.isDataTable(n))throw new Error('Parameter "data" must be a DataTable when options.type is "DataTable"')}else a=n?"DataTable"==z.getType(n)?"DataTable":"Array":"Array";void 0!=i&&void 0!=i.showInternalIds&&(this.showInternalIds=i.showInternalIds);var h,d,c,l,u=i&&i.convert||this.options.convert,p=i&&i.filter,f=[];if(void 0!=t)h=s._getItem(t,u),p&&!p(h)&&(h=null);else if(void 0!=e)for(c=0,l=e.length;l>c;c++)h=s._getItem(e[c],u),(!p||p(h))&&f.push(h);else for(d in this.data)this.data.hasOwnProperty(d)&&(h=s._getItem(d,u),(!p||p(h))&&f.push(h));if(this.showInternalIds=o,i&&i.order&&void 0==t&&this._sort(f,i.order),i&&i.fields){var g=i.fields;if(void 0!=t)h=this._filterFields(h,g);else for(c=0,l=f.length;l>c;c++)f[c]=this._filterFields(f[c],g)}if("DataTable"==a){var m=this._getColumnNames(n);if(void 0!=t)s._appendRow(n,m,h);else for(c=0,l=f.length;l>c;c++)s._appendRow(n,m,f[c]);return n}if(void 0!=t)return h;if(n){for(c=0,l=f.length;l>c;c++)n.push(f[c]);return n}return f},o.prototype.getIds=function(t){var e,i,n,s,o,r=this.data,a=t&&t.filter,h=t&&t.order,d=t&&t.convert||this.options.convert,c=[];if(a)if(h){o=[];for(n in r)r.hasOwnProperty(n)&&(s=this._getItem(n,d),a(s)&&o.push(s));for(this._sort(o,h),e=0,i=o.length;i>e;e++)c[e]=o[e][this.fieldId]}else for(n in r)r.hasOwnProperty(n)&&(s=this._getItem(n,d),a(s)&&c.push(s[this.fieldId]));else if(h){o=[];for(n in r)r.hasOwnProperty(n)&&o.push(r[n]);for(this._sort(o,h),e=0,i=o.length;i>e;e++)c[e]=o[e][this.fieldId]}else for(n in r)r.hasOwnProperty(n)&&(s=r[n],c.push(s[this.fieldId]));return c},o.prototype.forEach=function(t,e){var i,n,s=e&&e.filter,o=e&&e.convert||this.options.convert,r=this.data;if(e&&e.order)for(var a=this.get(e),h=0,d=a.length;d>h;h++)i=a[h],n=i[this.fieldId],t(i,n);else for(n in r)r.hasOwnProperty(n)&&(i=this._getItem(n,o),(!s||s(i))&&t(i,n))},o.prototype.map=function(t,e){var i,n=e&&e.filter,s=e&&e.convert||this.options.convert,o=[],r=this.data;for(var a in r)r.hasOwnProperty(a)&&(i=this._getItem(a,s),(!n||n(i))&&o.push(t(i,a)));return e&&e.order&&this._sort(o,e.order),o},o.prototype._filterFields=function(t,e){var i={};for(var n in t)t.hasOwnProperty(n)&&-1!=e.indexOf(n)&&(i[n]=t[n]);return i},o.prototype._sort=function(t,e){if(z.isString(e)){var i=e;t.sort(function(t,e){var n=t[i],s=e[i];return n>s?1:s>n?-1:0})}else{if("function"!=typeof e)throw new TypeError("Order must be a function or a string");t.sort(e)}},o.prototype.remove=function(t,e){var i,n,s,o=[];if(t instanceof Array)for(i=0,n=t.length;n>i;i++)s=this._remove(t[i]),null!=s&&o.push(s);else s=this._remove(t),null!=s&&o.push(s);return o.length&&this._trigger("remove",{items:o},e),o},o.prototype._remove=function(t){if(z.isNumber(t)||z.isString(t)){if(this.data[t])return delete this.data[t],delete this.internalIds[t],t}else if(t instanceof Object){var e=t[this.fieldId];if(e&&this.data[e])return delete this.data[e],delete this.internalIds[e],e}return null},o.prototype.clear=function(t){var e=Object.keys(this.data);return this.data={},this.internalIds={},this._trigger("remove",{items:e},t),e},o.prototype.max=function(t){var e=this.data,i=null,n=null;for(var s in e)if(e.hasOwnProperty(s)){var o=e[s],r=o[t];null!=r&&(!i||r>n)&&(i=o,n=r)}return i},o.prototype.min=function(t){var e=this.data,i=null,n=null;for(var s in e)if(e.hasOwnProperty(s)){var o=e[s],r=o[t];null!=r&&(!i||n>r)&&(i=o,n=r)}return i},o.prototype.distinct=function(t){var e=this.data,i=[],n=this.options.convert[t],s=0;for(var o in e)if(e.hasOwnProperty(o)){for(var r=e[o],a=z.convert(r[t],n),h=!1,d=0;s>d;d++)if(i[d]==a){h=!0;break}h||(i[s]=a,s++)}return i},o.prototype._addItem=function(t){var e=t[this.fieldId];if(void 0!=e){if(this.data[e])throw new Error("Cannot add item: item with id "+e+" already exists")}else e=z.randomUUID(),t[this.fieldId]=e,this.internalIds[e]=t;var i={};for(var n in t)if(t.hasOwnProperty(n)){var s=this.convert[n];i[n]=z.convert(t[n],s)}return this.data[e]=i,e},o.prototype._getItem=function(t,e){var i,n,s=this.data[t];if(!s)return null;var o={},r=this.fieldId,a=this.internalIds;if(e)for(i in s)s.hasOwnProperty(i)&&(n=s[i],i==r&&n in a&&!this.showInternalIds||(o[i]=z.convert(n,e[i])));else for(i in s)s.hasOwnProperty(i)&&(n=s[i],i==r&&n in a&&!this.showInternalIds||(o[i]=n));return o},o.prototype._updateItem=function(t){var e=t[this.fieldId];if(void 0==e)throw new Error("Cannot update item: item has no id (item: "+JSON.stringify(t)+")");var i=this.data[e];if(!i)throw new Error("Cannot update item: no item with id "+e+" found");for(var n in t)if(t.hasOwnProperty(n)){var s=this.convert[n];i[n]=z.convert(t[n],s)}return e},o.prototype.isInternalId=function(t){return t in this.internalIds},o.prototype._getColumnNames=function(t){for(var e=[],i=0,n=t.getNumberOfColumns();n>i;i++)e[i]=t.getColumnId(i)||t.getColumnLabel(i);return e},o.prototype._appendRow=function(t,e,i){for(var n=t.addRow(),s=0,o=e.length;o>s;s++){var r=e[s];t.setValue(n,s,i[r])}},r.prototype.setData=function(t){var e,i,n;if(this.data){this.data.unsubscribe&&this.data.unsubscribe("*",this.listener),e=[];for(var s in this.ids)this.ids.hasOwnProperty(s)&&e.push(s);this.ids={},this._trigger("remove",{items:e})}if(this.data=t,this.data){for(this.fieldId=this.options.fieldId||this.data&&this.data.options&&this.data.options.fieldId||"id",e=this.data.getIds({filter:this.options&&this.options.filter}),i=0,n=e.length;n>i;i++)s=e[i],this.ids[s]=!0;this._trigger("add",{items:e}),this.data.subscribe&&this.data.subscribe("*",this.listener)}},r.prototype.get=function(){var t,e,i,n=this,s=z.getType(arguments[0]);"String"==s||"Number"==s||"Array"==s?(t=arguments[0],e=arguments[1],i=arguments[2]):(e=arguments[0],i=arguments[1]);var o=z.extend({},this.options,e);this.options.filter&&e&&e.filter&&(o.filter=function(t){return n.options.filter(t)&&e.filter(t)});var r=[];return void 0!=t&&r.push(t),r.push(o),r.push(i),this.data&&this.data.get.apply(this.data,r)},r.prototype.getIds=function(t){var e;if(this.data){var i,n=this.options.filter;i=t&&t.filter?n?function(e){return n(e)&&t.filter(e)}:t.filter:n,e=this.data.getIds({filter:i,order:t&&t.order})}else e=[];return e},r.prototype._onEvent=function(t,e,i){var n,s,o,r,a=e&&e.items,h=this.data,d=[],c=[],l=[];if(a&&h){switch(t){case"add":for(n=0,s=a.length;s>n;n++)o=a[n],r=this.get(o),r&&(this.ids[o]=!0,d.push(o));break;case"update":for(n=0,s=a.length;s>n;n++)o=a[n],r=this.get(o),r?this.ids[o]?c.push(o):(this.ids[o]=!0,d.push(o)):this.ids[o]&&(delete this.ids[o],l.push(o));break;case"remove":for(n=0,s=a.length;s>n;n++)o=a[n],this.ids[o]&&(delete this.ids[o],l.push(o))}d.length&&this._trigger("add",{items:d},i),c.length&&this._trigger("update",{items:c},i),l.length&&this._trigger("remove",{items:l},i)}},r.prototype.subscribe=o.prototype.subscribe,r.prototype.unsubscribe=o.prototype.unsubscribe,r.prototype._trigger=o.prototype._trigger,TimeStep=function(t,e,i){this.current=new Date,this._start=new Date,this._end=new Date,this.autoScale=!0,this.scale=TimeStep.SCALE.DAY,this.step=1,this.setRange(t,e,i)},TimeStep.SCALE={MILLISECOND:1,SECOND:2,MINUTE:3,HOUR:4,DAY:5,WEEKDAY:6,MONTH:7,YEAR:8},TimeStep.prototype.setRange=function(t,e,i){if(!(t instanceof Date&&e instanceof Date))throw"No legal start or end date in method setRange";this._start=void 0!=t?new Date(t.valueOf()):new Date,this._end=void 0!=e?new Date(e.valueOf()):new Date,this.autoScale&&this.setMinimumStep(i)},TimeStep.prototype.first=function(){this.current=new Date(this._start.valueOf()),this.roundToMinor()},TimeStep.prototype.roundToMinor=function(){switch(this.scale){case TimeStep.SCALE.YEAR:this.current.setFullYear(this.step*Math.floor(this.current.getFullYear()/this.step)),this.current.setMonth(0);case TimeStep.SCALE.MONTH:this.current.setDate(1);case TimeStep.SCALE.DAY:case TimeStep.SCALE.WEEKDAY:this.current.setHours(0);case TimeStep.SCALE.HOUR:this.current.setMinutes(0);case TimeStep.SCALE.MINUTE:this.current.setSeconds(0);case TimeStep.SCALE.SECOND:this.current.setMilliseconds(0)}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.setMilliseconds(this.current.getMilliseconds()-this.current.getMilliseconds()%this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()-this.current.getSeconds()%this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()-this.current.getMinutes()%this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()-this.current.getHours()%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()-1-(this.current.getDate()-1)%this.step+1);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()-this.current.getMonth()%this.step);
-break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()-this.current.getFullYear()%this.step)}},TimeStep.prototype.hasNext=function(){return this.current.valueOf()<=this._end.valueOf()},TimeStep.prototype.next=function(){var t=this.current.valueOf();if(this.current.getMonth()<6)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current=new Date(this.current.valueOf()+1e3*this.step);break;case TimeStep.SCALE.MINUTE:this.current=new Date(this.current.valueOf()+1e3*this.step*60);break;case TimeStep.SCALE.HOUR:this.current=new Date(this.current.valueOf()+1e3*this.step*60*60);var e=this.current.getHours();this.current.setHours(e-e%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step)}else switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()+this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()+this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()+this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step)}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.getMilliseconds()0&&(this.step=e),this.autoScale=!1},TimeStep.prototype.setAutoScale=function(t){this.autoScale=t},TimeStep.prototype.setMinimumStep=function(t){if(void 0!=t){var e=31104e6,i=2592e6,n=864e5,s=36e5,o=6e4,r=1e3,a=1;1e3*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1e3),500*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=500),100*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=100),50*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=50),10*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=10),5*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=5),e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1),3*i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=3),i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=1),5*n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=5),2*n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=2),n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=1),n/2>t&&(this.scale=TimeStep.SCALE.WEEKDAY,this.step=1),4*s>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=4),s>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=1),15*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=15),10*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=10),5*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=5),o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=1),15*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=15),10*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=10),5*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=5),r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=1),200*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=200),100*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=100),50*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=50),10*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=10),5*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=5),a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=1)}},TimeStep.prototype.snap=function(t){if(this.scale==TimeStep.SCALE.YEAR){var e=t.getFullYear()+Math.round(t.getMonth()/12);t.setFullYear(Math.round(e/this.step)*this.step),t.setMonth(0),t.setDate(0),t.setHours(0),t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MONTH)t.getDate()>15?(t.setDate(1),t.setMonth(t.getMonth()+1)):t.setDate(1),t.setHours(0),t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0);else if(this.scale==TimeStep.SCALE.DAY||this.scale==TimeStep.SCALE.WEEKDAY){switch(this.step){case 5:case 2:t.setHours(24*Math.round(t.getHours()/24));break;default:t.setHours(12*Math.round(t.getHours()/12))}t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.HOUR){switch(this.step){case 4:t.setMinutes(60*Math.round(t.getMinutes()/60));break;default:t.setMinutes(30*Math.round(t.getMinutes()/30))}t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MINUTE){switch(this.step){case 15:case 10:t.setMinutes(5*Math.round(t.getMinutes()/5)),t.setSeconds(0);break;case 5:t.setSeconds(60*Math.round(t.getSeconds()/60));break;default:t.setSeconds(30*Math.round(t.getSeconds()/30))}t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.SECOND)switch(this.step){case 15:case 10:t.setSeconds(5*Math.round(t.getSeconds()/5)),t.setMilliseconds(0);break;case 5:t.setMilliseconds(1e3*Math.round(t.getMilliseconds()/1e3));break;default:t.setMilliseconds(500*Math.round(t.getMilliseconds()/500))}else if(this.scale==TimeStep.SCALE.MILLISECOND){var i=this.step>5?this.step/2:1;t.setMilliseconds(Math.round(t.getMilliseconds()/i)*i)}},TimeStep.prototype.isMajor=function(){switch(this.scale){case TimeStep.SCALE.MILLISECOND:return 0==this.current.getMilliseconds();case TimeStep.SCALE.SECOND:return 0==this.current.getSeconds();case TimeStep.SCALE.MINUTE:return 0==this.current.getHours()&&0==this.current.getMinutes();case TimeStep.SCALE.HOUR:return 0==this.current.getHours();case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return 1==this.current.getDate();case TimeStep.SCALE.MONTH:return 0==this.current.getMonth();case TimeStep.SCALE.YEAR:return!1;default:return!1}},TimeStep.prototype.getLabelMinor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return L(t).format("SSS");case TimeStep.SCALE.SECOND:return L(t).format("s");case TimeStep.SCALE.MINUTE:return L(t).format("HH:mm");case TimeStep.SCALE.HOUR:return L(t).format("HH:mm");case TimeStep.SCALE.WEEKDAY:return L(t).format("ddd D");case TimeStep.SCALE.DAY:return L(t).format("D");case TimeStep.SCALE.MONTH:return L(t).format("MMM");case TimeStep.SCALE.YEAR:return L(t).format("YYYY");default:return""}},TimeStep.prototype.getLabelMajor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return L(t).format("HH:mm:ss");case TimeStep.SCALE.SECOND:return L(t).format("D MMMM HH:mm");case TimeStep.SCALE.MINUTE:case TimeStep.SCALE.HOUR:return L(t).format("ddd D MMMM");case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return L(t).format("MMMM YYYY");case TimeStep.SCALE.MONTH:return L(t).format("YYYY");case TimeStep.SCALE.YEAR:return"";default:return""}},a.prototype.setOptions=function(t){z.extend(this.options,t)},a.prototype.update=function(){this._order(),this._stack()},a.prototype._order=function(){var t=this.parent.items;if(!t)throw new Error("Cannot stack items: parent does not contain items");var e=[],i=0;z.forEach(t,function(t){t.visible&&(e[i]=t,i++)});var n=this.options.order||this.defaultOptions.order;if("function"!=typeof n)throw new Error("Option order must be a function");e.sort(n),this.ordered=e},a.prototype._stack=function(){var t,e,i,n=this.ordered,s=this.options,o=s.orientation||this.defaultOptions.orientation,r="top"==o;for(i=s.margin&&void 0!==s.margin.item?s.margin.item:this.defaultOptions.margin.item,t=0,e=n.length;e>t;t++){var a=n[t],h=null;do h=this.checkOverlap(n,t,0,t-1,i),null!=h&&(a.top=r?h.top+h.height+i:h.top-a.height-i);while(h)}},a.prototype.checkOverlap=function(t,e,i,n,s){for(var o=this.collision,r=t[e],a=n;a>=i;a--){var h=t[a];if(o(r,h,s)&&a!=e)return h}return null},a.prototype.collision=function(t,e,i){return t.left-ie.left&&t.top-ie.top},h.prototype.setOptions=function(t){z.extend(this.options,t),null!==this.start&&null!==this.end&&this.setRange(this.start,this.end)},h.prototype.subscribe=function(t,e,i){function n(e){s._onMouseWheel(e,t,i)}var s=this;if("move"==e)t.on("dragstart",function(e){s._onDragStart(e,t)}),t.on("drag",function(e){s._onDrag(e,t,i)}),t.on("dragend",function(e){s._onDragEnd(e,t)});else{if("zoom"!=e)throw new TypeError('Unknown event "'+e+'". Choose "move" or "zoom".');t.on("mousewheel",n),t.on("DOMMouseScroll",n),t.on("touch",function(){s._onTouch()}),t.on("pinch",function(e){s._onPinch(e,t,i)})}},h.prototype.on=function(t,e){var i=["rangechange","rangechanged"];if(-1==i.indexOf(t))throw new Error('Unknown event "'+t+'". Choose from '+i.join());F.addListener(this,t,e)},h.prototype.off=function(t,e){F.removeListener(this,t,e)},h.prototype._trigger=function(t){F.trigger(this,t,{start:this.start,end:this.end})},h.prototype.setRange=function(t,e){var i=this._applyRange(t,e);i&&(this._trigger("rangechange"),this._trigger("rangechanged"))},h.prototype._applyRange=function(t,e){var i,n=null!=t?z.convert(t,"Date").valueOf():this.start,s=null!=e?z.convert(e,"Date").valueOf():this.end,o=null!=this.options.max?z.convert(this.options.max,"Date").valueOf():null,r=null!=this.options.min?z.convert(this.options.min,"Date").valueOf():null;if(isNaN(n)||null===n)throw new Error('Invalid start "'+t+'"');if(isNaN(s)||null===s)throw new Error('Invalid end "'+e+'"');if(n>s&&(s=n),null!==r&&r>n&&(i=r-n,n+=i,s+=i,null!=o&&s>o&&(s=o)),null!==o&&s>o&&(i=s-o,n-=i,s-=i,null!=r&&r>n&&(n=r)),null!==this.options.zoomMin){var a=parseFloat(this.options.zoomMin);0>a&&(a=0),a>s-n&&(this.end-this.start===a?(n=this.start,s=this.end):(i=a-(s-n),n-=i/2,s+=i/2))}if(null!==this.options.zoomMax){var h=parseFloat(this.options.zoomMax);0>h&&(h=0),s-n>h&&(this.end-this.start===h?(n=this.start,s=this.end):(i=s-n-h,n+=i/2,s-=i/2))}var d=this.start!=n||this.end!=s;return this.start=n,this.end=s,d},h.prototype.getRange=function(){return{start:this.start,end:this.end}},h.prototype.conversion=function(t){return h.conversion(this.start,this.end,t)},h.conversion=function(t,e,i){return 0!=i&&e-t!=0?{offset:t,scale:i/(e-t)}:{offset:0,scale:1}};var Y={};h.prototype._onDragStart=function(t,e){if(!Y.pinching){Y.start=this.start,Y.end=this.end;var i=e.frame;i&&(i.style.cursor="move")}},h.prototype._onDrag=function(t,e,i){if(d(i),!Y.pinching){var n="horizontal"==i?t.gesture.deltaX:t.gesture.deltaY,s=Y.end-Y.start,o="horizontal"==i?e.width:e.height,r=-n/o*s;this._applyRange(Y.start+r,Y.end+r),this._trigger("rangechange")}},h.prototype._onDragEnd=function(t,e){Y.pinching||(e.frame&&(e.frame.style.cursor="auto"),this._trigger("rangechanged"))},h.prototype._onMouseWheel=function(t,e,i){d(i);var n=0;if(t.wheelDelta?n=t.wheelDelta/120:t.detail&&(n=-t.detail/3),n){var s;s=0>n?1-n/5:1/(1+n/5);var o=z.fakeGesture(this,t),r=c(o.touches[0],e.frame),a=this._pointerToDate(e,i,r);this.zoom(s,a)}z.preventDefault(t)},h.prototype._onTouch=function(){Y.start=this.start,Y.end=this.end,Y.pinching=!1,Y.center=null},h.prototype._onPinch=function(t,e,i){if(Y.pinching=!0,t.gesture.touches.length>1){Y.center||(Y.center=c(t.gesture.center,e.frame));var n=1/t.gesture.scale,s=this._pointerToDate(e,i,Y.center),o=c(t.gesture.center,e.frame),r=(this._pointerToDate(e,i,o),parseInt(s+(Y.start-s)*n)),a=parseInt(s+(Y.end-s)*n);this.setRange(r,a)}},h.prototype._pointerToDate=function(t,e,i){var n;if("horizontal"==e){var s=t.width;return n=this.conversion(s),i.x/n.scale+n.offset}var o=t.height;return n=this.conversion(o),i.y/n.scale+n.offset},h.prototype.zoom=function(t,e){null==e&&(e=(this.start+this.end)/2);var i=e+(this.start-e)*t,n=e+(this.end-e)*t;this.setRange(i,n)},h.prototype.move=function(t){var e=this.end-this.start,i=this.start+e*t,n=this.end+e*t;this.start=i,this.end=n},h.prototype.moveTo=function(t){var e=(this.start+this.end)/2,i=e-t,n=this.start-i,s=this.end-i;this.setRange(n,s)},l.prototype.add=function(t){if(void 0==t.id)throw new Error("Component has no field id");if(!(t instanceof u||t instanceof l))throw new TypeError("Component must be an instance of prototype Component or Controller");t.controller=this,this.components[t.id]=t},l.prototype.remove=function(t){var e;for(e in this.components)if(this.components.hasOwnProperty(e)&&(e==t||this.components[e]==t))break;e&&delete this.components[e]},l.prototype.requestReflow=function(t){if(t)this.reflow();else if(!this.reflowTimer){var e=this;this.reflowTimer=setTimeout(function(){e.reflowTimer=void 0,e.reflow()},0)}},l.prototype.requestRepaint=function(t){if(t)this.repaint();else if(!this.repaintTimer){var e=this;this.repaintTimer=setTimeout(function(){e.repaintTimer=void 0,e.repaint()},0)}},l.prototype.repaint=function V(){function V(i,n){n in e||(i.depends&&i.depends.forEach(function(t){V(t,t.id)}),i.parent&&V(i.parent,i.parent.id),t=i.repaint()||t,e[n]=!0)}var t=!1;this.repaintTimer&&(clearTimeout(this.repaintTimer),this.repaintTimer=void 0);var e={};z.forEach(this.components,V),t&&this.reflow()},l.prototype.reflow=function G(){function G(i,n){n in e||(i.depends&&i.depends.forEach(function(t){G(t,t.id)}),i.parent&&G(i.parent,i.parent.id),t=i.reflow()||t,e[n]=!0)}var t=!1;this.reflowTimer&&(clearTimeout(this.reflowTimer),this.reflowTimer=void 0);var e={};z.forEach(this.components,G),t&&this.repaint()},u.prototype.setOptions=function(t){t&&(z.extend(this.options,t),this.controller&&(this.requestRepaint(),this.requestReflow()))},u.prototype.getOption=function(t){var e;return this.options&&(e=this.options[t]),void 0===e&&this.defaultOptions&&(e=this.defaultOptions[t]),e},u.prototype.getContainer=function(){return null},u.prototype.getFrame=function(){return this.frame},u.prototype.repaint=function(){return!1},u.prototype.reflow=function(){return!1},u.prototype.hide=function(){return this.frame&&this.frame.parentNode?(this.frame.parentNode.removeChild(this.frame),!0):!1},u.prototype.show=function(){return this.frame&&this.frame.parentNode?!1:this.repaint()},u.prototype.requestRepaint=function(){if(!this.controller)throw new Error("Cannot request a repaint: no controller configured");this.controller.requestRepaint()},u.prototype.requestReflow=function(){if(!this.controller)throw new Error("Cannot request a reflow: no controller configured");this.controller.requestReflow()},p.prototype=new u,p.prototype.setOptions=u.prototype.setOptions,p.prototype.getContainer=function(){return this.frame},p.prototype.repaint=function(){var t=0,e=z.updateProperty,i=z.option.asSize,n=this.options,s=this.frame;if(!s){s=document.createElement("div"),s.className="panel";var o=n.className;o&&("function"==typeof o?z.addClassName(s,String(o())):z.addClassName(s,String(o))),this.frame=s,t+=1}if(!s.parentNode){if(!this.parent)throw new Error("Cannot repaint panel: no parent attached");var r=this.parent.getContainer();if(!r)throw new Error("Cannot repaint panel: parent has no container element");r.appendChild(s),t+=1}return t+=e(s.style,"top",i(n.top,"0px")),t+=e(s.style,"left",i(n.left,"0px")),t+=e(s.style,"width",i(n.width,"100%")),t+=e(s.style,"height",i(n.height,"100%")),t>0},p.prototype.reflow=function(){var t=0,e=z.updateProperty,i=this.frame;return i?(t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft),t+=e(this,"width",i.offsetWidth),t+=e(this,"height",i.offsetHeight)):t+=1,t>0},f.prototype=new p,f.prototype.setOptions=u.prototype.setOptions,f.prototype.repaint=function(){var t=0,e=z.updateProperty,i=z.option.asSize,n=this.options,s=this.frame;if(s||(s=document.createElement("div"),this.frame=s,t+=1),!s.parentNode){if(!this.container)throw new Error("Cannot repaint root panel: no container attached");this.container.appendChild(s),t+=1}s.className="vis timeline rootpanel "+n.orientation;var o=n.className;return o&&z.addClassName(s,z.option.asString(o)),t+=e(s.style,"top",i(n.top,"0px")),t+=e(s.style,"left",i(n.left,"0px")),t+=e(s.style,"width",i(n.width,"100%")),t+=e(s.style,"height",i(n.height,"100%")),this._updateEventEmitters(),this._updateWatch(),t>0},f.prototype.reflow=function(){var t=0,e=z.updateProperty,i=this.frame;return i?(t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft),t+=e(this,"width",i.offsetWidth),t+=e(this,"height",i.offsetHeight)):t+=1,t>0},f.prototype._updateWatch=function(){var t=this.getOption("autoResize");t?this._watch():this._unwatch()},f.prototype._watch=function(){var t=this;this._unwatch();var e=function(){var e=t.getOption("autoResize");return e?void(t.frame&&(t.frame.clientWidth!=t.width||t.frame.clientHeight!=t.height)&&t.requestReflow()):void t._unwatch()};z.addEventListener(window,"resize",e),this.watchTimer=setInterval(e,1e3)},f.prototype._unwatch=function(){this.watchTimer&&(clearInterval(this.watchTimer),this.watchTimer=void 0)},f.prototype.on=function(t,e){var i=this.listeners[t];i||(i=[],this.listeners[t]=i),i.push(e),this._updateEventEmitters()},f.prototype._updateEventEmitters=function(){if(this.listeners){var t=this;z.forEach(this.listeners,function(e,i){if(t.emitters||(t.emitters={}),!(i in t.emitters)){var n=t.frame;if(n){var s=function(t){e.forEach(function(e){e(t)})};t.emitters[i]=s,t.hammer||(t.hammer=N(n,{prevent_default:!0})),t.hammer.on(i,s)}}})}},g.prototype=new u,g.prototype.setOptions=u.prototype.setOptions,g.prototype.setRange=function(t){if(!(t instanceof h||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},g.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.scale+e.offset)},g.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.scale},g.prototype.repaint=function(){var t=0,e=z.updateProperty,i=z.option.asSize,n=this.options,s=this.getOption("orientation"),o=this.props,r=this.step,a=this.frame;if(a||(a=document.createElement("div"),this.frame=a,t+=1),a.className="axis",!a.parentNode){if(!this.parent)throw new Error("Cannot repaint time axis: no parent attached");var h=this.parent.getContainer();if(!h)throw new Error("Cannot repaint time axis: parent has no container element");h.appendChild(a),t+=1}var d=a.parentNode;if(d){var c=a.nextSibling;d.removeChild(a);var l="bottom"==s&&this.props.parentHeight&&this.height?this.props.parentHeight-this.height+"px":"0px";if(t+=e(a.style,"top",i(n.top,l)),t+=e(a.style,"left",i(n.left,"0px")),t+=e(a.style,"width",i(n.width,"100%")),t+=e(a.style,"height",i(n.height,this.height+"px")),this._repaintMeasureChars(),this.step){this._repaintStart(),r.first();for(var u=void 0,p=0;r.hasNext()&&1e3>p;){p++;var f=r.getCurrent(),g=this.toScreen(f),m=r.isMajor();this.getOption("showMinorLabels")&&this._repaintMinorText(g,r.getLabelMinor()),m&&this.getOption("showMajorLabels")?(g>0&&(void 0==u&&(u=g),this._repaintMajorText(g,r.getLabelMajor())),this._repaintMajorLine(g)):this._repaintMinorLine(g),r.next()}if(this.getOption("showMajorLabels")){var v=this.toTime(0),y=r.getLabelMajor(v),_=y.length*(o.majorCharWidth||10)+10;(void 0==u||u>_)&&this._repaintMajorText(0,y)}this._repaintEnd()}this._repaintLine(),c?d.insertBefore(a,c):d.appendChild(a)}return t>0},g.prototype._repaintStart=function(){var t=this.dom,e=t.redundant;e.majorLines=t.majorLines,e.majorTexts=t.majorTexts,e.minorLines=t.minorLines,e.minorTexts=t.minorTexts,t.majorLines=[],t.majorTexts=[],t.minorLines=[],t.minorTexts=[]},g.prototype._repaintEnd=function(){z.forEach(this.dom.redundant,function(t){for(;t.length;){var e=t.pop();e&&e.parentNode&&e.parentNode.removeChild(e)}})},g.prototype._repaintMinorText=function(t,e){var i=this.dom.redundant.minorTexts.shift();if(!i){var n=document.createTextNode("");i=document.createElement("div"),i.appendChild(n),i.className="text minor",this.frame.appendChild(i)}this.dom.minorTexts.push(i),i.childNodes[0].nodeValue=e,i.style.left=t+"px",i.style.top=this.props.minorLabelTop+"px"},g.prototype._repaintMajorText=function(t,e){var i=this.dom.redundant.majorTexts.shift();if(!i){var n=document.createTextNode(e);i=document.createElement("div"),i.className="text major",i.appendChild(n),this.frame.appendChild(i)}this.dom.majorTexts.push(i),i.childNodes[0].nodeValue=e,i.style.top=this.props.majorLabelTop+"px",i.style.left=t+"px"},g.prototype._repaintMinorLine=function(t){var e=this.dom.redundant.minorLines.shift();e||(e=document.createElement("div"),e.className="grid vertical minor",this.frame.appendChild(e)),this.dom.minorLines.push(e);var i=this.props;e.style.top=i.minorLineTop+"px",e.style.height=i.minorLineHeight+"px",e.style.left=t-i.minorLineWidth/2+"px"},g.prototype._repaintMajorLine=function(t){var e=this.dom.redundant.majorLines.shift();e||(e=document.createElement("DIV"),e.className="grid vertical major",this.frame.appendChild(e)),this.dom.majorLines.push(e);var i=this.props;e.style.top=i.majorLineTop+"px",e.style.left=t-i.majorLineWidth/2+"px",e.style.height=i.majorLineHeight+"px"},g.prototype._repaintLine=function(){{var t=this.dom.line,e=this.frame;this.options}this.getOption("showMinorLabels")||this.getOption("showMajorLabels")?(t?(e.removeChild(t),e.appendChild(t)):(t=document.createElement("div"),t.className="grid horizontal major",e.appendChild(t),this.dom.line=t),t.style.top=this.props.lineTop+"px"):t&&t.parentElement&&(e.removeChild(t.line),delete this.dom.line)},g.prototype._repaintMeasureChars=function(){var t,e=this.dom;if(!e.measureCharMinor){t=document.createTextNode("0");var i=document.createElement("DIV");i.className="text minor measure",i.appendChild(t),this.frame.appendChild(i),e.measureCharMinor=i}if(!e.measureCharMajor){t=document.createTextNode("0");var n=document.createElement("DIV");n.className="text major measure",n.appendChild(t),this.frame.appendChild(n),e.measureCharMajor=n}},g.prototype.reflow=function(){var t=0,e=z.updateProperty,i=this.frame,n=this.range;if(!n)throw new Error("Cannot repaint time axis: no range configured");if(i){t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft);var s=this.props,o=this.getOption("showMinorLabels"),r=this.getOption("showMajorLabels"),a=this.dom.measureCharMinor,h=this.dom.measureCharMajor;a&&(s.minorCharHeight=a.clientHeight,s.minorCharWidth=a.clientWidth),h&&(s.majorCharHeight=h.clientHeight,s.majorCharWidth=h.clientWidth);var d=i.parentNode?i.parentNode.offsetHeight:0;switch(d!=s.parentHeight&&(s.parentHeight=d,t+=1),this.getOption("orientation")){case"bottom":s.minorLabelHeight=o?s.minorCharHeight:0,s.majorLabelHeight=r?s.majorCharHeight:0,s.minorLabelTop=0,s.majorLabelTop=s.minorLabelTop+s.minorLabelHeight,s.minorLineTop=-this.top,s.minorLineHeight=Math.max(this.top+s.majorLabelHeight,0),s.minorLineWidth=1,s.majorLineTop=-this.top,s.majorLineHeight=Math.max(this.top+s.minorLabelHeight+s.majorLabelHeight,0),s.majorLineWidth=1,s.lineTop=0;break;case"top":s.minorLabelHeight=o?s.minorCharHeight:0,s.majorLabelHeight=r?s.majorCharHeight:0,s.majorLabelTop=0,s.minorLabelTop=s.majorLabelTop+s.majorLabelHeight,s.minorLineTop=s.minorLabelTop,s.minorLineHeight=Math.max(d-s.majorLabelHeight-this.top),s.minorLineWidth=1,s.majorLineTop=0,s.majorLineHeight=Math.max(d-this.top),s.majorLineWidth=1,s.lineTop=s.majorLabelHeight+s.minorLabelHeight;break;default:throw new Error('Unkown orientation "'+this.getOption("orientation")+'"')}var c=s.minorLabelHeight+s.majorLabelHeight;t+=e(this,"width",i.offsetWidth),t+=e(this,"height",c),this._updateConversion();var l=z.convert(n.start,"Number"),u=z.convert(n.end,"Number"),p=this.toTime(5*(s.minorCharWidth||10)).valueOf()-this.toTime(0).valueOf();this.step=new TimeStep(new Date(l),new Date(u),p),t+=e(s.range,"start",l),t+=e(s.range,"end",u),t+=e(s.range,"minimumStep",p.valueOf())}return t>0},g.prototype._updateConversion=function(){var t=this.range;if(!t)throw new Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):h.conversion(t.start,t.end,this.width)},m.prototype=new u,m.prototype.setOptions=u.prototype.setOptions,m.prototype.getContainer=function(){return this.frame},m.prototype.repaint=function(){var t=this.frame,e=this.parent,i=e.parent.getContainer();if(!e)throw new Error("Cannot repaint bar: no parent attached");if(!i)throw new Error("Cannot repaint bar: parent has no container element");if(!this.getOption("showCurrentTime"))return void(t&&(i.removeChild(t),delete this.frame));t||(t=document.createElement("div"),t.className="currenttime",t.style.position="absolute",t.style.top="0px",t.style.height="100%",i.appendChild(t),this.frame=t),e.conversion||e._updateConversion();var n=new Date,s=e.toScreen(n);t.style.left=s+"px",t.title="Current time: "+n,void 0!==this.currentTimeTimer&&(clearTimeout(this.currentTimeTimer),delete this.currentTimeTimer);var o=this,r=1/e.conversion.scale/2;return 30>r&&(r=30),this.currentTimeTimer=setTimeout(function(){o.repaint()},r),!1},v.prototype=new u,v.prototype.setOptions=u.prototype.setOptions,v.prototype.getContainer=function(){return this.frame},v.prototype.repaint=function(){var t=this.frame,e=this.parent,i=e.parent.getContainer();if(!e)throw new Error("Cannot repaint bar: no parent attached");if(!i)throw new Error("Cannot repaint bar: parent has no container element");if(!this.getOption("showCustomTime"))return void(t&&(i.removeChild(t),delete this.frame));if(!t){t=document.createElement("div"),t.className="customtime",t.style.position="absolute",t.style.top="0px",t.style.height="100%",i.appendChild(t);var n=document.createElement("div");n.style.position="relative",n.style.top="0px",n.style.left="-10px",n.style.height="100%",n.style.width="20px",t.appendChild(n),this.frame=t,this.subscribe(this,"movetime")}e.conversion||e._updateConversion();var s=e.toScreen(this.customTime);return t.style.left=s+"px",t.title="Time: "+this.customTime,!1},v.prototype._setCustomTime=function(t){this.customTime=new Date(t.valueOf()),this.repaint()},v.prototype._getCustomTime=function(){return new Date(this.customTime.valueOf())},v.prototype.subscribe=function(t,e){var i=this,n={component:t,event:e,callback:function(t){i._onMouseDown(t,n)},params:{}};t.on("mousedown",n.callback),i.listeners.push(n)},v.prototype.on=function(t,e){var i=this.frame;if(!i)throw new Error("Cannot add event listener: no parent attached");F.addListener(this,t,e),z.addEventListener(i,t,e)},v.prototype._onMouseDown=function(t,e){t=t||window.event;var i=e.params,n=t.which?1==t.which:1==t.button;if(n){i.mouseX=z.getPageX(t),i.moved=!1,i.customTime=this.customTime;var s=this;i.onMouseMove||(i.onMouseMove=function(t){s._onMouseMove(t,e)},z.addEventListener(document,"mousemove",i.onMouseMove)),i.onMouseUp||(i.onMouseUp=function(t){s._onMouseUp(t,e)},z.addEventListener(document,"mouseup",i.onMouseUp)),z.stopPropagation(t),z.preventDefault(t)}},v.prototype._onMouseMove=function(t,e){t=t||window.event;var i=e.params,n=this.parent,s=z.getPageX(t);void 0===i.mouseX&&(i.mouseX=s);var o=s-i.mouseX;Math.abs(o)>=1&&(i.moved=!0);var r=n.toScreen(i.customTime),a=r+o,h=n.toTime(a);this._setCustomTime(h),F.trigger(this,"timechange",{customTime:this.customTime}),z.preventDefault(t)},v.prototype._onMouseUp=function(t,e){t=t||window.event;var i=e.params;i.onMouseMove&&(z.removeEventListener(document,"mousemove",i.onMouseMove),i.onMouseMove=null),i.onMouseUp&&(z.removeEventListener(document,"mouseup",i.onMouseUp),i.onMouseUp=null),i.moved&&F.trigger(this,"timechanged",{customTime:this.customTime})},y.prototype=new p,y.types={box:w,range:S,rangeoverflow:E,point:b},y.prototype.setOptions=u.prototype.setOptions,y.prototype.setRange=function(t){if(!(t instanceof h||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},y.prototype.setSelection=function(t){var e,i,n,s,o;if(t){if(!Array.isArray(t))throw new TypeError("Array expected");for(e=0,i=this.selection.length;i>e;e++)n=this.selection[e],s=this.items[n],s&&s.unselect();for(this.selection=[],e=0,i=t.length;i>e;e++)n=t[e],s=this.items[n],s&&(this.selection.push(n),s.select());o=this.selection.concat([]),F.trigger(this,"select",{ids:o}),this.controller&&this.requestRepaint()}},y.prototype.getSelection=function(){return this.selection.concat([])},y.prototype._deselect=function(t){for(var e=this.selection,i=0,n=e.length;n>i;i++)if(e[i]==t){e.splice(i,1);break}},y.prototype.repaint=function(){var t=0,e=z.updateProperty,i=z.option.asSize,n=this.options,s=this.getOption("orientation"),o=this.defaultOptions,r=this.frame;if(!r){r=document.createElement("div"),r.className="itemset";var a=n.className;a&&z.addClassName(r,z.option.asString(a));var h=document.createElement("div");h.className="background",r.appendChild(h),this.dom.background=h;var d=document.createElement("div");d.className="foreground",r.appendChild(d),this.dom.foreground=d;var c=document.createElement("div");c.className="itemset-axis",this.dom.axis=c,this.frame=r,t+=1}if(!this.parent)throw new Error("Cannot repaint itemset: no parent attached");var l=this.parent.getContainer();if(!l)throw new Error("Cannot repaint itemset: parent has no container element");r.parentNode||(l.appendChild(r),t+=1),this.dom.axis.parentNode||(l.appendChild(this.dom.axis),t+=1),t+=e(r.style,"left",i(n.left,"0px")),t+=e(r.style,"top",i(n.top,"0px")),t+=e(r.style,"width",i(n.width,"100%")),t+=e(r.style,"height",i(n.height,this.height+"px")),t+=e(this.dom.axis.style,"left",i(n.left,"0px")),t+=e(this.dom.axis.style,"width",i(n.width,"100%")),t+="bottom"==s?e(this.dom.axis.style,"top",this.height+this.top+"px"):e(this.dom.axis.style,"top",this.top+"px"),this._updateConversion();var u=this,p=this.queue,f=this.itemsData,g=this.items,m={};for(var v in p)if(p.hasOwnProperty(v)){var _=p[v],w=g[v],b=_.action;switch(b){case"add":case"update":var S=f&&f.get(v,m);if(S){var E=S.type||S.start&&S.end&&"range"||n.type||"box",T=y.types[E];if(w&&(T&&w instanceof T?(w.data=S,t++):(t+=w.hide(),w=null)),!w){if(!T)throw new TypeError('Unknown item type "'+E+'"');w=new T(u,S,n,o),w.id=_.id,t++}w.repaint(),g[v]=w}delete p[v];break;case"remove":w&&(w.selected&&u._deselect(v),t+=w.hide()),delete g[v],delete p[v];break;default:console.log('Error: unknown action "'+b+'"')}}return z.forEach(this.items,function(e){e.visible?(t+=e.show(),e.reposition()):t+=e.hide()}),t>0},y.prototype.getForeground=function(){return this.dom.foreground},y.prototype.getBackground=function(){return this.dom.background},y.prototype.getAxis=function(){return this.dom.axis},y.prototype.reflow=function(){var t=0,e=this.options,i=e.margin&&e.margin.axis||this.defaultOptions.margin.axis,n=e.margin&&e.margin.item||this.defaultOptions.margin.item,s=z.updateProperty,o=z.option.asNumber,r=z.option.asSize,a=this.frame;if(a){this._updateConversion(),z.forEach(this.items,function(e){t+=e.reflow()}),this.stack.update();var h,d=o(e.maxHeight),c=null!=r(e.height);if(c)h=a.offsetHeight;else{var l=this.stack.ordered;if(l.length){var u=l[0].top,p=l[0].top+l[0].height;z.forEach(l,function(t){u=Math.min(u,t.top),p=Math.max(p,t.top+t.height)}),h=p-u+i+n}else h=i+n}null!=d&&(h=Math.min(h,d)),t+=s(this,"height",h),t+=s(this,"top",a.offsetTop),t+=s(this,"left",a.offsetLeft),t+=s(this,"width",a.offsetWidth)}else t+=1;return t>0},y.prototype.hide=function(){var t=!1;return this.frame&&this.frame.parentNode&&(this.frame.parentNode.removeChild(this.frame),t=!0),this.dom.axis&&this.dom.axis.parentNode&&(this.dom.axis.parentNode.removeChild(this.dom.axis),t=!0),t
-},y.prototype.setItems=function(t){var e,i=this,n=this.itemsData;if(t){if(!(t instanceof o||t instanceof r))throw new TypeError("Data must be an instance of DataSet");this.itemsData=t}else this.itemsData=null;if(n&&(z.forEach(this.listeners,function(t,e){n.unsubscribe(e,t)}),e=n.getIds(),this._onRemove(e)),this.itemsData){var s=this.id;z.forEach(this.listeners,function(t,e){i.itemsData.subscribe(e,t,s)}),e=this.itemsData.getIds(),this._onAdd(e)}},y.prototype.getItems=function(){return this.itemsData},y.prototype._onUpdate=function(t){this._toQueue("update",t)},y.prototype._onAdd=function(t){this._toQueue("add",t)},y.prototype._onRemove=function(t){this._toQueue("remove",t)},y.prototype._toQueue=function(t,e){var i=this.queue;e.forEach(function(e){i[e]={id:e,action:t}}),this.controller&&this.requestRepaint()},y.prototype._updateConversion=function(){var t=this.range;if(!t)throw new Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):h.conversion(t.start,t.end,this.width)},y.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.scale+e.offset)},y.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.scale},_.prototype.select=function(){this.selected=!0,this.visible&&this.repaint()},_.prototype.unselect=function(){this.selected=!1,this.visible&&this.repaint()},_.prototype.show=function(){return!1},_.prototype.hide=function(){return!1},_.prototype.repaint=function(){return!1},_.prototype.reflow=function(){return!1},_.prototype.getWidth=function(){return this.width},w.prototype=new _(null,null),w.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw new Error("Cannot repaint item: no parent attached");if(!e.box.parentNode){var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");i.appendChild(e.box),t=!0}if(!e.line.parentNode){var n=this.parent.getBackground();if(!n)throw new Error("Cannot repaint time axis: parent has no background container element");n.appendChild(e.line),t=!0}if(!e.dot.parentNode){var s=this.parent.getAxis();if(!n)throw new Error("Cannot repaint time axis: parent has no axis container element");s.appendChild(e.dot),t=!0}if(this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var o=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=o&&(this.className=o,e.box.className="item box"+o,e.line.className="item line"+o,e.dot.className="item dot"+o,t=!0)}return t},w.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},w.prototype.hide=function(){var t=!1,e=this.dom;return e&&(e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),e.line.parentNode&&e.line.parentNode.removeChild(e.line),e.dot.parentNode&&e.dot.parentNode.removeChild(e.dot)),t},w.prototype.reflow=function(){var t,e,i,n,s,o,r,a,h,d,c,l,u=0;if(void 0==this.data.start)throw new Error('Property "start" missing in item '+this.data.id);if(c=this.data,l=this.parent&&this.parent.range,c&&l){var p=l.end-l.start;this.visible=c.start>l.start-p&&c.start0},w.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.box=document.createElement("DIV"),t.content=document.createElement("DIV"),t.content.className="content",t.box.appendChild(t.content),t.line=document.createElement("DIV"),t.line.className="line",t.dot=document.createElement("DIV"),t.dot.className="dot",t.box["timeline-item"]=this)},w.prototype.reposition=function(){var t=this.dom,e=this.props,i=this.options.orientation||this.defaultOptions.orientation;if(t){var n=t.box,s=t.line,o=t.dot;n.style.left=this.left+"px",n.style.top=this.top+"px",s.style.left=e.line.left+"px","top"==i?(s.style.top="0px",s.style.height=this.top+"px"):(s.style.top=this.top+this.height+"px",s.style.height=Math.max(this.parent.height-this.top-this.height+this.props.dot.height/2,0)+"px"),o.style.left=e.dot.left+"px",o.style.top=e.dot.top+"px"}},b.prototype=new _(null,null),b.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw new Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");if(e.point.parentNode||(i.appendChild(e.point),i.appendChild(e.point),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=n&&(this.className=n,e.point.className="item point"+n,t=!0)}return t},b.prototype.show=function(){return this.dom&&this.dom.point.parentNode?!1:this.repaint()},b.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.point.parentNode&&(e.point.parentNode.removeChild(e.point),t=!0),t},b.prototype.reflow=function(){var t,e,i,n,s,o,r,a,h,d,c=0;if(void 0==this.data.start)throw new Error('Property "start" missing in item '+this.data.id);if(h=this.data,d=this.parent&&this.parent.range,h&&d){var l=d.end-d.start;this.visible=h.start>d.start-l&&h.start0},b.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.point=document.createElement("div"),t.content=document.createElement("div"),t.content.className="content",t.point.appendChild(t.content),t.dot=document.createElement("div"),t.dot.className="dot",t.point.appendChild(t.dot),t.point["timeline-item"]=this)},b.prototype.reposition=function(){var t=this.dom,e=this.props;t&&(t.point.style.top=this.top+"px",t.point.style.left=this.left+"px",t.content.style.marginLeft=e.content.marginLeft+"px",t.dot.style.top=e.dot.top+"px")},S.prototype=new _(null,null),S.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw new Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=n&&(this.className=n,e.box.className="item range"+n,t=!0)}return t},S.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},S.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),t},S.prototype.reflow=function(){var t,e,i,n,s,o,r,a,h,d,c,l,u,p,f,g,m=0;if(void 0==this.data.start)throw new Error('Property "start" missing in item '+this.data.id);if(void 0==this.data.end)throw new Error('Property "end" missing in item '+this.data.id);return h=this.data,d=this.parent&&this.parent.range,this.visible=h&&d?h.startd.start:!1,this.visible&&(t=this.dom,t?(e=this.props,i=this.options,o=this.parent,r=o.toScreen(this.data.start),a=o.toScreen(this.data.end),c=z.updateProperty,l=t.box,u=o.width,f=i.orientation||this.defaultOptions.orientation,n=i.margin&&i.margin.axis||this.defaultOptions.margin.axis,s=i.padding||this.defaultOptions.padding,m+=c(e.content,"width",t.content.offsetWidth),m+=c(this,"height",l.offsetHeight),-u>r&&(r=-u),a>2*u&&(a=2*u),p=0>r?Math.min(-r,a-r-e.content.width-2*s):0,m+=c(e.content,"left",p),"top"==f?(g=n,m+=c(this,"top",g)):(g=o.height-this.height-n,m+=c(this,"top",g)),m+=c(this,"left",r),m+=c(this,"width",Math.max(a-r,1))):m+=1),m>0},S.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.box=document.createElement("div"),t.content=document.createElement("div"),t.content.className="content",t.box.appendChild(t.content),t.box["timeline-item"]=this)},S.prototype.reposition=function(){var t=this.dom,e=this.props;t&&(t.box.style.top=this.top+"px",t.box.style.left=this.left+"px",t.box.style.width=this.width+"px",t.content.style.left=e.content.left+"px")},E.prototype=new S(null,null),E.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw new Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw new Error("Cannot repaint time axis: parent has no foreground container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=this.data.className?" "+this.data.className:"";this.className!=n&&(this.className=n,e.box.className="item rangeoverflow"+n,t=!0)}return t},E.prototype.getWidth=function(){return void 0!==this.props.content&&this.width0},x.prototype=new p,x.prototype.setOptions=u.prototype.setOptions,x.prototype.setRange=function(){},x.prototype.setItems=function(t){this.itemsData=t;for(var e in this.groups)if(this.groups.hasOwnProperty(e)){var i=this.groups[e];i.setItems(t)}},x.prototype.getItems=function(){return this.itemsData},x.prototype.setRange=function(t){this.range=t},x.prototype.setGroups=function(t){var e,i=this;if(this.groupsData&&(z.forEach(this.listeners,function(t,e){i.groupsData.unsubscribe(e,t)}),e=this.groupsData.getIds(),this._onRemove(e)),t?t instanceof o?this.groupsData=t:(this.groupsData=new o({convert:{start:"Date",end:"Date"}}),this.groupsData.add(t)):this.groupsData=null,this.groupsData){var n=this.id;z.forEach(this.listeners,function(t,e){i.groupsData.subscribe(e,t,n)}),e=this.groupsData.getIds(),this._onAdd(e)}},x.prototype.getGroups=function(){return this.groupsData},x.prototype.setSelection=function(t){var e=[],i=this.groups;for(var n in i)if(i.hasOwnProperty(n)){var s=i[n];s.setSelection(t)}return e},x.prototype.getSelection=function(){var t=[],e=this.groups;for(var i in e)if(e.hasOwnProperty(i)){var n=e[i];t=t.concat(n.getSelection())}return t},x.prototype.repaint=function(){var t,e,i,n,s=0,o=z.updateProperty,r=z.option.asSize,a=z.option.asElement,h=this.options,d=this.dom.frame,c=this.dom.labels,l=this.dom.labelSet;if(!this.parent)throw new Error("Cannot repaint groupset: no parent attached");var u=this.parent.getContainer();if(!u)throw new Error("Cannot repaint groupset: parent has no container element");if(!d){d=document.createElement("div"),d.className="groupset",this.dom.frame=d;var p=h.className;p&&z.addClassName(d,z.option.asString(p)),s+=1}d.parentNode||(u.appendChild(d),s+=1);var f=a(h.labelContainer);if(!f)throw new Error('Cannot repaint groupset: option "labelContainer" not defined');c||(c=document.createElement("div"),c.className="labels",this.dom.labels=c),l||(l=document.createElement("div"),l.className="label-set",c.appendChild(l),this.dom.labelSet=l),c.parentNode&&c.parentNode==f||(c.parentNode&&c.parentNode.removeChild(c.parentNode),f.appendChild(c)),s+=o(d.style,"height",r(h.height,this.height+"px")),s+=o(d.style,"top",r(h.top,"0px")),s+=o(d.style,"left",r(h.left,"0px")),s+=o(d.style,"width",r(h.width,"100%")),s+=o(l.style,"top",r(h.top,"0px")),s+=o(l.style,"height",r(h.height,this.height+"px"));var g=this,m=this.queue,v=this.groups,y=this.groupsData,_=Object.keys(m);if(_.length){_.forEach(function(t){var e=m[t],i=v[t];switch(e){case"add":case"update":if(!i){var n=Object.create(g.options);z.extend(n,{height:null,maxHeight:null}),i=new T(g,t,n),i.setItems(g.itemsData),v[t]=i,g.controller.add(i)}i.data=y.get(t),delete m[t];break;case"remove":i&&(i.setItems(),delete v[t],g.controller.remove(i)),delete m[t];break;default:console.log('Error: unknown action "'+e+'"')}});var w=this.groupsData.getIds({order:this.options.groupOrder});for(t=0;t0},x.prototype._createLabel=function(t){var e=this.groups[t],i=document.createElement("div");i.className="label";var n=document.createElement("div");n.className="inner",i.appendChild(n);var s=e.data&&e.data.content;s instanceof Element?n.appendChild(s):void 0!=s&&(n.innerHTML=s);var o=e.data&&e.data.className;return o&&z.addClassName(i,o),e.label=i,i},x.prototype.getContainer=function(){return this.dom.frame},x.prototype.getLabelsWidth=function(){return this.props.labels.width},x.prototype.reflow=function(){var t,e,i=0,n=this.options,s=z.updateProperty,o=z.option.asNumber,r=z.option.asSize,a=this.dom.frame;if(a){var h,d=o(n.maxHeight),c=null!=r(n.height);if(c)h=a.offsetHeight;else{h=0;for(t in this.groups)this.groups.hasOwnProperty(t)&&(e=this.groups[t],h+=e.height)}null!=d&&(h=Math.min(h,d)),i+=s(this,"height",h),i+=s(this,"top",a.offsetTop),i+=s(this,"left",a.offsetLeft),i+=s(this,"width",a.offsetWidth)}var l=0;for(t in this.groups)if(this.groups.hasOwnProperty(t)){e=this.groups[t];var u=e.props&&e.props.label&&e.props.label.width||0;l=Math.max(l,u)}return i+=s(this.props.labels,"width",l),i>0},x.prototype.hide=function(){return this.dom.frame&&this.dom.frame.parentNode?(this.dom.frame.parentNode.removeChild(this.dom.frame),!0):!1},x.prototype.show=function(){return this.dom.frame&&this.dom.frame.parentNode?!1:this.repaint()},x.prototype._onUpdate=function(t){this._toQueue(t,"update")},x.prototype._onAdd=function(t){this._toQueue(t,"add")},x.prototype._onRemove=function(t){this._toQueue(t,"remove")},x.prototype._toQueue=function(t,e){var i=this.queue;t.forEach(function(t){i[t]=e}),this.controller&&this.requestRepaint()},C.prototype.setOptions=function(t){z.extend(this.options,t),this.range.setRange(t.start,t.end),this.controller.reflow(),this.controller.repaint()},C.prototype.setCustomTime=function(t){this.customtime._setCustomTime(t)},C.prototype.getCustomTime=function(){return new Date(this.customtime.customTime.valueOf())},C.prototype.setItems=function(t){var e,i=null==this.itemsData;if(t?t instanceof o&&(e=t):e=null,t instanceof o||(e=new o({convert:{start:"Date",end:"Date"}}),e.add(t)),this.itemsData=e,this.content.setItems(e),i&&(void 0==this.options.start||void 0==this.options.end)){var n=this.getItemRange(),s=n.min,r=n.max;if(null!=s&&null!=r){var a=r.valueOf()-s.valueOf();0>=a&&(a=864e5),s=new Date(s.valueOf()-.05*a),r=new Date(r.valueOf()+.05*a)}void 0!=this.options.start&&(s=z.convert(this.options.start,"Date")),void 0!=this.options.end&&(r=z.convert(this.options.end,"Date")),(null!=s||null!=r)&&this.range.setRange(s,r)}},C.prototype.setGroups=function(t){var e=this;this.groupsData=t;var i=this.groupsData?x:y;if(!(this.content instanceof i)){this.content&&(this.content.hide(),this.content.setItems&&this.content.setItems(),this.content.setGroups&&this.content.setGroups(),this.controller.remove(this.content));var n=Object.create(this.options);z.extend(n,{top:function(){return"top"==e.options.orientation?e.timeaxis.height:e.itemPanel.height-e.timeaxis.height-e.content.height},left:null,width:"100%",height:function(){return e.options.height?e.itemPanel.height-e.timeaxis.height:null},maxHeight:function(){if(e.options.maxHeight){if(!z.isNumber(e.options.maxHeight))throw new TypeError("Number expected for property maxHeight");return e.options.maxHeight-e.timeaxis.height}return null},labelContainer:function(){return e.labelPanel.getContainer()}}),this.content=new i(this.itemPanel,[this.timeaxis],n),this.content.setRange&&this.content.setRange(this.range),this.content.setItems&&this.content.setItems(this.itemsData),this.content.setGroups&&this.content.setGroups(this.groupsData),this.controller.add(this.content)}},C.prototype.getItemRange=function(){var t=this.itemsData,e=null,i=null;if(t){var n=t.min("start");e=n?n.start.valueOf():null;var s=t.max("start");s&&(i=s.start.valueOf());var o=t.max("end");o&&(i=null==i?o.end.valueOf():Math.max(i,o.end.valueOf()))}return{min:null!=e?new Date(e):null,max:null!=i?new Date(i):null}},C.prototype.setSelection=function(t){this.content&&this.content.setSelection(t)},C.prototype.getSelection=function(){return this.content?this.content.getSelection():[]},C.prototype.on=function(t,e){var i=["rangechange","rangechanged","select"];if(-1==i.indexOf(t))throw new Error('Unknown event "'+t+'". Choose from '+i.join());F.addListener(this,t,e)},C.prototype.off=function(t,e){F.removeListener(this,t,e)},C.prototype._trigger=function(t,e){F.trigger(this,t,e||{})},C.prototype._onSelectItem=function(t){var e=this._itemFromTarget(t),i=e?[e.id]:[];this.setSelection(i),this._trigger("select",{items:this.getSelection()}),t.stopPropagation()},C.prototype._onMultiSelectItem=function(t){var e,i=this._itemFromTarget(t);if(i){e=this.getSelection();var n=e.indexOf(i.id);-1==n?e.push(i.id):e.splice(n,1),this.setSelection(e),this._trigger("select",{items:this.getSelection()}),t.stopPropagation()}},C.prototype._itemFromTarget=function(t){for(var e=t.target;e;){if(e.hasOwnProperty("timeline-item"))return e["timeline-item"];e=e.parentNode}return null},function(t){function e(t){return C=t,u()}function i(){M=0,D=C.charAt(0)}function n(){M++,D=C.charAt(M)}function s(){return C.charAt(M+1)}function o(t){return N.test(t)}function r(t,e){if(t||(t={}),e)for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}function a(t,e,i){for(var n=e.split("."),s=t;n.length;){var o=n.shift();n.length?(s[o]||(s[o]={}),s=s[o]):s[o]=i}}function h(t,e){for(var i,n,s=null,o=[t],a=t;a.parent;)o.push(a.parent),a=a.parent;if(a.nodes)for(i=0,n=a.nodes.length;n>i;i++)if(e.id===a.nodes[i].id){s=a.nodes[i];break}for(s||(s={id:e.id},t.node&&(s.attr=r(s.attr,t.node))),i=o.length-1;i>=0;i--){var h=o[i];h.nodes||(h.nodes=[]),-1==h.nodes.indexOf(s)&&h.nodes.push(s)}e.attr&&(s.attr=r(s.attr,e.attr))}function d(t,e){if(t.edges||(t.edges=[]),t.edges.push(e),t.edge){var i=r({},t.edge);e.attr=r(i,e.attr)}}function c(t,e,i,n,s){var o={from:e,to:i,type:n};return t.edge&&(o.attr=r({},t.edge)),o.attr=r(o.attr||{},s),o}function l(){for(O=T.NULL,I="";" "==D||" "==D||"\n"==D||"\r"==D;)n();do{var t=!1;if("#"==D){for(var e=M-1;" "==C.charAt(e)||" "==C.charAt(e);)e--;if("\n"==C.charAt(e)||""==C.charAt(e)){for(;""!=D&&"\n"!=D;)n();t=!0}}if("/"==D&&"/"==s()){for(;""!=D&&"\n"!=D;)n();t=!0}if("/"==D&&"*"==s()){for(;""!=D;){if("*"==D&&"/"==s()){n(),n();break}n()}t=!0}for(;" "==D||" "==D||"\n"==D||"\r"==D;)n()}while(t);if(""==D)return void(O=T.DELIMITER);var i=D+s();if(x[i])return O=T.DELIMITER,I=i,n(),void n();if(x[D])return O=T.DELIMITER,I=D,void n();if(o(D)||"-"==D){for(I+=D,n();o(D);)I+=D,n();return"false"==I?I=!1:"true"==I?I=!0:isNaN(Number(I))||(I=Number(I)),void(O=T.IDENTIFIER)}if('"'==D){for(n();""!=D&&('"'!=D||'"'==D&&'"'==s());)I+=D,'"'==D&&n(),n();if('"'!=D)throw w('End of string " expected');return n(),void(O=T.IDENTIFIER)}for(O=T.UNKNOWN;""!=D;)I+=D,n();throw new SyntaxError('Syntax error in part "'+b(I,30)+'"')}function u(){var t={};if(i(),l(),"strict"==I&&(t.strict=!0,l()),("graph"==I||"digraph"==I)&&(t.type=I,l()),O==T.IDENTIFIER&&(t.id=I,l()),"{"!=I)throw w("Angle bracket { expected");if(l(),p(t),"}"!=I)throw w("Angle bracket } expected");if(l(),""!==I)throw w("End of file expected");return l(),delete t.node,delete t.edge,delete t.graph,t}function p(t){for(;""!==I&&"}"!=I;)f(t),";"==I&&l()}function f(t){var e=g(t);if(e)return void y(t,e);var i=m(t);if(!i){if(O!=T.IDENTIFIER)throw w("Identifier expected");var n=I;if(l(),"="==I){if(l(),O!=T.IDENTIFIER)throw w("Identifier expected");t[n]=I,l()}else v(t,n)}}function g(t){var e=null;if("subgraph"==I&&(e={},e.type="subgraph",l(),O==T.IDENTIFIER&&(e.id=I,l())),"{"==I){if(l(),e||(e={}),e.parent=t,e.node=t.node,e.edge=t.edge,e.graph=t.graph,p(e),"}"!=I)throw w("Angle bracket } expected");l(),delete e.node,delete e.edge,delete e.graph,delete e.parent,t.subgraphs||(t.subgraphs=[]),t.subgraphs.push(e)}return e}function m(t){return"node"==I?(l(),t.node=_(),"node"):"edge"==I?(l(),t.edge=_(),"edge"):"graph"==I?(l(),t.graph=_(),"graph"):null}function v(t,e){var i={id:e},n=_();n&&(i.attr=n),h(t,i),y(t,e)}function y(t,e){for(;"->"==I||"--"==I;){var i,n=I;l();var s=g(t);if(s)i=s;else{if(O!=T.IDENTIFIER)throw w("Identifier or subgraph expected");i=I,h(t,{id:i}),l()}var o=_(),r=c(t,e,i,n,o);d(t,r),e=i}}function _(){for(var t=null;"["==I;){for(l(),t={};""!==I&&"]"!=I;){if(O!=T.IDENTIFIER)throw w("Attribute name expected");var e=I;if(l(),"="!=I)throw w("Equal sign = expected");if(l(),O!=T.IDENTIFIER)throw w("Attribute value expected");var i=I;a(t,e,i),l(),","==I&&l()}if("]"!=I)throw w("Bracket ] expected");l()}return t}function w(t){return new SyntaxError(t+', got "'+b(I,30)+'" (char '+M+")")}function b(t,e){return t.length<=e?t:t.substr(0,27)+"..."}function S(t,e,i){t instanceof Array?t.forEach(function(t){e instanceof Array?e.forEach(function(e){i(t,e)}):i(t,e)}):e instanceof Array?e.forEach(function(e){i(t,e)}):i(t,e)}function E(t){function i(t){var e={from:t.from,to:t.to};return r(e,t.attr),e.style="->"==t.type?"arrow":"line",e}var n=e(t),s={nodes:[],edges:[],options:{}};return n.nodes&&n.nodes.forEach(function(t){var e={id:t.id,label:String(t.label||t.id)};r(e,t.attr),e.image&&(e.shape="image"),s.nodes.push(e)}),n.edges&&n.edges.forEach(function(t){var e,n;e=t.from instanceof Object?t.from.nodes:{id:t.from},n=t.to instanceof Object?t.to.nodes:{id:t.to},t.from instanceof Object&&t.from.edges&&t.from.edges.forEach(function(t){var e=i(t);s.edges.push(e)}),S(e,n,function(e,n){var o=c(s,e.id,n.id,t.type,t.attr),r=i(o);s.edges.push(r)}),t.to instanceof Object&&t.to.edges&&t.to.edges.forEach(function(t){var e=i(t);s.edges.push(e)})}),n.attr&&(s.options=n.attr),s}var T={NULL:0,DELIMITER:1,IDENTIFIER:2,UNKNOWN:3},x={"{":!0,"}":!0,"[":!0,"]":!0,";":!0,"=":!0,",":!0,"->":!0,"--":!0},C="",M=0,D="",I="",O=T.NULL,N=/[a-zA-Z_0-9.:#]/;t.parseDOT=e,t.DOTToGraph=E}("undefined"!=typeof z?z:n),"undefined"!=typeof CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.circle=function(t,e,i){this.beginPath(),this.arc(t,e,i,0,2*Math.PI,!1)},CanvasRenderingContext2D.prototype.square=function(t,e,i){this.beginPath(),this.rect(t-i,e-i,2*i,2*i)},CanvasRenderingContext2D.prototype.triangle=function(t,e,i){this.beginPath();var n=2*i,s=n/2,o=Math.sqrt(3)/6*n,r=Math.sqrt(n*n-s*s);this.moveTo(t,e-(r-o)),this.lineTo(t+s,e+o),this.lineTo(t-s,e+o),this.lineTo(t,e-(r-o)),this.closePath()},CanvasRenderingContext2D.prototype.triangleDown=function(t,e,i){this.beginPath();var n=2*i,s=n/2,o=Math.sqrt(3)/6*n,r=Math.sqrt(n*n-s*s);this.moveTo(t,e+(r-o)),this.lineTo(t+s,e-o),this.lineTo(t-s,e-o),this.lineTo(t,e+(r-o)),this.closePath()},CanvasRenderingContext2D.prototype.star=function(t,e,i){this.beginPath();for(var n=0;10>n;n++){var s=n%2===0?1.3*i:.5*i;this.lineTo(t+s*Math.sin(2*n*Math.PI/10),e-s*Math.cos(2*n*Math.PI/10))}this.closePath()},CanvasRenderingContext2D.prototype.roundRect=function(t,e,i,n,s){var o=Math.PI/180;0>i-2*s&&(s=i/2),0>n-2*s&&(s=n/2),this.beginPath(),this.moveTo(t+s,e),this.lineTo(t+i-s,e),this.arc(t+i-s,e+s,s,270*o,360*o,!1),this.lineTo(t+i,e+n-s),this.arc(t+i-s,e+n-s,s,0,90*o,!1),this.lineTo(t+s,e+n),this.arc(t+s,e+n-s,s,90*o,180*o,!1),this.lineTo(t,e+s),this.arc(t+s,e+s,s,180*o,270*o,!1)},CanvasRenderingContext2D.prototype.ellipse=function(t,e,i,n){var s=.5522848,o=i/2*s,r=n/2*s,a=t+i,h=e+n,d=t+i/2,c=e+n/2;this.beginPath(),this.moveTo(t,c),this.bezierCurveTo(t,c-r,d-o,e,d,e),this.bezierCurveTo(d+o,e,a,c-r,a,c),this.bezierCurveTo(a,c+r,d+o,h,d,h),this.bezierCurveTo(d-o,h,t,c+r,t,c)},CanvasRenderingContext2D.prototype.database=function(t,e,i,n){var s=1/3,o=i,r=n*s,a=.5522848,h=o/2*a,d=r/2*a,c=t+o,l=e+r,u=t+o/2,p=e+r/2,f=e+(n-r/2),g=e+n;this.beginPath(),this.moveTo(c,p),this.bezierCurveTo(c,p+d,u+h,l,u,l),this.bezierCurveTo(u-h,l,t,p+d,t,p),this.bezierCurveTo(t,p-d,u-h,e,u,e),this.bezierCurveTo(u+h,e,c,p-d,c,p),this.lineTo(c,f),this.bezierCurveTo(c,f+d,u+h,g,u,g),this.bezierCurveTo(u-h,g,t,f+d,t,f),this.lineTo(t,p)},CanvasRenderingContext2D.prototype.arrow=function(t,e,i,n){var s=t-n*Math.cos(i),o=e-n*Math.sin(i),r=t-.9*n*Math.cos(i),a=e-.9*n*Math.sin(i),h=s+n/3*Math.cos(i+.5*Math.PI),d=o+n/3*Math.sin(i+.5*Math.PI),c=s+n/3*Math.cos(i-.5*Math.PI),l=o+n/3*Math.sin(i-.5*Math.PI);this.beginPath(),this.moveTo(t,e),this.lineTo(h,d),this.lineTo(r,a),this.lineTo(c,l),this.closePath()},CanvasRenderingContext2D.prototype.dashedLine=function(t,e,i,n,s){s||(s=[10,5]),0==u&&(u=.001);var o=s.length;this.moveTo(t,e);for(var r=i-t,a=n-e,h=a/r,d=Math.sqrt(r*r+a*a),c=0,l=!0;d>=.1;){var u=s[c++%o];u>d&&(u=d);var p=Math.sqrt(u*u/(1+h*h));0>r&&(p=-p),t+=p,e+=h*p,this[l?"lineTo":"moveTo"](t,e),d-=u,l=!l}}),M.prototype.resetCluster=function(){this.formationScale=void 0,this.clusterSize=1,this.containedNodes={},this.containedEdges={},this.clusterSessions=[]},M.prototype.attachEdge=function(t){-1==this.edges.indexOf(t)&&this.edges.push(t),-1==this.dynamicEdges.indexOf(t)&&this.dynamicEdges.push(t),this.dynamicEdgesLength=this.dynamicEdges.length,this._updateMass()},M.prototype.detachEdge=function(t){var e=this.edges.indexOf(t);-1!=e&&(this.edges.splice(e,1),this.dynamicEdges.splice(e,1)),this.dynamicEdgesLength=this.dynamicEdges.length,this._updateMass()},M.prototype._updateMass=function(){this.mass=1+.6*this.edges.length},M.prototype.setProperties=function(t,e){if(t){if(this.originalLabel=void 0,void 0!==t.id&&(this.id=t.id),void 0!==t.label&&(this.label=t.label,this.originalLabel=t.label),void 0!==t.title&&(this.title=t.title),void 0!==t.group&&(this.group=t.group),void 0!==t.x&&(this.x=t.x),void 0!==t.y&&(this.y=t.y),void 0!==t.value&&(this.value=t.value),void 0!==t.horizontalAlignLeft&&(this.horizontalAlignLeft=t.horizontalAlignLeft),void 0!==t.verticalAlignTop&&(this.verticalAlignTop=t.verticalAlignTop),void 0!==t.triggerFunction&&(this.triggerFunction=t.triggerFunction),void 0===this.id)throw"Node must have an id";if(this.group){var i=this.grouplist.get(this.group);for(var n in i)i.hasOwnProperty(n)&&(this[n]=i[n])}if(void 0!==t.shape&&(this.shape=t.shape),void 0!==t.image&&(this.image=t.image),void 0!==t.radius&&(this.radius=t.radius),void 0!==t.color&&(this.color=M.parseColor(t.color)),void 0!==t.fontColor&&(this.fontColor=t.fontColor),void 0!==t.fontSize&&(this.fontSize=t.fontSize),void 0!==t.fontFace&&(this.fontFace=t.fontFace),void 0!==this.image){if(!this.imagelist)throw"No imagelist provided";this.imageObj=this.imagelist.load(this.image)}switch(this.xFixed=this.xFixed||void 0!==t.x,this.yFixed=this.yFixed||void 0!==t.y,this.radiusFixed=this.radiusFixed||void 0!==t.radius,"image"==this.shape&&(this.radiusMin=e.nodes.widthMin,this.radiusMax=e.nodes.widthMax),this.shape){case"database":this.draw=this._drawDatabase,this.resize=this._resizeDatabase;break;case"box":this.draw=this._drawBox,this.resize=this._resizeBox;break;case"circle":this.draw=this._drawCircle,this.resize=this._resizeCircle;break;case"ellipse":this.draw=this._drawEllipse,this.resize=this._resizeEllipse;break;case"image":this.draw=this._drawImage,this.resize=this._resizeImage;break;case"text":this.draw=this._drawText,this.resize=this._resizeText;break;case"dot":this.draw=this._drawDot,this.resize=this._resizeShape;break;case"square":this.draw=this._drawSquare,this.resize=this._resizeShape;break;case"triangle":this.draw=this._drawTriangle,this.resize=this._resizeShape;break;case"triangleDown":this.draw=this._drawTriangleDown,this.resize=this._resizeShape;break;case"star":this.draw=this._drawStar,this.resize=this._resizeShape;break;default:this.draw=this._drawEllipse,this.resize=this._resizeEllipse}this._reset()}},M.parseColor=function(t){var e;return z.isString(t)?e={border:t,background:t,highlight:{border:t,background:t}}:(e={},e.background=t.background||"white",e.border=t.border||e.background,z.isString(t.highlight)?e.highlight={border:t.highlight,background:t.highlight}:(e.highlight={},e.highlight.background=t.highlight&&t.highlight.background||e.background,e.highlight.border=t.highlight&&t.highlight.border||e.border)),e},M.prototype.select=function(){this.selected=!0,this._reset()},M.prototype.unselect=function(){this.selected=!1,this._reset()},M.prototype.clearSizeCache=function(){this._reset()},M.prototype._reset=function(){this.width=void 0,this.height=void 0},M.prototype.getTitle=function(){return this.title},M.prototype.distanceToBorder=function(t,e){var i=1;switch(this.width||this.resize(t),this.shape){case"circle":case"dot":return this.radius+i;case"ellipse":var n=this.width/2,s=this.height/2,o=Math.sin(e)*n,r=Math.cos(e)*s;
-return n*s/Math.sqrt(o*o+r*r);case"box":case"image":case"text":default:return this.width?Math.min(Math.abs(this.width/2/Math.cos(e)),Math.abs(this.height/2/Math.sin(e)))+i:0}},M.prototype._setForce=function(t,e){this.fx=t,this.fy=e},M.prototype._addForce=function(t,e){this.fx+=t,this.fy+=e},M.prototype.discreteStep=function(t){if(!this.xFixed){var e=-this.damping*this.vx,i=(this.fx+e)/this.mass;this.vx+=i*t,this.x+=this.vx*t}if(!this.yFixed){var n=-this.damping*this.vy,s=(this.fy+n)/this.mass;this.vy+=s*t,this.y+=this.vy*t}},M.prototype.isFixed=function(){return this.xFixed&&this.yFixed},M.prototype.isMoving=function(t){return Math.abs(this.vx)>t||Math.abs(this.vy)>t?!0:(this.vx=0,this.vy=0,!1)},M.prototype.isSelected=function(){return this.selected},M.prototype.getValue=function(){return this.value},M.prototype.getDistance=function(t,e){var i=this.x-t,n=this.y-e;return Math.sqrt(i*i+n*n)},M.prototype.setValueRange=function(t,e){if(!this.radiusFixed&&void 0!==this.value)if(e==t)this.radius=(this.radiusMin+this.radiusMax)/2;else{var i=(this.radiusMax-this.radiusMin)/(e-t);this.radius=(this.value-t)*i+this.radiusMin}this.baseRadiusValue=this.radius},M.prototype.draw=function(){throw"Draw method not initialized for node"},M.prototype.resize=function(){throw"Resize method not initialized for node"},M.prototype.isOverlappingWith=function(t){return this.leftt.left&&this.topt.top},M.prototype._resizeImage=function(){if(!this.width||!this.height){var t,e;if(this.value){this.radius=this.baseRadiusValue;var i=this.imageObj.height/this.imageObj.width;void 0!==i?(t=this.radius||this.imageObj.width,e=this.radius*i||this.imageObj.height):(t=0,e=0)}else t=this.imageObj.width,e=this.imageObj.height;this.width=t,this.height=e,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)}},M.prototype._drawImage=function(t){this._resizeImage(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e;if(0!=this.imageObj.width){if(this.clusterSize>1){var i=this.clusterSize>1?10:0;i*=this.graphScaleInv,i=Math.min(.2*this.width,i),t.globalAlpha=.5,t.drawImage(this.imageObj,this.left-i,this.top-i,this.width+2*i,this.height+2*i)}t.globalAlpha=1,t.drawImage(this.imageObj,this.left,this.top,this.width,this.height),e=this.y+this.height/2}else e=this.y;this._label(t,this.label,this.x,e,void 0,"top")},M.prototype._resizeBox=function(t){if(!this.width){var e=5,i=this.getTextSize(t);this.width=i.width+2*e,this.height=i.height+2*e,this.width+=.5*(this.clusterSize-1)*this.clusterSizeWidthFactor,this.height+=.5*(this.clusterSize-1)*this.clusterSizeHeightFactor}},M.prototype._drawBox=function(t){this._resizeBox(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e=2.5,i=2;t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.roundRect(this.left-2*t.lineWidth,this.top-2*t.lineWidth,this.width+4*t.lineWidth,this.height+4*t.lineWidth,this.radius),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.roundRect(this.left,this.top,this.width,this.height,this.radius),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},M.prototype._resizeDatabase=function(t){if(!this.width){var e=5,i=this.getTextSize(t),n=i.width+2*e;this.width=n,this.height=n,this.width+=(this.clusterSize-1)*this.clusterSizeWidthFactor,this.height+=(this.clusterSize-1)*this.clusterSizeHeightFactor,this.radius+=(this.clusterSize-1)*this.clusterSizeRadiusFactor}},M.prototype._drawDatabase=function(t){this._resizeDatabase(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e=2.5,i=2;t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.database(this.x-this.width/2-2*t.lineWidth,this.y-.5*this.height-2*t.lineWidth,this.width+4*t.lineWidth,this.height+4*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.database(this.x-this.width/2,this.y-.5*this.height,this.width,this.height),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},M.prototype._resizeCircle=function(t){if(!this.width){var e=5,i=this.getTextSize(t),n=Math.max(i.width,i.height)+2*e;this.radius=n/2,this.width=n,this.height=n,this.radius+=.5*(this.clusterSize-1)*this.clusterSizeRadiusFactor}},M.prototype._drawCircle=function(t){this._resizeCircle(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e=2.5,i=2;t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.circle(this.x,this.y,this.radius+2*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.circle(this.x,this.y,this.radius),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},M.prototype._resizeEllipse=function(t){if(!this.width){var e=this.getTextSize(t);this.width=1.5*e.width,this.height=2*e.height,this.width1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.ellipse(this.left-2*t.lineWidth,this.top-2*t.lineWidth,this.width+4*t.lineWidth,this.height+4*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.ellipse(this.left,this.top,this.width,this.height),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},M.prototype._drawDot=function(t){this._drawShape(t,"circle")},M.prototype._drawTriangle=function(t){this._drawShape(t,"triangle")},M.prototype._drawTriangleDown=function(t){this._drawShape(t,"triangleDown")},M.prototype._drawSquare=function(t){this._drawShape(t,"square")},M.prototype._drawStar=function(t){this._drawShape(t,"star")},M.prototype._resizeShape=function(){if(!this.width){this.radius=this.baseRadiusValue;var t=2*this.radius;this.width=t,this.height=t,this.width+=(this.clusterSize-1)*this.clusterSizeWidthFactor,this.height+=(this.clusterSize-1)*this.clusterSizeHeightFactor,this.radius+=.5*(this.clusterSize-1)*this.clusterSizeRadiusFactor}},M.prototype._drawShape=function(t,e){this._resizeShape(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var i=2.5,n=2,s=2;switch(e){case"dot":s=2;break;case"square":s=2;break;case"triangle":s=3;break;case"triangleDown":s=3;break;case"star":s=4}t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?n:1)+(this.clusterSize>1?i:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t[e](this.x,this.y,this.radius+s*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?n:1)+(this.clusterSize>1?i:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t[e](this.x,this.y,this.radius),t.fill(),t.stroke(),this.label&&this._label(t,this.label,this.x,this.y+this.height/2,void 0,"top")},M.prototype._resizeText=function(t){if(!this.width){var e=5,i=this.getTextSize(t);this.width=i.width+2*e,this.height=i.height+2*e,this.width+=(this.clusterSize-1)*this.clusterSizeWidthFactor,this.height+=(this.clusterSize-1)*this.clusterSizeHeightFactor,this.radius+=(this.clusterSize-1)*this.clusterSizeRadiusFactor}},M.prototype._drawText=function(t){this._resizeText(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,this._label(t,this.label,this.x,this.y)},M.prototype._label=function(t,e,i,n,s,o){if(e){t.font=(this.selected?"bold ":"")+this.fontSize+"px "+this.fontFace,t.fillStyle=this.fontColor||"black",t.textAlign=s||"center",t.textBaseline=o||"middle";for(var r=e.split("\n"),a=r.length,h=this.fontSize+4,d=n+(1-a)/2*h,c=0;a>c;c++)t.fillText(r[c],i,d),d+=h}},M.prototype.getTextSize=function(t){if(void 0!==this.label){t.font=(this.selected?"bold ":"")+this.fontSize+"px "+this.fontFace;for(var e=this.label.split("\n"),i=(this.fontSize+4)*e.length,n=0,s=0,o=e.length;o>s;s++)n=Math.max(n,t.measureText(e[s]).width);return{width:n,height:i}}return{width:0,height:0}},M.prototype.inArea=function(){return void 0!==this.width?this.x+this.width*this.graphScaleInv>=this.canvasTopLeft.x&&this.x-this.width*this.graphScaleInv=this.canvasTopLeft.y&&this.y-this.height*this.graphScaleInv=this.canvasTopLeft.x&&this.x=this.canvasTopLeft.y&&this.yh},D.prototype._drawLine=function(t){t.strokeStyle=this.color,t.lineWidth=this._getLineWidth();var e;if(this.from!=this.to)this._line(t),this.label&&(e=this._pointOnLine(.5),this._label(t,this.label,e.x,e.y));else{var i,n,s=this.length/4,o=this.from;o.width||o.resize(t),o.width>o.height?(i=o.x+o.width/2,n=o.y-s):(i=o.x+s,n=o.y-o.height/2),this._circle(t,i,n,s),e=this._pointOnCircle(i,n,s,.5),this._label(t,this.label,e.x,e.y)}},D.prototype._getLineWidth=function(){return this.from.selected||this.to.selected?Math.min(2*this.width,this.widthMax)*this.graphScaleInv:this.width*this.graphScaleInv},D.prototype._line=function(t){t.beginPath(),t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y),t.stroke()},D.prototype._circle=function(t,e,i,n){t.beginPath(),t.arc(e,i,n,0,2*Math.PI,!1),t.stroke()},D.prototype._label=function(t,e,i,n){if(e){t.font=(this.from.selected||this.to.selected?"bold ":"")+this.fontSize+"px "+this.fontFace,t.fillStyle="white";var s=t.measureText(e).width,o=this.fontSize,r=i-s/2,a=n-o/2;t.fillRect(r,a,s,o),t.fillStyle=this.fontColor||"black",t.textAlign="left",t.textBaseline="top",t.fillText(e,r,a)}},D.prototype._drawDashLine=function(t){if(t.strokeStyle=this.color,t.lineWidth=this._getLineWidth(),t.beginPath(),t.lineCap="round",void 0!==this.dash.altLength?t.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]):void 0!==this.dash.length&&void 0!==this.dash.gap?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dash.length,this.dash.gap]):(t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y)),t.stroke(),this.label){var e=this._pointOnLine(.5);this._label(t,this.label,e.x,e.y)}},D.prototype._pointOnLine=function(t){return{x:(1-t)*this.from.x+t*this.to.x,y:(1-t)*this.from.y+t*this.to.y}},D.prototype._pointOnCircle=function(t,e,i,n){var s=2*(n-3/8)*Math.PI;return{x:t+i*Math.cos(s),y:e-i*Math.sin(s)}},D.prototype._drawArrowCenter=function(t){var e;if(t.strokeStyle=this.color,t.fillStyle=this.color,t.lineWidth=this._getLineWidth(),this.from!=this.to){this._line(t);var i=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x),n=10+5*this.width;e=this._pointOnLine(.5),t.arrow(e.x,e.y,i,n),t.fill(),t.stroke(),this.label&&(e=this._pointOnLine(.5),this._label(t,this.label,e.x,e.y))}else{var s,o,r=this.length/4,a=this.from;a.width||a.resize(t),a.width>a.height?(s=a.x+a.width/2,o=a.y-r):(s=a.x+r,o=a.y-a.height/2),this._circle(t,s,o,r);var i=.2*Math.PI,n=10+5*this.width;e=this._pointOnCircle(s,o,r,.5),t.arrow(e.x,e.y,i,n),t.fill(),t.stroke(),this.label&&(e=this._pointOnCircle(s,o,r,.5),this._label(t,this.label,e.x,e.y))}},D.prototype._drawArrow=function(t){t.strokeStyle=this.color,t.fillStyle=this.color,t.lineWidth=this._getLineWidth();var e,i;if(this.from!=this.to){e=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x);var n=this.to.x-this.from.x,s=this.to.y-this.from.y,o=Math.sqrt(n*n+s*s),r=this.from.distanceToBorder(t,e+Math.PI),a=(o-r)/o,h=a*this.from.x+(1-a)*this.to.x,d=a*this.from.y+(1-a)*this.to.y,c=this.to.distanceToBorder(t,e),l=(o-c)/o,u=(1-l)*this.from.x+l*this.to.x,p=(1-l)*this.from.y+l*this.to.y;if(t.beginPath(),t.moveTo(h,d),t.lineTo(u,p),t.stroke(),i=10+5*this.width,t.arrow(u,p,e,i),t.fill(),t.stroke(),this.label){var f=this._pointOnLine(.5);this._label(t,this.label,f.x,f.y)}}else{var g,m,v,y=this.from,_=this.length/4;y.width||y.resize(t),y.width>y.height?(g=y.x+y.width/2,m=y.y-_,v={x:g,y:y.y,angle:.9*Math.PI}):(g=y.x+_,m=y.y-y.height/2,v={x:y.x,y:m,angle:.6*Math.PI}),t.beginPath(),t.arc(g,m,_,0,2*Math.PI,!1),t.stroke(),i=10+5*this.width,t.arrow(v.x,v.y,v.angle,i),t.fill(),t.stroke(),this.label&&(f=this._pointOnCircle(g,m,_,.5),this._label(t,this.label,f.x,f.y))}},D._dist=function(t,e,i,n,s,o){var r=i-t,a=n-e,h=r*r+a*a,d=((s-t)*r+(o-e)*a)/h;d>1?d=1:0>d&&(d=0);var c=t+d*r,l=e+d*a,u=c-s,p=l-o;return Math.sqrt(u*u+p*p)},D.prototype.setScale=function(t){this.graphScaleInv=1/t},I.prototype.setPosition=function(t,e){this.x=parseInt(t),this.y=parseInt(e)},I.prototype.setText=function(t){this.frame.innerHTML=t},I.prototype.show=function(t){if(void 0===t&&(t=!0),t){var e=this.frame.clientHeight,i=this.frame.clientWidth,n=this.frame.parentNode.clientHeight,s=this.frame.parentNode.clientWidth,o=this.y-e;o+e+this.padding>n&&(o=n-e-this.padding),os&&(r=s-i-this.padding),r1)return this.activeSector[this.activeSector.length-2];throw new TypeError("there are not enough sectors in the this.activeSector array.")},_setActiveSector:function(t){this.activeSector.push(t)},_forgetLastSector:function(){this.activeSector.pop()},_createNewSector:function(t){this.sectors.active[t]={nodes:{},edges:{},nodeIndices:[],formationScale:this.scale,drawingNode:void 0},this.sectors.active[t].drawingNode=new M({id:t,color:{background:"#eaefef",border:"495c5e"}},{},{},this.constants),this.sectors.active[t].drawingNode.clusterSize=2},_deleteActiveSector:function(t){delete this.sectors.active[t]},_deleteFrozenSector:function(t){delete this.sectors.frozen[t]},_freezeSector:function(t){this.sectors.frozen[t]=this.sectors.active[t],this._deleteActiveSector(t)},_activateSector:function(t){this.sectors.active[t]=this.sectors.frozen[t],this._deleteFrozenSector(t)},_mergeThisWithFrozen:function(t){for(var e in this.nodes)this.nodes.hasOwnProperty(e)&&(this.sectors.frozen[t].nodes[e]=this.nodes[e]);for(var i in this.edges)this.edges.hasOwnProperty(i)&&(this.sectors.frozen[t].edges[i]=this.edges[i]);for(var n=0;n1?this[t](n[0],n[1]):this[t](e)}this._loadLatestSector()},_doInAllFrozenSectors:function(t,e){if(void 0===e)for(var i in this.sectors.frozen)this.sectors.frozen.hasOwnProperty(i)&&(this._switchToFrozenSector(i),this[t]());else for(var i in this.sectors.frozen)if(this.sectors.frozen.hasOwnProperty(i)){this._switchToFrozenSector(i);var n=Array.prototype.splice.call(arguments,1);n.length>1?this[t](n[0],n[1]):this[t](e)}this._loadLatestSector()},_doInNavigationSector:function(t,e){if(this._switchToNavigationSector(),void 0===e)this[t]();else{var i=Array.prototype.splice.call(arguments,1);i.length>1?this[t](i[0],i[1]):this[t](e)}this._loadLatestSector()},_doInAllSectors:function(t,e){var i=Array.prototype.splice.call(arguments,1);void 0===e?(this._doInAllActiveSectors(t),this._doInAllFrozenSectors(t)):i.length>1?(this._doInAllActiveSectors(t,i[0],i[1]),this._doInAllFrozenSectors(t,i[0],i[1])):(this._doInAllActiveSectors(t,e),this._doInAllFrozenSectors(t,e))},_clearNodeIndexList:function(){var t=this._sector();this.sectors.active[t].nodeIndices=[],this.nodeIndices=this.sectors.active[t].nodeIndices},_drawSectorNodes:function(t,e){var i,n=1e9,s=-1e9,o=1e9,r=-1e9;for(var a in this.sectors[e])if(this.sectors[e].hasOwnProperty(a)&&void 0!==this.sectors[e][a].drawingNode){this._switchToSector(a,e),n=1e9,s=-1e9,o=1e9,r=-1e9;for(var h in this.nodes)this.nodes.hasOwnProperty(h)&&(i=this.nodes[h],i.resize(t),o>i.x-.5*i.width&&(o=i.x-.5*i.width),ri.y-.5*i.height&&(n=i.y-.5*i.height),st&&n>s;)s%3==0?this.forceAggregateHubs():this.increaseClusterLevel(),i=this.nodeIndices.length,s+=1;s>1&&1==e&&this.repositionNodes()},openCluster:function(t){var e=this.moving;if(t.clusterSize>this.constants.clustering.sectorThreshold&&this._nodeInActiveArea(t)&&("default"!=this._sector()||1!=this.nodeIndices.length)){this._addSector(t);for(var i=0;this.nodeIndices.lengthi;)this.decreaseClusterLevel(),i+=1}else this._expandClusterNode(t,!1,!0),this._updateNodeIndexList(),this._updateDynamicEdges(),this.updateLabels();this.moving!=e&&this.start()},updateClustersDefault:function(){1==this.constants.clustering.enabled&&this.updateClusters(0,!1,!1)},increaseClusterLevel:function(){this.updateClusters(-1,!1,!0)},decreaseClusterLevel:function(){this.updateClusters(1,!1,!0)},updateClusters:function(t,e,i){var n=this.moving,s=this.nodeIndices.length;this.previousScale>this.scale&&0==t&&this._collapseSector(),this.previousScale>this.scale||-1==t?this._formClusters(i):(this.previousScalethis.scale||-1==t)&&(this._aggregateHubs(i),this._updateNodeIndexList()),(this.previousScale>this.scale||-1==t)&&(this.handleChains(),this._updateNodeIndexList()),this.previousScale=this.scale,this._updateDynamicEdges(),this.updateLabels(),this.nodeIndices.lengththis.constants.clustering.chainThreshold&&this._reduceAmountOfChains(1-this.constants.clustering.chainThreshold/t)},_aggregateHubs:function(t){this._getHubSize(),this._formClustersByHub(t,!1)},forceAggregateHubs:function(){var t=this.moving,e=this.nodeIndices.length;this._aggregateHubs(!0),this._updateNodeIndexList(),this._updateDynamicEdges(),this.updateLabels(),this.nodeIndices.length!=e&&(this.clusterSession+=1),this.moving!=t&&this.start()},_openClustersBySize:function(){for(var t in this.nodes)if(this.nodes.hasOwnProperty(t)){var e=this.nodes[t];1==e.inView()&&(e.width*this.scale>this.constants.clustering.screenSizeThreshold*this.frame.canvas.clientWidth||e.height*this.scale>this.constants.clustering.screenSizeThreshold*this.frame.canvas.clientHeight)&&this.openCluster(e)}},_openClusters:function(t,e){for(var i=0;i1&&(t.clusterSizei)){var r=o.from,a=o.to;o.to.mass>o.from.mass&&(r=o.to,a=o.from),1==a.dynamicEdgesLength?this._addToCluster(r,a,!1):1==r.dynamicEdgesLength&&this._addToCluster(a,r,!1)}}},_forceClustersByZoom:function(){for(var t in this.nodes)if(this.nodes.hasOwnProperty(t)){var e=this.nodes[t];if(1==e.dynamicEdgesLength&&0!=e.dynamicEdges.length){var i=e.dynamicEdges[0],n=i.toId==e.id?this.nodes[i.fromId]:this.nodes[i.toId];e.id!=n.id&&(n.mass>e.mass?this._addToCluster(n,e,!0):this._addToCluster(e,n,!0))}}},_formClustersByHub:function(t,e){for(var i in this.nodes)this.nodes.hasOwnProperty(i)&&this._formClusterFromHub(this.nodes[i],t,e)},_formClusterFromHub:function(t,e,i,n){if(void 0===n&&(n=0),t.dynamicEdgesLength>=this.hubThreshold&&0==i||t.dynamicEdgesLength==this.hubThreshold&&1==i){for(var s,o,r,a=this.constants.clustering.clusterEdgeThreshold/this.scale,h=!1,d=[],c=t.dynamicEdges.length,l=0;c>l;l++)d.push(t.dynamicEdges[l].id);if(0==e)for(h=!1,l=0;c>l;l++){var u=this.edges[d[l]];if(void 0!==u&&u.connected&&u.toId!=u.fromId&&(s=u.to.x-u.from.x,o=u.to.y-u.from.y,r=Math.sqrt(s*s+o*o),a>r)){h=!0;break}}if(!e&&h||e)for(l=0;c>l;l++)if(u=this.edges[d[l]],void 0!==u){var p=this.nodes[u.fromId==t.id?u.toId:u.fromId];p.dynamicEdges.length<=this.hubThreshold+n&&p.id!=t.id&&this._addToCluster(t,p,e)}}},_addToCluster:function(t,e,i){t.containedNodes[e.id]=e;for(var n=0;n1)for(var n=0;n1&&(e.label="[".concat(String(e.clusterSize),"]"))}for(t in this.nodes)this.nodes.hasOwnProperty(t)&&(e=this.nodes[t],1==e.clusterSize&&(e.label=void 0!==e.originalLabel?e.originalLabel:String(e.id)))},_nodeInActiveArea:function(t){return Math.abs(t.x-this.areaCenter.x)<=this.constants.clustering.activeAreaBoxSize/this.scale&&Math.abs(t.y-this.areaCenter.y)<=this.constants.clustering.activeAreaBoxSize/this.scale},repositionNodes:function(){for(var t=0;tn&&(n=o.dynamicEdgesLength),t+=o.dynamicEdgesLength,e+=Math.pow(o.dynamicEdgesLength,2),i+=1}t/=i,e/=i;var r=e-Math.pow(t,2),a=Math.sqrt(r);this.hubThreshold=Math.floor(t+2*a),this.hubThreshold>n&&(this.hubThreshold=n)},_reduceAmountOfChains:function(t){this.hubThreshold=2;var e=Math.floor(this.nodeIndices.length*t);for(var i in this.nodes)this.nodes.hasOwnProperty(i)&&2==this.nodes[i].dynamicEdgesLength&&this.nodes[i].dynamicEdges.length>=2&&e>0&&(this._formClusterFromHub(this.nodes[i],!0,!0,1),e-=1)},_getChainFraction:function(){var t=0,e=0;for(var i in this.nodes)this.nodes.hasOwnProperty(i)&&(2==this.nodes[i].dynamicEdgesLength&&this.nodes[i].dynamicEdges.length>=2&&(t+=1),e+=1);return t/e}},W={_getNodesOverlappingWith:function(t,e){var i=this.nodes;for(var n in i)i.hasOwnProperty(n)&&i[n].isOverlappingWith(t)&&e.push(n)},_getAllNodesOverlappingWith:function(t){var e=[];return this._doInAllActiveSectors("_getNodesOverlappingWith",t,e),e},_getAllNavigationNodesOverlappingWith:function(t){var e=[];return this._doInNavigationSector("_getNodesOverlappingWith",t,e),e},_pointerToPositionObject:function(t){var e=this._canvasToX(t.x),i=this._canvasToY(t.y);return{left:e,top:i,right:e,bottom:i}},_pointerToScreenPositionObject:function(t){var e=t.x,i=t.y;return{left:e,top:i,right:e,bottom:i}},_getNavigationNodeAt:function(t){var e=this._pointerToScreenPositionObject(t),i=this._getAllNavigationNodesOverlappingWith(e);return i.length>0?this.sectors.navigation.nodes[i[i.length-1]]:null},_getNodeAt:function(t){var e=this._pointerToPositionObject(t);return overlappingNodes=this._getAllNodesOverlappingWith(e),overlappingNodes.length>0?this.nodes[overlappingNodes[overlappingNodes.length-1]]:null},_getEdgeAt:function(){return null},_addToSelection:function(t){this.selection.push(t.id),this.selectionObj[t.id]=t},_removeFromSelection:function(t){for(var e=0;ee;e++){n=t[e];var s=this.nodes[n];if(!s)throw new RangeError('Node with id "'+n+'" not found');this._selectNode(s,!0,!0)}this.redraw()},_updateSelection:function(){for(var t=0;tt.x-t.width&&(n=t.x-t.width),st.y-t.height&&(e=t.y-t.height),i=this.constants.clustering.initialMaxNodes)var n=38.8467/(e-14.50184)+.0116;else var n=42.54117319/(e+39.31966387)+.1944405;else{var s=1.1*(Math.abs(i.minX)+Math.abs(i.maxX)),o=1.1*(Math.abs(i.minY)+Math.abs(i.maxY)),r=this.frame.canvas.clientWidth/s,a=this.frame.canvas.clientHeight/o;n=a>=r?r:a}n>1&&(n=1),this.pinch.mousewheelScale=n,this._setScale(n),this._centerGraph(i),this.start()},O.prototype._updateNodeIndexList=function(){this._clearNodeIndexList();for(var t in this.nodes)this.nodes.hasOwnProperty(t)&&this.nodeIndices.push(t)},O.prototype.setData=function(t,e){if(void 0===e&&(e=!1),t&&t.dot&&(t.nodes||t.edges))throw new SyntaxError('Data must contain either parameter "dot" or parameter pair "nodes" and "edges", but not both.');if(this.setOptions(t&&t.options),t&&t.dot){if(t&&t.dot){var i=U.util.DOTToGraph(t.dot);return void this.setData(i)}}else this._setNodes(t&&t.nodes),this._setEdges(t&&t.edges);this._putDataInSector(),e||(this.stabilize&&this._doStabilize(),this.moving=!0,this.start())},O.prototype.setOptions=function(t){if(t){if(void 0!==t.width&&(this.width=t.width),void 0!==t.height&&(this.height=t.height),void 0!==t.stabilize&&(this.stabilize=t.stabilize),void 0!==t.selectable&&(this.selectable=t.selectable),t.clustering){this.constants.clustering.enabled=!0;for(var e in t.clustering)t.clustering.hasOwnProperty(e)&&(this.constants.clustering[e]=t.clustering[e])}else void 0!==t.clustering&&(this.constants.clustering.enabled=!1);if(t.navigation){this.constants.navigation.enabled=!0;for(var e in t.navigation)t.navigation.hasOwnProperty(e)&&(this.constants.navigation[e]=t.navigation[e])}else void 0!==t.navigation&&(this.constants.navigation.enabled=!1);if(t.keyboard){this.constants.keyboard.enabled=!0;for(var e in t.keyboard)t.keyboard.hasOwnProperty(e)&&(this.constants.keyboard[e]=t.keyboard[e])}else void 0!==t.keyboard&&(this.constants.keyboard.enabled=!1);if(t.edges){for(e in t.edges)t.edges.hasOwnProperty(e)&&(this.constants.edges[e]=t.edges[e]);void 0!==t.edges.length&&t.nodes&&void 0===t.nodes.distance&&(this.constants.edges.length=t.edges.length,this.constants.nodes.distance=1.25*t.edges.length),t.edges.fontColor||(this.constants.edges.fontColor=t.edges.color),t.edges.dash&&(void 0!==t.edges.dash.length&&(this.constants.edges.dash.length=t.edges.dash.length),void 0!==t.edges.dash.gap&&(this.constants.edges.dash.gap=t.edges.dash.gap),void 0!==t.edges.dash.altLength&&(this.constants.edges.dash.altLength=t.edges.dash.altLength))}if(t.nodes){for(e in t.nodes)t.nodes.hasOwnProperty(e)&&(this.constants.nodes[e]=t.nodes[e]);t.nodes.color&&(this.constants.nodes.color=M.parseColor(t.nodes.color))}if(t.groups)for(var i in t.groups)if(t.groups.hasOwnProperty(i)){var n=t.groups[i];this.groups.add(i,n)}}this.setSize(this.width,this.height),this._setTranslation(this.frame.clientWidth/2,this.frame.clientHeight/2),this._setScale(1),this._loadNavigationControls(),this._createKeyBinds(),this._redraw()},O.prototype.on=function(t,e){var i=["select"];if(-1==i.indexOf(t))throw new Error('Unknown event "'+t+'". Choose from '+i.join());F.addListener(this,t,e)},O.prototype.off=function(t,e){F.removeListener(this,t,e)},O.prototype._trigger=function(t,e){F.trigger(this,t,e)},O.prototype._create=function(){for(;this.containerElement.hasChildNodes();)this.containerElement.removeChild(this.containerElement.firstChild);if(this.frame=document.createElement("div"),this.frame.className="graph-frame",this.frame.style.position="relative",this.frame.style.overflow="hidden",this.frame.canvas=document.createElement("canvas"),this.frame.canvas.style.position="relative",this.frame.appendChild(this.frame.canvas),!this.frame.canvas.getContext){var t=document.createElement("DIV");t.style.color="red",t.style.fontWeight="bold",t.style.padding="10px",t.innerHTML="Error: your browser does not support HTML canvas",this.frame.canvas.appendChild(t)}var e=this;this.drag={},this.pinch={},this.hammer=N(this.frame.canvas,{prevent_default:!0}),this.hammer.on("tap",e._onTap.bind(e)),this.hammer.on("doubletap",e._onDoubleTap.bind(e)),this.hammer.on("hold",e._onHold.bind(e)),this.hammer.on("pinch",e._onPinch.bind(e)),this.hammer.on("touch",e._onTouch.bind(e)),this.hammer.on("dragstart",e._onDragStart.bind(e)),this.hammer.on("drag",e._onDrag.bind(e)),this.hammer.on("dragend",e._onDragEnd.bind(e)),this.hammer.on("release",e._onRelease.bind(e)),this.hammer.on("mousewheel",e._onMouseWheel.bind(e)),this.hammer.on("DOMMouseScroll",e._onMouseWheel.bind(e)),this.hammer.on("mousemove",e._onMouseMoveTitle.bind(e)),this.containerElement.appendChild(this.frame)},O.prototype._createKeyBinds=function(){var t=this;this.mousetrap=A,this.mousetrap.reset(),1==this.constants.keyboard.enabled&&(this.mousetrap.bind("up",this._moveUp.bind(t),"keydown"),this.mousetrap.bind("up",this._yStopMoving.bind(t),"keyup"),this.mousetrap.bind("down",this._moveDown.bind(t),"keydown"),this.mousetrap.bind("down",this._yStopMoving.bind(t),"keyup"),this.mousetrap.bind("left",this._moveLeft.bind(t),"keydown"),this.mousetrap.bind("left",this._xStopMoving.bind(t),"keyup"),this.mousetrap.bind("right",this._moveRight.bind(t),"keydown"),this.mousetrap.bind("right",this._xStopMoving.bind(t),"keyup"),this.mousetrap.bind("=",this._zoomIn.bind(t),"keydown"),this.mousetrap.bind("=",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("-",this._zoomOut.bind(t),"keydown"),this.mousetrap.bind("-",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("[",this._zoomIn.bind(t),"keydown"),this.mousetrap.bind("[",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("]",this._zoomOut.bind(t),"keydown"),this.mousetrap.bind("]",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("pageup",this._zoomIn.bind(t),"keydown"),this.mousetrap.bind("pageup",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("pagedown",this._zoomOut.bind(t),"keydown"),this.mousetrap.bind("pagedown",this._stopZoom.bind(t),"keyup"))},O.prototype._getPointer=function(t){return{x:t.pageX-U.util.getAbsoluteLeft(this.frame.canvas),y:t.pageY-U.util.getAbsoluteTop(this.frame.canvas)}},O.prototype._onTouch=function(t){this.drag.pointer=this._getPointer(t.gesture.touches[0]),this.drag.pinched=!1,this.pinch.scale=this._getScale(),this._handleTouch(this.drag.pointer)},O.prototype._onDragStart=function(){var t=this.drag,e=this._getNodeAt(t.pointer);if(t.dragging=!0,t.selection=[],t.translation=this._getTranslation(),t.nodeId=null,null!=e){t.nodeId=e.id,e.isSelected()||this._selectNode(e,!1);var i=this;this.selection.forEach(function(e){var n=i.nodes[e];if(n){var s={id:e,node:n,x:n.x,y:n.y,xFixed:n.xFixed,yFixed:n.yFixed};n.xFixed=!0,n.yFixed=!0,t.selection.push(s)}})}},O.prototype._onDrag=function(t){if(!this.drag.pinched){var e=this._getPointer(t.gesture.touches[0]),i=this,n=this.drag,s=n.selection;if(s&&s.length){var o=e.x-n.pointer.x,r=e.y-n.pointer.y;s.forEach(function(t){var e=t.node;t.xFixed||(e.x=i._canvasToX(i._xToCanvas(t.x)+o)),t.yFixed||(e.y=i._canvasToY(i._yToCanvas(t.y)+r))}),this.moving||(this.moving=!0,this.start())}else{var a=e.x-this.drag.pointer.x,h=e.y-this.drag.pointer.y;this._setTranslation(this.drag.translation.x+a,this.drag.translation.y+h),this._redraw(),this.moved=!0}}},O.prototype._onDragEnd=function(){this.drag.dragging=!1;var t=this.drag.selection;t&&t.forEach(function(t){t.node.xFixed=t.xFixed,t.node.yFixed=t.yFixed})},O.prototype._onTap=function(t){var e=this._getPointer(t.gesture.touches[0]);this._handleTap(e)},O.prototype._onDoubleTap=function(t){var e=this._getPointer(t.gesture.touches[0]);this._handleDoubleTap(e)},O.prototype._onHold=function(t){var e=this._getPointer(t.gesture.touches[0]);this._handleOnHold(e)},O.prototype._onRelease=function(){this._handleOnRelease()},O.prototype._onPinch=function(t){var e=this._getPointer(t.gesture.center);this.drag.pinched=!0,"scale"in this.pinch||(this.pinch.scale=1);var i=this.pinch.scale*t.gesture.scale;this._zoom(i,e)},O.prototype._zoom=function(t,e){var i=this._getScale();1e-5>t&&(t=1e-5),t>10&&(t=10);var n=this._getTranslation(),s=t/i,o=(1-s)*e.x+n.x*s,r=(1-s)*e.y+n.y*s;return this.areaCenter={x:this._canvasToX(e.x),y:this._canvasToY(e.y)},this.pinch.mousewheelScale=t,this._setScale(t),this._setTranslation(o,r),this.updateClustersDefault(),this._redraw(),t},O.prototype._onMouseWheel=function(t){var e=0;if(t.wheelDelta?e=t.wheelDelta/120:t.detail&&(e=-t.detail/3),e){"mousewheelScale"in this.pinch||(this.pinch.mousewheelScale=1);var i=this.pinch.mousewheelScale,n=e/10;0>e&&(n/=1-n),i*=1+n;var s=z.fakeGesture(this,t),o=this._getPointer(s.center);i=this._zoom(i,o)}t.preventDefault()},O.prototype._onMouseMoveTitle=function(t){var e=z.fakeGesture(this,t),i=this._getPointer(e.center);this.popupNode&&this._checkHidePopup(i);var n=this,s=function(){n._checkShowPopup(i)};this.popupTimer&&clearInterval(this.popupTimer),this.drag.dragging||(this.popupTimer=setTimeout(s,300))},O.prototype._checkShowPopup=function(t){var e,i={left:this._canvasToX(t.x),top:this._canvasToY(t.y),right:this._canvasToX(t.x),bottom:this._canvasToY(t.y)},n=this.popupNode;if(void 0==this.popupNode){var s=this.nodes;for(e in s)if(s.hasOwnProperty(e)){var o=s[e];if(void 0!==o.getTitle()&&o.isOverlappingWith(i)){this.popupNode=o;break}}}if(void 0===this.popupNode){var r=this.edges;for(e in r)if(r.hasOwnProperty(e)){var a=r[e];if(a.connected&&void 0!==a.getTitle()&&a.isOverlappingWith(i)){this.popupNode=a;break}}}if(this.popupNode){if(this.popupNode!=n){var h=this;h.popup||(h.popup=new I(h.frame)),h.popup.setPosition(t.x-3,t.y-3),h.popup.setText(h.popupNode.getTitle()),h.popup.show()}}else this.popup&&this.popup.hide()},O.prototype._checkHidePopup=function(t){this.popupNode&&this._getNodeAt(t)||(this.popupNode=void 0,this.popup&&this.popup.hide())},O.prototype._getConnectionCount=function(t){function e(t){for(var e=[],i=0,n=t.length;n>i;i++)for(var s=t[i],o=s.edges,r=0,a=o.length;a>r;r++){var h=o[r],d=null;h.from==s?d=h.to:h.to==s&&(d=h.from);var c,l;if(d)for(c=0,l=t.length;l>c;c++)if(t[c]==d){d=null;break}if(d)for(c=0,l=e.length;l>c;c++)if(e[c]==d){d=null;break}d&&e.push(d)}return e}void 0==t&&(t=1);var i=[],n=this.nodes;for(var s in n)if(n.hasOwnProperty(s)){for(var o=[n[s]],r=0;t>r;r++)o=o.concat(e(o));i.push(o)}for(var a=[],h=0,d=i.length;d>h;h++)a.push(i[h].length);return a},O.prototype.setSize=function(t,e){this.frame.style.width=t,this.frame.style.height=e,this.frame.canvas.style.width="100%",this.frame.canvas.style.height="100%",this.frame.canvas.width=this.frame.canvas.clientWidth,this.frame.canvas.height=this.frame.canvas.clientHeight,1==this.constants.navigation.enabled&&this._relocateNavigation()},O.prototype._setNodes=function(t){var e=this.nodesData;if(t instanceof o||t instanceof r)this.nodesData=t;else if(t instanceof Array)this.nodesData=new o,this.nodesData.add(t);else{if(t)throw new TypeError("Array or DataSet expected");this.nodesData=new o}if(e&&z.forEach(this.nodesListeners,function(t,i){e.unsubscribe(i,t)}),this.nodes={},this.nodesData){var i=this;z.forEach(this.nodesListeners,function(t,e){i.nodesData.subscribe(e,t)});var n=this.nodesData.getIds();this._addNodes(n)}this._updateSelection()},O.prototype._addNodes=function(t){for(var e,i=0,n=t.length;n>i;i++){e=t[i];var s=this.nodesData.get(e),o=new M(s,this.images,this.groups,this.constants);if(this.nodes[e]=o,!o.isFixed()){var r=2*this.constants.edges.length,a=t.length,h=2*Math.PI*(i/a);o.x=r*Math.cos(h),o.y=r*Math.sin(h),this.moving=!0}}this._updateNodeIndexList(),this._reconnectEdges(),this._updateValueRange(this.nodes)},O.prototype._updateNodes=function(t){for(var e=this.nodes,i=this.nodesData,n=0,s=t.length;s>n;n++){var o=t[n],r=e[o],a=i.get(o);r?r.setProperties(a,this.constants):(r=new M(properties,this.images,this.groups,this.constants),e[o]=r,r.isFixed()||(this.moving=!0))}this._updateNodeIndexList(),this._reconnectEdges(),this._updateValueRange(e)},O.prototype._removeNodes=function(t){for(var e=this.nodes,i=0,n=t.length;n>i;i++){var s=t[i];delete e[s]}this._updateNodeIndexList(),this._reconnectEdges(),this._updateSelection(),this._updateValueRange(e)},O.prototype._setEdges=function(t){var e=this.edgesData;if(t instanceof o||t instanceof r)this.edgesData=t;else if(t instanceof Array)this.edgesData=new o,this.edgesData.add(t);else{if(t)throw new TypeError("Array or DataSet expected");this.edgesData=new o}if(e&&z.forEach(this.edgesListeners,function(t,i){e.unsubscribe(i,t)}),this.edges={},this.edgesData){var i=this;z.forEach(this.edgesListeners,function(t,e){i.edgesData.subscribe(e,t)});var n=this.edgesData.getIds();this._addEdges(n)}this._reconnectEdges()},O.prototype._addEdges=function(t){for(var e=this.edges,i=this.edgesData,n=0,s=t.length;s>n;n++){var o=t[n],r=e[o];r&&r.disconnect();var a=i.get(o,{showInternalIds:!0});e[o]=new D(a,this,this.constants)}this.moving=!0,this._updateValueRange(e)},O.prototype._updateEdges=function(t){for(var e=this.edges,i=this.edgesData,n=0,s=t.length;s>n;n++){var o=t[n],r=i.get(o),a=e[o];a?(a.disconnect(),a.setProperties(r,this.constants),a.connect()):(a=new D(r,this,this.constants),this.edges[o]=a)}this.moving=!0,this._updateValueRange(e)},O.prototype._removeEdges=function(t){for(var e=this.edges,i=0,n=t.length;n>i;i++){var s=t[i],o=e[s];o&&(o.disconnect(),delete e[s])}this.moving=!0,this._updateValueRange(e)},O.prototype._reconnectEdges=function(){var t,e=this.nodes,i=this.edges;for(t in e)e.hasOwnProperty(t)&&(e[t].edges=[]);for(t in i)if(i.hasOwnProperty(t)){var n=i[t];n.from=null,n.to=null,n.connect()}},O.prototype._updateValueRange=function(t){var e,i=void 0,n=void 0;for(e in t)if(t.hasOwnProperty(e)){var s=t[e].getValue();void 0!==s&&(i=void 0===i?s:Math.min(s,i),n=void 0===n?s:Math.max(s,n))}if(void 0!==i&&void 0!==n)for(e in t)t.hasOwnProperty(e)&&t[e].setValueRange(i,n)},O.prototype.redraw=function(){this.setSize(this.width,this.height),this._redraw()},O.prototype._redraw=function(){var t=this.frame.canvas.getContext("2d"),e=this.frame.canvas.width,i=this.frame.canvas.height;t.clearRect(0,0,e,i),t.save(),t.translate(this.translation.x,this.translation.y),t.scale(this.scale,this.scale),this.canvasTopLeft={x:this._canvasToX(0),y:this._canvasToY(0)},this.canvasBottomRight={x:this._canvasToX(this.frame.canvas.clientWidth),y:this._canvasToY(this.frame.canvas.clientHeight)},this._doInAllSectors("_drawAllSectorNodes",t),this._doInAllSectors("_drawEdges",t),this._doInAllSectors("_drawNodes",t),t.restore(),1==this.constants.navigation.enabled&&this._doInNavigationSector("_drawNodes",t,!0)},O.prototype._setTranslation=function(t,e){void 0===this.translation&&(this.translation={x:0,y:0}),void 0!==t&&(this.translation.x=t),void 0!==e&&(this.translation.y=e)},O.prototype._getTranslation=function(){return{x:this.translation.x,y:this.translation.y}},O.prototype._setScale=function(t){this.scale=t},O.prototype._getScale=function(){return this.scale},O.prototype._canvasToX=function(t){return(t-this.translation.x)/this.scale},O.prototype._xToCanvas=function(t){return t*this.scale+this.translation.x},O.prototype._canvasToY=function(t){return(t-this.translation.y)/this.scale},O.prototype._yToCanvas=function(t){return t*this.scale+this.translation.y},O.prototype._drawNodes=function(t,e){void 0===e&&(e=!1);var i=this.nodes,n=[];for(var s in i)i.hasOwnProperty(s)&&(i[s].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight),i[s].isSelected()?n.push(s):(i[s].inArea()||e)&&i[s].draw(t));for(var o=0,r=n.length;r>o;o++)(i[n[o]].inArea()||e)&&i[n[o]].draw(t)},O.prototype._drawEdges=function(t){var e=this.edges;for(var i in e)if(e.hasOwnProperty(i)){var n=e[i];n.setScale(this.scale),n.connected&&e[i].draw(t)}},O.prototype._doStabilize=function(){for(var t=0,e=this.constants.minVelocity,i=!1;!i&&tthis.constants.clustering.clusterThreshold&&1==this.constants.clustering.enabled&&this.clusterToFit(this.constants.clustering.reduceToNodes,!1),this._calculateForces())},O.prototype._calculateForces=function(){var t,e,i,n,s,o,r,a,h,d,c,l,u,p,f,g,m,v,y=this.nodes,_=this.edges,w=.08*this.forceFactor;for(g=0;gn&&(i=Math.atan2(e,t),r=.5*b>n?1:1/(1+Math.exp((n/b-1)*S)),r*=0==v?1:1+v*this.constants.clustering.forceAmplification,r*=this.forceFactor,s=Math.cos(i)*r,o=Math.sin(i)*r,l._addForce(-s,-o),u._addForce(s,o));for(f in _)_.hasOwnProperty(f)&&(p=_[f],p.connected&&this.nodes.hasOwnProperty(p.toId)&&this.nodes.hasOwnProperty(p.fromId)&&(v=p.to.clusterSize+p.from.clusterSize-2,t=p.to.x-p.from.x,e=p.to.y-p.from.y,d=p.length,d+=v*this.constants.clustering.edgeGrowth,h=Math.sqrt(t*t+e*e),i=Math.atan2(e,t),a=p.stiffness*(d-h)*this.forceFactor,s=Math.cos(i)*a,o=Math.sin(i)*a,p.from._addForce(-s,-o),p.to._addForce(s,o)))},O.prototype._isMoving=function(t){var e=t/this.scale,i=this.nodes;for(var n in i)if(i.hasOwnProperty(n)&&i[n].isMoving(e))return!0;return!1},O.prototype._discreteStepNodes=function(){var t=.01,e=this.nodes;for(var i in e)e.hasOwnProperty(i)&&e[i].discreteStep(t);var n=this.constants.minVelocity;this.moving=this._isMoving(n)},O.prototype.start=function(){if(!this.freezeSimulation)if(this.moving&&(this._doInAllActiveSectors("_initializeForceCalculation"),this._doInAllActiveSectors("_discreteStepNodes"),this._findCenter(this._getRange())),this.moving||0!=this.xIncrement||0!=this.yIncrement||0!=this.zoomIncrement){if(!this.timer){var t=this;this.timer=window.setTimeout(function(){if(t.timer=void 0,0!=t.xIncrement||0!=t.yIncrement){var e=t._getTranslation();t._setTranslation(e.x+t.xIncrement,e.y+t.yIncrement)}if(0!=t.zoomIncrement){var i={x:t.frame.canvas.clientWidth/2,y:t.frame.canvas.clientHeight/2};t._zoom(t.scale*(1+t.zoomIncrement),i)}t.start(),t._redraw()},this.renderTimestep)}}else this._redraw()},O.prototype.singleStep=function(){if(this.moving){this._initializeForceCalculation(),this._discreteStepNodes();var t=this.constants.minVelocity;this.moving=this._isMoving(t),this._redraw()}},O.prototype.toggleFreeze=function(){0==this.freezeSimulation?this.freezeSimulation=!0:(this.freezeSimulation=!1,this.start())},O.prototype._loadClusterSystem=function(){this.clusterSession=0,this.hubThreshold=5;for(var t in H)H.hasOwnProperty(t)&&(O.prototype[t]=H[t])},O.prototype._loadSectorSystem=function(){this.sectors={},this.activeSector=["default"],this.sectors.active={},this.sectors.active["default"]={nodes:{},edges:{},nodeIndices:[],formationScale:1,drawingNode:void 0},this.sectors.frozen={},this.sectors.navigation={nodes:{},edges:{},nodeIndices:[],formationScale:1,drawingNode:void 0},this.nodeIndices=this.sectors.active["default"].nodeIndices;for(var t in R)R.hasOwnProperty(t)&&(O.prototype[t]=R[t])},O.prototype._loadSelectionSystem=function(){this.selection=[],this.selectionObj={};for(var t in W)W.hasOwnProperty(t)&&(O.prototype[t]=W[t])},O.prototype._loadNavigationControls=function(){for(var t in j)j.hasOwnProperty(t)&&(O.prototype[t]=j[t]);1==this.constants.navigation.enabled&&this._loadNavigationElements()},O.prototype._relocateNavigation=function(){},O.prototype._unHighlightAll=function(){};var U={util:z,events:F,Controller:l,DataSet:o,DataView:r,Range:h,Stack:a,TimeStep:TimeStep,EventBus:s,components:{items:{Item:_,ItemBox:w,ItemPoint:b,ItemRange:S},Component:u,Panel:p,RootPanel:f,ItemSet:y,TimeAxis:g},graph:{Node:M,Edge:D,Popup:I,Groups:Groups,Images:Images},Timeline:C,Graph:O};"undefined"!=typeof n&&(n=U),"undefined"!=typeof i&&"undefined"!=typeof i.exports&&(i.exports=U),"function"==typeof t&&t(function(){return U}),"undefined"!=typeof window&&(window.vis=U)},{hammerjs:2,moment:3,mousetrap:4}],2:[function(t,e){!function(t,i){"use strict";function n(){if(!s.READY){s.event.determineEventTypes();for(var t in s.gestures)s.gestures.hasOwnProperty(t)&&s.detection.register(s.gestures[t]);s.event.onTouch(s.DOCUMENT,s.EVENT_MOVE,s.detection.detect),s.event.onTouch(s.DOCUMENT,s.EVENT_END,s.detection.detect),s.READY=!0}}var s=function(t,e){return new s.Instance(t,e||{})};s.defaults={stop_browser_behavior:{userSelect:"none",touchAction:"none",touchCallout:"none",contentZooming:"none",userDrag:"none",tapHighlightColor:"rgba(0,0,0,0)"}},s.HAS_POINTEREVENTS=navigator.pointerEnabled||navigator.msPointerEnabled,s.HAS_TOUCHEVENTS="ontouchstart"in t,s.MOBILE_REGEX=/mobile|tablet|ip(ad|hone|od)|android/i,s.NO_MOUSEEVENTS=s.HAS_TOUCHEVENTS&&navigator.userAgent.match(s.MOBILE_REGEX),s.EVENT_TYPES={},s.DIRECTION_DOWN="down",s.DIRECTION_LEFT="left",s.DIRECTION_UP="up",s.DIRECTION_RIGHT="right",s.POINTER_MOUSE="mouse",s.POINTER_TOUCH="touch",s.POINTER_PEN="pen",s.EVENT_START="start",s.EVENT_MOVE="move",s.EVENT_END="end",s.DOCUMENT=document,s.plugins={},s.READY=!1,s.Instance=function(t,e){var i=this;
-return n(),this.element=t,this.enabled=!0,this.options=s.utils.extend(s.utils.extend({},s.defaults),e||{}),this.options.stop_browser_behavior&&s.utils.stopDefaultBrowserBehavior(this.element,this.options.stop_browser_behavior),s.event.onTouch(t,s.EVENT_START,function(t){i.enabled&&s.detection.startDetect(i,t)}),this},s.Instance.prototype={on:function(t,e){for(var i=t.split(" "),n=0;n0&&e==s.EVENT_END?e=s.EVENT_MOVE:c||(e=s.EVENT_END),c||null===o?o=h:h=o,i.call(s.detection,n.collectEventData(t,e,h)),s.HAS_POINTEREVENTS&&e==s.EVENT_END&&(c=s.PointerEvent.updatePointer(e,h))),c||(o=null,r=!1,a=!1,s.PointerEvent.reset())}})},determineEventTypes:function(){var t;t=s.HAS_POINTEREVENTS?s.PointerEvent.getEvents():s.NO_MOUSEEVENTS?["touchstart","touchmove","touchend touchcancel"]:["touchstart mousedown","touchmove mousemove","touchend touchcancel mouseup"],s.EVENT_TYPES[s.EVENT_START]=t[0],s.EVENT_TYPES[s.EVENT_MOVE]=t[1],s.EVENT_TYPES[s.EVENT_END]=t[2]},getTouchList:function(t){return s.HAS_POINTEREVENTS?s.PointerEvent.getTouchList():t.touches?t.touches:[{identifier:1,pageX:t.pageX,pageY:t.pageY,target:t.target}]},collectEventData:function(t,e,i){var n=this.getTouchList(i,e),o=s.POINTER_TOUCH;return(i.type.match(/mouse/)||s.PointerEvent.matchType(s.POINTER_MOUSE,i))&&(o=s.POINTER_MOUSE),{center:s.utils.getCenter(n),timeStamp:(new Date).getTime(),target:i.target,touches:n,eventType:e,pointerType:o,srcEvent:i,preventDefault:function(){this.srcEvent.preventManipulation&&this.srcEvent.preventManipulation(),this.srcEvent.preventDefault&&this.srcEvent.preventDefault()},stopPropagation:function(){this.srcEvent.stopPropagation()},stopDetect:function(){return s.detection.stopDetect()}}}},s.PointerEvent={pointers:{},getTouchList:function(){var t=this,e=[];return Object.keys(t.pointers).sort().forEach(function(i){e.push(t.pointers[i])}),e},updatePointer:function(t,e){return t==s.EVENT_END?this.pointers={}:(e.identifier=e.pointerId,this.pointers[e.pointerId]=e),Object.keys(this.pointers).length},matchType:function(t,e){if(!e.pointerType)return!1;var i={};return i[s.POINTER_MOUSE]=e.pointerType==e.MSPOINTER_TYPE_MOUSE||e.pointerType==s.POINTER_MOUSE,i[s.POINTER_TOUCH]=e.pointerType==e.MSPOINTER_TYPE_TOUCH||e.pointerType==s.POINTER_TOUCH,i[s.POINTER_PEN]=e.pointerType==e.MSPOINTER_TYPE_PEN||e.pointerType==s.POINTER_PEN,i[t]},getEvents:function(){return["pointerdown MSPointerDown","pointermove MSPointerMove","pointerup pointercancel MSPointerUp MSPointerCancel"]},reset:function(){this.pointers={}}},s.utils={extend:function(t,e,n){for(var s in e)t[s]!==i&&n||(t[s]=e[s]);return t},hasParent:function(t,e){for(;t;){if(t==e)return!0;t=t.parentNode}return!1},getCenter:function(t){for(var e=[],i=[],n=0,s=t.length;s>n;n++)e.push(t[n].pageX),i.push(t[n].pageY);return{pageX:(Math.min.apply(Math,e)+Math.max.apply(Math,e))/2,pageY:(Math.min.apply(Math,i)+Math.max.apply(Math,i))/2}},getVelocity:function(t,e,i){return{x:Math.abs(e/t)||0,y:Math.abs(i/t)||0}},getAngle:function(t,e){var i=e.pageY-t.pageY,n=e.pageX-t.pageX;return 180*Math.atan2(i,n)/Math.PI},getDirection:function(t,e){var i=Math.abs(t.pageX-e.pageX),n=Math.abs(t.pageY-e.pageY);return i>=n?t.pageX-e.pageX>0?s.DIRECTION_LEFT:s.DIRECTION_RIGHT:t.pageY-e.pageY>0?s.DIRECTION_UP:s.DIRECTION_DOWN},getDistance:function(t,e){var i=e.pageX-t.pageX,n=e.pageY-t.pageY;return Math.sqrt(i*i+n*n)},getScale:function(t,e){return t.length>=2&&e.length>=2?this.getDistance(e[0],e[1])/this.getDistance(t[0],t[1]):1},getRotation:function(t,e){return t.length>=2&&e.length>=2?this.getAngle(e[1],e[0])-this.getAngle(t[1],t[0]):0},isVertical:function(t){return t==s.DIRECTION_UP||t==s.DIRECTION_DOWN},stopDefaultBrowserBehavior:function(t,e){var i,n=["webkit","khtml","moz","ms","o",""];if(e&&t.style){for(var s=0;si;i++){var o=this.gestures[i];if(!this.stopped&&e[o.name]!==!1&&o.handler.call(o,t,this.current.inst)===!1){this.stopDetect();break}}return this.current&&(this.current.lastEvent=t),t.eventType==s.EVENT_END&&!t.touches.length-1&&this.stopDetect(),t}},stopDetect:function(){this.previous=s.utils.extend({},this.current),this.current=null,this.stopped=!0},extendEventData:function(t){var e=this.current.startEvent;if(e&&(t.touches.length!=e.touches.length||t.touches===e.touches)){e.touches=[];for(var i=0,n=t.touches.length;n>i;i++)e.touches.push(s.utils.extend({},t.touches[i]))}var o=t.timeStamp-e.timeStamp,r=t.center.pageX-e.center.pageX,a=t.center.pageY-e.center.pageY,h=s.utils.getVelocity(o,r,a);return s.utils.extend(t,{deltaTime:o,deltaX:r,deltaY:a,velocityX:h.x,velocityY:h.y,distance:s.utils.getDistance(e.center,t.center),angle:s.utils.getAngle(e.center,t.center),direction:s.utils.getDirection(e.center,t.center),scale:s.utils.getScale(e.touches,t.touches),rotation:s.utils.getRotation(e.touches,t.touches),startEvent:e}),t},register:function(t){var e=t.defaults||{};return e[t.name]===i&&(e[t.name]=!0),s.utils.extend(s.defaults,e,!0),t.index=t.index||1e3,this.gestures.push(t),this.gestures.sort(function(t,e){return t.indexe.index?1:0}),this.gestures}},s.gestures=s.gestures||{},s.gestures.Hold={name:"hold",index:10,defaults:{hold_timeout:500,hold_threshold:1},timer:null,handler:function(t,e){switch(t.eventType){case s.EVENT_START:clearTimeout(this.timer),s.detection.current.name=this.name,this.timer=setTimeout(function(){"hold"==s.detection.current.name&&e.trigger("hold",t)},e.options.hold_timeout);break;case s.EVENT_MOVE:t.distance>e.options.hold_threshold&&clearTimeout(this.timer);break;case s.EVENT_END:clearTimeout(this.timer)}}},s.gestures.Tap={name:"tap",index:100,defaults:{tap_max_touchtime:250,tap_max_distance:10,tap_always:!0,doubletap_distance:20,doubletap_interval:300},handler:function(t,e){if(t.eventType==s.EVENT_END){var i=s.detection.previous,n=!1;if(t.deltaTime>e.options.tap_max_touchtime||t.distance>e.options.tap_max_distance)return;i&&"tap"==i.name&&t.timeStamp-i.lastEvent.timeStamp0&&t.touches.length>e.options.swipe_max_touches)return;(t.velocityX>e.options.swipe_velocity||t.velocityY>e.options.swipe_velocity)&&(e.trigger(this.name,t),e.trigger(this.name+t.direction,t))}}},s.gestures.Drag={name:"drag",index:50,defaults:{drag_min_distance:10,drag_max_touches:1,drag_block_horizontal:!1,drag_block_vertical:!1,drag_lock_to_axis:!1,drag_lock_min_distance:25},triggered:!1,handler:function(t,e){if(s.detection.current.name!=this.name&&this.triggered)return e.trigger(this.name+"end",t),void(this.triggered=!1);if(!(e.options.drag_max_touches>0&&t.touches.length>e.options.drag_max_touches))switch(t.eventType){case s.EVENT_START:this.triggered=!1;break;case s.EVENT_MOVE:if(t.distancee.options.transform_min_rotation&&e.trigger("rotate",t),i>e.options.transform_min_scale&&(e.trigger("pinch",t),e.trigger("pinch"+(t.scale<1?"in":"out"),t));break;case s.EVENT_END:this.triggered&&e.trigger(this.name+"end",t),this.triggered=!1}}},s.gestures.Touch={name:"touch",index:-1/0,defaults:{prevent_default:!1,prevent_mouseevents:!1},handler:function(t,e){return e.options.prevent_mouseevents&&t.pointerType==s.POINTER_MOUSE?void t.stopDetect():(e.options.prevent_default&&t.preventDefault(),void(t.eventType==s.EVENT_START&&e.trigger(this.name,t)))}},s.gestures.Release={name:"release",index:1/0,handler:function(t,e){t.eventType==s.EVENT_END&&e.trigger(this.name,t)}},"object"==typeof e&&"object"==typeof e.exports?e.exports=s:(t.Hammer=s,"function"==typeof t.define&&t.define.amd&&t.define("hammer",[],function(){return s}))}(this)},{}],3:[function(e,i){(function(n){function s(){return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function o(t,e){return function(i){return p(t.call(this,i),e)}}function r(t,e){return function(i){return this.lang().ordinal(t.call(this,i),e)}}function a(){}function h(t){x(t),c(this,t)}function d(t){var e=_(t),i=e.year||0,n=e.month||0,s=e.week||0,o=e.day||0,r=e.hour||0,a=e.minute||0,h=e.second||0,d=e.millisecond||0;this._milliseconds=+d+1e3*h+6e4*a+36e5*r,this._days=+o+7*s,this._months=+n+12*i,this._data={},this._bubble()}function c(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return e.hasOwnProperty("toString")&&(t.toString=e.toString),e.hasOwnProperty("valueOf")&&(t.valueOf=e.valueOf),t}function l(t){var e,i={};for(e in t)t.hasOwnProperty(e)&&_e.hasOwnProperty(e)&&(i[e]=t[e]);return i}function u(t){return 0>t?Math.ceil(t):Math.floor(t)}function p(t,e,i){for(var n=""+Math.abs(t),s=t>=0;n.lengthn;n++)(i&&t[n]!==e[n]||!i&&b(t[n])!==b(e[n]))&&r++;return r+o}function y(t){if(t){var e=t.toLowerCase().replace(/(.)s$/,"$1");t=Xe[t]||Ze[e]||e}return t}function _(t){var e,i,n={};for(i in t)t.hasOwnProperty(i)&&(e=y(i),e&&(n[e]=t[i]));return n}function w(t){var e,i;if(0===t.indexOf("week"))e=7,i="day";else{if(0!==t.indexOf("month"))return;e=12,i="month"}re[t]=function(s,o){var r,a,h=re.fn._lang[t],d=[];if("number"==typeof s&&(o=s,s=n),a=function(t){var e=re().utc().set(i,t);return h.call(re.fn._lang,e,s||"")},null!=o)return a(o);for(r=0;e>r;r++)d.push(a(r));return d}}function b(t){var e=+t,i=0;return 0!==e&&isFinite(e)&&(i=e>=0?Math.floor(e):Math.ceil(e)),i}function S(t,e){return new Date(Date.UTC(t,e+1,0)).getUTCDate()}function E(t){return T(t)?366:365}function T(t){return t%4===0&&t%100!==0||t%400===0}function x(t){var e;t._a&&-2===t._pf.overflow&&(e=t._a[ue]<0||t._a[ue]>11?ue:t._a[pe]<1||t._a[pe]>S(t._a[le],t._a[ue])?pe:t._a[fe]<0||t._a[fe]>23?fe:t._a[ge]<0||t._a[ge]>59?ge:t._a[me]<0||t._a[me]>59?me:t._a[ve]<0||t._a[ve]>999?ve:-1,t._pf._overflowDayOfYear&&(le>e||e>pe)&&(e=pe),t._pf.overflow=e)}function C(t){return null==t._isValid&&(t._isValid=!isNaN(t._d.getTime())&&t._pf.overflow<0&&!t._pf.empty&&!t._pf.invalidMonth&&!t._pf.nullInput&&!t._pf.invalidFormat&&!t._pf.userInvalidated,t._strict&&(t._isValid=t._isValid&&0===t._pf.charsLeftOver&&0===t._pf.unusedTokens.length)),t._isValid}function M(t){return t?t.toLowerCase().replace("_","-"):t}function D(t,e){return e._isUTC?re(t).zone(e._offset||0):re(t).local()}function I(t,e){return e.abbr=t,ye[t]||(ye[t]=new a),ye[t].set(e),ye[t]}function O(t){delete ye[t]}function N(t){var i,n,s,o,r=0,a=function(t){if(!ye[t]&&we)try{e("./lang/"+t)}catch(i){}return ye[t]};if(!t)return re.fn._lang;if(!g(t)){if(n=a(t))return n;t=[t]}for(;r0;){if(n=a(o.slice(0,i).join("-")))return n;if(s&&s.length>=i&&v(o,s,!0)>=i-1)break;i--}r++}return re.fn._lang}function L(t){return t.match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function A(t){var e,i,n=t.match(Te);for(e=0,i=n.length;i>e;e++)n[e]=Je[n[e]]?Je[n[e]]:L(n[e]);return function(s){var o="";for(e=0;i>e;e++)o+=n[e]instanceof Function?n[e].call(s,t):n[e];return o}}function k(t,e){return t.isValid()?(e=z(e,t.lang()),Ke[e]||(Ke[e]=A(e)),Ke[e](t)):t.lang().invalidDate()}function z(t,e){function i(t){return e.longDateFormat(t)||t}var n=5;for(xe.lastIndex=0;n>=0&&xe.test(t);)t=t.replace(xe,i),xe.lastIndex=0,n-=1;return t}function P(t,e){var i,n=e._strict;switch(t){case"DDDD":return Fe;case"YYYY":case"GGGG":case"gggg":return n?Ye:De;case"Y":case"G":case"g":return He;case"YYYYYY":case"YYYYY":case"GGGGG":case"ggggg":return n?Re:Ie;case"S":if(n)return ze;case"SS":if(n)return Pe;case"SSS":if(n)return Fe;case"DDD":return Me;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return Ne;case"a":case"A":return N(e._l)._meridiemParse;case"X":return ke;case"Z":case"ZZ":return Le;case"T":return Ae;case"SSSS":return Oe;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"ww":case"WW":return n?Pe:Ce;case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"W":case"e":case"E":return Ce;default:return i=new RegExp(V(U(t.replace("\\","")),"i"))}}function F(t){t=t||"";var e=t.match(Le)||[],i=e[e.length-1]||[],n=(i+"").match(Ge)||["-",0,0],s=+(60*n[1])+b(n[2]);return"+"===n[0]?-s:s}function Y(t,e,i){var n,s=i._a;switch(t){case"M":case"MM":null!=e&&(s[ue]=b(e)-1);break;case"MMM":case"MMMM":n=N(i._l).monthsParse(e),null!=n?s[ue]=n:i._pf.invalidMonth=e;break;case"D":case"DD":null!=e&&(s[pe]=b(e));break;case"DDD":case"DDDD":null!=e&&(i._dayOfYear=b(e));break;case"YY":s[le]=b(e)+(b(e)>68?1900:2e3);break;case"YYYY":case"YYYYY":case"YYYYYY":s[le]=b(e);break;case"a":case"A":i._isPm=N(i._l).isPM(e);break;case"H":case"HH":case"h":case"hh":s[fe]=b(e);break;case"m":case"mm":s[ge]=b(e);break;case"s":case"ss":s[me]=b(e);break;case"S":case"SS":case"SSS":case"SSSS":s[ve]=b(1e3*("0."+e));break;case"X":i._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":i._useUTC=!0,i._tzm=F(e);break;case"w":case"ww":case"W":case"WW":case"d":case"dd":case"ddd":case"dddd":case"e":case"E":t=t.substr(0,1);case"gg":case"gggg":case"GG":case"GGGG":case"GGGGG":t=t.substr(0,2),e&&(i._w=i._w||{},i._w[t]=e)}}function R(t){var e,i,n,s,o,r,a,h,d,c,l=[];if(!t._d){for(n=W(t),t._w&&null==t._a[pe]&&null==t._a[ue]&&(o=function(e){var i=parseInt(e,10);return e?e.length<3?i>68?1900+i:2e3+i:i:null==t._a[le]?re().weekYear():t._a[le]},r=t._w,null!=r.GG||null!=r.W||null!=r.E?a=te(o(r.GG),r.W||1,r.E,4,1):(h=N(t._l),d=null!=r.d?K(r.d,h):null!=r.e?parseInt(r.e,10)+h._week.dow:0,c=parseInt(r.w,10)||1,null!=r.d&&dE(s)&&(t._pf._overflowDayOfYear=!0),i=Z(s,0,t._dayOfYear),t._a[ue]=i.getUTCMonth(),t._a[pe]=i.getUTCDate()),e=0;3>e&&null==t._a[e];++e)t._a[e]=l[e]=n[e];for(;7>e;e++)t._a[e]=l[e]=null==t._a[e]?2===e?1:0:t._a[e];l[fe]+=b((t._tzm||0)/60),l[ge]+=b((t._tzm||0)%60),t._d=(t._useUTC?Z:X).apply(null,l)}}function H(t){var e;t._d||(e=_(t._i),t._a=[e.year,e.month,e.day,e.hour,e.minute,e.second,e.millisecond],R(t))}function W(t){var e=new Date;return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}function j(t){t._a=[],t._pf.empty=!0;var e,i,n,s,o,r=N(t._l),a=""+t._i,h=a.length,d=0;for(n=z(t._f,r).match(Te)||[],e=0;e0&&t._pf.unusedInput.push(o),a=a.slice(a.indexOf(i)+i.length),d+=i.length),Je[s]?(i?t._pf.empty=!1:t._pf.unusedTokens.push(s),Y(s,i,t)):t._strict&&!i&&t._pf.unusedTokens.push(s);t._pf.charsLeftOver=h-d,a.length>0&&t._pf.unusedInput.push(a),t._isPm&&t._a[fe]<12&&(t._a[fe]+=12),t._isPm===!1&&12===t._a[fe]&&(t._a[fe]=0),R(t),x(t)}function U(t){return t.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,i,n,s){return e||i||n||s})}function V(t){return t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function G(t){var e,i,n,o,r;if(0===t._f.length)return t._pf.invalidFormat=!0,void(t._d=new Date(0/0));for(o=0;or)&&(n=r,i=e));c(t,i||e)}function B(t){var e,i,n=t._i,s=We.exec(n);if(s){for(t._pf.iso=!0,e=0,i=Ue.length;i>e;e++)if(Ue[e][1].exec(n)){t._f=Ue[e][0]+(s[6]||" ");break}for(e=0,i=Ve.length;i>e;e++)if(Ve[e][1].exec(n)){t._f+=Ve[e][0];break}n.match(Le)&&(t._f+="Z"),j(t)}else t._d=new Date(n)}function q(t){var e=t._i,i=be.exec(e);e===n?t._d=new Date:i?t._d=new Date(+i[1]):"string"==typeof e?B(t):g(e)?(t._a=e.slice(0),R(t)):m(e)?t._d=new Date(+e):"object"==typeof e?H(t):t._d=new Date(e)}function X(t,e,i,n,s,o,r){var a=new Date(t,e,i,n,s,o,r);return 1970>t&&a.setFullYear(t),a}function Z(t){var e=new Date(Date.UTC.apply(null,arguments));return 1970>t&&e.setUTCFullYear(t),e}function K(t,e){if("string"==typeof t)if(isNaN(t)){if(t=e.weekdaysParse(t),"number"!=typeof t)return null}else t=parseInt(t,10);return t}function $(t,e,i,n,s){return s.relativeTime(e||1,!!i,t,n)}function Q(t,e,i){var n=ce(Math.abs(t)/1e3),s=ce(n/60),o=ce(s/60),r=ce(o/24),a=ce(r/365),h=45>n&&["s",n]||1===s&&["m"]||45>s&&["mm",s]||1===o&&["h"]||22>o&&["hh",o]||1===r&&["d"]||25>=r&&["dd",r]||45>=r&&["M"]||345>r&&["MM",ce(r/30)]||1===a&&["y"]||["yy",a];return h[2]=e,h[3]=t>0,h[4]=i,$.apply({},h)}function J(t,e,i){var n,s=i-e,o=i-t.day();return o>s&&(o-=7),s-7>o&&(o+=7),n=re(t).add("d",o),{week:Math.ceil(n.dayOfYear()/7),year:n.year()}}function te(t,e,i,n,s){var o,r,a=Z(t,0,1).getUTCDay();return i=null!=i?i:s,o=s-a+(a>n?7:0)-(s>a?7:0),r=7*(e-1)+(i-s)+o+1,{year:r>0?t:t-1,dayOfYear:r>0?r:E(t-1)+r}}function ee(t){var e=t._i,i=t._f;return null===e?re.invalid({nullInput:!0}):("string"==typeof e&&(t._i=e=N().preparse(e)),re.isMoment(e)?(t=l(e),t._d=new Date(+e._d)):i?g(i)?G(t):j(t):q(t),new h(t))}function ie(t,e){re.fn[t]=re.fn[t+"s"]=function(t){var i=this._isUTC?"UTC":"";return null!=t?(this._d["set"+i+e](t),re.updateOffset(this),this):this._d["get"+i+e]()}}function ne(t){re.duration.fn[t]=function(){return this._data[t]}}function se(t,e){re.duration.fn["as"+t]=function(){return+this/e}}function oe(t){var e=!1,i=re;"undefined"==typeof ender&&(t?(de.moment=function(){return!e&&console&&console.warn&&(e=!0,console.warn("Accessing Moment through the global scope is deprecated, and will be removed in an upcoming release.")),i.apply(null,arguments)},c(de.moment,i)):de.moment=re)}for(var re,ae,he="2.5.1",de=this,ce=Math.round,le=0,ue=1,pe=2,fe=3,ge=4,me=5,ve=6,ye={},_e={_isAMomentObject:null,_i:null,_f:null,_l:null,_strict:null,_isUTC:null,_offset:null,_pf:null,_lang:null},we="undefined"!=typeof i&&i.exports&&"undefined"!=typeof e,be=/^\/?Date\((\-?\d+)/i,Se=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,Ee=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,Te=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,xe=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,Ce=/\d\d?/,Me=/\d{1,3}/,De=/\d{1,4}/,Ie=/[+\-]?\d{1,6}/,Oe=/\d+/,Ne=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Le=/Z|[\+\-]\d\d:?\d\d/gi,Ae=/T/i,ke=/[\+\-]?\d+(\.\d{1,3})?/,ze=/\d/,Pe=/\d\d/,Fe=/\d{3}/,Ye=/\d{4}/,Re=/[+-]?\d{6}/,He=/[+-]?\d+/,We=/^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,je="YYYY-MM-DDTHH:mm:ssZ",Ue=[["YYYYYY-MM-DD",/[+-]\d{6}-\d{2}-\d{2}/],["YYYY-MM-DD",/\d{4}-\d{2}-\d{2}/],["GGGG-[W]WW-E",/\d{4}-W\d{2}-\d/],["GGGG-[W]WW",/\d{4}-W\d{2}/],["YYYY-DDD",/\d{4}-\d{3}/]],Ve=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],Ge=/([\+\-]|\d\d)/gi,Be="Date|Hours|Minutes|Seconds|Milliseconds".split("|"),qe={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},Xe={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",D:"date",w:"week",W:"isoWeek",M:"month",y:"year",DDD:"dayOfYear",e:"weekday",E:"isoWeekday",gg:"weekYear",GG:"isoWeekYear"},Ze={dayofyear:"dayOfYear",isoweekday:"isoWeekday",isoweek:"isoWeek",weekyear:"weekYear",isoweekyear:"isoWeekYear"},Ke={},$e="DDD w W M D d".split(" "),Qe="M D H h m s w W".split(" "),Je={M:function(){return this.month()+1},MMM:function(t){return this.lang().monthsShort(this,t)},MMMM:function(t){return this.lang().months(this,t)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(t){return this.lang().weekdaysMin(this,t)},ddd:function(t){return this.lang().weekdaysShort(this,t)},dddd:function(t){return this.lang().weekdays(this,t)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return p(this.year()%100,2)},YYYY:function(){return p(this.year(),4)},YYYYY:function(){return p(this.year(),5)},YYYYYY:function(){var t=this.year(),e=t>=0?"+":"-";return e+p(Math.abs(t),6)},gg:function(){return p(this.weekYear()%100,2)},gggg:function(){return p(this.weekYear(),4)},ggggg:function(){return p(this.weekYear(),5)},GG:function(){return p(this.isoWeekYear()%100,2)},GGGG:function(){return p(this.isoWeekYear(),4)},GGGGG:function(){return p(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return b(this.milliseconds()/100)},SS:function(){return p(b(this.milliseconds()/10),2)},SSS:function(){return p(this.milliseconds(),3)},SSSS:function(){return p(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+p(b(t/60),2)+":"+p(b(t)%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+p(b(t/60),2)+p(b(t)%60,2)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()},Q:function(){return this.quarter()}},ti=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"];$e.length;)ae=$e.pop(),Je[ae+"o"]=r(Je[ae],ae);for(;Qe.length;)ae=Qe.pop(),Je[ae+ae]=o(Je[ae],2);for(Je.DDDD=o(Je.DDD,3),c(a.prototype,{set:function(t){var e,i;for(i in t)e=t[i],"function"==typeof e?this[i]=e:this["_"+i]=e},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(t){return this._months[t.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(t){return this._monthsShort[t.month()]},monthsParse:function(t){var e,i,n;for(this._monthsParse||(this._monthsParse=[]),e=0;12>e;e++)if(this._monthsParse[e]||(i=re.utc([2e3,e]),n="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[e]=new RegExp(n.replace(".",""),"i")),this._monthsParse[e].test(t))return e},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(t){return this._weekdays[t.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(t){return this._weekdaysShort[t.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(t){return this._weekdaysMin[t.day()]},weekdaysParse:function(t){var e,i,n;for(this._weekdaysParse||(this._weekdaysParse=[]),e=0;7>e;e++)if(this._weekdaysParse[e]||(i=re([2e3,1]).day(e),n="^"+this.weekdays(i,"")+"|^"+this.weekdaysShort(i,"")+"|^"+this.weekdaysMin(i,""),this._weekdaysParse[e]=new RegExp(n.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(t){var e=this._longDateFormat[t];return!e&&this._longDateFormat[t.toUpperCase()]&&(e=this._longDateFormat[t.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t]=e),e},isPM:function(t){return"p"===(t+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(t,e,i){return t>11?i?"pm":"PM":i?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(t,e){var i=this._calendar[t];return"function"==typeof i?i.apply(e):i},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(t,e,i,n){var s=this._relativeTime[i];return"function"==typeof s?s(t,e,i,n):s.replace(/%d/i,t)},pastFuture:function(t,e){var i=this._relativeTime[t>0?"future":"past"];return"function"==typeof i?i(e):i.replace(/%s/i,e)},ordinal:function(t){return this._ordinal.replace("%d",t)},_ordinal:"%d",preparse:function(t){return t},postformat:function(t){return t},week:function(t){return J(t,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),re=function(t,e,i,o){var r;return"boolean"==typeof i&&(o=i,i=n),r={},r._isAMomentObject=!0,r._i=t,r._f=e,r._l=i,r._strict=o,r._isUTC=!1,r._pf=s(),ee(r)},re.utc=function(t,e,i,o){var r;return"boolean"==typeof i&&(o=i,i=n),r={},r._isAMomentObject=!0,r._useUTC=!0,r._isUTC=!0,r._l=i,r._i=t,r._f=e,r._strict=o,r._pf=s(),ee(r).utc()},re.unix=function(t){return re(1e3*t)},re.duration=function(t,e){var i,n,s,o=t,r=null;return re.isDuration(t)?o={ms:t._milliseconds,d:t._days,M:t._months}:"number"==typeof t?(o={},e?o[e]=t:o.milliseconds=t):(r=Se.exec(t))?(i="-"===r[1]?-1:1,o={y:0,d:b(r[pe])*i,h:b(r[fe])*i,m:b(r[ge])*i,s:b(r[me])*i,ms:b(r[ve])*i}):(r=Ee.exec(t))&&(i="-"===r[1]?-1:1,s=function(t){var e=t&&parseFloat(t.replace(",","."));return(isNaN(e)?0:e)*i},o={y:s(r[2]),M:s(r[3]),d:s(r[4]),h:s(r[5]),m:s(r[6]),s:s(r[7]),w:s(r[8])}),n=new d(o),re.isDuration(t)&&t.hasOwnProperty("_lang")&&(n._lang=t._lang),n},re.version=he,re.defaultFormat=je,re.updateOffset=function(){},re.lang=function(t,e){var i;return t?(e?I(M(t),e):null===e?(O(t),t="en"):ye[t]||N(t),i=re.duration.fn._lang=re.fn._lang=N(t),i._abbr):re.fn._lang._abbr},re.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),N(t)},re.isMoment=function(t){return t instanceof h||null!=t&&t.hasOwnProperty("_isAMomentObject")},re.isDuration=function(t){return t instanceof d},ae=ti.length-1;ae>=0;--ae)w(ti[ae]);for(re.normalizeUnits=function(t){return y(t)},re.invalid=function(t){var e=re.utc(0/0);return null!=t?c(e._pf,t):e._pf.userInvalidated=!0,e},re.parseZone=function(t){return re(t).parseZone()},c(re.fn=h.prototype,{clone:function(){return re(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().lang("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){var t=re(this).utc();return 00:!1},parsingFlags:function(){return c({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(t){var e=k(this,t||re.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var i;return i="string"==typeof t?re.duration(+e,t):re.duration(t,e),f(this,i,1),this},subtract:function(t,e){var i;return i="string"==typeof t?re.duration(+e,t):re.duration(t,e),f(this,i,-1),this},diff:function(t,e,i){var n,s,o=D(t,this),r=6e4*(this.zone()-o.zone());return e=y(e),"year"===e||"month"===e?(n=432e5*(this.daysInMonth()+o.daysInMonth()),s=12*(this.year()-o.year())+(this.month()-o.month()),s+=(this-re(this).startOf("month")-(o-re(o).startOf("month")))/n,s-=6e4*(this.zone()-re(this).startOf("month").zone()-(o.zone()-re(o).startOf("month").zone()))/n,"year"===e&&(s/=12)):(n=this-o,s="second"===e?n/1e3:"minute"===e?n/6e4:"hour"===e?n/36e5:"day"===e?(n-r)/864e5:"week"===e?(n-r)/6048e5:n),i?s:u(s)},from:function(t,e){return re.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(re(),t)},calendar:function(){var t=D(re(),this).startOf("day"),e=this.diff(t,"days",!0),i=-6>e?"sameElse":-1>e?"lastWeek":0>e?"lastDay":1>e?"sameDay":2>e?"nextDay":7>e?"nextWeek":"sameElse";return this.format(this.lang().calendar(i,this))},isLeapYear:function(){return T(this.year())},isDST:function(){return this.zone()+re(t).startOf(e)},isBefore:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)<+re(t).startOf(e)},isSame:function(t,e){return e=e||"ms",+this.clone().startOf(e)===+D(t,this).startOf(e)},min:function(t){return t=re.apply(null,arguments),this>t?this:t},max:function(t){return t=re.apply(null,arguments),t>this?this:t},zone:function(t){var e=this._offset||0;return null==t?this._isUTC?e:this._d.getTimezoneOffset():("string"==typeof t&&(t=F(t)),Math.abs(t)<16&&(t=60*t),this._offset=t,this._isUTC=!0,e!==t&&f(this,re.duration(e-t,"m"),1,!0),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return this._tzm?this.zone(this._tzm):"string"==typeof this._i&&this.zone(this._i),this},hasAlignedHourOffset:function(t){return t=t?re(t).zone():0,(this.zone()-t)%60===0},daysInMonth:function(){return S(this.year(),this.month())},dayOfYear:function(t){var e=ce((re(this).startOf("day")-re(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},quarter:function(){return Math.ceil((this.month()+1)/3)},weekYear:function(t){var e=J(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==t?e:this.add("y",t-e)},isoWeekYear:function(t){var e=J(this,1,4).year;return null==t?e:this.add("y",t-e)},week:function(t){var e=this.lang().week(this);return null==t?e:this.add("d",7*(t-e))},isoWeek:function(t){var e=J(this,1,4).week;return null==t?e:this.add("d",7*(t-e))},weekday:function(t){var e=(this.day()+7-this.lang()._week.dow)%7;return null==t?e:this.add("d",t-e)},isoWeekday:function(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)},get:function(t){return t=y(t),this[t]()},set:function(t,e){return t=y(t),"function"==typeof this[t]&&this[t](e),this},lang:function(t){return t===n?this._lang:(this._lang=N(t),this)}}),ae=0;ae-1?!1:"INPUT"==i||"SELECT"==i||"TEXTAREA"==i||e.contentEditable&&"true"==e.contentEditable}function o(t,e){return t.sort().join(",")===e.sort().join(",")}function r(t){t=t||{};var e,i=!1;for(e in M)t[e]?i=!0:M[e]=0;i||(I=!1)}function a(t,e,i,n,s){var r,a,h=[];if(!x[t])return[];for("keyup"==i&&u(t)&&(e=[t]),r=0;r95&&112>t||b.hasOwnProperty(t)&&(_[b[t]]=t)}return _}function g(t,e,i){return i||(i=f()[t]?"keydown":"keypress"),"keypress"==i&&e.length&&(i="keydown"),i}function m(t,e,i,s){M[t]=0,s||(s=g(e[0],[]));var o,a=function(){I=s,++M[t],p()},h=function(t){d(i,t),"keyup"!==s&&(D=n(t)),setTimeout(r,10)};for(o=0;o1)return m(t,d,e,i);for(h="+"===t?["+"]:t.split("+"),o=0;o":".","?":"/","|":"\\"},T={option:"alt",command:"meta","return":"enter",escape:"esc"},x={},C={},M={},D=!1,I=!1,O=1;20>O;++O)b[111+O]="f"+O;for(O=0;9>=O;++O)b[O+96]=O;i(document,"keypress",l),i(document,"keydown",l),i(document,"keyup",l);var N={bind:function(t,e,i){return y(t instanceof Array?t:[t],e,i),C[t+":"+i]=e,this},unbind:function(t,e){return C[t+":"+e]&&(delete C[t+":"+e],this.bind(t,function(){},e)),this},trigger:function(t,e){return C[t+":"+e](),this},reset:function(){return x={},C={},this}};e.exports=N},{}]},{},[1])(1)});
\ No newline at end of file
+!function(t){if("object"==typeof exports)module.exports=t();else if("function"==typeof define&&define.amd)define(t);else{var e;"undefined"!=typeof window?e=window:"undefined"!=typeof global?e=global:"undefined"!=typeof self&&(e=self),e.vis=t()}}(function(){var define,module,exports;return function t(e,i,s){function n(a,r){if(!i[a]){if(!e[a]){var h="function"==typeof require&&require;if(!r&&h)return h(a,!0);if(o)return o(a,!0);throw new Error("Cannot find module '"+a+"'")}var d=i[a]={exports:{}};e[a][0].call(d.exports,function(t){var i=e[a][1][t];return n(i?i:t)},d,d.exports,t,e,i,s)}return i[a].exports}for(var o="function"==typeof require&&require,a=0;ai;++i)t.call(e||this,this[i],i,this)}),Array.prototype.map||(Array.prototype.map=function(t,e){var i,s,n;if(null==this)throw new TypeError(" this is null or not defined");var o=Object(this),a=o.length>>>0;if("function"!=typeof t)throw new TypeError(t+" is not a function");for(e&&(i=e),s=new Array(a),n=0;a>n;){var r,h;n in o&&(r=o[n],h=t.call(i,r,n,o),s[n]=h),n++}return s}),Array.prototype.filter||(Array.prototype.filter=function(t){"use strict";if(null==this)throw new TypeError;var e=Object(this),i=e.length>>>0;if("function"!=typeof t)throw new TypeError;for(var s=[],n=arguments[1],o=0;i>o;o++)if(o in e){var a=e[o];t.call(n,a,o,e)&&s.push(a)}return s}),Object.keys||(Object.keys=function(){var t=Object.prototype.hasOwnProperty,e=!{toString:null}.propertyIsEnumerable("toString"),i=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],s=i.length;return function(n){if("object"!=typeof n&&"function"!=typeof n||null===n)throw new TypeError("Object.keys called on non-object");var o=[];for(var a in n)t.call(n,a)&&o.push(a);if(e)for(var r=0;s>r;r++)t.call(n,i[r])&&o.push(i[r]);return o}}()),Array.isArray||(Array.isArray=function(t){return"[object Array]"===Object.prototype.toString.call(t)}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,s=function(){},n=function(){return i.apply(this instanceof s&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return s.prototype=this.prototype,n.prototype=new s,n}),Object.create||(Object.create=function(t){function e(){}if(arguments.length>1)throw new Error("Object.create implementation only accepts the first parameter.");return e.prototype=t,new e}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,s=function(){},n=function(){return i.apply(this instanceof s&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return s.prototype=this.prototype,n.prototype=new s,n});var util={};util.isNumber=function(t){return t instanceof Number||"number"==typeof t},util.isString=function(t){return t instanceof String||"string"==typeof t},util.isDate=function(t){if(t instanceof Date)return!0;if(util.isString(t)){var e=ASPDateRegex.exec(t);if(e)return!0;if(!isNaN(Date.parse(t)))return!0}return!1},util.isDataTable=function(t){return"undefined"!=typeof google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable},util.randomUUID=function(){var t=function(){return Math.floor(65536*Math.random()).toString(16)};return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()},util.extend=function(t){for(var e=1,i=arguments.length;i>e;e++){var s=arguments[e];for(var n in s)s.hasOwnProperty(n)&&void 0!==s[n]&&(t[n]=s[n])}return t},util.equalArray=function(t,e){if(t.length!=e.length)return!1;for(var i=1,s=t.length;s>i;i++)if(t[i]!=e[i])return!1;return!0},util.convert=function(t,e){var i;if(void 0===t)return void 0;if(null===t)return null;if(!e)return t;if("string"!=typeof e&&!(e instanceof String))throw new Error("Type must be a string");switch(e){case"boolean":case"Boolean":return Boolean(t);case"number":case"Number":return Number(t.valueOf());case"string":case"String":return String(t);case"Date":if(util.isNumber(t))return new Date(t);if(t instanceof Date)return new Date(t.valueOf());if(moment.isMoment(t))return new Date(t.valueOf());if(util.isString(t))return i=ASPDateRegex.exec(t),i?new Date(Number(i[1])):moment(t).toDate();throw new Error("Cannot convert object of type "+util.getType(t)+" to type Date");case"Moment":if(util.isNumber(t))return moment(t);if(t instanceof Date)return moment(t.valueOf());if(moment.isMoment(t))return moment(t);if(util.isString(t))return i=ASPDateRegex.exec(t),moment(i?Number(i[1]):t);throw new Error("Cannot convert object of type "+util.getType(t)+" to type Date");case"ISODate":if(util.isNumber(t))return new Date(t);if(t instanceof Date)return t.toISOString();if(moment.isMoment(t))return t.toDate().toISOString();if(util.isString(t))return i=ASPDateRegex.exec(t),i?new Date(Number(i[1])).toISOString():new Date(t).toISOString();throw new Error("Cannot convert object of type "+util.getType(t)+" to type ISODate");case"ASPDate":if(util.isNumber(t))return"/Date("+t+")/";if(t instanceof Date)return"/Date("+t.valueOf()+")/";if(util.isString(t)){i=ASPDateRegex.exec(t);var s;return s=i?new Date(Number(i[1])).valueOf():new Date(t).valueOf(),"/Date("+s+")/"}throw new Error("Cannot convert object of type "+util.getType(t)+" to type ASPDate");default:throw new Error("Cannot convert object of type "+util.getType(t)+' to type "'+e+'"')}};var ASPDateRegex=/^\/?Date\((\-?\d+)/i;util.getType=function(t){var e=typeof t;return"object"==e?null==t?"null":t instanceof Boolean?"Boolean":t instanceof Number?"Number":t instanceof String?"String":t instanceof Array?"Array":t instanceof Date?"Date":"Object":"number"==e?"Number":"boolean"==e?"Boolean":"string"==e?"String":e},util.getAbsoluteLeft=function(t){for(var e=document.documentElement,i=document.body,s=t.offsetLeft,n=t.offsetParent;null!=n&&n!=i&&n!=e;)s+=n.offsetLeft,s-=n.scrollLeft,n=n.offsetParent;return s},util.getAbsoluteTop=function(t){for(var e=document.documentElement,i=document.body,s=t.offsetTop,n=t.offsetParent;null!=n&&n!=i&&n!=e;)s+=n.offsetTop,s-=n.scrollTop,n=n.offsetParent;return s},util.getPageY=function(t){if("pageY"in t)return t.pageY;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientY:t.clientY;var i=document.documentElement,s=document.body;return e+(i&&i.scrollTop||s&&s.scrollTop||0)-(i&&i.clientTop||s&&s.clientTop||0)},util.getPageX=function(t){if("pageY"in t)return t.pageX;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientX:t.clientX;var i=document.documentElement,s=document.body;return e+(i&&i.scrollLeft||s&&s.scrollLeft||0)-(i&&i.clientLeft||s&&s.clientLeft||0)},util.addClassName=function(t,e){var i=t.className.split(" ");-1==i.indexOf(e)&&(i.push(e),t.className=i.join(" "))},util.removeClassName=function(t,e){var i=t.className.split(" "),s=i.indexOf(e);-1!=s&&(i.splice(s,1),t.className=i.join(" "))},util.forEach=function(t,e){var i,s;if(t instanceof Array)for(i=0,s=t.length;s>i;i++)e(t[i],i,t);else for(i in t)t.hasOwnProperty(i)&&e(t[i],i,t)},util.toArray=function(t){var e=[];for(var i in t)t.hasOwnProperty(i)&&e.push(t[i]);return e},util.updateProperty=function(t,e,i){return t[e]!==i?(t[e]=i,!0):!1},util.addEventListener=function(t,e,i,s){t.addEventListener?(void 0===s&&(s=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.addEventListener(e,i,s)):t.attachEvent("on"+e,i)},util.removeEventListener=function(t,e,i,s){t.removeEventListener?(void 0===s&&(s=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.removeEventListener(e,i,s)):t.detachEvent("on"+e,i)},util.getTarget=function(t){t||(t=window.event);var e;return t.target?e=t.target:t.srcElement&&(e=t.srcElement),void 0!=e.nodeType&&3==e.nodeType&&(e=e.parentNode),e},util.fakeGesture=function(t,e){var i=null,s=Hammer.event.collectEventData(this,i,e);return isNaN(s.center.pageX)&&(s.center.pageX=e.pageX),isNaN(s.center.pageY)&&(s.center.pageY=e.pageY),s},util.option={},util.option.asBoolean=function(t,e){return"function"==typeof t&&(t=t()),null!=t?0!=t:e||null},util.option.asNumber=function(t,e){return"function"==typeof t&&(t=t()),null!=t?Number(t)||e||null:e||null
+},util.option.asString=function(t,e){return"function"==typeof t&&(t=t()),null!=t?String(t):e||null},util.option.asSize=function(t,e){return"function"==typeof t&&(t=t()),util.isString(t)?t:util.isNumber(t)?t+"px":e||null},util.option.asElement=function(t,e){return"function"==typeof t&&(t=t()),t||e||null},util.GiveDec=function GiveDec(Hex){var Value;return Value="A"==Hex?10:"B"==Hex?11:"C"==Hex?12:"D"==Hex?13:"E"==Hex?14:"F"==Hex?15:eval(Hex)},util.GiveHex=function(t){var e;return e=10==t?"A":11==t?"B":12==t?"C":13==t?"D":14==t?"E":15==t?"F":""+t},util.parseColor=function(t){var e;if(util.isString(t))if(util.isValidHex(t)){var i=util.hexToHSV(t),s={h:i.h,s:.45*i.s,v:Math.min(1,1.05*i.v)},n={h:i.h,s:Math.min(1,1.25*i.v),v:.6*i.v},o=util.HSVToHex(n.h,n.h,n.v),a=util.HSVToHex(s.h,s.s,s.v);e={background:t,border:o,highlight:{background:a,border:o}}}else e={background:t,border:t,highlight:{background:t,border:t}};else e={},e.background=t.background||"white",e.border=t.border||e.background,util.isString(t.highlight)?e.highlight={border:t.highlight,background:t.highlight}:(e.highlight={},e.highlight.background=t.highlight&&t.highlight.background||e.background,e.highlight.border=t.highlight&&t.highlight.border||e.border);return e},util.hexToRGB=function(t){t=t.replace("#","").toUpperCase();var e=util.GiveDec(t.substring(0,1)),i=util.GiveDec(t.substring(1,2)),s=util.GiveDec(t.substring(2,3)),n=util.GiveDec(t.substring(3,4)),o=util.GiveDec(t.substring(4,5)),a=util.GiveDec(t.substring(5,6)),r=16*e+i,h=16*s+n,i=16*o+a;return{r:r,g:h,b:i}},util.RGBToHex=function(t,e,i){var s=util.GiveHex(Math.floor(t/16)),n=util.GiveHex(t%16),o=util.GiveHex(Math.floor(e/16)),a=util.GiveHex(e%16),r=util.GiveHex(Math.floor(i/16)),h=util.GiveHex(i%16),d=s+n+o+a+r+h;return"#"+d},util.RGBToHSV=function(t,e,i){t/=255,e/=255,i/=255;var s=Math.min(t,Math.min(e,i)),n=Math.max(t,Math.max(e,i));if(s==n)return{h:0,s:0,v:s};var o=t==s?e-i:i==s?t-e:i-t,a=t==s?3:i==s?1:5,r=60*(a-o/(n-s))/360,h=(n-s)/n,d=n;return{h:r,s:h,v:d}},util.HSVToRGB=function(t,e,i){var s,n,o,a=Math.floor(6*t),r=6*t-a,h=i*(1-e),d=i*(1-r*e),c=i*(1-(1-r)*e);switch(a%6){case 0:s=i,n=c,o=h;break;case 1:s=d,n=i,o=h;break;case 2:s=h,n=i,o=c;break;case 3:s=h,n=d,o=i;break;case 4:s=c,n=h,o=i;break;case 5:s=i,n=h,o=d}return{r:Math.floor(255*s),g:Math.floor(255*n),b:Math.floor(255*o)}},util.HSVToHex=function(t,e,i){var s=util.HSVToRGB(t,e,i);return util.RGBToHex(s.r,s.g,s.b)},util.hexToHSV=function(t){var e=util.hexToRGB(t);return util.RGBToHSV(e.r,e.g,e.b)},util.isValidHex=function(t){var e=/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(t);return e},util.copyObject=function(t,e){for(var i in t)t.hasOwnProperty(i)&&("object"==typeof t[i]?(e[i]={},util.copyObject(t[i],e[i])):e[i]=t[i])},DataSet.prototype.on=function(t,e){var i=this.subscribers[t];i||(i=[],this.subscribers[t]=i),i.push({callback:e})},DataSet.prototype.subscribe=DataSet.prototype.on,DataSet.prototype.off=function(t,e){var i=this.subscribers[t];i&&(this.subscribers[t]=i.filter(function(t){return t.callback!=e}))},DataSet.prototype.unsubscribe=DataSet.prototype.off,DataSet.prototype._trigger=function(t,e,i){if("*"==t)throw new Error("Cannot trigger event *");var s=[];t in this.subscribers&&(s=s.concat(this.subscribers[t])),"*"in this.subscribers&&(s=s.concat(this.subscribers["*"]));for(var n=0;no;o++)i=n._addItem(t[o]),s.push(i);else if(util.isDataTable(t))for(var r=this._getColumnNames(t),h=0,d=t.getNumberOfRows();d>h;h++){for(var c={},l=0,u=r.length;u>l;l++){var p=r[l];c[p]=t.getValue(h,l)}i=n._addItem(c),s.push(i)}else{if(!(t instanceof Object))throw new Error("Unknown dataType");i=n._addItem(t),s.push(i)}return s.length&&this._trigger("add",{items:s},e),s},DataSet.prototype.update=function(t,e){var i=[],s=[],n=this,o=n.fieldId,a=function(t){var e=t[o];n.data[e]?(e=n._updateItem(t),s.push(e)):(e=n._addItem(t),i.push(e))};if(t instanceof Array)for(var r=0,h=t.length;h>r;r++)a(t[r]);else if(util.isDataTable(t))for(var d=this._getColumnNames(t),c=0,l=t.getNumberOfRows();l>c;c++){for(var u={},p=0,g=d.length;g>p;p++){var m=d[p];u[m]=t.getValue(c,p)}a(u)}else{if(!(t instanceof Object))throw new Error("Unknown dataType");a(t)}return i.length&&this._trigger("add",{items:i},e),s.length&&this._trigger("update",{items:s},e),i.concat(s)},DataSet.prototype.get=function(){var t,e,i,s,n=this,o=this.showInternalIds,a=util.getType(arguments[0]);"String"==a||"Number"==a?(t=arguments[0],i=arguments[1],s=arguments[2]):"Array"==a?(e=arguments[0],i=arguments[1],s=arguments[2]):(i=arguments[0],s=arguments[1]);var r;if(i&&i.type){if(r="DataTable"==i.type?"DataTable":"Array",s&&r!=util.getType(s))throw new Error('Type of parameter "data" ('+util.getType(s)+") does not correspond with specified options.type ("+i.type+")");if("DataTable"==r&&!util.isDataTable(s))throw new Error('Parameter "data" must be a DataTable when options.type is "DataTable"')}else r=s&&"DataTable"==util.getType(s)?"DataTable":"Array";void 0!=i&&void 0!=i.showInternalIds&&(this.showInternalIds=i.showInternalIds);var h,d,c,l,u=i&&i.convert||this.options.convert,p=i&&i.filter,g=[];if(void 0!=t)h=n._getItem(t,u),p&&!p(h)&&(h=null);else if(void 0!=e)for(c=0,l=e.length;l>c;c++)h=n._getItem(e[c],u),(!p||p(h))&&g.push(h);else for(d in this.data)this.data.hasOwnProperty(d)&&(h=n._getItem(d,u),(!p||p(h))&&g.push(h));if(this.showInternalIds=o,i&&i.order&&void 0==t&&this._sort(g,i.order),i&&i.fields){var m=i.fields;if(void 0!=t)h=this._filterFields(h,m);else for(c=0,l=g.length;l>c;c++)g[c]=this._filterFields(g[c],m)}if("DataTable"==r){var f=this._getColumnNames(s);if(void 0!=t)n._appendRow(s,f,h);else for(c=0,l=g.length;l>c;c++)n._appendRow(s,f,g[c]);return s}if(void 0!=t)return h;if(s){for(c=0,l=g.length;l>c;c++)s.push(g[c]);return s}return g},DataSet.prototype.getIds=function(t){var e,i,s,n,o,a=this.data,r=t&&t.filter,h=t&&t.order,d=t&&t.convert||this.options.convert,c=[];if(r)if(h){o=[];for(s in a)a.hasOwnProperty(s)&&(n=this._getItem(s,d),r(n)&&o.push(n));for(this._sort(o,h),e=0,i=o.length;i>e;e++)c[e]=o[e][this.fieldId]}else for(s in a)a.hasOwnProperty(s)&&(n=this._getItem(s,d),r(n)&&c.push(n[this.fieldId]));else if(h){o=[];for(s in a)a.hasOwnProperty(s)&&o.push(a[s]);for(this._sort(o,h),e=0,i=o.length;i>e;e++)c[e]=o[e][this.fieldId]}else for(s in a)a.hasOwnProperty(s)&&(n=a[s],c.push(n[this.fieldId]));return c},DataSet.prototype.forEach=function(t,e){var i,s,n=e&&e.filter,o=e&&e.convert||this.options.convert,a=this.data;if(e&&e.order)for(var r=this.get(e),h=0,d=r.length;d>h;h++)i=r[h],s=i[this.fieldId],t(i,s);else for(s in a)a.hasOwnProperty(s)&&(i=this._getItem(s,o),(!n||n(i))&&t(i,s))},DataSet.prototype.map=function(t,e){var i,s=e&&e.filter,n=e&&e.convert||this.options.convert,o=[],a=this.data;for(var r in a)a.hasOwnProperty(r)&&(i=this._getItem(r,n),(!s||s(i))&&o.push(t(i,r)));return e&&e.order&&this._sort(o,e.order),o},DataSet.prototype._filterFields=function(t,e){var i={};for(var s in t)t.hasOwnProperty(s)&&-1!=e.indexOf(s)&&(i[s]=t[s]);return i},DataSet.prototype._sort=function(t,e){if(util.isString(e)){var i=e;t.sort(function(t,e){var s=t[i],n=e[i];return s>n?1:n>s?-1:0})}else{if("function"!=typeof e)throw new TypeError("Order must be a function or a string");t.sort(e)}},DataSet.prototype.remove=function(t,e){var i,s,n,o=[];if(t instanceof Array)for(i=0,s=t.length;s>i;i++)n=this._remove(t[i]),null!=n&&o.push(n);else n=this._remove(t),null!=n&&o.push(n);return o.length&&this._trigger("remove",{items:o},e),o},DataSet.prototype._remove=function(t){if(util.isNumber(t)||util.isString(t)){if(this.data[t])return delete this.data[t],delete this.internalIds[t],t}else if(t instanceof Object){var e=t[this.fieldId];if(e&&this.data[e])return delete this.data[e],delete this.internalIds[e],e}return null},DataSet.prototype.clear=function(t){var e=Object.keys(this.data);return this.data={},this.internalIds={},this._trigger("remove",{items:e},t),e},DataSet.prototype.max=function(t){var e=this.data,i=null,s=null;for(var n in e)if(e.hasOwnProperty(n)){var o=e[n],a=o[t];null!=a&&(!i||a>s)&&(i=o,s=a)}return i},DataSet.prototype.min=function(t){var e=this.data,i=null,s=null;for(var n in e)if(e.hasOwnProperty(n)){var o=e[n],a=o[t];null!=a&&(!i||s>a)&&(i=o,s=a)}return i},DataSet.prototype.distinct=function(t){var e=this.data,i=[],s=this.options.convert[t],n=0;for(var o in e)if(e.hasOwnProperty(o)){for(var a=e[o],r=util.convert(a[t],s),h=!1,d=0;n>d;d++)if(i[d]==r){h=!0;break}h||(i[n]=r,n++)}return i},DataSet.prototype._addItem=function(t){var e=t[this.fieldId];if(void 0!=e){if(this.data[e])throw new Error("Cannot add item: item with id "+e+" already exists")}else e=util.randomUUID(),t[this.fieldId]=e,this.internalIds[e]=t;var i={};for(var s in t)if(t.hasOwnProperty(s)){var n=this.convert[s];i[s]=util.convert(t[s],n)}return this.data[e]=i,e},DataSet.prototype._getItem=function(t,e){var i,s,n=this.data[t];if(!n)return null;var o={},a=this.fieldId,r=this.internalIds;if(e)for(i in n)n.hasOwnProperty(i)&&(s=n[i],i==a&&s in r&&!this.showInternalIds||(o[i]=util.convert(s,e[i])));else for(i in n)n.hasOwnProperty(i)&&(s=n[i],i==a&&s in r&&!this.showInternalIds||(o[i]=s));return o},DataSet.prototype._updateItem=function(t){var e=t[this.fieldId];if(void 0==e)throw new Error("Cannot update item: item has no id (item: "+JSON.stringify(t)+")");var i=this.data[e];if(!i)throw new Error("Cannot update item: no item with id "+e+" found");for(var s in t)if(t.hasOwnProperty(s)){var n=this.convert[s];i[s]=util.convert(t[s],n)}return e},DataSet.prototype.isInternalId=function(t){return t in this.internalIds},DataSet.prototype._getColumnNames=function(t){for(var e=[],i=0,s=t.getNumberOfColumns();s>i;i++)e[i]=t.getColumnId(i)||t.getColumnLabel(i);return e},DataSet.prototype._appendRow=function(t,e,i){for(var s=t.addRow(),n=0,o=e.length;o>n;n++){var a=e[n];t.setValue(s,n,i[a])}},DataView.prototype.setData=function(t){var e,i,s;if(this.data){this.data.unsubscribe&&this.data.unsubscribe("*",this.listener),e=[];for(var n in this.ids)this.ids.hasOwnProperty(n)&&e.push(n);this.ids={},this._trigger("remove",{items:e})}if(this.data=t,this.data){for(this.fieldId=this.options.fieldId||this.data&&this.data.options&&this.data.options.fieldId||"id",e=this.data.getIds({filter:this.options&&this.options.filter}),i=0,s=e.length;s>i;i++)n=e[i],this.ids[n]=!0;this._trigger("add",{items:e}),this.data.on&&this.data.on("*",this.listener)}},DataView.prototype.get=function(){var t,e,i,s=this,n=util.getType(arguments[0]);"String"==n||"Number"==n||"Array"==n?(t=arguments[0],e=arguments[1],i=arguments[2]):(e=arguments[0],i=arguments[1]);var o=util.extend({},this.options,e);this.options.filter&&e&&e.filter&&(o.filter=function(t){return s.options.filter(t)&&e.filter(t)});var a=[];return void 0!=t&&a.push(t),a.push(o),a.push(i),this.data&&this.data.get.apply(this.data,a)},DataView.prototype.getIds=function(t){var e;if(this.data){var i,s=this.options.filter;i=t&&t.filter?s?function(e){return s(e)&&t.filter(e)}:t.filter:s,e=this.data.getIds({filter:i,order:t&&t.order})}else e=[];return e},DataView.prototype._onEvent=function(t,e,i){var s,n,o,a,r=e&&e.items,h=this.data,d=[],c=[],l=[];if(r&&h){switch(t){case"add":for(s=0,n=r.length;n>s;s++)o=r[s],a=this.get(o),a&&(this.ids[o]=!0,d.push(o));break;case"update":for(s=0,n=r.length;n>s;s++)o=r[s],a=this.get(o),a?this.ids[o]?c.push(o):(this.ids[o]=!0,d.push(o)):this.ids[o]&&(delete this.ids[o],l.push(o));break;case"remove":for(s=0,n=r.length;n>s;s++)o=r[s],this.ids[o]&&(delete this.ids[o],l.push(o))}d.length&&this._trigger("add",{items:d},i),c.length&&this._trigger("update",{items:c},i),l.length&&this._trigger("remove",{items:l},i)}},DataView.prototype.on=DataSet.prototype.on,DataView.prototype.off=DataSet.prototype.off,DataView.prototype._trigger=DataSet.prototype._trigger,DataView.prototype.subscribe=DataView.prototype.on,DataView.prototype.unsubscribe=DataView.prototype.off,TimeStep=function(t,e,i){this.current=new Date,this._start=new Date,this._end=new Date,this.autoScale=!0,this.scale=TimeStep.SCALE.DAY,this.step=1,this.setRange(t,e,i)},TimeStep.SCALE={MILLISECOND:1,SECOND:2,MINUTE:3,HOUR:4,DAY:5,WEEKDAY:6,MONTH:7,YEAR:8},TimeStep.prototype.setRange=function(t,e,i){if(!(t instanceof Date&&e instanceof Date))throw"No legal start or end date in method setRange";this._start=void 0!=t?new Date(t.valueOf()):new Date,this._end=void 0!=e?new Date(e.valueOf()):new Date,this.autoScale&&this.setMinimumStep(i)},TimeStep.prototype.first=function(){this.current=new Date(this._start.valueOf()),this.roundToMinor()},TimeStep.prototype.roundToMinor=function(){switch(this.scale){case TimeStep.SCALE.YEAR:this.current.setFullYear(this.step*Math.floor(this.current.getFullYear()/this.step)),this.current.setMonth(0);case TimeStep.SCALE.MONTH:this.current.setDate(1);case TimeStep.SCALE.DAY:case TimeStep.SCALE.WEEKDAY:this.current.setHours(0);case TimeStep.SCALE.HOUR:this.current.setMinutes(0);case TimeStep.SCALE.MINUTE:this.current.setSeconds(0);case TimeStep.SCALE.SECOND:this.current.setMilliseconds(0)}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.setMilliseconds(this.current.getMilliseconds()-this.current.getMilliseconds()%this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()-this.current.getSeconds()%this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()-this.current.getMinutes()%this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()-this.current.getHours()%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()-1-(this.current.getDate()-1)%this.step+1);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()-this.current.getMonth()%this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()-this.current.getFullYear()%this.step)}},TimeStep.prototype.hasNext=function(){return this.current.valueOf()<=this._end.valueOf()},TimeStep.prototype.next=function(){var t=this.current.valueOf();if(this.current.getMonth()<6)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current=new Date(this.current.valueOf()+1e3*this.step);break;case TimeStep.SCALE.MINUTE:this.current=new Date(this.current.valueOf()+1e3*this.step*60);break;case TimeStep.SCALE.HOUR:this.current=new Date(this.current.valueOf()+1e3*this.step*60*60);var e=this.current.getHours();this.current.setHours(e-e%this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step)}else switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current=new Date(this.current.valueOf()+this.step);break;case TimeStep.SCALE.SECOND:this.current.setSeconds(this.current.getSeconds()+this.step);break;case TimeStep.SCALE.MINUTE:this.current.setMinutes(this.current.getMinutes()+this.step);break;case TimeStep.SCALE.HOUR:this.current.setHours(this.current.getHours()+this.step);break;case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:this.current.setDate(this.current.getDate()+this.step);break;case TimeStep.SCALE.MONTH:this.current.setMonth(this.current.getMonth()+this.step);break;case TimeStep.SCALE.YEAR:this.current.setFullYear(this.current.getFullYear()+this.step)}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.getMilliseconds()0&&(this.step=e),this.autoScale=!1},TimeStep.prototype.setAutoScale=function(t){this.autoScale=t},TimeStep.prototype.setMinimumStep=function(t){if(void 0!=t){var e=31104e6,i=2592e6,s=864e5,n=36e5,o=6e4,a=1e3,r=1;1e3*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1e3),500*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=500),100*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=100),50*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=50),10*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=10),5*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=5),e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1),3*i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=3),i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=1),5*s>t&&(this.scale=TimeStep.SCALE.DAY,this.step=5),2*s>t&&(this.scale=TimeStep.SCALE.DAY,this.step=2),s>t&&(this.scale=TimeStep.SCALE.DAY,this.step=1),s/2>t&&(this.scale=TimeStep.SCALE.WEEKDAY,this.step=1),4*n>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=4),n>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=1),15*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=15),10*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=10),5*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=5),o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=1),15*a>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=15),10*a>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=10),5*a>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=5),a>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=1),200*r>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=200),100*r>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=100),50*r>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=50),10*r>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=10),5*r>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=5),r>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=1)}},TimeStep.prototype.snap=function(t){var e=new Date(t.valueOf());if(this.scale==TimeStep.SCALE.YEAR){var i=e.getFullYear()+Math.round(e.getMonth()/12);e.setFullYear(Math.round(i/this.step)*this.step),e.setMonth(0),e.setDate(0),e.setHours(0),e.setMinutes(0),e.setSeconds(0),e.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MONTH)e.getDate()>15?(e.setDate(1),e.setMonth(e.getMonth()+1)):e.setDate(1),e.setHours(0),e.setMinutes(0),e.setSeconds(0),e.setMilliseconds(0);else if(this.scale==TimeStep.SCALE.DAY||this.scale==TimeStep.SCALE.WEEKDAY){switch(this.step){case 5:case 2:e.setHours(24*Math.round(e.getHours()/24));break;default:e.setHours(12*Math.round(e.getHours()/12))}e.setMinutes(0),e.setSeconds(0),e.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.HOUR){switch(this.step){case 4:e.setMinutes(60*Math.round(e.getMinutes()/60));break;default:e.setMinutes(30*Math.round(e.getMinutes()/30))}e.setSeconds(0),e.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MINUTE){switch(this.step){case 15:case 10:e.setMinutes(5*Math.round(e.getMinutes()/5)),e.setSeconds(0);break;case 5:e.setSeconds(60*Math.round(e.getSeconds()/60));break;default:e.setSeconds(30*Math.round(e.getSeconds()/30))}e.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.SECOND)switch(this.step){case 15:case 10:e.setSeconds(5*Math.round(e.getSeconds()/5)),e.setMilliseconds(0);break;case 5:e.setMilliseconds(1e3*Math.round(e.getMilliseconds()/1e3));break;default:e.setMilliseconds(500*Math.round(e.getMilliseconds()/500))}else if(this.scale==TimeStep.SCALE.MILLISECOND){var s=this.step>5?this.step/2:1;e.setMilliseconds(Math.round(e.getMilliseconds()/s)*s)}return e},TimeStep.prototype.isMajor=function(){switch(this.scale){case TimeStep.SCALE.MILLISECOND:return 0==this.current.getMilliseconds();case TimeStep.SCALE.SECOND:return 0==this.current.getSeconds();case TimeStep.SCALE.MINUTE:return 0==this.current.getHours()&&0==this.current.getMinutes();case TimeStep.SCALE.HOUR:return 0==this.current.getHours();case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return 1==this.current.getDate();case TimeStep.SCALE.MONTH:return 0==this.current.getMonth();case TimeStep.SCALE.YEAR:return!1;default:return!1}},TimeStep.prototype.getLabelMinor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return moment(t).format("SSS");case TimeStep.SCALE.SECOND:return moment(t).format("s");case TimeStep.SCALE.MINUTE:return moment(t).format("HH:mm");case TimeStep.SCALE.HOUR:return moment(t).format("HH:mm");case TimeStep.SCALE.WEEKDAY:return moment(t).format("ddd D");case TimeStep.SCALE.DAY:return moment(t).format("D");case TimeStep.SCALE.MONTH:return moment(t).format("MMM");case TimeStep.SCALE.YEAR:return moment(t).format("YYYY");default:return""}},TimeStep.prototype.getLabelMajor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return moment(t).format("HH:mm:ss");case TimeStep.SCALE.SECOND:return moment(t).format("D MMMM HH:mm");case TimeStep.SCALE.MINUTE:case TimeStep.SCALE.HOUR:return moment(t).format("ddd D MMMM");case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return moment(t).format("MMMM YYYY");case TimeStep.SCALE.MONTH:return moment(t).format("YYYY");case TimeStep.SCALE.YEAR:return"";default:return""}},Stack.prototype.setOptions=function(t){util.extend(this.options,t)},Stack.prototype.order=function t(e){var t=this.options.order||this.defaultOptions.order;if("function"!=typeof t)throw new Error("Option order must be a function");e.sort(t)},Stack.prototype.orderByStart=function(t){t.sort(function(t,e){return t.data.start-e.data.start})},Stack.prototype.orderByEnd=function(t){t.sort(function(t,e){var i="end"in t.data?t.data.end:t.data.start,s="end"in e.data?e.data.end:e.data.start;return i-s})},Stack.prototype.stack=function(t,e){var i,s,n,o,a=this.options;if(n=a.margin&&void 0!==a.margin.item?a.margin.item:this.defaultOptions.margin.item,o=a.margin&&void 0!==a.margin.axis?a.margin.axis:this.defaultOptions.margin.axis,e)for(i=0,s=t.length;s>i;i++)t[i].top=null;for(i=0,s=t.length;s>i;i++){var r=t[i];if(null===r.top){r.top=o;do{for(var h=null,d=0,c=t.length;c>d;d++){var l=t[d];if(null!==l.top&&l!==r&&this.collision(r,l,n)){h=l;break}}null!=h&&(r.top=h.top+h.height+n)}while(h)}}},Stack.prototype.collision=function(t,e,i){return t.left-ie.left&&t.top-ie.top},Emitter(Range.prototype),Range.prototype.setOptions=function(t){util.extend(this.options,t),null!==this.start&&null!==this.end&&this.setRange(this.start,this.end)},Range.prototype.setRange=function(t,e){var i=this._applyRange(t,e);if(i){var s={start:new Date(this.start),end:new Date(this.end)};this.emit("rangechange",s),this.emit("rangechanged",s)}},Range.prototype._applyRange=function(t,e){var i,s=null!=t?util.convert(t,"Date").valueOf():this.start,n=null!=e?util.convert(e,"Date").valueOf():this.end,o=null!=this.options.max?util.convert(this.options.max,"Date").valueOf():null,a=null!=this.options.min?util.convert(this.options.min,"Date").valueOf():null;if(isNaN(s)||null===s)throw new Error('Invalid start "'+t+'"');if(isNaN(n)||null===n)throw new Error('Invalid end "'+e+'"');if(s>n&&(n=s),null!==a&&a>s&&(i=a-s,s+=i,n+=i,null!=o&&n>o&&(n=o)),null!==o&&n>o&&(i=n-o,s-=i,n-=i,null!=a&&a>s&&(s=a)),null!==this.options.zoomMin){var r=parseFloat(this.options.zoomMin);0>r&&(r=0),r>n-s&&(this.end-this.start===r?(s=this.start,n=this.end):(i=r-(n-s),s-=i/2,n+=i/2))}if(null!==this.options.zoomMax){var h=parseFloat(this.options.zoomMax);0>h&&(h=0),n-s>h&&(this.end-this.start===h?(s=this.start,n=this.end):(i=n-s-h,s+=i/2,n-=i/2))}var d=this.start!=s||this.end!=n;return this.start=s,this.end=n,d},Range.prototype.getRange=function(){return{start:this.start,end:this.end}},Range.prototype.conversion=function(t){return Range.conversion(this.start,this.end,t)},Range.conversion=function(t,e,i){return 0!=i&&e-t!=0?{offset:t,scale:i/(e-t)}:{offset:0,scale:1}};var touchParams={};Range.prototype._onDragStart=function(){if(!touchParams.ignore){touchParams.start=this.start,touchParams.end=this.end;var t=this.parent.frame;t&&(t.style.cursor="move")}},Range.prototype._onDrag=function(t){var e=this.options.direction;if(validateDirection(e),!touchParams.ignore){var i="horizontal"==e?t.gesture.deltaX:t.gesture.deltaY,s=touchParams.end-touchParams.start,n="horizontal"==e?this.parent.width:this.parent.height,o=-i/n*s;this._applyRange(touchParams.start+o,touchParams.end+o),this.emit("rangechange",{start:new Date(this.start),end:new Date(this.end)})}},Range.prototype._onDragEnd=function(){touchParams.ignore||(this.parent.frame&&(this.parent.frame.style.cursor="auto"),this.emit("rangechanged",{start:new Date(this.start),end:new Date(this.end)}))},Range.prototype._onMouseWheel=function(t){var e=0;if(t.wheelDelta?e=t.wheelDelta/120:t.detail&&(e=-t.detail/3),e){var i;i=0>e?1-e/5:1/(1+e/5);var s=util.fakeGesture(this,t),n=getPointer(s.center,this.parent.frame),o=this._pointerToDate(n);this.zoom(i,o)}t.preventDefault()},Range.prototype._onTouch=function(t){touchParams.start=this.start,touchParams.end=this.end,touchParams.ignore=!1,touchParams.center=null;var e=ItemSet.itemFromTarget(t);e&&e.selected&&this.options.editable&&(touchParams.ignore=!0)},Range.prototype._onHold=function(){touchParams.ignore=!0},Range.prototype._onPinch=function(t){this.options.direction;if(touchParams.ignore=!0,t.gesture.touches.length>1){touchParams.center||(touchParams.center=getPointer(t.gesture.center,this.parent.frame));var e=1/t.gesture.scale,i=this._pointerToDate(touchParams.center),s=getPointer(t.gesture.center,this.parent.frame),n=(this._pointerToDate(this.parent,s),parseInt(i+(touchParams.start-i)*e)),o=parseInt(i+(touchParams.end-i)*e);this.setRange(n,o)}},Range.prototype._pointerToDate=function(t){var e,i=this.options.direction;if(validateDirection(i),"horizontal"==i){var s=this.parent.width;return e=this.conversion(s),t.x/e.scale+e.offset}var n=this.parent.height;return e=this.conversion(n),t.y/e.scale+e.offset},Range.prototype.zoom=function(t,e){null==e&&(e=(this.start+this.end)/2);var i=e+(this.start-e)*t,s=e+(this.end-e)*t;this.setRange(i,s)},Range.prototype.move=function(t){var e=this.end-this.start,i=this.start+e*t,s=this.end+e*t;this.start=i,this.end=s},Range.prototype.moveTo=function(t){var e=(this.start+this.end)/2,i=e-t,s=this.start-i,n=this.end-i;this.setRange(s,n)},Emitter(Component.prototype),Component.prototype.setOptions=function(t){t&&(util.extend(this.options,t),this.repaint())},Component.prototype.getOption=function(t){var e;return this.options&&(e=this.options[t]),void 0===e&&this.defaultOptions&&(e=this.defaultOptions[t]),e},Component.prototype.getFrame=function(){return null},Component.prototype.repaint=function(){return!1},Component.prototype._isResized=function(){var t=this._previousWidth!==this.width||this._previousHeight!==this.height;return this._previousWidth=this.width,this._previousHeight=this.height,t},Panel.prototype=new Component,Panel.prototype.setOptions=Component.prototype.setOptions,Panel.prototype.getFrame=function(){return this.frame},Panel.prototype.appendChild=function(t){this.childs.push(t),t.parent=this;var e=t.getFrame();e&&(e.parentNode&&e.parentNode.removeChild(e),this.frame.appendChild(e))},Panel.prototype.insertBefore=function(t,e){var i=this.childs.indexOf(e);if(-1!=i){this.childs.splice(i,0,t),t.parent=this;var s=t.getFrame();if(s){s.parentNode&&s.parentNode.removeChild(s);var n=e.getFrame();n?this.frame.insertBefore(s,n):this.frame.appendChild(s)}}},Panel.prototype.removeChild=function(t){var e=this.childs.indexOf(t);if(-1!=e){this.childs.splice(e,1),t.parent=null;var i=t.getFrame();i&&i.parentNode&&this.frame.removeChild(i)}},Panel.prototype.hasChild=function(t){var e=this.childs.indexOf(t);return-1!=e},Panel.prototype.repaint=function(){var t=util.option.asString,e=this.options,i=this.getFrame();i.className="vpanel"+(e.className?" "+t(e.className):"");var s=this._repaintChilds();return this._updateSize(),this._isResized()||s},Panel.prototype._repaintChilds=function(){for(var t=!1,e=0,i=this.childs.length;i>e;e++)t=this.childs[e].repaint()||t;return t},Panel.prototype._updateSize=function(){this.frame.style.top=util.option.asSize(this.options.top),this.frame.style.bottom=util.option.asSize(this.options.bottom),this.frame.style.left=util.option.asSize(this.options.left),this.frame.style.right=util.option.asSize(this.options.right),this.frame.style.width=util.option.asSize(this.options.width,"100%"),this.frame.style.height=util.option.asSize(this.options.height,""),this.top=this.frame.offsetTop,this.left=this.frame.offsetLeft,this.width=this.frame.offsetWidth,this.height=this.frame.offsetHeight},RootPanel.prototype=new Panel,RootPanel.prototype._create=function(){this.frame=document.createElement("div"),this.hammer=Hammer(this.frame,{prevent_default:!0}),this.listeners={};var t=this,e=["touch","pinch","tap","doubletap","hold","dragstart","drag","dragend","mousewheel","DOMMouseScroll"];e.forEach(function(e){var i=function(){var i=[e].concat(Array.prototype.slice.call(arguments,0));t.emit.apply(t,i)};t.hammer.on(e,i),t.listeners[e]=i})},RootPanel.prototype.setOptions=function(t){t&&(util.extend(this.options,t),this.repaint(),this._initWatch())},RootPanel.prototype.getFrame=function(){return this.frame},RootPanel.prototype.repaint=function(){var t=this.options,e="vis timeline rootpanel "+t.orientation+(t.editable?" editable":"");t.className&&(e+=" "+util.option.asString(e)),this.frame.className=e;var i=this._repaintChilds();this.frame.style.maxHeight=util.option.asSize(this.options.maxHeight,""),this._updateSize();var s=this._isResized()||i;s&&setTimeout(this.repaint.bind(this),0)},RootPanel.prototype._initWatch=function(){var t=this.getOption("autoResize");t?this._watch():this._unwatch()},RootPanel.prototype._watch=function(){var t=this;this._unwatch();var e=function(){var e=t.getOption("autoResize");return e?void(t.frame&&(t.frame.clientWidth!=t.lastWidth||t.frame.clientHeight!=t.lastHeight)&&(t.lastWidth=t.frame.clientWidth,t.lastHeight=t.frame.clientHeight,t.repaint())):void t._unwatch()};util.addEventListener(window,"resize",e),this.watchTimer=setInterval(e,1e3)},RootPanel.prototype._unwatch=function(){this.watchTimer&&(clearInterval(this.watchTimer),this.watchTimer=void 0)},TimeAxis.prototype=new Component,TimeAxis.prototype.setOptions=Component.prototype.setOptions,TimeAxis.prototype._create=function(){this.frame=document.createElement("div")},TimeAxis.prototype.setRange=function(t){if(!(t instanceof Range||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},TimeAxis.prototype.getFrame=function(){return this.frame},TimeAxis.prototype.repaint=function(){var t=util.option.asSize,e=this.options,i=this.props,s=this.frame;s.className="timeaxis";var n=s.parentNode;if(n){this._calculateCharSize();var o=this.getOption("orientation"),a=this.getOption("showMinorLabels"),r=this.getOption("showMajorLabels"),h=this.parent.height;i.minorLabelHeight=a?i.minorCharHeight:0,i.majorLabelHeight=r?i.majorCharHeight:0,this.height=i.minorLabelHeight+i.majorLabelHeight,this.width=s.offsetWidth,i.minorLineHeight=h+i.minorLabelHeight,i.minorLineWidth=1,i.majorLineHeight=h+this.height,i.majorLineWidth=1;var d=s.nextSibling;n.removeChild(s),"top"==o?(s.style.top="0",s.style.left="0",s.style.bottom="",s.style.width=t(e.width,"100%"),s.style.height=this.height+"px"):(s.style.top="",s.style.bottom="0",s.style.left="0",s.style.width=t(e.width,"100%"),s.style.height=this.height+"px"),this._repaintLabels(),this._repaintLine(),d?n.insertBefore(s,d):n.appendChild(s)
+}return this._isResized()},TimeAxis.prototype._repaintLabels=function(){var t=this.getOption("orientation"),e=util.convert(this.range.start,"Number"),i=util.convert(this.range.end,"Number"),s=this.options.toTime(5*(this.props.minorCharWidth||10)).valueOf()-this.options.toTime(0).valueOf(),n=new TimeStep(new Date(e),new Date(i),s);this.step=n;var o=this.dom;o.redundant.majorLines=o.majorLines,o.redundant.majorTexts=o.majorTexts,o.redundant.minorLines=o.minorLines,o.redundant.minorTexts=o.minorTexts,o.majorLines=[],o.majorTexts=[],o.minorLines=[],o.minorTexts=[],n.first();for(var a=void 0,r=0;n.hasNext()&&1e3>r;){r++;var h=n.getCurrent(),d=this.options.toScreen(h),c=n.isMajor();this.getOption("showMinorLabels")&&this._repaintMinorText(d,n.getLabelMinor(),t),c&&this.getOption("showMajorLabels")?(d>0&&(void 0==a&&(a=d),this._repaintMajorText(d,n.getLabelMajor(),t)),this._repaintMajorLine(d,t)):this._repaintMinorLine(d,t),n.next()}if(this.getOption("showMajorLabels")){var l=this.options.toTime(0),u=n.getLabelMajor(l),p=u.length*(this.props.majorCharWidth||10)+10;(void 0==a||a>p)&&this._repaintMajorText(0,u,t)}util.forEach(this.dom.redundant,function(t){for(;t.length;){var e=t.pop();e&&e.parentNode&&e.parentNode.removeChild(e)}})},TimeAxis.prototype._repaintMinorText=function(t,e,i){var s=this.dom.redundant.minorTexts.shift();if(!s){var n=document.createTextNode("");s=document.createElement("div"),s.appendChild(n),s.className="text minor",this.frame.appendChild(s)}this.dom.minorTexts.push(s),s.childNodes[0].nodeValue=e,"top"==i?(s.style.top=this.props.majorLabelHeight+"px",s.style.bottom=""):(s.style.top="",s.style.bottom=this.props.majorLabelHeight+"px"),s.style.left=t+"px"},TimeAxis.prototype._repaintMajorText=function(t,e,i){var s=this.dom.redundant.majorTexts.shift();if(!s){var n=document.createTextNode(e);s=document.createElement("div"),s.className="text major",s.appendChild(n),this.frame.appendChild(s)}this.dom.majorTexts.push(s),s.childNodes[0].nodeValue=e,"top"==i?(s.style.top="0px",s.style.bottom=""):(s.style.top="",s.style.bottom="0px"),s.style.left=t+"px"},TimeAxis.prototype._repaintMinorLine=function(t,e){var i=this.dom.redundant.minorLines.shift();i||(i=document.createElement("div"),i.className="grid vertical minor",this.frame.appendChild(i)),this.dom.minorLines.push(i);var s=this.props;"top"==e?(i.style.top=this.props.majorLabelHeight+"px",i.style.bottom=""):(i.style.top="",i.style.bottom=this.props.majorLabelHeight+"px"),i.style.height=s.minorLineHeight+"px",i.style.left=t-s.minorLineWidth/2+"px"},TimeAxis.prototype._repaintMajorLine=function(t,e){var i=this.dom.redundant.majorLines.shift();i||(i=document.createElement("DIV"),i.className="grid vertical major",this.frame.appendChild(i)),this.dom.majorLines.push(i);var s=this.props;"top"==e?(i.style.top="0px",i.style.bottom=""):(i.style.top="",i.style.bottom="0px"),i.style.left=t-s.majorLineWidth/2+"px",i.style.height=s.majorLineHeight+"px"},TimeAxis.prototype._repaintLine=function(){var t=this.dom.line,e=this.frame,i=this.getOption("orientation");this.getOption("showMinorLabels")||this.getOption("showMajorLabels")?(t?(e.removeChild(t),e.appendChild(t)):(t=document.createElement("div"),t.className="grid horizontal major",e.appendChild(t),this.dom.line=t),"top"==i?(t.style.top=this.height+"px",t.style.bottom=""):(t.style.top="",t.style.bottom=this.height+"px")):t&&t.parentNode&&(t.parentNode.removeChild(t),delete this.dom.line)},TimeAxis.prototype._calculateCharSize=function(){if(!("minorCharHeight"in this.props)){var t=document.createTextNode("0"),e=document.createElement("DIV");e.className="text minor measure",e.appendChild(t),this.frame.appendChild(e),this.props.minorCharHeight=e.clientHeight,this.props.minorCharWidth=e.clientWidth,this.frame.removeChild(e)}if(!("majorCharHeight"in this.props)){var i=document.createTextNode("0"),s=document.createElement("DIV");s.className="text major measure",s.appendChild(i),this.frame.appendChild(s),this.props.majorCharHeight=s.clientHeight,this.props.majorCharWidth=s.clientWidth,this.frame.removeChild(s)}},TimeAxis.prototype.snap=function(t){return this.step.snap(t)},CurrentTime.prototype=new Component,CurrentTime.prototype.setOptions=Component.prototype.setOptions,CurrentTime.prototype._create=function(){var t=document.createElement("div");t.className="currenttime",t.style.position="absolute",t.style.top="0px",t.style.height="100%",this.bar=t},CurrentTime.prototype.getFrame=function(){return this.bar},CurrentTime.prototype.repaint=function(){var t=(this.parent,new Date),e=this.options.toScreen(t);return this.bar.style.left=e+"px",this.bar.title="Current time: "+t,!1},CurrentTime.prototype.start=function(){function t(){e.stop();var i=e.range.conversion(e.parent.width).scale,s=1/i/10;30>s&&(s=30),s>1e3&&(s=1e3),e.repaint(),e.currentTimeTimer=setTimeout(t,s)}var e=this;t()},CurrentTime.prototype.stop=function(){void 0!==this.currentTimeTimer&&(clearTimeout(this.currentTimeTimer),delete this.currentTimeTimer)},CustomTime.prototype=new Component,CustomTime.prototype.setOptions=Component.prototype.setOptions,CustomTime.prototype._create=function(){var t=document.createElement("div");t.className="customtime",t.style.position="absolute",t.style.top="0px",t.style.height="100%",this.bar=t;var e=document.createElement("div");e.style.position="relative",e.style.top="0px",e.style.left="-10px",e.style.height="100%",e.style.width="20px",t.appendChild(e),this.hammer=Hammer(t,{prevent_default:!0}),this.hammer.on("dragstart",this._onDragStart.bind(this)),this.hammer.on("drag",this._onDrag.bind(this)),this.hammer.on("dragend",this._onDragEnd.bind(this))},CustomTime.prototype.getFrame=function(){return this.bar},CustomTime.prototype.repaint=function(){var t=this.options.toScreen(this.customTime);return this.bar.style.left=t+"px",this.bar.title="Time: "+this.customTime,!1},CustomTime.prototype.setCustomTime=function(t){this.customTime=new Date(t.valueOf()),this.repaint()},CustomTime.prototype.getCustomTime=function(){return new Date(this.customTime.valueOf())},CustomTime.prototype._onDragStart=function(t){this.eventParams.dragging=!0,this.eventParams.customTime=this.customTime,t.stopPropagation(),t.preventDefault()},CustomTime.prototype._onDrag=function(t){if(this.eventParams.dragging){var e=t.gesture.deltaX,i=this.options.toScreen(this.eventParams.customTime)+e,s=this.options.toTime(i);this.setCustomTime(s),this.emit("timechange",{time:new Date(this.customTime.valueOf())}),t.stopPropagation(),t.preventDefault()}},CustomTime.prototype._onDragEnd=function(t){this.eventParams.dragging&&(this.emit("timechanged",{time:new Date(this.customTime.valueOf())}),t.stopPropagation(),t.preventDefault())},ItemSet.prototype=new Panel,ItemSet.types={box:ItemBox,range:ItemRange,rangeoverflow:ItemRangeOverflow,point:ItemPoint},ItemSet.prototype._create=function(){var t=document.createElement("div");t["timeline-itemset"]=this,this.frame=t;var e=document.createElement("div");e.className="background",this.backgroundPanel.frame.appendChild(e),this.dom.background=e;var i=document.createElement("div");i.className="foreground",t.appendChild(i),this.dom.foreground=i;var s=document.createElement("div");s.className="axis",this.dom.axis=s,this.axisPanel.frame.appendChild(s),this.hammer=Hammer(t,{prevent_default:!0}),this.hammer.on("dragstart",this._onDragStart.bind(this)),this.hammer.on("drag",this._onDrag.bind(this)),this.hammer.on("dragend",this._onDragEnd.bind(this))},ItemSet.prototype.setOptions=Component.prototype.setOptions,ItemSet.prototype.hide=function(){this.dom.axis.parentNode&&this.dom.axis.parentNode.removeChild(this.dom.axis),this.dom.background.parentNode&&this.dom.background.parentNode.removeChild(this.dom.background)},ItemSet.prototype.show=function(){this.dom.axis.parentNode||this.axisPanel.frame.appendChild(this.dom.axis),this.dom.background.parentNode||this.backgroundPanel.frame.appendChild(this.dom.background)},ItemSet.prototype.setRange=function(t){if(!(t instanceof Range||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},ItemSet.prototype.setSelection=function(t){var e,i,s,n;if(t){if(!Array.isArray(t))throw new TypeError("Array expected");for(e=0,i=this.selection.length;i>e;e++)s=this.selection[e],n=this.items[s],n&&n.unselect();for(this.selection=[],e=0,i=t.length;i>e;e++)s=t[e],n=this.items[s],n&&(this.selection.push(s),n.select())}},ItemSet.prototype.getSelection=function(){return this.selection.concat([])},ItemSet.prototype._deselect=function(t){for(var e=this.selection,i=0,s=e.length;s>i;i++)if(e[i]==t){e.splice(i,1);break}},ItemSet.prototype.getFrame=function(){return this.frame},ItemSet.prototype.repaint=function(){var t=util.option.asSize,e=util.option.asString,i=this.options,s=this.getOption("orientation"),n=this.frame;n.className="itemset"+(i.className?" "+e(i.className):"");var o=this.range.end-this.range.start,a=o!=this.lastVisibleInterval||this.width!=this.lastWidth;this.lastVisibleInterval=o,this.lastWidth=this.width,this.visibleItems=[];for(var r in this.items)if(this.items.hasOwnProperty(r)){var h=this.items[r];h.isVisible(this.range)?(h.displayed||h.show(),h.repositionX(),this.visibleItems.push(h)):h.displayed&&h.hide()}var d=this.stackDirty||a;this.stack.stack(this.visibleItems,d),this.stackDirty=!1;for(var c=0,l=this.visibleItems.length;l>c;c++)this.visibleItems[c].repositionY();var u,p=i.margin&&"axis"in i.margin?i.margin.axis:this.itemOptions.margin.axis,g=i.margin&&"item"in i.margin?i.margin.item:this.itemOptions.margin.item,m=this.visibleItems;if(m.length){var f=m[0].top,v=m[0].top+m[0].height;util.forEach(m,function(t){f=Math.min(f,t.top),v=Math.max(v,t.top+t.height)}),u=v-f+p+g}else u=p+g;return n.style.left=t(i.left,""),n.style.right=t(i.right,""),n.style.top=t("top"==s?"0":""),n.style.bottom=t("top"==s?"":"0"),n.style.width=t(i.width,"100%"),n.style.height=t(u),this.top=n.offsetTop,this.left=n.offsetLeft,this.width=n.offsetWidth,this.height=u,this.dom.axis.style.left=t(i.left,"0"),this.dom.axis.style.right=t(i.right,""),this.dom.axis.style.width=t(i.width,"100%"),this.dom.axis.style.height=t(0),this.dom.axis.style.top=t("top"==s?"0":""),this.dom.axis.style.bottom=t("top"==s?"":"0"),this._isResized()},ItemSet.prototype.getForeground=function(){return this.dom.foreground},ItemSet.prototype.getBackground=function(){return this.dom.background},ItemSet.prototype.getAxis=function(){return this.dom.axis},ItemSet.prototype.setItems=function(t){var e,i=this,s=this.itemsData;if(t){if(!(t instanceof DataSet||t instanceof DataView))throw new TypeError("Data must be an instance of DataSet");this.itemsData=t}else this.itemsData=null;if(s&&(util.forEach(this.listeners,function(t,e){s.unsubscribe(e,t)}),e=s.getIds(),this._onRemove(e)),this.itemsData){var n=this.id;util.forEach(this.listeners,function(t,e){i.itemsData.on(e,t,n)}),e=this.itemsData.getIds(),this._onAdd(e)}},ItemSet.prototype.getItems=function(){return this.itemsData},ItemSet.prototype.removeItem=function(t){var e=this.itemsData.get(t),i=this._myDataSet();e&&this.options.onRemove(e,function(e){e&&i.remove(t)})},ItemSet.prototype._onUpdate=function(t){var e=this,i=this.items,s=this.itemOptions;t.forEach(function(t){var n=e.itemsData.get(t),o=i[t],a=n.type||n.start&&n.end&&"range"||e.options.type||"box",r=ItemSet.types[a];if(o&&(r&&o instanceof r?o.data=n:(o.hide(),o=null)),!o){if(!r)throw new TypeError('Unknown item type "'+a+'"');o=new r(e,n,e.options,s),o.id=t}e.items[t]=o}),this._order(),this.stackDirty=!0,this.emit("change")},ItemSet.prototype._onAdd=ItemSet.prototype._onUpdate,ItemSet.prototype._onRemove=function(t){var e=0,i=this;t.forEach(function(t){var s=i.items[t];if(s){e++,s.hide(),delete i.items[t],delete i.visibleItems[t];var n=i.selection.indexOf(t);-1!=n&&i.selection.splice(n,1)}}),e&&(this._order(),this.stackDirty=!0,this.emit("change"))},ItemSet.prototype._order=function(){var t=util.toArray(this.items);this.orderedItems.byStart=t,this.orderedItems.byEnd=[].concat(t),this.stack.orderByStart(this.orderedItems.byStart),this.stack.orderByEnd(this.orderedItems.byEnd)},ItemSet.prototype._onDragStart=function(t){if(this.options.editable){var e=ItemSet.itemFromTarget(t),i=this;if(e&&e.selected){var s=t.target.dragLeftItem,n=t.target.dragRightItem;this.touchParams.itemProps=s?[{item:s,start:e.data.start.valueOf()}]:n?[{item:n,end:e.data.end.valueOf()}]:this.getSelection().map(function(t){var e=i.items[t],s={item:e};return"start"in e.data&&(s.start=e.data.start.valueOf()),"end"in e.data&&(s.end=e.data.end.valueOf()),s}),t.stopPropagation()}}},ItemSet.prototype._onDrag=function(t){if(this.touchParams.itemProps){var e=this.options.snap||null,i=t.gesture.deltaX,s=this.width/(this.range.end-this.range.start),n=i/s;this.touchParams.itemProps.forEach(function(t){if("start"in t){var i=new Date(t.start+n);t.item.data.start=e?e(i):i}if("end"in t){var s=new Date(t.end+n);t.item.data.end=e?e(s):s}}),this.stackDirty=!0,this.emit("change"),t.stopPropagation()}},ItemSet.prototype._onDragEnd=function(t){if(this.touchParams.itemProps){var e=[],i=this,s=this._myDataSet();this.touchParams.itemProps.forEach(function(t){var n=t.item.id,o=i.itemsData.get(n),a=!1;"start"in t.item.data&&(a=t.start!=t.item.data.start.valueOf(),o.start=util.convert(t.item.data.start,s.convert.start)),"end"in t.item.data&&(a=a||t.end!=t.item.data.end.valueOf(),o.end=util.convert(t.item.data.end,s.convert.end)),a&&i.options.onMove(o,function(i){i?(i[s.fieldId]=n,e.push(i)):("start"in t&&(t.item.data.start=t.start),"end"in t&&(t.item.data.end=t.end),this.stackDirty=!0,this.emit("change"))})}),this.touchParams.itemProps=null,e.length&&s.update(e),t.stopPropagation()}},ItemSet.itemFromTarget=function(t){for(var e=t.target;e;){if(e.hasOwnProperty("timeline-item"))return e["timeline-item"];e=e.parentNode}return null},ItemSet.itemSetFromTarget=function(t){for(var e=t.target;e;){if(e.hasOwnProperty("timeline-itemset"))return e["timeline-itemset"];e=e.parentNode}return null},ItemSet.prototype._myDataSet=function(){for(var t=this.itemsData;t instanceof DataView;)t=t.data;return t},Item.prototype.select=function(){this.selected=!0,this.displayed&&this.repaint()},Item.prototype.unselect=function(){this.selected=!1,this.displayed&&this.repaint()},Item.prototype.show=function(){return!1},Item.prototype.hide=function(){return!1},Item.prototype.repaint=function(){},Item.prototype.repositionX=function(){},Item.prototype.repositionY=function(){},Item.prototype._repaintDeleteButton=function(t){if(this.selected&&this.options.editable&&!this.dom.deleteButton){var e=this.parent,i=this.id,s=document.createElement("div");s.className="delete",s.title="Delete this item",Hammer(s,{preventDefault:!0}).on("tap",function(t){e.removeItem(i),t.stopPropagation()}),t.appendChild(s),this.dom.deleteButton=s}else!this.selected&&this.dom.deleteButton&&(this.dom.deleteButton.parentNode&&this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton),this.dom.deleteButton=null)},ItemBox.prototype=new Item(null,null),ItemBox.prototype.isVisible=function(t){var e=(t.end-t.start)/4;return this.data.start>t.start-e&&this.data.startt.start-e&&this.data.startt.start},ItemRange.prototype.repaint=function(){var t=this.dom;if(t||(this.dom={},t=this.dom,t.box=document.createElement("div"),t.content=document.createElement("div"),t.content.className="content",t.box.appendChild(t.content),t.box["timeline-item"]=this),!this.parent)throw new Error("Cannot repaint item: no parent attached");if(!t.box.parentNode){var e=this.parent.getForeground();if(!e)throw new Error("Cannot repaint time axis: parent has no foreground container element");e.appendChild(t.box)}if(this.displayed=!0,this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)t.content.innerHTML="",t.content.appendChild(this.content);else{if(void 0==this.data.content)throw new Error('Property "content" missing in item '+this.data.id);t.content.innerHTML=this.content}this.dirty=!0}var i=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=i&&(this.className=i,t.box.className=this.baseClassName+i,this.dirty=!0),this.dirty&&(this.props.content.width=this.dom.content.offsetWidth,this.height=this.dom.box.offsetHeight,this.dirty=!1),this._repaintDeleteButton(t.box),this._repaintDragLeft(),this._repaintDragRight()},ItemRange.prototype.show=function(){this.displayed||this.repaint()},ItemRange.prototype.hide=function(){if(this.displayed){var t=this.dom.box;t.parentNode&&t.parentNode.removeChild(t),this.top=null,this.left=null,this.displayed=!1}},ItemRange.prototype.repositionX=function(){var t,e=this.props,i=this.parent.width,s=this.defaultOptions.toScreen(this.data.start),n=this.defaultOptions.toScreen(this.data.end),o="padding"in this.options?this.options.padding:this.defaultOptions.padding;-i>s&&(s=-i),n>2*i&&(n=2*i),t=0>s?Math.min(-s,n-s-e.content.width-2*o):0,this.left=s,this.width=Math.max(n-s,1),this.dom.box.style.left=this.left+"px",this.dom.box.style.width=this.width+"px",this.dom.content.style.left=t+"px"},ItemRange.prototype.repositionY=function(){var t=this.options.orientation||this.defaultOptions.orientation,e=this.dom.box;"top"==t?(e.style.top=this.top+"px",e.style.bottom=""):(e.style.top="",e.style.bottom=this.top+"px")},ItemRange.prototype._repaintDragLeft=function(){if(this.selected&&this.options.editable&&!this.dom.dragLeft){var t=document.createElement("div");t.className="drag-left",t.dragLeftItem=this,Hammer(t,{preventDefault:!0}).on("drag",function(){}),this.dom.box.appendChild(t),this.dom.dragLeft=t}else!this.selected&&this.dom.dragLeft&&(this.dom.dragLeft.parentNode&&this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft),this.dom.dragLeft=null)},ItemRange.prototype._repaintDragRight=function(){if(this.selected&&this.options.editable&&!this.dom.dragRight){var t=document.createElement("div");t.className="drag-right",t.dragRightItem=this,Hammer(t,{preventDefault:!0}).on("drag",function(){}),this.dom.box.appendChild(t),this.dom.dragRight=t}else!this.selected&&this.dom.dragRight&&(this.dom.dragRight.parentNode&&this.dom.dragRight.parentNode.removeChild(this.dom.dragRight),this.dom.dragRight=null)},ItemRangeOverflow.prototype=new ItemRange(null,null),ItemRangeOverflow.prototype.baseClassName="item rangeoverflow",ItemRangeOverflow.prototype.repositionX=function(){{var t,e=this.parent.width,i=this.defaultOptions.toScreen(this.data.start),s=this.defaultOptions.toScreen(this.data.end);"padding"in this.options?this.options.padding:this.defaultOptions.padding}-e>i&&(i=-e),s>2*e&&(s=2*e),t=Math.max(-i,0),this.left=i;var n=Math.max(s-i,1);this.width=this.props.content.width=a&&(a=864e5),n=new Date(n.valueOf()-.05*a),o=new Date(o.valueOf()+.05*a)}void 0!=this.options.start&&(n=util.convert(this.options.start,"Date")),void 0!=this.options.end&&(o=util.convert(this.options.end,"Date")),(null!=n||null!=o)&&this.range.setRange(n,o)}},Timeline.prototype.setGroups=function(t){var e=this;this.groupsData=t;var i=util.extend(Object.create(this.options),{top:null,bottom:null,right:null,left:null,width:null,height:null});this.groupsData?(this.itemSet&&(this.itemSet.hide(),this.contentPanel.removeChild(this.itemSet),this.itemSet.setItems(),this.itemSet=null),this.groupSet?this.groupSet.setGroups(this.groupsData):(this.groupSet=new GroupSet(this.contentPanel,this.sideContentPanel,this.backgroundPanel,this.axisPanel,i),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))):(this.groupSet&&(this.groupSet.hide(),this.groupSet.setItems(),this.contentPanel.removeChild(this.groupSet),this.groupSet=null),this.itemSet=new ItemSet(this.backgroundPanel,this.axisPanel,i),this.itemSet.setRange(this.range),this.itemSet.setItems(this.itemsData),this.itemSet.on("change",e.rootPanel.repaint.bind(e.rootPanel)),this.contentPanel.appendChild(this.itemSet))},Timeline.prototype.getItemRange=function(){var t=this.itemsData,e=null,i=null;if(t){var s=t.min("start");e=s?s.start.valueOf():null;var n=t.max("start");n&&(i=n.start.valueOf());var o=t.max("end");o&&(i=null==i?o.end.valueOf():Math.max(i,o.end.valueOf()))}return{min:null!=e?new Date(e):null,max:null!=i?new Date(i):null}},Timeline.prototype.setSelection=function(t){var e=this.itemSet||this.groupSet;e&&e.setSelection(t)},Timeline.prototype.getSelection=function(){var t=this.itemSet||this.groupSet;return t?t.getSelection():[]},Timeline.prototype.setWindow=function(t,e){this.range.setRange(t,e)},Timeline.prototype.getWindow=function(){var t=this.range.getRange();return{start:new Date(t.start),end:new Date(t.end)}},Timeline.prototype._onSelectItem=function(t){if(this.options.selectable){var e=t.gesture.srcEvent&&t.gesture.srcEvent.ctrlKey,i=t.gesture.srcEvent&&t.gesture.srcEvent.shiftKey;if(e||i)return void this._onMultiSelectItem(t);var s=this.getSelection(),n=ItemSet.itemFromTarget(t),o=n?[n.id]:[];this.setSelection(o);var a=this.getSelection();util.equalArray(s,a)||this.emit("select",{items:this.getSelection()}),t.stopPropagation()}},Timeline.prototype._onAddItem=function(t){if(this.options.selectable&&this.options.editable){var e=this,i=ItemSet.itemFromTarget(t);if(i){var s=e.itemsData.get(i.id);this.options.onUpdate(s,function(t){t&&e.itemsData.update(t)})}else{var n=vis.util.getAbsoluteLeft(this.rootPanel.frame),o=t.gesture.center.pageX-n,a={start:this.timeAxis.snap(this._toTime(o)),content:"new item"},r=util.randomUUID();a[this.itemsData.fieldId]=r;var h=GroupSet.groupFromTarget(t);h&&(a.group=h.groupId),this.options.onAdd(a,function(t){t&&e.itemsData.add(a)})}}},Timeline.prototype._onMultiSelectItem=function(t){if(this.options.selectable){var e,i=ItemSet.itemFromTarget(t);if(i){e=this.getSelection();var s=e.indexOf(i.id);-1==s?e.push(i.id):e.splice(s,1),this.setSelection(e),this.emit("select",{items:this.getSelection()}),t.stopPropagation()}}},Timeline.prototype._toTime=function(t){var e=this.range.conversion(this.mainPanel.width);return new Date(t/e.scale+e.offset)},Timeline.prototype._toScreen=function(t){var e=this.range.conversion(this.mainPanel.width);return(t.valueOf()-e.offset)*e.scale},function(t){function e(t){return D=t,u()}function i(){C=0,I=D.charAt(0)}function s(){C++,I=D.charAt(C)}function n(){return D.charAt(C+1)}function o(t){return O.test(t)}function a(t,e){if(t||(t={}),e)for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}function r(t,e,i){for(var s=e.split("."),n=t;s.length;){var o=s.shift();s.length?(n[o]||(n[o]={}),n=n[o]):n[o]=i}}function h(t,e){for(var i,s,n=null,o=[t],r=t;r.parent;)o.push(r.parent),r=r.parent;if(r.nodes)for(i=0,s=r.nodes.length;s>i;i++)if(e.id===r.nodes[i].id){n=r.nodes[i];break}for(n||(n={id:e.id},t.node&&(n.attr=a(n.attr,t.node))),i=o.length-1;i>=0;i--){var h=o[i];h.nodes||(h.nodes=[]),-1==h.nodes.indexOf(n)&&h.nodes.push(n)}e.attr&&(n.attr=a(n.attr,e.attr))}function d(t,e){if(t.edges||(t.edges=[]),t.edges.push(e),t.edge){var i=a({},t.edge);e.attr=a(i,e.attr)}}function c(t,e,i,s,n){var o={from:e,to:i,type:s};return t.edge&&(o.attr=a({},t.edge)),o.attr=a(o.attr||{},n),o}function l(){for(N=E.NULL,M="";" "==I||" "==I||"\n"==I||"\r"==I;)s();do{var t=!1;if("#"==I){for(var e=C-1;" "==D.charAt(e)||" "==D.charAt(e);)e--;if("\n"==D.charAt(e)||""==D.charAt(e)){for(;""!=I&&"\n"!=I;)s();t=!0}}if("/"==I&&"/"==n()){for(;""!=I&&"\n"!=I;)s();t=!0}if("/"==I&&"*"==n()){for(;""!=I;){if("*"==I&&"/"==n()){s(),s();break}s()}t=!0}for(;" "==I||" "==I||"\n"==I||"\r"==I;)s()}while(t);if(""==I)return void(N=E.DELIMITER);var i=I+n();if(T[i])return N=E.DELIMITER,M=i,s(),void s();if(T[I])return N=E.DELIMITER,M=I,void s();if(o(I)||"-"==I){for(M+=I,s();o(I);)M+=I,s();return"false"==M?M=!1:"true"==M?M=!0:isNaN(Number(M))||(M=Number(M)),void(N=E.IDENTIFIER)}if('"'==I){for(s();""!=I&&('"'!=I||'"'==I&&'"'==n());)M+=I,'"'==I&&s(),s();if('"'!=I)throw b('End of string " expected');return s(),void(N=E.IDENTIFIER)}for(N=E.UNKNOWN;""!=I;)M+=I,s();throw new SyntaxError('Syntax error in part "'+S(M,30)+'"')}function u(){var t={};if(i(),l(),"strict"==M&&(t.strict=!0,l()),("graph"==M||"digraph"==M)&&(t.type=M,l()),N==E.IDENTIFIER&&(t.id=M,l()),"{"!=M)throw b("Angle bracket { expected");if(l(),p(t),"}"!=M)throw b("Angle bracket } expected");if(l(),""!==M)throw b("End of file expected");return l(),delete t.node,delete t.edge,delete t.graph,t}function p(t){for(;""!==M&&"}"!=M;)g(t),";"==M&&l()}function g(t){var e=m(t);if(e)return void y(t,e);var i=f(t);if(!i){if(N!=E.IDENTIFIER)throw b("Identifier expected");var s=M;if(l(),"="==M){if(l(),N!=E.IDENTIFIER)throw b("Identifier expected");t[s]=M,l()}else v(t,s)}}function m(t){var e=null;if("subgraph"==M&&(e={},e.type="subgraph",l(),N==E.IDENTIFIER&&(e.id=M,l())),"{"==M){if(l(),e||(e={}),e.parent=t,e.node=t.node,e.edge=t.edge,e.graph=t.graph,p(e),"}"!=M)throw b("Angle bracket } expected");l(),delete e.node,delete e.edge,delete e.graph,delete e.parent,t.subgraphs||(t.subgraphs=[]),t.subgraphs.push(e)}return e}function f(t){return"node"==M?(l(),t.node=_(),"node"):"edge"==M?(l(),t.edge=_(),"edge"):"graph"==M?(l(),t.graph=_(),"graph"):null}function v(t,e){var i={id:e},s=_();s&&(i.attr=s),h(t,i),y(t,e)}function y(t,e){for(;"->"==M||"--"==M;){var i,s=M;l();var n=m(t);if(n)i=n;else{if(N!=E.IDENTIFIER)throw b("Identifier or subgraph expected");i=M,h(t,{id:i}),l()}var o=_(),a=c(t,e,i,s,o);d(t,a),e=i}}function _(){for(var t=null;"["==M;){for(l(),t={};""!==M&&"]"!=M;){if(N!=E.IDENTIFIER)throw b("Attribute name expected");var e=M;if(l(),"="!=M)throw b("Equal sign = expected");if(l(),N!=E.IDENTIFIER)throw b("Attribute value expected");var i=M;r(t,e,i),l(),","==M&&l()}if("]"!=M)throw b("Bracket ] expected");l()}return t}function b(t){return new SyntaxError(t+', got "'+S(M,30)+'" (char '+C+")")}function S(t,e){return t.length<=e?t:t.substr(0,27)+"..."}function w(t,e,i){t instanceof Array?t.forEach(function(t){e instanceof Array?e.forEach(function(e){i(t,e)}):i(t,e)}):e instanceof Array?e.forEach(function(e){i(t,e)}):i(t,e)}function x(t){function i(t){var e={from:t.from,to:t.to};return a(e,t.attr),e.style="->"==t.type?"arrow":"line",e}var s=e(t),n={nodes:[],edges:[],options:{}};return s.nodes&&s.nodes.forEach(function(t){var e={id:t.id,label:String(t.label||t.id)};a(e,t.attr),e.image&&(e.shape="image"),n.nodes.push(e)}),s.edges&&s.edges.forEach(function(t){var e,s;e=t.from instanceof Object?t.from.nodes:{id:t.from},s=t.to instanceof Object?t.to.nodes:{id:t.to},t.from instanceof Object&&t.from.edges&&t.from.edges.forEach(function(t){var e=i(t);n.edges.push(e)}),w(e,s,function(e,s){var o=c(n,e.id,s.id,t.type,t.attr),a=i(o);n.edges.push(a)}),t.to instanceof Object&&t.to.edges&&t.to.edges.forEach(function(t){var e=i(t);n.edges.push(e)})}),s.attr&&(n.options=s.attr),n}var E={NULL:0,DELIMITER:1,IDENTIFIER:2,UNKNOWN:3},T={"{":!0,"}":!0,"[":!0,"]":!0,";":!0,"=":!0,",":!0,"->":!0,"--":!0},D="",C=0,I="",M="",N=E.NULL,O=/[a-zA-Z_0-9.:#]/;t.parseDOT=e,t.DOTToGraph=x}("undefined"!=typeof util?util:exports),"undefined"!=typeof CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.circle=function(t,e,i){this.beginPath(),this.arc(t,e,i,0,2*Math.PI,!1)},CanvasRenderingContext2D.prototype.square=function(t,e,i){this.beginPath(),this.rect(t-i,e-i,2*i,2*i)},CanvasRenderingContext2D.prototype.triangle=function(t,e,i){this.beginPath();var s=2*i,n=s/2,o=Math.sqrt(3)/6*s,a=Math.sqrt(s*s-n*n);this.moveTo(t,e-(a-o)),this.lineTo(t+n,e+o),this.lineTo(t-n,e+o),this.lineTo(t,e-(a-o)),this.closePath()},CanvasRenderingContext2D.prototype.triangleDown=function(t,e,i){this.beginPath();var s=2*i,n=s/2,o=Math.sqrt(3)/6*s,a=Math.sqrt(s*s-n*n);this.moveTo(t,e+(a-o)),this.lineTo(t+n,e-o),this.lineTo(t-n,e-o),this.lineTo(t,e+(a-o)),this.closePath()},CanvasRenderingContext2D.prototype.star=function(t,e,i){this.beginPath();for(var s=0;10>s;s++){var n=s%2===0?1.3*i:.5*i;this.lineTo(t+n*Math.sin(2*s*Math.PI/10),e-n*Math.cos(2*s*Math.PI/10))}this.closePath()},CanvasRenderingContext2D.prototype.roundRect=function(t,e,i,s,n){var o=Math.PI/180;0>i-2*n&&(n=i/2),0>s-2*n&&(n=s/2),this.beginPath(),this.moveTo(t+n,e),this.lineTo(t+i-n,e),this.arc(t+i-n,e+n,n,270*o,360*o,!1),this.lineTo(t+i,e+s-n),this.arc(t+i-n,e+s-n,n,0,90*o,!1),this.lineTo(t+n,e+s),this.arc(t+n,e+s-n,n,90*o,180*o,!1),this.lineTo(t,e+n),this.arc(t+n,e+n,n,180*o,270*o,!1)},CanvasRenderingContext2D.prototype.ellipse=function(t,e,i,s){var n=.5522848,o=i/2*n,a=s/2*n,r=t+i,h=e+s,d=t+i/2,c=e+s/2;this.beginPath(),this.moveTo(t,c),this.bezierCurveTo(t,c-a,d-o,e,d,e),this.bezierCurveTo(d+o,e,r,c-a,r,c),this.bezierCurveTo(r,c+a,d+o,h,d,h),this.bezierCurveTo(d-o,h,t,c+a,t,c)},CanvasRenderingContext2D.prototype.database=function(t,e,i,s){var n=1/3,o=i,a=s*n,r=.5522848,h=o/2*r,d=a/2*r,c=t+o,l=e+a,u=t+o/2,p=e+a/2,g=e+(s-a/2),m=e+s;this.beginPath(),this.moveTo(c,p),this.bezierCurveTo(c,p+d,u+h,l,u,l),this.bezierCurveTo(u-h,l,t,p+d,t,p),this.bezierCurveTo(t,p-d,u-h,e,u,e),this.bezierCurveTo(u+h,e,c,p-d,c,p),this.lineTo(c,g),this.bezierCurveTo(c,g+d,u+h,m,u,m),this.bezierCurveTo(u-h,m,t,g+d,t,g),this.lineTo(t,p)},CanvasRenderingContext2D.prototype.arrow=function(t,e,i,s){var n=t-s*Math.cos(i),o=e-s*Math.sin(i),a=t-.9*s*Math.cos(i),r=e-.9*s*Math.sin(i),h=n+s/3*Math.cos(i+.5*Math.PI),d=o+s/3*Math.sin(i+.5*Math.PI),c=n+s/3*Math.cos(i-.5*Math.PI),l=o+s/3*Math.sin(i-.5*Math.PI);this.beginPath(),this.moveTo(t,e),this.lineTo(h,d),this.lineTo(a,r),this.lineTo(c,l),this.closePath()},CanvasRenderingContext2D.prototype.dashedLine=function(t,e,i,s,n){n||(n=[10,5]),0==u&&(u=.001);var o=n.length;this.moveTo(t,e);for(var a=i-t,r=s-e,h=r/a,d=Math.sqrt(a*a+r*r),c=0,l=!0;d>=.1;){var u=n[c++%o];u>d&&(u=d);var p=Math.sqrt(u*u/(1+h*h));0>a&&(p=-p),t+=p,e+=h*p,this[l?"lineTo":"moveTo"](t,e),d-=u,l=!l}}),Node.prototype.resetCluster=function(){this.formationScale=void 0,this.clusterSize=1,this.containedNodes={},this.containedEdges={},this.clusterSessions=[]},Node.prototype.attachEdge=function(t){-1==this.edges.indexOf(t)&&this.edges.push(t),-1==this.dynamicEdges.indexOf(t)&&this.dynamicEdges.push(t),this.dynamicEdgesLength=this.dynamicEdges.length},Node.prototype.detachEdge=function(t){var e=this.edges.indexOf(t);-1!=e&&(this.edges.splice(e,1),this.dynamicEdges.splice(e,1)),this.dynamicEdgesLength=this.dynamicEdges.length},Node.prototype.setProperties=function(t,e){if(t){if(this.originalLabel=void 0,void 0!==t.id&&(this.id=t.id),void 0!==t.label&&(this.label=t.label,this.originalLabel=t.label),void 0!==t.title&&(this.title=t.title),void 0!==t.group&&(this.group=t.group),void 0!==t.x&&(this.x=t.x),void 0!==t.y&&(this.y=t.y),void 0!==t.value&&(this.value=t.value),void 0!==t.level&&(this.level=t.level,this.preassignedLevel=!0),void 0!==t.mass&&(this.mass=t.mass),void 0!==t.horizontalAlignLeft&&(this.horizontalAlignLeft=t.horizontalAlignLeft),void 0!==t.verticalAlignTop&&(this.verticalAlignTop=t.verticalAlignTop),void 0!==t.triggerFunction&&(this.triggerFunction=t.triggerFunction),void 0===this.id)throw"Node must have an id";if(this.group){var i=this.grouplist.get(this.group);for(var s in i)i.hasOwnProperty(s)&&(this[s]=i[s])}if(void 0!==t.shape&&(this.shape=t.shape),void 0!==t.image&&(this.image=t.image),void 0!==t.radius&&(this.radius=t.radius),void 0!==t.color&&(this.color=util.parseColor(t.color)),void 0!==t.fontColor&&(this.fontColor=t.fontColor),void 0!==t.fontSize&&(this.fontSize=t.fontSize),void 0!==t.fontFace&&(this.fontFace=t.fontFace),void 0!==this.image&&""!=this.image){if(!this.imagelist)throw"No imagelist provided";this.imageObj=this.imagelist.load(this.image)}switch(this.xFixed=this.xFixed||void 0!==t.x&&!t.allowedToMoveX,this.yFixed=this.yFixed||void 0!==t.y&&!t.allowedToMoveY,this.radiusFixed=this.radiusFixed||void 0!==t.radius,"image"==this.shape&&(this.radiusMin=e.nodes.widthMin,this.radiusMax=e.nodes.widthMax),this.shape){case"database":this.draw=this._drawDatabase,this.resize=this._resizeDatabase;break;case"box":this.draw=this._drawBox,this.resize=this._resizeBox;break;case"circle":this.draw=this._drawCircle,this.resize=this._resizeCircle;break;case"ellipse":this.draw=this._drawEllipse,this.resize=this._resizeEllipse;break;case"image":this.draw=this._drawImage,this.resize=this._resizeImage;break;case"text":this.draw=this._drawText,this.resize=this._resizeText;break;case"dot":this.draw=this._drawDot,this.resize=this._resizeShape;break;case"square":this.draw=this._drawSquare,this.resize=this._resizeShape;break;case"triangle":this.draw=this._drawTriangle,this.resize=this._resizeShape;break;case"triangleDown":this.draw=this._drawTriangleDown,this.resize=this._resizeShape;break;case"star":this.draw=this._drawStar,this.resize=this._resizeShape;break;default:this.draw=this._drawEllipse,this.resize=this._resizeEllipse}this._reset()}},Node.prototype.select=function(){this.selected=!0,this._reset()},Node.prototype.unselect=function(){this.selected=!1,this._reset()},Node.prototype.clearSizeCache=function(){this._reset()},Node.prototype._reset=function(){this.width=void 0,this.height=void 0},Node.prototype.getTitle=function(){return"function"==typeof this.title?this.title():this.title},Node.prototype.distanceToBorder=function(t,e){var i=1;switch(this.width||this.resize(t),this.shape){case"circle":case"dot":return this.radius+i;case"ellipse":var s=this.width/2,n=this.height/2,o=Math.sin(e)*s,a=Math.cos(e)*n;return s*n/Math.sqrt(o*o+a*a);case"box":case"image":case"text":default:return this.width?Math.min(Math.abs(this.width/2/Math.cos(e)),Math.abs(this.height/2/Math.sin(e)))+i:0}},Node.prototype._setForce=function(t,e){this.fx=t,this.fy=e},Node.prototype._addForce=function(t,e){this.fx+=t,this.fy+=e},Node.prototype.discreteStep=function(t){if(!this.xFixed){var e=this.damping*this.vx,i=(this.fx-e)/this.mass;this.vx+=i*t,this.x+=this.vx*t}if(!this.yFixed){var s=this.damping*this.vy,n=(this.fy-s)/this.mass;this.vy+=n*t,this.y+=this.vy*t}},Node.prototype.discreteStepLimited=function(t,e){if(this.xFixed)this.fx=0;else{var i=this.damping*this.vx,s=(this.fx-i)/this.mass;this.vx+=s*t,this.vx=Math.abs(this.vx)>e?this.vx>0?e:-e:this.vx,this.x+=this.vx*t}if(this.yFixed)this.fy=0;else{var n=this.damping*this.vy,o=(this.fy-n)/this.mass;this.vy+=o*t,this.vy=Math.abs(this.vy)>e?this.vy>0?e:-e:this.vy,this.y+=this.vy*t}},Node.prototype.isFixed=function(){return this.xFixed&&this.yFixed},Node.prototype.isMoving=function(t){return Math.abs(this.vx)>t||Math.abs(this.vy)>t},Node.prototype.isSelected=function(){return this.selected},Node.prototype.getValue=function(){return this.value},Node.prototype.getDistance=function(t,e){var i=this.x-t,s=this.y-e;return Math.sqrt(i*i+s*s)},Node.prototype.setValueRange=function(t,e){if(!this.radiusFixed&&void 0!==this.value)if(e==t)this.radius=(this.radiusMin+this.radiusMax)/2;else{var i=(this.radiusMax-this.radiusMin)/(e-t);this.radius=(this.value-t)*i+this.radiusMin}this.baseRadiusValue=this.radius},Node.prototype.draw=function(){throw"Draw method not initialized for node"},Node.prototype.resize=function(){throw"Resize method not initialized for node"},Node.prototype.isOverlappingWith=function(t){return this.leftt.left&&this.topt.top},Node.prototype._resizeImage=function(){if(!this.width||!this.height){var t,e;if(this.value){this.radius=this.baseRadiusValue;var i=this.imageObj.height/this.imageObj.width;void 0!==i?(t=this.radius||this.imageObj.width,e=this.radius*i||this.imageObj.height):(t=0,e=0)}else t=this.imageObj.width,e=this.imageObj.height;this.width=t,this.height=e,this.growthIndicator=0,this.width>0&&this.height>0&&(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-t)}},Node.prototype._drawImage=function(t){this._resizeImage(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e;if(0!=this.imageObj.width){if(this.clusterSize>1){var i=this.clusterSize>1?10:0;i*=this.graphScaleInv,i=Math.min(.2*this.width,i),t.globalAlpha=.5,t.drawImage(this.imageObj,this.left-i,this.top-i,this.width+2*i,this.height+2*i)}t.globalAlpha=1,t.drawImage(this.imageObj,this.left,this.top,this.width,this.height),e=this.y+this.height/2}else e=this.y;this._label(t,this.label,this.x,e,void 0,"top")},Node.prototype._resizeBox=function(t){if(!this.width){var e=5,i=this.getTextSize(t);this.width=i.width+2*e,this.height=i.height+2*e,this.width+=.5*Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeWidthFactor,this.height+=.5*Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeHeightFactor,this.growthIndicator=this.width-(i.width+2*e)}},Node.prototype._drawBox=function(t){this._resizeBox(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e=2.5,i=2;t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.roundRect(this.left-2*t.lineWidth,this.top-2*t.lineWidth,this.width+4*t.lineWidth,this.height+4*t.lineWidth,this.radius),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.roundRect(this.left,this.top,this.width,this.height,this.radius),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},Node.prototype._resizeDatabase=function(t){if(!this.width){var e=5,i=this.getTextSize(t),s=i.width+2*e;this.width=s,this.height=s,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-s}},Node.prototype._drawDatabase=function(t){this._resizeDatabase(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e=2.5,i=2;t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.database(this.x-this.width/2-2*t.lineWidth,this.y-.5*this.height-2*t.lineWidth,this.width+4*t.lineWidth,this.height+4*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.database(this.x-this.width/2,this.y-.5*this.height,this.width,this.height),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},Node.prototype._resizeCircle=function(t){if(!this.width){var e=5,i=this.getTextSize(t),s=Math.max(i.width,i.height)+2*e;this.radius=s/2,this.width=s,this.height=s,this.radius+=.5*Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeRadiusFactor,this.growthIndicator=this.radius-.5*s}},Node.prototype._drawCircle=function(t){this._resizeCircle(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e=2.5,i=2;t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.circle(this.x,this.y,this.radius+2*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.circle(this.x,this.y,this.radius),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},Node.prototype._resizeEllipse=function(t){if(!this.width){var e=this.getTextSize(t);this.width=1.5*e.width,this.height=2*e.height,this.width1&&(t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.ellipse(this.left-2*t.lineWidth,this.top-2*t.lineWidth,this.width+4*t.lineWidth,this.height+4*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?i:1)+(this.clusterSize>1?e:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.ellipse(this.left,this.top,this.width,this.height),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},Node.prototype._drawDot=function(t){this._drawShape(t,"circle")},Node.prototype._drawTriangle=function(t){this._drawShape(t,"triangle")},Node.prototype._drawTriangleDown=function(t){this._drawShape(t,"triangleDown")},Node.prototype._drawSquare=function(t){this._drawShape(t,"square")},Node.prototype._drawStar=function(t){this._drawShape(t,"star")},Node.prototype._resizeShape=function(){if(!this.width){this.radius=this.baseRadiusValue;var t=2*this.radius;this.width=t,this.height=t,this.width+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeWidthFactor,this.height+=Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeHeightFactor,this.radius+=.5*Math.min(this.clusterSize-1,this.maxNodeSizeIncrements)*this.clusterSizeRadiusFactor,this.growthIndicator=this.width-t}},Node.prototype._drawShape=function(t,e){this._resizeShape(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var i=2.5,s=2,n=2;switch(e){case"dot":n=2;break;case"square":n=2;break;case"triangle":n=3;break;case"triangleDown":n=3;break;case"star":n=4}t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,this.clusterSize>1&&(t.lineWidth=(this.selected?s:1)+(this.clusterSize>1?i:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t[e](this.x,this.y,this.radius+n*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?s:1)+(this.clusterSize>1?i:0),t.lineWidth*=this.graphScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t[e](this.x,this.y,this.radius),t.fill(),t.stroke(),this.label&&this._label(t,this.label,this.x,this.y+this.height/2,void 0,"top")},Node.prototype._resizeText=function(t){if(!this.width){var e=5,i=this.getTextSize(t);this.width=i.width+2*e,this.height=i.height+2*e,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-(i.width+2*e)}},Node.prototype._drawText=function(t){this._resizeText(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,this._label(t,this.label,this.x,this.y)},Node.prototype._label=function(t,e,i,s,n,o){if(e&&this.fontSize*this.graphScale>this.fontDrawThreshold){t.font=(this.selected?"bold ":"")+this.fontSize+"px "+this.fontFace,t.fillStyle=this.fontColor||"black",t.textAlign=n||"center",t.textBaseline=o||"middle";for(var a=e.split("\n"),r=a.length,h=this.fontSize+4,d=s+(1-r)/2*h,c=0;r>c;c++)t.fillText(a[c],i,d),d+=h}},Node.prototype.getTextSize=function(t){if(void 0!==this.label){t.font=(this.selected?"bold ":"")+this.fontSize+"px "+this.fontFace;for(var e=this.label.split("\n"),i=(this.fontSize+4)*e.length,s=0,n=0,o=e.length;o>n;n++)s=Math.max(s,t.measureText(e[n]).width);return{width:s,height:i}}return{width:0,height:0}},Node.prototype.inArea=function(){return void 0!==this.width?this.x+this.width*this.graphScaleInv>=this.canvasTopLeft.x&&this.x-this.width*this.graphScaleInv=this.canvasTopLeft.y&&this.y-this.height*this.graphScaleInv=this.canvasTopLeft.x&&this.x=this.canvasTopLeft.y&&this.yh}return!1},Edge.prototype._drawLine=function(t){if(t.strokeStyle=1==this.selected?this.color.highlight:this.color.color,t.lineWidth=this._getLineWidth(),this.from!=this.to){this._line(t);var e;if(this.label){if(1==this.smooth){var i=.5*(.5*(this.from.x+this.via.x)+.5*(this.to.x+this.via.x)),s=.5*(.5*(this.from.y+this.via.y)+.5*(this.to.y+this.via.y));e={x:i,y:s}}else e=this._pointOnLine(.5);this._label(t,this.label,e.x,e.y)}}else{var n,o,a=this.length/4,r=this.from;r.width||r.resize(t),r.width>r.height?(n=r.x+r.width/2,o=r.y-a):(n=r.x+a,o=r.y-r.height/2),this._circle(t,n,o,a),e=this._pointOnCircle(n,o,a,.5),this._label(t,this.label,e.x,e.y)}},Edge.prototype._getLineWidth=function(){return 1==this.selected?Math.min(2*this.width,this.widthMax)*this.graphScaleInv:this.width*this.graphScaleInv},Edge.prototype._line=function(t){t.beginPath(),t.moveTo(this.from.x,this.from.y),1==this.smooth?t.quadraticCurveTo(this.via.x,this.via.y,this.to.x,this.to.y):t.lineTo(this.to.x,this.to.y),t.stroke()},Edge.prototype._circle=function(t,e,i,s){t.beginPath(),t.arc(e,i,s,0,2*Math.PI,!1),t.stroke()},Edge.prototype._label=function(t,e,i,s){if(e){t.font=(this.from.selected||this.to.selected?"bold ":"")+this.fontSize+"px "+this.fontFace,t.fillStyle=this.fontFill;var n=t.measureText(e).width,o=this.fontSize,a=i-n/2,r=s-o/2;t.fillRect(a,r,n,o),t.fillStyle=this.fontColor||"black",t.textAlign="left",t.textBaseline="top",t.fillText(e,a,r)}},Edge.prototype._drawDashLine=function(t){if(t.strokeStyle=1==this.selected?this.color.highlight:this.color.color,t.lineWidth=this._getLineWidth(),void 0!==t.mozDash||void 0!==t.setLineDash){t.beginPath(),t.moveTo(this.from.x,this.from.y);var e=[0];e=void 0!==this.dash.length&&void 0!==this.dash.gap?[this.dash.length,this.dash.gap]:[5,5],"undefined"!=typeof t.setLineDash?(t.setLineDash(e),t.lineDashOffset=0):(t.mozDash=e,t.mozDashOffset=0),1==this.smooth?t.quadraticCurveTo(this.via.x,this.via.y,this.to.x,this.to.y):t.lineTo(this.to.x,this.to.y),t.stroke(),"undefined"!=typeof t.setLineDash?(t.setLineDash([0]),t.lineDashOffset=0):(t.mozDash=[0],t.mozDashOffset=0)}else t.beginPath(),t.lineCap="round",void 0!==this.dash.altLength?t.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]):void 0!==this.dash.length&&void 0!==this.dash.gap?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dash.length,this.dash.gap]):(t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y)),t.stroke();if(this.label){var i;if(1==this.smooth){var s=.5*(.5*(this.from.x+this.via.x)+.5*(this.to.x+this.via.x)),n=.5*(.5*(this.from.y+this.via.y)+.5*(this.to.y+this.via.y));
+i={x:s,y:n}}else i=this._pointOnLine(.5);this._label(t,this.label,i.x,i.y)}},Edge.prototype._pointOnLine=function(t){return{x:(1-t)*this.from.x+t*this.to.x,y:(1-t)*this.from.y+t*this.to.y}},Edge.prototype._pointOnCircle=function(t,e,i,s){var n=2*(s-3/8)*Math.PI;return{x:t+i*Math.cos(n),y:e-i*Math.sin(n)}},Edge.prototype._drawArrowCenter=function(t){var e;if(1==this.selected?(t.strokeStyle=this.color.highlight,t.fillStyle=this.color.highlight):(t.strokeStyle=this.color.color,t.fillStyle=this.color.color),t.lineWidth=this._getLineWidth(),this.from!=this.to){this._line(t);var i=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x),s=10+5*this.width;if(1==this.smooth){var n=.5*(.5*(this.from.x+this.via.x)+.5*(this.to.x+this.via.x)),o=.5*(.5*(this.from.y+this.via.y)+.5*(this.to.y+this.via.y));e={x:n,y:o}}else e=this._pointOnLine(.5);t.arrow(e.x,e.y,i,s),t.fill(),t.stroke(),this.label&&this._label(t,this.label,e.x,e.y)}else{var a,r,h=.25*Math.max(100,this.length),d=this.from;d.width||d.resize(t),d.width>d.height?(a=d.x+.5*d.width,r=d.y-h):(a=d.x+h,r=d.y-.5*d.height),this._circle(t,a,r,h);var i=.2*Math.PI,s=10+5*this.width;e=this._pointOnCircle(a,r,h,.5),t.arrow(e.x,e.y,i,s),t.fill(),t.stroke(),this.label&&(e=this._pointOnCircle(a,r,h,.5),this._label(t,this.label,e.x,e.y))}},Edge.prototype._drawArrow=function(t){1==this.selected?(t.strokeStyle=this.color.highlight,t.fillStyle=this.color.highlight):(t.strokeStyle=this.color.color,t.fillStyle=this.color.color),t.lineWidth=this._getLineWidth();var e,i;if(this.from!=this.to){e=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x);var s=this.to.x-this.from.x,n=this.to.y-this.from.y,o=Math.sqrt(s*s+n*n),a=this.from.distanceToBorder(t,e+Math.PI),r=(o-a)/o,h=r*this.from.x+(1-r)*this.to.x,d=r*this.from.y+(1-r)*this.to.y;1==this.smooth&&(e=Math.atan2(this.to.y-this.via.y,this.to.x-this.via.x),s=this.to.x-this.via.x,n=this.to.y-this.via.y,o=Math.sqrt(s*s+n*n));var c,l,u=this.to.distanceToBorder(t,e),p=(o-u)/o;if(1==this.smooth?(c=(1-p)*this.via.x+p*this.to.x,l=(1-p)*this.via.y+p*this.to.y):(c=(1-p)*this.from.x+p*this.to.x,l=(1-p)*this.from.y+p*this.to.y),t.beginPath(),t.moveTo(h,d),1==this.smooth?t.quadraticCurveTo(this.via.x,this.via.y,c,l):t.lineTo(c,l),t.stroke(),i=10+5*this.width,t.arrow(c,l,e,i),t.fill(),t.stroke(),this.label){var g;if(1==this.smooth){var m=.5*(.5*(this.from.x+this.via.x)+.5*(this.to.x+this.via.x)),f=.5*(.5*(this.from.y+this.via.y)+.5*(this.to.y+this.via.y));g={x:m,y:f}}else g=this._pointOnLine(.5);this._label(t,this.label,g.x,g.y)}}else{var v,y,_,b=this.from,S=.25*Math.max(100,this.length);b.width||b.resize(t),b.width>b.height?(v=b.x+.5*b.width,y=b.y-S,_={x:v,y:b.y,angle:.9*Math.PI}):(v=b.x+S,y=b.y-.5*b.height,_={x:b.x,y:y,angle:.6*Math.PI}),t.beginPath(),t.arc(v,y,S,0,2*Math.PI,!1),t.stroke(),i=10+5*this.width,t.arrow(_.x,_.y,_.angle,i),t.fill(),t.stroke(),this.label&&(g=this._pointOnCircle(v,y,S,.5),this._label(t,this.label,g.x,g.y))}},Edge.prototype._getDistanceToEdge=function(t,e,i,s,n,o){if(1==this.smooth){var a,r,h,d,c,l,u=1e9;for(a=0;10>a;a++)r=.1*a,h=Math.pow(1-r,2)*t+2*r*(1-r)*this.via.x+Math.pow(r,2)*i,d=Math.pow(1-r,2)*e+2*r*(1-r)*this.via.y+Math.pow(r,2)*s,c=Math.abs(n-h),l=Math.abs(o-d),u=Math.min(u,Math.sqrt(c*c+l*l));return u}var p=i-t,g=s-e,m=p*p+g*g,f=((n-t)*p+(o-e)*g)/m;f>1?f=1:0>f&&(f=0);var h=t+f*p,d=e+f*g,c=h-n,l=d-o;return Math.sqrt(c*c+l*l)},Edge.prototype.setScale=function(t){this.graphScaleInv=1/t},Edge.prototype.select=function(){this.selected=!0},Edge.prototype.unselect=function(){this.selected=!1},Edge.prototype.positionBezierNode=function(){null!==this.via&&(this.via.x=.5*(this.from.x+this.to.x),this.via.y=.5*(this.from.y+this.to.y))},Popup.prototype.setPosition=function(t,e){this.x=parseInt(t),this.y=parseInt(e)},Popup.prototype.setText=function(t){this.frame.innerHTML=t},Popup.prototype.show=function(t){if(void 0===t&&(t=!0),t){var e=this.frame.clientHeight,i=this.frame.clientWidth,s=this.frame.parentNode.clientHeight,n=this.frame.parentNode.clientWidth,o=this.y-e;o+e+this.padding>s&&(o=s-e-this.padding),on&&(a=n-i-this.padding),athis.constants.clustering.clusterThreshold&&1==this.constants.clustering.enabled&&this.clusterToFit(this.constants.clustering.reduceToNodes,!1),this._calculateForces())},_calculateForces:function(){this._calculateGravitationalForces(),this._calculateNodeForces(),1==this.constants.smoothCurves?this._calculateSpringForcesWithSupport():this._calculateSpringForces()},_updateCalculationNodes:function(){if(1==this.constants.smoothCurves){this.calculationNodes={},this.calculationNodeIndices=[];for(var t in this.nodes)this.nodes.hasOwnProperty(t)&&(this.calculationNodes[t]=this.nodes[t]);var e=this.sectors.support.nodes;for(var i in e)e.hasOwnProperty(i)&&(this.edges.hasOwnProperty(e[i].parentEdgeId)?this.calculationNodes[i]=e[i]:e[i]._setForce(0,0));for(var s in this.calculationNodes)this.calculationNodes.hasOwnProperty(s)&&this.calculationNodeIndices.push(s)}else this.calculationNodes=this.nodes,this.calculationNodeIndices=this.nodeIndices},_calculateGravitationalForces:function(){var t,e,i,s,n,o=this.calculationNodes,a=this.constants.physics.centralGravity,r=0;for(n=0;nSimulation Mode: |
Barnes Hut | Repulsion | Hierarchical |
',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 e;e=document.getElementById("graph_BH_gc"),e.onchange=showValueOfRange.bind(this,"graph_BH_gc",-1,"physics_barnesHut_gravitationalConstant"),e=document.getElementById("graph_BH_cg"),e.onchange=showValueOfRange.bind(this,"graph_BH_cg",1,"physics_centralGravity"),e=document.getElementById("graph_BH_sc"),e.onchange=showValueOfRange.bind(this,"graph_BH_sc",1,"physics_springConstant"),e=document.getElementById("graph_BH_sl"),e.onchange=showValueOfRange.bind(this,"graph_BH_sl",1,"physics_springLength"),e=document.getElementById("graph_BH_damp"),e.onchange=showValueOfRange.bind(this,"graph_BH_damp",1,"physics_damping"),e=document.getElementById("graph_R_nd"),e.onchange=showValueOfRange.bind(this,"graph_R_nd",1,"physics_repulsion_nodeDistance"),e=document.getElementById("graph_R_cg"),e.onchange=showValueOfRange.bind(this,"graph_R_cg",1,"physics_centralGravity"),e=document.getElementById("graph_R_sc"),e.onchange=showValueOfRange.bind(this,"graph_R_sc",1,"physics_springConstant"),e=document.getElementById("graph_R_sl"),e.onchange=showValueOfRange.bind(this,"graph_R_sl",1,"physics_springLength"),e=document.getElementById("graph_R_damp"),e.onchange=showValueOfRange.bind(this,"graph_R_damp",1,"physics_damping"),e=document.getElementById("graph_H_nd"),e.onchange=showValueOfRange.bind(this,"graph_H_nd",1,"physics_hierarchicalRepulsion_nodeDistance"),e=document.getElementById("graph_H_cg"),e.onchange=showValueOfRange.bind(this,"graph_H_cg",1,"physics_centralGravity"),e=document.getElementById("graph_H_sc"),e.onchange=showValueOfRange.bind(this,"graph_H_sc",1,"physics_springConstant"),e=document.getElementById("graph_H_sl"),e.onchange=showValueOfRange.bind(this,"graph_H_sl",1,"physics_springLength"),e=document.getElementById("graph_H_damp"),e.onchange=showValueOfRange.bind(this,"graph_H_damp",1,"physics_damping"),e=document.getElementById("graph_H_direction"),e.onchange=showValueOfRange.bind(this,"graph_H_direction",t,"hierarchicalLayout_direction"),e=document.getElementById("graph_H_levsep"),e.onchange=showValueOfRange.bind(this,"graph_H_levsep",1,"hierarchicalLayout_levelSeparation"),e=document.getElementById("graph_H_nspac"),e.onchange=showValueOfRange.bind(this,"graph_H_nspac",1,"hierarchicalLayout_nodeSpacing");var i=document.getElementById("graph_physicsMethod1"),s=document.getElementById("graph_physicsMethod2"),n=document.getElementById("graph_physicsMethod3");s.checked=!0,this.constants.physics.barnesHut.enabled&&(i.checked=!0),this.constants.hierarchicalLayout.enabled&&(n.checked=!0);var o=document.getElementById("graph_toggleSmooth"),a=document.getElementById("graph_repositionNodes"),r=document.getElementById("graph_generateOptions");o.onclick=graphToggleSmoothCurves.bind(this),a.onclick=graphRepositionNodes.bind(this),r.onclick=graphGenerateOptions.bind(this),o.style.background=1==this.constants.smoothCurves?"#A4FF56":"#FF8532",switchConfigurations.apply(this),i.onchange=switchConfigurations.bind(this),s.onchange=switchConfigurations.bind(this),n.onchange=switchConfigurations.bind(this)}},_overWriteGraphConstants:function(t,e){var i=t.split("_");1==i.length?this.constants[i[0]]=e:2==i.length?this.constants[i[0]][i[1]]=e:3==i.length&&(this.constants[i[0]][i[1]][i[2]]=e)}},hierarchalRepulsionMixin={_calculateNodeForces:function(){var t,e,i,s,n,o,a,r,h,d,c=this.calculationNodes,l=this.calculationNodeIndices,u=5,p=.5*-u,g=this.constants.physics.hierarchicalRepulsion.nodeDistance,m=g;for(h=0;hi&&(o=f*i+u,0==i?i=.01:o/=i,s=t*o,n=e*o,a.fx-=s,a.fy-=n,r.fx+=s,r.fy+=n)}}},barnesHutMixin={_calculateNodeForces:function(){if(0!=this.constants.physics.barnesHut.gravitationalConstant){var t,e=this.calculationNodes,i=this.calculationNodeIndices,s=i.length;this._formBarnesHutTree(e,i);for(var n=this.barnesHutTree,o=0;s>o;o++)t=e[i[o]],this._getForceContribution(n.root.children.NW,t),this._getForceContribution(n.root.children.NE,t),this._getForceContribution(n.root.children.SW,t),this._getForceContribution(n.root.children.SE,t)}},_getForceContribution:function(t,e){if(t.childrenCount>0){var i,s,n;if(i=t.centerOfMass.x-e.x,s=t.centerOfMass.y-e.y,n=Math.sqrt(i*i+s*s),n*t.calcSize>this.constants.physics.barnesHut.theta){0==n&&(n=.1*Math.random(),i=n);var o=this.constants.physics.barnesHut.gravitationalConstant*t.mass*e.mass/(n*n*n),a=i*o,r=s*o;e.fx+=a,e.fy+=r}else if(4==t.childrenCount)this._getForceContribution(t.children.NW,e),this._getForceContribution(t.children.NE,e),this._getForceContribution(t.children.SW,e),this._getForceContribution(t.children.SE,e);else if(t.children.data.id!=e.id){0==n&&(n=.5*Math.random(),i=n);var o=this.constants.physics.barnesHut.gravitationalConstant*t.mass*e.mass/(n*n*n),a=i*o,r=s*o;e.fx+=a,e.fy+=r}}},_formBarnesHutTree:function(t,e){for(var i,s=e.length,n=Number.MAX_VALUE,o=Number.MAX_VALUE,a=-Number.MAX_VALUE,r=-Number.MAX_VALUE,h=0;s>h;h++){var d=t[e[h]].x,c=t[e[h]].y;n>d&&(n=d),d>a&&(a=d),o>c&&(o=c),c>r&&(r=c)}var l=Math.abs(a-n)-Math.abs(r-o);l>0?(o-=.5*l,r+=.5*l):(n+=.5*l,a-=.5*l);var u=1e-5,p=Math.max(u,Math.abs(a-n)),g=.5*p,m=.5*(n+a),f=.5*(o+r),v={root:{centerOfMass:{x:0,y:0},mass:0,range:{minX:m-g,maxX:m+g,minY:f-g,maxY:f+g},size:p,calcSize:1/p,children:{data:null},maxWidth:0,level:0,childrenCount:4}};for(this._splitBranch(v.root),h=0;s>h;h++)i=t[e[h]],this._placeInTree(v.root,i);this.barnesHutTree=v},_updateBranchMass:function(t,e){var i=t.mass+e.mass,s=1/i;t.centerOfMass.x=t.centerOfMass.x*t.mass+e.x*e.mass,t.centerOfMass.x*=s,t.centerOfMass.y=t.centerOfMass.y*t.mass+e.y*e.mass,t.centerOfMass.y*=s,t.mass=i;var n=Math.max(Math.max(e.height,e.radius),e.width);t.maxWidth=t.maxWidthe.x?t.children.NW.range.maxY>e.y?this._placeInRegion(t,e,"NW"):this._placeInRegion(t,e,"SW"):t.children.NW.range.maxY>e.y?this._placeInRegion(t,e,"NE"):this._placeInRegion(t,e,"SE")},_placeInRegion:function(t,e,i){switch(t.children[i].childrenCount){case 0:t.children[i].children.data=e,t.children[i].childrenCount=1,this._updateBranchMass(t.children[i],e);break;case 1:t.children[i].children.data.x==e.x&&t.children[i].children.data.y==e.y?(e.x+=Math.random(),e.y+=Math.random()):(this._splitBranch(t.children[i]),this._placeInTree(t.children[i],e));break;case 4:this._placeInTree(t.children[i],e)}},_splitBranch:function(t){var e=null;1==t.childrenCount&&(e=t.children.data,t.mass=0,t.centerOfMass.x=0,t.centerOfMass.y=0),t.childrenCount=4,t.children.data=null,this._insertRegion(t,"NW"),this._insertRegion(t,"NE"),this._insertRegion(t,"SW"),this._insertRegion(t,"SE"),null!=e&&this._placeInTree(t,e)},_insertRegion:function(t,e){var i,s,n,o,a=.5*t.size;switch(e){case"NW":i=t.range.minX,s=t.range.minX+a,n=t.range.minY,o=t.range.minY+a;break;case"NE":i=t.range.minX+a,s=t.range.maxX,n=t.range.minY,o=t.range.minY+a;break;case"SW":i=t.range.minX,s=t.range.minX+a,n=t.range.minY+a,o=t.range.maxY;break;case"SE":i=t.range.minX+a,s=t.range.maxX,n=t.range.minY+a,o=t.range.maxY}t.children[e]={centerOfMass:{x:0,y:0},mass:0,range:{minX:i,maxX:s,minY:n,maxY:o},size:.5*t.size,calcSize:2*t.calcSize,children:{data:null},maxWidth:0,level:t.level+1,childrenCount:0}},_drawTree:function(t,e){void 0!==this.barnesHutTree&&(t.lineWidth=1,this._drawBranch(this.barnesHutTree.root,t,e))},_drawBranch:function(t,e,i){void 0===i&&(i="#FF0000"),4==t.childrenCount&&(this._drawBranch(t.children.NW,e),this._drawBranch(t.children.NE,e),this._drawBranch(t.children.SE,e),this._drawBranch(t.children.SW,e)),e.strokeStyle=i,e.beginPath(),e.moveTo(t.range.minX,t.range.minY),e.lineTo(t.range.maxX,t.range.minY),e.stroke(),e.beginPath(),e.moveTo(t.range.maxX,t.range.minY),e.lineTo(t.range.maxX,t.range.maxY),e.stroke(),e.beginPath(),e.moveTo(t.range.maxX,t.range.maxY),e.lineTo(t.range.minX,t.range.maxY),e.stroke(),e.beginPath(),e.moveTo(t.range.minX,t.range.maxY),e.lineTo(t.range.minX,t.range.minY),e.stroke()}},repulsionMixin={_calculateNodeForces:function(){var t,e,i,s,n,o,a,r,h,d,c,l=this.calculationNodes,u=this.calculationNodeIndices,p=-2/3,g=4/3,m=this.constants.physics.repulsion.nodeDistance,f=m;for(d=0;di&&(a=.5*f>i?1:v*i+g,a*=0==o?1:1+o*this.constants.clustering.forceAmplification,a/=i,s=t*a,n=e*a,r.fx-=s,r.fy-=n,h.fx+=s,h.fy+=n)}}},HierarchicalLayoutMixin={_resetLevels:function(){for(var t in this.nodes)if(this.nodes.hasOwnProperty(t)){var e=this.nodes[t];0==e.preassignedLevel&&(e.level=-1)}},_setupHierarchicalLayout:function(){if(1==this.constants.hierarchicalLayout.enabled){"RL"==this.constants.hierarchicalLayout.direction||"DU"==this.constants.hierarchicalLayout.direction?this.constants.hierarchicalLayout.levelSeparation*=-1:this.constants.hierarchicalLayout.levelSeparation=Math.abs(this.constants.hierarchicalLayout.levelSeparation);var t,e,i=0,s=!1,n=!1;for(e in this.nodes)this.nodes.hasOwnProperty(e)&&(t=this.nodes[e],-1!=t.level?s=!0:n=!0,is&&(o.xFixed=!1,o.x=i[o.level].minPos,a=!0):o.yFixed&&o.level>s&&(o.yFixed=!1,o.y=i[o.level].minPos,a=!0),1==a&&(i[o.level].minPos+=i[o.level].nodeSpacing,o.edges.length>1&&this._placeBranchNodes(o.edges,o.id,i,o.level))}},_setLevel:function(t,e,i){for(var s=0;st)&&(n.level=t,e.length>1&&this._setLevel(t+1,n.edges,n.id))}},_restoreNodes:function(){for(nodeId in this.nodes)this.nodes.hasOwnProperty(nodeId)&&(this.nodes[nodeId].xFixed=!1,this.nodes[nodeId].yFixed=!1)}},manipulationMixin={_clearManipulatorBar:function(){for(;this.manipulationDiv.hasChildNodes();)this.manipulationDiv.removeChild(this.manipulationDiv.firstChild)},_restoreOverloadedFunctions:function(){for(var t in this.cachedFunctions)this.cachedFunctions.hasOwnProperty(t)&&(this[t]=this.cachedFunctions[t])},_toggleEditMode:function(){this.editMode=!this.editMode;var t=document.getElementById("graph-manipulationDiv"),e=document.getElementById("graph-manipulation-closeDiv"),i=document.getElementById("graph-manipulation-editMode");1==this.editMode?(t.style.display="block",e.style.display="block",i.style.display="none",e.onclick=this._toggleEditMode.bind(this)):(t.style.display="none",e.style.display="none",i.style.display="block",e.onclick=null),this._createManipulatorBar()},_createManipulatorBar:function(){if(this.boundFunction&&this.off("select",this.boundFunction),this._restoreOverloadedFunctions(),this.freezeSimulation=!1,this.blockConnectingEdgeSelection=!1,this.forceAppendSelection=!1,1==this.editMode){for(;this.manipulationDiv.hasChildNodes();)this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);this.manipulationDiv.innerHTML=""+this.constants.labels.add+""+this.constants.labels.link+"",1==this._getSelectedNodeCount()&&this.triggerFunctions.edit&&(this.manipulationDiv.innerHTML+=""+this.constants.labels.editNode+""),0==this._selectionIsEmpty()&&(this.manipulationDiv.innerHTML+=""+this.constants.labels.del+"");
+var t=document.getElementById("graph-manipulate-addNode");t.onclick=this._createAddNodeToolbar.bind(this);var e=document.getElementById("graph-manipulate-connectNode");if(e.onclick=this._createAddEdgeToolbar.bind(this),1==this._getSelectedNodeCount()&&this.triggerFunctions.edit){var i=document.getElementById("graph-manipulate-editNode");i.onclick=this._editNode.bind(this)}if(0==this._selectionIsEmpty()){var s=document.getElementById("graph-manipulate-delete");s.onclick=this._deleteSelected.bind(this)}var n=document.getElementById("graph-manipulation-closeDiv");n.onclick=this._toggleEditMode.bind(this),this.boundFunction=this._createManipulatorBar.bind(this),this.on("select",this.boundFunction)}else{this.editModeDiv.innerHTML=""+this.constants.labels.edit+"";var o=document.getElementById("graph-manipulate-editModeButton");o.onclick=this._toggleEditMode.bind(this)}},_createAddNodeToolbar:function(){this._clearManipulatorBar(),this.boundFunction&&this.off("select",this.boundFunction),this.manipulationDiv.innerHTML=""+this.constants.labels.back+" "+this.constants.labels.addDescription+"";var t=document.getElementById("graph-manipulate-back");t.onclick=this._createManipulatorBar.bind(this),this.boundFunction=this._addNode.bind(this),this.on("select",this.boundFunction)},_createAddEdgeToolbar:function(){this._clearManipulatorBar(),this._unselectAll(!0),this.freezeSimulation=!0,this.boundFunction&&this.off("select",this.boundFunction),this._unselectAll(),this.forceAppendSelection=!1,this.blockConnectingEdgeSelection=!0,this.manipulationDiv.innerHTML=""+this.constants.labels.back+" "+this.constants.labels.linkDescription+"";var t=document.getElementById("graph-manipulate-back");t.onclick=this._createManipulatorBar.bind(this),this.boundFunction=this._handleConnect.bind(this),this.on("select",this.boundFunction),this.cachedFunctions._handleTouch=this._handleTouch,this.cachedFunctions._handleOnRelease=this._handleOnRelease,this._handleTouch=this._handleConnect,this._handleOnRelease=this._finishConnect,this._redraw()},_handleConnect:function(t){if(0==this._getSelectedNodeCount()){var e=this._getNodeAt(t);null!=e&&(e.clusterSize>1?alert("Cannot create edges to a cluster."):(this._selectObject(e,!1),this.sectors.support.nodes.targetNode=new Node({id:"targetNode"},{},{},this.constants),this.sectors.support.nodes.targetNode.x=e.x,this.sectors.support.nodes.targetNode.y=e.y,this.sectors.support.nodes.targetViaNode=new Node({id:"targetViaNode"},{},{},this.constants),this.sectors.support.nodes.targetViaNode.x=e.x,this.sectors.support.nodes.targetViaNode.y=e.y,this.sectors.support.nodes.targetViaNode.parentEdgeId="connectionEdge",this.edges.connectionEdge=new Edge({id:"connectionEdge",from:e.id,to:this.sectors.support.nodes.targetNode.id},this,this.constants),this.edges.connectionEdge.from=e,this.edges.connectionEdge.connected=!0,this.edges.connectionEdge.smooth=!0,this.edges.connectionEdge.selected=!0,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(t){var e=this._getPointer(t.gesture.center);this.sectors.support.nodes.targetNode.x=this._canvasToX(e.x),this.sectors.support.nodes.targetNode.y=this._canvasToY(e.y),this.sectors.support.nodes.targetViaNode.x=.5*(this._canvasToX(e.x)+this.edges.connectionEdge.from.x),this.sectors.support.nodes.targetViaNode.y=this._canvasToY(e.y)},this.moving=!0,this.start()))}},_finishConnect:function(t){if(1==this._getSelectedNodeCount()){this._handleOnDrag=this.cachedFunctions._handleOnDrag,delete this.cachedFunctions._handleOnDrag;var e=this.edges.connectionEdge.fromId;delete this.edges.connectionEdge,delete this.sectors.support.nodes.targetNode,delete this.sectors.support.nodes.targetViaNode;var i=this._getNodeAt(t);null!=i&&(i.clusterSize>1?alert("Cannot create edges to a cluster."):(this._createEdge(e,i.id),this._createManipulatorBar())),this._unselectAll()}},_addNode:function(){if(this._selectionIsEmpty()&&1==this.editMode){var t=this._pointerToPositionObject(this.pointerPosition),e={id:util.randomUUID(),x:t.left,y:t.top,label:"new",allowedToMoveX:!0,allowedToMoveY:!0};if(this.triggerFunctions.add)if(2==this.triggerFunctions.add.length){var i=this;this.triggerFunctions.add(e,function(t){i.nodesData.add(t),i._createManipulatorBar(),i.moving=!0,i.start()})}else alert(this.constants.labels.addError),this._createManipulatorBar(),this.moving=!0,this.start();else this.nodesData.add(e),this._createManipulatorBar(),this.moving=!0,this.start()}},_createEdge:function(t,e){if(1==this.editMode){var i={from:t,to:e};if(this.triggerFunctions.connect)if(2==this.triggerFunctions.connect.length){var s=this;this.triggerFunctions.connect(i,function(t){s.edgesData.add(t),s.moving=!0,s.start()})}else alert(this.constants.labels.linkError),this.moving=!0,this.start();else this.edgesData.add(i),this.moving=!0,this.start()}},_editNode:function(){if(this.triggerFunctions.edit&&1==this.editMode){var t=this._getSelectedNode(),e={id:t.id,label:t.label,group:t.group,shape:t.shape,color:{background:t.color.background,border:t.color.border,highlight:{background:t.color.highlight.background,border:t.color.highlight.border}}};if(2==this.triggerFunctions.edit.length){var i=this;this.triggerFunctions.edit(e,function(t){i.nodesData.update(t),i._createManipulatorBar(),i.moving=!0,i.start()})}else alert(this.constants.labels.editError)}else alert(this.constants.labels.editBoundError)},_deleteSelected:function(){if(!this._selectionIsEmpty()&&1==this.editMode)if(this._clusterInSelection())alert(this.constants.labels.deleteClusterError);else{var t=this.getSelectedNodes(),e=this.getSelectedEdges();if(this.triggerFunctions.del){var i=this,s={nodes:t,edges:e};(this.triggerFunctions.del.length=2)?this.triggerFunctions.del(s,function(t){i.edgesData.remove(t.edges),i.nodesData.remove(t.nodes),i._unselectAll(),i.moving=!0,i.start()}):alert(this.constants.labels.deleteError)}else this.edgesData.remove(e),this.nodesData.remove(t),this._unselectAll(),this.moving=!0,this.start()}}},SectorMixin={_putDataInSector:function(){this.sectors.active[this._sector()].nodes=this.nodes,this.sectors.active[this._sector()].edges=this.edges,this.sectors.active[this._sector()].nodeIndices=this.nodeIndices},_switchToSector:function(t,e){void 0===e||"active"==e?this._switchToActiveSector(t):this._switchToFrozenSector(t)},_switchToActiveSector:function(t){this.nodeIndices=this.sectors.active[t].nodeIndices,this.nodes=this.sectors.active[t].nodes,this.edges=this.sectors.active[t].edges},_switchToSupportSector:function(){this.nodeIndices=this.sectors.support.nodeIndices,this.nodes=this.sectors.support.nodes,this.edges=this.sectors.support.edges},_switchToFrozenSector:function(t){this.nodeIndices=this.sectors.frozen[t].nodeIndices,this.nodes=this.sectors.frozen[t].nodes,this.edges=this.sectors.frozen[t].edges},_loadLatestSector:function(){this._switchToSector(this._sector())},_sector:function(){return this.activeSector[this.activeSector.length-1]},_previousSector:function(){if(this.activeSector.length>1)return this.activeSector[this.activeSector.length-2];throw new TypeError("there are not enough sectors in the this.activeSector array.")},_setActiveSector:function(t){this.activeSector.push(t)},_forgetLastSector:function(){this.activeSector.pop()},_createNewSector:function(t){this.sectors.active[t]={nodes:{},edges:{},nodeIndices:[],formationScale:this.scale,drawingNode:void 0},this.sectors.active[t].drawingNode=new Node({id:t,color:{background:"#eaefef",border:"495c5e"}},{},{},this.constants),this.sectors.active[t].drawingNode.clusterSize=2},_deleteActiveSector:function(t){delete this.sectors.active[t]},_deleteFrozenSector:function(t){delete this.sectors.frozen[t]},_freezeSector:function(t){this.sectors.frozen[t]=this.sectors.active[t],this._deleteActiveSector(t)},_activateSector:function(t){this.sectors.active[t]=this.sectors.frozen[t],this._deleteFrozenSector(t)},_mergeThisWithFrozen:function(t){for(var e in this.nodes)this.nodes.hasOwnProperty(e)&&(this.sectors.frozen[t].nodes[e]=this.nodes[e]);for(var i in this.edges)this.edges.hasOwnProperty(i)&&(this.sectors.frozen[t].edges[i]=this.edges[i]);for(var s=0;s1?this[t](s[0],s[1]):this[t](e)}this._loadLatestSector()},_doInSupportSector:function(t,e){if(void 0===e)this._switchToSupportSector(),this[t]();else{this._switchToSupportSector();var i=Array.prototype.splice.call(arguments,1);i.length>1?this[t](i[0],i[1]):this[t](e)}this._loadLatestSector()},_doInAllFrozenSectors:function(t,e){if(void 0===e)for(var i in this.sectors.frozen)this.sectors.frozen.hasOwnProperty(i)&&(this._switchToFrozenSector(i),this[t]());else for(var i in this.sectors.frozen)if(this.sectors.frozen.hasOwnProperty(i)){this._switchToFrozenSector(i);var s=Array.prototype.splice.call(arguments,1);s.length>1?this[t](s[0],s[1]):this[t](e)}this._loadLatestSector()},_doInAllSectors:function(t,e){var i=Array.prototype.splice.call(arguments,1);void 0===e?(this._doInAllActiveSectors(t),this._doInAllFrozenSectors(t)):i.length>1?(this._doInAllActiveSectors(t,i[0],i[1]),this._doInAllFrozenSectors(t,i[0],i[1])):(this._doInAllActiveSectors(t,e),this._doInAllFrozenSectors(t,e))},_clearNodeIndexList:function(){var t=this._sector();this.sectors.active[t].nodeIndices=[],this.nodeIndices=this.sectors.active[t].nodeIndices},_drawSectorNodes:function(t,e){var i,s=1e9,n=-1e9,o=1e9,a=-1e9;for(var r in this.sectors[e])if(this.sectors[e].hasOwnProperty(r)&&void 0!==this.sectors[e][r].drawingNode){this._switchToSector(r,e),s=1e9,n=-1e9,o=1e9,a=-1e9;for(var h in this.nodes)this.nodes.hasOwnProperty(h)&&(i=this.nodes[h],i.resize(t),o>i.x-.5*i.width&&(o=i.x-.5*i.width),ai.y-.5*i.height&&(s=i.y-.5*i.height),nt&&s>n;)n%3==0?(this.forceAggregateHubs(!0),this.normalizeClusterLevels()):this.increaseClusterLevel(),i=this.nodeIndices.length,n+=1;n>0&&1==e&&this.repositionNodes(),this._updateCalculationNodes()},openCluster:function(t){var e=this.moving;if(t.clusterSize>this.constants.clustering.sectorThreshold&&this._nodeInActiveArea(t)&&("default"!=this._sector()||1!=this.nodeIndices.length)){this._addSector(t);for(var i=0;this.nodeIndices.lengthi;)this.decreaseClusterLevel(),i+=1}else this._expandClusterNode(t,!1,!0),this._updateNodeIndexList(),this._updateDynamicEdges(),this._updateCalculationNodes(),this.updateLabels();this.moving!=e&&this.start()},updateClustersDefault:function(){1==this.constants.clustering.enabled&&this.updateClusters(0,!1,!1)},increaseClusterLevel:function(){this.updateClusters(-1,!1,!0)},decreaseClusterLevel:function(){this.updateClusters(1,!1,!0)},updateClusters:function(t,e,i,s){var n=this.moving,o=this.nodeIndices.length;this.previousScale>this.scale&&0==t&&this._collapseSector(),this.previousScale>this.scale||-1==t?this._formClusters(i):(this.previousScalethis.scale||-1==t)&&(this._aggregateHubs(i),this._updateNodeIndexList()),(this.previousScale>this.scale||-1==t)&&(this.handleChains(),this._updateNodeIndexList()),this.previousScale=this.scale,this._updateDynamicEdges(),this.updateLabels(),this.nodeIndices.lengththis.constants.clustering.chainThreshold&&this._reduceAmountOfChains(1-this.constants.clustering.chainThreshold/t)},_aggregateHubs:function(t){this._getHubSize(),this._formClustersByHub(t,!1)},forceAggregateHubs:function(t){var e=this.moving,i=this.nodeIndices.length;this._aggregateHubs(!0),this._updateNodeIndexList(),this._updateDynamicEdges(),this.updateLabels(),this.nodeIndices.length!=i&&(this.clusterSession+=1),(0==t||void 0===t)&&this.moving!=e&&this.start()},_openClustersBySize:function(){for(var t in this.nodes)if(this.nodes.hasOwnProperty(t)){var e=this.nodes[t];1==e.inView()&&(e.width*this.scale>this.constants.clustering.screenSizeThreshold*this.frame.canvas.clientWidth||e.height*this.scale>this.constants.clustering.screenSizeThreshold*this.frame.canvas.clientHeight)&&this.openCluster(e)}},_openClusters:function(t,e){for(var i=0;i1&&(t.clusterSizei)){var a=o.from,r=o.to;o.to.mass>o.from.mass&&(a=o.to,r=o.from),1==r.dynamicEdgesLength?this._addToCluster(a,r,!1):1==a.dynamicEdgesLength&&this._addToCluster(r,a,!1)}}},_forceClustersByZoom:function(){for(var t in this.nodes)if(this.nodes.hasOwnProperty(t)){var e=this.nodes[t];if(1==e.dynamicEdgesLength&&0!=e.dynamicEdges.length){var i=e.dynamicEdges[0],s=i.toId==e.id?this.nodes[i.fromId]:this.nodes[i.toId];e.id!=s.id&&(s.mass>e.mass?this._addToCluster(s,e,!0):this._addToCluster(e,s,!0))}}},_clusterToSmallestNeighbour:function(t){for(var e=-1,i=null,s=0;sn.clusterSessions.length&&(e=n.clusterSessions.length,i=n)}null!=n&&void 0!==this.nodes[n.id]&&this._addToCluster(n,t,!0)},_formClustersByHub:function(t,e){for(var i in this.nodes)this.nodes.hasOwnProperty(i)&&this._formClusterFromHub(this.nodes[i],t,e)},_formClusterFromHub:function(t,e,i,s){if(void 0===s&&(s=0),t.dynamicEdgesLength>=this.hubThreshold&&0==i||t.dynamicEdgesLength==this.hubThreshold&&1==i){for(var n,o,a,r=this.constants.clustering.clusterEdgeThreshold/this.scale,h=!1,d=[],c=t.dynamicEdges.length,l=0;c>l;l++)d.push(t.dynamicEdges[l].id);if(0==e)for(h=!1,l=0;c>l;l++){var u=this.edges[d[l]];if(void 0!==u&&u.connected&&u.toId!=u.fromId&&(n=u.to.x-u.from.x,o=u.to.y-u.from.y,a=Math.sqrt(n*n+o*o),r>a)){h=!0;break}}if(!e&&h||e)for(l=0;c>l;l++)if(u=this.edges[d[l]],void 0!==u){var p=this.nodes[u.fromId==t.id?u.toId:u.fromId];p.dynamicEdges.length<=this.hubThreshold+s&&p.id!=t.id&&this._addToCluster(t,p,e)}}},_addToCluster:function(t,e,i){t.containedNodes[e.id]=e;for(var s=0;s1)for(var s=0;s1&&(e.label="[".concat(String(e.clusterSize),"]"))}for(t in this.nodes)this.nodes.hasOwnProperty(t)&&(e=this.nodes[t],1==e.clusterSize&&(e.label=void 0!==e.originalLabel?e.originalLabel:String(e.id)))},normalizeClusterLevels:function(){var t=0,e=1e9,i=0;for(var s in this.nodes)this.nodes.hasOwnProperty(s)&&(i=this.nodes[s].clusterSessions.length,i>t&&(t=i),e>i&&(e=i));if(t-e>this.constants.clustering.clusterLevelDifference){var n=this.nodeIndices.length,o=t-this.constants.clustering.clusterLevelDifference;for(var s in this.nodes)this.nodes.hasOwnProperty(s)&&this.nodes[s].clusterSessions.lengths&&(s=o.dynamicEdgesLength),t+=o.dynamicEdgesLength,e+=Math.pow(o.dynamicEdgesLength,2),i+=1}t/=i,e/=i;var a=e-Math.pow(t,2),r=Math.sqrt(a);this.hubThreshold=Math.floor(t+2*r),this.hubThreshold>s&&(this.hubThreshold=s)},_reduceAmountOfChains:function(t){this.hubThreshold=2;var e=Math.floor(this.nodeIndices.length*t);for(var i in this.nodes)this.nodes.hasOwnProperty(i)&&2==this.nodes[i].dynamicEdgesLength&&this.nodes[i].dynamicEdges.length>=2&&e>0&&(this._formClusterFromHub(this.nodes[i],!0,!0,1),e-=1)},_getChainFraction:function(){var t=0,e=0;for(var i in this.nodes)this.nodes.hasOwnProperty(i)&&(2==this.nodes[i].dynamicEdgesLength&&this.nodes[i].dynamicEdges.length>=2&&(t+=1),e+=1);return t/e}},SelectionMixin={_getNodesOverlappingWith:function(t,e){var i=this.nodes;for(var s in i)i.hasOwnProperty(s)&&i[s].isOverlappingWith(t)&&e.push(s)},_getAllNodesOverlappingWith:function(t){var e=[];return this._doInAllActiveSectors("_getNodesOverlappingWith",t,e),e},_pointerToPositionObject:function(t){var e=this._canvasToX(t.x),i=this._canvasToY(t.y);return{left:e,top:i,right:e,bottom:i}},_getNodeAt:function(t){var e=this._pointerToPositionObject(t),i=this._getAllNodesOverlappingWith(e);return i.length>0?this.nodes[i[i.length-1]]:null},_getEdgesOverlappingWith:function(t,e){var i=this.edges;for(var s in i)i.hasOwnProperty(s)&&i[s].isOverlappingWith(t)&&e.push(s)},_getAllEdgesOverlappingWith:function(t){var e=[];return this._doInAllActiveSectors("_getEdgesOverlappingWith",t,e),e},_getEdgeAt:function(t){var e=this._pointerToPositionObject(t),i=this._getAllEdgesOverlappingWith(e);return i.length>0?this.edges[i[i.length-1]]:null},_addToSelection:function(t){t instanceof Node?this.selectionObj.nodes[t.id]=t:this.selectionObj.edges[t.id]=t},_removeFromSelection:function(t){t instanceof Node?delete this.selectionObj.nodes[t.id]:delete this.selectionObj.edges[t.id]},_unselectAll:function(t){void 0===t&&(t=!1);for(var e in this.selectionObj.nodes)this.selectionObj.nodes.hasOwnProperty(e)&&this.selectionObj.nodes[e].unselect();for(var i in this.selectionObj.edges)this.selectionObj.edges.hasOwnProperty(i)&&this.selectionObj.edges[i].unselect();this.selectionObj={nodes:{},edges:{}},0==t&&this.emit("select",this.getSelection())},_unselectClusters:function(t){void 0===t&&(t=!1);for(var e in this.selectionObj.nodes)this.selectionObj.nodes.hasOwnProperty(e)&&this.selectionObj.nodes[e].clusterSize>1&&(this.selectionObj.nodes[e].unselect(),this._removeFromSelection(this.selectionObj.nodes[e]));0==t&&this.emit("select",this.getSelection())},_getSelectedNodeCount:function(){var t=0;for(var e in this.selectionObj.nodes)this.selectionObj.nodes.hasOwnProperty(e)&&(t+=1);return t},_getSelectedNode:function(){for(var t in this.selectionObj.nodes)if(this.selectionObj.nodes.hasOwnProperty(t))return this.selectionObj.nodes[t];return null},_getSelectedEdgeCount:function(){var t=0;for(var e in this.selectionObj.edges)this.selectionObj.edges.hasOwnProperty(e)&&(t+=1);return t},_getSelectedObjectCount:function(){var t=0;for(var e in this.selectionObj.nodes)this.selectionObj.nodes.hasOwnProperty(e)&&(t+=1);for(var i in this.selectionObj.edges)this.selectionObj.edges.hasOwnProperty(i)&&(t+=1);return t},_selectionIsEmpty:function(){for(var t in this.selectionObj.nodes)if(this.selectionObj.nodes.hasOwnProperty(t))return!1;for(var e in this.selectionObj.edges)if(this.selectionObj.edges.hasOwnProperty(e))return!1;return!0},_clusterInSelection:function(){for(var t in this.selectionObj.nodes)if(this.selectionObj.nodes.hasOwnProperty(t)&&this.selectionObj.nodes[t].clusterSize>1)return!0;return!1},_selectConnectedEdges:function(t){for(var e=0;ee;e++){s=t[e];var n=this.nodes[s];if(!n)throw new RangeError('Node with id "'+s+'" not found');this._selectObject(n,!0,!0)}this.redraw()},_updateSelection:function(){for(var t in this.selectionObj.nodes)this.selectionObj.nodes.hasOwnProperty(t)&&(this.nodes.hasOwnProperty(t)||delete this.selectionObj.nodes[t]);for(var e in this.selectionObj.edges)this.selectionObj.edges.hasOwnProperty(e)&&(this.edges.hasOwnProperty(e)||delete this.selectionObj.edges[e])}},NavigationMixin={_cleanNavigation:function(){var t=document.getElementById("graph-navigation_wrapper");null!=t&&this.containerElement.removeChild(t),document.onmouseup=null},_loadNavigationElements:function(){this._cleanNavigation(),this.navigationDivs={};var t=["up","down","left","right","zoomIn","zoomOut","zoomExtends"],e=["_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;it.x&&(s=t.x),nt.y&&(e=t.y),i=this.constants.clustering.initialMaxNodes?49.07548/(n+142.05338)+91444e-8:12.662/(n+7.4147)+.0964822:1==this.constants.clustering.enabled&&n>=this.constants.clustering.initialMaxNodes?77.5271985/(n+187.266146)+476710517e-13:30.5062972/(n+19.93597763)+.08413486;var o=Math.min(this.frame.canvas.clientWidth/600,this.frame.canvas.clientHeight/600);i*=o}else{var a=1.1*(Math.abs(s.minX)+Math.abs(s.maxX)),r=1.1*(Math.abs(s.minY)+Math.abs(s.maxY)),h=this.frame.canvas.clientWidth/a,d=this.frame.canvas.clientHeight/r;i=d>=h?h:d}i>1&&(i=1),this._setScale(i),this._centerGraph(s),0==e&&(this.moving=!0,this.start())},Graph.prototype._updateNodeIndexList=function(){this._clearNodeIndexList();for(var t in this.nodes)this.nodes.hasOwnProperty(t)&&this.nodeIndices.push(t)},Graph.prototype.setData=function(t,e){if(void 0===e&&(e=!1),t&&t.dot&&(t.nodes||t.edges))throw new SyntaxError('Data must contain either parameter "dot" or parameter pair "nodes" and "edges", but not both.');if(this.setOptions(t&&t.options),t&&t.dot){if(t&&t.dot){var i=vis.util.DOTToGraph(t.dot);return void this.setData(i)}}else this._setNodes(t&&t.nodes),this._setEdges(t&&t.edges);this._putDataInSector(),e||(this.stabilize&&this._stabilize(),this.start())},Graph.prototype.setOptions=function(t){if(t){var e;if(void 0!==t.width&&(this.width=t.width),void 0!==t.height&&(this.height=t.height),void 0!==t.stabilize&&(this.stabilize=t.stabilize),void 0!==t.selectable&&(this.selectable=t.selectable),void 0!==t.smoothCurves&&(this.constants.smoothCurves=t.smoothCurves),void 0!==t.freezeForStabilization&&(this.constants.freezeForStabilization=t.freezeForStabilization),void 0!==t.configurePhysics&&(this.constants.configurePhysics=t.configurePhysics),void 0!==t.stabilizationIterations&&(this.constants.stabilizationIterations=t.stabilizationIterations),void 0!==t.labels)for(e in t.labels)t.labels.hasOwnProperty(e)&&(this.constants.labels[e]=t.labels[e]);if(t.onAdd&&(this.triggerFunctions.add=t.onAdd),t.onEdit&&(this.triggerFunctions.edit=t.onEdit),t.onConnect&&(this.triggerFunctions.connect=t.onConnect),t.onDelete&&(this.triggerFunctions.del=t.onDelete),t.physics){if(t.physics.barnesHut){this.constants.physics.barnesHut.enabled=!0;for(e in t.physics.barnesHut)t.physics.barnesHut.hasOwnProperty(e)&&(this.constants.physics.barnesHut[e]=t.physics.barnesHut[e])}if(t.physics.repulsion){this.constants.physics.barnesHut.enabled=!1;for(e in t.physics.repulsion)t.physics.repulsion.hasOwnProperty(e)&&(this.constants.physics.repulsion[e]=t.physics.repulsion[e])}}if(t.hierarchicalLayout){this.constants.hierarchicalLayout.enabled=!0;for(e in t.hierarchicalLayout)t.hierarchicalLayout.hasOwnProperty(e)&&(this.constants.hierarchicalLayout[e]=t.hierarchicalLayout[e])}else void 0!==t.hierarchicalLayout&&(this.constants.hierarchicalLayout.enabled=!1);if(t.clustering){this.constants.clustering.enabled=!0;for(e in t.clustering)t.clustering.hasOwnProperty(e)&&(this.constants.clustering[e]=t.clustering[e])}else void 0!==t.clustering&&(this.constants.clustering.enabled=!1);if(t.navigation){this.constants.navigation.enabled=!0;for(e in t.navigation)t.navigation.hasOwnProperty(e)&&(this.constants.navigation[e]=t.navigation[e])}else void 0!==t.navigation&&(this.constants.navigation.enabled=!1);if(t.keyboard){this.constants.keyboard.enabled=!0;for(e in t.keyboard)t.keyboard.hasOwnProperty(e)&&(this.constants.keyboard[e]=t.keyboard[e])}else void 0!==t.keyboard&&(this.constants.keyboard.enabled=!1);if(t.dataManipulation){this.constants.dataManipulation.enabled=!0;for(e in t.dataManipulation)t.dataManipulation.hasOwnProperty(e)&&(this.constants.dataManipulation[e]=t.dataManipulation[e])}else void 0!==t.dataManipulation&&(this.constants.dataManipulation.enabled=!1);if(t.edges){for(e in t.edges)t.edges.hasOwnProperty(e)&&"object"!=typeof t.edges[e]&&(this.constants.edges[e]=t.edges[e]);void 0!==t.edges.color&&(util.isString(t.edges.color)?(this.constants.edges.color={},this.constants.edges.color.color=t.edges.color,this.constants.edges.color.highlight=t.edges.color):(void 0!==t.edges.color.color&&(this.constants.edges.color.color=t.edges.color.color),void 0!==t.edges.color.highlight&&(this.constants.edges.color.highlight=t.edges.color.highlight))),t.edges.fontColor||void 0!==t.edges.color&&(util.isString(t.edges.color)?this.constants.edges.fontColor=t.edges.color:void 0!==t.edges.color.color&&(this.constants.edges.fontColor=t.edges.color.color)),t.edges.dash&&(void 0!==t.edges.dash.length&&(this.constants.edges.dash.length=t.edges.dash.length),void 0!==t.edges.dash.gap&&(this.constants.edges.dash.gap=t.edges.dash.gap),void 0!==t.edges.dash.altLength&&(this.constants.edges.dash.altLength=t.edges.dash.altLength))}if(t.nodes){for(e in t.nodes)t.nodes.hasOwnProperty(e)&&(this.constants.nodes[e]=t.nodes[e]);t.nodes.color&&(this.constants.nodes.color=util.parseColor(t.nodes.color))}if(t.groups)for(var i in t.groups)if(t.groups.hasOwnProperty(i)){var s=t.groups[i];this.groups.add(i,s)}if(t.tooltip){for(e in t.tooltip)t.tooltip.hasOwnProperty(e)&&(this.constants.tooltip[e]=t.tooltip[e]);t.tooltip.color&&(this.constants.tooltip.color=util.parseColor(t.tooltip.color))}}this._loadPhysicsSystem(),this._loadNavigationControls(),this._loadManipulationSystem(),this._configureSmoothCurves(),this._createKeyBinds(),this.setSize(this.width,this.height),this._setTranslation(this.frame.clientWidth/2,this.frame.clientHeight/2),this._setScale(1),this._redraw()},Graph.prototype._create=function(){for(;this.containerElement.hasChildNodes();)this.containerElement.removeChild(this.containerElement.firstChild);if(this.frame=document.createElement("div"),this.frame.className="graph-frame",this.frame.style.position="relative",this.frame.style.overflow="hidden",this.frame.canvas=document.createElement("canvas"),this.frame.canvas.style.position="relative",this.frame.appendChild(this.frame.canvas),!this.frame.canvas.getContext){var t=document.createElement("DIV");t.style.color="red",t.style.fontWeight="bold",t.style.padding="10px",t.innerHTML="Error: your browser does not support HTML canvas",this.frame.canvas.appendChild(t)}var e=this;this.drag={},this.pinch={},this.hammer=Hammer(this.frame.canvas,{prevent_default:!0}),this.hammer.on("tap",e._onTap.bind(e)),this.hammer.on("doubletap",e._onDoubleTap.bind(e)),this.hammer.on("hold",e._onHold.bind(e)),this.hammer.on("pinch",e._onPinch.bind(e)),this.hammer.on("touch",e._onTouch.bind(e)),this.hammer.on("dragstart",e._onDragStart.bind(e)),this.hammer.on("drag",e._onDrag.bind(e)),this.hammer.on("dragend",e._onDragEnd.bind(e)),this.hammer.on("release",e._onRelease.bind(e)),this.hammer.on("mousewheel",e._onMouseWheel.bind(e)),this.hammer.on("DOMMouseScroll",e._onMouseWheel.bind(e)),this.hammer.on("mousemove",e._onMouseMoveTitle.bind(e)),this.containerElement.appendChild(this.frame)},Graph.prototype._createKeyBinds=function(){var t=this;this.mousetrap=mousetrap,this.mousetrap.reset(),1==this.constants.keyboard.enabled&&(this.mousetrap.bind("up",this._moveUp.bind(t),"keydown"),this.mousetrap.bind("up",this._yStopMoving.bind(t),"keyup"),this.mousetrap.bind("down",this._moveDown.bind(t),"keydown"),this.mousetrap.bind("down",this._yStopMoving.bind(t),"keyup"),this.mousetrap.bind("left",this._moveLeft.bind(t),"keydown"),this.mousetrap.bind("left",this._xStopMoving.bind(t),"keyup"),this.mousetrap.bind("right",this._moveRight.bind(t),"keydown"),this.mousetrap.bind("right",this._xStopMoving.bind(t),"keyup"),this.mousetrap.bind("=",this._zoomIn.bind(t),"keydown"),this.mousetrap.bind("=",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("-",this._zoomOut.bind(t),"keydown"),this.mousetrap.bind("-",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("[",this._zoomIn.bind(t),"keydown"),this.mousetrap.bind("[",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("]",this._zoomOut.bind(t),"keydown"),this.mousetrap.bind("]",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("pageup",this._zoomIn.bind(t),"keydown"),this.mousetrap.bind("pageup",this._stopZoom.bind(t),"keyup"),this.mousetrap.bind("pagedown",this._zoomOut.bind(t),"keydown"),this.mousetrap.bind("pagedown",this._stopZoom.bind(t),"keyup")),1==this.constants.dataManipulation.enabled&&(this.mousetrap.bind("escape",this._createManipulatorBar.bind(t)),this.mousetrap.bind("del",this._deleteSelected.bind(t)))},Graph.prototype._getPointer=function(t){return{x:t.pageX-vis.util.getAbsoluteLeft(this.frame.canvas),y:t.pageY-vis.util.getAbsoluteTop(this.frame.canvas)}},Graph.prototype._onTouch=function(t){this.drag.pointer=this._getPointer(t.gesture.center),this.drag.pinched=!1,this.pinch.scale=this._getScale(),this._handleTouch(this.drag.pointer)},Graph.prototype._onDragStart=function(){this._handleDragStart()},Graph.prototype._handleDragStart=function(){var t=this.drag,e=this._getNodeAt(t.pointer);if(t.dragging=!0,t.selection=[],t.translation=this._getTranslation(),t.nodeId=null,null!=e){t.nodeId=e.id,e.isSelected()||this._selectObject(e,!1);for(var i in this.selectionObj.nodes)if(this.selectionObj.nodes.hasOwnProperty(i)){var s=this.selectionObj.nodes[i],n={id:s.id,node:s,x:s.x,y:s.y,xFixed:s.xFixed,yFixed:s.yFixed};s.xFixed=!0,s.yFixed=!0,t.selection.push(n)}}},Graph.prototype._onDrag=function(t){this._handleOnDrag(t)},Graph.prototype._handleOnDrag=function(t){if(!this.drag.pinched){var e=this._getPointer(t.gesture.center),i=this,s=this.drag,n=s.selection;if(n&&n.length){var o=e.x-s.pointer.x,a=e.y-s.pointer.y;n.forEach(function(t){var e=t.node;t.xFixed||(e.x=i._canvasToX(i._xToCanvas(t.x)+o)),t.yFixed||(e.y=i._canvasToY(i._yToCanvas(t.y)+a))}),this.moving||(this.moving=!0,this.start())}else{var r=e.x-this.drag.pointer.x,h=e.y-this.drag.pointer.y;this._setTranslation(this.drag.translation.x+r,this.drag.translation.y+h),this._redraw(),this.moved=!0}}},Graph.prototype._onDragEnd=function(){this.drag.dragging=!1;var t=this.drag.selection;t&&t.forEach(function(t){t.node.xFixed=t.xFixed,t.node.yFixed=t.yFixed})},Graph.prototype._onTap=function(t){var e=this._getPointer(t.gesture.center);this.pointerPosition=e,this._handleTap(e)},Graph.prototype._onDoubleTap=function(t){var e=this._getPointer(t.gesture.center);this._handleDoubleTap(e)},Graph.prototype._onHold=function(t){var e=this._getPointer(t.gesture.center);this.pointerPosition=e,this._handleOnHold(e)},Graph.prototype._onRelease=function(t){var e=this._getPointer(t.gesture.center);this._handleOnRelease(e)},Graph.prototype._onPinch=function(t){var e=this._getPointer(t.gesture.center);this.drag.pinched=!0,"scale"in this.pinch||(this.pinch.scale=1);var i=this.pinch.scale*t.gesture.scale;this._zoom(i,e)},Graph.prototype._zoom=function(t,e){var i=this._getScale();1e-5>t&&(t=1e-5),t>10&&(t=10);var s=this._getTranslation(),n=t/i,o=(1-n)*e.x+s.x*n,a=(1-n)*e.y+s.y*n;return this.areaCenter={x:this._canvasToX(e.x),y:this._canvasToY(e.y)},this._setScale(t),this._setTranslation(o,a),this.updateClustersDefault(),this._redraw(),t},Graph.prototype._onMouseWheel=function(t){var e=0;if(t.wheelDelta?e=t.wheelDelta/120:t.detail&&(e=-t.detail/3),e){var i=this._getScale(),s=e/10;0>e&&(s/=1-s),i*=1+s;var n=util.fakeGesture(this,t),o=this._getPointer(n.center);this._zoom(i,o)}t.preventDefault()},Graph.prototype._onMouseMoveTitle=function(t){var e=util.fakeGesture(this,t),i=this._getPointer(e.center);this.popupNode&&this._checkHidePopup(i);var s=this,n=function(){s._checkShowPopup(i)};this.popupTimer&&clearInterval(this.popupTimer),this.drag.dragging||(this.popupTimer=setTimeout(n,this.constants.tooltip.delay))},Graph.prototype._checkShowPopup=function(t){var e,i={left:this._canvasToX(t.x),top:this._canvasToY(t.y),right:this._canvasToX(t.x),bottom:this._canvasToY(t.y)},s=this.popupNode;if(void 0==this.popupNode){var n=this.nodes;for(e in n)if(n.hasOwnProperty(e)){var o=n[e];if(void 0!==o.getTitle()&&o.isOverlappingWith(i)){this.popupNode=o;break}}}if(void 0===this.popupNode){var a=this.edges;for(e in a)if(a.hasOwnProperty(e)){var r=a[e];if(r.connected&&void 0!==r.getTitle()&&r.isOverlappingWith(i)){this.popupNode=r;break}}}if(this.popupNode){if(this.popupNode!=s){var h=this;h.popup||(h.popup=new Popup(h.frame,h.constants.tooltip)),h.popup.setPosition(t.x-3,t.y-3),h.popup.setText(h.popupNode.getTitle()),h.popup.show()}}else this.popup&&this.popup.hide()},Graph.prototype._checkHidePopup=function(t){this.popupNode&&this._getNodeAt(t)||(this.popupNode=void 0,this.popup&&this.popup.hide())},Graph.prototype.setSize=function(t,e){this.frame.style.width=t,this.frame.style.height=e,this.frame.canvas.style.width="100%",this.frame.canvas.style.height="100%",this.frame.canvas.width=this.frame.canvas.clientWidth,this.frame.canvas.height=this.frame.canvas.clientHeight,void 0!==this.manipulationDiv&&(this.manipulationDiv.style.width=this.frame.canvas.clientWidth+"px"),void 0!==this.navigationDivs&&void 0!==this.navigationDivs.wrapper&&(this.navigationDivs.wrapper.style.width=this.frame.canvas.clientWidth+"px",this.navigationDivs.wrapper.style.height=this.frame.canvas.clientHeight+"px"),this.emit("resize",{width:this.frame.canvas.width,height:this.frame.canvas.height})},Graph.prototype._setNodes=function(t){var e=this.nodesData;if(t instanceof DataSet||t instanceof DataView)this.nodesData=t;else if(t instanceof Array)this.nodesData=new DataSet,this.nodesData.add(t);else{if(t)throw new TypeError("Array or DataSet expected");this.nodesData=new DataSet}if(e&&util.forEach(this.nodesListeners,function(t,i){e.off(i,t)}),this.nodes={},this.nodesData){var i=this;util.forEach(this.nodesListeners,function(t,e){i.nodesData.on(e,t)});var s=this.nodesData.getIds();this._addNodes(s)}this._updateSelection()},Graph.prototype._addNodes=function(t){for(var e,i=0,s=t.length;s>i;i++){e=t[i];var n=this.nodesData.get(e),o=new Node(n,this.images,this.groups,this.constants);if(this.nodes[e]=o,!(0!=o.xFixed&&0!=o.yFixed||null!==o.x&&null!==o.y)){var a=1*t.length,r=2*Math.PI*Math.random();0==o.xFixed&&(o.x=a*Math.cos(r)),0==o.yFixed&&(o.y=a*Math.sin(r))}this.moving=!0}this._updateNodeIndexList(),1==this.constants.hierarchicalLayout.enabled&&0==this.initializing&&(this._resetLevels(),this._setupHierarchicalLayout()),this._updateCalculationNodes(),this._reconnectEdges(),this._updateValueRange(this.nodes),this.updateLabels()},Graph.prototype._updateNodes=function(t){for(var e=this.nodes,i=this.nodesData,s=0,n=t.length;n>s;s++){var o=t[s],a=e[o],r=i.get(o);a?a.setProperties(r,this.constants):(a=new Node(properties,this.images,this.groups,this.constants),e[o]=a,a.isFixed()||(this.moving=!0))}this._updateNodeIndexList(),this._reconnectEdges(),this._updateValueRange(e)},Graph.prototype._removeNodes=function(t){for(var e=this.nodes,i=0,s=t.length;s>i;i++){var n=t[i];delete e[n]}this._updateNodeIndexList(),1==this.constants.hierarchicalLayout.enabled&&0==this.initializing&&(this._resetLevels(),this._setupHierarchicalLayout()),this._updateCalculationNodes(),this._reconnectEdges(),this._updateSelection(),this._updateValueRange(e)},Graph.prototype._setEdges=function(t){var e=this.edgesData;if(t instanceof DataSet||t instanceof DataView)this.edgesData=t;else if(t instanceof Array)this.edgesData=new DataSet,this.edgesData.add(t);else{if(t)throw new TypeError("Array or DataSet expected");this.edgesData=new DataSet}if(e&&util.forEach(this.edgesListeners,function(t,i){e.off(i,t)}),this.edges={},this.edgesData){var i=this;util.forEach(this.edgesListeners,function(t,e){i.edgesData.on(e,t)});var s=this.edgesData.getIds();this._addEdges(s)}this._reconnectEdges()},Graph.prototype._addEdges=function(t){for(var e=this.edges,i=this.edgesData,s=0,n=t.length;n>s;s++){var o=t[s],a=e[o];a&&a.disconnect();var r=i.get(o,{showInternalIds:!0});e[o]=new Edge(r,this,this.constants)}this.moving=!0,this._updateValueRange(e),this._createBezierNodes(),1==this.constants.hierarchicalLayout.enabled&&0==this.initializing&&(this._resetLevels(),this._setupHierarchicalLayout()),this._updateCalculationNodes()},Graph.prototype._updateEdges=function(t){for(var e=this.edges,i=this.edgesData,s=0,n=t.length;n>s;s++){var o=t[s],a=i.get(o),r=e[o];r?(r.disconnect(),r.setProperties(a,this.constants),r.connect()):(r=new Edge(a,this,this.constants),this.edges[o]=r)}this._createBezierNodes(),1==this.constants.hierarchicalLayout.enabled&&0==this.initializing&&(this._resetLevels(),this._setupHierarchicalLayout()),this.moving=!0,this._updateValueRange(e)},Graph.prototype._removeEdges=function(t){for(var e=this.edges,i=0,s=t.length;s>i;i++){var n=t[i],o=e[n];o&&(null!=o.via&&delete this.sectors.support.nodes[o.via.id],o.disconnect(),delete e[n])}this.moving=!0,this._updateValueRange(e),1==this.constants.hierarchicalLayout.enabled&&0==this.initializing&&(this._resetLevels(),this._setupHierarchicalLayout()),this._updateCalculationNodes()},Graph.prototype._reconnectEdges=function(){var t,e=this.nodes,i=this.edges;for(t in e)e.hasOwnProperty(t)&&(e[t].edges=[]);for(t in i)if(i.hasOwnProperty(t)){var s=i[t];s.from=null,s.to=null,s.connect()}},Graph.prototype._updateValueRange=function(t){var e,i=void 0,s=void 0;for(e in t)if(t.hasOwnProperty(e)){var n=t[e].getValue();void 0!==n&&(i=void 0===i?n:Math.min(n,i),s=void 0===s?n:Math.max(n,s))}if(void 0!==i&&void 0!==s)for(e in t)t.hasOwnProperty(e)&&t[e].setValueRange(i,s)},Graph.prototype.redraw=function(){this.setSize(this.width,this.height),this._redraw()},Graph.prototype._redraw=function(){var t=this.frame.canvas.getContext("2d"),e=this.frame.canvas.width,i=this.frame.canvas.height;t.clearRect(0,0,e,i),t.save(),t.translate(this.translation.x,this.translation.y),t.scale(this.scale,this.scale),this.canvasTopLeft={x:this._canvasToX(0),y:this._canvasToY(0)},this.canvasBottomRight={x:this._canvasToX(this.frame.canvas.clientWidth),y:this._canvasToY(this.frame.canvas.clientHeight)},this._doInAllSectors("_drawAllSectorNodes",t),this._doInAllSectors("_drawEdges",t),this._doInAllSectors("_drawNodes",t,!1),t.restore()},Graph.prototype._setTranslation=function(t,e){void 0===this.translation&&(this.translation={x:0,y:0}),void 0!==t&&(this.translation.x=t),void 0!==e&&(this.translation.y=e)},Graph.prototype._getTranslation=function(){return{x:this.translation.x,y:this.translation.y}},Graph.prototype._setScale=function(t){this.scale=t},Graph.prototype._getScale=function(){return this.scale},Graph.prototype._canvasToX=function(t){return(t-this.translation.x)/this.scale},Graph.prototype._xToCanvas=function(t){return t*this.scale+this.translation.x},Graph.prototype._canvasToY=function(t){return(t-this.translation.y)/this.scale},Graph.prototype._yToCanvas=function(t){return t*this.scale+this.translation.y},Graph.prototype._drawNodes=function(t,e){void 0===e&&(e=!1);var i=this.nodes,s=[];for(var n in i)i.hasOwnProperty(n)&&(i[n].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight),i[n].isSelected()?s.push(n):(i[n].inArea()||e)&&i[n].draw(t));for(var o=0,a=s.length;a>o;o++)(i[s[o]].inArea()||e)&&i[s[o]].draw(t)},Graph.prototype._drawEdges=function(t){var e=this.edges;for(var i in e)if(e.hasOwnProperty(i)){var s=e[i];s.setScale(this.scale),s.connected&&e[i].draw(t)}},Graph.prototype._stabilize=function(){1==this.constants.freezeForStabilization&&this._freezeDefinedNodes();for(var t=0;this.moving&&t0)for(t in i)i.hasOwnProperty(t)&&(i[t].discreteStepLimited(e,this.constants.maxVelocity),s=!0);else for(t in i)i.hasOwnProperty(t)&&(i[t].discreteStep(e),s=!0);if(1==s){var n=this.constants.minVelocity/Math.max(this.scale,.05);this.moving=n>.5*this.constants.maxVelocity?!0:this._isMoving(n)}},Graph.prototype._physicsTick=function(){this.freezeSimulation||this.moving&&(this._doInAllActiveSectors("_initializeForceCalculation"),this._doInAllActiveSectors("_discreteStepNodes"),this.constants.smoothCurves&&this._doInSupportSector("_discreteStepNodes"),this._findCenter(this._getRange()))},Graph.prototype._animationStep=function(){this.timer=void 0,this._handleNavigation(),this.start();var t=Date.now(),e=1;this._physicsTick();for(var i=Date.now()-t;is;++s)i[s].apply(this,e)}return this},i.prototype.listeners=function(t){return this._callbacks=this._callbacks||{},this._callbacks[t]||[]},i.prototype.hasListeners=function(t){return!!this.listeners(t).length}},{}],3:[function(t,e){!function(t,i){"use strict";function s(){if(!n.READY){n.event.determineEventTypes();for(var t in n.gestures)n.gestures.hasOwnProperty(t)&&n.detection.register(n.gestures[t]);n.event.onTouch(n.DOCUMENT,n.EVENT_MOVE,n.detection.detect),n.event.onTouch(n.DOCUMENT,n.EVENT_END,n.detection.detect),n.READY=!0}}var n=function(t,e){return new n.Instance(t,e||{})};n.defaults={stop_browser_behavior:{userSelect:"none",touchAction:"none",touchCallout:"none",contentZooming:"none",userDrag:"none",tapHighlightColor:"rgba(0,0,0,0)"}},n.HAS_POINTEREVENTS=navigator.pointerEnabled||navigator.msPointerEnabled,n.HAS_TOUCHEVENTS="ontouchstart"in t,n.MOBILE_REGEX=/mobile|tablet|ip(ad|hone|od)|android/i,n.NO_MOUSEEVENTS=n.HAS_TOUCHEVENTS&&navigator.userAgent.match(n.MOBILE_REGEX),n.EVENT_TYPES={},n.DIRECTION_DOWN="down",n.DIRECTION_LEFT="left",n.DIRECTION_UP="up",n.DIRECTION_RIGHT="right",n.POINTER_MOUSE="mouse",n.POINTER_TOUCH="touch",n.POINTER_PEN="pen",n.EVENT_START="start",n.EVENT_MOVE="move",n.EVENT_END="end",n.DOCUMENT=document,n.plugins={},n.READY=!1,n.Instance=function(t,e){var i=this;return s(),this.element=t,this.enabled=!0,this.options=n.utils.extend(n.utils.extend({},n.defaults),e||{}),this.options.stop_browser_behavior&&n.utils.stopDefaultBrowserBehavior(this.element,this.options.stop_browser_behavior),n.event.onTouch(t,n.EVENT_START,function(t){i.enabled&&n.detection.startDetect(i,t)}),this},n.Instance.prototype={on:function(t,e){for(var i=t.split(" "),s=0;s0&&e==n.EVENT_END?e=n.EVENT_MOVE:c||(e=n.EVENT_END),c||null===o?o=h:h=o,i.call(n.detection,s.collectEventData(t,e,h)),n.HAS_POINTEREVENTS&&e==n.EVENT_END&&(c=n.PointerEvent.updatePointer(e,h))),c||(o=null,a=!1,r=!1,n.PointerEvent.reset())}})},determineEventTypes:function(){var t;t=n.HAS_POINTEREVENTS?n.PointerEvent.getEvents():n.NO_MOUSEEVENTS?["touchstart","touchmove","touchend touchcancel"]:["touchstart mousedown","touchmove mousemove","touchend touchcancel mouseup"],n.EVENT_TYPES[n.EVENT_START]=t[0],n.EVENT_TYPES[n.EVENT_MOVE]=t[1],n.EVENT_TYPES[n.EVENT_END]=t[2]
+},getTouchList:function(t){return n.HAS_POINTEREVENTS?n.PointerEvent.getTouchList():t.touches?t.touches:[{identifier:1,pageX:t.pageX,pageY:t.pageY,target:t.target}]},collectEventData:function(t,e,i){var s=this.getTouchList(i,e),o=n.POINTER_TOUCH;return(i.type.match(/mouse/)||n.PointerEvent.matchType(n.POINTER_MOUSE,i))&&(o=n.POINTER_MOUSE),{center:n.utils.getCenter(s),timeStamp:(new Date).getTime(),target:i.target,touches:s,eventType:e,pointerType:o,srcEvent:i,preventDefault:function(){this.srcEvent.preventManipulation&&this.srcEvent.preventManipulation(),this.srcEvent.preventDefault&&this.srcEvent.preventDefault()},stopPropagation:function(){this.srcEvent.stopPropagation()},stopDetect:function(){return n.detection.stopDetect()}}}},n.PointerEvent={pointers:{},getTouchList:function(){var t=this,e=[];return Object.keys(t.pointers).sort().forEach(function(i){e.push(t.pointers[i])}),e},updatePointer:function(t,e){return t==n.EVENT_END?this.pointers={}:(e.identifier=e.pointerId,this.pointers[e.pointerId]=e),Object.keys(this.pointers).length},matchType:function(t,e){if(!e.pointerType)return!1;var i={};return i[n.POINTER_MOUSE]=e.pointerType==e.MSPOINTER_TYPE_MOUSE||e.pointerType==n.POINTER_MOUSE,i[n.POINTER_TOUCH]=e.pointerType==e.MSPOINTER_TYPE_TOUCH||e.pointerType==n.POINTER_TOUCH,i[n.POINTER_PEN]=e.pointerType==e.MSPOINTER_TYPE_PEN||e.pointerType==n.POINTER_PEN,i[t]},getEvents:function(){return["pointerdown MSPointerDown","pointermove MSPointerMove","pointerup pointercancel MSPointerUp MSPointerCancel"]},reset:function(){this.pointers={}}},n.utils={extend:function(t,e,s){for(var n in e)t[n]!==i&&s||(t[n]=e[n]);return t},hasParent:function(t,e){for(;t;){if(t==e)return!0;t=t.parentNode}return!1},getCenter:function(t){for(var e=[],i=[],s=0,n=t.length;n>s;s++)e.push(t[s].pageX),i.push(t[s].pageY);return{pageX:(Math.min.apply(Math,e)+Math.max.apply(Math,e))/2,pageY:(Math.min.apply(Math,i)+Math.max.apply(Math,i))/2}},getVelocity:function(t,e,i){return{x:Math.abs(e/t)||0,y:Math.abs(i/t)||0}},getAngle:function(t,e){var i=e.pageY-t.pageY,s=e.pageX-t.pageX;return 180*Math.atan2(i,s)/Math.PI},getDirection:function(t,e){var i=Math.abs(t.pageX-e.pageX),s=Math.abs(t.pageY-e.pageY);return i>=s?t.pageX-e.pageX>0?n.DIRECTION_LEFT:n.DIRECTION_RIGHT:t.pageY-e.pageY>0?n.DIRECTION_UP:n.DIRECTION_DOWN},getDistance:function(t,e){var i=e.pageX-t.pageX,s=e.pageY-t.pageY;return Math.sqrt(i*i+s*s)},getScale:function(t,e){return t.length>=2&&e.length>=2?this.getDistance(e[0],e[1])/this.getDistance(t[0],t[1]):1},getRotation:function(t,e){return t.length>=2&&e.length>=2?this.getAngle(e[1],e[0])-this.getAngle(t[1],t[0]):0},isVertical:function(t){return t==n.DIRECTION_UP||t==n.DIRECTION_DOWN},stopDefaultBrowserBehavior:function(t,e){var i,s=["webkit","khtml","moz","ms","o",""];if(e&&t.style){for(var n=0;ni;i++){var o=this.gestures[i];if(!this.stopped&&e[o.name]!==!1&&o.handler.call(o,t,this.current.inst)===!1){this.stopDetect();break}}return this.current&&(this.current.lastEvent=t),t.eventType==n.EVENT_END&&!t.touches.length-1&&this.stopDetect(),t}},stopDetect:function(){this.previous=n.utils.extend({},this.current),this.current=null,this.stopped=!0},extendEventData:function(t){var e=this.current.startEvent;if(e&&(t.touches.length!=e.touches.length||t.touches===e.touches)){e.touches=[];for(var i=0,s=t.touches.length;s>i;i++)e.touches.push(n.utils.extend({},t.touches[i]))}var o=t.timeStamp-e.timeStamp,a=t.center.pageX-e.center.pageX,r=t.center.pageY-e.center.pageY,h=n.utils.getVelocity(o,a,r);return n.utils.extend(t,{deltaTime:o,deltaX:a,deltaY:r,velocityX:h.x,velocityY:h.y,distance:n.utils.getDistance(e.center,t.center),angle:n.utils.getAngle(e.center,t.center),direction:n.utils.getDirection(e.center,t.center),scale:n.utils.getScale(e.touches,t.touches),rotation:n.utils.getRotation(e.touches,t.touches),startEvent:e}),t},register:function(t){var e=t.defaults||{};return e[t.name]===i&&(e[t.name]=!0),n.utils.extend(n.defaults,e,!0),t.index=t.index||1e3,this.gestures.push(t),this.gestures.sort(function(t,e){return t.indexe.index?1:0}),this.gestures}},n.gestures=n.gestures||{},n.gestures.Hold={name:"hold",index:10,defaults:{hold_timeout:500,hold_threshold:1},timer:null,handler:function(t,e){switch(t.eventType){case n.EVENT_START:clearTimeout(this.timer),n.detection.current.name=this.name,this.timer=setTimeout(function(){"hold"==n.detection.current.name&&e.trigger("hold",t)},e.options.hold_timeout);break;case n.EVENT_MOVE:t.distance>e.options.hold_threshold&&clearTimeout(this.timer);break;case n.EVENT_END:clearTimeout(this.timer)}}},n.gestures.Tap={name:"tap",index:100,defaults:{tap_max_touchtime:250,tap_max_distance:10,tap_always:!0,doubletap_distance:20,doubletap_interval:300},handler:function(t,e){if(t.eventType==n.EVENT_END){var i=n.detection.previous,s=!1;if(t.deltaTime>e.options.tap_max_touchtime||t.distance>e.options.tap_max_distance)return;i&&"tap"==i.name&&t.timeStamp-i.lastEvent.timeStamp0&&t.touches.length>e.options.swipe_max_touches)return;(t.velocityX>e.options.swipe_velocity||t.velocityY>e.options.swipe_velocity)&&(e.trigger(this.name,t),e.trigger(this.name+t.direction,t))}}},n.gestures.Drag={name:"drag",index:50,defaults:{drag_min_distance:10,drag_max_touches:1,drag_block_horizontal:!1,drag_block_vertical:!1,drag_lock_to_axis:!1,drag_lock_min_distance:25},triggered:!1,handler:function(t,e){if(n.detection.current.name!=this.name&&this.triggered)return e.trigger(this.name+"end",t),void(this.triggered=!1);if(!(e.options.drag_max_touches>0&&t.touches.length>e.options.drag_max_touches))switch(t.eventType){case n.EVENT_START:this.triggered=!1;break;case n.EVENT_MOVE:if(t.distancee.options.transform_min_rotation&&e.trigger("rotate",t),i>e.options.transform_min_scale&&(e.trigger("pinch",t),e.trigger("pinch"+(t.scale<1?"in":"out"),t));break;case n.EVENT_END:this.triggered&&e.trigger(this.name+"end",t),this.triggered=!1}}},n.gestures.Touch={name:"touch",index:-1/0,defaults:{prevent_default:!1,prevent_mouseevents:!1},handler:function(t,e){return e.options.prevent_mouseevents&&t.pointerType==n.POINTER_MOUSE?void t.stopDetect():(e.options.prevent_default&&t.preventDefault(),void(t.eventType==n.EVENT_START&&e.trigger(this.name,t)))}},n.gestures.Release={name:"release",index:1/0,handler:function(t,e){t.eventType==n.EVENT_END&&e.trigger(this.name,t)}},"object"==typeof e&&"object"==typeof e.exports?e.exports=n:(t.Hammer=n,"function"==typeof t.define&&t.define.amd&&t.define("hammer",[],function(){return n}))}(this)},{}],4:[function(t,e){var i="undefined"!=typeof self?self:"undefined"!=typeof window?window:{};(function(s){function n(){return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function o(t,e){function i(){le.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+t)}var s=!0;return l(function(){return s&&(i(),s=!1),e.apply(this,arguments)},e)}function a(t,e){return function(i){return g(t.call(this,i),e)}}function r(t,e){return function(i){return this.lang().ordinal(t.call(this,i),e)}}function h(){}function d(t){C(t),l(this,t)}function c(t){var e=b(t),i=e.year||0,s=e.quarter||0,n=e.month||0,o=e.week||0,a=e.day||0,r=e.hour||0,h=e.minute||0,d=e.second||0,c=e.millisecond||0;this._milliseconds=+c+1e3*d+6e4*h+36e5*r,this._days=+a+7*o,this._months=+n+3*s+12*i,this._data={},this._bubble()}function l(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return e.hasOwnProperty("toString")&&(t.toString=e.toString),e.hasOwnProperty("valueOf")&&(t.valueOf=e.valueOf),t}function u(t){var e,i={};for(e in t)t.hasOwnProperty(e)&&Te.hasOwnProperty(e)&&(i[e]=t[e]);return i}function p(t){return 0>t?Math.ceil(t):Math.floor(t)}function g(t,e,i){for(var s=""+Math.abs(t),n=t>=0;s.lengths;s++)(i&&t[s]!==e[s]||!i&&w(t[s])!==w(e[s]))&&a++;return a+o}function _(t){if(t){var e=t.toLowerCase().replace(/(.)s$/,"$1");t=ti[t]||ei[e]||e}return t}function b(t){var e,i,s={};for(i in t)t.hasOwnProperty(i)&&(e=_(i),e&&(s[e]=t[i]));return s}function S(t){var e,i;if(0===t.indexOf("week"))e=7,i="day";else{if(0!==t.indexOf("month"))return;e=12,i="month"}le[t]=function(n,o){var a,r,h=le.fn._lang[t],d=[];if("number"==typeof n&&(o=n,n=s),r=function(t){var e=le().utc().set(i,t);return h.call(le.fn._lang,e,n||"")},null!=o)return r(o);for(a=0;e>a;a++)d.push(r(a));return d}}function w(t){var e=+t,i=0;return 0!==e&&isFinite(e)&&(i=e>=0?Math.floor(e):Math.ceil(e)),i}function x(t,e){return new Date(Date.UTC(t,e+1,0)).getUTCDate()}function E(t,e,i){return ee(le([t,11,31+e-i]),e,i).week}function T(t){return D(t)?366:365}function D(t){return t%4===0&&t%100!==0||t%400===0}function C(t){var e;t._a&&-2===t._pf.overflow&&(e=t._a[ye]<0||t._a[ye]>11?ye:t._a[_e]<1||t._a[_e]>x(t._a[ve],t._a[ye])?_e:t._a[be]<0||t._a[be]>23?be:t._a[Se]<0||t._a[Se]>59?Se:t._a[we]<0||t._a[we]>59?we:t._a[xe]<0||t._a[xe]>999?xe:-1,t._pf._overflowDayOfYear&&(ve>e||e>_e)&&(e=_e),t._pf.overflow=e)}function I(t){return null==t._isValid&&(t._isValid=!isNaN(t._d.getTime())&&t._pf.overflow<0&&!t._pf.empty&&!t._pf.invalidMonth&&!t._pf.nullInput&&!t._pf.invalidFormat&&!t._pf.userInvalidated,t._strict&&(t._isValid=t._isValid&&0===t._pf.charsLeftOver&&0===t._pf.unusedTokens.length)),t._isValid}function M(t){return t?t.toLowerCase().replace("_","-"):t}function N(t,e){return e._isUTC?le(t).zone(e._offset||0):le(t).local()}function O(t,e){return e.abbr=t,Ee[t]||(Ee[t]=new h),Ee[t].set(e),Ee[t]}function L(t){delete Ee[t]}function P(e){var i,s,n,o,a=0,r=function(e){if(!Ee[e]&&De)try{t("./lang/"+e)}catch(i){}return Ee[e]};if(!e)return le.fn._lang;if(!f(e)){if(s=r(e))return s;e=[e]}for(;a0;){if(s=r(o.slice(0,i).join("-")))return s;if(n&&n.length>=i&&y(o,n,!0)>=i-1)break;i--}a++}return le.fn._lang}function k(t){return t.match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function A(t){var e,i,s=t.match(Ne);for(e=0,i=s.length;i>e;e++)s[e]=oi[s[e]]?oi[s[e]]:k(s[e]);return function(n){var o="";for(e=0;i>e;e++)o+=s[e]instanceof Function?s[e].call(n,t):s[e];return o}}function z(t,e){return t.isValid()?(e=F(e,t.lang()),ii[e]||(ii[e]=A(e)),ii[e](t)):t.lang().invalidDate()}function F(t,e){function i(t){return e.longDateFormat(t)||t}var s=5;for(Oe.lastIndex=0;s>=0&&Oe.test(t);)t=t.replace(Oe,i),Oe.lastIndex=0,s-=1;return t}function R(t,e){var i,s=e._strict;switch(t){case"Q":return We;case"DDDD":return je;case"YYYY":case"GGGG":case"gggg":return s?Ve:ke;case"Y":case"G":case"g":return Xe;case"YYYYYY":case"YYYYY":case"GGGGG":case"ggggg":return s?Ue:Ae;case"S":if(s)return We;case"SS":if(s)return Be;case"SSS":if(s)return je;case"DDD":return Pe;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return Fe;case"a":case"A":return P(e._l)._meridiemParse;case"X":return Ye;case"Z":case"ZZ":return Re;case"T":return He;case"SSSS":return ze;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"ww":case"WW":return s?Be:Le;case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"W":case"e":case"E":return Le;case"Do":return Ge;default:return i=new RegExp(U(V(t.replace("\\","")),"i"))}}function H(t){t=t||"";var e=t.match(Re)||[],i=e[e.length-1]||[],s=(i+"").match(Je)||["-",0,0],n=+(60*s[1])+w(s[2]);return"+"===s[0]?-n:n}function Y(t,e,i){var s,n=i._a;switch(t){case"Q":null!=e&&(n[ye]=3*(w(e)-1));break;case"M":case"MM":null!=e&&(n[ye]=w(e)-1);break;case"MMM":case"MMMM":s=P(i._l).monthsParse(e),null!=s?n[ye]=s:i._pf.invalidMonth=e;break;case"D":case"DD":null!=e&&(n[_e]=w(e));break;case"Do":null!=e&&(n[_e]=w(parseInt(e,10)));break;case"DDD":case"DDDD":null!=e&&(i._dayOfYear=w(e));break;case"YY":n[ve]=le.parseTwoDigitYear(e);break;case"YYYY":case"YYYYY":case"YYYYYY":n[ve]=w(e);break;case"a":case"A":i._isPm=P(i._l).isPM(e);break;case"H":case"HH":case"h":case"hh":n[be]=w(e);break;case"m":case"mm":n[Se]=w(e);break;case"s":case"ss":n[we]=w(e);break;case"S":case"SS":case"SSS":case"SSSS":n[xe]=w(1e3*("0."+e));break;case"X":i._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":i._useUTC=!0,i._tzm=H(e);break;case"w":case"ww":case"W":case"WW":case"d":case"dd":case"ddd":case"dddd":case"e":case"E":t=t.substr(0,1);case"gg":case"gggg":case"GG":case"GGGG":case"GGGGG":t=t.substr(0,2),e&&(i._w=i._w||{},i._w[t]=e)}}function G(t){var e,i,s,n,o,a,r,h,d,c,l=[];if(!t._d){for(s=B(t),t._w&&null==t._a[_e]&&null==t._a[ye]&&(o=function(e){var i=parseInt(e,10);return e?e.length<3?i>68?1900+i:2e3+i:i:null==t._a[ve]?le().weekYear():t._a[ve]},a=t._w,null!=a.GG||null!=a.W||null!=a.E?r=ie(o(a.GG),a.W||1,a.E,4,1):(h=P(t._l),d=null!=a.d?J(a.d,h):null!=a.e?parseInt(a.e,10)+h._week.dow:0,c=parseInt(a.w,10)||1,null!=a.d&&dT(n)&&(t._pf._overflowDayOfYear=!0),i=$(n,0,t._dayOfYear),t._a[ye]=i.getUTCMonth(),t._a[_e]=i.getUTCDate()),e=0;3>e&&null==t._a[e];++e)t._a[e]=l[e]=s[e];for(;7>e;e++)t._a[e]=l[e]=null==t._a[e]?2===e?1:0:t._a[e];l[be]+=w((t._tzm||0)/60),l[Se]+=w((t._tzm||0)%60),t._d=(t._useUTC?$:K).apply(null,l)}}function W(t){var e;t._d||(e=b(t._i),t._a=[e.year,e.month,e.day,e.hour,e.minute,e.second,e.millisecond],G(t))}function B(t){var e=new Date;return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}function j(t){t._a=[],t._pf.empty=!0;var e,i,s,n,o,a=P(t._l),r=""+t._i,h=r.length,d=0;for(s=F(t._f,a).match(Ne)||[],e=0;e0&&t._pf.unusedInput.push(o),r=r.slice(r.indexOf(i)+i.length),d+=i.length),oi[n]?(i?t._pf.empty=!1:t._pf.unusedTokens.push(n),Y(n,i,t)):t._strict&&!i&&t._pf.unusedTokens.push(n);t._pf.charsLeftOver=h-d,r.length>0&&t._pf.unusedInput.push(r),t._isPm&&t._a[be]<12&&(t._a[be]+=12),t._isPm===!1&&12===t._a[be]&&(t._a[be]=0),G(t),C(t)}function V(t){return t.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,i,s,n){return e||i||s||n})}function U(t){return t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function X(t){var e,i,s,o,a;if(0===t._f.length)return t._pf.invalidFormat=!0,void(t._d=new Date(0/0));for(o=0;oa)&&(s=a,i=e));l(t,i||e)}function q(t){var e,i,s=t._i,n=qe.exec(s);if(n){for(t._pf.iso=!0,e=0,i=Ke.length;i>e;e++)if(Ke[e][1].exec(s)){t._f=Ke[e][0]+(n[6]||" ");break}for(e=0,i=$e.length;i>e;e++)if($e[e][1].exec(s)){t._f+=$e[e][0];break}s.match(Re)&&(t._f+="Z"),j(t)}else le.createFromInputFallback(t)}function Z(t){var e=t._i,i=Ce.exec(e);e===s?t._d=new Date:i?t._d=new Date(+i[1]):"string"==typeof e?q(t):f(e)?(t._a=e.slice(0),G(t)):v(e)?t._d=new Date(+e):"object"==typeof e?W(t):"number"==typeof e?t._d=new Date(e):le.createFromInputFallback(t)}function K(t,e,i,s,n,o,a){var r=new Date(t,e,i,s,n,o,a);return 1970>t&&r.setFullYear(t),r}function $(t){var e=new Date(Date.UTC.apply(null,arguments));return 1970>t&&e.setUTCFullYear(t),e}function J(t,e){if("string"==typeof t)if(isNaN(t)){if(t=e.weekdaysParse(t),"number"!=typeof t)return null}else t=parseInt(t,10);return t}function Q(t,e,i,s,n){return n.relativeTime(e||1,!!i,t,s)}function te(t,e,i){var s=fe(Math.abs(t)/1e3),n=fe(s/60),o=fe(n/60),a=fe(o/24),r=fe(a/365),h=45>s&&["s",s]||1===n&&["m"]||45>n&&["mm",n]||1===o&&["h"]||22>o&&["hh",o]||1===a&&["d"]||25>=a&&["dd",a]||45>=a&&["M"]||345>a&&["MM",fe(a/30)]||1===r&&["y"]||["yy",r];return h[2]=e,h[3]=t>0,h[4]=i,Q.apply({},h)}function ee(t,e,i){var s,n=i-e,o=i-t.day();return o>n&&(o-=7),n-7>o&&(o+=7),s=le(t).add("d",o),{week:Math.ceil(s.dayOfYear()/7),year:s.year()}}function ie(t,e,i,s,n){var o,a,r=$(t,0,1).getUTCDay();return i=null!=i?i:n,o=n-r+(r>s?7:0)-(n>r?7:0),a=7*(e-1)+(i-n)+o+1,{year:a>0?t:t-1,dayOfYear:a>0?a:T(t-1)+a}}function se(t){var e=t._i,i=t._f;return null===e||i===s&&""===e?le.invalid({nullInput:!0}):("string"==typeof e&&(t._i=e=P().preparse(e)),le.isMoment(e)?(t=u(e),t._d=new Date(+e._d)):i?f(i)?X(t):j(t):Z(t),new d(t))}function ne(t,e){var i;return"string"==typeof e&&(e=t.lang().monthsParse(e),"number"!=typeof e)?t:(i=Math.min(t.date(),x(t.year(),e)),t._d["set"+(t._isUTC?"UTC":"")+"Month"](e,i),t)}function oe(t,e){return t._d["get"+(t._isUTC?"UTC":"")+e]()}function ae(t,e,i){return"Month"===e?ne(t,i):t._d["set"+(t._isUTC?"UTC":"")+e](i)}function re(t,e){return function(i){return null!=i?(ae(this,t,i),le.updateOffset(this,e),this):oe(this,t)}}function he(t){le.duration.fn[t]=function(){return this._data[t]}}function de(t,e){le.duration.fn["as"+t]=function(){return+this/e}}function ce(t){"undefined"==typeof ender&&(ue=me.moment,me.moment=t?o("Accessing Moment through the global scope is deprecated, and will be removed in an upcoming release.",le):le)}for(var le,ue,pe,ge="2.6.0",me="undefined"!=typeof i?i:this,fe=Math.round,ve=0,ye=1,_e=2,be=3,Se=4,we=5,xe=6,Ee={},Te={_isAMomentObject:null,_i:null,_f:null,_l:null,_strict:null,_isUTC:null,_offset:null,_pf:null,_lang:null},De="undefined"!=typeof e&&e.exports,Ce=/^\/?Date\((\-?\d+)/i,Ie=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,Me=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,Ne=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,Oe=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,Le=/\d\d?/,Pe=/\d{1,3}/,ke=/\d{1,4}/,Ae=/[+\-]?\d{1,6}/,ze=/\d+/,Fe=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Re=/Z|[\+\-]\d\d:?\d\d/gi,He=/T/i,Ye=/[\+\-]?\d+(\.\d{1,3})?/,Ge=/\d{1,2}/,We=/\d/,Be=/\d\d/,je=/\d{3}/,Ve=/\d{4}/,Ue=/[+-]?\d{6}/,Xe=/[+-]?\d+/,qe=/^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Ze="YYYY-MM-DDTHH:mm:ssZ",Ke=[["YYYYYY-MM-DD",/[+-]\d{6}-\d{2}-\d{2}/],["YYYY-MM-DD",/\d{4}-\d{2}-\d{2}/],["GGGG-[W]WW-E",/\d{4}-W\d{2}-\d/],["GGGG-[W]WW",/\d{4}-W\d{2}/],["YYYY-DDD",/\d{4}-\d{3}/]],$e=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],Je=/([\+\-]|\d\d)/gi,Qe=("Date|Hours|Minutes|Seconds|Milliseconds".split("|"),{Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6}),ti={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",D:"date",w:"week",W:"isoWeek",M:"month",Q:"quarter",y:"year",DDD:"dayOfYear",e:"weekday",E:"isoWeekday",gg:"weekYear",GG:"isoWeekYear"},ei={dayofyear:"dayOfYear",isoweekday:"isoWeekday",isoweek:"isoWeek",weekyear:"weekYear",isoweekyear:"isoWeekYear"},ii={},si="DDD w W M D d".split(" "),ni="M D H h m s w W".split(" "),oi={M:function(){return this.month()+1},MMM:function(t){return this.lang().monthsShort(this,t)},MMMM:function(t){return this.lang().months(this,t)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(t){return this.lang().weekdaysMin(this,t)},ddd:function(t){return this.lang().weekdaysShort(this,t)},dddd:function(t){return this.lang().weekdays(this,t)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return g(this.year()%100,2)},YYYY:function(){return g(this.year(),4)},YYYYY:function(){return g(this.year(),5)},YYYYYY:function(){var t=this.year(),e=t>=0?"+":"-";return e+g(Math.abs(t),6)},gg:function(){return g(this.weekYear()%100,2)},gggg:function(){return g(this.weekYear(),4)},ggggg:function(){return g(this.weekYear(),5)},GG:function(){return g(this.isoWeekYear()%100,2)},GGGG:function(){return g(this.isoWeekYear(),4)},GGGGG:function(){return g(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return w(this.milliseconds()/100)},SS:function(){return g(w(this.milliseconds()/10),2)},SSS:function(){return g(this.milliseconds(),3)},SSSS:function(){return g(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+g(w(t/60),2)+":"+g(w(t)%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+g(w(t/60),2)+g(w(t)%60,2)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()},Q:function(){return this.quarter()}},ai=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"];si.length;)pe=si.pop(),oi[pe+"o"]=r(oi[pe],pe);for(;ni.length;)pe=ni.pop(),oi[pe+pe]=a(oi[pe],2);for(oi.DDDD=a(oi.DDD,3),l(h.prototype,{set:function(t){var e,i;for(i in t)e=t[i],"function"==typeof e?this[i]=e:this["_"+i]=e},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(t){return this._months[t.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(t){return this._monthsShort[t.month()]},monthsParse:function(t){var e,i,s;for(this._monthsParse||(this._monthsParse=[]),e=0;12>e;e++)if(this._monthsParse[e]||(i=le.utc([2e3,e]),s="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[e]=new RegExp(s.replace(".",""),"i")),this._monthsParse[e].test(t))return e},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(t){return this._weekdays[t.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(t){return this._weekdaysShort[t.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(t){return this._weekdaysMin[t.day()]},weekdaysParse:function(t){var e,i,s;for(this._weekdaysParse||(this._weekdaysParse=[]),e=0;7>e;e++)if(this._weekdaysParse[e]||(i=le([2e3,1]).day(e),s="^"+this.weekdays(i,"")+"|^"+this.weekdaysShort(i,"")+"|^"+this.weekdaysMin(i,""),this._weekdaysParse[e]=new RegExp(s.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(t){var e=this._longDateFormat[t];return!e&&this._longDateFormat[t.toUpperCase()]&&(e=this._longDateFormat[t.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t]=e),e},isPM:function(t){return"p"===(t+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(t,e,i){return t>11?i?"pm":"PM":i?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(t,e){var i=this._calendar[t];return"function"==typeof i?i.apply(e):i},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(t,e,i,s){var n=this._relativeTime[i];return"function"==typeof n?n(t,e,i,s):n.replace(/%d/i,t)},pastFuture:function(t,e){var i=this._relativeTime[t>0?"future":"past"];return"function"==typeof i?i(e):i.replace(/%s/i,e)},ordinal:function(t){return this._ordinal.replace("%d",t)},_ordinal:"%d",preparse:function(t){return t},postformat:function(t){return t},week:function(t){return ee(t,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),le=function(t,e,i,o){var a;return"boolean"==typeof i&&(o=i,i=s),a={},a._isAMomentObject=!0,a._i=t,a._f=e,a._l=i,a._strict=o,a._isUTC=!1,a._pf=n(),se(a)},le.suppressDeprecationWarnings=!1,le.createFromInputFallback=o("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(t){t._d=new Date(t._i)}),le.utc=function(t,e,i,o){var a;return"boolean"==typeof i&&(o=i,i=s),a={},a._isAMomentObject=!0,a._useUTC=!0,a._isUTC=!0,a._l=i,a._i=t,a._f=e,a._strict=o,a._pf=n(),se(a).utc()},le.unix=function(t){return le(1e3*t)},le.duration=function(t,e){var i,s,n,o=t,a=null;return le.isDuration(t)?o={ms:t._milliseconds,d:t._days,M:t._months}:"number"==typeof t?(o={},e?o[e]=t:o.milliseconds=t):(a=Ie.exec(t))?(i="-"===a[1]?-1:1,o={y:0,d:w(a[_e])*i,h:w(a[be])*i,m:w(a[Se])*i,s:w(a[we])*i,ms:w(a[xe])*i}):(a=Me.exec(t))&&(i="-"===a[1]?-1:1,n=function(t){var e=t&&parseFloat(t.replace(",","."));return(isNaN(e)?0:e)*i},o={y:n(a[2]),M:n(a[3]),d:n(a[4]),h:n(a[5]),m:n(a[6]),s:n(a[7]),w:n(a[8])}),s=new c(o),le.isDuration(t)&&t.hasOwnProperty("_lang")&&(s._lang=t._lang),s},le.version=ge,le.defaultFormat=Ze,le.momentProperties=Te,le.updateOffset=function(){},le.lang=function(t,e){var i;return t?(e?O(M(t),e):null===e?(L(t),t="en"):Ee[t]||P(t),i=le.duration.fn._lang=le.fn._lang=P(t),i._abbr):le.fn._lang._abbr},le.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),P(t)},le.isMoment=function(t){return t instanceof d||null!=t&&t.hasOwnProperty("_isAMomentObject")},le.isDuration=function(t){return t instanceof c},pe=ai.length-1;pe>=0;--pe)S(ai[pe]);le.normalizeUnits=function(t){return _(t)},le.invalid=function(t){var e=le.utc(0/0);return null!=t?l(e._pf,t):e._pf.userInvalidated=!0,e},le.parseZone=function(){return le.apply(null,arguments).parseZone()},le.parseTwoDigitYear=function(t){return w(t)+(w(t)>68?1900:2e3)},l(le.fn=d.prototype,{clone:function(){return le(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().lang("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){var t=le(this).utc();return 00:!1},parsingFlags:function(){return l({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(t){var e=z(this,t||le.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var i;return i="string"==typeof t?le.duration(+e,t):le.duration(t,e),m(this,i,1),this},subtract:function(t,e){var i;return i="string"==typeof t?le.duration(+e,t):le.duration(t,e),m(this,i,-1),this},diff:function(t,e,i){var s,n,o=N(t,this),a=6e4*(this.zone()-o.zone());return e=_(e),"year"===e||"month"===e?(s=432e5*(this.daysInMonth()+o.daysInMonth()),n=12*(this.year()-o.year())+(this.month()-o.month()),n+=(this-le(this).startOf("month")-(o-le(o).startOf("month")))/s,n-=6e4*(this.zone()-le(this).startOf("month").zone()-(o.zone()-le(o).startOf("month").zone()))/s,"year"===e&&(n/=12)):(s=this-o,n="second"===e?s/1e3:"minute"===e?s/6e4:"hour"===e?s/36e5:"day"===e?(s-a)/864e5:"week"===e?(s-a)/6048e5:s),i?n:p(n)},from:function(t,e){return le.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(le(),t)},calendar:function(){var t=N(le(),this).startOf("day"),e=this.diff(t,"days",!0),i=-6>e?"sameElse":-1>e?"lastWeek":0>e?"lastDay":1>e?"sameDay":2>e?"nextDay":7>e?"nextWeek":"sameElse";return this.format(this.lang().calendar(i,this))},isLeapYear:function(){return D(this.year())},isDST:function(){return this.zone()+le(t).startOf(e)},isBefore:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)<+le(t).startOf(e)},isSame:function(t,e){return e=e||"ms",+this.clone().startOf(e)===+N(t,this).startOf(e)
+},min:function(t){return t=le.apply(null,arguments),this>t?this:t},max:function(t){return t=le.apply(null,arguments),t>this?this:t},zone:function(t,e){var i=this._offset||0;return null==t?this._isUTC?i:this._d.getTimezoneOffset():("string"==typeof t&&(t=H(t)),Math.abs(t)<16&&(t=60*t),this._offset=t,this._isUTC=!0,i!==t&&(!e||this._changeInProgress?m(this,le.duration(i-t,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,le.updateOffset(this,!0),this._changeInProgress=null)),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return this._tzm?this.zone(this._tzm):"string"==typeof this._i&&this.zone(this._i),this},hasAlignedHourOffset:function(t){return t=t?le(t).zone():0,(this.zone()-t)%60===0},daysInMonth:function(){return x(this.year(),this.month())},dayOfYear:function(t){var e=fe((le(this).startOf("day")-le(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},quarter:function(t){return null==t?Math.ceil((this.month()+1)/3):this.month(3*(t-1)+this.month()%3)},weekYear:function(t){var e=ee(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==t?e:this.add("y",t-e)},isoWeekYear:function(t){var e=ee(this,1,4).year;return null==t?e:this.add("y",t-e)},week:function(t){var e=this.lang().week(this);return null==t?e:this.add("d",7*(t-e))},isoWeek:function(t){var e=ee(this,1,4).week;return null==t?e:this.add("d",7*(t-e))},weekday:function(t){var e=(this.day()+7-this.lang()._week.dow)%7;return null==t?e:this.add("d",t-e)},isoWeekday:function(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)},isoWeeksInYear:function(){return E(this.year(),1,4)},weeksInYear:function(){var t=this._lang._week;return E(this.year(),t.dow,t.doy)},get:function(t){return t=_(t),this[t]()},set:function(t,e){return t=_(t),"function"==typeof this[t]&&this[t](e),this},lang:function(t){return t===s?this._lang:(this._lang=P(t),this)}}),le.fn.millisecond=le.fn.milliseconds=re("Milliseconds",!1),le.fn.second=le.fn.seconds=re("Seconds",!1),le.fn.minute=le.fn.minutes=re("Minutes",!1),le.fn.hour=le.fn.hours=re("Hours",!0),le.fn.date=re("Date",!0),le.fn.dates=o("dates accessor is deprecated. Use date instead.",re("Date",!0)),le.fn.year=re("FullYear",!0),le.fn.years=o("years accessor is deprecated. Use year instead.",re("FullYear",!0)),le.fn.days=le.fn.day,le.fn.months=le.fn.month,le.fn.weeks=le.fn.week,le.fn.isoWeeks=le.fn.isoWeek,le.fn.quarters=le.fn.quarter,le.fn.toJSON=le.fn.toISOString,l(le.duration.fn=c.prototype,{_bubble:function(){var t,e,i,s,n=this._milliseconds,o=this._days,a=this._months,r=this._data;r.milliseconds=n%1e3,t=p(n/1e3),r.seconds=t%60,e=p(t/60),r.minutes=e%60,i=p(e/60),r.hours=i%24,o+=p(i/24),r.days=o%30,a+=p(o/30),r.months=a%12,s=p(a/12),r.years=s},weeks:function(){return p(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*w(this._months/12)},humanize:function(t){var e=+this,i=te(e,!t,this.lang());return t&&(i=this.lang().pastFuture(e,i)),this.lang().postformat(i)},add:function(t,e){var i=le.duration(t,e);return this._milliseconds+=i._milliseconds,this._days+=i._days,this._months+=i._months,this._bubble(),this},subtract:function(t,e){var i=le.duration(t,e);return this._milliseconds-=i._milliseconds,this._days-=i._days,this._months-=i._months,this._bubble(),this},get:function(t){return t=_(t),this[t.toLowerCase()+"s"]()},as:function(t){return t=_(t),this["as"+t.charAt(0).toUpperCase()+t.slice(1)+"s"]()},lang:le.fn.lang,toIsoString:function(){var t=Math.abs(this.years()),e=Math.abs(this.months()),i=Math.abs(this.days()),s=Math.abs(this.hours()),n=Math.abs(this.minutes()),o=Math.abs(this.seconds()+this.milliseconds()/1e3);return this.asSeconds()?(this.asSeconds()<0?"-":"")+"P"+(t?t+"Y":"")+(e?e+"M":"")+(i?i+"D":"")+(s||n||o?"T":"")+(s?s+"H":"")+(n?n+"M":"")+(o?o+"S":""):"P0D"}});for(pe in Qe)Qe.hasOwnProperty(pe)&&(de(pe,Qe[pe]),he(pe.toLowerCase()));de("Weeks",6048e5),le.duration.fn.asMonths=function(){return(+this-31536e6*this.years())/2592e6+12*this.years()},le.lang("en",{ordinal:function(t){var e=t%10,i=1===w(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th";return t+i}}),De?e.exports=le:"function"==typeof define&&define.amd?(define("moment",function(t,e,i){return i.config&&i.config()&&i.config().noGlobal===!0&&(me.moment=ue),le}),ce(!0)):ce()}).call(this)},{}],5:[function(t,e){function i(t,e,i){return t.addEventListener?t.addEventListener(e,i,!1):void t.attachEvent("on"+e,i)}function s(t){return"keypress"==t.type?String.fromCharCode(t.which):S[t.which]?S[t.which]:w[t.which]?w[t.which]:String.fromCharCode(t.which).toLowerCase()}function n(t){var e=t.target||t.srcElement,i=e.tagName;return(" "+e.className+" ").indexOf(" mousetrap ")>-1?!1:"INPUT"==i||"SELECT"==i||"TEXTAREA"==i||e.contentEditable&&"true"==e.contentEditable}function o(t,e){return t.sort().join(",")===e.sort().join(",")}function a(t){t=t||{};var e,i=!1;for(e in C)t[e]?i=!0:C[e]=0;i||(M=!1)}function r(t,e,i,s,n){var a,r,h=[];if(!T[t])return[];for("keyup"==i&&u(t)&&(e=[t]),a=0;a95&&112>t||S.hasOwnProperty(t)&&(_[S[t]]=t)}return _}function m(t,e,i){return i||(i=g()[t]?"keydown":"keypress"),"keypress"==i&&e.length&&(i="keydown"),i}function f(t,e,i,n){C[t]=0,n||(n=m(e[0],[]));var o,r=function(){M=n,++C[t],p()},h=function(t){d(i,t),"keyup"!==n&&(I=s(t)),setTimeout(a,10)};for(o=0;o1)return f(t,d,e,i);for(h="+"===t?["+"]:t.split("+"),o=0;o":".","?":"/","|":"\\"},E={option:"alt",command:"meta","return":"enter",escape:"esc"},T={},D={},C={},I=!1,M=!1,N=1;20>N;++N)S[111+N]="f"+N;for(N=0;9>=N;++N)S[N+96]=N;i(document,"keypress",l),i(document,"keydown",l),i(document,"keyup",l);var O={bind:function(t,e,i){return y(t instanceof Array?t:[t],e,i),D[t+":"+i]=e,this},unbind:function(t,e){return D[t+":"+e]&&(delete D[t+":"+e],this.bind(t,function(){},e)),this},trigger:function(t,e){return D[t+":"+e](),this},reset:function(){return T={},D={},this}};e.exports=O},{}]},{},[1])(1)});
\ No newline at end of file
diff --git a/docs/css/style.css b/docs/css/style.css
index 12b5ac45..18916927 100644
--- a/docs/css/style.css
+++ b/docs/css/style.css
@@ -75,3 +75,8 @@ td {
padding: 5px;
vertical-align: top;
}
+
+p.important_note {
+ color: #3a6baa;
+ font-weight:bold;
+}
\ No newline at end of file
diff --git a/docs/dataset.html b/docs/dataset.html
index e6e8310b..477d26a4 100644
--- a/docs/dataset.html
+++ b/docs/dataset.html
@@ -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);
-var data = new vis.DataSet(options)
+var data = new vis.DataSet([data] [, options])
@@ -116,6 +116,11 @@ var data = new vis.DataSet(options)
Data Manipulation.
+
+ The parameter data
code> is optional and can be an Array or
+ Google DataTable with items.
+
+
The parameter options
is optional and is an object which can
contain the following properties:
@@ -545,8 +550,8 @@ var items = data.get({
One can subscribe on changes in a DataSet.
- A subscription can be created using the method subscribe
,
- and removed with unsubscribe
.
+ A subscription can be created using the method on
,
+ and removed with off
.
@@ -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
-Subscribe
+On
Subscribe to an event.
Syntax:
-DataSet.subscribe(event, callback)
+DataSet.on(event, callback)
Where:
@@ -587,17 +592,17 @@ Where:
-Unsubscribe
+Off
Unsubscribe from an event.
Syntax:
-DataSet.unsubscribe(event, callback)
+DataSet.off(event, callback)
Where event
and callback
correspond with the
-parameters used to subscribe to the event.
+parameters used to subscribe to the event.
Events
@@ -650,7 +655,7 @@ parameters used to subscribe to the event.
-function (event, params, senderId) {
+function (event, properties, senderId) {
// handle the event
});
@@ -674,13 +679,13 @@ function (event, params, senderId) {
- params |
+ properties |
Object | null |
- Optional parameters providing more information on the event.
+ Optional properties providing more information on the event.
In case of the events add ,
update , and remove ,
- params is always an object containing a property
+ properties is always an object containing a property
items, which contains an array with the ids of the affected
items.
|
diff --git a/docs/dataview.html b/docs/dataview.html
index 1698ffb1..a1fd350f 100644
--- a/docs/dataview.html
+++ b/docs/dataview.html
@@ -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...
diff --git a/docs/graph.html b/docs/graph.html
index 8dfe2594..bac86e1b 100644
--- a/docs/graph.html
+++ b/docs/graph.html
@@ -24,7 +24,14 @@
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 clustering support.
+
+
+
+ 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 Physics section or by
+ example 25.
@@ -53,9 +60,14 @@
Nodes
Edges
Groups
+ Physics
+ Data manipulation
Clustering
Navigation controls
Keyboard navigation
+ Hierarchical layout
+ Localization
+ Tooltips
Methods
@@ -288,6 +300,22 @@ var nodes = [
the same color schema.
+
+ allowedToMoveX |
+ Boolean |
+ false |
+ 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. |
+
+
+ allowedToMoveY |
+ Boolean |
+ false |
+ 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. |
+
fontColor |
String |
@@ -324,8 +352,21 @@ var nodes = [
string |
no |
Url of an image. Only applicable when the shape of the node is
- image . |
+ image
.
+
+
+ mass |
+ number |
+ 1 |
+ 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. |
+
+ level |
+ number |
+ -1 |
+ This level is used in the hierarchical layout. If this is not selected, the level does not do anything. |
+
radius |
@@ -377,10 +418,11 @@ var nodes = [
title |
- string |
+ string | function |
no |
Title to be displayed when the user hovers over the node.
- The title can contain HTML code. |
+ The title can contain HTML code. If using a function, returning undefined
+ will prevent the tooltip from being displayed.
@@ -449,12 +491,26 @@ var edges = [
Description |
-
- color |
- string |
- no |
- A HTML color for the edge. |
-
+
+ color |
+ String | Object |
+ no |
+ Color for the edge. |
+
+
+
+ color.color |
+ String |
+ no |
+ Color of the edge when not selected. |
+
+
+
+ color.highlight |
+ String |
+ no |
+ Color of the edge when selected. |
+
dash |
@@ -520,6 +576,14 @@ var edges = [
Only applicable when property label
is defined.
+
+ fontFill |
+ string |
+ no |
+ Font fill for the background color of the text label of the edge.
+ Only applicable when property label is defined. |
+
+
from |
Number | String |
@@ -529,13 +593,6 @@ var edges = [
type.
-
- length |
- number |
- no |
- The length of the edge in pixels. |
-
-
style |
string |
@@ -552,13 +609,19 @@ var edges = [
no |
Text label to be displayed halfway the edge. |
-
+
+ length |
+ number |
+ physics.[method].springLength |
+ 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. |
+
title |
- string |
+ string | function |
no |
Title to be displayed when the user hovers over the edge.
- The title can contain HTML code. |
+ The title can contain HTML code. If using a function, returning undefined
+ will prevent the tooltip from being displayed.
@@ -647,6 +710,34 @@ var options = {
Description |
+
+ physics |
+ Object |
+ none |
+
+ Configuration of the physics system governing the simulation of the nodes and edges.
+ Barnes-Hut nBody simulation is used by default. See section Physics for an overview of the available options.
+ |
+
+
+ configurePhysics |
+ Boolean |
+ false |
+
+ 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.
+ |
+
+
+
+ dataManipulation |
+ Object |
+ none |
+
+ Settings for manipulating the Dataset. See section Data manipulation for an overview of the available options.
+ |
+
+
clustering |
Object |
@@ -665,6 +756,17 @@ var options = {
+
+ freezeForStabilization |
+ Boolean |
+ false |
+
+ 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.
+ |
+
+
groups |
Object |
@@ -710,6 +812,13 @@ var options = {
+
+ smoothCurves |
+ Boolean |
+ true |
+ 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. |
+
+
selectable |
Boolean |
@@ -726,6 +835,14 @@ var options = {
the nodes move to a stabe position visibly in an animated way.
+
+ stabilizationIterations |
+ Number |
+ 1000 |
+ 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. |
+
+
width |
String |
@@ -806,6 +923,22 @@ var options = {
"#2B7CE9" |
Default border color of the node when selected. |
+
+ allowedToMoveX |
+ Boolean |
+ false |
+ 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. |
+
+
+ allowedToMoveY |
+ Boolean |
+ false |
+ 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. |
+
fontColor |
@@ -837,7 +970,19 @@ var options = {
none |
Default image url for the nodes. only applicable to shape image . |
-
+
+ mass |
+ number |
+ 1 |
+ 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. |
+
+
+ level |
+ number |
+ -1 |
+ This level is used in the hierarchical layout. If this is not selected, the level does not do anything. |
+
widthMin |
Number |
@@ -918,12 +1063,26 @@ var options = {
Description |
-
- color |
- String |
- "#2B7CE9" |
- The default color of a edge. |
-
+
+ color |
+ String | Object |
+ Object |
+ Colors of the edge. This object contains both colors for the selected and unselected state. |
+
+
+
+ color.color |
+ String |
+ "#848484" |
+ Color of the edge when not selected. |
+
+
+
+ color.highlight |
+ String |
+ "#848484" |
+ Color of the edge when selected. |
+
dash |
@@ -963,13 +1122,13 @@ var options = {
Default length of a gap in pixels on a dashed line.
Only applicable when the line style is dash-line . |
+
+ length |
+ number |
+ physics.[method].springLength |
+ 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. |
+
-
- length |
- Number |
- 100 |
- The default length of a edge. |
-
style |
String |
@@ -1122,6 +1281,248 @@ var nodes = [
+Physics
+
+ 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 Barnes-Hut 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.
+
+
Note: if the behaviour of your graph is not the way you want it, use configurePhysics as described below or by example 25.
+
+
+// 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
+ },
+ }
+
+barnesHut:
+
+
+ Name |
+ Type |
+ Default |
+ Description |
+
+
+ enabled |
+ Boolean |
+ true |
+ 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. |
+
+
+ gravitationalConstant |
+ Number |
+ -2000 |
+ This is the gravitational constand used to calculate the gravity forces. More information is available here. |
+
+
+ centralGravity |
+ Number |
+ 0.1 |
+ The central gravity is a force that pulls all nodes to the center. This ensures independent groups do not float apart. |
+
+
+ springLength |
+ Number |
+ 95 |
+ 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. |
+
+
+ springConstant |
+ Number |
+ 0.04 |
+ This is the spring constant used to calculate the spring forces based on Hooke′s Law. More information is available here. |
+
+
+ damping |
+ Number |
+ 0.09 |
+ 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 here. |
+
+
+repulsion:
+
+
+ Name |
+ Type |
+ Default |
+ Description |
+
+
+
+ centralGravity |
+ Number |
+ 0.1 |
+ The central gravity is a force that pulls all nodes to the center. This ensures independent groups do not float apart. |
+
+
+ springLength |
+ Number |
+ 50 |
+ 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. |
+
+
+ nodeDistance |
+ Number |
+ 100 |
+ 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. |
+
+
+
+ springConstant |
+ Number |
+ 0.05 |
+ This is the spring constant used to calculate the spring forces based on Hooke′s Law. More information is available here. |
+
+
+ damping |
+ Number |
+ 0.09 |
+ 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 here. |
+
+
+Configuration:
+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 disables smooth curves. These will not be enabled again afterwards.
+
+var options = {
+ configurePhysics:true
+}
+
+Data manipulation
+
+ 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 example 21,
+ 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 vis.css file must be included.
+ The user is free to alter or overload the CSS classes but without them the navigation icons are not visible.
+
+
+// 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
+}
+
+
+
+ Name |
+ Type |
+ Default |
+ Description |
+
+
+
+ enabled |
+ Boolean |
+ false |
+ Enabling or disabling of the data manipulation toolbar. If it is initially hidden, an edit button appears in the top left corner. |
+
+
+ initiallyVisible |
+ Boolean |
+ false |
+ Initially hide or show the data manipulation toolbar. |
+
+
+
+Data manipulation: custom functionality
+
+ 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. Example 21 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.
+ If there is no injected function supplied for the edit operation, the button will not be shown in the toolbar.
+
+
+// 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.
+ }
+};
+
+
+ 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.
+
+
+graph.on("resize", function(params) {console.log(params.width,params.height)});
+
+
Clustering
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: {
4.0 |
This parameter denotes the increase in fontSize of the cluster when a single node is added to it. |
+
+ maxFontSize |
+ Number |
+ 1000 |
+ This parameter denotes the largest allowed font size. If the font becomes too large, some browsers experience problems displaying this. |
+
forceAmplification |
Number |
@@ -1251,7 +1661,7 @@ var options: {
edgeGrowth |
Number |
- 11 |
+ 20 |
This factor determines the elongation of edges connected to a cluster. |
@@ -1272,56 +1682,50 @@ var options: {
10 |
This factor determines how much the radius of a cluster increases in pixels per added node. |
-
- activeAreaBoxSize |
+
+ maxNodeSizeIncrements |
+ Number |
+ 600 |
+ 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. |
+
+
+ activeAreaBoxSize |
+ Number |
+ 100 |
+ Imagine a square with an edge length of activeAreaBoxSize 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. |
+
+
+ clusterLevelDifference |
Number |
- 100 |
- Imagine a square with an edge length of activeAreaBoxSize 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. |
+ 2 |
+ 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. |
Navigation controls
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 vis.css file must be included.
+ The user is free to alter or overload the CSS classes but without them the navigation icons are not visible.
-// 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/'
- }
-}
-
-
- Name |
- Type |
- Default |
- Description |
-
-
-
- iconPath |
- string |
- "/img" |
- 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. |
-
-
-
Keyboard navigation
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 off. The predefined keys can be found in the example 20_navigation.html.
@@ -1372,6 +1776,192 @@ var options: {
+
+Hierarchical layout
+
+ 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 example 23 and the user-defined method is shown in example 24.
+ This layout method does not support smooth curves or clustering. It automatically turns these features off.
+
+
+
+// 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"
+ }
+}
+
+
+
+
+ Name |
+ Type |
+ Default |
+ Description |
+
+
+
+ enabled |
+ Boolean |
+ false |
+ Enable or disable the hierarchical layout.
+ |
+
+
+ levelSeparation |
+ Number |
+ 150 |
+ This defines the space between levels (in the Y-direction, considering UP-DOWN direction). |
+
+
+ nodeSpacing |
+ Number |
+ 100 |
+ 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. |
+
+
+ direction |
+ String |
+ UD |
+ 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. |
+
+
+
+Localization
+
+ When using vis.js in other languages, one can use the localization option to overwrite the labels used in the data manipulation interface.
+
+
+
+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."
+ }
+}
+
+
+
+
+ Name |
+ Type |
+ Default |
+ Description |
+
+
+
+ labels |
+ object |
+ (shown above) |
+ Overwrite one or all labels used in the datamanipulation interface with localized strings.
+ |
+
+
+
+
+
+ The behaviour and style of the tooltips used to display node and edge title attributes can be customized.
+
+
+
+// tooltip behaviour and style options
+var options: {
+ tooltip: {
+ delay: 300,
+ fontColor: "black",
+ fontSize: 14, // px
+ fontFace: "verdana",
+ color: {
+ border: "#666",
+ background: "#FFFFC6"
+ }
+ }
+}
+
+
+
+
+ Name |
+ Type |
+ Default |
+ Description |
+
+
+
+ delay |
+ Number |
+ 300 |
+ Time in milliseconds a user must hover over a node or edge before a tooltip appears. |
+
+
+ fontColor |
+ String |
+ "black" |
+ Default color for tooltip text. |
+
+
+ fontSize |
+ Number |
+ 14 |
+ Size in pixels of tooltip text. |
+
+
+ fontFace |
+ String |
+ "verdana" |
+ Font family to used for tooltip text. |
+
+
+ color.background |
+ String |
+ "#FFFFC6" |
+ Background color for the node. |
+
+
+ color.border |
+ String |
+ "#666" |
+ Border color for the node. |
+
+
+
+
Methods
Graph supports the following methods.
@@ -1392,6 +1982,13 @@ var options: {
The selections are not ordered.
+
+ storePosition() |
+ none |
+ 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.
+ |
+
on(event, callback) |
@@ -1449,6 +2046,12 @@ var options: {
or in percentages.
+
+ zoomExtent() |
+ none |
+ Scales the graph so all the nodes are in center view. |
+
+
Events
@@ -1505,9 +2108,49 @@ graph.off('select', onSelect);
nodes : an array with the ids of the selected nodes
+ edges : an array with the ids of the selected edges
|
+
+ click |
+ Fired after the user clicks or taps on a touchscreen. |
+
+
+ nodes : an array with the ids of the selected nodes
+ edges : an array with the ids of the selected edges
+
+ |
+
+
+ doubleClick |
+ Fired after the user double clicks or double taps on a touchscreen. |
+
+
+ nodes : an array with the ids of the selected nodes
+ edges : an array with the ids of the selected edges
+
+ |
+
+
+ resize |
+ Fired when the size of the canvas has been updated (not neccecarily changed) by the setSize() function or by the setOptions() function. |
+
+
+ width : the new width of the canvas
+ height : the new height of the canvas
+
+ |
+
+
+ stabilized |
+ Fired when the graph has been stabilized after initialization. This event can be used to trigger the .storePosition() function after stabilization. |
+
+
+ iterations : number of iterations used to stabilize
+
+ |
+
diff --git a/docs/index.html b/docs/index.html
index 0fcc85ca..75686de5 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -23,7 +23,9 @@
The library is developed by
- Almende B.V.
+ Almende B.V..
+ Vis.js runs fine on Chrome, Firefox, Opera, Safari, IE9+, and most mobile
+ browsers (with full touch support).
Components
diff --git a/docs/timeline.html b/docs/timeline.html
index eaaf2339..8a664610 100644
--- a/docs/timeline.html
+++ b/docs/timeline.html
@@ -39,6 +39,7 @@
Configuration Options
Methods
Events
+ Editing Items
Styles
Data Policy
@@ -194,8 +195,8 @@ var items = [
type |
String |
'box' |
- The type of the item. Can be 'box' (default), 'range', or 'point'.
-
+ | 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'.
|
@@ -215,13 +216,13 @@ var items = [
no |
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
-
- .red {
- background-color: red;
- border-color: dark-red;
- }
- .
+ 'red', one can define a css style like:
+
+.vis.timeline .red {
+ color: white;
+ background-color: red;
+ border-color: darkred;
+}
More details on how to style items can be found in the section
Styles.
|
@@ -309,7 +310,10 @@ var groups = [
var options = {
width: '100%',
- height: '30px'
+ height: '30px',
+ margin: {
+ item: 20
+ }
};
@@ -336,11 +340,19 @@ var options = {
autoResize |
boolean |
- false |
+ true |
If true, the Timeline will automatically detect when its
container is resized, and redraw itself accordingly. |
+
+ editable |
+ Boolean |
+ false |
+ If true, the items on the timeline can be dragged. Only applicable when option selectable is true . See also the callbacks onAdd , onUpdate , onMove , and onRemove , described in detail in section Editing Items.
+ |
+
+
end |
Date | Number | String |
@@ -412,6 +424,39 @@ var options = {
+
+ onAdd |
+ Function |
+ none |
+ Callback function triggered when an item is about to be added: when the user double taps an empty space in the Timeline. See section Editing Items for more information. Only applicable when both options selectable and editable are set true .
+ |
+
+
+
+ onUpdate |
+ Function |
+ none |
+ Callback function triggered when an item is about to be updated, when the user double taps an item in the Timeline. See section Editing Items for more information. Only applicable when both options selectable and editable are set true .
+ |
+
+
+
+ onMove |
+ Function |
+ none |
+ Callback function triggered when an item has been moved: after the user has dragged the item to an other position. See section Editing Items for more information. Only applicable when both options selectable and editable are set true .
+ |
+
+
+
+ onRemove |
+ Function |
+ none |
+ 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 Editing Items for more information. Only applicable when both options selectable and editable are set true .
+ |
+
+
+
orientation |
String |
'bottom' |
- 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. |
+ 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. |
@@ -437,7 +481,19 @@ var options = {
Number |
5 |
The padding of items, needed to correctly calculate the size
- of item ranges. Must correspond with the css of item ranges. |
+ of item ranges. Must correspond with the css of items, for example when setting options.padding=10
, corresponding css is:
+
+.vis.timeline .item {
+ padding: 10px;
+}
+
+
+
+
+ selectable |
+ Boolean |
+ true |
+ 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 select is fired each time the selection has changed (see section Events). |
@@ -451,9 +507,7 @@ var options = {
showCustomTime |
boolean |
false |
- 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.
- |
+ 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 timechange is fired repeatedly. After the bar is dragged, the event timechanged is fired once. |
@@ -493,8 +547,7 @@ var options = {
type |
String |
'box' |
- Specifies the type for the timeline items. Choose from 'dot' or 'point'.
- Note that individual items can override this global type.
+ | Specifies the type for the timeline items. Choose from 'box', 'point', 'range', and 'rangeoverflow'. Note that individual items can override this global type.
|
@@ -555,10 +608,16 @@ var options = {
getSelection() |
- ids |
+ Number[] |
Get an array with the ids of the currently selected items. |
+
+ getWindow() |
+ Object |
+ Get the current visible window. Returns an object with properties start: Date and end: Date . |
+
+
on(event, callback) |
none |
@@ -605,13 +664,19 @@ var options = {
+
+ setWindow(start, end) |
+ none |
+ Set the current visible window. The parameters start and end can be a Date , Number , or String . If the parameter value of start or end is null, the parameter will be left unchanged. |
+
+
Events
- 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.
@@ -674,7 +739,7 @@ timeline.off('select', onSelect);
rangechanged |
- Fired once after the user has dragging the timeline window.
+ | Fired once after the user has dragged the timeline window.
|
@@ -696,8 +761,75 @@ timeline.off('select', onSelect);
|
+
+ timechange |
+ Fired repeatedly when the user is dragging the custom time bar.
+ Only available when the custom time bar is enabled.
+ |
+
+
+ time (Date): the current time.
+
+ |
+
+
+
+ timechanged |
+ Fired once after the user has dragged the custom time bar.
+ Only available when the custom time bar is enabled.
+ |
+
+
+ time (Date): the current time.
+
+ |
+
+
+Editing Items
+
+ When the Timeline is configured to be editable (both options selectable
and editable
are true
), 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.
+
+
+
+ One can specify callback functions to validate changes made by the user. There are a number of callback functions for this purpose:
+
+
+
+ onAdd(item, callback)
Fired when a new item is about to be added. If not implemented, the item will be added with default text contents.
+ onUpdate(item, callback)
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.
+ onMove(item, callback)
Fired when an item has been moved. If not implemented, the move action will be accepted.
+ onRemove(item, callback)
Fired when an item is about to be deleted. If not implemented, the item will be always removed.
+
+
+
+ Each of the callbacks is invoked with two arguments:
+
+
+ item
: the item being manipulated
+ callback
: a callback function which must be invoked to report back. The callback must be invoked as callback(item | null)
. Here, item
can contain changes to the passed item. When invoked as callback(null)
, the action will be cancelled.
+
+
+
+ Example code:
+
+
+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
+ }
+ }
+}
+
+
+A full example is available here: 08_edit_items.html.
+
Styles
@@ -709,7 +841,7 @@ timeline.off('select', onSelect);
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.
<style>
- .graph .item {
+ .vis.timeline .item {
border-color: orange;
background-color: yellow;
}
diff --git a/examples/graph/02_random_nodes.html b/examples/graph/02_random_nodes.html
index f5a05364..b435fd56 100755
--- a/examples/graph/02_random_nodes.html
+++ b/examples/graph/02_random_nodes.html
@@ -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 @@
-
+
diff --git a/examples/graph/07_selections.html b/examples/graph/07_selections.html
index 98e44ade..cc0d8fe2 100644
--- a/examples/graph/07_selections.html
+++ b/examples/graph/07_selections.html
@@ -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 + '
';
+ graph.on('select', function(properties) {
+ document.getElementById('info').innerHTML += 'selection: ' + JSON.stringify(properties) + '
';
});
// set initial selection (id's of some nodes)
diff --git a/examples/graph/19_scale_free_graph_clustering.html b/examples/graph/19_scale_free_graph_clustering.html
index cced805b..ef3de8cd 100644
--- a/examples/graph/19_scale_free_graph_clustering.html
+++ b/examples/graph/19_scale_free_graph_clustering.html
@@ -88,9 +88,6 @@
};
*/
var options = {
- edges: {
- length: 50
- },
clustering: {
enabled: clusteringOn,
clusterEdgeThreshold: clusterEdgeThreshold
@@ -110,7 +107,7 @@
Clustering - Scale-Free-Graph
- This example shows therandomly generated
scale-free-graph set of nodes and connected edges from example 2.
+ This example shows the randomly generated
scale-free-graph 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).
diff --git a/examples/graph/20_navigation.html b/examples/graph/20_navigation.html
index cc9ed615..28a706ff 100644
--- a/examples/graph/20_navigation.html
+++ b/examples/graph/20_navigation.html
@@ -31,10 +31,10 @@
div.table_description {
width:100px;
}
-
+
+
+
+
+
+
+
+
Editing the dataset
+
+ 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.
+
+ 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.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/graph/22_les_miserables.html b/examples/graph/22_les_miserables.html
new file mode 100644
index 00000000..4f10ebc4
--- /dev/null
+++ b/examples/graph/22_les_miserables.html
@@ -0,0 +1,373 @@
+
+
+
+
Graph | Multiline text
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/graph/23_hierarchical_layout.html b/examples/graph/23_hierarchical_layout.html
new file mode 100644
index 00000000..508c9de6
--- /dev/null
+++ b/examples/graph/23_hierarchical_layout.html
@@ -0,0 +1,147 @@
+
+
+
+
Graph | Random nodes
+
+
+
+
+
+
+
+
+
+
Hierarchical Layout - Scale-Free-Graph
+
+ This example shows the randomly generated scale-free-graph set of nodes and connected edges from example 2.
+ In this example, hierarchical layout has been enabled and the vertical levels are determined automatically.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/graph/24_hierarchical_layout_userdefined.html b/examples/graph/24_hierarchical_layout_userdefined.html
new file mode 100644
index 00000000..0605485a
--- /dev/null
+++ b/examples/graph/24_hierarchical_layout_userdefined.html
@@ -0,0 +1,139 @@
+
+
+
+
Graph | Random nodes
+
+
+
+
+
+
+
+
+
+
Hierarchical Layout - User-defined
+
+ 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.
+
+
+
+
+
+
+
+
diff --git a/examples/graph/25_physics_configuration.html b/examples/graph/25_physics_configuration.html
new file mode 100644
index 00000000..7e32aed2
--- /dev/null
+++ b/examples/graph/25_physics_configuration.html
@@ -0,0 +1,110 @@
+
+
+
+
Graph | Playing with Physics
+
+
+
+
+
+
+
+
+
+
+
Playing with Physics
+
+ 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 disables smooth curves. These will not be enabled again afterwards.
+
+
+
+
+
+
+
diff --git a/examples/graph/graphviz/graphviz_gallery.html b/examples/graph/graphviz/graphviz_gallery.html
index 280ba6c5..f5d678e7 100644
--- a/examples/graph/graphviz/graphviz_gallery.html
+++ b/examples/graph/graphviz/graphviz_gallery.html
@@ -65,7 +65,7 @@