diff --git a/.gitignore b/.gitignore
index 7bd1502b..29c1ae2e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@ node_modules
.settings/org.eclipse.wst.jsdt.ui.superType.container
.settings/org.eclipse.wst.jsdt.ui.superType.name
npm-debug.log
+examples/graph/24_hierarchical_layout_userdefined2.html
diff --git a/HISTORY.md b/HISTORY.md
index 5a0e9d50..dfa0b20f 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -2,20 +2,71 @@
http://visjs.org
-## 2014-06-06, version 1.1.1
+## not yet released, version 3.0.1-SNAPSHOT
+
+
+## 2014-07-07, version 3.0.0
### Timeline
+- Implemented support for displaying a `title` for both items and groups.
+- Fixed auto detected item type being preferred over the global item `type`.
+- Throws an error when constructing without new keyword.
+- Removed the 'rangeoverflow' item type. Instead, one can use a regular range
+ and change css styling of the item contents to:
+
+ .vis.timeline .item.range .content {
+ overflow: visible;
+ }
+- Fixed the height of background and foreground panels of groups.
+- Fixed ranges in the Timeline sometimes overlapping when dragging the Timeline.
+- Fixed `DataView` not working in Timeline.
+
+### Network (formerly named Graph)
+
+- Renamed `Graph` to `Network` to prevent confusion with the visualizations
+ `Graph2d` and `Graph3d`.
+ - Renamed option `dragGraph` to `dragNetwork`.
+- Now throws an error when constructing without new keyword.
+- Added pull request from Vukk, user can now define the edge width multiplier
+ when selected.
+- Fixed `graph.storePositions()`.
+- Extended Selection API with `selectNodes` and `selectEdges`, deprecating
+ `setSelection`.
+- Fixed multiline labels.
+- Changed hierarchical physics solver and updated docs.
+
+### Graph2d
+
+- Added first iteration of the Graph2d.
+
+### Graph3d
+
+- Now throws an error when constructing without new keyword.
+
+
+## 2014-06-19, version 2.0.0
+
+### Timeline
+
+- Implemented function `destroy` to neatly cleanup a Timeline.
- Implemented support for dragging the timeline contents vertically.
- Implemented options `zoomable` and `moveable`.
- Changed default value of option `showCurrentTime` to true.
- Internal refactoring and simplification of the code.
+- Fixed property `className` of groups not being applied to related contents and
+ background elements, and not being updated once applied.
### Graph
- Reduced the timestep a little for smoother animations.
- Fixed dataManipulation.initiallyVisible functionality (thanks theGrue).
- Forced typecast of fontSize to Number.
+- Added editing of edges using the data manipulation toolkit.
+
+### DataSet
+
+- Renamed option `convert` to `type`.
## 2014-06-06, version 1.1.0
diff --git a/Jakefile.js b/Jakefile.js
index ea904115..53945f6d 100644
--- a/Jakefile.js
+++ b/Jakefile.js
@@ -46,8 +46,11 @@ task('build', {async: true}, function () {
'./src/timeline/component/css/customtime.css',
'./src/timeline/component/css/animation.css',
- './src/graph/css/graph-manipulation.css',
- './src/graph/css/graph-navigation.css'
+ './src/timeline/component/css/dataaxis.css',
+ './src/timeline/component/css/pathStyles.css',
+
+ './src/network/css/network-manipulation.css',
+ './src/network/css/network-navigation.css'
],
dest: VIS_CSS,
separator: '\n'
@@ -62,10 +65,17 @@ task('build', {async: true}, function () {
'./src/shim.js',
'./src/util.js',
+ './src/DOMutil.js',
'./src/DataSet.js',
'./src/DataView.js',
- './src/timeline/stack.js',
+ './src/timeline/component/GraphGroup.js',
+ './src/timeline/component/Legend.js',
+ './src/timeline/component/DataAxis.js',
+ './src/timeline/component/LineGraph.js',
+ './src/timeline/DataStep.js',
+
+ './src/timeline/Stack.js',
'./src/timeline/TimeStep.js',
'./src/timeline/Range.js',
'./src/timeline/component/Component.js',
@@ -76,26 +86,27 @@ task('build', {async: true}, function () {
'./src/timeline/component/item/*.js',
'./src/timeline/component/Group.js',
'./src/timeline/Timeline.js',
-
- './src/graph/dotparser.js',
- './src/graph/shapes.js',
- './src/graph/Node.js',
- './src/graph/Edge.js',
- './src/graph/Popup.js',
- './src/graph/Groups.js',
- './src/graph/Images.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/timeline/Graph2d.js',
+
+ './src/network/dotparser.js',
+ './src/network/shapes.js',
+ './src/network/Node.js',
+ './src/network/Edge.js',
+ './src/network/Popup.js',
+ './src/network/Groups.js',
+ './src/network/Images.js',
+ './src/network/networkMixins/physics/PhysicsMixin.js',
+ './src/network/networkMixins/physics/HierarchialRepulsion.js',
+ './src/network/networkMixins/physics/BarnesHut.js',
+ './src/network/networkMixins/physics/Repulsion.js',
+ './src/network/networkMixins/HierarchicalLayoutMixin.js',
+ './src/network/networkMixins/ManipulationMixin.js',
+ './src/network/networkMixins/SectorsMixin.js',
+ './src/network/networkMixins/ClusterMixin.js',
+ './src/network/networkMixins/SelectionMixin.js',
+ './src/network/networkMixins/NavigationMixin.js',
+ './src/network/networkMixins/MixinLoader.js',
+ './src/network/Network.js',
'./src/graph3d/Graph3d.js',
@@ -106,7 +117,7 @@ task('build', {async: true}, function () {
});
// copy images
- wrench.copyDirSyncRecursive('./src/graph/img', DIST + '/img/graph', {
+ wrench.copyDirSyncRecursive('./src/network/img', DIST + '/img/network', {
forceDelete: true
});
wrench.copyDirSyncRecursive('./src/timeline/img', DIST + '/img/timeline', {
diff --git a/README.md b/README.md
index 5aaac946..f2727a89 100644
--- a/README.md
+++ b/README.md
@@ -6,13 +6,14 @@ The library is designed to be easy to use, handle large amounts
of dynamic data, and enable manipulation of the data.
The library consists of the following components:
-- DataSet and DataView. A flexible key/value based data set.
- Add, update, and remove items. Subscribe on changes in the data set.
- Filter and order items and convert fields of items.
+- DataSet and DataView. A flexible key/value based data set. Add, update, and
+ remove items. Subscribe on changes in the data set. A DataSet can filter and
+ order items, and convert fields of items.
+- DataView. A filtered and/or formatted view on a DataSet.
+- Graph2d. Plot data on a timeline with lines or barcharts.
+- Graph3d. Display data in a three dimensional graph.
+- Network. Display a network (force directed graph) with nodes and edges.
- Timeline. Display different types of data on a timeline.
- The timeline and the items on the timeline can be interactively moved,
- zoomed, and manipulated.
-- Graph. Display an interactive graph or network with nodes and edges.
The vis.js library is developed by [Almende B.V](http://almende.com).
diff --git a/bower.json b/bower.json
index 336ebee4..19ddf1d3 100644
--- a/bower.json
+++ b/bower.json
@@ -1,6 +1,6 @@
{
"name": "vis",
- "version": "1.1.0",
+ "version": "3.0.1-SNAPSHOT",
"description": "A dynamic, browser-based visualization library.",
"homepage": "http://visjs.org/",
"repository": {
diff --git a/dist/img/graph/acceptDeleteIcon.png b/dist/img/network/acceptDeleteIcon.png
similarity index 100%
rename from dist/img/graph/acceptDeleteIcon.png
rename to dist/img/network/acceptDeleteIcon.png
diff --git a/dist/img/graph/addNodeIcon.png b/dist/img/network/addNodeIcon.png
similarity index 100%
rename from dist/img/graph/addNodeIcon.png
rename to dist/img/network/addNodeIcon.png
diff --git a/dist/img/graph/backIcon.png b/dist/img/network/backIcon.png
similarity index 100%
rename from dist/img/graph/backIcon.png
rename to dist/img/network/backIcon.png
diff --git a/dist/img/graph/connectIcon.png b/dist/img/network/connectIcon.png
similarity index 100%
rename from dist/img/graph/connectIcon.png
rename to dist/img/network/connectIcon.png
diff --git a/dist/img/graph/cross.png b/dist/img/network/cross.png
similarity index 100%
rename from dist/img/graph/cross.png
rename to dist/img/network/cross.png
diff --git a/dist/img/graph/cross2.png b/dist/img/network/cross2.png
similarity index 100%
rename from dist/img/graph/cross2.png
rename to dist/img/network/cross2.png
diff --git a/dist/img/graph/deleteIcon.png b/dist/img/network/deleteIcon.png
similarity index 100%
rename from dist/img/graph/deleteIcon.png
rename to dist/img/network/deleteIcon.png
diff --git a/dist/img/graph/downArrow.png b/dist/img/network/downArrow.png
similarity index 100%
rename from dist/img/graph/downArrow.png
rename to dist/img/network/downArrow.png
diff --git a/dist/img/graph/editIcon.png b/dist/img/network/editIcon.png
similarity index 100%
rename from dist/img/graph/editIcon.png
rename to dist/img/network/editIcon.png
diff --git a/dist/img/graph/leftArrow.png b/dist/img/network/leftArrow.png
similarity index 100%
rename from dist/img/graph/leftArrow.png
rename to dist/img/network/leftArrow.png
diff --git a/dist/img/graph/minus.png b/dist/img/network/minus.png
similarity index 100%
rename from dist/img/graph/minus.png
rename to dist/img/network/minus.png
diff --git a/dist/img/graph/plus.png b/dist/img/network/plus.png
similarity index 100%
rename from dist/img/graph/plus.png
rename to dist/img/network/plus.png
diff --git a/dist/img/graph/rightArrow.png b/dist/img/network/rightArrow.png
similarity index 100%
rename from dist/img/graph/rightArrow.png
rename to dist/img/network/rightArrow.png
diff --git a/dist/img/graph/upArrow.png b/dist/img/network/upArrow.png
similarity index 100%
rename from dist/img/graph/upArrow.png
rename to dist/img/network/upArrow.png
diff --git a/dist/img/graph/zoomExtends.png b/dist/img/network/zoomExtends.png
similarity index 100%
rename from dist/img/graph/zoomExtends.png
rename to dist/img/network/zoomExtends.png
diff --git a/dist/vis.css b/dist/vis.css
index 1f2c2de7..2a5d8a24 100644
--- a/dist/vis.css
+++ b/dist/vis.css
@@ -2,34 +2,76 @@
}
-.vis.timeline.rootpanel {
+.vis.timeline.root {
position: relative;
+ border: 1px solid #bfbfbf;
+
overflow: hidden;
+ padding: 0;
+ margin: 0;
- border: 1px solid #bfbfbf;
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 .vpanel {
+.vis.timeline .vispanel {
position: absolute;
- overflow: hidden;
+
+ padding: 0;
+ margin: 0;
box-sizing: border-box;
}
-.vis.timeline .vpanel.side {
- border-right: 1px solid #bfbfbf;
+.vis.timeline .vispanel.center,
+.vis.timeline .vispanel.left,
+.vis.timeline .vispanel.right,
+.vis.timeline .vispanel.top,
+.vis.timeline .vispanel.bottom {
+ border: 1px #bfbfbf;
}
-.vis.timeline .vpanel.side.hidden {
- display: none;
+.vis.timeline .vispanel.center,
+.vis.timeline .vispanel.left,
+.vis.timeline .vispanel.right {
+ border-top-style: solid;
+ border-bottom-style: solid;
+ overflow: hidden;
+}
+
+.vis.timeline .vispanel.center,
+.vis.timeline .vispanel.top,
+.vis.timeline .vispanel.bottom {
+ border-left-style: solid;
+ border-right-style: solid;
+}
+
+.vis.timeline .background {
+ overflow: hidden;
}
+.vis.timeline .vispanel > .content {
+ position: relative;
+}
+
+.vis.timeline .vispanel .shadow {
+ position: absolute;
+ width: 100%;
+ height: 1px;
+ box-shadow: 0 0 10px rgba(0,0,0,0.8);
+ /* TODO: find a nice way to ensure shadows are drawn on top of items
+ z-index: 1;
+ */
+}
+
+.vis.timeline .vispanel .shadow.top {
+ top: -1px;
+ left: 0;
+}
+
+.vis.timeline .vispanel .shadow.bottom {
+ bottom: -1px;
+ left: 0;
+}
.vis.timeline .labelset {
position: relative;
@@ -50,14 +92,12 @@
box-sizing: border-box;
}
-.vis.timeline.top .labelset .vlabel {
- border-top: 1px solid #bfbfbf;
- border-bottom: none;
+.vis.timeline .labelset .vlabel {
+ border-bottom: 1px solid #bfbfbf;
}
-.vis.timeline.bottom .labelset .vlabel {
- border-top: none;
- border-bottom: 1px solid #bfbfbf;
+.vis.timeline .labelset .vlabel:last-child {
+ border-bottom: none;
}
.vis.timeline .labelset .vlabel .inner {
@@ -65,6 +105,10 @@
padding: 5px;
}
+.vis.timeline .labelset .vlabel .inner.hidden {
+ padding: 0;
+}
+
.vis.timeline .itemset {
position: relative;
@@ -72,38 +116,33 @@
margin: 0;
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 {
-}
-
-.vis.timeline .foreground {
+.vis.timeline .itemset .background,
+.vis.timeline .itemset .foreground {
+ position: absolute;
+ width: 100%;
+ height: 100%;
}
.vis.timeline .axis {
- overflow: visible;
+ position: absolute;
+ width: 100%;
+ height: 0;
+ left: 1px;
+ z-index: 1;
}
-.vis.timeline .group {
+.vis.timeline .foreground .group {
position: relative;
box-sizing: border-box;
+ border-bottom: 1px solid #bfbfbf;
}
-.vis.timeline.top .group {
- border-top: 1px solid #bfbfbf;
+.vis.timeline .foreground .group:last-child {
border-bottom: none;
}
-.vis.timeline.bottom .group {
- border-top: none;
- border-bottom: 1px solid #bfbfbf;
-}
-
.vis.timeline .item {
position: absolute;
@@ -113,11 +152,6 @@
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 {
@@ -126,7 +160,7 @@
z-index: 999;
}
-.vis.timeline.editable .item.selected {
+.vis.timeline .editable .item.selected {
cursor: move;
}
@@ -152,15 +186,13 @@
border-radius: 4px;
}
-.vis.timeline .item.range,
-.vis.timeline .item.rangeoverflow{
+.vis.timeline .item.range {
border-style: solid;
border-radius: 2px;
box-sizing: border-box;
}
-.vis.timeline .item.range .content,
-.vis.timeline .item.rangeoverflow .content {
+.vis.timeline .item.range .content {
position: relative;
display: inline-block;
}
@@ -176,11 +208,6 @@
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 {
@@ -198,8 +225,7 @@
cursor: pointer;
}
-.vis.timeline .item.range .drag-left,
-.vis.timeline .item.rangeoverflow .drag-left {
+.vis.timeline .item.range .drag-left {
position: absolute;
width: 24px;
height: 100%;
@@ -210,8 +236,7 @@
z-index: 10000;
}
-.vis.timeline .item.range .drag-right,
-.vis.timeline .item.rangeoverflow .drag-right {
+.vis.timeline .item.range .drag-right {
position: absolute;
width: 24px;
height: 100%;
@@ -223,7 +248,22 @@
}
.vis.timeline .timeaxis {
+ position: relative;
+ overflow: hidden;
+}
+
+.vis.timeline .timeaxis.foreground {
+ top: 0;
+ left: 0;
+ width: 100%;
+}
+
+.vis.timeline .timeaxis.background {
position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
}
.vis.timeline .timeaxis .text {
@@ -248,14 +288,6 @@
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;
}
@@ -267,15 +299,218 @@
.vis.timeline .currenttime {
background-color: #FF7F6E;
width: 2px;
- z-index: 9;
+ z-index: 1;
}
.vis.timeline .customtime {
background-color: #6E94FF;
width: 2px;
cursor: move;
- z-index: 9;
+ z-index: 1;
+}
+.vis.timeline.root {
+ /*
+ -webkit-transition: height .4s ease-in-out;
+ transition: height .4s ease-in-out;
+ */
}
-div.graph-manipulationDiv {
+
+.vis.timeline .vispanel {
+ /*
+ -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 .axis {
+ /*
+ -webkit-transition: top .4s ease-in-out;
+ transition: top .4s ease-in-out;
+ */
+}
+
+/* TODO: get animation working nicely
+
+.vis.timeline .item {
+ -webkit-transition: top .4s ease-in-out;
+ transition: top .4s ease-in-out;
+}
+
+.vis.timeline .item.line {
+ -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 .vispanel.background.horizontal .grid.horizontal {
+ position: absolute;
+ width: 100%;
+ height: 0;
+ border-bottom: 1px solid;
+}
+
+.vis.timeline .vispanel.background.horizontal .grid.minor {
+ border-color: #e5e5e5;
+}
+
+.vis.timeline .vispanel.background.horizontal .grid.major {
+ border-color: #bfbfbf;
+}
+
+
+.vis.timeline .dataaxis .yAxis.major {
+ width: 100%;
+ position: absolute;
+ color: #4d4d4d;
+ white-space: nowrap;
+}
+
+.vis.timeline .dataaxis .yAxis.major.measure{
+ padding: 0px 0px 0px 0px;
+ margin: 0px 0px 0px 0px;
+ visibility: hidden;
+ width: auto;
+}
+
+
+.vis.timeline .dataaxis .yAxis.minor{
+ position: absolute;
+ width: 100%;
+ color: #bebebe;
+ white-space: nowrap;
+}
+
+.vis.timeline .dataaxis .yAxis.minor.measure{
+ padding: 0px 0px 0px 0px;
+ margin: 0px 0px 0px 0px;
+ visibility: hidden;
+ width: auto;
+}
+
+
+.vis.timeline .legend {
+ background-color: rgba(247, 252, 255, 0.65);
+ padding: 5px;
+ border-color: #b3b3b3;
+ border-style:solid;
+ border-width: 1px;
+ box-shadow: 2px 2px 10px rgba(154, 154, 154, 0.55);
+}
+
+.vis.timeline .legendText {
+ /*font-size: 10px;*/
+ white-space: nowrap;
+ display: inline-block
+}
+.vis.timeline .graphGroup0 {
+ fill:#4f81bd;
+ fill-opacity:0;
+ stroke-width:2px;
+ stroke: #4f81bd;
+}
+
+.vis.timeline .graphGroup1 {
+ fill:#f79646;
+ fill-opacity:0;
+ stroke-width:2px;
+ stroke: #f79646;
+}
+
+.vis.timeline .graphGroup2 {
+ fill: #8c51cf;
+ fill-opacity:0;
+ stroke-width:2px;
+ stroke: #8c51cf;
+}
+
+.vis.timeline .graphGroup3 {
+ fill: #75c841;
+ fill-opacity:0;
+ stroke-width:2px;
+ stroke: #75c841;
+}
+
+.vis.timeline .graphGroup4 {
+ fill: #ff0100;
+ fill-opacity:0;
+ stroke-width:2px;
+ stroke: #ff0100;
+}
+
+.vis.timeline .graphGroup5 {
+ fill: #37d8e6;
+ fill-opacity:0;
+ stroke-width:2px;
+ stroke: #37d8e6;
+}
+
+.vis.timeline .graphGroup6 {
+ fill: #042662;
+ fill-opacity:0;
+ stroke-width:2px;
+ stroke: #042662;
+}
+
+.vis.timeline .graphGroup7 {
+ fill:#00ff26;
+ fill-opacity:0;
+ stroke-width:2px;
+ stroke: #00ff26;
+}
+
+.vis.timeline .graphGroup8 {
+ fill:#ff00ff;
+ fill-opacity:0;
+ stroke-width:2px;
+ stroke: #ff00ff;
+}
+
+.vis.timeline .graphGroup9 {
+ fill: #8f3938;
+ fill-opacity:0;
+ stroke-width:2px;
+ stroke: #8f3938;
+}
+
+.vis.timeline .fill {
+ fill-opacity:0.1;
+ stroke: none;
+}
+
+
+.vis.timeline .bar {
+ fill-opacity:0.5;
+ stroke-width:1px;
+}
+
+.vis.timeline .point {
+ stroke-width:2px;
+ fill-opacity:1.0;
+}
+
+
+.vis.timeline .legendBackground {
+ stroke-width:1px;
+ fill-opacity:0.9;
+ fill: #ffffff;
+ stroke: #c2c2c2;
+}
+
+
+.vis.timeline .outline {
+ stroke-width:1px;
+ fill-opacity:1;
+ fill: #ffffff;
+ stroke: #e5e5e5;
+}
+
+.vis.timeline .iconFill {
+ fill-opacity:0.3;
+ stroke: none;
+}
+
+
+
+div.network-manipulationDiv {
border-width:0px;
border-bottom: 1px;
border-style:solid;
@@ -295,14 +530,14 @@ div.graph-manipulationDiv {
position:absolute;
}
-div.graph-manipulation-editMode {
+div.network-manipulation-editMode {
height:30px;
z-index:10;
position:absolute;
margin-top:20px;
}
-div.graph-manipulation-closeDiv {
+div.network-manipulation-closeDiv {
height:30px;
width:30px;
z-index:11;
@@ -311,7 +546,7 @@ div.graph-manipulation-closeDiv {
margin-left:590px;
background-position: 0px 0px;
background-repeat:no-repeat;
- background-image: url("img/graph/cross.png");
+ background-image: url("img/network/cross.png");
cursor: pointer;
-webkit-touch-callout: none;
-webkit-user-select: none;
@@ -321,7 +556,7 @@ div.graph-manipulation-closeDiv {
user-select: none;
}
-span.graph-manipulationUI {
+span.network-manipulationUI {
font-family: verdana;
font-size: 12px;
-moz-border-radius: 15px;
@@ -342,68 +577,68 @@ span.graph-manipulationUI {
user-select: none;
}
-span.graph-manipulationUI:hover {
+span.network-manipulationUI:hover {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.20);
}
-span.graph-manipulationUI:active {
+span.network-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.network-manipulationUI.back {
+ background-image: url("img/network/backIcon.png");
}
-span.graph-manipulationUI.none:hover {
+span.network-manipulationUI.none:hover {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0);
cursor: default;
}
-span.graph-manipulationUI.none:active {
+span.network-manipulationUI.none:active {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0);
}
-span.graph-manipulationUI.none {
+span.network-manipulationUI.none {
padding: 0px 0px 0px 0px;
}
-span.graph-manipulationUI.notification{
+span.network-manipulationUI.notification{
margin: 2px;
font-weight: bold;
}
-span.graph-manipulationUI.add {
- background-image: url("img/graph/addNodeIcon.png");
+span.network-manipulationUI.add {
+ background-image: url("img/network/addNodeIcon.png");
}
-span.graph-manipulationUI.edit {
- background-image: url("img/graph/editIcon.png");
+span.network-manipulationUI.edit {
+ background-image: url("img/network/editIcon.png");
}
-span.graph-manipulationUI.edit.editmode {
+span.network-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.network-manipulationUI.connect {
+ background-image: url("img/network/connectIcon.png");
}
-span.graph-manipulationUI.delete {
- background-image: url("img/graph/deleteIcon.png");
+span.network-manipulationUI.delete {
+ background-image: url("img/network/deleteIcon.png");
}
/* top right bottom left */
-span.graph-manipulationLabel {
+span.network-manipulationLabel {
margin: 0px 0px 0px 23px;
line-height: 25px;
}
-div.graph-seperatorLine {
+div.network-seperatorLine {
display:inline-block;
width:1px;
height:20px;
background-color: #bdbdbd;
margin: 5px 7px 0px 15px;
}
-div.graph-navigation {
+div.network-navigation {
width:34px;
height:34px;
z-index:10;
@@ -422,50 +657,50 @@ div.graph-navigation {
user-select: none;
}
-div.graph-navigation:hover {
+div.network-navigation:hover {
box-shadow: 0px 0px 3px 3px rgba(56, 207, 21, 0.30);
}
-div.graph-navigation:active {
+div.network-navigation:active {
box-shadow: 0px 0px 1px 3px rgba(56, 207, 21, 0.95);
}
-div.graph-navigation.active {
+div.network-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");
+div.network-navigation.up {
+ background-image: url("img/network/upArrow.png");
bottom:50px;
left:55px;
}
-div.graph-navigation.down {
- background-image: url("img/graph/downArrow.png");
+div.network-navigation.down {
+ background-image: url("img/network/downArrow.png");
bottom:10px;
left:55px;
}
-div.graph-navigation.left {
- background-image: url("img/graph/leftArrow.png");
+div.network-navigation.left {
+ background-image: url("img/network/leftArrow.png");
bottom:10px;
left:15px;
}
-div.graph-navigation.right {
- background-image: url("img/graph/rightArrow.png");
+div.network-navigation.right {
+ background-image: url("img/network/rightArrow.png");
bottom:10px;
left:95px;
}
-div.graph-navigation.zoomIn {
- background-image: url("img/graph/plus.png");
+div.network-navigation.zoomIn {
+ background-image: url("img/network/plus.png");
bottom:10px;
right:15px;
}
-div.graph-navigation.zoomOut {
- background-image: url("img/graph/minus.png");
+div.network-navigation.zoomOut {
+ background-image: url("img/network/minus.png");
bottom:10px;
right:55px;
}
-div.graph-navigation.zoomExtends {
- background-image: url("img/graph/zoomExtends.png");
+div.network-navigation.zoomExtends {
+ background-image: url("img/network/zoomExtends.png");
bottom:50px;
right:15px;
}
diff --git a/dist/vis.js b/dist/vis.js
index f1419461..bfad88c2 100644
--- a/dist/vis.js
+++ b/dist/vis.js
@@ -4,8 +4,8 @@
*
* A dynamic, browser-based visualization library.
*
- * @version 1.1.0
- * @date 2014-06-10
+ * @version 3.0.1-SNAPSHOT
+ * @date 2014-07-07
*
* @license
* Copyright (C) 2011-2014 Almende B.V, http://almende.com
@@ -318,7 +318,7 @@ var util = {};
* @param {*} object
* @return {Boolean} isNumber
*/
-util.isNumber = function isNumber(object) {
+util.isNumber = function(object) {
return (object instanceof Number || typeof object == 'number');
};
@@ -327,7 +327,7 @@ util.isNumber = function isNumber(object) {
* @param {*} object
* @return {Boolean} isString
*/
-util.isString = function isString(object) {
+util.isString = function(object) {
return (object instanceof String || typeof object == 'string');
};
@@ -336,7 +336,7 @@ util.isString = function isString(object) {
* @param {Date | String} object
* @return {Boolean} isDate
*/
-util.isDate = function isDate(object) {
+util.isDate = function(object) {
if (object instanceof Date) {
return true;
}
@@ -359,7 +359,7 @@ util.isDate = function isDate(object) {
* @param {*} object
* @return {Boolean} isDataTable
*/
-util.isDataTable = function isDataTable(object) {
+util.isDataTable = function(object) {
return (typeof (google) !== 'undefined') &&
(google.visualization) &&
(google.visualization.DataTable) &&
@@ -371,7 +371,7 @@ util.isDataTable = function isDataTable(object) {
* source: http://stackoverflow.com/a/105074/1262753
* @return {String} uuid
*/
-util.randomUUID = function randomUUID () {
+util.randomUUID = function() {
var S4 = function () {
return Math.floor(
Math.random() * 0x10000 /* 65536 */
@@ -398,7 +398,7 @@ util.extend = function (a, b) {
for (var i = 1, len = arguments.length; i < len; i++) {
var other = arguments[i];
for (var prop in other) {
- if (other.hasOwnProperty(prop) && other[prop] !== undefined) {
+ if (other.hasOwnProperty(prop)) {
a[prop] = other[prop];
}
}
@@ -407,13 +407,79 @@ util.extend = function (a, b) {
return a;
};
+/**
+ * Extend object a with selected properties of object b or a series of objects
+ * Only properties with defined values are copied
+ * @param {Array.} props
+ * @param {Object} a
+ * @param {... Object} b
+ * @return {Object} a
+ */
+util.selectiveExtend = function (props, a, b) {
+ if (!Array.isArray(props)) {
+ throw new Error('Array with property names expected as first argument');
+ }
+
+ for (var i = 2; i < arguments.length; i++) {
+ var other = arguments[i];
+
+ for (var p = 0; p < props.length; p++) {
+ var prop = props[p];
+ if (other.hasOwnProperty(prop)) {
+ a[prop] = other[prop];
+ }
+ }
+ }
+ return a;
+};
+
+/**
+ * Extend object a with selected properties of object b or a series of objects
+ * Only properties with defined values are copied
+ * @param {Array.} props
+ * @param {Object} a
+ * @param {... Object} b
+ * @return {Object} a
+ */
+util.selectiveDeepExtend = function (props, a, b) {
+ // TODO: add support for Arrays to deepExtend
+ if (Array.isArray(b)) {
+ throw new TypeError('Arrays are not supported by deepExtend');
+ }
+ for (var i = 2; i < arguments.length; i++) {
+ var other = arguments[i];
+ for (var p = 0; p < props.length; p++) {
+ var prop = props[p];
+ if (other.hasOwnProperty(prop)) {
+ if (b[prop] && b[prop].constructor === Object) {
+ if (a[prop] === undefined) {
+ a[prop] = {};
+ }
+ if (a[prop].constructor === Object) {
+ util.deepExtend(a[prop], b[prop]);
+ }
+ else {
+ a[prop] = b[prop];
+ }
+ } else if (Array.isArray(b[prop])) {
+ throw new TypeError('Arrays are not supported by deepExtend');
+ } else {
+ a[prop] = b[prop];
+ }
+
+ }
+ }
+ }
+ return a;
+};
+
/**
* Deep extend an object a with the properties of object b
* @param {Object} a
* @param {Object} b
* @returns {Object}
*/
-util.deepExtend = function deepExtend (a, b) {
+util.deepExtend = function(a, b) {
// TODO: add support for Arrays to deepExtend
if (Array.isArray(b)) {
throw new TypeError('Arrays are not supported by deepExtend');
@@ -426,7 +492,7 @@ util.deepExtend = function deepExtend (a, b) {
a[prop] = {};
}
if (a[prop].constructor === Object) {
- deepExtend(a[prop], b[prop]);
+ util.deepExtend(a[prop], b[prop]);
}
else {
a[prop] = b[prop];
@@ -467,7 +533,7 @@ util.equalArray = function (a, b) {
* @return {*} object
* @throws Error
*/
-util.convert = function convert(object, type) {
+util.convert = function(object, type) {
var match;
if (object === undefined) {
@@ -602,8 +668,7 @@ util.convert = function convert(object, type) {
}
default:
- throw new Error('Cannot convert object of type ' + util.getType(object) +
- ' to type "' + type + '"');
+ throw new Error('Unknown type "' + type + '"');
}
};
@@ -617,7 +682,7 @@ var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
* @param {*} object
* @return {String} type
*/
-util.getType = function getType(object) {
+util.getType = function(object) {
var type = typeof object;
if (type == 'object') {
@@ -660,7 +725,7 @@ util.getType = function getType(object) {
* @return {number} left The absolute left position of this element
* in the browser page.
*/
-util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
+util.getAbsoluteLeft = function(elem) {
var doc = document.documentElement;
var body = document.body;
@@ -680,7 +745,7 @@ util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
* @return {number} top The absolute top position of this element
* in the browser page.
*/
-util.getAbsoluteTop = function getAbsoluteTop (elem) {
+util.getAbsoluteTop = function(elem) {
var doc = document.documentElement;
var body = document.body;
@@ -699,7 +764,7 @@ util.getAbsoluteTop = function getAbsoluteTop (elem) {
* @param {Event} event
* @return {Number} pageY
*/
-util.getPageY = function getPageY (event) {
+util.getPageY = function(event) {
if ('pageY' in event) {
return event.pageY;
}
@@ -725,7 +790,7 @@ util.getPageY = function getPageY (event) {
* @param {Event} event
* @return {Number} pageX
*/
-util.getPageX = function getPageX (event) {
+util.getPageX = function(event) {
if ('pageY' in event) {
return event.pageX;
}
@@ -751,7 +816,7 @@ util.getPageX = function getPageX (event) {
* @param {Element} elem
* @param {String} className
*/
-util.addClassName = function addClassName(elem, className) {
+util.addClassName = function(elem, className) {
var classes = elem.className.split(' ');
if (classes.indexOf(className) == -1) {
classes.push(className); // add the class to the array
@@ -764,7 +829,7 @@ util.addClassName = function addClassName(elem, className) {
* @param {Element} elem
* @param {String} className
*/
-util.removeClassName = function removeClassname(elem, className) {
+util.removeClassName = function(elem, className) {
var classes = elem.className.split(' ');
var index = classes.indexOf(className);
if (index != -1) {
@@ -782,7 +847,7 @@ util.removeClassName = function removeClassname(elem, className) {
* the object or array with three parameters:
* callback(value, index, object)
*/
-util.forEach = function forEach (object, callback) {
+util.forEach = function(object, callback) {
var i,
len;
if (object instanceof Array) {
@@ -807,7 +872,7 @@ util.forEach = function forEach (object, callback) {
* @param {Object} object
* @param {Array} array
*/
-util.toArray = function toArray(object) {
+util.toArray = function(object) {
var array = [];
for (var prop in object) {
@@ -824,7 +889,7 @@ util.toArray = function toArray(object) {
* @param {*} value
* @return {Boolean} changed
*/
-util.updateProperty = function updateProperty (object, key, value) {
+util.updateProperty = function(object, key, value) {
if (object[key] !== value) {
object[key] = value;
return true;
@@ -842,7 +907,7 @@ util.updateProperty = function updateProperty (object, key, value) {
* @param {function} listener The callback function to be executed
* @param {boolean} [useCapture]
*/
-util.addEventListener = function addEventListener(element, action, listener, useCapture) {
+util.addEventListener = function(element, action, listener, useCapture) {
if (element.addEventListener) {
if (useCapture === undefined)
useCapture = false;
@@ -864,7 +929,7 @@ util.addEventListener = function addEventListener(element, action, listener, use
* @param {function} listener The listener function
* @param {boolean} [useCapture]
*/
-util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
+util.removeEventListener = function(element, action, listener, useCapture) {
if (element.removeEventListener) {
// non-IE browsers
if (useCapture === undefined)
@@ -887,7 +952,7 @@ util.removeEventListener = function removeEventListener(element, action, listene
* @param {Event} event
* @return {Element} target element
*/
-util.getTarget = function getTarget(event) {
+util.getTarget = function(event) {
// code from http://www.quirksmode.org/js/events_properties.html
if (!event) {
event = window.event;
@@ -915,7 +980,7 @@ util.getTarget = function getTarget(event) {
* @param {Element} element
* @param {Event} event
*/
-util.fakeGesture = function fakeGesture (element, event) {
+util.fakeGesture = function(element, event) {
var eventType = null;
// for hammer.js 1.0.5
@@ -1031,7 +1096,7 @@ util.option.asElement = function (value, defaultValue) {
-util.GiveDec = function GiveDec(Hex) {
+util.GiveDec = function(Hex) {
var Value;
if (Hex == "A")
@@ -1052,7 +1117,7 @@ util.GiveDec = function GiveDec(Hex) {
return Value;
};
-util.GiveHex = function GiveHex(Dec) {
+util.GiveHex = function(Dec) {
var Value;
if(Dec == 10)
@@ -1156,7 +1221,7 @@ util.parseColor = function(color) {
* @param {String} hex
* @returns {{r: *, g: *, b: *}}
*/
-util.hexToRGB = function hexToRGB(hex) {
+util.hexToRGB = function(hex) {
hex = hex.replace("#","").toUpperCase();
var a = util.GiveDec(hex.substring(0, 1));
@@ -1173,7 +1238,7 @@ util.hexToRGB = function hexToRGB(hex) {
return {r:r,g:g,b:b};
};
-util.RGBToHex = function RGBToHex(red,green,blue) {
+util.RGBToHex = function(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));
@@ -1195,7 +1260,7 @@ util.RGBToHex = function RGBToHex(red,green,blue) {
* @returns {*}
* @constructor
*/
-util.RGBToHSV = function RGBToHSV (red,green,blue) {
+util.RGBToHSV = function(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));
@@ -1223,7 +1288,7 @@ util.RGBToHSV = function RGBToHSV (red,green,blue) {
* @returns {{r: number, g: number, b: number}}
* @constructor
*/
-util.HSVToRGB = function HSVToRGB(h, s, v) {
+util.HSVToRGB = function(h, s, v) {
var r, g, b;
var i = Math.floor(h * 6);
@@ -1244,1271 +1309,3708 @@ util.HSVToRGB = function HSVToRGB(h, s, v) {
return {r:Math.floor(r * 255), g:Math.floor(g * 255), b:Math.floor(b * 255) };
};
-util.HSVToHex = function HSVToHex(h, s, v) {
+util.HSVToHex = function(h, s, v) {
var rgb = util.HSVToRGB(h, s, v);
return util.RGBToHex(rgb.r, rgb.g, rgb.b);
};
-util.hexToHSV = function hexToHSV(hex) {
+util.hexToHSV = function(hex) {
var rgb = util.hexToRGB(hex);
return util.RGBToHSV(rgb.r, rgb.g, rgb.b);
};
-util.isValidHex = function isValidHex(hex) {
+util.isValidHex = function(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];
- }
- }
- }
-};
/**
- * DataSet
- *
- * Usage:
- * var dataSet = new DataSet({
- * fieldId: '_id',
- * convert: {
- * // ...
- * }
- * });
- *
- * dataSet.add(item);
- * dataSet.add(data);
- * dataSet.update(item);
- * dataSet.update(data);
- * dataSet.remove(id);
- * dataSet.remove(ids);
- * var data = dataSet.get();
- * var data = dataSet.get(id);
- * var data = dataSet.get(ids);
- * var data = dataSet.get(ids, options, data);
- * dataSet.clear();
- *
- * A data set can:
- * - add/remove/update data
- * - gives triggers upon changes in the data
- * - can import/export data in various data formats
+ * This recursively redirects the prototype of JSON objects to the referenceObject
+ * This is used for default options.
*
- * @param {Array | DataTable} [data] Optional array with initial data
- * @param {Object} [options] Available options:
- * {String} fieldId Field name of the id in the
- * items, 'id' by default.
- * {Object. range.start - interval) && (value < range.end)) {
+ guess = 0;
+ }
+ else {
+ guess = -1;
}
- }
- else if (data instanceof Object) {
- // Single item
- id = me._addItem(data);
- addedIds.push(id);
}
else {
- throw new Error('Unknown dataType');
- }
-
- if (addedIds.length) {
- this._trigger('add', {items: addedIds}, senderId);
+ high -= 1;
+ while (found == false) {
+ value = field2 === undefined ? array[guess][field] : array[guess][field][field2];
+ if ((value > range.start - interval) && (value < range.end)) {
+ found = true;
+ }
+ else {
+ if (value < range.start - interval) { // it is too small --> increase low
+ low = Math.floor(0.5*(high+low));
+ }
+ else { // it is too big --> decrease high
+ high = Math.floor(0.5*(high+low));
+ }
+ newGuess = Math.floor(0.5*(high+low));
+ // not in list;
+ if (guess == newGuess) {
+ guess = -1;
+ found = true;
+ }
+ else {
+ guess = newGuess;
+ }
+ }
+ }
}
-
- return addedIds;
+ return guess;
};
/**
- * Update existing items. When an item does not exist, it will be created
- * @param {Object | Array | DataTable} data
- * @param {String} [senderId] Optional sender id
- * @return {Array} updatedIds The ids of the added or updated items
+ * This function does a binary search for a visible item. The user can select either the this.orderedItems.byStart or .byEnd
+ * arrays. This is done by giving a boolean value true if you want to use the byEnd.
+ * This is done to be able to select the correct if statement (we do not want to check if an item is visible, we want to check
+ * if the time we selected (start or end) is within the current range).
+ *
+ * The trick is that every interval has to either enter the screen at the initial load or by dragging. The case of the ItemRange that is
+ * before and after the current range is handled by simply checking if it was in view before and if it is again. For all the rest,
+ * either the start OR end time has to be in the range.
+ *
+ * @param {Array} orderedItems
+ * @param {{start: number, end: number}} target
+ * @param {Boolean} byEnd
+ * @returns {number}
+ * @private
*/
-DataSet.prototype.update = function (data, senderId) {
- var addedIds = [],
- updatedIds = [],
- me = this,
- fieldId = me.fieldId;
+util.binarySearchGeneric = function(orderedItems, target, field, sidePreference) {
+ var array = orderedItems;
+ var found = false;
+ var low = 0;
+ var high = array.length;
+ var guess = Math.floor(0.5*(high+low));
+ var newGuess;
+ var prevValue, value, nextValue;
- var addOrUpdate = function (item) {
- var id = item[fieldId];
- if (me.data[id]) {
- // update item
- id = me._updateItem(item);
- updatedIds.push(id);
+ if (high == 0) {guess = -1;}
+ else if (high == 1) {
+ value = array[guess][field];
+ if (value == target) {
+ guess = 0;
}
else {
- // add new item
- id = me._addItem(item);
- addedIds.push(id);
+ guess = -1;
}
- };
+ }
+ else {
+ high -= 1;
+ while (found == false) {
+ prevValue = array[Math.max(0,guess - 1)][field];
+ value = array[guess][field];
+ nextValue = array[Math.min(array.length-1,guess + 1)][field];
- if (data instanceof Array) {
- // Array
- for (var i = 0, len = data.length; i < len; i++) {
- addOrUpdate(data[i]);
+ if (value == target || prevValue < target && value > target || value < target && nextValue > target) {
+ found = true;
+ if (value != target) {
+ if (sidePreference == 'before') {
+ if (prevValue < target && value > target) {
+ guess = Math.max(0,guess - 1);
+ }
+ }
+ else {
+ if (value < target && nextValue > target) {
+ guess = Math.min(array.length-1,guess + 1);
+ }
+ }
+ }
+ }
+ else {
+ if (value < target) { // it is too small --> increase low
+ low = Math.floor(0.5*(high+low));
+ }
+ else { // it is too big --> decrease high
+ high = Math.floor(0.5*(high+low));
+ }
+ newGuess = Math.floor(0.5*(high+low));
+ // not in list;
+ if (guess == newGuess) {
+ guess = -2;
+ found = true;
+ }
+ else {
+ guess = newGuess;
+ }
+ }
}
}
- else if (util.isDataTable(data)) {
- // Google DataTable
- var columns = this._getColumnNames(data);
- for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
- var item = {};
- for (var col = 0, cols = columns.length; col < cols; col++) {
- var field = columns[col];
- item[field] = data.getValue(row, col);
- }
+ return guess;
+};
+/**
+ * Created by Alex on 6/20/14.
+ */
- addOrUpdate(item);
+var DOMutil = {};
+
+/**
+ * this prepares the JSON container for allocating SVG elements
+ * @param JSONcontainer
+ * @private
+ */
+DOMutil.prepareElements = function(JSONcontainer) {
+ // cleanup the redundant svgElements;
+ for (var elementType in JSONcontainer) {
+ if (JSONcontainer.hasOwnProperty(elementType)) {
+ JSONcontainer[elementType].redundant = JSONcontainer[elementType].used;
+ JSONcontainer[elementType].used = [];
}
}
- else if (data instanceof Object) {
- // Single item
- addOrUpdate(data);
+};
+
+/**
+ * this cleans up all the unused SVG elements. By asking for the parentNode, we only need to supply the JSON container from
+ * which to remove the redundant elements.
+ *
+ * @param JSONcontainer
+ * @private
+ */
+DOMutil.cleanupElements = function(JSONcontainer) {
+ // cleanup the redundant svgElements;
+ for (var elementType in JSONcontainer) {
+ if (JSONcontainer.hasOwnProperty(elementType)) {
+ if (JSONcontainer[elementType].redundant) {
+ for (var i = 0; i < JSONcontainer[elementType].redundant.length; i++) {
+ JSONcontainer[elementType].redundant[i].parentNode.removeChild(JSONcontainer[elementType].redundant[i]);
+ }
+ JSONcontainer[elementType].redundant = [];
+ }
+ }
+ }
+};
+
+/**
+ * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer
+ * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this.
+ *
+ * @param elementType
+ * @param JSONcontainer
+ * @param svgContainer
+ * @returns {*}
+ * @private
+ */
+DOMutil.getSVGElement = function (elementType, JSONcontainer, svgContainer) {
+ var element;
+ // allocate SVG element, if it doesnt yet exist, create one.
+ if (JSONcontainer.hasOwnProperty(elementType)) { // this element has been created before
+ // check if there is an redundant element
+ if (JSONcontainer[elementType].redundant.length > 0) {
+ element = JSONcontainer[elementType].redundant[0];
+ JSONcontainer[elementType].redundant.shift();
+ }
+ else {
+ // create a new element and add it to the SVG
+ element = document.createElementNS('http://www.w3.org/2000/svg', elementType);
+ svgContainer.appendChild(element);
+ }
}
else {
- throw new Error('Unknown dataType');
+ // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it.
+ element = document.createElementNS('http://www.w3.org/2000/svg', elementType);
+ JSONcontainer[elementType] = {used: [], redundant: []};
+ svgContainer.appendChild(element);
}
+ JSONcontainer[elementType].used.push(element);
+ return element;
+};
- if (addedIds.length) {
- this._trigger('add', {items: addedIds}, senderId);
+
+/**
+ * Allocate or generate an SVG element if needed. Store a reference to it in the JSON container and draw it in the svgContainer
+ * the JSON container and the SVG container have to be supplied so other svg containers (like the legend) can use this.
+ *
+ * @param elementType
+ * @param JSONcontainer
+ * @param DOMContainer
+ * @returns {*}
+ * @private
+ */
+DOMutil.getDOMElement = function (elementType, JSONcontainer, DOMContainer) {
+ var element;
+ // allocate SVG element, if it doesnt yet exist, create one.
+ if (JSONcontainer.hasOwnProperty(elementType)) { // this element has been created before
+ // check if there is an redundant element
+ if (JSONcontainer[elementType].redundant.length > 0) {
+ element = JSONcontainer[elementType].redundant[0];
+ JSONcontainer[elementType].redundant.shift();
+ }
+ else {
+ // create a new element and add it to the SVG
+ element = document.createElement(elementType);
+ DOMContainer.appendChild(element);
+ }
}
- if (updatedIds.length) {
- this._trigger('update', {items: updatedIds}, senderId);
+ else {
+ // create a new element and add it to the SVG, also create a new object in the svgElements to keep track of it.
+ element = document.createElement(elementType);
+ JSONcontainer[elementType] = {used: [], redundant: []};
+ DOMContainer.appendChild(element);
}
-
- return addedIds.concat(updatedIds);
+ JSONcontainer[elementType].used.push(element);
+ return element;
};
+
+
+
/**
- * Get a data item or multiple items.
- *
- * Usage:
+ * draw a point object. this is a seperate function because it can also be called by the legend.
+ * The reason the JSONcontainer and the target SVG svgContainer have to be supplied is so the legend can use these functions
+ * as well.
*
- * get()
- * get(options: Object)
- * get(options: Object, data: Array | DataTable)
+ * @param x
+ * @param y
+ * @param group
+ * @param JSONcontainer
+ * @param svgContainer
+ * @returns {*}
+ */
+DOMutil.drawPoint = function(x, y, group, JSONcontainer, svgContainer) {
+ var point;
+ if (group.options.drawPoints.style == 'circle') {
+ point = DOMutil.getSVGElement('circle',JSONcontainer,svgContainer);
+ point.setAttributeNS(null, "cx", x);
+ point.setAttributeNS(null, "cy", y);
+ point.setAttributeNS(null, "r", 0.5 * group.options.drawPoints.size);
+ point.setAttributeNS(null, "class", group.className + " point");
+ }
+ else {
+ point = DOMutil.getSVGElement('rect',JSONcontainer,svgContainer);
+ point.setAttributeNS(null, "x", x - 0.5*group.options.drawPoints.size);
+ point.setAttributeNS(null, "y", y - 0.5*group.options.drawPoints.size);
+ point.setAttributeNS(null, "width", group.options.drawPoints.size);
+ point.setAttributeNS(null, "height", group.options.drawPoints.size);
+ point.setAttributeNS(null, "class", group.className + " point");
+ }
+ return point;
+};
+
+/**
+ * draw a bar SVG element centered on the X coordinate
*
- * get(id: Number | String)
- * get(id: Number | String, options: Object)
- * get(id: Number | String, options: Object, data: Array | DataTable)
+ * @param x
+ * @param y
+ * @param className
+ */
+DOMutil.drawBar = function (x, y, width, height, className, JSONcontainer, svgContainer) {
+ var rect = DOMutil.getSVGElement('rect',JSONcontainer, svgContainer);
+ rect.setAttributeNS(null, "x", x - 0.5 * width);
+ rect.setAttributeNS(null, "y", y);
+ rect.setAttributeNS(null, "width", width);
+ rect.setAttributeNS(null, "height", height);
+ rect.setAttributeNS(null, "class", className);
+};
+/**
+ * DataSet
*
- * get(ids: Number[] | String[])
- * get(ids: Number[] | String[], options: Object)
- * get(ids: Number[] | String[], options: Object, data: Array | DataTable)
+ * Usage:
+ * var dataSet = new DataSet({
+ * fieldId: '_id',
+ * type: {
+ * // ...
+ * }
+ * });
*
- * Where:
+ * dataSet.add(item);
+ * dataSet.add(data);
+ * dataSet.update(item);
+ * dataSet.update(data);
+ * dataSet.remove(id);
+ * dataSet.remove(ids);
+ * var data = dataSet.get();
+ * var data = dataSet.get(id);
+ * var data = dataSet.get(ids);
+ * var data = dataSet.get(ids, options, data);
+ * dataSet.clear();
*
- * {Number | String} id The id of an item
- * {Number[] | String{}} ids An array with ids of items
- * {Object} options An Object with options. Available options:
- * {String} [type] Type of data to be returned. Can
- * be 'DataTable' or 'Array' (default)
- * {Object.} [convert]
- * {String[]} [fields] field names to be returned
- * {function} [filter] filter items
- * {String | function} [order] Order the items by
- * a field name or custom sort function.
- * {Array | DataTable} [data] If provided, items will be appended to this
- * array or table. Required in case of Google
- * DataTable.
+ * A data set can:
+ * - add/remove/update data
+ * - gives triggers upon changes in the data
+ * - can import/export data in various data formats
*
- * @throws Error
+ * @param {Array | DataTable} [data] Optional array with initial data
+ * @param {Object} [options] Available options:
+ * {String} fieldId Field name of the id in the
+ * items, 'id' by default.
+ * {Object.} [type]
+ * {String[]} [fields] field names to be returned
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ * {Array | DataTable} [data] If provided, items will be appended to this
+ * array or table. Required in case of Google
+ * DataTable.
+ *
+ * @throws Error
+ */
+DataSet.prototype.get = function (args) {
+ var me = this;
+
+ // parse the arguments
+ var id, ids, options, data;
+ var firstType = util.getType(arguments[0]);
+ if (firstType == 'String' || firstType == 'Number') {
+ // get(id [, options] [, data])
+ id = arguments[0];
+ options = arguments[1];
+ data = arguments[2];
+ }
+ else if (firstType == 'Array') {
+ // get(ids [, options] [, data])
+ ids = arguments[0];
+ options = arguments[1];
+ data = arguments[2];
+ }
+ else {
+ // get([, options] [, data])
+ options = arguments[0];
+ data = arguments[1];
+ }
+
+ // determine the return type
+ var returnType;
+ if (options && options.returnType) {
+ returnType = (options.returnType == 'DataTable') ? 'DataTable' : 'Array';
+
+ if (data && (returnType != util.getType(data))) {
+ throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
+ 'does not correspond with specified options.type (' + options.type + ')');
+ }
+ if (returnType == 'DataTable' && !util.isDataTable(data)) {
+ throw new Error('Parameter "data" must be a DataTable ' +
+ 'when options.type is "DataTable"');
+ }
+ }
+ else if (data) {
+ returnType = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
+ }
+ else {
+ returnType = 'Array';
+ }
+
+ // build options
+ var type = options && options.type || this._options.type;
+ var filter = options && options.filter;
+ var items = [], item, itemId, i, len;
+
+ // convert items
+ if (id != undefined) {
+ // return a single item
+ item = me._getItem(id, type);
+ if (filter && !filter(item)) {
+ item = null;
+ }
+ }
+ else if (ids != undefined) {
+ // return a subset of items
+ for (i = 0, len = ids.length; i < len; i++) {
+ item = me._getItem(ids[i], type);
+ if (!filter || filter(item)) {
+ items.push(item);
+ }
+ }
+ }
+ else {
+ // return all items
+ for (itemId in this._data) {
+ if (this._data.hasOwnProperty(itemId)) {
+ item = me._getItem(itemId, type);
+ if (!filter || filter(item)) {
+ items.push(item);
+ }
+ }
+ }
+ }
+
+ // order the results
+ if (options && options.order && id == undefined) {
+ this._sort(items, options.order);
+ }
+
+ // filter fields of the items
+ if (options && options.fields) {
+ var fields = options.fields;
+ if (id != undefined) {
+ item = this._filterFields(item, fields);
+ }
+ else {
+ for (i = 0, len = items.length; i < len; i++) {
+ items[i] = this._filterFields(items[i], fields);
+ }
+ }
+ }
+
+ // return the results
+ if (returnType == 'DataTable') {
+ var columns = this._getColumnNames(data);
+ if (id != undefined) {
+ // append a single item to the data table
+ me._appendRow(data, columns, item);
+ }
+ else {
+ // copy the items to the provided data table
+ for (i = 0, len = items.length; i < len; i++) {
+ me._appendRow(data, columns, items[i]);
+ }
+ }
+ return data;
+ }
+ else {
+ // return an array
+ if (id != undefined) {
+ // a single item
+ return item;
+ }
+ else {
+ // multiple items
+ if (data) {
+ // copy the items to the provided array
+ for (i = 0, len = items.length; i < len; i++) {
+ data.push(items[i]);
+ }
+ return data;
+ }
+ else {
+ // just return our array
+ return items;
+ }
+ }
+ }
+};
+
+/**
+ * Get ids of all items or from a filtered set of items.
+ * @param {Object} [options] An Object with options. Available options:
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ * @return {Array} ids
+ */
+DataSet.prototype.getIds = function (options) {
+ var data = this._data,
+ filter = options && options.filter,
+ order = options && options.order,
+ type = options && options.type || this._options.type,
+ i,
+ len,
+ id,
+ item,
+ items,
+ ids = [];
+
+ if (filter) {
+ // get filtered items
+ if (order) {
+ // create ordered list
+ items = [];
+ for (id in data) {
+ if (data.hasOwnProperty(id)) {
+ item = this._getItem(id, type);
+ if (filter(item)) {
+ items.push(item);
+ }
+ }
+ }
+
+ this._sort(items, order);
+
+ for (i = 0, len = items.length; i < len; i++) {
+ ids[i] = items[i][this._fieldId];
+ }
+ }
+ else {
+ // create unordered list
+ for (id in data) {
+ if (data.hasOwnProperty(id)) {
+ item = this._getItem(id, type);
+ if (filter(item)) {
+ ids.push(item[this._fieldId]);
+ }
+ }
+ }
+ }
+ }
+ else {
+ // get all items
+ if (order) {
+ // create an ordered list
+ items = [];
+ for (id in data) {
+ if (data.hasOwnProperty(id)) {
+ items.push(data[id]);
+ }
+ }
+
+ this._sort(items, order);
+
+ for (i = 0, len = items.length; i < len; i++) {
+ ids[i] = items[i][this._fieldId];
+ }
+ }
+ else {
+ // create unordered list
+ for (id in data) {
+ if (data.hasOwnProperty(id)) {
+ item = data[id];
+ ids.push(item[this._fieldId]);
+ }
+ }
+ }
+ }
+
+ return ids;
+};
+
+/**
+ * Returns the DataSet itself. Is overwritten for example by the DataView,
+ * which returns the DataSet it is connected to instead.
+ */
+DataSet.prototype.getDataSet = function () {
+ return this;
+};
+
+/**
+ * Execute a callback function for every item in the dataset.
+ * @param {function} callback
+ * @param {Object} [options] Available options:
+ * {Object.} [type]
+ * {String[]} [fields] filter fields
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ */
+DataSet.prototype.forEach = function (callback, options) {
+ var filter = options && options.filter,
+ type = options && options.type || this._options.type,
+ data = this._data,
+ item,
+ id;
+
+ if (options && options.order) {
+ // execute forEach on ordered list
+ var items = this.get(options);
+
+ for (var i = 0, len = items.length; i < len; i++) {
+ item = items[i];
+ id = item[this._fieldId];
+ callback(item, id);
+ }
+ }
+ else {
+ // unordered
+ for (id in data) {
+ if (data.hasOwnProperty(id)) {
+ item = this._getItem(id, type);
+ if (!filter || filter(item)) {
+ callback(item, id);
+ }
+ }
+ }
+ }
+};
+
+/**
+ * Map every item in the dataset.
+ * @param {function} callback
+ * @param {Object} [options] Available options:
+ * {Object.} [type]
+ * {String[]} [fields] filter fields
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ * @return {Object[]} mappedItems
+ */
+DataSet.prototype.map = function (callback, options) {
+ var filter = options && options.filter,
+ type = options && options.type || this._options.type,
+ mappedItems = [],
+ data = this._data,
+ item;
+
+ // convert and filter items
+ for (var id in data) {
+ if (data.hasOwnProperty(id)) {
+ item = this._getItem(id, type);
+ if (!filter || filter(item)) {
+ mappedItems.push(callback(item, id));
+ }
+ }
+ }
+
+ // order items
+ if (options && options.order) {
+ this._sort(mappedItems, options.order);
+ }
+
+ return mappedItems;
+};
+
+/**
+ * Filter the fields of an item
+ * @param {Object} item
+ * @param {String[]} fields Field names
+ * @return {Object} filteredItem
+ * @private
+ */
+DataSet.prototype._filterFields = function (item, fields) {
+ var filteredItem = {};
+
+ for (var field in item) {
+ if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
+ filteredItem[field] = item[field];
+ }
+ }
+
+ return filteredItem;
+};
+
+/**
+ * Sort the provided array with items
+ * @param {Object[]} items
+ * @param {String | function} order A field name or custom sort function.
+ * @private
+ */
+DataSet.prototype._sort = function (items, order) {
+ if (util.isString(order)) {
+ // order by provided field name
+ var name = order; // field name
+ items.sort(function (a, b) {
+ var av = a[name];
+ var bv = b[name];
+ return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
+ });
+ }
+ else if (typeof order === 'function') {
+ // order by sort function
+ items.sort(order);
+ }
+ // TODO: extend order by an Object {field:String, direction:String}
+ // where direction can be 'asc' or 'desc'
+ else {
+ throw new TypeError('Order must be a function or a string');
+ }
+};
+
+/**
+ * Remove an object by pointer or by id
+ * @param {String | Number | Object | Array} id Object or id, or an array with
+ * objects or ids to be removed
+ * @param {String} [senderId] Optional sender id
+ * @return {Array} removedIds
+ */
+DataSet.prototype.remove = function (id, senderId) {
+ var removedIds = [],
+ i, len, removedId;
+
+ if (Array.isArray(id)) {
+ for (i = 0, len = id.length; i < len; i++) {
+ removedId = this._remove(id[i]);
+ if (removedId != null) {
+ removedIds.push(removedId);
+ }
+ }
+ }
+ else {
+ removedId = this._remove(id);
+ if (removedId != null) {
+ removedIds.push(removedId);
+ }
+ }
+
+ if (removedIds.length) {
+ this._trigger('remove', {items: removedIds}, senderId);
+ }
+
+ return removedIds;
+};
+
+/**
+ * Remove an item by its id
+ * @param {Number | String | Object} id id or item
+ * @returns {Number | String | null} id
+ * @private
+ */
+DataSet.prototype._remove = function (id) {
+ if (util.isNumber(id) || util.isString(id)) {
+ if (this._data[id]) {
+ delete this._data[id];
+ return id;
+ }
+ }
+ else if (id instanceof Object) {
+ var itemId = id[this._fieldId];
+ if (itemId && this._data[itemId]) {
+ delete this._data[itemId];
+ return itemId;
+ }
+ }
+ return null;
+};
+
+/**
+ * Clear the data
+ * @param {String} [senderId] Optional sender id
+ * @return {Array} removedIds The ids of all removed items
+ */
+DataSet.prototype.clear = function (senderId) {
+ var ids = Object.keys(this._data);
+
+ this._data = {};
+
+ this._trigger('remove', {items: ids}, senderId);
+
+ return ids;
+};
+
+/**
+ * Find the item with maximum value of a specified field
+ * @param {String} field
+ * @return {Object | null} item Item containing max value, or null if no items
+ */
+DataSet.prototype.max = function (field) {
+ var data = this._data,
+ max = null,
+ maxField = null;
+
+ for (var id in data) {
+ if (data.hasOwnProperty(id)) {
+ var item = data[id];
+ var itemField = item[field];
+ if (itemField != null && (!max || itemField > maxField)) {
+ max = item;
+ maxField = itemField;
+ }
+ }
+ }
+
+ return max;
+};
+
+/**
+ * Find the item with minimum value of a specified field
+ * @param {String} field
+ * @return {Object | null} item Item containing max value, or null if no items
+ */
+DataSet.prototype.min = function (field) {
+ var data = this._data,
+ min = null,
+ minField = null;
+
+ for (var id in data) {
+ if (data.hasOwnProperty(id)) {
+ var item = data[id];
+ var itemField = item[field];
+ if (itemField != null && (!min || itemField < minField)) {
+ min = item;
+ minField = itemField;
+ }
+ }
+ }
+
+ return min;
+};
+
+/**
+ * Find all distinct values of a specified field
+ * @param {String} field
+ * @return {Array} values Array containing all distinct values. If data items
+ * do not contain the specified field are ignored.
+ * The returned array is unordered.
+ */
+DataSet.prototype.distinct = function (field) {
+ var data = this._data;
+ var values = [];
+ var fieldType = this._options.type && this._options.type[field] || null;
+ var count = 0;
+ var i;
+
+ for (var prop in data) {
+ if (data.hasOwnProperty(prop)) {
+ var item = data[prop];
+ var value = item[field];
+ var exists = false;
+ for (i = 0; i < count; i++) {
+ if (values[i] == value) {
+ exists = true;
+ break;
+ }
+ }
+ if (!exists && (value !== undefined)) {
+ values[count] = value;
+ count++;
+ }
+ }
+ }
+
+ if (fieldType) {
+ for (i = 0; i < values.length; i++) {
+ values[i] = util.convert(values[i], fieldType);
+ }
+ }
+
+ return values;
+};
+
+/**
+ * Add a single item. Will fail when an item with the same id already exists.
+ * @param {Object} item
+ * @return {String} id
+ * @private
+ */
+DataSet.prototype._addItem = function (item) {
+ var id = item[this._fieldId];
+
+ if (id != undefined) {
+ // check whether this id is already taken
+ if (this._data[id]) {
+ // item already exists
+ throw new Error('Cannot add item: item with id ' + id + ' already exists');
+ }
+ }
+ else {
+ // generate an id
+ id = util.randomUUID();
+ item[this._fieldId] = id;
+ }
+
+ var d = {};
+ for (var field in item) {
+ if (item.hasOwnProperty(field)) {
+ var fieldType = this._type[field]; // type may be undefined
+ d[field] = util.convert(item[field], fieldType);
+ }
+ }
+ this._data[id] = d;
+
+ return id;
+};
+
+/**
+ * Get an item. Fields can be converted to a specific type
+ * @param {String} id
+ * @param {Object.} [types] field types to convert
+ * @return {Object | null} item
+ * @private
+ */
+DataSet.prototype._getItem = function (id, types) {
+ var field, value;
+
+ // get the item from the dataset
+ var raw = this._data[id];
+ if (!raw) {
+ return null;
+ }
+
+ // convert the items field types
+ var converted = {};
+ if (types) {
+ for (field in raw) {
+ if (raw.hasOwnProperty(field)) {
+ value = raw[field];
+ converted[field] = util.convert(value, types[field]);
+ }
+ }
+ }
+ else {
+ // no field types specified, no converting needed
+ for (field in raw) {
+ if (raw.hasOwnProperty(field)) {
+ value = raw[field];
+ converted[field] = value;
+ }
+ }
+ }
+ return converted;
+};
+
+/**
+ * Update a single item: merge with existing item.
+ * Will fail when the item has no id, or when there does not exist an item
+ * with the same id.
+ * @param {Object} item
+ * @return {String} id
+ * @private
+ */
+DataSet.prototype._updateItem = function (item) {
+ var id = item[this._fieldId];
+ if (id == undefined) {
+ throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
+ }
+ var d = this._data[id];
+ if (!d) {
+ // item doesn't exist
+ throw new Error('Cannot update item: no item with id ' + id + ' found');
+ }
+
+ // merge with current item
+ for (var field in item) {
+ if (item.hasOwnProperty(field)) {
+ var fieldType = this._type[field]; // type may be undefined
+ d[field] = util.convert(item[field], fieldType);
+ }
+ }
+
+ return id;
+};
+
+/**
+ * Get an array with the column names of a Google DataTable
+ * @param {DataTable} dataTable
+ * @return {String[]} columnNames
+ * @private
+ */
+DataSet.prototype._getColumnNames = function (dataTable) {
+ var columns = [];
+ for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
+ columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
+ }
+ return columns;
+};
+
+/**
+ * Append an item as a row to the dataTable
+ * @param dataTable
+ * @param columns
+ * @param item
+ * @private
+ */
+DataSet.prototype._appendRow = function (dataTable, columns, item) {
+ var row = dataTable.addRow();
+
+ for (var col = 0, cols = columns.length; col < cols; col++) {
+ var field = columns[col];
+ dataTable.setValue(row, col, item[field]);
+ }
+};
+
+/**
+ * DataView
+ *
+ * a dataview offers a filtered view on a dataset or an other dataview.
+ *
+ * @param {DataSet | DataView} data
+ * @param {Object} [options] Available options: see method get
+ *
+ * @constructor DataView
+ */
+function DataView (data, options) {
+ this._data = null;
+ this._ids = {}; // ids of the items currently in memory (just contains a boolean true)
+ this._options = options || {};
+ this._fieldId = 'id'; // name of the field containing id
+ this._subscribers = {}; // event subscribers
+
+ var me = this;
+ this.listener = function () {
+ me._onEvent.apply(me, arguments);
+ };
+
+ this.setData(data);
+}
+
+// TODO: implement a function .config() to dynamically update things like configured filter
+// and trigger changes accordingly
+
+/**
+ * Set a data source for the view
+ * @param {DataSet | DataView} data
+ */
+DataView.prototype.setData = function (data) {
+ var ids, i, len;
+
+ if (this._data) {
+ // unsubscribe from current dataset
+ if (this._data.unsubscribe) {
+ this._data.unsubscribe('*', this.listener);
+ }
+
+ // trigger a remove of all items in memory
+ ids = [];
+ for (var id in this._ids) {
+ if (this._ids.hasOwnProperty(id)) {
+ ids.push(id);
+ }
+ }
+ this._ids = {};
+ this._trigger('remove', {items: ids});
+ }
+
+ this._data = data;
+
+ if (this._data) {
+ // update fieldId
+ this._fieldId = this._options.fieldId ||
+ (this._data && this._data.options && this._data.options.fieldId) ||
+ 'id';
+
+ // trigger an add of all added items
+ ids = this._data.getIds({filter: this._options && this._options.filter});
+ for (i = 0, len = ids.length; i < len; i++) {
+ id = ids[i];
+ this._ids[id] = true;
+ }
+ this._trigger('add', {items: ids});
+
+ // subscribe to new dataset
+ if (this._data.on) {
+ this._data.on('*', this.listener);
+ }
+ }
+};
+
+/**
+ * Get data from the data view
+ *
+ * Usage:
+ *
+ * get()
+ * get(options: Object)
+ * get(options: Object, data: Array | DataTable)
+ *
+ * get(id: Number)
+ * get(id: Number, options: Object)
+ * get(id: Number, options: Object, data: Array | DataTable)
+ *
+ * get(ids: Number[])
+ * get(ids: Number[], options: Object)
+ * get(ids: Number[], options: Object, data: Array | DataTable)
+ *
+ * Where:
+ *
+ * {Number | String} id The id of an item
+ * {Number[] | String{}} ids An array with ids of items
+ * {Object} options An Object with options. Available options:
+ * {String} [type] Type of data to be returned. Can
+ * be 'DataTable' or 'Array' (default)
+ * {Object.} [convert]
+ * {String[]} [fields] field names to be returned
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ * {Array | DataTable} [data] If provided, items will be appended to this
+ * array or table. Required in case of Google
+ * DataTable.
+ * @param args
+ */
+DataView.prototype.get = function (args) {
+ var me = this;
+
+ // parse the arguments
+ var ids, options, data;
+ var firstType = util.getType(arguments[0]);
+ if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
+ // get(id(s) [, options] [, data])
+ ids = arguments[0]; // can be a single id or an array with ids
+ options = arguments[1];
+ data = arguments[2];
+ }
+ else {
+ // get([, options] [, data])
+ options = arguments[0];
+ data = arguments[1];
+ }
+
+ // extend the options with the default options and provided options
+ var viewOptions = util.extend({}, this._options, options);
+
+ // create a combined filter method when needed
+ if (this._options.filter && options && options.filter) {
+ viewOptions.filter = function (item) {
+ return me._options.filter(item) && options.filter(item);
+ }
+ }
+
+ // build up the call to the linked data set
+ var getArguments = [];
+ if (ids != undefined) {
+ getArguments.push(ids);
+ }
+ getArguments.push(viewOptions);
+ getArguments.push(data);
+
+ return this._data && this._data.get.apply(this._data, getArguments);
+};
+
+/**
+ * Get ids of all items or from a filtered set of items.
+ * @param {Object} [options] An Object with options. Available options:
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ * @return {Array} ids
+ */
+DataView.prototype.getIds = function (options) {
+ var ids;
+
+ if (this._data) {
+ var defaultFilter = this._options.filter;
+ var filter;
+
+ if (options && options.filter) {
+ if (defaultFilter) {
+ filter = function (item) {
+ return defaultFilter(item) && options.filter(item);
+ }
+ }
+ else {
+ filter = options.filter;
+ }
+ }
+ else {
+ filter = defaultFilter;
+ }
+
+ ids = this._data.getIds({
+ filter: filter,
+ order: options && options.order
+ });
+ }
+ else {
+ ids = [];
+ }
+
+ return ids;
+};
+
+/**
+ * Get the DataSet to which this DataView is connected. In case there is a chain
+ * of multiple DataViews, the root DataSet of this chain is returned.
+ * @return {DataSet} dataSet
+ */
+DataView.prototype.getDataSet = function () {
+ var dataSet = this;
+ while (dataSet instanceof DataView) {
+ dataSet = dataSet._data;
+ }
+ return dataSet || null;
+};
+
+/**
+ * Event listener. Will propagate all events from the connected data set to
+ * the subscribers of the DataView, but will filter the items and only trigger
+ * when there are changes in the filtered data set.
+ * @param {String} event
+ * @param {Object | null} params
+ * @param {String} senderId
+ * @private
+ */
+DataView.prototype._onEvent = function (event, params, senderId) {
+ var i, len, id, item,
+ ids = params && params.items,
+ data = this._data,
+ added = [],
+ updated = [],
+ removed = [];
+
+ if (ids && data) {
+ switch (event) {
+ case 'add':
+ // filter the ids of the added items
+ for (i = 0, len = ids.length; i < len; i++) {
+ id = ids[i];
+ item = this.get(id);
+ if (item) {
+ this._ids[id] = true;
+ added.push(id);
+ }
+ }
+
+ break;
+
+ case 'update':
+ // determine the event from the views viewpoint: an updated
+ // item can be added, updated, or removed from this view.
+ for (i = 0, len = ids.length; i < len; i++) {
+ id = ids[i];
+ item = this.get(id);
+
+ if (item) {
+ if (this._ids[id]) {
+ updated.push(id);
+ }
+ else {
+ this._ids[id] = true;
+ added.push(id);
+ }
+ }
+ else {
+ if (this._ids[id]) {
+ delete this._ids[id];
+ removed.push(id);
+ }
+ else {
+ // nothing interesting for me :-(
+ }
+ }
+ }
+
+ break;
+
+ case 'remove':
+ // filter the ids of the removed items
+ for (i = 0, len = ids.length; i < len; i++) {
+ id = ids[i];
+ if (this._ids[id]) {
+ delete this._ids[id];
+ removed.push(id);
+ }
+ }
+
+ break;
+ }
+
+ if (added.length) {
+ this._trigger('add', {items: added}, senderId);
+ }
+ if (updated.length) {
+ this._trigger('update', {items: updated}, senderId);
+ }
+ if (removed.length) {
+ this._trigger('remove', {items: removed}, senderId);
+ }
+ }
+};
+
+// copy subscription functionality from DataSet
+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 Group
+ * @param {Number | String} groupId
+ * @param {Object} data
+ * @param {ItemSet} itemSet
+ */
+function GraphGroup (group, groupId, options, groupsUsingDefaultStyles) {
+ this.id = groupId;
+ var fields = ['sampling','style','sort','yAxisOrientation','barChart','drawPoints','shaded','catmullRom']
+ this.options = util.selectiveBridgeObject(fields,options);
+ this.usingDefaultStyle = group.className === undefined;
+ this.groupsUsingDefaultStyles = groupsUsingDefaultStyles;
+ this.zeroPosition = 0;
+ this.update(group);
+ if (this.usingDefaultStyle == true) {
+ this.groupsUsingDefaultStyles[0] += 1;
+ }
+ this.itemsData = [];
+}
+
+GraphGroup.prototype.setItems = function(items) {
+ if (items != null) {
+ this.itemsData = items;
+ if (this.options.sort == true) {
+ this.itemsData.sort(function (a,b) {return a.x - b.x;})
+ }
+ }
+ else {
+ this.itemsData = [];
+ }
+}
+
+GraphGroup.prototype.setZeroPosition = function(pos) {
+ this.zeroPosition = pos;
+}
+
+GraphGroup.prototype.setOptions = function(options) {
+ if (options !== undefined) {
+ var fields = ['sampling','style','sort','yAxisOrientation','barChart'];
+ util.selectiveDeepExtend(fields, this.options, options);
+
+ util.mergeOptions(this.options, options,'catmullRom');
+ util.mergeOptions(this.options, options,'drawPoints');
+ util.mergeOptions(this.options, options,'shaded');
+
+ if (options.catmullRom) {
+ if (typeof options.catmullRom == 'object') {
+ if (options.catmullRom.parametrization) {
+ if (options.catmullRom.parametrization == 'uniform') {
+ this.options.catmullRom.alpha = 0;
+ }
+ else if (options.catmullRom.parametrization == 'chordal') {
+ this.options.catmullRom.alpha = 1.0;
+ }
+ else {
+ this.options.catmullRom.parametrization = 'centripetal';
+ this.options.catmullRom.alpha = 0.5;
+ }
+ }
+ }
+ }
+ }
+};
+
+GraphGroup.prototype.update = function(group) {
+ this.group = group;
+ this.content = group.content || 'graph';
+ this.className = group.className || this.className || "graphGroup" + this.groupsUsingDefaultStyles[0] % 10;
+ this.setOptions(group.options);
+};
+
+GraphGroup.prototype.drawIcon = function(x, y, JSONcontainer, SVGcontainer, iconWidth, iconHeight) {
+ var fillHeight = iconHeight * 0.5;
+ var path, fillPath;
+
+ var outline = DOMutil.getSVGElement("rect", JSONcontainer, SVGcontainer);
+ outline.setAttributeNS(null, "x", x);
+ outline.setAttributeNS(null, "y", y - fillHeight);
+ outline.setAttributeNS(null, "width", iconWidth);
+ outline.setAttributeNS(null, "height", 2*fillHeight);
+ outline.setAttributeNS(null, "class", "outline");
+
+ if (this.options.style == 'line') {
+ path = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
+ path.setAttributeNS(null, "class", this.className);
+ path.setAttributeNS(null, "d", "M" + x + ","+y+" L" + (x + iconWidth) + ","+y+"");
+ if (this.options.shaded.enabled == true) {
+ fillPath = DOMutil.getSVGElement("path", JSONcontainer, SVGcontainer);
+ if (this.options.shaded.orientation == 'top') {
+ fillPath.setAttributeNS(null, "d", "M"+x+", " + (y - fillHeight) +
+ "L"+x+","+y+" L"+ (x + iconWidth) + ","+y+" L"+ (x + iconWidth) + "," + (y - fillHeight));
+ }
+ else {
+ fillPath.setAttributeNS(null, "d", "M"+x+","+y+" " +
+ "L"+x+"," + (y + fillHeight) + " " +
+ "L"+ (x + iconWidth) + "," + (y + fillHeight) +
+ "L"+ (x + iconWidth) + ","+y);
+ }
+ fillPath.setAttributeNS(null, "class", this.className + " iconFill");
+ }
+
+ if (this.options.drawPoints.enabled == true) {
+ DOMutil.drawPoint(x + 0.5 * iconWidth,y, this, JSONcontainer, SVGcontainer);
+ }
+ }
+ else {
+ var barWidth = Math.round(0.3 * iconWidth);
+ var bar1Height = Math.round(0.4 * iconHeight);
+ var bar2Height = Math.round(0.75 * iconHeight);
+
+ var offset = Math.round((iconWidth - (2 * barWidth))/3);
+
+ DOMutil.drawBar(x + 0.5*barWidth + offset , y + fillHeight - bar1Height - 1, barWidth, bar1Height, this.className + ' bar', JSONcontainer, SVGcontainer);
+ DOMutil.drawBar(x + 1.5*barWidth + offset + 2, y + fillHeight - bar2Height - 1, barWidth, bar2Height, this.className + ' bar', JSONcontainer, SVGcontainer);
+ }
+}
+
+/**
+ * Created by Alex on 6/17/14.
+ */
+function Legend(body, options, side) {
+ this.body = body;
+ this.defaultOptions = {
+ enabled: true,
+ icons: true,
+ iconSize: 20,
+ iconSpacing: 6,
+ left: {
+ visible: true,
+ position: 'top-left' // top/bottom - left,center,right
+ },
+ right: {
+ visible: true,
+ position: 'top-left' // top/bottom - left,center,right
+ }
+ }
+ this.side = side;
+ this.options = util.extend({},this.defaultOptions);
+
+ this.svgElements = {};
+ this.dom = {};
+ this.groups = {};
+ this.amountOfGroups = 0;
+ this._create();
+
+ this.setOptions(options);
+};
+
+Legend.prototype = new Component();
+
+
+Legend.prototype.addGroup = function(label, graphOptions) {
+ if (!this.groups.hasOwnProperty(label)) {
+ this.groups[label] = graphOptions;
+ }
+ this.amountOfGroups += 1;
+};
+
+Legend.prototype.updateGroup = function(label, graphOptions) {
+ this.groups[label] = graphOptions;
+};
+
+Legend.prototype.removeGroup = function(label) {
+ if (this.groups.hasOwnProperty(label)) {
+ delete this.groups[label];
+ this.amountOfGroups -= 1;
+ }
+};
+
+Legend.prototype._create = function() {
+ this.dom.frame = document.createElement('div');
+ this.dom.frame.className = 'legend';
+ this.dom.frame.style.position = "absolute";
+ this.dom.frame.style.top = "10px";
+ this.dom.frame.style.display = "block";
+
+ this.dom.textArea = document.createElement('div');
+ this.dom.textArea.className = 'legendText';
+ this.dom.textArea.style.position = "relative";
+ this.dom.textArea.style.top = "0px";
+
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
+ this.svg.style.position = 'absolute';
+ this.svg.style.top = 0 +'px';
+ this.svg.style.width = this.options.iconSize + 5 + 'px';
+
+ this.dom.frame.appendChild(this.svg);
+ this.dom.frame.appendChild(this.dom.textArea);
+}
+
+/**
+ * Hide the component from the DOM
+ */
+Legend.prototype.hide = function() {
+ // remove the frame containing the items
+ if (this.dom.frame.parentNode) {
+ this.dom.frame.parentNode.removeChild(this.dom.frame);
+ }
+};
+
+/**
+ * Show the component in the DOM (when not already visible).
+ * @return {Boolean} changed
+ */
+Legend.prototype.show = function() {
+ // show frame containing the items
+ if (!this.dom.frame.parentNode) {
+ this.body.dom.center.appendChild(this.dom.frame);
+ }
+};
+
+Legend.prototype.setOptions = function(options) {
+ var fields = ['enabled','orientation','icons','left','right'];
+ util.selectiveDeepExtend(fields, this.options, options);
+}
+
+Legend.prototype.redraw = function() {
+ if (this.options[this.side].visible == false || this.amountOfGroups == 0 || this.options.enabled == false) {
+ this.hide();
+ }
+ else {
+ this.show();
+ if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'bottom-left') {
+ this.dom.frame.style.left = '4px';
+ this.dom.frame.style.textAlign = "left";
+ this.dom.textArea.style.textAlign = "left";
+ this.dom.textArea.style.left = (this.options.iconSize + 15) + 'px';
+ this.dom.textArea.style.right = '';
+ this.svg.style.left = 0 +'px';
+ this.svg.style.right = '';
+ }
+ else {
+ this.dom.frame.style.right = '4px';
+ this.dom.frame.style.textAlign = "right";
+ this.dom.textArea.style.textAlign = "right";
+ this.dom.textArea.style.right = (this.options.iconSize + 15) + 'px';
+ this.dom.textArea.style.left = '';
+ this.svg.style.right = 0 +'px';
+ this.svg.style.left = '';
+ }
+
+ if (this.options[this.side].position == 'top-left' || this.options[this.side].position == 'top-right') {
+ this.dom.frame.style.top = 4 - Number(this.body.dom.center.style.top.replace("px","")) + 'px';
+ this.dom.frame.style.bottom = '';
+ }
+ else {
+ this.dom.frame.style.bottom = 4 - Number(this.body.dom.center.style.top.replace("px","")) + 'px';
+ this.dom.frame.style.top = '';
+ }
+
+ if (this.options.icons == false) {
+ this.dom.frame.style.width = this.dom.textArea.offsetWidth + 10 + 'px';
+ this.dom.textArea.style.right = '';
+ this.dom.textArea.style.left = '';
+ this.svg.style.width = '0px';
+ }
+ else {
+ this.dom.frame.style.width = this.options.iconSize + 15 + this.dom.textArea.offsetWidth + 10 + 'px'
+ this.drawLegendIcons();
+ }
+
+ var content = "";
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ content += this.groups[groupId].content + ' ';
+ }
+ }
+ this.dom.textArea.innerHTML = content;
+ this.dom.textArea.style.lineHeight = ((0.75 * this.options.iconSize) + this.options.iconSpacing) + 'px';
+ }
+}
+
+Legend.prototype.drawLegendIcons = function() {
+ if (this.dom.frame.parentNode) {
+ DOMutil.prepareElements(this.svgElements);
+ var padding = window.getComputedStyle(this.dom.frame).paddingTop;
+ var iconOffset = Number(padding.replace("px",''));
+ var x = iconOffset;
+ var iconWidth = this.options.iconSize;
+ var iconHeight = 0.75 * this.options.iconSize;
+ var y = iconOffset + 0.5 * iconHeight + 3;
+
+ this.svg.style.width = iconWidth + 5 + iconOffset + 'px';
+
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
+ y += iconHeight + this.options.iconSpacing;
+ }
+ }
+
+ DOMutil.cleanupElements(this.svgElements);
+ }
+}
+/**
+ * A horizontal time axis
+ * @param {Object} [options] See DataAxis.setOptions for the available
+ * options.
+ * @constructor DataAxis
+ * @extends Component
+ * @param body
+ */
+function DataAxis (body, options, svg) {
+ this.id = util.randomUUID();
+ this.body = body;
+
+ this.defaultOptions = {
+ orientation: 'left', // supported: 'left', 'right'
+ showMinorLabels: true,
+ showMajorLabels: true,
+ icons: true,
+ majorLinesOffset: 7,
+ minorLinesOffset: 4,
+ labelOffsetX: 10,
+ labelOffsetY: 2,
+ iconWidth: 20,
+ width: '40px',
+ visible: true
+ };
+
+ this.linegraphSVG = svg;
+ this.props = {};
+ this.DOMelements = { // dynamic elements
+ lines: {},
+ labels: {}
+ };
+
+ this.dom = {};
+
+ this.range = {start:0, end:0};
+
+ this.options = util.extend({}, this.defaultOptions);
+ this.conversionFactor = 1;
+
+ this.setOptions(options);
+ this.width = Number(('' + this.options.width).replace("px",""));
+ this.minWidth = this.width;
+ this.height = this.linegraphSVG.offsetHeight;
+
+ this.stepPixels = 25;
+ this.stepPixelsForced = 25;
+ this.lineOffset = 0;
+ this.master = true;
+ this.svgElements = {};
+
+
+ this.groups = {};
+ this.amountOfGroups = 0;
+
+ // create the HTML DOM
+ this._create();
+}
+
+DataAxis.prototype = new Component();
+
+
+
+DataAxis.prototype.addGroup = function(label, graphOptions) {
+ if (!this.groups.hasOwnProperty(label)) {
+ this.groups[label] = graphOptions;
+ }
+ this.amountOfGroups += 1;
+};
+
+DataAxis.prototype.updateGroup = function(label, graphOptions) {
+ this.groups[label] = graphOptions;
+};
+
+DataAxis.prototype.removeGroup = function(label) {
+ if (this.groups.hasOwnProperty(label)) {
+ delete this.groups[label];
+ this.amountOfGroups -= 1;
+ }
+};
+
+
+DataAxis.prototype.setOptions = function (options) {
+ if (options) {
+ var redraw = false;
+ if (this.options.orientation != options.orientation && options.orientation !== undefined) {
+ redraw = true;
+ }
+ var fields = [
+ 'orientation',
+ 'showMinorLabels',
+ 'showMajorLabels',
+ 'icons',
+ 'majorLinesOffset',
+ 'minorLinesOffset',
+ 'labelOffsetX',
+ 'labelOffsetY',
+ 'iconWidth',
+ 'width',
+ 'visible'];
+ util.selectiveExtend(fields, this.options, options);
+
+ this.minWidth = Number(('' + this.options.width).replace("px",""));
+
+ if (redraw == true && this.dom.frame) {
+ this.hide();
+ this.show();
+ }
+ }
+};
+
+
+/**
+ * Create the HTML DOM for the DataAxis
+ */
+DataAxis.prototype._create = function() {
+ this.dom.frame = document.createElement('div');
+ this.dom.frame.style.width = this.options.width;
+ this.dom.frame.style.height = this.height;
+
+ this.dom.lineContainer = document.createElement('div');
+ this.dom.lineContainer.style.width = '100%';
+ this.dom.lineContainer.style.height = this.height;
+
+ // create svg element for graph drawing.
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
+ this.svg.style.position = "absolute";
+ this.svg.style.top = '0px';
+ this.svg.style.height = '100%';
+ this.svg.style.width = '100%';
+ this.svg.style.display = "block";
+ this.dom.frame.appendChild(this.svg);
+};
+
+DataAxis.prototype._redrawGroupIcons = function () {
+ DOMutil.prepareElements(this.svgElements);
+
+ var x;
+ var iconWidth = this.options.iconWidth;
+ var iconHeight = 15;
+ var iconOffset = 4;
+ var y = iconOffset + 0.5 * iconHeight;
+
+ if (this.options.orientation == 'left') {
+ x = iconOffset;
+ }
+ else {
+ x = this.width - iconWidth - iconOffset;
+ }
+
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ this.groups[groupId].drawIcon(x, y, this.svgElements, this.svg, iconWidth, iconHeight);
+ y += iconHeight + iconOffset;
+ }
+ }
+
+ DOMutil.cleanupElements(this.svgElements);
+};
+
+/**
+ * Create the HTML DOM for the DataAxis
+ */
+DataAxis.prototype.show = function() {
+ if (!this.dom.frame.parentNode) {
+ if (this.options.orientation == 'left') {
+ this.body.dom.left.appendChild(this.dom.frame);
+ }
+ else {
+ this.body.dom.right.appendChild(this.dom.frame);
+ }
+ }
+
+ if (!this.dom.lineContainer.parentNode) {
+ this.body.dom.backgroundHorizontal.appendChild(this.dom.lineContainer);
+ }
+};
+
+/**
+ * Create the HTML DOM for the DataAxis
+ */
+DataAxis.prototype.hide = function() {
+ if (this.dom.frame.parentNode) {
+ this.dom.frame.parentNode.removeChild(this.dom.frame);
+ }
+
+ if (this.dom.lineContainer.parentNode) {
+ this.dom.lineContainer.parentNode.removeChild(this.dom.lineContainer);
+ }
+};
+
+/**
+ * Set a range (start and end)
+ * @param end
+ * @param start
+ * @param end
+ */
+DataAxis.prototype.setRange = function (start, end) {
+ this.range.start = start;
+ this.range.end = end;
+};
+
+/**
+ * Repaint the component
+ * @return {boolean} Returns true if the component is resized
+ */
+DataAxis.prototype.redraw = function () {
+ var changeCalled = false;
+ if (this.amountOfGroups == 0) {
+ this.hide();
+ }
+ else {
+ this.show();
+ this.height = Number(this.linegraphSVG.style.height.replace("px",""));
+ // svg offsetheight did not work in firefox and explorer...
+
+ this.dom.lineContainer.style.height = this.height + 'px';
+ this.width = this.options.visible == true ? Number(('' + this.options.width).replace("px","")) : 0;
+
+ var props = this.props;
+ var frame = this.dom.frame;
+
+ // update classname
+ frame.className = 'dataaxis';
+
+ // calculate character width and height
+ this._calculateCharSize();
+
+ var orientation = this.options.orientation;
+ var showMinorLabels = this.options.showMinorLabels;
+ var showMajorLabels = this.options.showMajorLabels;
+
+ // determine the width and height of the elemens for the axis
+ props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
+ props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
+
+ props.minorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.minorLinesOffset;
+ props.minorLineHeight = 1;
+ props.majorLineWidth = this.body.dom.backgroundHorizontal.offsetWidth - this.lineOffset - this.width + 2 * this.options.majorLinesOffset;
+ props.majorLineHeight = 1;
+
+ // take frame offline while updating (is almost twice as fast)
+ if (orientation == 'left') {
+ frame.style.top = '0';
+ frame.style.left = '0';
+ frame.style.bottom = '';
+ frame.style.width = this.width + 'px';
+ frame.style.height = this.height + "px";
+ }
+ else { // right
+ frame.style.top = '';
+ frame.style.bottom = '0';
+ frame.style.left = '0';
+ frame.style.width = this.width + 'px';
+ frame.style.height = this.height + "px";
+ }
+ changeCalled = this._redrawLabels();
+ if (this.options.icons == true) {
+ this._redrawGroupIcons();
+ }
+ }
+ return changeCalled;
+};
+
+/**
+ * Repaint major and minor text labels and vertical grid lines
+ * @private
+ */
+DataAxis.prototype._redrawLabels = function () {
+ DOMutil.prepareElements(this.DOMelements);
+
+ var orientation = this.options['orientation'];
+
+ // calculate range and step (step such that we have space for 7 characters per label)
+ var minimumStep = this.master ? this.props.majorCharHeight || 10 : this.stepPixelsForced;
+ var step = new DataStep(this.range.start, this.range.end, minimumStep, this.dom.frame.offsetHeight);
+ this.step = step;
+ step.first();
+
+ // get the distance in pixels for a step
+ var stepPixels = this.dom.frame.offsetHeight / ((step.marginRange / step.step) + 1);
+ this.stepPixels = stepPixels;
+
+ var amountOfSteps = this.height / stepPixels;
+ var stepDifference = 0;
+
+ if (this.master == false) {
+ stepPixels = this.stepPixelsForced;
+ stepDifference = Math.round((this.height / stepPixels) - amountOfSteps);
+ for (var i = 0; i < 0.5 * stepDifference; i++) {
+ step.previous();
+ }
+ amountOfSteps = this.height / stepPixels;
+ }
+
+
+ this.valueAtZero = step.marginEnd;
+ var marginStartPos = 0;
+
+ // do not draw the first label
+ var max = 1;
+ step.next();
+
+ this.maxLabelSize = 0;
+ var y = 0;
+ while (max < Math.round(amountOfSteps)) {
+
+ y = Math.round(max * stepPixels);
+ marginStartPos = max * stepPixels;
+ var isMajor = step.isMajor();
+
+ if (this.options['showMinorLabels'] && isMajor == false || this.master == false && this.options['showMinorLabels'] == true) {
+ this._redrawLabel(y - 2, step.getCurrent(), orientation, 'yAxis minor', this.props.minorCharHeight);
+ }
+
+ if (isMajor && this.options['showMajorLabels'] && this.master == true ||
+ this.options['showMinorLabels'] == false && this.master == false && isMajor == true) {
+
+ if (y >= 0) {
+ this._redrawLabel(y - 2, step.getCurrent(), orientation, 'yAxis major', this.props.majorCharHeight);
+ }
+ this._redrawLine(y, orientation, 'grid horizontal major', this.options.majorLinesOffset, this.props.majorLineWidth);
+ }
+ else {
+ this._redrawLine(y, orientation, 'grid horizontal minor', this.options.minorLinesOffset, this.props.minorLineWidth);
+ }
+
+ step.next();
+ max++;
+ }
+
+ this.conversionFactor = marginStartPos/((amountOfSteps-1) * step.step);
+
+ var offset = this.options.icons == true ? this.options.iconWidth + this.options.labelOffsetX + 15 : this.options.labelOffsetX + 15;
+ // this will resize the yAxis to accomodate the labels.
+ if (this.maxLabelSize > (this.width - offset) && this.options.visible == true) {
+ this.width = this.maxLabelSize + offset;
+ this.options.width = this.width + "px";
+ DOMutil.cleanupElements(this.DOMelements);
+ this.redraw();
+ return true;
}
- else if (firstType == 'Array') {
- // get(ids [, options] [, data])
- ids = arguments[0];
- options = arguments[1];
- data = arguments[2];
+ // this will resize the yAxis if it is too big for the labels.
+ else if (this.maxLabelSize < (this.width - offset) && this.options.visible == true && this.width > this.minWidth) {
+ this.width = Math.max(this.minWidth,this.maxLabelSize + offset);
+ this.options.width = this.width + "px";
+ DOMutil.cleanupElements(this.DOMelements);
+ this.redraw();
+ return true;
}
else {
- // get([, options] [, data])
- options = arguments[0];
- data = arguments[1];
+ DOMutil.cleanupElements(this.DOMelements);
+ return false;
}
+};
- // determine the return type
- var type;
- if (options && options.type) {
- type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
+/**
+ * Create a label for the axis at position x
+ * @private
+ * @param y
+ * @param text
+ * @param orientation
+ * @param className
+ * @param characterHeight
+ */
+DataAxis.prototype._redrawLabel = function (y, text, orientation, className, characterHeight) {
+ // reuse redundant label
+ var label = DOMutil.getDOMElement('div',this.DOMelements, this.dom.frame); //this.dom.redundant.labels.shift();
+ label.className = className;
+ label.innerHTML = text;
- if (data && (type != util.getType(data))) {
- throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
- 'does not correspond with specified options.type (' + options.type + ')');
- }
- if (type == 'DataTable' && !util.isDataTable(data)) {
- throw new Error('Parameter "data" must be a DataTable ' +
- 'when options.type is "DataTable"');
- }
- }
- else if (data) {
- type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
+ if (orientation == 'left') {
+ label.style.left = '-' + this.options.labelOffsetX + 'px';
+ label.style.textAlign = "right";
}
else {
- type = 'Array';
+ label.style.right = '-' + this.options.labelOffsetX + 'px';
+ label.style.textAlign = "left";
}
- // we allow the setting of this value for a single get request.
- if (options != undefined) {
- if (options.showInternalIds != undefined) {
- this.showInternalIds = options.showInternalIds;
- }
+ label.style.top = y - 0.5 * characterHeight + this.options.labelOffsetY + 'px';
+
+ text += '';
+
+ var largestWidth = Math.max(this.props.majorCharWidth,this.props.minorCharWidth);
+ if (this.maxLabelSize < text.length * largestWidth) {
+ this.maxLabelSize = text.length * largestWidth;
}
+};
- // build options
- var convert = options && options.convert || this.options.convert;
- var filter = options && options.filter;
- var items = [], item, itemId, i, len;
+/**
+ * Create a minor line for the axis at position y
+ * @param y
+ * @param orientation
+ * @param className
+ * @param offset
+ * @param width
+ */
+DataAxis.prototype._redrawLine = function (y, orientation, className, offset, width) {
+ if (this.master == true) {
+ var line = DOMutil.getDOMElement('div',this.DOMelements, this.dom.lineContainer);//this.dom.redundant.lines.shift();
+ line.className = className;
+ line.innerHTML = '';
- // convert items
- if (id != undefined) {
- // return a single item
- item = me._getItem(id, convert);
- if (filter && !filter(item)) {
- item = null;
+ if (orientation == 'left') {
+ line.style.left = (this.width - offset) + 'px';
}
- }
- else if (ids != undefined) {
- // return a subset of items
- for (i = 0, len = ids.length; i < len; i++) {
- item = me._getItem(ids[i], convert);
- if (!filter || filter(item)) {
- items.push(item);
- }
+ else {
+ line.style.right = (this.width - offset) + 'px';
}
+
+ line.style.width = width + 'px';
+ line.style.top = y + 'px';
}
- else {
- // return all items
- for (itemId in this.data) {
- if (this.data.hasOwnProperty(itemId)) {
- item = me._getItem(itemId, convert);
- if (!filter || filter(item)) {
- items.push(item);
- }
- }
- }
+};
+
+
+DataAxis.prototype.convertValue = function (value) {
+ var invertedValue = this.valueAtZero - value;
+ var convertedValue = invertedValue * this.conversionFactor;
+ return convertedValue; // the -2 is to compensate for the borders
+};
+
+
+/**
+ * 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
+ */
+DataAxis.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 = 'yAxis minor measure';
+ measureCharMinor.appendChild(textMinor);
+ this.dom.frame.appendChild(measureCharMinor);
+
+ this.props.minorCharHeight = measureCharMinor.clientHeight;
+ this.props.minorCharWidth = measureCharMinor.clientWidth;
+
+ this.dom.frame.removeChild(measureCharMinor);
}
- // restore the global value of showInternalIds
- this.showInternalIds = globalShowInternalIds;
+ if (!('majorCharHeight' in this.props)) {
+ var textMajor = document.createTextNode('0');
+ var measureCharMajor = document.createElement('DIV');
+ measureCharMajor.className = 'yAxis major measure';
+ measureCharMajor.appendChild(textMajor);
+ this.dom.frame.appendChild(measureCharMajor);
- // order the results
- if (options && options.order && id == undefined) {
- this._sort(items, options.order);
+ this.props.majorCharHeight = measureCharMajor.clientHeight;
+ this.props.majorCharWidth = measureCharMajor.clientWidth;
+
+ this.dom.frame.removeChild(measureCharMajor);
}
+};
- // filter fields of the items
- if (options && options.fields) {
- var fields = options.fields;
- if (id != undefined) {
- item = this._filterFields(item, fields);
- }
- else {
- for (i = 0, len = items.length; i < len; i++) {
- items[i] = this._filterFields(items[i], fields);
+/**
+ * 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
+ */
+DataAxis.prototype.snap = function(date) {
+ return this.step.snap(date);
+};
+
+var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
+
+/**
+ * This is the constructor of the LineGraph. It requires a Timeline body and options.
+ *
+ * @param body
+ * @param options
+ * @constructor
+ */
+function LineGraph(body, options) {
+ this.id = util.randomUUID();
+ this.body = body;
+
+ this.defaultOptions = {
+ yAxisOrientation: 'left',
+ defaultGroup: 'default',
+ sort: true,
+ sampling: true,
+ graphHeight: '400px',
+ shaded: {
+ enabled: false,
+ orientation: 'bottom' // top, bottom
+ },
+ style: 'line', // line, bar
+ barChart: {
+ width: 50,
+ align: 'center' // left, center, right
+ },
+ catmullRom: {
+ enabled: true,
+ parametrization: 'centripetal', // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5)
+ alpha: 0.5
+ },
+ drawPoints: {
+ enabled: true,
+ size: 6,
+ style: 'square' // square, circle
+ },
+ dataAxis: {
+ showMinorLabels: true,
+ showMajorLabels: true,
+ icons: false,
+ width: '40px',
+ visible: true
+ },
+ legend: {
+ enabled: false,
+ icons: true,
+ left: {
+ visible: true,
+ position: 'top-left' // top/bottom - left,right
+ },
+ right: {
+ visible: true,
+ position: 'top-right' // top/bottom - left,right
}
}
- }
+ };
- // return the results
- if (type == 'DataTable') {
- var columns = this._getColumnNames(data);
- if (id != undefined) {
- // append a single item to the data table
- me._appendRow(data, columns, item);
- }
- else {
- // copy the items to the provided data table
- for (i = 0, len = items.length; i < len; i++) {
- me._appendRow(data, columns, items[i]);
- }
+ // options is shared by this ItemSet and all its items
+ this.options = util.extend({}, this.defaultOptions);
+ this.dom = {};
+ this.props = {};
+ this.hammer = null;
+ this.groups = {};
+
+ var me = this;
+ this.itemsData = null; // DataSet
+ this.groupsData = null; // DataSet
+
+ // listeners for the DataSet of the items
+ this.itemListeners = {
+ 'add': function (event, params, senderId) {
+ me._onAdd(params.items);
+ },
+ 'update': function (event, params, senderId) {
+ me._onUpdate(params.items);
+ },
+ 'remove': function (event, params, senderId) {
+ me._onRemove(params.items);
}
- return data;
- }
- else {
- // return an array
- if (id != undefined) {
- // a single item
- return item;
+ };
+
+ // listeners for the DataSet of the groups
+ this.groupListeners = {
+ 'add': function (event, params, senderId) {
+ me._onAddGroups(params.items);
+ },
+ 'update': function (event, params, senderId) {
+ me._onUpdateGroups(params.items);
+ },
+ 'remove': function (event, params, senderId) {
+ me._onRemoveGroups(params.items);
}
- else {
- // multiple items
- if (data) {
- // copy the items to the provided array
- for (i = 0, len = items.length; i < len; i++) {
- data.push(items[i]);
+ };
+
+ this.items = {}; // object with an Item for every data item
+ this.selection = []; // list with the ids of all selected nodes
+ this.lastStart = this.body.range.start;
+ this.touchParams = {}; // stores properties while dragging
+
+ this.svgElements = {};
+ this.setOptions(options);
+ this.groupsUsingDefaultStyles = [0];
+
+ this.body.emitter.on("rangechange",function() {
+ if (me.lastStart != 0) {
+ var offset = me.body.range.start - me.lastStart;
+ var range = me.body.range.end - me.body.range.start;
+ if (me.width != 0) {
+ var rangePerPixelInv = me.width/range;
+ var xOffset = offset * rangePerPixelInv;
+ me.svg.style.left = (-me.width - xOffset) + "px";
}
- return data;
- }
- else {
- // just return our array
- return items;
}
- }
- }
-};
+ });
+ this.body.emitter.on("rangechanged", function() {
+ me.lastStart = me.body.range.start;
+ me.svg.style.left = util.option.asSize(-me.width);
+ me._updateGraph.apply(me);
+ });
+
+ // create the HTML DOM
+ this._create();
+ this.body.emitter.emit("change");
+}
+
+LineGraph.prototype = new Component();
/**
- * Get ids of all items or from a filtered set of items.
- * @param {Object} [options] An Object with options. Available options:
- * {function} [filter] filter items
- * {String | function} [order] Order the items by
- * a field name or custom sort function.
- * @return {Array} ids
+ * Create the HTML DOM for the ItemSet
*/
-DataSet.prototype.getIds = function (options) {
- var data = this.data,
- filter = options && options.filter,
- order = options && options.order,
- convert = options && options.convert || this.options.convert,
- i,
- len,
- id,
- item,
- items,
- ids = [];
+LineGraph.prototype._create = function(){
+ var frame = document.createElement('div');
+ frame.className = 'LineGraph';
+ this.dom.frame = frame;
- if (filter) {
- // get filtered items
- if (order) {
- // create ordered list
- items = [];
- for (id in data) {
- if (data.hasOwnProperty(id)) {
- item = this._getItem(id, convert);
- if (filter(item)) {
- items.push(item);
- }
- }
- }
+ // create svg element for graph drawing.
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
+ this.svg.style.position = "relative";
+ this.svg.style.height = ('' + this.options.graphHeight).replace("px",'') + 'px';
+ this.svg.style.display = "block";
+ frame.appendChild(this.svg);
- this._sort(items, order);
+ // data axis
+ this.options.dataAxis.orientation = 'left';
+ this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg);
- for (i = 0, len = items.length; i < len; i++) {
- ids[i] = items[i][this.fieldId];
- }
- }
- else {
- // create unordered list
- for (id in data) {
- if (data.hasOwnProperty(id)) {
- item = this._getItem(id, convert);
- if (filter(item)) {
- ids.push(item[this.fieldId]);
+ this.options.dataAxis.orientation = 'right';
+ this.yAxisRight = new DataAxis(this.body, this.options.dataAxis, this.svg);
+ delete this.options.dataAxis.orientation;
+
+ // legends
+ this.legendLeft = new Legend(this.body, this.options.legend, 'left');
+ this.legendRight = new Legend(this.body, this.options.legend, 'right');
+
+ this.show();
+};
+
+/**
+ * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element.
+ * @param options
+ */
+LineGraph.prototype.setOptions = function(options) {
+ if (options) {
+ var fields = ['sampling','defaultGroup','graphHeight','yAxisOrientation','style','barChart','dataAxis','sort'];
+ util.selectiveDeepExtend(fields, this.options, options);
+ util.mergeOptions(this.options, options,'catmullRom');
+ util.mergeOptions(this.options, options,'drawPoints');
+ util.mergeOptions(this.options, options,'shaded');
+ util.mergeOptions(this.options, options,'legend');
+
+ if (options.catmullRom) {
+ if (typeof options.catmullRom == 'object') {
+ if (options.catmullRom.parametrization) {
+ if (options.catmullRom.parametrization == 'uniform') {
+ this.options.catmullRom.alpha = 0;
+ }
+ else if (options.catmullRom.parametrization == 'chordal') {
+ this.options.catmullRom.alpha = 1.0;
+ }
+ else {
+ this.options.catmullRom.parametrization = 'centripetal';
+ this.options.catmullRom.alpha = 0.5;
}
}
}
}
- }
- else {
- // get all items
- if (order) {
- // create an ordered list
- items = [];
- for (id in data) {
- if (data.hasOwnProperty(id)) {
- items.push(data[id]);
- }
- }
-
- this._sort(items, order);
- for (i = 0, len = items.length; i < len; i++) {
- ids[i] = items[i][this.fieldId];
+ if (this.yAxisLeft) {
+ if (options.dataAxis !== undefined) {
+ this.yAxisLeft.setOptions(this.options.dataAxis);
+ this.yAxisRight.setOptions(this.options.dataAxis);
}
}
- else {
- // create unordered list
- for (id in data) {
- if (data.hasOwnProperty(id)) {
- item = data[id];
- ids.push(item[this.fieldId]);
- }
+
+ if (this.legendLeft) {
+ if (options.legend !== undefined) {
+ this.legendLeft.setOptions(this.options.legend);
+ this.legendRight.setOptions(this.options.legend);
}
}
+
+ if (this.groups.hasOwnProperty(UNGROUPED)) {
+ this.groups[UNGROUPED].setOptions(options);
+ }
+ }
+ if (this.dom.frame) {
+ this._updateGraph();
}
+};
- return ids;
+/**
+ * Hide the component from the DOM
+ */
+LineGraph.prototype.hide = function() {
+ // remove the frame containing the items
+ if (this.dom.frame.parentNode) {
+ this.dom.frame.parentNode.removeChild(this.dom.frame);
+ }
};
/**
- * Execute a callback function for every item in the dataset.
- * @param {function} callback
- * @param {Object} [options] Available options:
- * {Object.} [convert]
- * {String[]} [fields] filter fields
- * {function} [filter] filter items
- * {String | function} [order] Order the items by
- * a field name or custom sort function.
+ * Show the component in the DOM (when not already visible).
+ * @return {Boolean} changed
*/
-DataSet.prototype.forEach = function (callback, options) {
- var filter = options && options.filter,
- convert = options && options.convert || this.options.convert,
- data = this.data,
- item,
- id;
+LineGraph.prototype.show = function() {
+ // show frame containing the items
+ if (!this.dom.frame.parentNode) {
+ this.body.dom.center.appendChild(this.dom.frame);
+ }
+};
- if (options && options.order) {
- // execute forEach on ordered list
- var items = this.get(options);
- for (var i = 0, len = items.length; i < len; i++) {
- item = items[i];
- id = item[this.fieldId];
- callback(item, id);
- }
+/**
+ * Set items
+ * @param {vis.DataSet | null} items
+ */
+LineGraph.prototype.setItems = function(items) {
+ var me = this,
+ ids,
+ oldItemsData = this.itemsData;
+
+ // replace the dataset
+ if (!items) {
+ this.itemsData = null;
+ }
+ else if (items instanceof DataSet || items instanceof DataView) {
+ this.itemsData = items;
}
else {
- // unordered
- for (id in data) {
- if (data.hasOwnProperty(id)) {
- item = this._getItem(id, convert);
- if (!filter || filter(item)) {
- callback(item, id);
- }
- }
- }
+ throw new TypeError('Data must be an instance of DataSet or DataView');
+ }
+
+ if (oldItemsData) {
+ // unsubscribe from old dataset
+ util.forEach(this.itemListeners, function (callback, event) {
+ oldItemsData.off(event, callback);
+ });
+
+ // remove all drawn items
+ ids = oldItemsData.getIds();
+ this._onRemove(ids);
+ }
+
+ if (this.itemsData) {
+ // subscribe to new dataset
+ var id = this.id;
+ util.forEach(this.itemListeners, function (callback, event) {
+ me.itemsData.on(event, callback, id);
+ });
+
+ // add all new items
+ ids = this.itemsData.getIds();
+ this._onAdd(ids);
}
+ this._updateUngrouped();
+ this._updateGraph();
+ this.redraw();
};
/**
- * Map every item in the dataset.
- * @param {function} callback
- * @param {Object} [options] Available options:
- * {Object.} [convert]
- * {String[]} [fields] filter fields
- * {function} [filter] filter items
- * {String | function} [order] Order the items by
- * a field name or custom sort function.
- * @return {Object[]} mappedItems
+ * Set groups
+ * @param {vis.DataSet} groups
*/
-DataSet.prototype.map = function (callback, options) {
- var filter = options && options.filter,
- convert = options && options.convert || this.options.convert,
- mappedItems = [],
- data = this.data,
- item;
+LineGraph.prototype.setGroups = function(groups) {
+ var me = this,
+ ids;
- // convert and filter items
- for (var id in data) {
- if (data.hasOwnProperty(id)) {
- item = this._getItem(id, convert);
- if (!filter || filter(item)) {
- mappedItems.push(callback(item, id));
- }
- }
+ // unsubscribe from current dataset
+ if (this.groupsData) {
+ util.forEach(this.groupListeners, function (callback, event) {
+ me.groupsData.unsubscribe(event, callback);
+ });
+
+ // remove all drawn groups
+ ids = this.groupsData.getIds();
+ this.groupsData = null;
+ this._onRemoveGroups(ids); // note: this will cause a redraw
}
- // order items
- if (options && options.order) {
- this._sort(mappedItems, options.order);
+ // replace the dataset
+ if (!groups) {
+ this.groupsData = null;
+ }
+ else if (groups instanceof DataSet || groups instanceof DataView) {
+ this.groupsData = groups;
+ }
+ else {
+ throw new TypeError('Data must be an instance of DataSet or DataView');
}
- return mappedItems;
+ if (this.groupsData) {
+ // subscribe to new dataset
+ var id = this.id;
+ util.forEach(this.groupListeners, function (callback, event) {
+ me.groupsData.on(event, callback, id);
+ });
+
+ // draw all ms
+ ids = this.groupsData.getIds();
+ this._onAddGroups(ids);
+ }
+ this._onUpdate();
};
-/**
- * Filter the fields of an item
- * @param {Object} item
- * @param {String[]} fields Field names
- * @return {Object} filteredItem
- * @private
- */
-DataSet.prototype._filterFields = function (item, fields) {
- var filteredItem = {};
- for (var field in item) {
- if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
- filteredItem[field] = item[field];
- }
+
+LineGraph.prototype._onUpdate = function(ids) {
+ this._updateUngrouped();
+ this._updateAllGroupData();
+ this._updateGraph();
+ this.redraw();
+};
+LineGraph.prototype._onAdd = function (ids) {this._onUpdate(ids);};
+LineGraph.prototype._onRemove = function (ids) {this._onUpdate(ids);};
+LineGraph.prototype._onUpdateGroups = function (groupIds) {
+ for (var i = 0; i < groupIds.length; i++) {
+ var group = this.groupsData.get(groupIds[i]);
+ this._updateGroup(group, groupIds[i]);
}
- return filteredItem;
+ this._updateGraph();
+ this.redraw();
+};
+LineGraph.prototype._onAddGroups = function (groupIds) {this._onUpdateGroups(groupIds);};
+
+LineGraph.prototype._onRemoveGroups = function (groupIds) {
+ for (var i = 0; i < groupIds.length; i++) {
+ if (!this.groups.hasOwnProperty(groupIds[i])) {
+ if (this.groups[groupIds[i]].options.yAxisOrientation == 'right') {
+ this.yAxisRight.removeGroup(groupIds[i]);
+ this.legendRight.removeGroup(groupIds[i]);
+ this.legendRight.redraw();
+ }
+ else {
+ this.yAxisLeft.removeGroup(groupIds[i]);
+ this.legendLeft.removeGroup(groupIds[i]);
+ this.legendLeft.redraw();
+ }
+ delete this.groups[groupIds[i]];
+ }
+ }
+ this._updateUngrouped();
+ this._updateGraph();
+ this.redraw();
};
/**
- * Sort the provided array with items
- * @param {Object[]} items
- * @param {String | function} order A field name or custom sort function.
+ * update a group object
+ *
+ * @param group
+ * @param groupId
* @private
*/
-DataSet.prototype._sort = function (items, order) {
- if (util.isString(order)) {
- // order by provided field name
- var name = order; // field name
- items.sort(function (a, b) {
- var av = a[name];
- var bv = b[name];
- return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
- });
- }
- else if (typeof order === 'function') {
- // order by sort function
- items.sort(order);
+LineGraph.prototype._updateGroup = function (group, groupId) {
+ if (!this.groups.hasOwnProperty(groupId)) {
+ this.groups[groupId] = new GraphGroup(group, groupId, this.options, this.groupsUsingDefaultStyles);
+ if (this.groups[groupId].options.yAxisOrientation == 'right') {
+ this.yAxisRight.addGroup(groupId, this.groups[groupId]);
+ this.legendRight.addGroup(groupId, this.groups[groupId]);
+ }
+ else {
+ this.yAxisLeft.addGroup(groupId, this.groups[groupId]);
+ this.legendLeft.addGroup(groupId, this.groups[groupId]);
+ }
}
- // TODO: extend order by an Object {field:String, direction:String}
- // where direction can be 'asc' or 'desc'
else {
- throw new TypeError('Order must be a function or a string');
+ this.groups[groupId].update(group);
+ if (this.groups[groupId].options.yAxisOrientation == 'right') {
+ this.yAxisRight.updateGroup(groupId, this.groups[groupId]);
+ this.legendRight.updateGroup(groupId, this.groups[groupId]);
+ }
+ else {
+ this.yAxisLeft.updateGroup(groupId, this.groups[groupId]);
+ this.legendLeft.updateGroup(groupId, this.groups[groupId]);
+ }
}
+ this.legendLeft.redraw();
+ this.legendRight.redraw();
};
-/**
- * Remove an object by pointer or by id
- * @param {String | Number | Object | Array} id Object or id, or an array with
- * objects or ids to be removed
- * @param {String} [senderId] Optional sender id
- * @return {Array} removedIds
- */
-DataSet.prototype.remove = function (id, senderId) {
- var removedIds = [],
- i, len, removedId;
+LineGraph.prototype._updateAllGroupData = function () {
+ if (this.itemsData != null) {
+ // ~450 ms @ 500k
- if (id instanceof Array) {
- for (i = 0, len = id.length; i < len; i++) {
- removedId = this._remove(id[i]);
- if (removedId != null) {
- removedIds.push(removedId);
+ var groupsContent = {};
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ groupsContent[groupId] = [];
}
}
- }
- else {
- removedId = this._remove(id);
- if (removedId != null) {
- removedIds.push(removedId);
+ for (var itemId in this.itemsData._data) {
+ if (this.itemsData._data.hasOwnProperty(itemId)) {
+ var item = this.itemsData._data[itemId];
+ item.x = util.convert(item.x,"Date");
+ groupsContent[item.group].push(item);
+ }
}
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ this.groups[groupId].setItems(groupsContent[groupId]);
+ }
+ }
+// // ~4500ms @ 500k
+// for (var groupId in this.groups) {
+// if (this.groups.hasOwnProperty(groupId)) {
+// this.groups[groupId].setItems(this.itemsData.get({filter:
+// function (item) {
+// return (item.group == groupId);
+// }, type:{x:"Date"}}
+// ));
+// }
+// }
}
-
- if (removedIds.length) {
- this._trigger('remove', {items: removedIds}, senderId);
- }
-
- return removedIds;
};
/**
- * Remove an item by its id
- * @param {Number | String | Object} id id or item
- * @returns {Number | String | null} id
- * @private
+ * Create or delete the group holding all ungrouped items. This group is used when
+ * there are no groups specified. This anonymous group is called 'graph'.
+ * @protected
*/
-DataSet.prototype._remove = function (id) {
- if (util.isNumber(id) || util.isString(id)) {
- if (this.data[id]) {
- delete this.data[id];
- delete this.internalIds[id];
- return id;
+LineGraph.prototype._updateUngrouped = function() {
+ if (this.itemsData != null) {
+// var t0 = new Date();
+ var group = {id: UNGROUPED, content: this.options.defaultGroup};
+ this._updateGroup(group, UNGROUPED);
+ var ungroupedCounter = 0;
+ if (this.itemsData) {
+ for (var itemId in this.itemsData._data) {
+ if (this.itemsData._data.hasOwnProperty(itemId)) {
+ var item = this.itemsData._data[itemId];
+ if (item != undefined) {
+ if (item.hasOwnProperty('group')) {
+ if (item.group === undefined) {
+ item.group = UNGROUPED;
+ }
+ }
+ else {
+ item.group = UNGROUPED;
+ }
+ ungroupedCounter = item.group == UNGROUPED ? ungroupedCounter + 1 : ungroupedCounter;
+ }
+ }
+ }
}
- }
- else if (id instanceof Object) {
- var itemId = id[this.fieldId];
- if (itemId && this.data[itemId]) {
- delete this.data[itemId];
- delete this.internalIds[itemId];
- return itemId;
+
+ // much much slower
+// var datapoints = this.itemsData.get({
+// filter: function (item) {return item.group === undefined;},
+// showInternalIds:true
+// });
+// if (datapoints.length > 0) {
+// var updateQuery = [];
+// for (var i = 0; i < datapoints.length; i++) {
+// updateQuery.push({id:datapoints[i].id, group: UNGROUPED});
+// }
+// this.itemsData.update(updateQuery, true);
+// }
+// var t1 = new Date();
+// var pointInUNGROUPED = this.itemsData.get({filter: function (item) {return item.group == UNGROUPED;}});
+ if (ungroupedCounter == 0) {
+ delete this.groups[UNGROUPED];
+ this.legendLeft.removeGroup(UNGROUPED);
+ this.legendRight.removeGroup(UNGROUPED);
+ this.yAxisLeft.removeGroup(UNGROUPED);
+ this.yAxisRight.removeGroup(UNGROUPED);
}
+// console.log("getting amount ungrouped",new Date() - t1);
+// console.log("putting in ungrouped",new Date() - t0);
}
- return null;
+ else {
+ delete this.groups[UNGROUPED];
+ this.legendLeft.removeGroup(UNGROUPED);
+ this.legendRight.removeGroup(UNGROUPED);
+ this.yAxisLeft.removeGroup(UNGROUPED);
+ this.yAxisRight.removeGroup(UNGROUPED);
+ }
+
+ this.legendLeft.redraw();
+ this.legendRight.redraw();
};
+
/**
- * Clear the data
- * @param {String} [senderId] Optional sender id
- * @return {Array} removedIds The ids of all removed items
+ * Redraw the component, mandatory function
+ * @return {boolean} Returns true if the component is resized
*/
-DataSet.prototype.clear = function (senderId) {
- var ids = Object.keys(this.data);
+LineGraph.prototype.redraw = function() {
+ var resized = false;
+
+ this.svg.style.height = ('' + this.options.graphHeight).replace('px','') + 'px';
+ if (this.lastWidth === undefined && this.width || this.lastWidth != this.width) {
+ resized = true;
+ }
+ // check if this component is resized
+ resized = this._isResized() || resized;
+ // check whether zoomed (in that case we need to re-stack everything)
+ var visibleInterval = this.body.range.end - this.body.range.start;
+ var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
+ this.lastVisibleInterval = visibleInterval;
+ this.lastWidth = this.width;
- this.data = {};
- this.internalIds = {};
+ // calculate actual size and position
+ this.width = this.dom.frame.offsetWidth;
- this._trigger('remove', {items: ids}, senderId);
+ // the svg element is three times as big as the width, this allows for fully dragging left and right
+ // without reloading the graph. the controls for this are bound to events in the constructor
+ if (resized == true) {
+ this.svg.style.width = util.option.asSize(3*this.width);
+ this.svg.style.left = util.option.asSize(-this.width);
+ }
+ if (zoomed == true) {
+ this._updateGraph();
+ }
- return ids;
+ this.legendLeft.redraw();
+ this.legendRight.redraw();
+
+ return resized;
};
/**
- * Find the item with maximum value of a specified field
- * @param {String} field
- * @return {Object | null} item Item containing max value, or null if no items
+ * Update and redraw the graph.
+ *
*/
-DataSet.prototype.max = function (field) {
- var data = this.data,
- max = null,
- maxField = null;
-
- for (var id in data) {
- if (data.hasOwnProperty(id)) {
- var item = data[id];
- var itemField = item[field];
- if (itemField != null && (!max || itemField > maxField)) {
- max = item;
- maxField = itemField;
+LineGraph.prototype._updateGraph = function () {
+ // reset the svg elements
+ DOMutil.prepareElements(this.svgElements);
+// // very slow...
+// groupData = group.itemsData.get({filter:
+// function (item) {
+// return (item.x > minDate && item.x < maxDate);
+// }}
+// );
+
+
+ if (this.width != 0 && this.itemsData != null) {
+ var group, groupData, preprocessedGroup, i;
+ var preprocessedGroupData = [];
+ var processedGroupData = [];
+ var groupRanges = [];
+ var changeCalled = false;
+
+ // getting group Ids
+ var groupIds = [];
+ for (var groupId in this.groups) {
+ if (this.groups.hasOwnProperty(groupId)) {
+ groupIds.push(groupId);
+ }
+ }
+
+ // this is the range of the SVG canvas
+ var minDate = this.body.util.toGlobalTime(- this.body.domProps.root.width);
+ var maxDate = this.body.util.toGlobalTime(2 * this.body.domProps.root.width);
+
+ // first select and preprocess the data from the datasets.
+ // the groups have their preselection of data, we now loop over this data to see
+ // what data we need to draw. Sorted data is much faster.
+ // more optimization is possible by doing the sampling before and using the binary search
+ // to find the end date to determine the increment.
+ if (groupIds.length > 0) {
+ for (i = 0; i < groupIds.length; i++) {
+ group = this.groups[groupIds[i]];
+ groupData = [];
+ // optimization for sorted data
+ if (group.options.sort == true) {
+ var guess = Math.max(0,util.binarySearchGeneric(group.itemsData, minDate, 'x', 'before'));
+
+ for (var j = guess; j < group.itemsData.length; j++) {
+ var item = group.itemsData[j];
+ if (item !== undefined) {
+ if (item.x > maxDate) {
+ groupData.push(item);
+ break;
+ }
+ else {
+ groupData.push(item);
+ }
+ }
+ }
+ }
+ else {
+ for (var j = 0; j < group.itemsData.length; j++) {
+ var item = group.itemsData[j];
+ if (item !== undefined) {
+ if (item.x > minDate && item.x < maxDate) {
+ groupData.push(item);
+ }
+ }
+ }
+ }
+ // preprocess, split into ranges and data
+ preprocessedGroup = this._preprocessData(groupData, group);
+ groupRanges.push({min: preprocessedGroup.min, max: preprocessedGroup.max});
+ preprocessedGroupData.push(preprocessedGroup.data);
}
- }
- }
- return max;
-};
+ // update the Y axis first, we use this data to draw at the correct Y points
+ // changeCalled is required to clean the SVG on a change emit.
+ changeCalled = this._updateYAxis(groupIds, groupRanges);
+ if (changeCalled == true) {
+ DOMutil.cleanupElements(this.svgElements);
+ this.body.emitter.emit("change");
+ return;
+ }
-/**
- * Find the item with minimum value of a specified field
- * @param {String} field
- * @return {Object | null} item Item containing max value, or null if no items
- */
-DataSet.prototype.min = function (field) {
- var data = this.data,
- min = null,
- minField = null;
+ // with the yAxis scaled correctly, use this to get the Y values of the points.
+ for (i = 0; i < groupIds.length; i++) {
+ group = this.groups[groupIds[i]];
+ processedGroupData.push(this._convertYvalues(preprocessedGroupData[i],group))
+ }
- for (var id in data) {
- if (data.hasOwnProperty(id)) {
- var item = data[id];
- var itemField = item[field];
- if (itemField != null && (!min || itemField < minField)) {
- min = item;
- minField = itemField;
+ // draw the groups
+ for (i = 0; i < groupIds.length; i++) {
+ group = this.groups[groupIds[i]];
+ if (group.options.style == 'line') {
+ this._drawLineGraph(processedGroupData[i], group);
+ }
+ else {
+ this._drawBarGraph (processedGroupData[i], group);
+ }
}
}
}
- return min;
+ // cleanup unused svg elements
+ DOMutil.cleanupElements(this.svgElements);
};
/**
- * Find all distinct values of a specified field
- * @param {String} field
- * @return {Array} values Array containing all distinct values. If data items
- * do not contain the specified field are ignored.
- * The returned array is unordered.
+ * this sets the Y ranges for the Y axis. It also determines which of the axis should be shown or hidden.
+ * @param {array} groupIds
+ * @private
*/
-DataSet.prototype.distinct = function (field) {
- var data = this.data,
- values = [],
- fieldType = this.options.convert[field],
- count = 0;
+LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) {
+ var changeCalled = false;
+ var yAxisLeftUsed = false;
+ var yAxisRightUsed = false;
+ var minLeft = 1e9, minRight = 1e9, maxLeft = -1e9, maxRight = -1e9, minVal, maxVal;
+ var orientation = 'left';
- for (var prop in data) {
- if (data.hasOwnProperty(prop)) {
- var item = data[prop];
- var value = util.convert(item[field], fieldType);
- var exists = false;
- for (var i = 0; i < count; i++) {
- if (values[i] == value) {
- exists = true;
- break;
- }
+ // if groups are present
+ if (groupIds.length > 0) {
+ for (var i = 0; i < groupIds.length; i++) {
+ orientation = 'left';
+ var group = this.groups[groupIds[i]];
+ if (group.options.yAxisOrientation == 'right') {
+ orientation = 'right';
}
- if (!exists && (value !== undefined)) {
- values[count] = value;
- count++;
+
+ minVal = groupRanges[i].min;
+ maxVal = groupRanges[i].max;
+
+ if (orientation == 'left') {
+ yAxisLeftUsed = true;
+ minLeft = minLeft > minVal ? minVal : minLeft;
+ maxLeft = maxLeft < maxVal ? maxVal : maxLeft;
+ }
+ else {
+ yAxisRightUsed = true;
+ minRight = minRight > minVal ? minVal : minRight;
+ maxRight = maxRight < maxVal ? maxVal : maxRight;
}
}
+ if (yAxisLeftUsed == true) {
+ this.yAxisLeft.setRange(minLeft, maxLeft);
+ }
+ if (yAxisRightUsed == true) {
+ this.yAxisRight.setRange(minRight, maxRight);
+ }
}
- return values;
+ changeCalled = this._toggleAxisVisiblity(yAxisLeftUsed , this.yAxisLeft) || changeCalled;
+ changeCalled = this._toggleAxisVisiblity(yAxisRightUsed, this.yAxisRight) || changeCalled;
+
+ if (yAxisRightUsed == true && yAxisLeftUsed == true) {
+ this.yAxisLeft.drawIcons = true;
+ this.yAxisRight.drawIcons = true;
+ }
+ else {
+ this.yAxisLeft.drawIcons = false;
+ this.yAxisRight.drawIcons = false;
+ }
+
+ this.yAxisRight.master = !yAxisLeftUsed;
+
+ if (this.yAxisRight.master == false) {
+ if (yAxisRightUsed == true) {
+ this.yAxisLeft.lineOffset = this.yAxisRight.width;
+ }
+ changeCalled = this.yAxisLeft.redraw() || changeCalled;
+ this.yAxisRight.stepPixelsForced = this.yAxisLeft.stepPixels;
+ changeCalled = this.yAxisRight.redraw() || changeCalled;
+ }
+ else {
+ changeCalled = this.yAxisRight.redraw() || changeCalled;
+ }
+ return changeCalled;
};
/**
- * Add a single item. Will fail when an item with the same id already exists.
- * @param {Object} item
- * @return {String} id
+ * This shows or hides the Y axis if needed. If there is a change, the changed event is emitted by the updateYAxis function
+ *
+ * @param {boolean} axisUsed
+ * @returns {boolean}
* @private
+ * @param axis
*/
-DataSet.prototype._addItem = function (item) {
- var id = item[this.fieldId];
-
- if (id != undefined) {
- // check whether this id is already taken
- if (this.data[id]) {
- // item already exists
- throw new Error('Cannot add item: item with id ' + id + ' already exists');
+LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) {
+ var changed = false;
+ if (axisUsed == false) {
+ if (axis.dom.frame.parentNode) {
+ axis.hide();
+ changed = true;
}
}
else {
- // generate an id
- id = util.randomUUID();
- item[this.fieldId] = id;
- this.internalIds[id] = item;
- }
-
- var d = {};
- for (var field in item) {
- if (item.hasOwnProperty(field)) {
- var fieldType = this.convert[field]; // type may be undefined
- d[field] = util.convert(item[field], fieldType);
+ if (!axis.dom.frame.parentNode) {
+ axis.show();
+ changed = true;
}
}
- this.data[id] = d;
-
- return id;
+ return changed;
};
+
/**
- * Get an item. Fields can be converted to a specific type
- * @param {String} id
- * @param {Object.} [convert] field types to convert
- * @return {Object | null} item
- * @private
+ * draw a bar graph
+ * @param datapoints
+ * @param group
*/
-DataSet.prototype._getItem = function (id, convert) {
- var field, value;
+LineGraph.prototype._drawBarGraph = function (dataset, group) {
+ if (dataset != null) {
+ if (dataset.length > 0) {
+ var coreDistance;
+ var minWidth = 0.1 * group.options.barChart.width;
+ var offset = 0;
+ var width = group.options.barChart.width;
- // get the item from the dataset
- var raw = this.data[id];
- if (!raw) {
- return null;
- }
+ if (group.options.barChart.align == 'left') {offset -= 0.5*width;}
+ else if (group.options.barChart.align == 'right') {offset += 0.5*width;}
- // convert the items field types
- var converted = {},
- fieldId = this.fieldId,
- internalIds = this.internalIds;
- if (convert) {
- for (field in raw) {
- if (raw.hasOwnProperty(field)) {
- value = raw[field];
- // output all fields, except internal ids
- if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
- converted[field] = util.convert(value, convert[field]);
- }
+ for (var i = 0; i < dataset.length; i++) {
+ // dynammically downscale the width so there is no overlap up to 1/10th the original width
+ if (i+1 < dataset.length) {coreDistance = Math.abs(dataset[i+1].x - dataset[i].x);}
+ if (i > 0) {coreDistance = Math.min(coreDistance,Math.abs(dataset[i-1].x - dataset[i].x));}
+ if (coreDistance < width) {width = coreDistance < minWidth ? minWidth : coreDistance;}
+
+ DOMutil.drawBar(dataset[i].x + offset, dataset[i].y, width, group.zeroPosition - dataset[i].y, group.className + ' bar', this.svgElements, this.svg);
}
- }
- }
- else {
- // no field types specified, no converting needed
- for (field in raw) {
- if (raw.hasOwnProperty(field)) {
- value = raw[field];
- // output all fields, except internal ids
- if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
- converted[field] = value;
- }
+
+ // draw points
+ if (group.options.drawPoints.enabled == true) {
+ this._drawPoints(dataset, group, this.svgElements, this.svg, offset);
}
}
}
- return converted;
};
+
/**
- * Update a single item: merge with existing item.
- * Will fail when the item has no id, or when there does not exist an item
- * with the same id.
- * @param {Object} item
- * @return {String} id
- * @private
+ * draw a line graph
+ *
+ * @param datapoints
+ * @param group
*/
-DataSet.prototype._updateItem = function (item) {
- var id = item[this.fieldId];
- if (id == undefined) {
- throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
- }
- var d = this.data[id];
- if (!d) {
- // item doesn't exist
- throw new Error('Cannot update item: no item with id ' + id + ' found');
- }
+LineGraph.prototype._drawLineGraph = function (dataset, group) {
+ if (dataset != null) {
+ if (dataset.length > 0) {
+ var path, d;
+ var svgHeight = Number(this.svg.style.height.replace("px",""));
+ path = DOMutil.getSVGElement('path', this.svgElements, this.svg);
+ path.setAttributeNS(null, "class", group.className);
- // merge with current item
- for (var field in item) {
- if (item.hasOwnProperty(field)) {
- var fieldType = this.convert[field]; // type may be undefined
- d[field] = util.convert(item[field], fieldType);
+ // construct path from dataset
+ if (group.options.catmullRom.enabled == true) {
+ d = this._catmullRom(dataset, group);
+ }
+ else {
+ d = this._linear(dataset);
+ }
+
+ // append with points for fill and finalize the path
+ if (group.options.shaded.enabled == true) {
+ var fillPath = DOMutil.getSVGElement('path',this.svgElements, this.svg);
+ var dFill;
+ if (group.options.shaded.orientation == 'top') {
+ dFill = "M" + dataset[0].x + "," + 0 + " " + d + "L" + dataset[dataset.length - 1].x + "," + 0;
+ }
+ else {
+ dFill = "M" + dataset[0].x + "," + svgHeight + " " + d + "L" + dataset[dataset.length - 1].x + "," + svgHeight;
+ }
+ fillPath.setAttributeNS(null, "class", group.className + " fill");
+ fillPath.setAttributeNS(null, "d", dFill);
+ }
+ // copy properties to path for drawing.
+ path.setAttributeNS(null, "d", "M" + d);
+
+ // draw points
+ if (group.options.drawPoints.enabled == true) {
+ this._drawPoints(dataset, group, this.svgElements, this.svg);
+ }
}
}
-
- return id;
};
/**
- * check if an id is an internal or external id
- * @param id
- * @returns {boolean}
- * @private
+ * draw the data points
+ *
+ * @param dataset
+ * @param JSONcontainer
+ * @param svg
+ * @param group
*/
-DataSet.prototype.isInternalId = function(id) {
- return (id in this.internalIds);
+LineGraph.prototype._drawPoints = function (dataset, group, JSONcontainer, svg, offset) {
+ if (offset === undefined) {offset = 0;}
+ for (var i = 0; i < dataset.length; i++) {
+ DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, JSONcontainer, svg);
+ }
};
+
/**
- * Get an array with the column names of a Google DataTable
- * @param {DataTable} dataTable
- * @return {String[]} columnNames
+ * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the
+ * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for
+ * the yAxis.
+ *
+ * @param datapoints
+ * @returns {Array}
* @private
*/
-DataSet.prototype._getColumnNames = function (dataTable) {
- var columns = [];
- for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
- columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
+LineGraph.prototype._preprocessData = function (datapoints, group) {
+ var extractedData = [];
+ var xValue, yValue;
+ var toScreen = this.body.util.toScreen;
+
+ var increment = 1;
+ var amountOfPoints = datapoints.length;
+
+ var yMin = datapoints[0].y;
+ var yMax = datapoints[0].y;
+
+ // the global screen is used because changing the width of the yAxis may affect the increment, resulting in an endless loop
+ // of width changing of the yAxis.
+ if (group.options.sampling == true) {
+ var xDistance = this.body.util.toGlobalScreen(datapoints[datapoints.length-1].x) - this.body.util.toGlobalScreen(datapoints[0].x);
+ var pointsPerPixel = amountOfPoints/xDistance;
+ increment = Math.min(Math.ceil(0.2 * amountOfPoints), Math.max(1,Math.round(pointsPerPixel)));
}
- return columns;
+
+ for (var i = 0; i < amountOfPoints; i += increment) {
+ xValue = toScreen(datapoints[i].x) + this.width - 1;
+ yValue = datapoints[i].y;
+ extractedData.push({x: xValue, y: yValue});
+ yMin = yMin > yValue ? yValue : yMin;
+ yMax = yMax < yValue ? yValue : yMax;
+ }
+
+ // extractedData.sort(function (a,b) {return a.x - b.x;});
+ return {min: yMin, max: yMax, data: extractedData};
};
/**
- * Append an item as a row to the dataTable
- * @param dataTable
- * @param columns
- * @param item
+ * This uses the DataAxis object to generate the correct Y coordinate on the SVG window. It uses the
+ * util function toScreen to get the x coordinate from the timestamp.
+ *
+ * @param datapoints
+ * @param options
+ * @returns {Array}
* @private
*/
-DataSet.prototype._appendRow = function (dataTable, columns, item) {
- var row = dataTable.addRow();
+LineGraph.prototype._convertYvalues = function (datapoints, group) {
+ var extractedData = [];
+ var xValue, yValue;
+ var axis = this.yAxisLeft;
+ var svgHeight = Number(this.svg.style.height.replace("px",""));
- for (var col = 0, cols = columns.length; col < cols; col++) {
- var field = columns[col];
- dataTable.setValue(row, col, item[field]);
+ if (group.options.yAxisOrientation == 'right') {
+ axis = this.yAxisRight;
+ }
+
+ for (var i = 0; i < datapoints.length; i++) {
+ xValue = datapoints[i].x;
+ yValue = Math.round(axis.convertValue(datapoints[i].y));
+ extractedData.push({x: xValue, y: yValue});
}
+
+ group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0)));
+
+ // extractedData.sort(function (a,b) {return a.x - b.x;});
+ return extractedData;
};
+
/**
- * DataView
- *
- * a dataview offers a filtered view on a dataset or an other dataview.
- *
- * @param {DataSet | DataView} data
- * @param {Object} [options] Available options: see method get
- *
- * @constructor DataView
+ * This uses an uniform parametrization of the CatmullRom algorithm:
+ * "On the Parameterization of Catmull-Rom Curves" by Cem Yuksel et al.
+ * @param data
+ * @returns {string}
+ * @private
*/
-function DataView (data, options) {
- this.id = util.randomUUID();
+LineGraph.prototype._catmullRomUniform = function(data) {
+ // catmull rom
+ var p0, p1, p2, p3, bp1, bp2;
+ var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
+ var normalization = 1/6;
+ var length = data.length;
+ for (var i = 0; i < length - 1; i++) {
- this.data = null;
- this.ids = {}; // ids of the items currently in memory (just contains a boolean true)
- this.options = options || {};
- this.fieldId = 'id'; // name of the field containing id
- this.subscribers = {}; // event subscribers
+ p0 = (i == 0) ? data[0] : data[i-1];
+ p1 = data[i];
+ p2 = data[i+1];
+ p3 = (i + 2 < length) ? data[i+2] : p2;
- var me = this;
- this.listener = function () {
- me._onEvent.apply(me, arguments);
- };
- this.setData(data);
-}
+ // Catmull-Rom to Cubic Bezier conversion matrix
+ // 0 1 0 0
+ // -1/6 1 1/6 0
+ // 0 1/6 1 -1/6
+ // 0 0 1 0
-// TODO: implement a function .config() to dynamically update things like configured filter
-// and trigger changes accordingly
+ // bp0 = { x: p1.x, y: p1.y };
+ bp1 = { x: ((-p0.x + 6*p1.x + p2.x) *normalization), y: ((-p0.y + 6*p1.y + p2.y) *normalization)};
+ bp2 = { x: (( p1.x + 6*p2.x - p3.x) *normalization), y: (( p1.y + 6*p2.y - p3.y) *normalization)};
+ // bp0 = { x: p2.x, y: p2.y };
+
+ d += "C" +
+ bp1.x + "," +
+ bp1.y + " " +
+ bp2.x + "," +
+ bp2.y + " " +
+ p2.x + "," +
+ p2.y + " ";
+ }
+
+ return d;
+};
/**
- * Set a data source for the view
- * @param {DataSet | DataView} data
+ * This uses either the chordal or centripetal parameterization of the catmull-rom algorithm.
+ * By default, the centripetal parameterization is used because this gives the nicest results.
+ * These parameterizations are relatively heavy because the distance between 4 points have to be calculated.
+ *
+ * One optimization can be used to reuse distances since this is a sliding window approach.
+ * @param data
+ * @returns {string}
+ * @private
*/
-DataView.prototype.setData = function (data) {
- var ids, dataItems, i, len;
+LineGraph.prototype._catmullRom = function(data, group) {
+ var alpha = group.options.catmullRom.alpha;
+ if (alpha == 0 || alpha === undefined) {
+ return this._catmullRomUniform(data);
+ }
+ else {
+ var p0, p1, p2, p3, bp1, bp2, d1,d2,d3, A, B, N, M;
+ var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA;
+ var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
+ var length = data.length;
+ for (var i = 0; i < length - 1; i++) {
+
+ p0 = (i == 0) ? data[0] : data[i-1];
+ p1 = data[i];
+ p2 = data[i+1];
+ p3 = (i + 2 < length) ? data[i+2] : p2;
+
+ d1 = Math.sqrt(Math.pow(p0.x - p1.x,2) + Math.pow(p0.y - p1.y,2));
+ d2 = Math.sqrt(Math.pow(p1.x - p2.x,2) + Math.pow(p1.y - p2.y,2));
+ d3 = Math.sqrt(Math.pow(p2.x - p3.x,2) + Math.pow(p2.y - p3.y,2));
+
+ // Catmull-Rom to Cubic Bezier conversion matrix
+ //
+ // A = 2d1^2a + 3d1^a * d2^a + d3^2a
+ // B = 2d3^2a + 3d3^a * d2^a + d2^2a
+ //
+ // [ 0 1 0 0 ]
+ // [ -d2^2a/N A/N d1^2a/N 0 ]
+ // [ 0 d3^2a/M B/M -d2^2a/M ]
+ // [ 0 0 1 0 ]
- if (this.data) {
- // unsubscribe from current dataset
- if (this.data.unsubscribe) {
- this.data.unsubscribe('*', this.listener);
- }
+ // [ 0 1 0 0 ]
+ // [ -d2pow2a/N A/N d1pow2a/N 0 ]
+ // [ 0 d3pow2a/M B/M -d2pow2a/M ]
+ // [ 0 0 1 0 ]
- // trigger a remove of all items in memory
- ids = [];
- for (var id in this.ids) {
- if (this.ids.hasOwnProperty(id)) {
- ids.push(id);
- }
- }
- this.ids = {};
- this._trigger('remove', {items: ids});
- }
+ d3powA = Math.pow(d3, alpha);
+ d3pow2A = Math.pow(d3,2*alpha);
+ d2powA = Math.pow(d2, alpha);
+ d2pow2A = Math.pow(d2,2*alpha);
+ d1powA = Math.pow(d1, alpha);
+ d1pow2A = Math.pow(d1,2*alpha);
- this.data = data;
+ A = 2*d1pow2A + 3*d1powA * d2powA + d2pow2A;
+ B = 2*d3pow2A + 3*d3powA * d2powA + d2pow2A;
+ N = 3*d1powA * (d1powA + d2powA);
+ if (N > 0) {N = 1 / N;}
+ M = 3*d3powA * (d3powA + d2powA);
+ if (M > 0) {M = 1 / M;}
- if (this.data) {
- // update fieldId
- this.fieldId = this.options.fieldId ||
- (this.data && this.data.options && this.data.options.fieldId) ||
- 'id';
+ bp1 = { x: ((-d2pow2A * p0.x + A*p1.x + d1pow2A * p2.x) * N),
+ y: ((-d2pow2A * p0.y + A*p1.y + d1pow2A * p2.y) * N)};
- // trigger an add of all added items
- ids = this.data.getIds({filter: this.options && this.options.filter});
- for (i = 0, len = ids.length; i < len; i++) {
- id = ids[i];
- this.ids[id] = true;
+ bp2 = { x: (( d3pow2A * p1.x + B*p2.x - d2pow2A * p3.x) * M),
+ y: (( d3pow2A * p1.y + B*p2.y - d2pow2A * p3.y) * M)};
+
+ if (bp1.x == 0 && bp1.y == 0) {bp1 = p1;}
+ if (bp2.x == 0 && bp2.y == 0) {bp2 = p2;}
+ d += "C" +
+ bp1.x + "," +
+ bp1.y + " " +
+ bp2.x + "," +
+ bp2.y + " " +
+ p2.x + "," +
+ p2.y + " ";
}
- this._trigger('add', {items: ids});
- // subscribe to new dataset
- if (this.data.on) {
- this.data.on('*', this.listener);
+ return d;
+ }
+};
+
+/**
+ * this generates the SVG path for a linear drawing between datapoints.
+ * @param data
+ * @returns {string}
+ * @private
+ */
+LineGraph.prototype._linear = function(data) {
+ // linear
+ var d = "";
+ for (var i = 0; i < data.length; i++) {
+ if (i == 0) {
+ d += data[i].x + "," + data[i].y;
+ }
+ else {
+ d += " " + data[i].x + "," + data[i].y;
}
}
+ return d;
};
+
+
+
+
+
/**
- * Get data from the data view
- *
- * Usage:
- *
- * get()
- * get(options: Object)
- * get(options: Object, data: Array | DataTable)
+ * @constructor DataStep
+ * The class DataStep is an iterator for data for the lineGraph. You provide a start data point and an
+ * end data point. The class itself determines the best scale (step size) based on the
+ * provided start Date, end Date, and minimumStep.
*
- * get(id: Number)
- * get(id: Number, options: Object)
- * get(id: Number, options: Object, data: Array | DataTable)
+ * If minimumStep is provided, the step size is chosen as close as possible
+ * to the minimumStep but larger than minimumStep. If minimumStep is not
+ * provided, the scale is set to 1 DAY.
+ * The minimumStep should correspond with the onscreen size of about 6 characters
*
- * get(ids: Number[])
- * get(ids: Number[], options: Object)
- * get(ids: Number[], options: Object, data: Array | DataTable)
+ * Alternatively, you can set a scale by hand.
+ * After creation, you can initialize the class by executing first(). Then you
+ * can iterate from the start date to the end date via next(). You can check if
+ * the end date is reached with the function hasNext(). After each step, you can
+ * retrieve the current date via getCurrent().
+ * The DataStep has scales ranging from milliseconds, seconds, minutes, hours,
+ * days, to years.
*
- * Where:
+ * Version: 1.2
*
- * {Number | String} id The id of an item
- * {Number[] | String{}} ids An array with ids of items
- * {Object} options An Object with options. Available options:
- * {String} [type] Type of data to be returned. Can
- * be 'DataTable' or 'Array' (default)
- * {Object.} [convert]
- * {String[]} [fields] field names to be returned
- * {function} [filter] filter items
- * {String | function} [order] Order the items by
- * a field name or custom sort function.
- * {Array | DataTable} [data] If provided, items will be appended to this
- * array or table. Required in case of Google
- * DataTable.
- * @param args
+ * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
+ * or new Date(2010, 9, 21, 23, 45, 00)
+ * @param {Date} [end] The end date
+ * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
*/
-DataView.prototype.get = function (args) {
- var me = this;
+function DataStep(start, end, minimumStep, containerHeight, forcedStepSize) {
+ // variables
+ this.current = 0;
- // parse the arguments
- var ids, options, data;
- var firstType = util.getType(arguments[0]);
- if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
- // get(id(s) [, options] [, data])
- ids = arguments[0]; // can be a single id or an array with ids
- options = arguments[1];
- data = arguments[2];
- }
- else {
- // get([, options] [, data])
- options = arguments[0];
- data = arguments[1];
- }
+ this.autoScale = true;
+ this.stepIndex = 0;
+ this.step = 1;
+ this.scale = 1;
- // extend the options with the default options and provided options
- var viewOptions = util.extend({}, this.options, options);
+ this.marginStart;
+ this.marginEnd;
- // create a combined filter method when needed
- if (this.options.filter && options && options.filter) {
- viewOptions.filter = function (item) {
- return me.options.filter(item) && options.filter(item);
- }
- }
+ this.majorSteps = [1, 2, 5, 10];
+ this.minorSteps = [0.25, 0.5, 1, 2];
+
+ this.setRange(start, end, minimumStep, containerHeight, forcedStepSize);
+}
- // build up the call to the linked data set
- var getArguments = [];
- if (ids != undefined) {
- getArguments.push(ids);
- }
- getArguments.push(viewOptions);
- getArguments.push(data);
- return this.data && this.data.get.apply(this.data, getArguments);
-};
/**
- * Get ids of all items or from a filtered set of items.
- * @param {Object} [options] An Object with options. Available options:
- * {function} [filter] filter items
- * {String | function} [order] Order the items by
- * a field name or custom sort function.
- * @return {Array} ids
+ * Set a new range
+ * If minimumStep is provided, the step size is chosen as close as possible
+ * to the minimumStep but larger than minimumStep. If minimumStep is not
+ * provided, the scale is set to 1 DAY.
+ * The minimumStep should correspond with the onscreen size of about 6 characters
+ * @param {Number} [start] The start date and time.
+ * @param {Number} [end] The end date and time.
+ * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
*/
-DataView.prototype.getIds = function (options) {
- var ids;
+DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight, forcedStepSize) {
+ this._start = start;
+ this._end = end;
- if (this.data) {
- var defaultFilter = this.options.filter;
- var filter;
+ if (this.autoScale) {
+ this.setMinimumStep(minimumStep, containerHeight, forcedStepSize);
+ }
+ this.setFirst();
+};
- if (options && options.filter) {
- if (defaultFilter) {
- filter = function (item) {
- return defaultFilter(item) && options.filter(item);
- }
- }
- else {
- filter = options.filter;
+/**
+ * Automatically determine the scale that bests fits the provided minimum step
+ * @param {Number} [minimumStep] The minimum step size in milliseconds
+ */
+DataStep.prototype.setMinimumStep = function(minimumStep, containerHeight) {
+ // round to floor
+ var size = this._end - this._start;
+ var safeSize = size * 1.1;
+ var minimumStepValue = minimumStep * (safeSize / containerHeight);
+ var orderOfMagnitude = Math.round(Math.log(safeSize)/Math.LN10);
+
+ var minorStepIdx = -1;
+ var magnitudefactor = Math.pow(10,orderOfMagnitude);
+
+ var start = 0;
+ if (orderOfMagnitude < 0) {
+ start = orderOfMagnitude;
+ }
+
+ var solutionFound = false;
+ for (var i = start; Math.abs(i) <= Math.abs(orderOfMagnitude); i++) {
+ magnitudefactor = Math.pow(10,i);
+ for (var j = 0; j < this.minorSteps.length; j++) {
+ var stepSize = magnitudefactor * this.minorSteps[j];
+ if (stepSize >= minimumStepValue) {
+ solutionFound = true;
+ minorStepIdx = j;
+ break;
}
}
- else {
- filter = defaultFilter;
+ if (solutionFound == true) {
+ break;
}
-
- ids = this.data.getIds({
- filter: filter,
- order: options && options.order
- });
- }
- else {
- ids = [];
}
+ this.stepIndex = minorStepIdx;
+ this.scale = magnitudefactor;
+ this.step = magnitudefactor * this.minorSteps[minorStepIdx];
+};
- return ids;
+
+/**
+ * Set the range iterator to the start date.
+ */
+DataStep.prototype.first = function() {
+ this.setFirst();
};
/**
- * Event listener. Will propagate all events from the connected data set to
- * the subscribers of the DataView, but will filter the items and only trigger
- * when there are changes in the filtered data set.
- * @param {String} event
- * @param {Object | null} params
- * @param {String} senderId
- * @private
+ * Round the current date to the first minor date value
+ * This must be executed once when the current date is set to start Date
*/
-DataView.prototype._onEvent = function (event, params, senderId) {
- var i, len, id, item,
- ids = params && params.items,
- data = this.data,
- added = [],
- updated = [],
- removed = [];
+DataStep.prototype.setFirst = function() {
+ var niceStart = this._start - (this.scale * this.minorSteps[this.stepIndex]);
+ var niceEnd = this._end + (this.scale * this.minorSteps[this.stepIndex]);
- if (ids && data) {
- switch (event) {
- case 'add':
- // filter the ids of the added items
- for (i = 0, len = ids.length; i < len; i++) {
- id = ids[i];
- item = this.get(id);
- if (item) {
- this.ids[id] = true;
- added.push(id);
- }
- }
+ this.marginEnd = this.roundToMinor(niceEnd);
+ this.marginStart = this.roundToMinor(niceStart);
+ this.marginRange = this.marginEnd - this.marginStart;
- break;
+ this.current = this.marginEnd;
- case 'update':
- // determine the event from the views viewpoint: an updated
- // item can be added, updated, or removed from this view.
- for (i = 0, len = ids.length; i < len; i++) {
- id = ids[i];
- item = this.get(id);
+};
- if (item) {
- if (this.ids[id]) {
- updated.push(id);
- }
- else {
- this.ids[id] = true;
- added.push(id);
- }
- }
- else {
- if (this.ids[id]) {
- delete this.ids[id];
- removed.push(id);
- }
- else {
- // nothing interesting for me :-(
- }
- }
- }
+DataStep.prototype.roundToMinor = function(value) {
+ var rounded = value - (value % (this.scale * this.minorSteps[this.stepIndex]));
+ if (value % (this.scale * this.minorSteps[this.stepIndex]) > 0.5 * (this.scale * this.minorSteps[this.stepIndex])) {
+ return rounded + (this.scale * this.minorSteps[this.stepIndex]);
+ }
+ else {
+ return rounded;
+ }
+}
- break;
- case 'remove':
- // filter the ids of the removed items
- for (i = 0, len = ids.length; i < len; i++) {
- id = ids[i];
- if (this.ids[id]) {
- delete this.ids[id];
- removed.push(id);
- }
- }
+/**
+ * Check if the there is a next step
+ * @return {boolean} true if the current date has not passed the end date
+ */
+DataStep.prototype.hasNext = function () {
+ return (this.current >= this.marginStart);
+};
- break;
- }
+/**
+ * Do the next step
+ */
+DataStep.prototype.next = function() {
+ var prev = this.current;
+ this.current -= this.step;
- if (added.length) {
- this._trigger('add', {items: added}, senderId);
+ // safety mechanism: if current time is still unchanged, move to the end
+ if (this.current == prev) {
+ this.current = this._end;
+ }
+};
+
+/**
+ * Do the next step
+ */
+DataStep.prototype.previous = function() {
+ this.current += this.step;
+ this.marginEnd += this.step;
+ this.marginRange = this.marginEnd - this.marginStart;
+};
+
+
+
+/**
+ * Get the current datetime
+ * @return {Number} current The current date
+ */
+DataStep.prototype.getCurrent = function() {
+ var toPrecision = '' + Number(this.current).toPrecision(5);
+ for (var i = toPrecision.length-1; i > 0; i--) {
+ if (toPrecision[i] == "0") {
+ toPrecision = toPrecision.slice(0,i);
}
- if (updated.length) {
- this._trigger('update', {items: updated}, senderId);
+ else if (toPrecision[i] == "." || toPrecision[i] == ",") {
+ toPrecision = toPrecision.slice(0,i);
+ break;
}
- if (removed.length) {
- this._trigger('remove', {items: removed}, senderId);
+ else{
+ break;
}
}
+
+
+ return toPrecision;
};
-// copy subscription functionality from DataSet
-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;
+
+/**
+ * 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
+ */
+DataStep.prototype.snap = function(date) {
+
+};
+
+/**
+ * Check if the current value is a major value (for example when the step
+ * is DAY, a major value is each first day of the MONTH)
+ * @return {boolean} true if current date is major, else false.
+ */
+DataStep.prototype.isMajor = function() {
+ return (this.current % (this.scale * this.majorSteps[this.stepIndex]) == 0);
+};
/**
* Utility functions for ordering and stacking of items
@@ -2519,7 +5021,7 @@ var stack = {};
* Order items by their start data
* @param {Item[]} items
*/
-stack.orderByStart = function orderByStart(items) {
+stack.orderByStart = function(items) {
items.sort(function (a, b) {
return a.data.start - b.data.start;
});
@@ -2530,7 +5032,7 @@ stack.orderByStart = function orderByStart(items) {
* is used.
* @param {Item[]} items
*/
-stack.orderByEnd = function orderByEnd(items) {
+stack.orderByEnd = function(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;
@@ -2550,7 +5052,7 @@ stack.orderByEnd = function orderByEnd(items) {
* If true, all items will be repositioned. If false (default), only
* items having a top===null will be re-stacked
*/
-stack.stack = function _stack (items, margin, force) {
+stack.stack = function(items, margin, force) {
var i, iMax;
if (force) {
@@ -2595,7 +5097,7 @@ stack.stack = function _stack (items, margin, force) {
* @param {{item: number, axis: number}} margin
* Margins between items and between items and the axis.
*/
-stack.nostack = function nostack (items, margin) {
+stack.nostack = function(items, margin) {
var i, iMax;
// reset top position of all items
@@ -2616,7 +5118,7 @@ stack.nostack = function nostack (items, margin) {
* the requested margin.
* @return {boolean} true if a and b collide, else false
*/
-stack.collision = function collision (a, b, margin) {
+stack.collision = function(a, b, margin) {
return ((a.left - margin) < (b.left + b.width) &&
(a.left + a.width + margin) > b.left &&
(a.top - margin) < (b.top + b.height) &&
@@ -3095,57 +5597,81 @@ TimeStep.prototype.getLabelMajor = function(date) {
* 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 {RootPanel} root Root panel, used to subscribe to events
- * @param {Panel} parent Parent panel, used to attach to the DOM
+ * @param {{dom: Object, domProps: Object, emitter: Emitter}} body
* @param {Object} [options] See description at Range.setOptions
*/
-function Range(root, parent, options) {
- this.id = util.randomUUID();
- this.start = null; // Number
- this.end = null; // Number
+function Range(body, options) {
+ var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
+ this.start = now.clone().add('days', -3).valueOf(); // Number
+ this.end = now.clone().add('days', 4).valueOf(); // Number
- this.root = root;
- this.parent = parent;
- this.options = options || {};
+ this.body = body;
+
+ // default options
+ this.defaultOptions = {
+ start: null,
+ end: null,
+ direction: 'horizontal', // 'horizontal' or 'vertical'
+ moveable: true,
+ zoomable: true,
+ min: null,
+ max: null,
+ zoomMin: 10, // milliseconds
+ zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000 // milliseconds
+ };
+ this.options = util.extend({}, this.defaultOptions);
+
+ this.props = {
+ touch: {}
+ };
// 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));
+ this.body.emitter.on('dragstart', this._onDragStart.bind(this));
+ this.body.emitter.on('drag', this._onDrag.bind(this));
+ this.body.emitter.on('dragend', this._onDragEnd.bind(this));
// ignore dragging when holding
- this.root.on('hold', this._onHold.bind(this));
+ this.body.emitter.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
+ this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this));
+ this.body.emitter.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.body.emitter.on('touch', this._onTouch.bind(this));
+ this.body.emitter.on('pinch', this._onPinch.bind(this));
this.setOptions(options);
}
-// turn Range into an event emitter
-Emitter(Range.prototype);
+Range.prototype = new Component();
/**
* Set options for the range controller
* @param {Object} options Available options:
+ * {Number | Date | String} start Start date for the range
+ * {Number | Date | String} end End date for the range
* {Number} min Minimum value for start
* {Number} max Maximum value for end
* {Number} zoomMin Set a minimum value for
* (end - start).
* {Number} zoomMax Set a maximum value for
* (end - start).
+ * {Boolean} moveable Enable moving of the range
+ * by dragging. True by default
+ * {Boolean} zoomable Enable zooming of the range
+ * by pinching/scrolling. True by default
*/
Range.prototype.setOptions = function (options) {
- util.extend(this.options, options);
+ if (options) {
+ // copy the options that we know
+ var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable'];
+ util.selectiveExtend(fields, this.options, options);
- // re-apply range with new limitations
- if (this.start !== null && this.end !== null) {
- this.setRange(this.start, this.end);
+ if ('start' in options || 'end' in options) {
+ // apply a new range. both start and end are optional
+ this.setRange(options.start, options.end);
+ }
}
};
@@ -3172,8 +5698,8 @@ Range.prototype.setRange = function(start, end) {
start: new Date(this.start),
end: new Date(this.end)
};
- this.emit('rangechange', params);
- this.emit('rangechanged', params);
+ this.body.emitter.emit('rangechange', params);
+ this.body.emitter.emit('rangechanged', params);
}
};
@@ -3332,77 +5858,70 @@ Range.conversion = function (start, end, width) {
}
};
-// global (private) object to store drag params
-var touchParams = {};
-
/**
* Start dragging horizontally or vertically
* @param {Event} event
* @private
*/
Range.prototype._onDragStart = function(event) {
+ // only allow dragging when configured as movable
+ if (!this.options.moveable) return;
+
// 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.ignore) return;
-
- // TODO: reckon with option movable
+ if (!this.props.touch.allowDragging) return;
- touchParams.start = this.start;
- touchParams.end = this.end;
+ this.props.touch.start = this.start;
+ this.props.touch.end = this.end;
- var frame = this.parent.frame;
- if (frame) {
- frame.style.cursor = 'move';
+ if (this.body.dom.root) {
+ this.body.dom.root.style.cursor = 'move';
}
};
/**
- * Perform dragging operating.
+ * Perform dragging operation
* @param {Event} event
* @private
*/
Range.prototype._onDrag = function (event) {
+ // only allow dragging when configured as movable
+ if (!this.options.moveable) return;
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.ignore) return;
-
+ if (!this.props.touch.allowDragging) return;
var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
- interval = (touchParams.end - touchParams.start),
- width = (direction == 'horizontal') ? this.parent.width : this.parent.height,
+ interval = (this.props.touch.end - this.props.touch.start),
+ width = (direction == 'horizontal') ? this.body.domProps.center.width : this.body.domProps.center.height,
diffRange = -delta / width * interval;
-
- this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange);
-
- this.emit('rangechange', {
+ this._applyRange(this.props.touch.start + diffRange, this.props.touch.end + diffRange);
+ this.body.emitter.emit('rangechange', {
start: new Date(this.start),
end: new Date(this.end)
});
};
/**
- * Stop dragging operating.
+ * Stop dragging operation
* @param {event} event
* @private
*/
Range.prototype._onDragEnd = function (event) {
+ // only allow dragging when configured as movable
+ if (!this.options.moveable) return;
+
// 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.ignore) return;
-
- // TODO: reckon with option movable
+ if (!this.props.touch.allowDragging) return;
- if (this.parent.frame) {
- this.parent.frame.style.cursor = 'auto';
+ if (this.body.dom.root) {
+ this.body.dom.root.style.cursor = 'auto';
}
// fire a rangechanged event
- this.emit('rangechanged', {
+ this.body.emitter.emit('rangechanged', {
start: new Date(this.start),
end: new Date(this.end)
});
@@ -3415,7 +5934,8 @@ Range.prototype._onDragEnd = function (event) {
* @private
*/
Range.prototype._onMouseWheel = function(event) {
- // TODO: reckon with option zoomable
+ // only allow zooming when configured as zoomable and moveable
+ if (!(this.options.zoomable && this.options.moveable)) return;
// retrieve delta
var delta = 0;
@@ -3445,7 +5965,7 @@ Range.prototype._onMouseWheel = function(event) {
// calculate center, the date to zoom around
var gesture = util.fakeGesture(this, event),
- pointer = getPointer(gesture.center, this.parent.frame),
+ pointer = getPointer(gesture.center, this.body.dom.center),
pointerDate = this._pointerToDate(pointer);
this.zoom(scale, pointerDate);
@@ -3461,17 +5981,10 @@ Range.prototype._onMouseWheel = function(event) {
* @private
*/
Range.prototype._onTouch = function (event) {
- touchParams.start = this.start;
- touchParams.end = this.end;
- 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;
- }
+ this.props.touch.start = this.start;
+ this.props.touch.end = this.end;
+ this.props.touch.allowDragging = true;
+ this.props.touch.center = null;
};
/**
@@ -3479,7 +5992,7 @@ Range.prototype._onTouch = function (event) {
* @private
*/
Range.prototype._onHold = function () {
- touchParams.ignore = true;
+ this.props.touch.allowDragging = false;
};
/**
@@ -3487,575 +6000,190 @@ Range.prototype._onHold = function () {
* @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, this.parent.frame);
- }
-
- var scale = 1 / event.gesture.scale,
- 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
- var newStart = parseInt(initDate + (touchParams.start - initDate) * scale);
- var newEnd = parseInt(initDate + (touchParams.end - initDate) * scale);
-
- // apply new range
- this.setRange(newStart, newEnd);
- }
-};
-
-/**
- * Helper function to calculate the center date for zooming
- * @param {{x: Number, y: Number}} pointer
- * @return {number} date
- * @private
- */
-Range.prototype._pointerToDate = function (pointer) {
- var conversion;
- var direction = this.options.direction;
-
- validateDirection(direction);
-
- if (direction == 'horizontal') {
- var width = this.parent.width;
- conversion = this.conversion(width);
- return pointer.x / conversion.scale + conversion.offset;
- }
- else {
- var height = this.parent.height;
- conversion = this.conversion(height);
- return pointer.y / conversion.scale + conversion.offset;
- }
-};
-
-/**
- * Get the pointer location relative to the location of the dom element
- * @param {{pageX: Number, pageY: Number}} touch
- * @param {Element} element HTML DOM element
- * @return {{x: Number, y: Number}} pointer
- * @private
- */
-function getPointer (touch, element) {
- return {
- x: touch.pageX - vis.util.getAbsoluteLeft(element),
- y: touch.pageY - vis.util.getAbsoluteTop(element)
- };
-}
-
-/**
- * Zoom the range the given scale in or out. Start and end date will
- * be adjusted, and the timeline will be redrawn. You can optionally give a
- * date around which to zoom.
- * For example, try scale = 0.9 or 1.1
- * @param {Number} scale Scaling factor. Values above 1 will zoom out,
- * values below 1 will zoom in.
- * @param {Number} [center] Value representing a date around which will
- * be zoomed.
- */
-Range.prototype.zoom = function(scale, center) {
- // if centerDate is not provided, take it half between start Date and end Date
- if (center == null) {
- center = (this.start + this.end) / 2;
- }
-
- // calculate new start and end
- var newStart = center + (this.start - center) * scale;
- var newEnd = center + (this.end - center) * scale;
-
- this.setRange(newStart, newEnd);
-};
-
-/**
- * Move the range with a given delta to the left or right. Start and end
- * value will be adjusted. For example, try delta = 0.1 or -0.1
- * @param {Number} delta Moving amount. Positive value will move right,
- * negative value will move left
- */
-Range.prototype.move = function(delta) {
- // zoom start Date and end Date relative to the centerDate
- var diff = (this.end - this.start);
-
- // apply new values
- var newStart = this.start + diff * delta;
- var newEnd = this.end + diff * delta;
-
- // TODO: reckon with min and max range
-
- this.start = newStart;
- this.end = newEnd;
-};
-
-/**
- * Move the range to a new center point
- * @param {Number} moveTo New center point of the range
- */
-Range.prototype.moveTo = function(moveTo) {
- var center = (this.start + this.end) / 2;
-
- var diff = center - moveTo;
-
- // calculate new start and end
- var newStart = this.start - diff;
- var newEnd = this.end - diff;
-
- this.setRange(newStart, newEnd);
-};
-
-/**
- * Prototype for visual components
- */
-function Component () {
- this.id = null;
- this.parent = null;
- this.childs = null;
- this.options = null;
-
- 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]
- * {String | Number | function} [left]
- * {String | Number | function} [top]
- * {String | Number | function} [width]
- * {String | Number | function} [height]
- */
-Component.prototype.setOptions = function setOptions(options) {
- if (options) {
- util.extend(this.options, options);
-
- this.repaint();
- }
-};
-
-/**
- * Get an option value by name
- * The function will first check this.options object, and else will check
- * this.defaultOptions.
- * @param {String} name
- * @return {*} value
- */
-Component.prototype.getOption = function getOption(name) {
- var value;
- if (this.options) {
- value = this.options[name];
- }
- if (value === undefined && this.defaultOptions) {
- value = this.defaultOptions[name];
- }
- return value;
-};
-
-/**
- * Get the frame element of the component, the outer HTML DOM element.
- * @returns {HTMLElement | null} frame
- */
-Component.prototype.getFrame = function getFrame() {
- // should be implemented by the component
- return null;
-};
-
-/**
- * Repaint the component
- * @return {boolean} Returns true if the component is resized
- */
-Component.prototype.repaint = function repaint() {
- // should be implemented by the component
- return false;
-};
-
-/**
- * Test whether the component is resized since the last time _isResized() was
- * called.
- * @return {Boolean} Returns true if the component is resized
- * @protected
- */
-Component.prototype._isResized = function _isResized() {
- var resized = (this._previousWidth !== this.width || this._previousHeight !== this.height);
-
- this._previousWidth = this.width;
- this._previousHeight = this.height;
-
- return resized;
-};
-
-/**
- * A panel can contain components
- * @param {Object} [options] Available parameters:
- * {String | Number | function} [left]
- * {String | Number | function} [top]
- * {String | Number | function} [width]
- * {String | Number | function} [height]
- * {String | function} [className]
- * @constructor Panel
- * @extends Component
- */
-function Panel(options) {
- this.id = util.randomUUID();
- this.parent = null;
- this.childs = [];
-
- this.options = options || {};
-
- // create frame
- this.frame = (typeof document !== 'undefined') ? document.createElement('div') : null;
-}
-
-Panel.prototype = new Component();
-
-/**
- * Set options. Will extend the current options.
- * @param {Object} [options] Available parameters:
- * {String | function} [className]
- * {String | Number | function} [left]
- * {String | Number | function} [top]
- * {String | Number | function} [width]
- * {String | Number | function} [height]
- */
-Panel.prototype.setOptions = Component.prototype.setOptions;
-
-/**
- * Get the outer frame of the panel
- * @returns {HTMLElement} frame
- */
-Panel.prototype.getFrame = function () {
- return this.frame;
-};
-
-/**
- * Append a child to the panel
- * @param {Component} child
- */
-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 beforeFrame = beforeChild.getFrame();
- if (beforeFrame) {
- this.frame.insertBefore(frame, beforeFrame);
- }
- else {
- this.frame.appendChild(frame);
- }
- }
- }
-};
-
-/**
- * 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);
- }
- }
-};
-
-/**
- * Test whether the panel contains given child
- * @param {Component} child
- */
-Panel.prototype.hasChild = function (child) {
- var index = this.childs.indexOf(child);
- return (index != -1);
-};
-
-/**
- * Repaint the component
- * @return {boolean} Returns true if the component was resized since previous repaint
- */
-Panel.prototype.repaint = function () {
- var asString = util.option.asString,
- options = this.options,
- frame = this.getFrame();
+Range.prototype._onPinch = function (event) {
+ // only allow zooming when configured as zoomable and moveable
+ if (!(this.options.zoomable && this.options.moveable)) return;
- // update className
- frame.className = 'vpanel' + (options.className ? (' ' + asString(options.className)) : '');
+ this.props.touch.allowDragging = false;
- // repaint the child components
- var childsResized = this._repaintChilds();
+ if (event.gesture.touches.length > 1) {
+ if (!this.props.touch.center) {
+ this.props.touch.center = getPointer(event.gesture.center, this.body.dom.center);
+ }
- // update frame size
- this._updateSize();
+ var scale = 1 / event.gesture.scale,
+ initDate = this._pointerToDate(this.props.touch.center);
- return this._isResized() || childsResized;
-};
+ // calculate new start and end
+ var newStart = parseInt(initDate + (this.props.touch.start - initDate) * scale);
+ var newEnd = parseInt(initDate + (this.props.touch.end - initDate) * scale);
-/**
- * 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;
+ // apply new range
+ this.setRange(newStart, newEnd);
}
- return resized;
};
/**
- * Apply the size from options to the panel, and recalculate it's actual size.
+ * Helper function to calculate the center date for zooming
+ * @param {{x: Number, y: Number}} pointer
+ * @return {number} date
* @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, '');
+Range.prototype._pointerToDate = function (pointer) {
+ var conversion;
+ var direction = this.options.direction;
+
+ validateDirection(direction);
- // get actual size
- this.top = this.frame.offsetTop;
- this.left = this.frame.offsetLeft;
- this.width = this.frame.offsetWidth;
- this.height = this.frame.offsetHeight;
+ if (direction == 'horizontal') {
+ var width = this.body.domProps.center.width;
+ conversion = this.conversion(width);
+ return pointer.x / conversion.scale + conversion.offset;
+ }
+ else {
+ var height = this.body.domProps.center.height;
+ conversion = this.conversion(height);
+ return pointer.y / conversion.scale + conversion.offset;
+ }
};
/**
- * A root panel can hold components. The root panel must be initialized with
- * a DOM element as container.
- * @param {HTMLElement} container
- * @param {Object} [options] Available parameters: see RootPanel.setOptions.
- * @constructor RootPanel
- * @extends Panel
+ * Get the pointer location relative to the location of the dom element
+ * @param {{pageX: Number, pageY: Number}} touch
+ * @param {Element} element HTML DOM element
+ * @return {{x: Number, y: Number}} pointer
+ * @private
*/
-function RootPanel(container, options) {
- this.id = util.randomUUID();
- this.container = container;
-
- this.options = options || {};
- this.defaultOptions = {
- autoResize: true
+function getPointer (touch, element) {
+ return {
+ x: touch.pageX - vis.util.getAbsoluteLeft(element),
+ y: touch.pageY - vis.util.getAbsoluteTop(element)
};
+}
- // 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());
-
+/**
+ * Zoom the range the given scale in or out. Start and end date will
+ * be adjusted, and the timeline will be redrawn. You can optionally give a
+ * date around which to zoom.
+ * For example, try scale = 0.9 or 1.1
+ * @param {Number} scale Scaling factor. Values above 1 will zoom out,
+ * values below 1 will zoom in.
+ * @param {Number} [center] Value representing a date around which will
+ * be zoomed.
+ */
+Range.prototype.zoom = function(scale, center) {
+ // if centerDate is not provided, take it half between start Date and end Date
+ if (center == null) {
+ center = (this.start + this.end) / 2;
+ }
- this._initWatch();
-}
+ // calculate new start and end
+ var newStart = center + (this.start - center) * scale;
+ var newEnd = center + (this.end - center) * scale;
-RootPanel.prototype = new Panel();
+ this.setRange(newStart, newEnd);
+};
/**
- * Create the HTML DOM for the root panel
+ * Move the range with a given delta to the left or right. Start and end
+ * value will be adjusted. For example, try delta = 0.1 or -0.1
+ * @param {Number} delta Moving amount. Positive value will move right,
+ * negative value will move left
*/
-RootPanel.prototype._create = function _create() {
- // create frame
- this.frame = document.createElement('div');
+Range.prototype.move = function(delta) {
+ // zoom start Date and end Date relative to the centerDate
+ var diff = (this.end - this.start);
- // create event listeners for all interesting events, these events will be
- // emitted via emitter
- this.hammer = Hammer(this.frame, {
- prevent_default: true
- });
- this.listeners = {};
+ // apply new values
+ var newStart = this.start + diff * delta;
+ var newEnd = this.end + diff * delta;
- 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;
- });
+ // TODO: reckon with min and max range
+
+ this.start = newStart;
+ this.end = newEnd;
};
/**
- * Set options. Will extend the current options.
- * @param {Object} [options] Available parameters:
- * {String | function} [className]
- * {String | Number | function} [left]
- * {String | Number | function} [top]
- * {String | Number | function} [width]
- * {String | Number | function} [height]
- * {Boolean | function} [autoResize]
+ * Move the range to a new center point
+ * @param {Number} moveTo New center point of the range
*/
-RootPanel.prototype.setOptions = function setOptions(options) {
- if (options) {
- util.extend(this.options, options);
+Range.prototype.moveTo = function(moveTo) {
+ var center = (this.start + this.end) / 2;
- this.repaint();
+ var diff = center - moveTo;
- this._initWatch();
- }
+ // calculate new start and end
+ var newStart = this.start - diff;
+ var newEnd = this.end - diff;
+
+ this.setRange(newStart, newEnd);
};
/**
- * Get the frame of the root panel
+ * Prototype for visual components
+ * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} [body]
+ * @param {Object} [options]
*/
-RootPanel.prototype.getFrame = function getFrame() {
- return this.frame;
-};
+function Component (body, options) {
+ this.options = null;
+ this.props = null;
+}
/**
- * Repaint the root panel
+ * Set options for the component. The new options will be merged into the
+ * current options.
+ * @param {Object} options
*/
-RootPanel.prototype.repaint = function repaint() {
- // update class name
- var options = this.options;
- var editable = options.editable.updateTime || options.editable.updateGroup;
- var className = 'vis timeline rootpanel ' + options.orientation + (editable ? ' editable' : '');
- if (options.className) className += ' ' + util.option.asString(className);
- this.frame.className = className;
-
- // repaint the child components
- var childsResized = this._repaintChilds();
-
- // update frame size
- this.frame.style.maxHeight = util.option.asSize(this.options.maxHeight, '');
- this.frame.style.minHeight = util.option.asSize(this.options.minHeight, '');
- this._updateSize();
-
- // 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);
+Component.prototype.setOptions = function(options) {
+ if (options) {
+ util.extend(this.options, options);
}
};
/**
- * Initialize watching when option autoResize is true
- * @private
+ * Repaint the component
+ * @return {boolean} Returns true if the component is resized
*/
-RootPanel.prototype._initWatch = function _initWatch() {
- var autoResize = this.getOption('autoResize');
- if (autoResize) {
- this._watch();
- }
- else {
- this._unwatch();
- }
+Component.prototype.redraw = function() {
+ // should be implemented by the component
+ return false;
};
/**
- * Watch for changes in the size of the frame. On resize, the Panel will
- * automatically redraw itself.
- * @private
+ * Destroy the component. Cleanup DOM and event listeners
*/
-RootPanel.prototype._watch = function _watch() {
- var me = this;
-
- this._unwatch();
-
- var checkSize = function checkSize() {
- var autoResize = me.getOption('autoResize');
- if (!autoResize) {
- // stop watching when the option autoResize is changed to false
- me._unwatch();
- return;
- }
-
- if (me.frame) {
- // check whether the frame is resized
- 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?
- }
- }
- };
-
- // TODO: automatically cleanup the event listener when the frame is deleted
- util.addEventListener(window, 'resize', checkSize);
-
- this.watchTimer = setInterval(checkSize, 1000);
+Component.prototype.destroy = function() {
+ // should be implemented by the component
};
/**
- * Stop watching for a resize of the frame.
- * @private
+ * Test whether the component is resized since the last time _isResized() was
+ * called.
+ * @return {Boolean} Returns true if the component is resized
+ * @protected
*/
-RootPanel.prototype._unwatch = function _unwatch() {
- if (this.watchTimer) {
- clearInterval(this.watchTimer);
- this.watchTimer = undefined;
- }
+Component.prototype._isResized = function() {
+ var resized = (this.props._previousWidth !== this.props.width ||
+ this.props._previousHeight !== this.props.height);
- // TODO: remove event listener on window.resize
+ this.props._previousWidth = this.props.width;
+ this.props._previousHeight = this.props.height;
+
+ return resized;
};
/**
* A horizontal time axis
+ * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body
* @param {Object} [options] See TimeAxis.setOptions for the available
* options.
* @constructor TimeAxis
* @extends Component
*/
-function TimeAxis (options) {
- this.id = util.randomUUID();
-
+function TimeAxis (body, options) {
this.dom = {
+ foreground: null,
majorLines: [],
majorTexts: [],
minorLines: [],
@@ -4076,121 +6204,124 @@ function TimeAxis (options) {
lineTop: 0
};
- this.options = options || {};
this.defaultOptions = {
orientation: 'bottom', // supported: 'top', 'bottom'
// TODO: implement timeaxis orientations 'left' and 'right'
showMinorLabels: true,
showMajorLabels: true
};
+ this.options = util.extend({}, this.defaultOptions);
- this.range = null;
+ this.body = body;
// create the HTML DOM
this._create();
+
+ this.setOptions(options);
}
TimeAxis.prototype = new Component();
-// TODO: comment options
-TimeAxis.prototype.setOptions = Component.prototype.setOptions;
-
/**
- * Create the HTML DOM for the TimeAxis
+ * Set options for the TimeAxis.
+ * Parameters will be merged in current options.
+ * @param {Object} options Available options:
+ * {string} [orientation]
+ * {boolean} [showMinorLabels]
+ * {boolean} [showMajorLabels]
*/
-TimeAxis.prototype._create = function _create() {
- this.frame = document.createElement('div');
+TimeAxis.prototype.setOptions = function(options) {
+ if (options) {
+ // copy all options that we know
+ util.selectiveExtend(['orientation', 'showMinorLabels', 'showMajorLabels'], this.options, options);
+ }
};
/**
- * Set a range (start and end)
- * @param {Range | Object} range A Range or an object containing start and end.
+ * Create the HTML DOM for the TimeAxis
*/
-TimeAxis.prototype.setRange = function (range) {
- if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
- throw new TypeError('Range must be an instance of Range, ' +
- 'or an object containing start and end.');
- }
- this.range = range;
+TimeAxis.prototype._create = function() {
+ this.dom.foreground = document.createElement('div');
+ this.dom.background = document.createElement('div');
+
+ this.dom.foreground.className = 'timeaxis foreground';
+ this.dom.background.className = 'timeaxis background';
};
/**
- * Get the outer frame of the time axis
- * @return {HTMLElement} frame
+ * Destroy the TimeAxis
*/
-TimeAxis.prototype.getFrame = function getFrame() {
- return this.frame;
+TimeAxis.prototype.destroy = function() {
+ // remove from DOM
+ if (this.dom.foreground.parentNode) {
+ this.dom.foreground.parentNode.removeChild(this.dom.foreground);
+ }
+ if (this.dom.background.parentNode) {
+ this.dom.background.parentNode.removeChild(this.dom.background);
+ }
+
+ this.body = null;
};
/**
* Repaint the component
* @return {boolean} Returns true if the component is resized
*/
-TimeAxis.prototype.repaint = function () {
- var asSize = util.option.asSize,
- options = this.options,
+TimeAxis.prototype.redraw = function () {
+ var options = this.options,
props = this.props,
- frame = this.frame;
+ foreground = this.dom.foreground,
+ background = this.dom.background;
- // update classname
- frame.className = 'timeaxis'; // TODO: add className from options if defined
+ // determine the correct parent DOM element (depending on option orientation)
+ var parent = (options.orientation == 'top') ? this.body.dom.top : this.body.dom.bottom;
+ var parentChanged = (foreground.parentNode !== parent);
- var parent = frame.parentNode;
- if (parent) {
- // calculate character width and height
- this._calculateCharSize();
+ // calculate character width and height
+ this._calculateCharSize();
- // 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');
-
- // 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?
+ // TODO: recalculate sizes only needed when parent is resized or options is changed
+ var orientation = this.options.orientation,
+ showMinorLabels = this.options.showMinorLabels,
+ showMajorLabels = this.options.showMajorLabels;
- props.minorLineHeight = parentHeight + props.minorLabelHeight;
- props.minorLineWidth = 1; // TODO: really calculate width
- props.majorLineHeight = parentHeight + this.height;
- props.majorLineWidth = 1; // TODO: really calculate width
+ // determine the width and height of the elemens for the axis
+ props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
+ props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
+ props.height = props.minorLabelHeight + props.majorLabelHeight;
+ props.width = foreground.offsetWidth;
- // take frame offline while updating (is almost twice as fast)
- var beforeChild = frame.nextSibling;
- parent.removeChild(frame);
+ props.minorLineHeight = this.body.domProps.root.height - props.majorLabelHeight -
+ (options.orientation == 'top' ? this.body.domProps.bottom.height : this.body.domProps.top.height);
+ props.minorLineWidth = 1; // TODO: really calculate width
+ props.majorLineHeight = props.minorLineHeight + props.majorLabelHeight;
+ props.majorLineWidth = 1; // TODO: really calculate width
- // 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';
- }
+ // take foreground and background offline while updating (is almost twice as fast)
+ var foregroundNextSibling = foreground.nextSibling;
+ var backgroundNextSibling = background.nextSibling;
+ foreground.parentNode && foreground.parentNode.removeChild(foreground);
+ background.parentNode && background.parentNode.removeChild(background);
- this._repaintLabels();
+ foreground.style.height = this.props.height + 'px';
- this._repaintLine();
+ this._repaintLabels();
- // put frame online again
- if (beforeChild) {
- parent.insertBefore(frame, beforeChild);
- }
- else {
- parent.appendChild(frame)
- }
+ // put DOM online again (at the same place)
+ if (foregroundNextSibling) {
+ parent.insertBefore(foreground, foregroundNextSibling);
+ }
+ else {
+ parent.appendChild(foreground)
+ }
+ if (backgroundNextSibling) {
+ this.body.dom.backgroundVertical.insertBefore(background, backgroundNextSibling);
+ }
+ else {
+ this.body.dom.backgroundVertical.appendChild(background)
}
- return this._isResized();
+ return this._isResized() || parentChanged;
};
/**
@@ -4198,13 +6329,13 @@ TimeAxis.prototype.repaint = function () {
* @private
*/
TimeAxis.prototype._repaintLabels = function () {
- var orientation = this.getOption('orientation');
+ var orientation = this.options.orientation;
// calculate range and step (step such that we have space for 7 characters per label)
- var start = util.convert(this.range.start, 'Number'),
- end = util.convert(this.range.end, 'Number'),
- minimumStep = this.options.toTime((this.props.minorCharWidth || 10) * 7).valueOf()
- -this.options.toTime(0).valueOf();
+ var start = util.convert(this.body.range.start, 'Number'),
+ end = util.convert(this.body.range.end, 'Number'),
+ minimumStep = this.body.util.toTime((this.props.minorCharWidth || 10) * 7).valueOf()
+ -this.body.util.toTime(0).valueOf();
var step = new TimeStep(new Date(start), new Date(end), minimumStep);
this.step = step;
@@ -4227,16 +6358,16 @@ TimeAxis.prototype._repaintLabels = function () {
while (step.hasNext() && max < 1000) {
max++;
var cur = step.getCurrent(),
- x = this.options.toScreen(cur),
+ x = this.body.util.toScreen(cur),
isMajor = step.isMajor();
// TODO: lines must have a width, such that we can create css backgrounds
- if (this.getOption('showMinorLabels')) {
+ if (this.options.showMinorLabels) {
this._repaintMinorText(x, step.getLabelMinor(), orientation);
}
- if (isMajor && this.getOption('showMajorLabels')) {
+ if (isMajor && this.options.showMajorLabels) {
if (x > 0) {
if (xFirstMajorLabel == undefined) {
xFirstMajorLabel = x;
@@ -4253,8 +6384,8 @@ TimeAxis.prototype._repaintLabels = function () {
}
// create a major label on the left when needed
- if (this.getOption('showMajorLabels')) {
- var leftTime = this.options.toTime(0),
+ if (this.options.showMajorLabels) {
+ var leftTime = this.body.util.toTime(0),
leftText = step.getLabelMajor(leftTime),
widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation
@@ -4291,20 +6422,13 @@ TimeAxis.prototype._repaintMinorText = function (x, text, orientation) {
label = document.createElement('div');
label.appendChild(content);
label.className = 'text minor';
- this.frame.appendChild(label);
+ this.dom.foreground.appendChild(label);
}
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.top = (orientation == 'top') ? (this.props.majorLabelHeight + 'px') : '0';
label.style.left = x + 'px';
//label.title = title; // TODO: this is a heavy operation
};
@@ -4326,21 +6450,14 @@ TimeAxis.prototype._repaintMajorText = function (x, text, orientation) {
label = document.createElement('div');
label.className = 'text major';
label.appendChild(content);
- this.frame.appendChild(label);
+ this.dom.foreground.appendChild(label);
}
this.dom.majorTexts.push(label);
label.childNodes[0].nodeValue = text;
//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.top = (orientation == 'top') ? '0' : (this.props.minorLabelHeight + 'px');
label.style.left = x + 'px';
};
@@ -4358,18 +6475,16 @@ TimeAxis.prototype._repaintMinorLine = function (x, orientation) {
// create vertical line
line = document.createElement('div');
line.className = 'grid vertical minor';
- this.frame.appendChild(line);
+ this.dom.background.appendChild(line);
}
this.dom.minorLines.push(line);
var props = this.props;
if (orientation == 'top') {
- line.style.top = this.props.majorLabelHeight + 'px';
- line.style.bottom = '';
+ line.style.top = props.majorLabelHeight + 'px';
}
else {
- line.style.top = '';
- line.style.bottom = this.props.majorLabelHeight + 'px';
+ line.style.top = this.body.domProps.top.height + 'px';
}
line.style.height = props.minorLineHeight + 'px';
line.style.left = (x - props.minorLineWidth / 2) + 'px';
@@ -4389,72 +6504,28 @@ TimeAxis.prototype._repaintMajorLine = function (x, orientation) {
// create vertical line
line = document.createElement('DIV');
line.className = 'grid vertical major';
- this.frame.appendChild(line);
+ this.dom.background.appendChild(line);
}
this.dom.majorLines.push(line);
var props = this.props;
if (orientation == 'top') {
- line.style.top = '0px';
- line.style.bottom = '';
+ line.style.top = '0';
}
else {
- line.style.top = '';
- line.style.bottom = '0px';
+ line.style.top = this.body.domProps.top.height + 'px';
}
line.style.left = (x - props.majorLineWidth / 2) + 'px';
line.style.height = props.majorLineHeight + 'px';
};
-
-/**
- * Repaint the horizontal line for the axis
- * @private
- */
-TimeAxis.prototype._repaintLine = function() {
- var line = this.dom.line,
- frame = this.frame,
- orientation = this.getOption('orientation');
-
- // line before all axis elements
- if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
- if (line) {
- // put this line at the end of all childs
- frame.removeChild(line);
- frame.appendChild(line);
- }
- else {
- // create the axis line
- line = document.createElement('div');
- line.className = 'grid horizontal major';
- frame.appendChild(line);
- this.dom.line = line;
- }
-
- 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.parentNode) {
- line.parentNode.removeChild(line);
- delete this.dom.line;
- }
- }
-};
-
/**
* 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._calculateCharSize = function () {
- // Note: We calculate char size with every repaint. Size may change, for
+ // Note: We calculate char size with every redraw. Size may change, for
// example when any of the timelines parents had display:none for example.
// determine the char width and height on the minor axis
@@ -4464,7 +6535,7 @@ TimeAxis.prototype._calculateCharSize = function () {
this.dom.measureCharMinor.style.position = 'absolute';
this.dom.measureCharMinor.appendChild(document.createTextNode('0'));
- this.frame.appendChild(this.dom.measureCharMinor);
+ this.dom.foreground.appendChild(this.dom.measureCharMinor);
}
this.props.minorCharHeight = this.dom.measureCharMinor.clientHeight;
this.props.minorCharWidth = this.dom.measureCharMinor.clientWidth;
@@ -4476,7 +6547,7 @@ TimeAxis.prototype._calculateCharSize = function () {
this.dom.measureCharMajor.style.position = 'absolute';
this.dom.measureCharMajor.appendChild(document.createTextNode('0'));
- this.frame.appendChild(this.dom.measureCharMajor);
+ this.dom.foreground.appendChild(this.dom.measureCharMajor);
}
this.props.majorCharHeight = this.dom.measureCharMajor.clientHeight;
this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth;
@@ -4488,40 +6559,40 @@ TimeAxis.prototype._calculateCharSize = function () {
* @param {Date} date the date to be snapped.
* @return {Date} snappedDate
*/
-TimeAxis.prototype.snap = function snap (date) {
+TimeAxis.prototype.snap = function(date) {
return this.step.snap(date);
};
/**
* A current time bar
- * @param {Range} range
+ * @param {{range: Range, dom: Object, domProps: Object}} body
* @param {Object} [options] Available parameters:
* {Boolean} [showCurrentTime]
* @constructor CurrentTime
* @extends Component
*/
-function CurrentTime (range, options) {
- this.id = util.randomUUID();
+function CurrentTime (body, options) {
+ this.body = body;
- this.range = range;
- this.options = options || {};
+ // default options
this.defaultOptions = {
- showCurrentTime: false
+ showCurrentTime: true
};
+ this.options = util.extend({}, this.defaultOptions);
this._create();
+
+ this.setOptions(options);
}
CurrentTime.prototype = new Component();
-CurrentTime.prototype.setOptions = Component.prototype.setOptions;
-
/**
* Create the HTML DOM for the current time bar
* @private
*/
-CurrentTime.prototype._create = function _create () {
+CurrentTime.prototype._create = function() {
var bar = document.createElement('div');
bar.className = 'currenttime';
bar.style.position = 'absolute';
@@ -4532,25 +6603,57 @@ CurrentTime.prototype._create = function _create () {
};
/**
- * Get the frame element of the current time bar
- * @returns {HTMLElement} frame
+ * Destroy the CurrentTime bar
+ */
+CurrentTime.prototype.destroy = function () {
+ this.options.showCurrentTime = false;
+ this.redraw(); // will remove the bar from the DOM and stop refreshing
+
+ this.body = null;
+};
+
+/**
+ * Set options for the component. Options will be merged in current options.
+ * @param {Object} options Available parameters:
+ * {boolean} [showCurrentTime]
*/
-CurrentTime.prototype.getFrame = function getFrame() {
- return this.bar;
+CurrentTime.prototype.setOptions = function(options) {
+ if (options) {
+ // copy all options that we know
+ util.selectiveExtend(['showCurrentTime'], this.options, options);
+ }
};
/**
* Repaint the component
* @return {boolean} Returns true if the component is resized
*/
-CurrentTime.prototype.repaint = function repaint() {
- var parent = this.parent;
+CurrentTime.prototype.redraw = function() {
+ if (this.options.showCurrentTime) {
+ var parent = this.body.dom.backgroundVertical;
+ if (this.bar.parentNode != parent) {
+ // attach to the dom
+ if (this.bar.parentNode) {
+ this.bar.parentNode.removeChild(this.bar);
+ }
+ parent.appendChild(this.bar);
+
+ this.start();
+ }
- var now = new Date();
- var x = this.options.toScreen(now);
+ var now = new Date();
+ var x = this.body.util.toScreen(now);
- this.bar.style.left = x + 'px';
- this.bar.title = 'Current time: ' + now;
+ this.bar.style.left = x + 'px';
+ this.bar.title = 'Current time: ' + now;
+ }
+ else {
+ // remove the line from the DOM
+ if (this.bar.parentNode) {
+ this.bar.parentNode.removeChild(this.bar);
+ }
+ this.stop();
+ }
return false;
};
@@ -4558,19 +6661,19 @@ CurrentTime.prototype.repaint = function repaint() {
/**
* Start auto refreshing the current time bar
*/
-CurrentTime.prototype.start = function start() {
+CurrentTime.prototype.start = function() {
var me = this;
function update () {
me.stop();
// determine interval to refresh
- var scale = me.range.conversion(me.parent.width).scale;
+ var scale = me.body.range.conversion(me.body.domProps.center.width).scale;
var interval = 1 / scale / 10;
if (interval < 30) interval = 30;
if (interval > 1000) interval = 1000;
- me.repaint();
+ me.redraw();
// start a timer to adjust for the new time
me.currentTimeTimer = setTimeout(update, interval);
@@ -4582,7 +6685,7 @@ CurrentTime.prototype.start = function start() {
/**
* Stop auto refreshing the current time bar
*/
-CurrentTime.prototype.stop = function stop() {
+CurrentTime.prototype.stop = function() {
if (this.currentTimeTimer !== undefined) {
clearTimeout(this.currentTimeTimer);
delete this.currentTimeTimer;
@@ -4591,36 +6694,50 @@ CurrentTime.prototype.stop = function stop() {
/**
* A custom time bar
+ * @param {{range: Range, dom: Object}} body
* @param {Object} [options] Available parameters:
* {Boolean} [showCustomTime]
* @constructor CustomTime
* @extends Component
*/
-function CustomTime (options) {
- this.id = util.randomUUID();
+function CustomTime (body, options) {
+ this.body = body;
- this.options = options || {};
+ // default options
this.defaultOptions = {
showCustomTime: false
};
+ this.options = util.extend({}, this.defaultOptions);
this.customTime = new Date();
this.eventParams = {}; // stores state parameters while dragging the bar
// create the DOM
this._create();
+
+ this.setOptions(options);
}
CustomTime.prototype = new Component();
-CustomTime.prototype.setOptions = Component.prototype.setOptions;
+/**
+ * Set options for the component. Options will be merged in current options.
+ * @param {Object} options Available parameters:
+ * {boolean} [showCustomTime]
+ */
+CustomTime.prototype.setOptions = function(options) {
+ if (options) {
+ // copy all options that we know
+ util.selectiveExtend(['showCustomTime'], this.options, options);
+ }
+};
/**
* Create the DOM for the custom time
* @private
*/
-CustomTime.prototype._create = function _create () {
+CustomTime.prototype._create = function() {
var bar = document.createElement('div');
bar.className = 'customtime';
bar.style.position = 'absolute';
@@ -4646,22 +6763,44 @@ CustomTime.prototype._create = function _create () {
};
/**
- * Get the frame element of the custom time bar
- * @returns {HTMLElement} frame
+ * Destroy the CustomTime bar
*/
-CustomTime.prototype.getFrame = function getFrame() {
- return this.bar;
+CustomTime.prototype.destroy = function () {
+ this.options.showCustomTime = false;
+ this.redraw(); // will remove the bar from the DOM
+
+ this.hammer.enable(false);
+ this.hammer = null;
+
+ this.body = null;
};
/**
* Repaint the component
* @return {boolean} Returns true if the component is resized
*/
-CustomTime.prototype.repaint = function () {
- var x = this.options.toScreen(this.customTime);
+CustomTime.prototype.redraw = function () {
+ if (this.options.showCustomTime) {
+ var parent = this.body.dom.backgroundVertical;
+ if (this.bar.parentNode != parent) {
+ // attach to the dom
+ if (this.bar.parentNode) {
+ this.bar.parentNode.removeChild(this.bar);
+ }
+ parent.appendChild(this.bar);
+ }
+
+ var x = this.body.util.toScreen(this.customTime);
- this.bar.style.left = x + 'px';
- this.bar.title = 'Time: ' + this.customTime;
+ this.bar.style.left = x + 'px';
+ this.bar.title = 'Time: ' + this.customTime;
+ }
+ else {
+ // remove the line from the DOM
+ if (this.bar.parentNode) {
+ this.bar.parentNode.removeChild(this.bar);
+ }
+ }
return false;
};
@@ -4672,7 +6811,7 @@ CustomTime.prototype.repaint = function () {
*/
CustomTime.prototype.setCustomTime = function(time) {
this.customTime = new Date(time.valueOf());
- this.repaint();
+ this.redraw();
};
/**
@@ -4705,13 +6844,13 @@ CustomTime.prototype._onDrag = function (event) {
if (!this.eventParams.dragging) return;
var deltaX = event.gesture.deltaX,
- x = this.options.toScreen(this.eventParams.customTime) + deltaX,
- time = this.options.toTime(x);
+ x = this.body.util.toScreen(this.eventParams.customTime) + deltaX,
+ time = this.body.util.toTime(x);
this.setCustomTime(time);
// fire a timechange event
- this.emit('timechange', {
+ this.body.emitter.emit('timechange', {
time: new Date(this.customTime.valueOf())
});
@@ -4728,7 +6867,7 @@ CustomTime.prototype._onDragEnd = function (event) {
if (!this.eventParams.dragging) return;
// fire a timechanged event
- this.emit('timechanged', {
+ this.body.emitter.emit('timechanged', {
time: new Date(this.customTime.valueOf())
});
@@ -4742,55 +6881,92 @@ var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
* 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 {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 {Panel} sidePanel Left side panel holding labels
+ * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body
* @param {Object} [options] See ItemSet.setOptions for the available options.
* @constructor ItemSet
- * @extends Panel
+ * @extends Component
*/
-function ItemSet(backgroundPanel, axisPanel, sidePanel, options) {
- this.id = util.randomUUID();
+function ItemSet(body, options) {
+ this.body = body;
- // one options object is shared by this itemset and all its items
- this.options = options || {};
- this.backgroundPanel = backgroundPanel;
- this.axisPanel = axisPanel;
- this.sidePanel = sidePanel;
- this.itemOptions = Object.create(this.options);
+ this.defaultOptions = {
+ type: null, // 'box', 'point', 'range'
+ orientation: 'bottom', // 'top' or 'bottom'
+ align: 'center', // alignment of box items
+ stack: true,
+ groupOrder: null,
+
+ selectable: true,
+ editable: {
+ updateTime: false,
+ updateGroup: false,
+ add: false,
+ remove: false
+ },
+
+ onAdd: function (item, callback) {
+ callback(item);
+ },
+ onUpdate: function (item, callback) {
+ callback(item);
+ },
+ onMove: function (item, callback) {
+ callback(item);
+ },
+ onRemove: function (item, callback) {
+ callback(item);
+ },
+
+ margin: {
+ item: 10,
+ axis: 20
+ },
+ padding: 5
+ };
+
+ // options is shared by this ItemSet and all its items
+ this.options = util.extend({}, this.defaultOptions);
+
+ // options for getting items from the DataSet with the correct type
+ this.itemOptions = {
+ type: {start: 'Date', end: 'Date'}
+ };
+
+ this.conversion = {
+ toScreen: body.util.toScreen,
+ toTime: body.util.toTime
+ };
this.dom = {};
+ this.props = {};
this.hammer = null;
var me = this;
this.itemsData = null; // DataSet
this.groupsData = null; // DataSet
- this.range = null; // Range or Object {start: number, end: number}
// listeners for the DataSet of the items
this.itemListeners = {
'add': function (event, params, senderId) {
- if (senderId != me.id) me._onAdd(params.items);
+ me._onAdd(params.items);
},
'update': function (event, params, senderId) {
- if (senderId != me.id) me._onUpdate(params.items);
+ me._onUpdate(params.items);
},
'remove': function (event, params, senderId) {
- if (senderId != me.id) me._onRemove(params.items);
+ me._onRemove(params.items);
}
};
// listeners for the DataSet of the groups
this.groupListeners = {
'add': function (event, params, senderId) {
- if (senderId != me.id) me._onAddGroups(params.items);
+ me._onAddGroups(params.items);
},
'update': function (event, params, senderId) {
- if (senderId != me.id) me._onUpdateGroups(params.items);
+ me._onUpdateGroups(params.items);
},
'remove': function (event, params, senderId) {
- if (senderId != me.id) me._onRemoveGroups(params.items);
+ me._onRemoveGroups(params.items);
}
};
@@ -4799,36 +6975,38 @@ function ItemSet(backgroundPanel, axisPanel, sidePanel, options) {
this.groupIds = [];
this.selection = []; // list with the ids of all selected nodes
- this.stackDirty = true; // if true, all items will be restacked on next repaint
+ this.stackDirty = true; // if true, all items will be restacked on next redraw
this.touchParams = {}; // stores properties while dragging
// create the HTML DOM
this._create();
+
+ this.setOptions(options);
}
-ItemSet.prototype = new Panel();
+ItemSet.prototype = new Component();
// available item types will be registered here
ItemSet.types = {
box: ItemBox,
range: ItemRange,
- rangeoverflow: ItemRangeOverflow,
point: ItemPoint
};
/**
* Create the HTML DOM for the ItemSet
*/
-ItemSet.prototype._create = function _create(){
+ItemSet.prototype._create = function(){
var frame = document.createElement('div');
+ frame.className = 'itemset';
frame['timeline-itemset'] = this;
- this.frame = frame;
+ this.dom.frame = frame;
// create background panel
var background = document.createElement('div');
background.className = 'background';
- this.backgroundPanel.frame.appendChild(background);
+ frame.appendChild(background);
this.dom.background = background;
// create foreground panel
@@ -4841,33 +7019,46 @@ ItemSet.prototype._create = function _create(){
var axis = document.createElement('div');
axis.className = 'axis';
this.dom.axis = axis;
- this.axisPanel.frame.appendChild(axis);
// create labelset
var labelSet = document.createElement('div');
labelSet.className = 'labelset';
this.dom.labelSet = labelSet;
- this.sidePanel.frame.appendChild(labelSet);
// create ungrouped Group
this._updateUngrouped();
// attach event listeners
- // TODO: use event listeners from the rootpanel to improve performance?
- this.hammer = Hammer(frame, {
+ // Note: we bind to the centerContainer for the case where the height
+ // of the center container is larger than of the ItemSet, so we
+ // can click in the empty area to create a new item or deselect an item.
+ this.hammer = Hammer(this.body.dom.centerContainer, {
prevent_default: true
});
+
+ // drag items when selected
+ this.hammer.on('touch', this._onTouch.bind(this));
this.hammer.on('dragstart', this._onDragStart.bind(this));
this.hammer.on('drag', this._onDrag.bind(this));
this.hammer.on('dragend', this._onDragEnd.bind(this));
+
+ // single select (or unselect) when tapping an item
+ this.hammer.on('tap', this._onSelectItem.bind(this));
+
+ // multi select when holding mouse/touch, or on ctrl+click
+ this.hammer.on('hold', this._onMultiSelectItem.bind(this));
+
+ // add item on doubletap
+ this.hammer.on('doubletap', this._onAddItem.bind(this));
+
+ // attach to the DOM
+ this.show();
};
/**
* Set options for the ItemSet. Existing options will be extended/overwritten.
* @param {Object} [options] The following options are available:
- * {String | function} [className]
- * class name for the itemset
- * {String} [type]
+ * {String} type
* Default type for the items. Choose from 'box'
* (default), 'point', or 'range'. The default
* Style can be overwritten by individual items.
@@ -4878,44 +7069,129 @@ ItemSet.prototype._create = function _create(){
* {String} orientation
* Orientation of the item set. Choose 'top' or
* 'bottom' (default).
+ * {Function} groupOrder
+ * A sorting function for ordering groups
+ * {Boolean} stack
+ * If true (deafult), items will be stacked on
+ * top of each other.
* {Number} margin.axis
* Margin between the axis and the items in pixels.
* Default is 20.
* {Number} margin.item
* Margin between items in pixels. Default is 10.
+ * {Number} margin
+ * Set margin for both axis and items in pixels.
* {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 = function setOptions(options) {
- Component.prototype.setOptions.call(this, options);
+ * {Boolean} selectable
+ * If true (default), items can be selected.
+ * {Boolean} editable
+ * Set all editable options to true or false
+ * {Boolean} editable.updateTime
+ * Allow dragging an item to an other moment in time
+ * {Boolean} editable.updateGroup
+ * Allow dragging an item to an other group
+ * {Boolean} editable.add
+ * Allow creating new items on double tap
+ * {Boolean} editable.remove
+ * Allow removing items by clicking the delete button
+ * top right of a selected item.
+ * {Function(item: Item, callback: Function)} onAdd
+ * Callback function triggered when an item is about to be added:
+ * when the user double taps an empty space in the Timeline.
+ * {Function(item: Item, callback: Function)} onUpdate
+ * Callback function 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.
+ * {Function(item: Item, callback: Function)} onMove
+ * Fired when an item has been moved. If not implemented,
+ * the move action will be accepted.
+ * {Function(item: Item, callback: Function)} onRemove
+ * Fired when an item is about to be deleted.
+ * If not implemented, the item will be always removed.
+ */
+ItemSet.prototype.setOptions = function(options) {
+ if (options) {
+ // copy all options that we know
+ var fields = ['type', 'align', 'orientation', 'padding', 'stack', 'selectable', 'groupOrder'];
+ util.selectiveExtend(fields, this.options, options);
+
+ if ('margin' in options) {
+ if (typeof options.margin === 'number') {
+ this.options.margin.axis = options.margin;
+ this.options.margin.item = options.margin;
+ }
+ else if (typeof options.margin === 'object'){
+ util.selectiveExtend(['axis', 'item'], this.options.margin, options.margin);
+ }
+ }
+
+ if ('editable' in options) {
+ if (typeof options.editable === 'boolean') {
+ this.options.editable.updateTime = options.editable;
+ this.options.editable.updateGroup = options.editable;
+ this.options.editable.add = options.editable;
+ this.options.editable.remove = options.editable;
+ }
+ else if (typeof options.editable === 'object') {
+ util.selectiveExtend(['updateTime', 'updateGroup', 'add', 'remove'], this.options.editable, options.editable);
+ }
+ }
+
+ // callback functions
+ var addCallback = (function (name) {
+ if (name in options) {
+ var fn = options[name];
+ if (!(fn instanceof Function) || fn.length != 2) {
+ throw new Error('option ' + name + ' must be a function ' + name + '(item, callback)');
+ }
+ this.options[name] = fn;
+ }
+ }).bind(this);
+ ['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(addCallback);
+
+ // force the itemSet to refresh: options like orientation and margins may be changed
+ this.markDirty();
+ }
};
/**
- * Mark the ItemSet dirty so it will refresh everything with next repaint
+ * Mark the ItemSet dirty so it will refresh everything with next redraw
*/
-ItemSet.prototype.markDirty = function markDirty() {
+ItemSet.prototype.markDirty = function() {
this.groupIds = [];
this.stackDirty = true;
};
+/**
+ * Destroy the ItemSet
+ */
+ItemSet.prototype.destroy = function() {
+ this.hide();
+ this.setItems(null);
+ this.setGroups(null);
+
+ this.hammer = null;
+
+ this.body = null;
+ this.conversion = null;
+};
+
/**
* Hide the component from the DOM
*/
-ItemSet.prototype.hide = function hide() {
+ItemSet.prototype.hide = function() {
+ // remove the frame containing the items
+ if (this.dom.frame.parentNode) {
+ this.dom.frame.parentNode.removeChild(this.dom.frame);
+ }
+
// 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);
- }
-
// remove the labelset containing all group labels
if (this.dom.labelSet.parentNode) {
this.dom.labelSet.parentNode.removeChild(this.dom.labelSet);
@@ -4926,33 +7202,21 @@ ItemSet.prototype.hide = function hide() {
* 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);
+ItemSet.prototype.show = function() {
+ // show frame containing the items
+ if (!this.dom.frame.parentNode) {
+ this.body.dom.center.appendChild(this.dom.frame);
}
- // show background with vertical lines
- if (!this.dom.background.parentNode) {
- this.backgroundPanel.frame.appendChild(this.dom.background);
+ // show axis with dots
+ if (!this.dom.axis.parentNode) {
+ this.body.dom.backgroundVertical.appendChild(this.dom.axis);
}
// show labelset containing labels
if (!this.dom.labelSet.parentNode) {
- this.sidePanel.frame.appendChild(this.dom.labelSet);
- }
-};
-
-/**
- * Set range (start and end).
- * @param {Range | Object} range A Range or an object containing start and end.
- */
-ItemSet.prototype.setRange = function setRange(range) {
- if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
- throw new TypeError('Range must be an instance of Range, ' +
- 'or an object containing start and end.');
+ this.body.dom.left.appendChild(this.dom.labelSet);
}
- this.range = range;
};
/**
@@ -4962,7 +7226,7 @@ ItemSet.prototype.setRange = function setRange(range) {
* selected. If ids is an empty array, all items will be
* unselected.
*/
-ItemSet.prototype.setSelection = function setSelection(ids) {
+ItemSet.prototype.setSelection = function(ids) {
var i, ii, id, item;
if (ids) {
@@ -4994,7 +7258,7 @@ ItemSet.prototype.setSelection = function setSelection(ids) {
* Get the selected items by their id
* @return {Array} ids The ids of the selected items
*/
-ItemSet.prototype.getSelection = function getSelection() {
+ItemSet.prototype.getSelection = function() {
return this.selection.concat([]);
};
@@ -5003,7 +7267,7 @@ ItemSet.prototype.getSelection = function getSelection() {
* @param {String | Number} id
* @private
*/
-ItemSet.prototype._deselect = function _deselect(id) {
+ItemSet.prototype._deselect = function(id) {
var selection = this.selection;
for (var i = 0, ii = selection.length; i < ii; i++) {
if (selection[i] == id) { // non-strict comparison!
@@ -5013,51 +7277,35 @@ 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} Returns true if the component is resized
*/
-ItemSet.prototype.repaint = function repaint() {
+ItemSet.prototype.redraw = function() {
var margin = this.options.margin,
- range = this.range,
+ range = this.body.range,
asSize = util.option.asSize,
- asString = util.option.asString,
options = this.options,
- orientation = this.getOption('orientation'),
+ orientation = options.orientation,
resized = false,
- frame = this.frame;
-
- // TODO: document this feature to specify one margin for both item and axis distance
- if (typeof margin === 'number') {
- margin = {
- item: margin,
- axis: margin
- };
- }
+ frame = this.dom.frame,
+ editable = options.editable.updateTime || options.editable.updateGroup;
- // update className
- frame.className = 'itemset' + (options.className ? (' ' + asString(options.className)) : '');
+ // update class name
+ frame.className = 'itemset' + (editable ? ' editable' : '');
// reorder the groups (if needed)
resized = this._orderGroups() || resized;
// check whether zoomed (in that case we need to re-stack everything)
// TODO: would be nicer to get this as a trigger from Range
- var visibleInterval = this.range.end - this.range.start;
- var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
+ var visibleInterval = range.end - range.start;
+ var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.props.width != this.props.lastWidth);
if (zoomed) this.stackDirty = true;
this.lastVisibleInterval = visibleInterval;
- this.lastWidth = this.width;
+ this.props.lastWidth = this.props.width;
- // repaint all groups
+ // redraw all groups
var restack = this.stackDirty,
firstGroup = this._firstGroup(),
firstMargin = {
@@ -5072,34 +7320,27 @@ ItemSet.prototype.repaint = function repaint() {
minHeight = margin.axis + margin.item;
util.forEach(this.groups, function (group) {
var groupMargin = (group == firstGroup) ? firstMargin : nonFirstMargin;
- resized = group.repaint(range, groupMargin, restack) || resized;
+ var groupResized = group.redraw(range, groupMargin, restack);
+ resized = groupResized || resized;
height += group.height;
});
height = Math.max(height, minHeight);
this.stackDirty = false;
- // 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%');
+ // update frame height
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;
+ this.props.top = frame.offsetTop;
+ this.props.left = frame.offsetLeft;
+ this.props.width = frame.offsetWidth;
+ this.props.height = height;
// 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');
+ this.dom.axis.style.top = asSize((orientation == 'top') ?
+ (this.body.domProps.top.height + this.body.domProps.border.top) :
+ (this.body.domProps.top.height + this.body.domProps.centerContainer.height));
+ this.dom.axis.style.left = this.body.domProps.border.left + 'px';
// check if this component is resized
resized = this._isResized() || resized;
@@ -5112,7 +7353,7 @@ ItemSet.prototype.repaint = function repaint() {
* @return {Group | null} firstGroup
* @private
*/
-ItemSet.prototype._firstGroup = function _firstGroup() {
+ItemSet.prototype._firstGroup = function() {
var firstGroupIndex = (this.options.orientation == 'top') ? 0 : (this.groupIds.length - 1);
var firstGroupId = this.groupIds[firstGroupIndex];
var firstGroup = this.groups[firstGroupId] || this.groups[UNGROUPED];
@@ -5125,7 +7366,7 @@ ItemSet.prototype._firstGroup = function _firstGroup() {
* there are no groups specified.
* @protected
*/
-ItemSet.prototype._updateUngrouped = function _updateUngrouped() {
+ItemSet.prototype._updateUngrouped = function() {
var ungrouped = this.groups[UNGROUPED];
if (this.groupsData) {
@@ -5154,35 +7395,11 @@ ItemSet.prototype._updateUngrouped = function _updateUngrouped() {
}
};
-/**
- * Get the foreground container element
- * @return {HTMLElement} foreground
- */
-ItemSet.prototype.getForeground = function getForeground() {
- return this.dom.foreground;
-};
-
-/**
- * Get the background container element
- * @return {HTMLElement} background
- */
-ItemSet.prototype.getBackground = function getBackground() {
- return this.dom.background;
-};
-
-/**
- * Get the axis container element
- * @return {HTMLElement} axis
- */
-ItemSet.prototype.getAxis = function getAxis() {
- return this.dom.axis;
-};
-
/**
* Get the element for the labelset
* @return {HTMLElement} labelSet
*/
-ItemSet.prototype.getLabelSet = function getLabelSet() {
+ItemSet.prototype.getLabelSet = function() {
return this.dom.labelSet;
};
@@ -5190,7 +7407,7 @@ ItemSet.prototype.getLabelSet = function getLabelSet() {
* Set items
* @param {vis.DataSet | null} items
*/
-ItemSet.prototype.setItems = function setItems(items) {
+ItemSet.prototype.setItems = function(items) {
var me = this,
ids,
oldItemsData = this.itemsData;
@@ -5209,7 +7426,7 @@ ItemSet.prototype.setItems = function setItems(items) {
if (oldItemsData) {
// unsubscribe from old dataset
util.forEach(this.itemListeners, function (callback, event) {
- oldItemsData.unsubscribe(event, callback);
+ oldItemsData.off(event, callback);
});
// remove all drawn items
@@ -5237,7 +7454,7 @@ ItemSet.prototype.setItems = function setItems(items) {
* Get the current items
* @returns {vis.DataSet | null}
*/
-ItemSet.prototype.getItems = function getItems() {
+ItemSet.prototype.getItems = function() {
return this.itemsData;
};
@@ -5245,7 +7462,7 @@ ItemSet.prototype.getItems = function getItems() {
* Set groups
* @param {vis.DataSet} groups
*/
-ItemSet.prototype.setGroups = function setGroups(groups) {
+ItemSet.prototype.setGroups = function(groups) {
var me = this,
ids;
@@ -5258,7 +7475,7 @@ ItemSet.prototype.setGroups = function setGroups(groups) {
// remove all drawn groups
ids = this.groupsData.getIds();
this.groupsData = null;
- this._onRemoveGroups(ids); // note: this will cause a repaint
+ this._onRemoveGroups(ids); // note: this will cause a redraw
}
// replace the dataset
@@ -5290,14 +7507,14 @@ ItemSet.prototype.setGroups = function setGroups(groups) {
// update the order of all items in each group
this._order();
- this.emit('change');
+ this.body.emitter.emit('change');
};
/**
* Get the current groups
* @returns {vis.DataSet | null} groups
*/
-ItemSet.prototype.getGroups = function getGroups() {
+ItemSet.prototype.getGroups = function() {
return this.groupsData;
};
@@ -5305,9 +7522,9 @@ ItemSet.prototype.getGroups = function getGroups() {
* Remove an item by its id
* @param {String | Number} id
*/
-ItemSet.prototype.removeItem = function removeItem (id) {
+ItemSet.prototype.removeItem = function(id) {
var item = this.itemsData.get(id),
- dataset = this._myDataSet();
+ dataset = this.itemsData.getDataSet();
if (item) {
// confirm deletion
@@ -5326,18 +7543,13 @@ ItemSet.prototype.removeItem = function removeItem (id) {
* @param {Number[]} ids
* @protected
*/
-ItemSet.prototype._onUpdate = function _onUpdate(ids) {
- var me = this,
- items = this.items,
- itemOptions = this.itemOptions;
+ItemSet.prototype._onUpdate = function(ids) {
+ var me = this;
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 itemData = me.itemsData.get(id, me.itemOptions),
+ item = me.items[id],
+ type = itemData.type || me.options.type || (itemData.end ? 'range' : 'box');
var constructor = ItemSet.types[type];
@@ -5356,10 +7568,15 @@ ItemSet.prototype._onUpdate = function _onUpdate(ids) {
if (!item) {
// create item
if (constructor) {
- item = new constructor(itemData, me.options, itemOptions);
+ item = new constructor(itemData, me.conversion, me.options);
item.id = id; // TODO: not so nice setting id afterwards
me._addItem(item);
}
+ else if (type == 'rangeoverflow') {
+ // TODO: deprecated since version 2.1.0 (or 3.0.0?). cleanup some day
+ throw new TypeError('Item type "rangeoverflow" is deprecated. Use css styling instead: ' +
+ '.vis.timeline .item.range .content {overflow: visible;}');
+ }
else {
throw new TypeError('Unknown item type "' + type + '"');
}
@@ -5367,8 +7584,8 @@ ItemSet.prototype._onUpdate = function _onUpdate(ids) {
});
this._order();
- this.stackDirty = true; // force re-stacking of all items next repaint
- this.emit('change');
+ this.stackDirty = true; // force re-stacking of all items next redraw
+ this.body.emitter.emit('change');
};
/**
@@ -5383,7 +7600,7 @@ ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate;
* @param {Number[]} ids
* @protected
*/
-ItemSet.prototype._onRemove = function _onRemove(ids) {
+ItemSet.prototype._onRemove = function(ids) {
var count = 0;
var me = this;
ids.forEach(function (id) {
@@ -5397,8 +7614,8 @@ ItemSet.prototype._onRemove = function _onRemove(ids) {
if (count) {
// update order
this._order();
- this.stackDirty = true; // force re-stacking of all items next repaint
- this.emit('change');
+ this.stackDirty = true; // force re-stacking of all items next redraw
+ this.body.emitter.emit('change');
}
};
@@ -5406,7 +7623,7 @@ ItemSet.prototype._onRemove = function _onRemove(ids) {
* Update the order of item in all groups
* @private
*/
-ItemSet.prototype._order = function _order() {
+ItemSet.prototype._order = function() {
// reorder the items in all groups
// TODO: optimization: only reorder groups affected by the changed items
util.forEach(this.groups, function (group) {
@@ -5419,7 +7636,7 @@ ItemSet.prototype._order = function _order() {
* @param {Number[]} ids
* @private
*/
-ItemSet.prototype._onUpdateGroups = function _onUpdateGroups(ids) {
+ItemSet.prototype._onUpdateGroups = function(ids) {
this._onAddGroups(ids);
};
@@ -5428,7 +7645,7 @@ ItemSet.prototype._onUpdateGroups = function _onUpdateGroups(ids) {
* @param {Number[]} ids
* @private
*/
-ItemSet.prototype._onAddGroups = function _onAddGroups(ids) {
+ItemSet.prototype._onAddGroups = function(ids) {
var me = this;
ids.forEach(function (id) {
@@ -5468,7 +7685,7 @@ ItemSet.prototype._onAddGroups = function _onAddGroups(ids) {
}
});
- this.emit('change');
+ this.body.emitter.emit('change');
};
/**
@@ -5476,7 +7693,7 @@ ItemSet.prototype._onAddGroups = function _onAddGroups(ids) {
* @param {Number[]} ids
* @private
*/
-ItemSet.prototype._onRemoveGroups = function _onRemoveGroups(ids) {
+ItemSet.prototype._onRemoveGroups = function(ids) {
var groups = this.groups;
ids.forEach(function (id) {
var group = groups[id];
@@ -5489,7 +7706,7 @@ ItemSet.prototype._onRemoveGroups = function _onRemoveGroups(ids) {
this.markDirty();
- this.emit('change');
+ this.body.emitter.emit('change');
};
/**
@@ -5532,7 +7749,7 @@ ItemSet.prototype._orderGroups = function () {
* @param {Item} item
* @private
*/
-ItemSet.prototype._addItem = function _addItem(item) {
+ItemSet.prototype._addItem = function(item) {
this.items[item.id] = item;
// add to group
@@ -5547,12 +7764,12 @@ ItemSet.prototype._addItem = function _addItem(item) {
* @param {Object} itemData
* @private
*/
-ItemSet.prototype._updateItem = function _updateItem(item, itemData) {
+ItemSet.prototype._updateItem = function(item, itemData) {
var oldGroupId = item.data.group;
item.data = itemData;
if (item.displayed) {
- item.repaint();
+ item.redraw();
}
// update group
@@ -5572,7 +7789,7 @@ ItemSet.prototype._updateItem = function _updateItem(item, itemData) {
* @param {Item} item
* @private
*/
-ItemSet.prototype._removeItem = function _removeItem(item) {
+ItemSet.prototype._removeItem = function(item) {
// remove from DOM
item.hide();
@@ -5595,7 +7812,7 @@ ItemSet.prototype._removeItem = function _removeItem(item) {
* @returns {Array}
* @private
*/
-ItemSet.prototype._constructByEndArray = function _constructByEndArray(array) {
+ItemSet.prototype._constructByEndArray = function(array) {
var endArray = [];
for (var i = 0; i < array.length; i++) {
@@ -5607,25 +7824,17 @@ ItemSet.prototype._constructByEndArray = function _constructByEndArray(array) {
};
/**
- * Get the width of the group labels
- * @return {Number} width
- */
-ItemSet.prototype.getLabelsWidth = function getLabelsWidth() {
- var width = 0;
-
- util.forEach(this.groups, function (group) {
- width = Math.max(width, group.getLabelWidth());
- });
-
- return width;
-};
-
-/**
- * Get the height of the itemsets background
- * @return {Number} height
+ * Register the clicked item on touch, before dragStart is initiated.
+ *
+ * dragStart is initiated from a mousemove event, which can have left the item
+ * already resulting in an item == null
+ *
+ * @param {Event} event
+ * @private
*/
-ItemSet.prototype.getBackgroundHeight = function getBackgroundHeight() {
- return this.height;
+ItemSet.prototype._onTouch = function (event) {
+ // store the touched item, used in _onDragStart
+ this.touchParams.item = ItemSet.itemFromTarget(event);
};
/**
@@ -5638,7 +7847,7 @@ ItemSet.prototype._onDragStart = function (event) {
return;
}
- var item = ItemSet.itemFromTarget(event),
+ var item = this.touchParams.item || null,
me = this,
props;
@@ -5704,9 +7913,10 @@ ItemSet.prototype._onDragStart = function (event) {
*/
ItemSet.prototype._onDrag = function (event) {
if (this.touchParams.itemProps) {
- var snap = this.options.snap || null,
+ var range = this.body.range,
+ snap = this.body.util.snap || null,
deltaX = event.gesture.deltaX,
- scale = (this.width / (this.range.end - this.range.start)),
+ scale = (this.props.width / (range.end - range.start)),
offset = deltaX / scale;
// move
@@ -5738,8 +7948,8 @@ ItemSet.prototype._onDrag = function (event) {
// TODO: implement onMoving handler
- this.stackDirty = true; // force re-stacking of all items next repaint
- this.emit('change');
+ this.stackDirty = true; // force re-stacking of all items next redraw
+ this.body.emitter.emit('change');
event.stopPropagation();
}
@@ -5755,20 +7965,22 @@ ItemSet.prototype._onDragEnd = function (event) {
// prepare a change set for the changed items
var changes = [],
me = this,
- dataset = this._myDataSet();
+ dataset = this.itemsData.getDataSet();
this.touchParams.itemProps.forEach(function (props) {
var id = props.item.id,
- itemData = me.itemsData.get(id);
+ itemData = me.itemsData.get(id, me.itemOptions);
var changed = false;
if ('start' in props.item.data) {
changed = (props.start != props.item.data.start.valueOf());
- itemData.start = util.convert(props.item.data.start, dataset.convert['start']);
+ itemData.start = util.convert(props.item.data.start,
+ dataset._options.type && dataset._options.type.start || 'Date');
}
if ('end' in props.item.data) {
changed = changed || (props.end != props.item.data.end.valueOf());
- itemData.end = util.convert(props.item.data.end, dataset.convert['end']);
+ itemData.end = util.convert(props.item.data.end,
+ dataset._options.type && dataset._options.type.end || 'Date');
}
if ('group' in props.item.data) {
changed = changed || (props.group != props.item.data.group);
@@ -5780,7 +7992,7 @@ ItemSet.prototype._onDragEnd = function (event) {
me.options.onMove(itemData, function (itemData) {
if (itemData) {
// apply changes
- itemData[dataset.fieldId] = id; // ensure the item contains its id (can be undefined)
+ itemData[dataset._fieldId] = id; // ensure the item contains its id (can be undefined)
changes.push(itemData);
}
else {
@@ -5788,8 +8000,8 @@ ItemSet.prototype._onDragEnd = function (event) {
if ('start' in props) props.item.data.start = props.start;
if ('end' in props) props.item.data.end = props.end;
- me.stackDirty = true; // force re-stacking of all items next repaint
- me.emit('change');
+ me.stackDirty = true; // force re-stacking of all items next redraw
+ me.body.emitter.emit('change');
}
});
}
@@ -5805,13 +8017,137 @@ ItemSet.prototype._onDragEnd = function (event) {
}
};
+/**
+ * Handle selecting/deselecting an item when tapping it
+ * @param {Event} event
+ * @private
+ */
+ItemSet.prototype._onSelectItem = function (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);
+
+ var newSelection = this.getSelection();
+
+ // emit a select event,
+ // except when old selection is empty and new selection is still empty
+ if (newSelection.length > 0 || oldSelection.length > 0) {
+ this.body.emitter.emit('select', {
+ items: this.getSelection()
+ });
+ }
+
+ event.stopPropagation();
+};
+
+/**
+ * Handle creation and updates of an item on double tap
+ * @param event
+ * @private
+ */
+ItemSet.prototype._onAddItem = function (event) {
+ if (!this.options.selectable) return;
+ if (!this.options.editable.add) return;
+
+ var me = this,
+ snap = this.body.util.snap || null,
+ item = ItemSet.itemFromTarget(event);
+
+ if (item) {
+ // update item
+
+ // execute async handler to update the item (or cancel it)
+ var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset
+ this.options.onUpdate(itemData, function (itemData) {
+ if (itemData) {
+ me.itemsData.update(itemData);
+ }
+ });
+ }
+ else {
+ // add item
+ var xAbs = vis.util.getAbsoluteLeft(this.dom.frame);
+ var x = event.gesture.center.pageX - xAbs;
+ var start = this.body.util.toTime(x);
+ var newItem = {
+ start: snap ? snap(start) : start,
+ content: 'new item'
+ };
+
+ // when default type is a range, add a default end date to the new item
+ if (this.options.type === 'range') {
+ var end = this.body.util.toTime(x + this.props.width / 5);
+ newItem.end = snap ? snap(end) : end;
+ }
+
+ newItem[this.itemsData.fieldId] = util.randomUUID();
+
+ var group = ItemSet.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 redraw?
+ }
+ });
+ }
+};
+
+/**
+ * Handle selecting/deselecting multiple items when holding an item
+ * @param {Event} event
+ * @private
+ */
+ItemSet.prototype._onMultiSelectItem = function (event) {
+ if (!this.options.selectable) return;
+
+ var selection,
+ item = ItemSet.itemFromTarget(event);
+
+ if (item) {
+ // multi select items
+ selection = this.getSelection(); // current selection
+ var index = selection.indexOf(item.id);
+ if (index == -1) {
+ // item is not yet selected -> select it
+ selection.push(item.id);
+ }
+ else {
+ // item is already selected -> deselect it
+ selection.splice(index, 1);
+ }
+ this.setSelection(selection);
+
+ this.body.emitter.emit('select', {
+ items: this.getSelection()
+ });
+
+ event.stopPropagation();
+ }
+};
+
/**
* Find an item from an event target:
* searches for the attribute 'timeline-item' in the event target's element tree
* @param {Event} event
* @return {Item | null} item
*/
-ItemSet.itemFromTarget = function itemFromTarget (event) {
+ItemSet.itemFromTarget = function(event) {
var target = event.target;
while (target) {
if (target.hasOwnProperty('timeline-item')) {
@@ -5829,7 +8165,7 @@ ItemSet.itemFromTarget = function itemFromTarget (event) {
* @param {Event} event
* @return {Group | null} group
*/
-ItemSet.groupFromTarget = function groupFromTarget (event) {
+ItemSet.groupFromTarget = function(event) {
var target = event.target;
while (target) {
if (target.hasOwnProperty('timeline-group')) {
@@ -5847,7 +8183,7 @@ ItemSet.groupFromTarget = function groupFromTarget (event) {
* @param {Event} event
* @return {ItemSet | null} item
*/
-ItemSet.itemSetFromTarget = function itemSetFromTarget (event) {
+ItemSet.itemSetFromTarget = function(event) {
var target = event.target;
while (target) {
if (target.hasOwnProperty('timeline-itemset')) {
@@ -5856,255 +8192,663 @@ ItemSet.itemSetFromTarget = function itemSetFromTarget (event) {
target = target.parentNode;
}
- return null;
+ return null;
+};
+
+
+/**
+ * @constructor Item
+ * @param {Object} data Object containing (optional) parameters type,
+ * start, end, content, group, className.
+ * @param {{toScreen: function, toTime: function}} conversion
+ * Conversion functions from time to screen and vice versa
+ * @param {Object} options Configuration options
+ * // TODO: describe available options
+ */
+function Item (data, conversion, options) {
+ this.id = null;
+ this.parent = null;
+ this.data = data;
+ this.dom = null;
+ this.conversion = conversion || {};
+ this.options = options || {};
+
+ this.selected = false;
+ this.displayed = false;
+ this.dirty = true;
+
+ this.top = null;
+ this.left = null;
+ this.width = null;
+ this.height = null;
+}
+
+/**
+ * Select current item
+ */
+Item.prototype.select = function() {
+ this.selected = true;
+ if (this.displayed) this.redraw();
+};
+
+/**
+ * Unselect current item
+ */
+Item.prototype.unselect = function() {
+ this.selected = false;
+ if (this.displayed) this.redraw();
+};
+
+/**
+ * Set a parent for the item
+ * @param {ItemSet | Group} parent
+ */
+Item.prototype.setParent = function(parent) {
+ if (this.displayed) {
+ this.hide();
+ this.parent = parent;
+ if (this.parent) {
+ this.show();
+ }
+ }
+ else {
+ this.parent = parent;
+ }
+};
+
+/**
+ * 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
+ */
+Item.prototype.isVisible = function(range) {
+ // Should be implemented by Item implementations
+ return false;
+};
+
+/**
+ * Show the Item in the DOM (when not already visible)
+ * @return {Boolean} changed
+ */
+Item.prototype.show = function() {
+ return false;
+};
+
+/**
+ * Hide the Item from the DOM (when visible)
+ * @return {Boolean} changed
+ */
+Item.prototype.hide = function() {
+ return false;
+};
+
+/**
+ * Repaint the item
+ */
+Item.prototype.redraw = function() {
+ // should be implemented by the item
+};
+
+/**
+ * Reposition the Item horizontally
+ */
+Item.prototype.repositionX = function() {
+ // should be implemented by the item
+};
+
+/**
+ * Reposition the Item vertically
+ */
+Item.prototype.repositionY = function() {
+ // 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
+ * @protected
+ */
+Item.prototype._repaintDeleteButton = function (anchor) {
+ if (this.selected && this.options.editable.remove && !this.dom.deleteButton) {
+ // create and show button
+ var me = this;
+
+ var deleteButton = document.createElement('div');
+ deleteButton.className = 'delete';
+ deleteButton.title = 'Delete this item';
+
+ Hammer(deleteButton, {
+ preventDefault: true
+ }).on('tap', function (event) {
+ me.parent.removeFromDataSet(me);
+ 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
+ * @extends Item
+ * @param {Object} data Object containing parameters start
+ * content, className.
+ * @param {{toScreen: function, toTime: function}} conversion
+ * Conversion functions from time to screen and vice versa
+ * @param {Object} [options] Configuration options
+ * // TODO: describe available options
+ */
+function ItemBox (data, conversion, options) {
+ this.props = {
+ dot: {
+ width: 0,
+ height: 0
+ },
+ line: {
+ width: 0,
+ height: 0
+ }
+ };
+
+ // validate data
+ if (data) {
+ if (data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + data);
+ }
+ }
+
+ Item.call(this, data, conversion, options);
+}
+
+ItemBox.prototype = new Item (null, 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(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
+ */
+ItemBox.prototype.redraw = function() {
+ var dom = this.dom;
+ if (!dom) {
+ // create DOM
+ this.dom = {};
+ dom = this.dom;
+
+ // 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;
+ }
+
+ // append DOM to parent DOM
+ if (!this.parent) {
+ throw new Error('Cannot redraw item: no parent attached');
+ }
+ if (!dom.box.parentNode) {
+ var foreground = this.parent.dom.foreground;
+ if (!foreground) throw new Error('Cannot redraw time axis: parent has no foreground container element');
+ foreground.appendChild(dom.box);
+ }
+ if (!dom.line.parentNode) {
+ var background = this.parent.dom.background;
+ if (!background) throw new Error('Cannot redraw time axis: parent has no background container element');
+ background.appendChild(dom.line);
+ }
+ if (!dom.dot.parentNode) {
+ var axis = this.parent.dom.axis;
+ if (!background) throw new Error('Cannot redraw time axis: parent has no axis container element');
+ axis.appendChild(dom.dot);
+ }
+ this.displayed = 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);
+ }
+ else if (this.data.content != undefined) {
+ dom.content.innerHTML = this.content;
+ }
+ else {
+ throw new Error('Property "content" missing in item ' + this.data.id);
+ }
+
+ this.dirty = true;
+ }
+
+ // update title
+ if (this.data.title != this.title) {
+ dom.box.title = this.data.title;
+ this.title = this.data.title;
+ }
+
+ // 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;
+ }
+
+ // 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);
};
/**
- * Find the DataSet to which this ItemSet is connected
- * @returns {null | DataSet} dataset
- * @private
+ * Show the item in the DOM (when not already displayed). The items DOM will
+ * be created when needed.
*/
-ItemSet.prototype._myDataSet = function _myDataSet() {
- // find the root DataSet
- var dataset = this.itemsData;
- while (dataset instanceof DataView) {
- dataset = dataset.data;
+ItemBox.prototype.show = function() {
+ if (!this.displayed) {
+ this.redraw();
}
- return dataset;
};
+
/**
- * @constructor Item
- * @param {Object} data Object containing (optional) parameters type,
- * start, end, content, group, className.
- * @param {Object} [options] Options to set initial property values
- * @param {Object} [defaultOptions] default options
- * // TODO: describe available options
+ * Hide the item from the DOM (when visible)
*/
-function Item (data, options, defaultOptions) {
- this.id = null;
- this.parent = null;
- this.data = data;
- this.dom = null;
- this.options = options || {};
- this.defaultOptions = defaultOptions || {};
+ItemBox.prototype.hide = function() {
+ if (this.displayed) {
+ var dom = this.dom;
- this.selected = false;
- this.displayed = false;
- this.dirty = true;
+ 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.width = null;
- this.height = null;
-}
+ this.top = null;
+ this.left = null;
-/**
- * Select current item
- */
-Item.prototype.select = function select() {
- this.selected = true;
- if (this.displayed) this.repaint();
+ this.displayed = false;
+ }
};
/**
- * Unselect current item
+ * Reposition the item horizontally
+ * @Override
*/
-Item.prototype.unselect = function unselect() {
- this.selected = false;
- if (this.displayed) this.repaint();
-};
+ItemBox.prototype.repositionX = function() {
+ var start = this.conversion.toScreen(this.data.start),
+ align = this.options.align,
+ left,
+ box = this.dom.box,
+ line = this.dom.line,
+ dot = this.dom.dot;
-/**
- * Set a parent for the item
- * @param {ItemSet | Group} parent
- */
-Item.prototype.setParent = function setParent(parent) {
- if (this.displayed) {
- this.hide();
- this.parent = parent;
- if (this.parent) {
- this.show();
- }
+ // calculate left position of the box
+ if (align == 'right') {
+ this.left = start - this.width;
+ }
+ else if (align == 'left') {
+ this.left = start;
}
else {
- this.parent = parent;
+ // default or 'center'
+ this.left = start - this.width / 2;
}
+
+ // reposition box
+ box.style.left = this.left + 'px';
+
+ // reposition line
+ line.style.left = (start - this.props.line.width / 2) + 'px';
+
+ // reposition dot
+ dot.style.left = (start - this.props.dot.width / 2) + 'px';
};
/**
- * 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
+ * Reposition the item vertically
+ * @Override
*/
-Item.prototype.isVisible = function isVisible (range) {
- // Should be implemented by Item implementations
- return false;
+ItemBox.prototype.repositionY = function() {
+ var orientation = this.options.orientation,
+ box = this.dom.box,
+ line = this.dom.line,
+ dot = this.dom.dot;
+
+ if (orientation == 'top') {
+ box.style.top = (this.top || 0) + 'px';
+
+ line.style.top = '0';
+ line.style.height = (this.parent.top + this.top + 1) + 'px';
+ line.style.bottom = '';
+ }
+ else { // orientation 'bottom'
+ var itemSetHeight = this.parent.itemSet.props.height; // TODO: this is nasty
+ var lineHeight = itemSetHeight - this.parent.top - this.parent.height + this.top;
+
+ box.style.top = (this.parent.height - this.top - this.height || 0) + 'px';
+ line.style.top = (itemSetHeight - lineHeight) + 'px';
+ line.style.bottom = '0';
+ }
+
+ dot.style.top = (-this.props.dot.height / 2) + 'px';
};
/**
- * Show the Item in the DOM (when not already visible)
- * @return {Boolean} changed
+ * @constructor ItemPoint
+ * @extends Item
+ * @param {Object} data Object containing parameters start
+ * content, className.
+ * @param {{toScreen: function, toTime: function}} conversion
+ * Conversion functions from time to screen and vice versa
+ * @param {Object} [options] Configuration options
+ * // TODO: describe available options
*/
-Item.prototype.show = function show() {
- return false;
-};
+function ItemPoint (data, conversion, options) {
+ this.props = {
+ dot: {
+ top: 0,
+ width: 0,
+ height: 0
+ },
+ content: {
+ height: 0,
+ marginLeft: 0
+ }
+ };
+
+ // validate data
+ if (data) {
+ if (data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + data);
+ }
+ }
+
+ Item.call(this, data, conversion, options);
+}
+
+ItemPoint.prototype = new Item (null, null, null);
/**
- * Hide the Item from the DOM (when visible)
- * @return {Boolean} changed
+ * 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
*/
-Item.prototype.hide = function hide() {
- return false;
+ItemPoint.prototype.isVisible = function(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
*/
-Item.prototype.repaint = function repaint() {
- // should be implemented by the item
+ItemPoint.prototype.redraw = function() {
+ var dom = this.dom;
+ if (!dom) {
+ // create DOM
+ this.dom = {};
+ dom = this.dom;
+
+ // background box
+ dom.point = document.createElement('div');
+ // className is updated in redraw()
+
+ // 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.point.appendChild(dom.dot);
+
+ // attach this item as attribute
+ dom.point['timeline-item'] = this;
+ }
+
+ // append DOM to parent DOM
+ if (!this.parent) {
+ throw new Error('Cannot redraw item: no parent attached');
+ }
+ if (!dom.point.parentNode) {
+ var foreground = this.parent.dom.foreground;
+ if (!foreground) {
+ throw new Error('Cannot redraw time axis: parent has no foreground container element');
+ }
+ foreground.appendChild(dom.point);
+ }
+ this.displayed = 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);
+ }
+ else if (this.data.content != undefined) {
+ dom.content.innerHTML = this.content;
+ }
+ else {
+ throw new Error('Property "content" missing in item ' + this.data.id);
+ }
+
+ this.dirty = true;
+ }
+
+ // update title
+ if (this.data.title != this.title) {
+ dom.point.title = this.data.title;
+ this.title = this.data.title;
+ }
+
+ // 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;
+ dom.dot.className = 'item dot' + 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 = 2 * 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';
+ dom.dot.style.left = (this.props.dot.width / 2) + 'px';
+
+ this.dirty = false;
+ }
+
+ this._repaintDeleteButton(dom.point);
};
/**
- * Reposition the Item horizontally
+ * Show the item in the DOM (when not already visible). The items DOM will
+ * be created when needed.
*/
-Item.prototype.repositionX = function repositionX() {
- // should be implemented by the item
+ItemPoint.prototype.show = function() {
+ if (!this.displayed) {
+ this.redraw();
+ }
};
/**
- * Reposition the Item vertically
+ * Hide the item from the DOM (when visible)
*/
-Item.prototype.repositionY = function repositionY() {
- // should be implemented by the item
+ItemPoint.prototype.hide = function() {
+ 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;
+ }
};
/**
- * Repaint a delete button on the top right of the item when the item is selected
- * @param {HTMLElement} anchor
- * @protected
+ * Reposition the item horizontally
+ * @Override
*/
-Item.prototype._repaintDeleteButton = function (anchor) {
- if (this.selected && this.options.editable.remove && !this.dom.deleteButton) {
- // create and show button
- var me = this;
+ItemPoint.prototype.repositionX = function() {
+ var start = this.conversion.toScreen(this.data.start);
- var deleteButton = document.createElement('div');
- deleteButton.className = 'delete';
- deleteButton.title = 'Delete this item';
+ this.left = start - this.props.dot.width;
- Hammer(deleteButton, {
- preventDefault: true
- }).on('tap', function (event) {
- me.parent.removeFromDataSet(me);
- event.stopPropagation();
- });
+ // reposition point
+ this.dom.point.style.left = this.left + 'px';
+};
- anchor.appendChild(deleteButton);
- this.dom.deleteButton = deleteButton;
+/**
+ * Reposition the item vertically
+ * @Override
+ */
+ItemPoint.prototype.repositionY = function() {
+ var orientation = this.options.orientation,
+ point = this.dom.point;
+
+ if (orientation == 'top') {
+ point.style.top = this.top + 'px';
}
- 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;
+ else {
+ point.style.top = (this.parent.height - this.top - this.height) + 'px';
}
};
/**
- * @constructor ItemBox
+ * @constructor ItemRange
* @extends Item
- * @param {Object} data Object containing parameters start
+ * @param {Object} data Object containing parameters start, end
* content, className.
- * @param {Object} [options] Options to set initial property values
- * @param {Object} [defaultOptions] default options
- * // TODO: describe available options
+ * @param {{toScreen: function, toTime: function}} conversion
+ * Conversion functions from time to screen and vice versa
+ * @param {Object} [options] Configuration options
+ * // TODO: describe options
*/
-function ItemBox (data, options, defaultOptions) {
+function ItemRange (data, conversion, options) {
this.props = {
- dot: {
- width: 0,
- height: 0
- },
- line: {
- width: 0,
- height: 0
+ content: {
+ width: 0
}
};
+ this.overflow = false; // if contents can overflow (css styling), this flag is set to true
// validate data
if (data) {
if (data.start == undefined) {
- throw new Error('Property "start" missing in item ' + data);
+ 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, data, options, defaultOptions);
+ Item.call(this, data, conversion, options);
}
-ItemBox.prototype = new Item (null);
+ItemRange.prototype = new Item (null, 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
*/
-ItemBox.prototype.isVisible = function isVisible (range) {
+ItemRange.prototype.isVisible = function(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);
+ return (this.data.start < range.end) && (this.data.end > range.start);
};
/**
* Repaint the item
*/
-ItemBox.prototype.repaint = function repaint() {
+ItemRange.prototype.redraw = function() {
var dom = this.dom;
if (!dom) {
// create DOM
this.dom = {};
dom = this.dom;
- // create main box
- dom.box = document.createElement('DIV');
+ // background box
+ dom.box = document.createElement('div');
+ // className is updated in redraw()
- // contents box (inside the background box). used for making margins
- dom.content = document.createElement('DIV');
+ // contents box
+ 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;
}
// append DOM to parent DOM
if (!this.parent) {
- throw new Error('Cannot repaint item: no parent attached');
+ throw new Error('Cannot redraw 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');
+ var foreground = this.parent.dom.foreground;
+ if (!foreground) {
+ throw new Error('Cannot redraw 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;
// update contents
@@ -6124,1385 +8868,1569 @@ ItemBox.prototype.repaint = function repaint() {
this.dirty = true;
}
+ // update title
+ if (this.data.title != this.title) {
+ dom.box.title = this.data.title;
+ this.title = this.data.title;
+ }
+
// update class
- var className = (this.data.className? ' ' + this.data.className : '') +
+ 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;
+ dom.box.className = this.baseClassName + className;
this.dirty = true;
}
// 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;
+ // determine from css whether this box has overflow
+ this.overflow = window.getComputedStyle(dom.content).overflow !== 'hidden';
+
+ this.props.content.width = this.dom.content.offsetWidth;
+ this.height = this.dom.box.offsetHeight;
this.dirty = false;
}
- this._repaintDeleteButton(dom.box);
-};
+ 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.
+ */
+ItemRange.prototype.show = function() {
+ if (!this.displayed) {
+ this.redraw();
+ }
+};
+
+/**
+ * Hide the item from the DOM (when visible)
+ * @return {Boolean} changed
+ */
+ItemRange.prototype.hide = function() {
+ if (this.displayed) {
+ var box = this.dom.box;
+
+ if (box.parentNode) {
+ box.parentNode.removeChild(box);
+ }
+
+ this.top = null;
+ this.left = null;
+
+ this.displayed = false;
+ }
+};
+
+/**
+ * Reposition the item horizontally
+ * @Override
+ */
+// TODO: delete the old function
+ItemRange.prototype.repositionX = function() {
+ var props = this.props,
+ parentWidth = this.parent.width,
+ start = this.conversion.toScreen(this.data.start),
+ end = this.conversion.toScreen(this.data.end),
+ padding = this.options.padding,
+ contentLeft;
+
+ // 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;
+ }
+ var boxWidth = Math.max(end - start, 1);
+
+ if (this.overflow) {
+ // when range exceeds left of the window, position the contents at the left of the visible area
+ contentLeft = Math.max(-start, 0);
+
+ this.left = start;
+ this.width = boxWidth + this.props.content.width;
+ // Note: The calculation of width is an optimistic calculation, giving
+ // a width which will not change when moving the Timeline
+ // So no restacking needed, which is nicer for the eye;
+ }
+ else { // no overflow
+ // 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;
+ }
+
+ this.left = start;
+ this.width = boxWidth;
+ }
+
+ this.dom.box.style.left = this.left + 'px';
+ this.dom.box.style.width = boxWidth + 'px';
+ this.dom.content.style.left = contentLeft + 'px';
+};
+
+/**
+ * Reposition the item vertically
+ * @Override
+ */
+ItemRange.prototype.repositionY = function() {
+ var orientation = this.options.orientation,
+ box = this.dom.box;
+
+ if (orientation == 'top') {
+ box.style.top = this.top + 'px';
+ }
+ else {
+ box.style.top = (this.parent.height - this.top - this.height) + 'px';
+ }
+};
+
+/**
+ * Repaint a drag area on the left side of the range when the range is selected
+ * @protected
+ */
+ItemRange.prototype._repaintDragLeft = function () {
+ if (this.selected && this.options.editable.updateTime && !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')
+ });
+
+ 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;
+ }
+};
+
+/**
+ * Repaint a drag area on the right side of the range when the range is selected
+ * @protected
+ */
+ItemRange.prototype._repaintDragRight = function () {
+ if (this.selected && this.options.editable.updateTime && !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')
+ });
+
+ 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;
+ }
+};
+
+/**
+ * @constructor Group
+ * @param {Number | String} groupId
+ * @param {Object} data
+ * @param {ItemSet} itemSet
+ */
+function Group (groupId, data, itemSet) {
+ this.groupId = groupId;
+
+ this.itemSet = itemSet;
+
+ this.dom = {};
+ this.props = {
+ label: {
+ width: 0,
+ height: 0
+ }
+ };
+ this.className = null;
+
+ this.items = {}; // items filtered by groupId of this group
+ this.visibleItems = []; // items currently visible in window
+ this.orderedItems = { // items sorted by start and by end
+ byStart: [],
+ byEnd: []
+ };
+
+ this._create();
+
+ this.setData(data);
+}
/**
- * Show the item in the DOM (when not already displayed). The items DOM will
- * be created when needed.
+ * Create DOM elements for the group
+ * @private
*/
-ItemBox.prototype.show = function show() {
- if (!this.displayed) {
- this.repaint();
- }
-};
+Group.prototype._create = function() {
+ var label = document.createElement('div');
+ label.className = 'vlabel';
+ this.dom.label = label;
-/**
- * Hide the item from the DOM (when visible)
- */
-ItemBox.prototype.hide = function hide() {
- if (this.displayed) {
- var dom = this.dom;
+ var inner = document.createElement('div');
+ inner.className = 'inner';
+ label.appendChild(inner);
+ this.dom.inner = inner;
- 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);
+ var foreground = document.createElement('div');
+ foreground.className = 'group';
+ foreground['timeline-group'] = this;
+ this.dom.foreground = foreground;
- this.top = null;
- this.left = null;
+ this.dom.background = document.createElement('div');
+ this.dom.background.className = 'group';
- this.displayed = false;
- }
+ this.dom.axis = document.createElement('div');
+ this.dom.axis.className = 'group';
+
+ // create a hidden marker to detect when the Timelines container is attached
+ // to the DOM, or the style of a parent of the Timeline is changed from
+ // display:none is changed to visible.
+ this.dom.marker = document.createElement('div');
+ this.dom.marker.style.visibility = 'hidden';
+ this.dom.marker.innerHTML = '?';
+ this.dom.background.appendChild(this.dom.marker);
};
/**
- * Reposition the item horizontally
- * @Override
+ * Set the group data for this group
+ * @param {Object} data Group data, can contain properties content and className
*/
-ItemBox.prototype.repositionX = function repositionX() {
- var start = this.defaultOptions.toScreen(this.data.start),
- align = this.options.align || this.defaultOptions.align,
- left,
- box = this.dom.box,
- line = this.dom.line,
- dot = this.dom.dot;
-
- // calculate left position of the box
- if (align == 'right') {
- this.left = start - this.width;
+Group.prototype.setData = function(data) {
+ // update contents
+ var content = data && data.content;
+ if (content instanceof Element) {
+ this.dom.inner.appendChild(content);
}
- else if (align == 'left') {
- this.left = start;
+ else if (content != undefined) {
+ this.dom.inner.innerHTML = content;
}
else {
- // default or 'center'
- this.left = start - this.width / 2;
+ this.dom.inner.innerHTML = this.groupId;
}
- // reposition box
- box.style.left = this.left + 'px';
-
- // reposition line
- line.style.left = (start - this.props.line.width / 2) + 'px';
-
- // reposition dot
- dot.style.left = (start - this.props.dot.width / 2) + 'px';
-};
-
-/**
- * Reposition the item vertically
- * @Override
- */
-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;
-
- if (orientation == 'top') {
- box.style.top = (this.top || 0) + 'px';
- box.style.bottom = '';
+ // update title
+ this.dom.label.title = data && data.title || '';
- line.style.top = '0';
- line.style.bottom = '';
- line.style.height = (this.parent.top + this.top + 1) + 'px';
+ if (!this.dom.inner.firstChild) {
+ util.addClassName(this.dom.inner, 'hidden');
}
- else { // orientation 'bottom'
- box.style.top = '';
- box.style.bottom = (this.top || 0) + 'px';
-
- line.style.top = (this.parent.top + this.parent.height - this.top - 1) + 'px';
- line.style.bottom = '0';
- line.style.height = '';
+ else {
+ util.removeClassName(this.dom.inner, 'hidden');
}
- dot.style.top = (-this.props.dot.height / 2) + 'px';
-};
-
-/**
- * @constructor ItemPoint
- * @extends Item
- * @param {Object} data Object containing parameters start
- * content, className.
- * @param {Object} [options] Options to set initial property values
- * @param {Object} [defaultOptions] default options
- * // TODO: describe available options
- */
-function ItemPoint (data, options, defaultOptions) {
- this.props = {
- dot: {
- top: 0,
- width: 0,
- height: 0
- },
- content: {
- height: 0,
- marginLeft: 0
- }
- };
-
- // validate data
- if (data) {
- if (data.start == undefined) {
- throw new Error('Property "start" missing in item ' + data);
+ // update className
+ var className = data && data.className || null;
+ if (className != this.className) {
+ if (this.className) {
+ util.removeClassName(this.dom.label, className);
+ util.removeClassName(this.dom.foreground, className);
+ util.removeClassName(this.dom.background, className);
+ util.removeClassName(this.dom.axis, className);
}
+ util.addClassName(this.dom.label, className);
+ util.addClassName(this.dom.foreground, className);
+ util.addClassName(this.dom.background, className);
+ util.addClassName(this.dom.axis, className);
}
-
- Item.call(this, data, options, defaultOptions);
-}
-
-ItemPoint.prototype = new Item (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
+ * Get the width of the group label
+ * @return {number} width
*/
-ItemPoint.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);
+Group.prototype.getLabelWidth = function() {
+ return this.props.label.width;
};
+
/**
- * Repaint the item
+ * Repaint this group
+ * @param {{start: number, end: number}} range
+ * @param {{item: number, axis: number}} margin
+ * @param {boolean} [restack=false] Force restacking of all items
+ * @return {boolean} Returns true if the group is resized
*/
-ItemPoint.prototype.repaint = function repaint() {
- var dom = this.dom;
- if (!dom) {
- // create DOM
- this.dom = {};
- dom = this.dom;
+Group.prototype.redraw = function(range, margin, restack) {
+ var resized = false;
- // background box
- dom.point = document.createElement('div');
- // className is updated in repaint()
+ this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range);
- // contents box, right from the dot
- dom.content = document.createElement('div');
- dom.content.className = 'content';
- dom.point.appendChild(dom.content);
+ // force recalculation of the height of the items when the marker height changed
+ // (due to the Timeline being attached to the DOM or changed from display:none to visible)
+ var markerHeight = this.dom.marker.clientHeight;
+ if (markerHeight != this.lastMarkerHeight) {
+ this.lastMarkerHeight = markerHeight;
- // dot at start
- dom.dot = document.createElement('div');
- dom.point.appendChild(dom.dot);
+ util.forEach(this.items, function (item) {
+ item.dirty = true;
+ if (item.displayed) item.redraw();
+ });
- // attach this item as attribute
- dom.point['timeline-item'] = this;
+ restack = true;
}
- // append DOM to parent DOM
- if (!this.parent) {
- throw new Error('Cannot repaint item: no parent attached');
+ // reposition visible items vertically
+ if (this.itemSet.options.stack) { // TODO: ugly way to access options...
+ stack.stack(this.visibleItems, margin, restack);
}
- if (!dom.point.parentNode) {
- var foreground = this.parent.getForeground();
- if (!foreground) {
- throw new Error('Cannot repaint time axis: parent has no foreground container element');
- }
- foreground.appendChild(dom.point);
+ else { // no stacking
+ stack.nostack(this.visibleItems, margin);
}
- this.displayed = 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);
- }
- else if (this.data.content != undefined) {
- dom.content.innerHTML = this.content;
- }
- else {
- throw new Error('Property "content" missing in item ' + this.data.id);
+ // recalculate the height of the group
+ var height;
+ 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));
+ });
+ if (min > margin.axis) {
+ // there is an empty gap between the lowest item and the axis
+ var offset = min - margin.axis;
+ max -= offset;
+ util.forEach(visibleItems, function (item) {
+ item.top -= offset;
+ });
}
-
- this.dirty = true;
+ height = max + margin.item / 2;
+ }
+ else {
+ height = margin.axis + margin.item;
}
+ height = Math.max(height, this.props.label.height);
- // 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;
- dom.dot.className = 'item dot' + className;
+ // calculate actual size and position
+ var foreground = this.dom.foreground;
+ this.top = foreground.offsetTop;
+ this.left = foreground.offsetLeft;
+ this.width = foreground.offsetWidth;
+ resized = util.updateProperty(this, 'height', height) || resized;
- this.dirty = true;
+ // recalculate size of 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;
+
+ // apply new height
+ this.dom.background.style.height = height + 'px';
+ this.dom.foreground.style.height = height + 'px';
+ this.dom.label.style.height = height + 'px';
+
+ // update vertical position of items after they are re-stacked and the height of the group is calculated
+ for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
+ var item = this.visibleItems[i];
+ item.repositionY();
}
- // 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;
+ return resized;
+};
- // resize contents
- dom.content.style.marginLeft = 2 * this.props.dot.width + 'px';
- //dom.content.style.marginRight = ... + 'px'; // TODO: margin right
+/**
+ * Show this group: attach to the DOM
+ */
+Group.prototype.show = function() {
+ if (!this.dom.label.parentNode) {
+ this.itemSet.dom.labelSet.appendChild(this.dom.label);
+ }
- dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px';
- dom.dot.style.left = (this.props.dot.width / 2) + 'px';
+ if (!this.dom.foreground.parentNode) {
+ this.itemSet.dom.foreground.appendChild(this.dom.foreground);
+ }
- this.dirty = false;
+ if (!this.dom.background.parentNode) {
+ this.itemSet.dom.background.appendChild(this.dom.background);
}
- this._repaintDeleteButton(dom.point);
+ if (!this.dom.axis.parentNode) {
+ this.itemSet.dom.axis.appendChild(this.dom.axis);
+ }
};
/**
- * Show the item in the DOM (when not already visible). The items DOM will
- * be created when needed.
+ * Hide this group: remove from the DOM
*/
-ItemPoint.prototype.show = function show() {
- if (!this.displayed) {
- this.repaint();
+Group.prototype.hide = function() {
+ var label = this.dom.label;
+ if (label.parentNode) {
+ label.parentNode.removeChild(label);
+ }
+
+ var foreground = this.dom.foreground;
+ if (foreground.parentNode) {
+ foreground.parentNode.removeChild(foreground);
+ }
+
+ var background = this.dom.background;
+ if (background.parentNode) {
+ background.parentNode.removeChild(background);
+ }
+
+ var axis = this.dom.axis;
+ if (axis.parentNode) {
+ axis.parentNode.removeChild(axis);
}
};
/**
- * Hide the item from the DOM (when visible)
+ * Add an item to the group
+ * @param {Item} item
*/
-ItemPoint.prototype.hide = function hide() {
- if (this.displayed) {
- if (this.dom.point.parentNode) {
- this.dom.point.parentNode.removeChild(this.dom.point);
- }
-
- this.top = null;
- this.left = null;
+Group.prototype.add = function(item) {
+ this.items[item.id] = item;
+ item.setParent(this);
- this.displayed = false;
+ if (item instanceof ItemRange && this.visibleItems.indexOf(item) == -1) {
+ var range = this.itemSet.body.range; // TODO: not nice accessing the range like this
+ this._checkIfVisible(item, this.visibleItems, range);
}
};
/**
- * Reposition the item horizontally
- * @Override
+ * Remove an item from the group
+ * @param {Item} item
*/
-ItemPoint.prototype.repositionX = function repositionX() {
- var start = this.defaultOptions.toScreen(this.data.start);
+Group.prototype.remove = function(item) {
+ delete this.items[item.id];
+ item.setParent(this.itemSet);
- this.left = start - this.props.dot.width;
+ // remove from visible items
+ var index = this.visibleItems.indexOf(item);
+ if (index != -1) this.visibleItems.splice(index, 1);
- // reposition point
- this.dom.point.style.left = this.left + 'px';
+ // TODO: also remove from ordered items?
};
/**
- * Reposition the item vertically
- * @Override
+ * Remove an item from the corresponding DataSet
+ * @param {Item} item
*/
-ItemPoint.prototype.repositionY = function repositionY () {
- var orientation = this.options.orientation || this.defaultOptions.orientation,
- point = this.dom.point;
-
- if (orientation == 'top') {
- point.style.top = this.top + 'px';
- point.style.bottom = '';
- }
- else {
- point.style.top = '';
- point.style.bottom = this.top + 'px';
- }
+Group.prototype.removeFromDataSet = function(item) {
+ this.itemSet.removeItem(item.id);
};
/**
- * @constructor ItemRange
- * @extends Item
- * @param {Object} data Object containing parameters start, end
- * content, className.
- * @param {Object} [options] Options to set initial property values
- * @param {Object} [defaultOptions] default options
- * // TODO: describe available options
+ * Reorder the items
*/
-function ItemRange (data, options, defaultOptions) {
- this.props = {
- content: {
- 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, data, options, defaultOptions);
-}
-
-ItemRange.prototype = new Item (null);
+Group.prototype.order = function() {
+ var array = util.toArray(this.items);
+ this.orderedItems.byStart = array;
+ this.orderedItems.byEnd = this._constructByEndArray(array);
-ItemRange.prototype.baseClassName = 'item range';
+ stack.orderByStart(this.orderedItems.byStart);
+ stack.orderByEnd(this.orderedItems.byEnd);
+};
/**
- * 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
+ * Create an array containing all items being a range (having an end date)
+ * @param {Item[]} array
+ * @returns {ItemRange[]}
+ * @private
*/
-ItemRange.prototype.isVisible = function isVisible (range) {
- // determine visibility
- return (this.data.start < range.end) && (this.data.end > range.start);
+Group.prototype._constructByEndArray = function(array) {
+ var endArray = [];
+
+ for (var i = 0; i < array.length; i++) {
+ if (array[i] instanceof ItemRange) {
+ endArray.push(array[i]);
+ }
+ }
+ return endArray;
};
/**
- * Repaint the item
+ * Update the visible items
+ * @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date
+ * @param {Item[]} visibleItems The previously visible items.
+ * @param {{start: number, end: number}} range Visible range
+ * @return {Item[]} visibleItems The new visible items.
+ * @private
*/
-ItemRange.prototype.repaint = function repaint() {
- var dom = this.dom;
- if (!dom) {
- // create DOM
- this.dom = {};
- dom = this.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);
+Group.prototype._updateVisibleItems = function(orderedItems, visibleItems, range) {
+ var initialPosByStart,
+ newVisibleItems = [],
+ i;
- // attach this item as attribute
- dom.box['timeline-item'] = this;
+ // first check if the items that were in view previously are still in view.
+ // this handles the case for the ItemRange that is both before and after the current one.
+ if (visibleItems.length > 0) {
+ for (i = 0; i < visibleItems.length; i++) {
+ this._checkIfVisible(visibleItems[i], newVisibleItems, range);
+ }
}
- // append DOM to parent DOM
- if (!this.parent) {
- throw new Error('Cannot repaint item: no parent attached');
+ // If there were no visible items previously, use binarySearch to find a visible ItemPoint or ItemRange (based on startTime)
+ if (newVisibleItems.length == 0) {
+ initialPosByStart = util.binarySearch(orderedItems.byStart, range, 'data','start');
}
- 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);
+ else {
+ initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]);
}
- this.displayed = 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);
- }
- else if (this.data.content != undefined) {
- dom.content.innerHTML = this.content;
+ // use visible search to find a visible ItemRange (only based on endTime)
+ var initialPosByEnd = util.binarySearch(orderedItems.byEnd, range, 'data','end');
+
+ // if we found a initial ID to use, trace it up and down until we meet an invisible item.
+ if (initialPosByStart != -1) {
+ for (i = initialPosByStart; i >= 0; i--) {
+ if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
}
- else {
- throw new Error('Property "content" missing in item ' + this.data.id);
+ for (i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) {
+ if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
}
-
- 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 = 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;
+ // if we found a initial ID to use, trace it up and down until we meet an invisible item.
+ if (initialPosByEnd != -1) {
+ for (i = initialPosByEnd; i >= 0; i--) {
+ if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
+ }
+ for (i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) {
+ if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
+ }
}
- 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.
- */
-ItemRange.prototype.show = function show() {
- if (!this.displayed) {
- this.repaint();
- }
+ return newVisibleItems;
};
-/**
- * Hide the item from the DOM (when visible)
- * @return {Boolean} changed
- */
-ItemRange.prototype.hide = function hide() {
- if (this.displayed) {
- var box = this.dom.box;
-
- if (box.parentNode) {
- box.parentNode.removeChild(box);
- }
-
- this.top = null;
- this.left = null;
- this.displayed = false;
- }
-};
/**
- * Reposition the item horizontally
- * @Override
+ * this function checks if an item is invisible. If it is NOT we make it visible
+ * and add it to the global visible items. If it is, return true.
+ *
+ * @param {Item} item
+ * @param {Item[]} visibleItems
+ * @param {{start:number, end:number}} range
+ * @returns {boolean}
+ * @private
*/
-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;
-
- // 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.
+Group.prototype._checkIfInvisible = function(item, visibleItems, range) {
+ if (item.isVisible(range)) {
+ if (!item.displayed) item.show();
+ item.repositionX();
+ if (visibleItems.indexOf(item) == -1) {
+ visibleItems.push(item);
+ }
+ return false;
}
else {
- contentLeft = 0;
+ return true;
}
-
- this.left = start;
- this.width = Math.max(end - start, 1);
-
- this.dom.box.style.left = this.left + 'px';
- this.dom.box.style.width = this.width + 'px';
- this.dom.content.style.left = contentLeft + 'px';
};
/**
- * Reposition the item vertically
- * @Override
+ * this function is very similar to the _checkIfInvisible() but it does not
+ * return booleans, hides the item if it should not be seen and always adds to
+ * the visibleItems.
+ * this one is for brute forcing and hiding.
+ *
+ * @param {Item} item
+ * @param {Array} visibleItems
+ * @param {{start:number, end:number}} range
+ * @private
*/
-ItemRange.prototype.repositionY = function repositionY() {
- var orientation = this.options.orientation || this.defaultOptions.orientation,
- box = this.dom.box;
-
- if (orientation == 'top') {
- box.style.top = this.top + 'px';
- box.style.bottom = '';
+Group.prototype._checkIfVisible = function(item, visibleItems, range) {
+ if (item.isVisible(range)) {
+ if (!item.displayed) item.show();
+ // reposition item horizontally
+ item.repositionX();
+ visibleItems.push(item);
}
else {
- box.style.top = '';
- box.style.bottom = this.top + 'px';
+ if (item.displayed) item.hide();
}
};
/**
- * Repaint a drag area on the left side of the range when the range is selected
- * @protected
+ * Create a timeline visualization
+ * @param {HTMLElement} container
+ * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
+ * @param {Object} [options] See Timeline.setOptions for the available options.
+ * @constructor
*/
-ItemRange.prototype._repaintDragLeft = function () {
- if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) {
- // create and show drag area
- var dragLeft = document.createElement('div');
- dragLeft.className = 'drag-left';
- dragLeft.dragLeftItem = this;
+function Timeline (container, items, options) {
+ if (!(this instanceof Timeline)) {
+ throw new SyntaxError('Constructor must be called with the new operator');
+ }
- // TODO: this should be redundant?
- Hammer(dragLeft, {
- preventDefault: true
- }).on('drag', function () {
- //console.log('drag left')
- });
+ var me = this;
+ this.defaultOptions = {
+ start: null,
+ end: null,
- 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;
- }
-};
+ autoResize: true,
-/**
- * Repaint a drag area on the right side of the range when the range is selected
- * @protected
- */
-ItemRange.prototype._repaintDragRight = function () {
- if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) {
- // create and show drag area
- var dragRight = document.createElement('div');
- dragRight.className = 'drag-right';
- dragRight.dragRightItem = this;
+ orientation: 'bottom',
+ width: null,
+ height: null,
+ maxHeight: null,
+ minHeight: null
+ };
+ this.options = util.deepExtend({}, this.defaultOptions);
- // TODO: this should be redundant?
- Hammer(dragRight, {
- preventDefault: true
- }).on('drag', function () {
- //console.log('drag right')
- });
+ // Create the DOM, props, and emitter
+ this._create(container);
- 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;
- }
-};
+ // all components listed here will be repainted automatically
+ this.components = [];
-/**
- * @constructor ItemRangeOverflow
- * @extends ItemRange
- * @param {Object} data Object containing parameters start, end
- * content, className.
- * @param {Object} [options] Options to set initial property values
- * @param {Object} [defaultOptions] default options
- * // TODO: describe available options
- */
-function ItemRangeOverflow (data, options, defaultOptions) {
- this.props = {
- content: {
- left: 0,
- width: 0
+ this.body = {
+ dom: this.dom,
+ domProps: this.props,
+ emitter: {
+ on: this.on.bind(this),
+ off: this.off.bind(this),
+ emit: this.emit.bind(this)
+ },
+ util: {
+ snap: null, // will be specified after TimeAxis is created
+ toScreen: me._toScreen.bind(me),
+ toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
+ toTime: me._toTime.bind(me),
+ toGlobalTime : me._toGlobalTime.bind(me)
}
};
- ItemRange.call(this, data, options, defaultOptions);
-}
+ // range
+ this.range = new Range(this.body);
+ this.components.push(this.range);
+ this.body.range = this.range;
-ItemRangeOverflow.prototype = new ItemRange (null);
+ // time axis
+ this.timeAxis = new TimeAxis(this.body);
+ this.components.push(this.timeAxis);
+ this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
-ItemRangeOverflow.prototype.baseClassName = 'item rangeoverflow';
+ // current time bar
+ this.currentTime = new CurrentTime(this.body);
+ this.components.push(this.currentTime);
-/**
- * Reposition the item horizontally
- * @Override
- */
-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;
+ // custom time bar
+ // Note: time bar will be attached in this.setOptions when selected
+ this.customTime = new CustomTime(this.body);
+ this.components.push(this.customTime);
- // 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;
- }
+ // item set
+ this.itemSet = new ItemSet(this.body);
+ this.components.push(this.itemSet);
- // when range exceeds left of the window, position the contents at the left of the visible area
- contentLeft = Math.max(-start, 0);
+ this.itemsData = null; // DataSet
+ this.groupsData = null; // DataSet
- this.left = start;
- var boxWidth = Math.max(end - start, 1);
- this.width = boxWidth + this.props.content.width;
- // Note: The calculation of width is an optimistic calculation, giving
- // a width which will not change when moving the Timeline
- // So no restacking needed, which is nicer for the eye
+ // apply options
+ if (options) {
+ this.setOptions(options);
+ }
- this.dom.box.style.left = this.left + 'px';
- this.dom.box.style.width = boxWidth + 'px';
- this.dom.content.style.left = contentLeft + 'px';
-};
+ // create itemset
+ if (items) {
+ this.setItems(items);
+ }
+ else {
+ this.redraw();
+ }
+}
+
+// turn Timeline into an event emitter
+Emitter(Timeline.prototype);
/**
- * @constructor Group
- * @param {Number | String} groupId
- * @param {Object} data
- * @param {ItemSet} itemSet
+ * Create the main DOM for the Timeline: a root panel containing left, right,
+ * top, bottom, content, and background panel.
+ * @param {Element} container The container element where the Timeline will
+ * be attached.
+ * @private
*/
-function Group (groupId, data, itemSet) {
- this.groupId = groupId;
+Timeline.prototype._create = function (container) {
+ this.dom = {};
- this.itemSet = itemSet;
+ this.dom.root = document.createElement('div');
+ this.dom.background = document.createElement('div');
+ this.dom.backgroundVertical = document.createElement('div');
+ this.dom.backgroundHorizontal = document.createElement('div');
+ this.dom.centerContainer = document.createElement('div');
+ this.dom.leftContainer = document.createElement('div');
+ this.dom.rightContainer = document.createElement('div');
+ this.dom.center = document.createElement('div');
+ this.dom.left = document.createElement('div');
+ this.dom.right = document.createElement('div');
+ this.dom.top = document.createElement('div');
+ this.dom.bottom = document.createElement('div');
+ this.dom.shadowTop = document.createElement('div');
+ this.dom.shadowBottom = document.createElement('div');
+ this.dom.shadowTopLeft = document.createElement('div');
+ this.dom.shadowBottomLeft = document.createElement('div');
+ this.dom.shadowTopRight = document.createElement('div');
+ this.dom.shadowBottomRight = document.createElement('div');
+
+ this.dom.background.className = 'vispanel background';
+ this.dom.backgroundVertical.className = 'vispanel background vertical';
+ this.dom.backgroundHorizontal.className = 'vispanel background horizontal';
+ this.dom.centerContainer.className = 'vispanel center';
+ this.dom.leftContainer.className = 'vispanel left';
+ this.dom.rightContainer.className = 'vispanel right';
+ this.dom.top.className = 'vispanel top';
+ this.dom.bottom.className = 'vispanel bottom';
+ this.dom.left.className = 'content';
+ this.dom.center.className = 'content';
+ this.dom.right.className = 'content';
+ this.dom.shadowTop.className = 'shadow top';
+ this.dom.shadowBottom.className = 'shadow bottom';
+ this.dom.shadowTopLeft.className = 'shadow top';
+ this.dom.shadowBottomLeft.className = 'shadow bottom';
+ this.dom.shadowTopRight.className = 'shadow top';
+ this.dom.shadowBottomRight.className = 'shadow bottom';
+
+ this.dom.root.appendChild(this.dom.background);
+ this.dom.root.appendChild(this.dom.backgroundVertical);
+ this.dom.root.appendChild(this.dom.backgroundHorizontal);
+ this.dom.root.appendChild(this.dom.centerContainer);
+ this.dom.root.appendChild(this.dom.leftContainer);
+ this.dom.root.appendChild(this.dom.rightContainer);
+ this.dom.root.appendChild(this.dom.top);
+ this.dom.root.appendChild(this.dom.bottom);
+
+ this.dom.centerContainer.appendChild(this.dom.center);
+ this.dom.leftContainer.appendChild(this.dom.left);
+ this.dom.rightContainer.appendChild(this.dom.right);
+
+ this.dom.centerContainer.appendChild(this.dom.shadowTop);
+ this.dom.centerContainer.appendChild(this.dom.shadowBottom);
+ this.dom.leftContainer.appendChild(this.dom.shadowTopLeft);
+ this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft);
+ this.dom.rightContainer.appendChild(this.dom.shadowTopRight);
+ this.dom.rightContainer.appendChild(this.dom.shadowBottomRight);
+
+ this.on('rangechange', this.redraw.bind(this));
+ this.on('change', this.redraw.bind(this));
+ this.on('touch', this._onTouch.bind(this));
+ this.on('pinch', this._onPinch.bind(this));
+ this.on('dragstart', this._onDragStart.bind(this));
+ this.on('drag', this._onDrag.bind(this));
- this.dom = {};
- this.props = {
- label: {
- width: 0,
- height: 0
- }
- };
+ // create event listeners for all interesting events, these events will be
+ // emitted via emitter
+ this.hammer = Hammer(this.dom.root, {
+ prevent_default: true
+ });
+ this.listeners = {};
- this.items = {}; // items filtered by groupId of this group
- this.visibleItems = []; // items currently visible in window
- this.orderedItems = { // items sorted by start and by end
- byStart: [],
- byEnd: []
- };
+ var me = this;
+ var events = [
+ 'touch', 'pinch',
+ 'tap', 'doubletap', 'hold',
+ 'dragstart', 'drag', 'dragend',
+ 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed 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;
+ });
- this._create();
+ // size properties of each of the panels
+ this.props = {
+ root: {},
+ background: {},
+ centerContainer: {},
+ leftContainer: {},
+ rightContainer: {},
+ center: {},
+ left: {},
+ right: {},
+ top: {},
+ bottom: {},
+ border: {},
+ scrollTop: 0,
+ scrollTopMin: 0
+ };
+ this.touch = {}; // store state information needed for touch events
- this.setData(data);
-}
+ // attach the root panel to the provided container
+ if (!container) throw new Error('No container provided');
+ container.appendChild(this.dom.root);
+};
/**
- * Create DOM elements for the group
- * @private
+ * Destroy the Timeline, clean up all DOM elements and event listeners.
*/
-Group.prototype._create = function() {
- var label = document.createElement('div');
- label.className = 'vlabel';
- this.dom.label = label;
+Timeline.prototype.destroy = function () {
+ // unbind datasets
+ this.clear();
- var inner = document.createElement('div');
- inner.className = 'inner';
- label.appendChild(inner);
- this.dom.inner = inner;
+ // remove all event listeners
+ this.off();
- var foreground = document.createElement('div');
- foreground.className = 'group';
- foreground['timeline-group'] = this;
- this.dom.foreground = foreground;
+ // stop checking for changed size
+ this._stopAutoResize();
- this.dom.background = document.createElement('div');
+ // remove from DOM
+ if (this.dom.root.parentNode) {
+ this.dom.root.parentNode.removeChild(this.dom.root);
+ }
+ this.dom = null;
- this.dom.axis = document.createElement('div');
+ // cleanup hammer touch events
+ for (var event in this.listeners) {
+ if (this.listeners.hasOwnProperty(event)) {
+ delete this.listeners[event];
+ }
+ }
+ this.listeners = null;
+ this.hammer = null;
- // create a hidden marker to detect when the Timelines container is attached
- // to the DOM, or the style of a parent of the Timeline is changed from
- // display:none is changed to visible.
- this.dom.marker = document.createElement('div');
- this.dom.marker.style.visibility = 'hidden';
- this.dom.marker.innerHTML = '?';
- this.dom.background.appendChild(this.dom.marker);
+ // give all components the opportunity to cleanup
+ this.components.forEach(function (component) {
+ component.destroy();
+ });
+
+ this.body = null;
};
/**
- * Set the group data for this group
- * @param {Object} data Group data, can contain properties content and className
+ * Set options. Options will be passed to all components loaded in the Timeline.
+ * @param {Object} [options]
+ * {String} orientation
+ * Vertical orientation for the Timeline,
+ * can be 'bottom' (default) or 'top'.
+ * {String | Number} width
+ * Width for the timeline, a number in pixels or
+ * a css string like '1000px' or '75%'. '100%' by default.
+ * {String | Number} height
+ * Fixed height for the Timeline, a number in pixels or
+ * a css string like '400px' or '75%'. If undefined,
+ * The Timeline will automatically size such that
+ * its contents fit.
+ * {String | Number} minHeight
+ * Minimum height for the Timeline, a number in pixels or
+ * a css string like '400px' or '75%'.
+ * {String | Number} maxHeight
+ * Maximum height for the Timeline, a number in pixels or
+ * a css string like '400px' or '75%'.
+ * {Number | Date | String} start
+ * Start date for the visible window
+ * {Number | Date | String} end
+ * End date for the visible window
*/
-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;
+Timeline.prototype.setOptions = function (options) {
+ if (options) {
+ // copy the known options
+ var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation'];
+ util.selectiveExtend(fields, this.options, options);
+
+ // enable/disable autoResize
+ this._initAutoResize();
}
- // update className
- var className = data && data.className;
- if (className) {
- util.addClassName(this.dom.label, className);
+ // propagate options to all components
+ this.components.forEach(function (component) {
+ component.setOptions(options);
+ });
+
+ // TODO: remove deprecation error one day (deprecated since version 0.8.0)
+ if (options && options.order) {
+ throw new Error('Option order is deprecated. There is no replacement for this feature.');
}
+
+ // redraw everything
+ this.redraw();
};
/**
- * Get the foreground container element
- * @return {HTMLElement} foreground
+ * Set a custom time bar
+ * @param {Date} time
*/
-Group.prototype.getForeground = function getForeground() {
- return this.dom.foreground;
-};
+Timeline.prototype.setCustomTime = function (time) {
+ if (!this.customTime) {
+ throw new Error('Cannot get custom time: Custom time bar is not enabled');
+ }
-/**
- * Get the background container element
- * @return {HTMLElement} background
- */
-Group.prototype.getBackground = function getBackground() {
- return this.dom.background;
+ this.customTime.setCustomTime(time);
};
/**
- * Get the axis container element
- * @return {HTMLElement} axis
+ * Retrieve the current custom time.
+ * @return {Date} customTime
*/
-Group.prototype.getAxis = function getAxis() {
- return this.dom.axis;
-};
+Timeline.prototype.getCustomTime = function() {
+ if (!this.customTime) {
+ throw new Error('Cannot get custom time: Custom time bar is not enabled');
+ }
-/**
- * Get the width of the group label
- * @return {number} width
- */
-Group.prototype.getLabelWidth = function getLabelWidth() {
- return this.props.label.width;
+ return this.customTime.getCustomTime();
};
-
/**
- * Repaint this group
- * @param {{start: number, end: number}} range
- * @param {{item: number, axis: number}} margin
- * @param {boolean} [restack=false] Force restacking of all items
- * @return {boolean} Returns true if the group is resized
+ * Set items
+ * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
*/
-Group.prototype.repaint = function repaint(range, margin, restack) {
- var resized = false;
+Timeline.prototype.setItems = function(items) {
+ var initialLoad = (this.itemsData == null);
- this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range);
+ // convert to type DataSet when needed
+ var newDataSet;
+ if (!items) {
+ newDataSet = null;
+ }
+ else if (items instanceof DataSet || items instanceof DataView) {
+ newDataSet = items;
+ }
+ else {
+ // turn an array into a dataset
+ newDataSet = new DataSet(items, {
+ type: {
+ start: 'Date',
+ end: 'Date'
+ }
+ });
+ }
- // force recalculation of the height of the items when the marker height changed
- // (due to the Timeline being attached to the DOM or changed from display:none to visible)
- var markerHeight = this.dom.marker.clientHeight;
- if (markerHeight != this.lastMarkerHeight) {
- this.lastMarkerHeight = markerHeight;
+ // set items
+ this.itemsData = newDataSet;
+ this.itemSet && this.itemSet.setItems(newDataSet);
- util.forEach(this.items, function (item) {
- item.dirty = true;
- if (item.displayed) item.repaint();
- });
+ if (initialLoad && ('start' in this.options || 'end' in this.options)) {
+ this.fit();
- restack = true;
- }
+ var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null;
+ var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null;
- // reposition visible items vertically
- if (this.itemSet.options.stack) { // TODO: ugly way to access options...
- stack.stack(this.visibleItems, margin, restack);
- }
- else { // no stacking
- stack.nostack(this.visibleItems, margin);
- }
- for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
- var item = this.visibleItems[i];
- item.repositionY();
+ this.setWindow(start, end);
}
+};
- // recalculate the height of the group
- var height;
- 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) + margin.axis + margin.item;
+/**
+ * Set groups
+ * @param {vis.DataSet | Array | google.visualization.DataTable} groups
+ */
+Timeline.prototype.setGroups = function(groups) {
+ // convert to type DataSet when needed
+ var newDataSet;
+ if (!groups) {
+ newDataSet = null;
+ }
+ else if (groups instanceof DataSet || groups instanceof DataView) {
+ newDataSet = groups;
}
else {
- height = margin.axis + margin.item;
+ // turn an array into a dataset
+ newDataSet = new DataSet(groups);
}
- height = Math.max(height, this.props.label.height);
-
- // calculate actual size and position
- var foreground = this.dom.foreground;
- this.top = foreground.offsetTop;
- this.left = foreground.offsetLeft;
- this.width = foreground.offsetWidth;
- resized = util.updateProperty(this, 'height', height) || resized;
-
- // recalculate size of 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;
-
- // apply new height
- foreground.style.height = height + 'px';
- this.dom.label.style.height = height + 'px';
- return resized;
+ this.groupsData = newDataSet;
+ this.itemSet.setGroups(newDataSet);
};
/**
- * Show this group: attach to the DOM
+ * Clear the Timeline. By Default, items, groups and options are cleared.
+ * Example usage:
+ *
+ * timeline.clear(); // clear items, groups, and options
+ * timeline.clear({options: true}); // clear options only
+ *
+ * @param {Object} [what] Optionally specify what to clear. By default:
+ * {items: true, groups: true, options: true}
*/
-Group.prototype.show = function show() {
- if (!this.dom.label.parentNode) {
- this.itemSet.getLabelSet().appendChild(this.dom.label);
+Timeline.prototype.clear = function(what) {
+ // clear items
+ if (!what || what.items) {
+ this.setItems(null);
}
- if (!this.dom.foreground.parentNode) {
- this.itemSet.getForeground().appendChild(this.dom.foreground);
+ // clear groups
+ if (!what || what.groups) {
+ this.setGroups(null);
}
- if (!this.dom.background.parentNode) {
- this.itemSet.getBackground().appendChild(this.dom.background);
- }
+ // clear options of timeline and of each of the components
+ if (!what || what.options) {
+ this.components.forEach(function (component) {
+ component.setOptions(component.defaultOptions);
+ });
- if (!this.dom.axis.parentNode) {
- this.itemSet.getAxis().appendChild(this.dom.axis);
+ this.setOptions(this.defaultOptions); // this will also do a redraw
}
};
/**
- * Hide this group: remove from the DOM
+ * Set Timeline window such that it fits all items
*/
-Group.prototype.hide = function hide() {
- var label = this.dom.label;
- if (label.parentNode) {
- label.parentNode.removeChild(label);
- }
+Timeline.prototype.fit = function() {
+ // apply the data range as range
+ var dataRange = this.getItemRange();
- var foreground = this.dom.foreground;
- if (foreground.parentNode) {
- foreground.parentNode.removeChild(foreground);
+ // add 5% space on both sides
+ var start = dataRange.min;
+ var end = dataRange.max;
+ if (start != null && end != null) {
+ var interval = (end.valueOf() - start.valueOf());
+ if (interval <= 0) {
+ // prevent an empty interval
+ interval = 24 * 60 * 60 * 1000; // 1 day
+ }
+ start = new Date(start.valueOf() - interval * 0.05);
+ end = new Date(end.valueOf() + interval * 0.05);
}
- var background = this.dom.background;
- if (background.parentNode) {
- background.parentNode.removeChild(background);
+ // skip range set if there is no start and end date
+ if (start === null && end === null) {
+ return;
}
- var axis = this.dom.axis;
- if (axis.parentNode) {
- axis.parentNode.removeChild(axis);
+ this.range.setRange(start, end);
+};
+
+/**
+ * Get the data range of the item set.
+ * @returns {{min: Date, max: Date}} range A range with a start and end Date.
+ * When no minimum is found, min==null
+ * When no maximum is found, max==null
+ */
+Timeline.prototype.getItemRange = function() {
+ // calculate min from start filed
+ var dataset = this.itemsData.getDataSet(),
+ min = null,
+ max = null;
+
+ if (dataset) {
+ // calculate the minimum value of the field 'start'
+ var minItem = dataset.min('start');
+ min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null;
+ // Note: we convert first to Date and then to number because else
+ // a conversion from ISODate to Number will fail
+
+ // calculate maximum value of fields 'start' and 'end'
+ var maxStartItem = dataset.max('start');
+ if (maxStartItem) {
+ max = util.convert(maxStartItem.start, 'Date').valueOf();
+ }
+ var maxEndItem = dataset.max('end');
+ if (maxEndItem) {
+ if (max == null) {
+ max = util.convert(maxEndItem.end, 'Date').valueOf();
+ }
+ else {
+ max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf());
+ }
+ }
}
+
+ return {
+ min: (min != null) ? new Date(min) : null,
+ max: (max != null) ? new Date(max) : null
+ };
};
/**
- * Add an item to the group
- * @param {Item} item
+ * Set selected items by their id. Replaces the current selection
+ * Unknown id's are silently ignored.
+ * @param {Array} [ids] An array with zero or more id's of the items to be
+ * selected. If ids is an empty array, all items will be
+ * unselected.
*/
-Group.prototype.add = function add(item) {
- this.items[item.id] = item;
- item.setParent(this);
+Timeline.prototype.setSelection = function(ids) {
+ this.itemSet && this.itemSet.setSelection(ids);
+};
- if (item instanceof ItemRange && this.visibleItems.indexOf(item) == -1) {
- var range = this.itemSet.range; // TODO: not nice accessing the range like this
- this._checkIfVisible(item, this.visibleItems, range);
+/**
+ * Get the selected items by their id
+ * @return {Array} ids The ids of the selected items
+ */
+Timeline.prototype.getSelection = function() {
+ return this.itemSet && this.itemSet.getSelection() || [];
+};
+
+/**
+ * Set the visible window. Both parameters are optional, you can change only
+ * start or only end. Syntax:
+ *
+ * TimeLine.setWindow(start, end)
+ * TimeLine.setWindow(range)
+ *
+ * Where start and end can be a Date, number, or string, and range is an
+ * object with properties start and end.
+ *
+ * @param {Date | Number | String | Object} [start] Start date of visible window
+ * @param {Date | Number | String} [end] End date of visible window
+ */
+Timeline.prototype.setWindow = function(start, end) {
+ if (arguments.length == 1) {
+ var range = arguments[0];
+ this.range.setRange(range.start, range.end);
+ }
+ else {
+ this.range.setRange(start, end);
}
};
/**
- * Remove an item from the group
- * @param {Item} item
+ * Get the visible window
+ * @return {{start: Date, end: Date}} Visible range
*/
-Group.prototype.remove = function remove(item) {
- delete this.items[item.id];
- item.setParent(this.itemSet);
+Timeline.prototype.getWindow = function() {
+ var range = this.range.getRange();
+ return {
+ start: new Date(range.start),
+ end: new Date(range.end)
+ };
+};
- // remove from visible items
- var index = this.visibleItems.indexOf(item);
- if (index != -1) this.visibleItems.splice(index, 1);
+/**
+ * Force a redraw of the Timeline. Can be useful to manually redraw when
+ * option autoResize=false
+ */
+Timeline.prototype.redraw = function() {
+ var resized = false,
+ options = this.options,
+ props = this.props,
+ dom = this.dom;
+
+ if (!dom) return; // when destroyed
+
+ // update class names
+ dom.root.className = 'vis timeline root ' + options.orientation;
+
+ // update root width and height options
+ dom.root.style.maxHeight = util.option.asSize(options.maxHeight, '');
+ dom.root.style.minHeight = util.option.asSize(options.minHeight, '');
+ dom.root.style.width = util.option.asSize(options.width, '');
+
+ // calculate border widths
+ props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2;
+ props.border.right = props.border.left;
+ props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2;
+ props.border.bottom = props.border.top;
+ var borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight;
+ var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth;
+
+ // calculate the heights. If any of the side panels is empty, we set the height to
+ // minus the border width, such that the border will be invisible
+ props.center.height = dom.center.offsetHeight;
+ props.left.height = dom.left.offsetHeight;
+ props.right.height = dom.right.offsetHeight;
+ props.top.height = dom.top.clientHeight || -props.border.top;
+ props.bottom.height = dom.bottom.clientHeight || -props.border.bottom;
+
+ // TODO: compensate borders when any of the panels is empty.
+
+ // apply auto height
+ // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM)
+ var contentHeight = Math.max(props.left.height, props.center.height, props.right.height);
+ var autoHeight = props.top.height + contentHeight + props.bottom.height +
+ borderRootHeight + props.border.top + props.border.bottom;
+ dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px');
+
+ // calculate heights of the content panels
+ props.root.height = dom.root.offsetHeight;
+ props.background.height = props.root.height - borderRootHeight;
+ var containerHeight = props.root.height - props.top.height - props.bottom.height -
+ borderRootHeight;
+ props.centerContainer.height = containerHeight;
+ props.leftContainer.height = containerHeight;
+ props.rightContainer.height = props.leftContainer.height;
+
+ // calculate the widths of the panels
+ props.root.width = dom.root.offsetWidth;
+ props.background.width = props.root.width - borderRootWidth;
+ props.left.width = dom.leftContainer.clientWidth || -props.border.left;
+ props.leftContainer.width = props.left.width;
+ props.right.width = dom.rightContainer.clientWidth || -props.border.right;
+ props.rightContainer.width = props.right.width;
+ var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth;
+ props.center.width = centerWidth;
+ props.centerContainer.width = centerWidth;
+ props.top.width = centerWidth;
+ props.bottom.width = centerWidth;
+
+ // resize the panels
+ dom.background.style.height = props.background.height + 'px';
+ dom.backgroundVertical.style.height = props.background.height + 'px';
+ dom.backgroundHorizontal.style.height = props.centerContainer.height + 'px';
+ dom.centerContainer.style.height = props.centerContainer.height + 'px';
+ dom.leftContainer.style.height = props.leftContainer.height + 'px';
+ dom.rightContainer.style.height = props.rightContainer.height + 'px';
+
+ dom.background.style.width = props.background.width + 'px';
+ dom.backgroundVertical.style.width = props.centerContainer.width + 'px';
+ dom.backgroundHorizontal.style.width = props.background.width + 'px';
+ dom.centerContainer.style.width = props.center.width + 'px';
+ dom.top.style.width = props.top.width + 'px';
+ dom.bottom.style.width = props.bottom.width + 'px';
+
+ // reposition the panels
+ dom.background.style.left = '0';
+ dom.background.style.top = '0';
+ dom.backgroundVertical.style.left = props.left.width + 'px';
+ dom.backgroundVertical.style.top = '0';
+ dom.backgroundHorizontal.style.left = '0';
+ dom.backgroundHorizontal.style.top = props.top.height + 'px';
+ dom.centerContainer.style.left = props.left.width + 'px';
+ dom.centerContainer.style.top = props.top.height + 'px';
+ dom.leftContainer.style.left = '0';
+ dom.leftContainer.style.top = props.top.height + 'px';
+ dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px';
+ dom.rightContainer.style.top = props.top.height + 'px';
+ dom.top.style.left = props.left.width + 'px';
+ dom.top.style.top = '0';
+ dom.bottom.style.left = props.left.width + 'px';
+ dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px';
+
+ // update the scrollTop, feasible range for the offset can be changed
+ // when the height of the Timeline or of the contents of the center changed
+ this._updateScrollTop();
+
+ // reposition the scrollable contents
+ var offset = this.props.scrollTop;
+ if (options.orientation == 'bottom') {
+ offset += Math.max(this.props.centerContainer.height - this.props.center.height -
+ this.props.border.top - this.props.border.bottom, 0);
+ }
+ dom.center.style.left = '0';
+ dom.center.style.top = offset + 'px';
+ dom.left.style.left = '0';
+ dom.left.style.top = offset + 'px';
+ dom.right.style.left = '0';
+ dom.right.style.top = offset + 'px';
+
+ // show shadows when vertical scrolling is available
+ var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : '';
+ var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : '';
+ dom.shadowTop.style.visibility = visibilityTop;
+ dom.shadowBottom.style.visibility = visibilityBottom;
+ dom.shadowTopLeft.style.visibility = visibilityTop;
+ dom.shadowBottomLeft.style.visibility = visibilityBottom;
+ dom.shadowTopRight.style.visibility = visibilityTop;
+ dom.shadowBottomRight.style.visibility = visibilityBottom;
+
+ // redraw all components
+ this.components.forEach(function (component) {
+ resized = component.redraw() || resized;
+ });
+ if (resized) {
+ // keep repainting until all sizes are settled
+ this.redraw();
+ }
+};
- // TODO: also remove from ordered items?
+// TODO: deprecated since version 1.1.0, remove some day
+Timeline.prototype.repaint = function () {
+ throw new Error('Function repaint is deprecated. Use redraw instead.');
};
/**
- * Remove an item from the corresponding DataSet
- * @param {Item} item
+ * 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
*/
-Group.prototype.removeFromDataSet = function removeFromDataSet(item) {
- this.itemSet.removeItem(item.id);
+// TODO: move this function to Range
+Timeline.prototype._toTime = function(x) {
+ var conversion = this.range.conversion(this.props.center.width);
+ return new Date(x / conversion.scale + conversion.offset);
};
+
/**
- * Reorder the items
+ * Convert a position on the global 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
*/
-Group.prototype.order = function order() {
- var array = util.toArray(this.items);
- this.orderedItems.byStart = array;
- this.orderedItems.byEnd = this._constructByEndArray(array);
-
- stack.orderByStart(this.orderedItems.byStart);
- stack.orderByEnd(this.orderedItems.byEnd);
+// TODO: move this function to Range
+Timeline.prototype._toGlobalTime = function(x) {
+ var conversion = this.range.conversion(this.props.root.width);
+ return new Date(x / conversion.scale + conversion.offset);
};
/**
- * Create an array containing all items being a range (having an end date)
- * @param {Item[]} array
- * @returns {ItemRange[]}
+ * 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
*/
-Group.prototype._constructByEndArray = function _constructByEndArray(array) {
- var endArray = [];
-
- for (var i = 0; i < array.length; i++) {
- if (array[i] instanceof ItemRange) {
- endArray.push(array[i]);
- }
- }
- return endArray;
+// TODO: move this function to Range
+Timeline.prototype._toScreen = function(time) {
+ var conversion = this.range.conversion(this.props.center.width);
+ return (time.valueOf() - conversion.offset) * conversion.scale;
};
+
/**
- * Update the visible items
- * @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date
- * @param {Item[]} visibleItems The previously visible items.
- * @param {{start: number, end: number}} range Visible range
- * @return {Item[]} visibleItems The new visible items.
+ * Convert a datetime (Date object) into a position on the root
+ * This is used to get the pixel density estimate for the screen, not the center panel
+ * @param {Date} time A date
+ * @return {int} x The position on root in pixels which corresponds
+ * with the given date.
* @private
*/
-Group.prototype._updateVisibleItems = function _updateVisibleItems(orderedItems, visibleItems, range) {
- var initialPosByStart,
- newVisibleItems = [],
- i;
+// TODO: move this function to Range
+Timeline.prototype._toGlobalScreen = function(time) {
+ var conversion = this.range.conversion(this.props.root.width);
+ return (time.valueOf() - conversion.offset) * conversion.scale;
+};
- // first check if the items that were in view previously are still in view.
- // this handles the case for the ItemRange that is both before and after the current one.
- if (visibleItems.length > 0) {
- for (i = 0; i < visibleItems.length; i++) {
- this._checkIfVisible(visibleItems[i], newVisibleItems, range);
- }
- }
- // If there were no visible items previously, use binarySearch to find a visible ItemPoint or ItemRange (based on startTime)
- if (newVisibleItems.length == 0) {
- initialPosByStart = this._binarySearch(orderedItems, range, false);
+/**
+ * Initialize watching when option autoResize is true
+ * @private
+ */
+Timeline.prototype._initAutoResize = function () {
+ if (this.options.autoResize == true) {
+ this._startAutoResize();
}
else {
- initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]);
- }
-
- // use visible search to find a visible ItemRange (only based on endTime)
- var initialPosByEnd = this._binarySearch(orderedItems, range, true);
-
- // if we found a initial ID to use, trace it up and down until we meet an invisible item.
- if (initialPosByStart != -1) {
- for (i = initialPosByStart; i >= 0; i--) {
- if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
- }
- for (i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) {
- if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
- }
- }
-
- // if we found a initial ID to use, trace it up and down until we meet an invisible item.
- if (initialPosByEnd != -1) {
- for (i = initialPosByEnd; i >= 0; i--) {
- if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
- }
- for (i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) {
- if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
- }
+ this._stopAutoResize();
}
-
- return newVisibleItems;
};
/**
- * This function does a binary search for a visible item. The user can select either the this.orderedItems.byStart or .byEnd
- * arrays. This is done by giving a boolean value true if you want to use the byEnd.
- * This is done to be able to select the correct if statement (we do not want to check if an item is visible, we want to check
- * if the time we selected (start or end) is within the current range).
- *
- * The trick is that every interval has to either enter the screen at the initial load or by dragging. The case of the ItemRange that is
- * before and after the current range is handled by simply checking if it was in view before and if it is again. For all the rest,
- * either the start OR end time has to be in the range.
- *
- * @param {{byStart: Item[], byEnd: Item[]}} orderedItems
- * @param {{start: number, end: number}} range
- * @param {Boolean} byEnd
- * @returns {number}
+ * Watch for changes in the size of the container. On resize, the Panel will
+ * automatically redraw itself.
* @private
*/
-Group.prototype._binarySearch = function _binarySearch(orderedItems, range, byEnd) {
- var array = [];
- var byTime = byEnd ? 'end' : 'start';
- if (byEnd == true) {array = orderedItems.byEnd; }
- else {array = orderedItems.byStart;}
-
- var interval = range.end - range.start;
+Timeline.prototype._startAutoResize = function () {
+ var me = this;
- var found = false;
- var low = 0;
- var high = array.length;
- var guess = Math.floor(0.5*(high+low));
- var newGuess;
+ this._stopAutoResize();
- if (high == 0) {guess = -1;}
- else if (high == 1) {
- if ((array[guess].data[byTime] > range.start - interval) && (array[guess].data[byTime] < range.end)) {
- guess = 0;
- }
- else {
- guess = -1;
+ this._onResize = function() {
+ if (me.options.autoResize != true) {
+ // stop watching when the option autoResize is changed to false
+ me._stopAutoResize();
+ return;
}
- }
- else {
- high -= 1;
- while (found == false) {
- if ((array[guess].data[byTime] > range.start - interval) && (array[guess].data[byTime] < range.end)) {
- found = true;
- }
- else {
- if (array[guess].data[byTime] < range.start - interval) { // it is too small --> increase low
- low = Math.floor(0.5*(high+low));
- }
- else { // it is too big --> decrease high
- high = Math.floor(0.5*(high+low));
- }
- newGuess = Math.floor(0.5*(high+low));
- // not in list;
- if (guess == newGuess) {
- guess = -1;
- found = true;
- }
- else {
- guess = newGuess;
- }
+
+ if (me.dom.root) {
+ // check whether the frame is resized
+ if ((me.dom.root.clientWidth != me.props.lastWidth) ||
+ (me.dom.root.clientHeight != me.props.lastHeight)) {
+ me.props.lastWidth = me.dom.root.clientWidth;
+ me.props.lastHeight = me.dom.root.clientHeight;
+
+ me.emit('change');
}
}
- }
- return guess;
+ };
+
+ // add event listener to window resize
+ util.addEventListener(window, 'resize', this._onResize);
+
+ this.watchTimer = setInterval(this._onResize, 1000);
};
/**
- * this function checks if an item is invisible. If it is NOT we make it visible
- * and add it to the global visible items. If it is, return true.
- *
- * @param {Item} item
- * @param {Item[]} visibleItems
- * @param {{start:number, end:number}} range
- * @returns {boolean}
+ * Stop watching for a resize of the frame.
* @private
*/
-Group.prototype._checkIfInvisible = function _checkIfInvisible(item, visibleItems, range) {
- if (item.isVisible(range)) {
- if (!item.displayed) item.show();
- item.repositionX();
- if (visibleItems.indexOf(item) == -1) {
- visibleItems.push(item);
- }
- return false;
- }
- else {
- return true;
+Timeline.prototype._stopAutoResize = function () {
+ if (this.watchTimer) {
+ clearInterval(this.watchTimer);
+ this.watchTimer = undefined;
}
+
+ // remove event listener on window.resize
+ util.removeEventListener(window, 'resize', this._onResize);
+ this._onResize = null;
};
/**
- * this function is very similar to the _checkIfInvisible() but it does not
- * return booleans, hides the item if it should not be seen and always adds to
- * the visibleItems.
- * this one is for brute forcing and hiding.
- *
- * @param {Item} item
- * @param {Array} visibleItems
- * @param {{start:number, end:number}} range
+ * Start moving the timeline vertically
+ * @param {Event} event
* @private
*/
-Group.prototype._checkIfVisible = function _checkIfVisible(item, visibleItems, range) {
- if (item.isVisible(range)) {
- if (!item.displayed) item.show();
- // reposition item horizontally
- item.repositionX();
- visibleItems.push(item);
- }
- else {
- if (item.displayed) item.hide();
- }
+Timeline.prototype._onTouch = function (event) {
+ this.touch.allowDragging = true;
};
/**
- * Create a timeline visualization
- * @param {HTMLElement} container
- * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
- * @param {Object} [options] See Timeline.setOptions for the available options.
- * @constructor
+ * Start moving the timeline vertically
+ * @param {Event} event
+ * @private
*/
-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.defaultOptions = {
- orientation: 'bottom',
- direction: 'horizontal', // 'horizontal' or 'vertical'
- autoResize: true,
- stack: true,
-
- editable: {
- updateTime: false,
- updateGroup: false,
- add: false,
- remove: false
- },
-
- selectable: true,
-
- start: null,
- end: null,
- 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,
+Timeline.prototype._onPinch = function (event) {
+ this.touch.allowDragging = false;
+};
- groupOrder: null,
+/**
+ * Start moving the timeline vertically
+ * @param {Event} event
+ * @private
+ */
+Timeline.prototype._onDragStart = function (event) {
+ this.touch.initialScrollTop = this.props.scrollTop;
+};
- width: null,
- height: null,
- maxHeight: null,
- minHeight: null,
+/**
+ * Move the timeline vertically
+ * @param {Event} event
+ * @private
+ */
+Timeline.prototype._onDrag = 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 (!this.touch.allowDragging) return;
- type: 'box',
- align: 'center',
- margin: {
- axis: 20,
- item: 10
- },
- padding: 5,
+ var delta = event.gesture.deltaY;
- onAdd: function (item, callback) {
- callback(item);
- },
- onUpdate: function (item, callback) {
- callback(item);
- },
- onMove: function (item, callback) {
- callback(item);
- },
- onRemove: function (item, callback) {
- callback(item);
- }
- };
+ var oldScrollTop = this._getScrollTop();
+ var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta);
- this.options = {};
- util.deepExtend(this.options, this.defaultOptions);
- util.deepExtend(this.options, {
- snap: null, // will be specified after timeaxis is created
+ if (newScrollTop != oldScrollTop) {
+ this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already
+ }
+};
- toScreen: me._toScreen.bind(me),
- toTime: me._toTime.bind(me)
- });
+/**
+ * Apply a scrollTop
+ * @param {Number} scrollTop
+ * @returns {Number} scrollTop Returns the applied scrollTop
+ * @private
+ */
+Timeline.prototype._setScrollTop = function (scrollTop) {
+ this.props.scrollTop = scrollTop;
+ this._updateScrollTop();
+ return this.props.scrollTop;
+};
- // root panel
- 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';
- }
+/**
+ * Update the current scrollTop when the height of the containers has been changed
+ * @returns {Number} scrollTop Returns the applied scrollTop
+ * @private
+ */
+Timeline.prototype._updateScrollTop = function () {
+ // recalculate the scrollTopMin
+ var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero
+ if (scrollTopMin != this.props.scrollTopMin) {
+ // in case of bottom orientation, change the scrollTop such that the contents
+ // do not move relative to the time axis at the bottom
+ if (this.options.orientation == 'bottom') {
+ this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin);
}
- });
- this.rootPanel = new RootPanel(container, rootOptions);
+ this.props.scrollTopMin = scrollTopMin;
+ }
- // single select (or unselect) when tapping an item
- this.rootPanel.on('tap', this._onSelectItem.bind(this));
+ // limit the scrollTop to the feasible scroll range
+ if (this.props.scrollTop > 0) this.props.scrollTop = 0;
+ if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin;
- // multi select when holding mouse/touch, or on ctrl+click
- this.rootPanel.on('hold', this._onMultiSelectItem.bind(this));
+ return this.props.scrollTop;
+};
- // add item on doubletap
- this.rootPanel.on('doubletap', this._onAddItem.bind(this));
+/**
+ * Get the current scrollTop
+ * @returns {number} scrollTop
+ * @private
+ */
+Timeline.prototype._getScrollTop = function () {
+ return this.props.scrollTop;
+};
- // 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.itemSet) {
- return me.itemSet.getLabelsWidth();
- }
- else {
- return 0;
- }
- },
- className: function () {
- return 'side' + (me.groupsData ? '' : ' hidden');
- }
- });
- 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);
+/**
+ * Create a timeline visualization
+ * @param {HTMLElement} container
+ * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
+ * @param {Object} [options] See Graph2d.setOptions for the available options.
+ * @constructor
+ */
+function Graph2d (container, items, options, groups) {
+ var me = this;
+ this.defaultOptions = {
+ start: null,
+ end: null,
- // range
- // TODO: move range inside rootPanel?
- var rangeOptions = Object.create(this.options);
- this.range = new Range(this.rootPanel, this.mainPanel, rangeOptions);
- this.range.setRange(
- now.clone().add('days', -3).valueOf(),
- now.clone().add('days', 4).valueOf()
- );
- this.range.on('rangechange', function (properties) {
- me.rootPanel.repaint();
- me.emit('rangechange', properties);
- });
- this.range.on('rangechanged', function (properties) {
- me.rootPanel.repaint();
- me.emit('rangechanged', properties);
- });
+ autoResize: true,
- // panel with time axis
- var timeAxisOptions = util.extend(Object.create(rootOptions), {
- range: this.range,
- left: null,
- top: null,
+ orientation: 'bottom',
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);
+ maxHeight: null,
+ minHeight: null
+ };
+ this.options = util.deepExtend({}, this.defaultOptions);
- // 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);
+ // Create the DOM, props, and emitter
+ this._create(container);
- // 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');
+ // all components listed here will be repainted automatically
+ this.components = [];
+
+ this.body = {
+ dom: this.dom,
+ domProps: this.props,
+ emitter: {
+ on: this.on.bind(this),
+ off: this.off.bind(this),
+ emit: this.emit.bind(this)
},
- left: null,
- right: null,
- height: null,
- width: null,
- className: 'side-content'
- });
- this.sideContentPanel = new Panel(sideContentOptions);
- this.sidePanel.appendChild(this.sideContentPanel);
+ util: {
+ snap: null, // will be specified after TimeAxis is created
+ toScreen: me._toScreen.bind(me),
+ toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width
+ toTime: me._toTime.bind(me),
+ toGlobalTime : me._toGlobalTime.bind(me)
+ }
+ };
+
+ // range
+ this.range = new Range(this.body);
+ this.components.push(this.range);
+ this.body.range = this.range;
+
+ // time axis
+ this.timeAxis = new TimeAxis(this.body);
+ this.components.push(this.timeAxis);
+ this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis);
// current time bar
- // Note: time bar will be attached in this.setOptions when selected
- this.currentTime = new CurrentTime(this.range, rootOptions);
+ this.currentTime = new CurrentTime(this.body);
+ this.components.push(this.currentTime);
// custom time bar
// 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.customTime = new CustomTime(this.body);
+ this.components.push(this.customTime);
- // itemset containing items and groups
- var itemOptions = util.extend(Object.create(this.options), {
- left: null,
- right: null,
- top: null,
- bottom: null,
- width: null,
- height: null
- });
- this.itemSet = new ItemSet(this.backgroundPanel, this.axisPanel, this.sideContentPanel, itemOptions);
- this.itemSet.setRange(this.range);
- this.itemSet.on('change', me.rootPanel.repaint.bind(me.rootPanel));
- this.contentPanel.appendChild(this.itemSet);
+ // item set
+ this.linegraph = new LineGraph(this.body);
+ this.components.push(this.linegraph);
this.itemsData = null; // DataSet
this.groupsData = null; // DataSet
@@ -7512,99 +10440,236 @@ function Timeline (container, items, options) {
this.setOptions(options);
}
+ // IMPORTANT: THIS HAPPENS BEFORE SET ITEMS!
+ if (groups) {
+ this.setGroups(groups);
+ }
+
// create itemset
if (items) {
this.setItems(items);
}
+ else {
+ this.redraw();
+ }
}
-// turn Timeline into an event emitter
-Emitter(Timeline.prototype);
+// turn Graph2d into an event emitter
+Emitter(Graph2d.prototype);
/**
- * Set options
- * @param {Object} options TODO: describe the available options
+ * Create the main DOM for the Graph2d: a root panel containing left, right,
+ * top, bottom, content, and background panel.
+ * @param {Element} container The container element where the Graph2d will
+ * be attached.
+ * @private
*/
-Timeline.prototype.setOptions = function (options) {
- util.deepExtend(this.options, options);
+Graph2d.prototype._create = function (container) {
+ this.dom = {};
- if ('editable' in options) {
- var isBoolean = typeof options.editable === 'boolean';
+ this.dom.root = document.createElement('div');
+ this.dom.background = document.createElement('div');
+ this.dom.backgroundVertical = document.createElement('div');
+ this.dom.backgroundHorizontalContainer = document.createElement('div');
+ this.dom.centerContainer = document.createElement('div');
+ this.dom.leftContainer = document.createElement('div');
+ this.dom.rightContainer = document.createElement('div');
+ this.dom.backgroundHorizontal = document.createElement('div');
+ this.dom.center = document.createElement('div');
+ this.dom.left = document.createElement('div');
+ this.dom.right = document.createElement('div');
+ this.dom.top = document.createElement('div');
+ this.dom.bottom = document.createElement('div');
+ this.dom.shadowTop = document.createElement('div');
+ this.dom.shadowBottom = document.createElement('div');
+ this.dom.shadowTopLeft = document.createElement('div');
+ this.dom.shadowBottomLeft = document.createElement('div');
+ this.dom.shadowTopRight = document.createElement('div');
+ this.dom.shadowBottomRight = document.createElement('div');
+
+ this.dom.background.className = 'vispanel background';
+ this.dom.backgroundVertical.className = 'vispanel background vertical';
+ this.dom.backgroundHorizontalContainer.className = 'vispanel background horizontal';
+ this.dom.backgroundHorizontal.className = 'vispanel background horizontal';
+ this.dom.centerContainer.className = 'vispanel center';
+ this.dom.leftContainer.className = 'vispanel left';
+ this.dom.rightContainer.className = 'vispanel right';
+ this.dom.top.className = 'vispanel top';
+ this.dom.bottom.className = 'vispanel bottom';
+ this.dom.left.className = 'content';
+ this.dom.center.className = 'content';
+ this.dom.right.className = 'content';
+ this.dom.shadowTop.className = 'shadow top';
+ this.dom.shadowBottom.className = 'shadow bottom';
+ this.dom.shadowTopLeft.className = 'shadow top';
+ this.dom.shadowBottomLeft.className = 'shadow bottom';
+ this.dom.shadowTopRight.className = 'shadow top';
+ this.dom.shadowBottomRight.className = 'shadow bottom';
+
+ this.dom.root.appendChild(this.dom.background);
+ this.dom.root.appendChild(this.dom.backgroundVertical);
+ this.dom.root.appendChild(this.dom.backgroundHorizontalContainer);
+ this.dom.root.appendChild(this.dom.centerContainer);
+ this.dom.root.appendChild(this.dom.leftContainer);
+ this.dom.root.appendChild(this.dom.rightContainer);
+ this.dom.root.appendChild(this.dom.top);
+ this.dom.root.appendChild(this.dom.bottom);
+
+ this.dom.backgroundHorizontalContainer.appendChild(this.dom.backgroundHorizontal);
+ this.dom.centerContainer.appendChild(this.dom.center);
+ this.dom.leftContainer.appendChild(this.dom.left);
+ this.dom.rightContainer.appendChild(this.dom.right);
+
+ this.dom.centerContainer.appendChild(this.dom.shadowTop);
+ this.dom.centerContainer.appendChild(this.dom.shadowBottom);
+ this.dom.leftContainer.appendChild(this.dom.shadowTopLeft);
+ this.dom.leftContainer.appendChild(this.dom.shadowBottomLeft);
+ this.dom.rightContainer.appendChild(this.dom.shadowTopRight);
+ this.dom.rightContainer.appendChild(this.dom.shadowBottomRight);
+
+ this.on('rangechange', this.redraw.bind(this));
+ this.on('change', this.redraw.bind(this));
+ this.on('touch', this._onTouch.bind(this));
+ this.on('pinch', this._onPinch.bind(this));
+ this.on('dragstart', this._onDragStart.bind(this));
+ this.on('drag', this._onDrag.bind(this));
+
+ // create event listeners for all interesting events, these events will be
+ // emitted via emitter
+ this.hammer = Hammer(this.dom.root, {
+ prevent_default: true
+ });
+ this.listeners = {};
- this.options.editable = {
- updateTime: isBoolean ? options.editable : (options.editable.updateTime || false),
- updateGroup: isBoolean ? options.editable : (options.editable.updateGroup || false),
- add: isBoolean ? options.editable : (options.editable.add || false),
- remove: isBoolean ? options.editable : (options.editable.remove || false)
+ var me = this;
+ var events = [
+ 'touch', 'pinch',
+ 'tap', 'doubletap', 'hold',
+ 'dragstart', 'drag', 'dragend',
+ 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is needed 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;
+ });
- // force update of range (apply new min/max etc.)
- // both start and end are optional
- this.range.setRange(options.start, options.end);
+ // size properties of each of the panels
+ this.props = {
+ root: {},
+ background: {},
+ centerContainer: {},
+ leftContainer: {},
+ rightContainer: {},
+ center: {},
+ left: {},
+ right: {},
+ top: {},
+ bottom: {},
+ border: {},
+ scrollTop: 0,
+ scrollTopMin: 0
+ };
+ this.touch = {}; // store state information needed for touch events
- if ('editable' in options || 'selectable' in options) {
- if (this.options.selectable) {
- // force update of selection
- this.setSelection(this.getSelection());
- }
- else {
- // remove selection
- this.setSelection([]);
- }
- }
+ // attach the root panel to the provided container
+ if (!container) throw new Error('No container provided');
+ container.appendChild(this.dom.root);
+};
- // force the itemSet to refresh: options like orientation and margins may be changed
- this.itemSet.markDirty();
+/**
+ * Destroy the Graph2d, clean up all DOM elements and event listeners.
+ */
+Graph2d.prototype.destroy = function () {
+ // unbind datasets
+ this.clear();
- // 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);
+ // remove all event listeners
+ this.off();
- // 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);
- }
+ // stop checking for changed size
+ this._stopAutoResize();
+
+ // remove from DOM
+ if (this.dom.root.parentNode) {
+ this.dom.root.parentNode.removeChild(this.dom.root);
}
+ this.dom = null;
- // add/remove the custom time bar
- if (this.options.showCustomTime) {
- if (!this.mainPanel.hasChild(this.customTime)) {
- this.mainPanel.appendChild(this.customTime);
+ // cleanup hammer touch events
+ for (var event in this.listeners) {
+ if (this.listeners.hasOwnProperty(event)) {
+ delete this.listeners[event];
}
}
- else {
- if (this.mainPanel.hasChild(this.customTime)) {
- this.mainPanel.removeChild(this.customTime);
- }
+ this.listeners = null;
+ this.hammer = null;
+
+ // give all components the opportunity to cleanup
+ this.components.forEach(function (component) {
+ component.destroy();
+ });
+
+ this.body = null;
+};
+
+/**
+ * Set options. Options will be passed to all components loaded in the Graph2d.
+ * @param {Object} [options]
+ * {String} orientation
+ * Vertical orientation for the Graph2d,
+ * can be 'bottom' (default) or 'top'.
+ * {String | Number} width
+ * Width for the timeline, a number in pixels or
+ * a css string like '1000px' or '75%'. '100%' by default.
+ * {String | Number} height
+ * Fixed height for the Graph2d, a number in pixels or
+ * a css string like '400px' or '75%'. If undefined,
+ * The Graph2d will automatically size such that
+ * its contents fit.
+ * {String | Number} minHeight
+ * Minimum height for the Graph2d, a number in pixels or
+ * a css string like '400px' or '75%'.
+ * {String | Number} maxHeight
+ * Maximum height for the Graph2d, a number in pixels or
+ * a css string like '400px' or '75%'.
+ * {Number | Date | String} start
+ * Start date for the visible window
+ * {Number | Date | String} end
+ * End date for the visible window
+ */
+Graph2d.prototype.setOptions = function (options) {
+ if (options) {
+ // copy the known options
+ var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation'];
+ util.selectiveExtend(fields, this.options, options);
+
+ // enable/disable autoResize
+ this._initAutoResize();
}
+ // propagate options to all components
+ this.components.forEach(function (component) {
+ component.setOptions(options);
+ });
+
// TODO: remove deprecation error one day (deprecated since version 0.8.0)
if (options && options.order) {
throw new Error('Option order is deprecated. There is no replacement for this feature.');
}
- // repaint everything
- this.rootPanel.repaint();
+ // redraw everything
+ this.redraw();
};
/**
* Set a custom time bar
* @param {Date} time
*/
-Timeline.prototype.setCustomTime = function (time) {
+Graph2d.prototype.setCustomTime = function (time) {
if (!this.customTime) {
throw new Error('Cannot get custom time: Custom time bar is not enabled');
}
@@ -7616,7 +10681,7 @@ Timeline.prototype.setCustomTime = function (time) {
* Retrieve the current custom time.
* @return {Date} customTime
*/
-Timeline.prototype.getCustomTime = function() {
+Graph2d.prototype.getCustomTime = function() {
if (!this.customTime) {
throw new Error('Cannot get custom time: Custom time bar is not enabled');
}
@@ -7628,7 +10693,7 @@ Timeline.prototype.getCustomTime = function() {
* Set items
* @param {vis.DataSet | Array | google.visualization.DataTable | null} items
*/
-Timeline.prototype.setItems = function(items) {
+Graph2d.prototype.setItems = function(items) {
var initialLoad = (this.itemsData == null);
// convert to type DataSet when needed
@@ -7642,7 +10707,7 @@ Timeline.prototype.setItems = function(items) {
else {
// turn an array into a dataset
newDataSet = new DataSet(items, {
- convert: {
+ type: {
start: 'Date',
end: 'Date'
}
@@ -7651,13 +10716,13 @@ Timeline.prototype.setItems = function(items) {
// set items
this.itemsData = newDataSet;
- this.itemSet.setItems(newDataSet);
+ this.linegraph && this.linegraph.setItems(newDataSet);
- if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
+ if (initialLoad && ('start' in this.options || 'end' in this.options)) {
this.fit();
- var start = (this.options.start != undefined) ? util.convert(this.options.start, 'Date') : null;
- var end = (this.options.end != undefined) ? util.convert(this.options.end, 'Date') : null;
+ var start = ('start' in this.options) ? util.convert(this.options.start, 'Date') : null;
+ var end = ('end' in this.options) ? util.convert(this.options.end, 'Date') : null;
this.setWindow(start, end);
}
@@ -7667,7 +10732,7 @@ Timeline.prototype.setItems = function(items) {
* Set groups
* @param {vis.DataSet | Array | google.visualization.DataTable} groups
*/
-Timeline.prototype.setGroups = function setGroups(groups) {
+Graph2d.prototype.setGroups = function(groups) {
// convert to type DataSet when needed
var newDataSet;
if (!groups) {
@@ -7682,11 +10747,11 @@ Timeline.prototype.setGroups = function setGroups(groups) {
}
this.groupsData = newDataSet;
- this.itemSet.setGroups(newDataSet);
+ this.linegraph.setGroups(newDataSet);
};
/**
- * Clear the Timeline. By Default, items, groups and options are cleared.
+ * Clear the Graph2d. By Default, items, groups and options are cleared.
* Example usage:
*
* timeline.clear(); // clear items, groups, and options
@@ -7695,7 +10760,7 @@ Timeline.prototype.setGroups = function setGroups(groups) {
* @param {Object} [what] Optionally specify what to clear. By default:
* {items: true, groups: true, options: true}
*/
-Timeline.prototype.clear = function clear(what) {
+Graph2d.prototype.clear = function(what) {
// clear items
if (!what || what.items) {
this.setItems(null);
@@ -7706,16 +10771,20 @@ Timeline.prototype.clear = function clear(what) {
this.setGroups(null);
}
- // clear options
+ // clear options of timeline and of each of the components
if (!what || what.options) {
- this.setOptions(this.defaultOptions);
+ this.components.forEach(function (component) {
+ component.setOptions(component.defaultOptions);
+ });
+
+ this.setOptions(this.defaultOptions); // this will also do a redraw
}
};
/**
- * Set Timeline window such that it fits all items
+ * Set Graph2d window such that it fits all items
*/
-Timeline.prototype.fit = function fit() {
+Graph2d.prototype.fit = function() {
// apply the data range as range
var dataRange = this.getItemRange();
@@ -7746,29 +10815,31 @@ Timeline.prototype.fit = function fit() {
* When no minimum is found, min==null
* When no maximum is found, max==null
*/
-Timeline.prototype.getItemRange = function getItemRange() {
+Graph2d.prototype.getItemRange = function() {
// calculate min from start filed
var itemsData = this.itemsData,
- min = null,
- max = null;
+ min = null,
+ max = null;
if (itemsData) {
// calculate the minimum value of the field 'start'
var minItem = itemsData.min('start');
- min = minItem ? minItem.start.valueOf() : null;
+ min = minItem ? util.convert(minItem.start, 'Date').valueOf() : null;
+ // Note: we convert first to Date and then to number because else
+ // a conversion from ISODate to Number will fail
// calculate maximum value of fields 'start' and 'end'
var maxStartItem = itemsData.max('start');
if (maxStartItem) {
- max = maxStartItem.start.valueOf();
+ max = util.convert(maxStartItem.start, 'Date').valueOf();
}
var maxEndItem = itemsData.max('end');
if (maxEndItem) {
if (max == null) {
- max = maxEndItem.end.valueOf();
+ max = util.convert(maxEndItem.end, 'Date').valueOf();
}
else {
- max = Math.max(max, maxEndItem.end.valueOf());
+ max = Math.max(max, util.convert(maxEndItem.end, 'Date').valueOf());
}
}
}
@@ -7779,25 +10850,6 @@ Timeline.prototype.getItemRange = function getItemRange() {
};
};
-/**
- * Set selected items by their id. Replaces the current selection
- * Unknown id's are silently ignored.
- * @param {Array} [ids] An array with zero or more id's of the items to be
- * selected. If ids is an empty array, all items will be
- * unselected.
- */
-Timeline.prototype.setSelection = function setSelection (ids) {
- this.itemSet.setSelection(ids);
-};
-
-/**
- * Get the selected items by their id
- * @return {Array} ids The ids of the selected items
- */
-Timeline.prototype.getSelection = function getSelection() {
- return this.itemSet.getSelection();
-};
-
/**
* Set the visible window. Both parameters are optional, you can change only
* start or only end. Syntax:
@@ -7811,7 +10863,7 @@ Timeline.prototype.getSelection = function getSelection() {
* @param {Date | Number | String | Object} [start] Start date of visible window
* @param {Date | Number | String} [end] End date of visible window
*/
-Timeline.prototype.setWindow = function setWindow(start, end) {
+Graph2d.prototype.setWindow = function(start, end) {
if (arguments.length == 1) {
var range = arguments[0];
this.range.setRange(range.start, range.end);
@@ -7825,7 +10877,7 @@ Timeline.prototype.setWindow = function setWindow(start, end) {
* Get the visible window
* @return {{start: Date, end: Date}} Visible range
*/
-Timeline.prototype.getWindow = function setWindow() {
+Graph2d.prototype.getWindow = function() {
var range = this.range.getRange();
return {
start: new Date(range.start),
@@ -7834,163 +10886,352 @@ Timeline.prototype.getWindow = function setWindow() {
};
/**
- * Force a redraw of the Timeline. Can be useful to manually redraw when
+ * Force a redraw of the Graph2d. Can be useful to manually redraw when
* option autoResize=false
*/
-Timeline.prototype.redraw = function redraw() {
- this.rootPanel.repaint();
-};
+Graph2d.prototype.redraw = function() {
+ var resized = false,
+ options = this.options,
+ props = this.props,
+ dom = this.dom;
-// TODO: deprecated since version 1.1.0, remove some day
-Timeline.prototype.repaint = function repaint() {
- throw new Error('Function repaint is deprecated. Use redraw instead.');
+ if (!dom) return; // when destroyed
+
+ // update class names
+ dom.root.className = 'vis timeline root ' + options.orientation;
+
+ // update root width and height options
+ dom.root.style.maxHeight = util.option.asSize(options.maxHeight, '');
+ dom.root.style.minHeight = util.option.asSize(options.minHeight, '');
+ dom.root.style.width = util.option.asSize(options.width, '');
+
+ // calculate border widths
+ props.border.left = (dom.centerContainer.offsetWidth - dom.centerContainer.clientWidth) / 2;
+ props.border.right = props.border.left;
+ props.border.top = (dom.centerContainer.offsetHeight - dom.centerContainer.clientHeight) / 2;
+ props.border.bottom = props.border.top;
+ var borderRootHeight= dom.root.offsetHeight - dom.root.clientHeight;
+ var borderRootWidth = dom.root.offsetWidth - dom.root.clientWidth;
+
+ // calculate the heights. If any of the side panels is empty, we set the height to
+ // minus the border width, such that the border will be invisible
+ props.center.height = dom.center.offsetHeight;
+ props.left.height = dom.left.offsetHeight;
+ props.right.height = dom.right.offsetHeight;
+ props.top.height = dom.top.clientHeight || -props.border.top;
+ props.bottom.height = dom.bottom.clientHeight || -props.border.bottom;
+
+ // TODO: compensate borders when any of the panels is empty.
+
+ // apply auto height
+ // TODO: only calculate autoHeight when needed (else we cause an extra reflow/repaint of the DOM)
+ var contentHeight = Math.max(props.left.height, props.center.height, props.right.height);
+ var autoHeight = props.top.height + contentHeight + props.bottom.height +
+ borderRootHeight + props.border.top + props.border.bottom;
+ dom.root.style.height = util.option.asSize(options.height, autoHeight + 'px');
+
+ // calculate heights of the content panels
+ props.root.height = dom.root.offsetHeight;
+ props.background.height = props.root.height - borderRootHeight;
+ var containerHeight = props.root.height - props.top.height - props.bottom.height -
+ borderRootHeight;
+ props.centerContainer.height = containerHeight;
+ props.leftContainer.height = containerHeight;
+ props.rightContainer.height = props.leftContainer.height;
+
+ // calculate the widths of the panels
+ props.root.width = dom.root.offsetWidth;
+ props.background.width = props.root.width - borderRootWidth;
+ props.left.width = dom.leftContainer.clientWidth || -props.border.left;
+ props.leftContainer.width = props.left.width;
+ props.right.width = dom.rightContainer.clientWidth || -props.border.right;
+ props.rightContainer.width = props.right.width;
+ var centerWidth = props.root.width - props.left.width - props.right.width - borderRootWidth;
+ props.center.width = centerWidth;
+ props.centerContainer.width = centerWidth;
+ props.top.width = centerWidth;
+ props.bottom.width = centerWidth;
+
+ // resize the panels
+ dom.background.style.height = props.background.height + 'px';
+ dom.backgroundVertical.style.height = props.background.height + 'px';
+ dom.backgroundHorizontalContainer.style.height = props.centerContainer.height + 'px';
+ dom.centerContainer.style.height = props.centerContainer.height + 'px';
+ dom.leftContainer.style.height = props.leftContainer.height + 'px';
+ dom.rightContainer.style.height = props.rightContainer.height + 'px';
+
+ dom.background.style.width = props.background.width + 'px';
+ dom.backgroundVertical.style.width = props.centerContainer.width + 'px';
+ dom.backgroundHorizontalContainer.style.width = props.background.width + 'px';
+ dom.backgroundHorizontal.style.width = props.background.width + 'px';
+ dom.centerContainer.style.width = props.center.width + 'px';
+ dom.top.style.width = props.top.width + 'px';
+ dom.bottom.style.width = props.bottom.width + 'px';
+
+ // reposition the panels
+ dom.background.style.left = '0';
+ dom.background.style.top = '0';
+ dom.backgroundVertical.style.left = props.left.width + 'px';
+ dom.backgroundVertical.style.top = '0';
+ dom.backgroundHorizontalContainer.style.left = '0';
+ dom.backgroundHorizontalContainer.style.top = props.top.height + 'px';
+ dom.centerContainer.style.left = props.left.width + 'px';
+ dom.centerContainer.style.top = props.top.height + 'px';
+ dom.leftContainer.style.left = '0';
+ dom.leftContainer.style.top = props.top.height + 'px';
+ dom.rightContainer.style.left = (props.left.width + props.center.width) + 'px';
+ dom.rightContainer.style.top = props.top.height + 'px';
+ dom.top.style.left = props.left.width + 'px';
+ dom.top.style.top = '0';
+ dom.bottom.style.left = props.left.width + 'px';
+ dom.bottom.style.top = (props.top.height + props.centerContainer.height) + 'px';
+
+ // update the scrollTop, feasible range for the offset can be changed
+ // when the height of the Graph2d or of the contents of the center changed
+ this._updateScrollTop();
+
+ // reposition the scrollable contents
+ var offset = this.props.scrollTop;
+ if (options.orientation == 'bottom') {
+ offset += Math.max(this.props.centerContainer.height - this.props.center.height -
+ this.props.border.top - this.props.border.bottom, 0);
+ }
+ dom.center.style.left = '0';
+ dom.center.style.top = offset + 'px';
+ dom.backgroundHorizontal.style.left = '0';
+ dom.backgroundHorizontal.style.top = offset + 'px';
+ dom.left.style.left = '0';
+ dom.left.style.top = offset + 'px';
+ dom.right.style.left = '0';
+ dom.right.style.top = offset + 'px';
+
+ // show shadows when vertical scrolling is available
+ var visibilityTop = this.props.scrollTop == 0 ? 'hidden' : '';
+ var visibilityBottom = this.props.scrollTop == this.props.scrollTopMin ? 'hidden' : '';
+ dom.shadowTop.style.visibility = visibilityTop;
+ dom.shadowBottom.style.visibility = visibilityBottom;
+ dom.shadowTopLeft.style.visibility = visibilityTop;
+ dom.shadowBottomLeft.style.visibility = visibilityBottom;
+ dom.shadowTopRight.style.visibility = visibilityTop;
+ dom.shadowBottomRight.style.visibility = visibilityBottom;
+
+ // redraw all components
+ this.components.forEach(function (component) {
+ resized = component.redraw() || resized;
+ });
+ if (resized) {
+ // keep redrawing until all sizes are settled
+ this.redraw();
+ }
};
/**
- * Handle selecting/deselecting an item when tapping it
- * @param {Event} event
+ * 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
*/
-// TODO: move this function to ItemSet
-Timeline.prototype._onSelectItem = function (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);
-
- var newSelection = this.getSelection();
-
- // emit a select event,
- // except when old selection is empty and new selection is still empty
- if (newSelection.length > 0 || oldSelection.length > 0) {
- this.emit('select', {
- items: this.getSelection()
- });
- }
+// TODO: move this function to Range
+Graph2d.prototype._toTime = function(x) {
+ var conversion = this.range.conversion(this.props.center.width);
+ return new Date(x / conversion.scale + conversion.offset);
+};
- event.stopPropagation();
+/**
+ * Convert a datetime (Date object) into a position on the root
+ * This is used to get the pixel density estimate for the screen, not the center panel
+ * @param {Date} time A date
+ * @return {int} x The position on root in pixels which corresponds
+ * with the given date.
+ * @private
+ */
+// TODO: move this function to Range
+Graph2d.prototype._toGlobalTime = function(x) {
+ var conversion = this.range.conversion(this.props.root.width);
+ return new Date(x / conversion.scale + conversion.offset);
};
/**
- * Handle creation and updates of an item on double tap
- * @param event
+ * 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._onAddItem = function (event) {
- if (!this.options.selectable) return;
- if (!this.options.editable.add) return;
+// TODO: move this function to Range
+Graph2d.prototype._toScreen = function(time) {
+ var conversion = this.range.conversion(this.props.center.width);
+ return (time.valueOf() - conversion.offset) * conversion.scale;
+};
- var me = this,
- item = ItemSet.itemFromTarget(event);
- if (item) {
- // update item
+/**
+ * Convert a datetime (Date object) into a position on the root
+ * This is used to get the pixel density estimate for the screen, not the center panel
+ * @param {Date} time A date
+ * @return {int} x The position on root in pixels which corresponds
+ * with the given date.
+ * @private
+ */
+// TODO: move this function to Range
+Graph2d.prototype._toGlobalScreen = function(time) {
+ var conversion = this.range.conversion(this.props.root.width);
+ return (time.valueOf() - conversion.offset) * conversion.scale;
+};
- // 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);
- }
- });
+/**
+ * Initialize watching when option autoResize is true
+ * @private
+ */
+Graph2d.prototype._initAutoResize = function () {
+ if (this.options.autoResize == true) {
+ this._startAutoResize();
}
else {
- // add item
- var xAbs = vis.util.getAbsoluteLeft(this.contentPanel.frame);
- var x = event.gesture.center.pageX - xAbs;
- var newItem = {
- start: this.timeAxis.snap(this._toTime(x)),
- content: 'new item'
- };
+ this._stopAutoResize();
+ }
+};
+
+/**
+ * Watch for changes in the size of the container. On resize, the Panel will
+ * automatically redraw itself.
+ * @private
+ */
+Graph2d.prototype._startAutoResize = function () {
+ var me = this;
- // when default type is a range, add a default end date to the new item
- if (this.options.type === 'range' || this.options.type == 'rangeoverflow') {
- newItem.end = this.timeAxis.snap(this._toTime(x + this.rootPanel.width / 5));
+ this._stopAutoResize();
+
+ this._onResize = function() {
+ if (me.options.autoResize != true) {
+ // stop watching when the option autoResize is changed to false
+ me._stopAutoResize();
+ return;
}
- var id = util.randomUUID();
- newItem[this.itemsData.fieldId] = id;
+ if (me.dom.root) {
+ // check whether the frame is resized
+ if ((me.dom.root.clientWidth != me.props.lastWidth) ||
+ (me.dom.root.clientHeight != me.props.lastHeight)) {
+ me.props.lastWidth = me.dom.root.clientWidth;
+ me.props.lastHeight = me.dom.root.clientHeight;
- var group = ItemSet.groupFromTarget(event);
- if (group) {
- newItem.group = group.groupId;
+ me.emit('change');
+ }
}
+ };
- // 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 redraw?
- }
- });
+ // add event listener to window resize
+ util.addEventListener(window, 'resize', this._onResize);
+
+ this.watchTimer = setInterval(this._onResize, 1000);
+};
+
+/**
+ * Stop watching for a resize of the frame.
+ * @private
+ */
+Graph2d.prototype._stopAutoResize = function () {
+ if (this.watchTimer) {
+ clearInterval(this.watchTimer);
+ this.watchTimer = undefined;
}
+
+ // remove event listener on window.resize
+ util.removeEventListener(window, 'resize', this._onResize);
+ this._onResize = null;
};
/**
- * Handle selecting/deselecting multiple items when holding an item
+ * Start moving the timeline vertically
* @param {Event} event
* @private
*/
-// TODO: move this function to ItemSet
-Timeline.prototype._onMultiSelectItem = function (event) {
- if (!this.options.selectable) return;
+Graph2d.prototype._onTouch = function (event) {
+ this.touch.allowDragging = true;
+};
- var selection,
- item = ItemSet.itemFromTarget(event);
+/**
+ * Start moving the timeline vertically
+ * @param {Event} event
+ * @private
+ */
+Graph2d.prototype._onPinch = function (event) {
+ this.touch.allowDragging = false;
+};
- if (item) {
- // multi select items
- selection = this.getSelection(); // current selection
- var index = selection.indexOf(item.id);
- if (index == -1) {
- // item is not yet selected -> select it
- selection.push(item.id);
- }
- else {
- // item is already selected -> deselect it
- selection.splice(index, 1);
- }
- this.setSelection(selection);
+/**
+ * Start moving the timeline vertically
+ * @param {Event} event
+ * @private
+ */
+Graph2d.prototype._onDragStart = function (event) {
+ this.touch.initialScrollTop = this.props.scrollTop;
+};
- this.emit('select', {
- items: this.getSelection()
- });
+/**
+ * Move the timeline vertically
+ * @param {Event} event
+ * @private
+ */
+Graph2d.prototype._onDrag = 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 (!this.touch.allowDragging) return;
- event.stopPropagation();
+ var delta = event.gesture.deltaY;
+
+ var oldScrollTop = this._getScrollTop();
+ var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta);
+
+ if (newScrollTop != oldScrollTop) {
+ this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already
}
};
/**
- * 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
+ * Apply a scrollTop
+ * @param {Number} scrollTop
+ * @returns {Number} scrollTop Returns the applied scrollTop
* @private
*/
-Timeline.prototype._toTime = function _toTime(x) {
- var conversion = this.range.conversion(this.mainPanel.width);
- return new Date(x / conversion.scale + conversion.offset);
+Graph2d.prototype._setScrollTop = function (scrollTop) {
+ this.props.scrollTop = scrollTop;
+ this._updateScrollTop();
+ return this.props.scrollTop;
};
/**
- * 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.
+ * Update the current scrollTop when the height of the containers has been changed
+ * @returns {Number} scrollTop Returns the applied scrollTop
* @private
*/
-Timeline.prototype._toScreen = function _toScreen(time) {
- var conversion = this.range.conversion(this.mainPanel.width);
- return (time.valueOf() - conversion.offset) * conversion.scale;
+Graph2d.prototype._updateScrollTop = function () {
+ // recalculate the scrollTopMin
+ var scrollTopMin = Math.min(this.props.centerContainer.height - this.props.center.height, 0); // is negative or zero
+ if (scrollTopMin != this.props.scrollTopMin) {
+ // in case of bottom orientation, change the scrollTop such that the contents
+ // do not move relative to the time axis at the bottom
+ if (this.options.orientation == 'bottom') {
+ this.props.scrollTop += (scrollTopMin - this.props.scrollTopMin);
+ }
+ this.props.scrollTopMin = scrollTopMin;
+ }
+
+ // limit the scrollTop to the feasible scroll range
+ if (this.props.scrollTop > 0) this.props.scrollTop = 0;
+ if (this.props.scrollTop < scrollTopMin) this.props.scrollTop = scrollTopMin;
+
+ return this.props.scrollTop;
+};
+
+/**
+ * Get the current scrollTop
+ * @returns {number} scrollTop
+ * @private
+ */
+Graph2d.prototype._getScrollTop = function () {
+ return this.props.scrollTop;
};
(function(exports) {
@@ -8824,7 +12065,7 @@ Timeline.prototype._toScreen = function _toScreen(time) {
})(typeof util !== 'undefined' ? util : exports);
/**
- * Canvas shapes used by the Graph
+ * Canvas shapes used by Network
*/
if (typeof CanvasRenderingContext2D !== 'undefined') {
@@ -9066,9 +12307,9 @@ if (typeof CanvasRenderingContext2D !== 'undefined') {
* {string} image An image url
* {string} title An title text, can be HTML
* {anytype} group A group name or number
- * @param {Graph.Images} imagelist A list with images. Only needed
+ * @param {Network.Images} imagelist A list with images. Only needed
* when the node has an image
- * @param {Graph.Groups} grouplist A list with groups. Needed for
+ * @param {Network.Groups} grouplist A list with groups. Needed for
* retrieving group properties
* @param {Object} constants An object with default values for
* example for the color
@@ -9081,8 +12322,8 @@ function Node(properties, imagelist, grouplist, constants) {
this.edges = []; // all edges connected to this node
this.dynamicEdges = [];
this.reroutedEdges = {};
- this.group = constants.nodes.group;
+ this.group = constants.nodes.group;
this.fontSize = Number(constants.nodes.fontSize);
this.fontFace = constants.nodes.fontFace;
this.fontColor = constants.nodes.fontColor;
@@ -9134,9 +12375,9 @@ function Node(properties, imagelist, grouplist, constants) {
this.maxNodeSizeIncrements = constants.clustering.maxNodeSizeIncrements;
this.growthIndicator = 0;
- // variables to tell the node about the graph.
- this.graphScaleInv = 1;
- this.graphScale = 1;
+ // variables to tell the node about the network.
+ this.networkScaleInv = 1;
+ this.networkScale = 1;
this.canvasTopLeft = {"x": -300, "y": -300};
this.canvasBottomRight = {"x": 300, "y": 300};
this.parentEdgeId = null;
@@ -9576,7 +12817,7 @@ Node.prototype._drawImage = function (ctx) {
// draw the shade
if (this.clusterSize > 1) {
var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0);
- lineWidth *= this.graphScaleInv;
+ lineWidth *= this.networkScaleInv;
lineWidth = Math.min(0.2 * this.width,lineWidth);
ctx.globalAlpha = 0.5;
@@ -9626,14 +12867,14 @@ Node.prototype._drawBox = function (ctx) {
// draw the outer border
if (this.clusterSize > 1) {
ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.graphScaleInv;
+ ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.radius);
ctx.stroke();
}
ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.graphScaleInv;
+ ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
@@ -9675,14 +12916,14 @@ Node.prototype._drawDatabase = function (ctx) {
// draw the outer border
if (this.clusterSize > 1) {
ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.graphScaleInv;
+ ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
ctx.database(this.x - this.width/2 - 2*ctx.lineWidth, this.y - this.height*0.5 - 2*ctx.lineWidth, this.width + 4*ctx.lineWidth, this.height + 4*ctx.lineWidth);
ctx.stroke();
}
ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.graphScaleInv;
+ ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
ctx.fillStyle = this.selected ? this.color.highlight.background : this.hover ? this.color.hover.background : this.color.background;
@@ -9725,14 +12966,14 @@ Node.prototype._drawCircle = function (ctx) {
// draw the outer border
if (this.clusterSize > 1) {
ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.graphScaleInv;
+ ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
ctx.circle(this.x, this.y, this.radius+2*ctx.lineWidth);
ctx.stroke();
}
ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.graphScaleInv;
+ ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
ctx.fillStyle = this.selected ? this.color.highlight.background : this.hover ? this.color.hover.background : this.color.background;
@@ -9775,14 +13016,14 @@ Node.prototype._drawEllipse = function (ctx) {
// draw the outer border
if (this.clusterSize > 1) {
ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.graphScaleInv;
+ ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth);
ctx.stroke();
}
ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.graphScaleInv;
+ ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
ctx.fillStyle = this.selected ? this.color.highlight.background : this.hover ? this.color.hover.background : this.color.background;
@@ -9852,24 +13093,23 @@ Node.prototype._drawShape = function (ctx, shape) {
// draw the outer border
if (this.clusterSize > 1) {
ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.graphScaleInv;
+ ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
ctx[shape](this.x, this.y, this.radius + radiusMultiplier * ctx.lineWidth);
ctx.stroke();
}
ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.graphScaleInv;
+ ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
ctx.fillStyle = this.selected ? this.color.highlight.background : this.hover ? this.color.hover.background : this.color.background;
-
ctx[shape](this.x, this.y, this.radius);
ctx.fill();
ctx.stroke();
if (this.label) {
- this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top');
+ this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top',true);
}
};
@@ -9897,17 +13137,20 @@ Node.prototype._drawText = function (ctx) {
};
-Node.prototype._label = function (ctx, text, x, y, align, baseline) {
- if (text && this.fontSize * this.graphScale > this.fontDrawThreshold) {
+Node.prototype._label = function (ctx, text, x, y, align, baseline, labelUnderNode) {
+ if (text && this.fontSize * this.networkScale > this.fontDrawThreshold) {
ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
ctx.fillStyle = this.fontColor || "black";
ctx.textAlign = align || "center";
ctx.textBaseline = baseline || "middle";
- var lines = text.split('\n'),
- lineCount = lines.length,
- fontSize = (this.fontSize + 4),
- yLine = y + (1 - lineCount) / 2 * fontSize;
+ var lines = text.split('\n');
+ var lineCount = lines.length;
+ var fontSize = (this.fontSize + 4);
+ var yLine = y + (1 - lineCount) / 2 * fontSize;
+ if (labelUnderNode == true) {
+ yLine = y + (1 - lineCount) / (2 * fontSize);
+ }
for (var i = 0; i < lineCount; i++) {
ctx.fillText(lines[i], x, yLine);
@@ -9944,10 +13187,10 @@ Node.prototype.getTextSize = function(ctx) {
*/
Node.prototype.inArea = function() {
if (this.width !== undefined) {
- return (this.x + this.width *this.graphScaleInv >= this.canvasTopLeft.x &&
- this.x - this.width *this.graphScaleInv < this.canvasBottomRight.x &&
- this.y + this.height*this.graphScaleInv >= this.canvasTopLeft.y &&
- this.y - this.height*this.graphScaleInv < this.canvasBottomRight.y);
+ return (this.x + this.width *this.networkScaleInv >= this.canvasTopLeft.x &&
+ this.x - this.width *this.networkScaleInv < this.canvasBottomRight.x &&
+ this.y + this.height*this.networkScaleInv >= this.canvasTopLeft.y &&
+ this.y - this.height*this.networkScaleInv < this.canvasBottomRight.y);
}
else {
return true;
@@ -9966,7 +13209,7 @@ Node.prototype.inView = function() {
};
/**
- * This allows the zoom level of the graph to influence the rendering
+ * This allows the zoom level of the network to influence the rendering
* We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas
*
* @param scale
@@ -9974,21 +13217,21 @@ Node.prototype.inView = function() {
* @param canvasBottomRight
*/
Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) {
- this.graphScaleInv = 1.0/scale;
- this.graphScale = scale;
+ this.networkScaleInv = 1.0/scale;
+ this.networkScale = scale;
this.canvasTopLeft = canvasTopLeft;
this.canvasBottomRight = canvasBottomRight;
};
/**
- * This allows the zoom level of the graph to influence the rendering
+ * This allows the zoom level of the network to influence the rendering
*
* @param scale
*/
Node.prototype.setScale = function(scale) {
- this.graphScaleInv = 1.0/scale;
- this.graphScale = scale;
+ this.networkScaleInv = 1.0/scale;
+ this.networkScale = scale;
};
@@ -10027,16 +13270,16 @@ Node.prototype.updateVelocity = function(massBeforeClustering) {
* to (number), label (string, color (string),
* width (number), style (string),
* length (number), title (string)
- * @param {Graph} graph A graph object, used to find and edge to
+ * @param {Network} network A Network object, used to find and edge to
* nodes.
* @param {Object} constants An object with default values for
* example for the color
*/
-function Edge (properties, graph, constants) {
- if (!graph) {
- throw "No graph provided";
+function Edge (properties, network, constants) {
+ if (!network) {
+ throw "No network provided";
}
- this.graph = graph;
+ this.network = network;
// initialize constants
this.widthMin = constants.edges.widthMin;
@@ -10049,6 +13292,8 @@ function Edge (properties, graph, constants) {
this.style = constants.edges.style;
this.title = undefined;
this.width = constants.edges.width;
+ this.widthSelectionMultiplier = constants.edges.widthSelectionMultiplier;
+ this.widthSelected = this.width * this.widthSelectionMultiplier;
this.hoverWidth = constants.edges.hoverWidth;
this.value = undefined;
this.length = constants.physics.springLength;
@@ -10081,6 +13326,10 @@ function Edge (properties, graph, constants) {
this.lengthFixed = false;
this.setProperties(properties, constants);
+
+ this.controlNodesEnabled = false;
+ this.controlNodes = {from:null, to:null, positions:{}};
+ this.connectedNode = null;
}
/**
@@ -10114,6 +13363,8 @@ Edge.prototype.setProperties = function(properties, constants) {
if (properties.title !== undefined) {this.title = properties.title;}
if (properties.width !== undefined) {this.width = properties.width;}
+ if (properties.widthSelectionMultiplier !== undefined)
+ {this.widthSelectionMultiplier = properties.widthSelectionMultiplier;}
if (properties.hoverWidth !== undefined) {this.hoverWidth = properties.hoverWidth;}
if (properties.value !== undefined) {this.value = properties.value;}
if (properties.length !== undefined) {this.length = properties.length;
@@ -10139,6 +13390,7 @@ Edge.prototype.setProperties = function(properties, constants) {
else {
if (properties.color.color !== undefined) {this.color.color = properties.color.color;}
if (properties.color.highlight !== undefined) {this.color.highlight = properties.color.highlight;}
+ if (properties.color.hover !== undefined) {this.color.hover = properties.color.hover;}
}
}
@@ -10148,6 +13400,8 @@ Edge.prototype.setProperties = function(properties, constants) {
this.widthFixed = this.widthFixed || (properties.width !== undefined);
this.lengthFixed = this.lengthFixed || (properties.length !== undefined);
+ this.widthSelected = this.width * this.widthSelectionMultiplier;
+
// set draw method based on style
switch (this.style) {
case 'line': this.draw = this._drawLine; break;
@@ -10164,8 +13418,8 @@ Edge.prototype.setProperties = function(properties, constants) {
Edge.prototype.connect = function () {
this.disconnect();
- this.from = this.graph.nodes[this.fromId] || null;
- this.to = this.graph.nodes[this.toId] || null;
+ this.from = this.network.nodes[this.fromId] || null;
+ this.to = this.network.nodes[this.toId] || null;
this.connected = (this.from && this.to);
if (this.connected) {
@@ -10325,14 +13579,14 @@ Edge.prototype._drawLine = function(ctx) {
*/
Edge.prototype._getLineWidth = function() {
if (this.selected == true) {
- return Math.min(this.width * 2, this.widthMax)*this.graphScaleInv;
+ return Math.min(this.widthSelected, this.widthMax)*this.networkScaleInv;
}
else {
if (this.hover == true) {
- return Math.min(this.hoverWidth, this.widthMax)*this.graphScaleInv;
+ return Math.min(this.hoverWidth, this.widthMax)*this.networkScaleInv;
}
else {
- return this.width*this.graphScaleInv;
+ return this.width*this.networkScaleInv;
}
}
};
@@ -10740,56 +13994,77 @@ Edge.prototype._drawArrow = function(ctx) {
* @private
*/
Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
- if (this.smooth == true) {
- var minDistance = 1e9;
- var i,t,x,y,dx,dy;
- for (i = 0; i < 10; i++) {
- t = 0.1*i;
- x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*this.via.x + Math.pow(t,2)*x2;
- y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*this.via.y + Math.pow(t,2)*y2;
- dx = Math.abs(x3-x);
- dy = Math.abs(y3-y);
- minDistance = Math.min(minDistance,Math.sqrt(dx*dx + dy*dy));
- }
- return minDistance
- }
- else {
- var px = x2-x1,
- py = y2-y1,
- something = px*px + py*py,
- u = ((x3 - x1) * px + (y3 - y1) * py) / something;
-
- if (u > 1) {
- u = 1;
- }
- else if (u < 0) {
- u = 0;
+ if (this.from != this.to) {
+ 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);
+ }
+ }
+ else {
+ var x, y, dx, dy;
+ var radius = this.length / 4;
+ var node = this.from;
+ if (!node.width) {
+ node.resize(ctx);
+ }
+ if (node.width > node.height) {
+ x = node.x + node.width / 2;
+ y = node.y - radius;
+ }
+ else {
+ x = node.x + radius;
+ y = node.y - node.height / 2;
+ }
+ dx = x - x3;
+ dy = y - y3;
+ return Math.abs(Math.sqrt(dx*dx + dy*dy) - radius);
}
};
/**
- * This allows the zoom level of the graph to influence the rendering
+ * This allows the zoom level of the network to influence the rendering
*
* @param scale
*/
Edge.prototype.setScale = function(scale) {
- this.graphScaleInv = 1.0/scale;
+ this.networkScaleInv = 1.0/scale;
};
@@ -10807,6 +14082,149 @@ Edge.prototype.positionBezierNode = function() {
this.via.y = 0.5 * (this.from.y + this.to.y);
}
};
+
+/**
+ * This function draws the control nodes for the manipulator. In order to enable this, only set the this.controlNodesEnabled to true.
+ * @param ctx
+ */
+Edge.prototype._drawControlNodes = function(ctx) {
+ if (this.controlNodesEnabled == true) {
+ if (this.controlNodes.from === null && this.controlNodes.to === null) {
+ var nodeIdFrom = "edgeIdFrom:".concat(this.id);
+ var nodeIdTo = "edgeIdTo:".concat(this.id);
+ var constants = {
+ nodes:{group:'', radius:8},
+ physics:{damping:0},
+ clustering: {maxNodeSizeIncrements: 0 ,nodeScaling: {width:0, height: 0, radius:0}}
+ };
+ this.controlNodes.from = new Node(
+ {id:nodeIdFrom,
+ shape:'dot',
+ color:{background:'#ff4e00', border:'#3c3c3c', highlight: {background:'#07f968'}}
+ },{},{},constants);
+ this.controlNodes.to = new Node(
+ {id:nodeIdTo,
+ shape:'dot',
+ color:{background:'#ff4e00', border:'#3c3c3c', highlight: {background:'#07f968'}}
+ },{},{},constants);
+ }
+
+ if (this.controlNodes.from.selected == false && this.controlNodes.to.selected == false) {
+ this.controlNodes.positions = this.getControlNodePositions(ctx);
+ this.controlNodes.from.x = this.controlNodes.positions.from.x;
+ this.controlNodes.from.y = this.controlNodes.positions.from.y;
+ this.controlNodes.to.x = this.controlNodes.positions.to.x;
+ this.controlNodes.to.y = this.controlNodes.positions.to.y;
+ }
+
+ this.controlNodes.from.draw(ctx);
+ this.controlNodes.to.draw(ctx);
+ }
+ else {
+ this.controlNodes = {from:null, to:null, positions:{}};
+ }
+}
+
+/**
+ * Enable control nodes.
+ * @private
+ */
+Edge.prototype._enableControlNodes = function() {
+ this.controlNodesEnabled = true;
+}
+
+/**
+ * disable control nodes
+ * @private
+ */
+Edge.prototype._disableControlNodes = function() {
+ this.controlNodesEnabled = false;
+}
+
+/**
+ * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns null.
+ * @param x
+ * @param y
+ * @returns {null}
+ * @private
+ */
+Edge.prototype._getSelectedControlNode = function(x,y) {
+ var positions = this.controlNodes.positions;
+ var fromDistance = Math.sqrt(Math.pow(x - positions.from.x,2) + Math.pow(y - positions.from.y,2));
+ var toDistance = Math.sqrt(Math.pow(x - positions.to.x ,2) + Math.pow(y - positions.to.y ,2));
+
+ if (fromDistance < 15) {
+ this.connectedNode = this.from;
+ this.from = this.controlNodes.from;
+ return this.controlNodes.from;
+ }
+ else if (toDistance < 15) {
+ this.connectedNode = this.to;
+ this.to = this.controlNodes.to;
+ return this.controlNodes.to;
+ }
+ else {
+ return null;
+ }
+}
+
+
+/**
+ * this resets the control nodes to their original position.
+ * @private
+ */
+Edge.prototype._restoreControlNodes = function() {
+ if (this.controlNodes.from.selected == true) {
+ this.from = this.connectedNode;
+ this.connectedNode = null;
+ this.controlNodes.from.unselect();
+ }
+ if (this.controlNodes.to.selected == true) {
+ this.to = this.connectedNode;
+ this.connectedNode = null;
+ this.controlNodes.to.unselect();
+ }
+}
+
+/**
+ * this calculates the position of the control nodes on the edges of the parent nodes.
+ *
+ * @param ctx
+ * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}}
+ */
+Edge.prototype.getControlNodePositions = function(ctx) {
+ var 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 edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
+ var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
+ var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
+ var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
+ var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
+
+
+ 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;
+ }
+
+ return {from:{x:xFrom,y:yFrom},to:{x:xTo,y:yTo}};
+}
+
/**
* Popup is a class to create a popup window with some text
* @param {Element} container The container object.
@@ -10833,7 +14251,7 @@ function Popup(container, x, y, text, style) {
style = text;
text = undefined;
} else {
- // for backwards compatibility, in case clients other than Graph are creating Popup directly
+ // for backwards compatibility, in case clients other than Network are creating Popup directly
style = {
fontColor: 'black',
fontSize: 14, // px
@@ -11166,7 +14584,12 @@ var physicsMixin = {
this._calculateSpringForcesWithSupport();
}
else {
- this._calculateSpringForces();
+ if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
+ this._calculateHierarchicalSpringForces();
+ }
+ else {
+ this._calculateSpringForces();
+ }
}
},
@@ -11246,6 +14669,8 @@ var physicsMixin = {
},
+
+
/**
* this function calculates the effects of the springs in the case of unsmooth curves.
*
@@ -11292,6 +14717,8 @@ var physicsMixin = {
},
+
+
/**
* This function calculates the springforces on the nodes, accounting for the support nodes.
*
@@ -11368,7 +14795,7 @@ var physicsMixin = {
_loadPhysicsConfiguration: function () {
if (this.physicsConfiguration === undefined) {
this.backupConstants = {};
- util.copyObject(this.constants, this.backupConstants);
+ util.deepExtend(this.backupConstants,this.constants);
var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"];
this.physicsConfiguration = document.createElement('div');
@@ -11789,6 +15216,7 @@ var hierarchalRepulsionMixin = {
// repulsing forces between nodes
var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance;
var minimumDistance = nodeDistance;
+ var a = a_base / 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
@@ -11797,29 +15225,97 @@ var hierarchalRepulsionMixin = {
node1 = nodes[nodeIndices[i]];
for (j = i + 1; j < nodeIndices.length; j++) {
node2 = nodes[nodeIndices[j]];
+ if (node1.level == node2.level) {
- dx = node2.x - node1.x;
- dy = node2.y - node1.y;
- distance = Math.sqrt(dx * dx + dy * dy);
+ 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;
+ if (distance < 2 * minimumDistance) {
+ repulsingForce = a * distance + b;
+ var c = 0.05;
+ var d = 2 * minimumDistance * 2 * c;
+ repulsingForce = c * Math.pow(distance,2) - d * distance + d*d/(4*c);
+
+ // 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;
}
- fx = dx * repulsingForce;
- fy = dy * repulsingForce;
+ }
+ }
+ }
+ },
- node1.fx -= fx;
- node1.fy -= fy;
- node2.fx += fx;
- node2.fy += fy;
+
+ /**
+ * this function calculates the effects of the springs in the case of unsmooth curves.
+ *
+ * @private
+ */
+ _calculateHierarchicalSpringForces: function () {
+ var edgeLength, edge, edgeId;
+ var dx, dy, fx, fy, springForce, distance;
+ 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);
+ distance = Math.sqrt(dx * dx + dy * dy);
+
+ if (distance == 0) {
+ distance = 0.01;
+ }
+
+ distance = Math.max(0.8*edgeLength,Math.min(5*edgeLength, distance));
+
+ // the 1/distance is so the fx and fy can be calculated without sine or cosine.
+ springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
+
+ fx = dx * springForce;
+ fy = dy * springForce;
+
+ edge.to.fx -= fx;
+ edge.to.fy -= fy;
+ edge.from.fx += fx;
+ edge.from.fy += fy;
+
+
+ var factor = 5;
+ if (distance > edgeLength) {
+ factor = 25;
+ }
+
+ if (edge.from.level > edge.to.level) {
+ edge.to.fx -= factor*fx;
+ edge.to.fy -= factor*fy;
+ }
+ else if (edge.from.level < edge.to.level) {
+ edge.from.fx += factor*fx;
+ edge.from.fy += factor*fy;
+ }
+ }
}
}
}
@@ -12083,7 +15579,7 @@ var barnesHutMixin = {
* @private
*/
_splitBranch : function(parentBranch) {
- // if the branch is filled with a node, replace the node in the new subset.
+ // if the branch is shaded with a node, replace the node in the new subset.
var containedNode = null;
if (parentBranch.childrenCount == 1) {
containedNode = parentBranch.children.data;
@@ -12639,9 +16135,9 @@ var manipulationMixin = {
*/
_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");
+ var toolbar = document.getElementById("network-manipulationDiv");
+ var closeDiv = document.getElementById("network-manipulation-closeDiv");
+ var editModeDiv = document.getElementById("network-manipulation-editMode");
if (this.editMode == true) {
toolbar.style.display="block";
closeDiv.style.display="block";
@@ -12667,6 +16163,11 @@ var manipulationMixin = {
if (this.boundFunction) {
this.off('select', this.boundFunction);
}
+ if (this.edgeBeingEdited !== undefined) {
+ this.edgeBeingEdited._disableControlNodes();
+ this.edgeBeingEdited = undefined;
+ this.selectedControlNode = null;
+ }
// restore overloaded functions
this._restoreOverloadedFunctions();
@@ -12684,39 +16185,49 @@ var manipulationMixin = {
}
// add the icons to the manipulator div
this.manipulationDiv.innerHTML = "" +
- "" +
- ""+this.constants.labels['add'] +"" +
- "" +
- "" +
- ""+this.constants.labels['link'] +"";
+ "" +
+ ""+this.constants.labels['add'] +"" +
+ "" +
+ "" +
+ ""+this.constants.labels['link'] +"";
if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
this.manipulationDiv.innerHTML += "" +
- "" +
- "" +
- ""+this.constants.labels['editNode'] +"";
+ "" +
+ "" +
+ ""+this.constants.labels['editNode'] +"";
+ }
+ else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
+ this.manipulationDiv.innerHTML += "" +
+ "" +
+ "" +
+ ""+this.constants.labels['editEdge'] +"";
}
if (this._selectionIsEmpty() == false) {
this.manipulationDiv.innerHTML += "" +
- "" +
- "" +
- ""+this.constants.labels['del'] +"";
+ "" +
+ "" +
+ ""+this.constants.labels['del'] +"";
}
// bind the icons
- var addNodeButton = document.getElementById("graph-manipulate-addNode");
+ var addNodeButton = document.getElementById("network-manipulate-addNode");
addNodeButton.onclick = this._createAddNodeToolbar.bind(this);
- var addEdgeButton = document.getElementById("graph-manipulate-connectNode");
+ var addEdgeButton = document.getElementById("network-manipulate-connectNode");
addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this);
if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
- var editButton = document.getElementById("graph-manipulate-editNode");
+ var editButton = document.getElementById("network-manipulate-editNode");
editButton.onclick = this._editNode.bind(this);
}
+ else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) {
+ var editButton = document.getElementById("network-manipulate-editEdge");
+ editButton.onclick = this._createEditEdgeToolbar.bind(this);
+ }
if (this._selectionIsEmpty() == false) {
- var deleteButton = document.getElementById("graph-manipulate-delete");
+ var deleteButton = document.getElementById("network-manipulate-delete");
deleteButton.onclick = this._deleteSelected.bind(this);
}
- var closeDiv = document.getElementById("graph-manipulation-closeDiv");
+ var closeDiv = document.getElementById("network-manipulation-closeDiv");
closeDiv.onclick = this._toggleEditMode.bind(this);
this.boundFunction = this._createManipulatorBar.bind(this);
@@ -12724,9 +16235,9 @@ var manipulationMixin = {
}
else {
this.editModeDiv.innerHTML = "" +
- "" +
- "" + this.constants.labels['edit'] + "";
- var editModeButton = document.getElementById("graph-manipulate-editModeButton");
+ "" +
+ "" + this.constants.labels['edit'] + "";
+ var editModeButton = document.getElementById("network-manipulate-editModeButton");
editModeButton.onclick = this._toggleEditMode.bind(this);
}
},
@@ -12747,14 +16258,14 @@ var manipulationMixin = {
// create the toolbar contents
this.manipulationDiv.innerHTML = "" +
- "" +
- "" + this.constants.labels['back'] + " " +
- "" +
- "" +
- "" + this.constants.labels['addDescription'] + "";
+ "" +
+ "" + this.constants.labels['back'] + " " +
+ "" +
+ "" +
+ "" + this.constants.labels['addDescription'] + "";
// bind the icon
- var backButton = document.getElementById("graph-manipulate-back");
+ var backButton = document.getElementById("network-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.
@@ -12783,14 +16294,14 @@ var manipulationMixin = {
this.blockConnectingEdgeSelection = true;
this.manipulationDiv.innerHTML = "" +
- "" +
- "" + this.constants.labels['back'] + " " +
- "" +
- "" +
- "" + this.constants.labels['linkDescription'] + "";
+ "" +
+ "" + this.constants.labels['back'] + " " +
+ "" +
+ "" +
+ "" + this.constants.labels['linkDescription'] + "";
// bind the icon
- var backButton = document.getElementById("graph-manipulate-back");
+ var backButton = document.getElementById("network-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.
@@ -12805,9 +16316,105 @@ var manipulationMixin = {
// redraw to show the unselect
this._redraw();
+ },
+
+ /**
+ * create the toolbar to edit edges
+ *
+ * @private
+ */
+ _createEditEdgeToolbar : function() {
+ // clear the toolbar
+ this._clearManipulatorBar();
+
+ if (this.boundFunction) {
+ this.off('select', this.boundFunction);
+ }
+
+ this.edgeBeingEdited = this._getSelectedEdge();
+ this.edgeBeingEdited._enableControlNodes();
+
+ this.manipulationDiv.innerHTML = "" +
+ "" +
+ "" + this.constants.labels['back'] + " " +
+ "" +
+ "" +
+ "" + this.constants.labels['editEdgeDescription'] + "";
+
+ // bind the icon
+ var backButton = document.getElementById("network-manipulate-back");
+ backButton.onclick = this._createManipulatorBar.bind(this);
+
+ // temporarily overload functions
+ this.cachedFunctions["_handleTouch"] = this._handleTouch;
+ this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
+ this.cachedFunctions["_handleTap"] = this._handleTap;
+ this.cachedFunctions["_handleDragStart"] = this._handleDragStart;
+ this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
+ this._handleTouch = this._selectControlNode;
+ this._handleTap = function () {};
+ this._handleOnDrag = this._controlNodeDrag;
+ this._handleDragStart = function () {}
+ this._handleOnRelease = this._releaseControlNode;
+
+ // 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
+ */
+ _selectControlNode : function(pointer) {
+ this.edgeBeingEdited.controlNodes.from.unselect();
+ this.edgeBeingEdited.controlNodes.to.unselect();
+ this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y));
+ if (this.selectedControlNode !== null) {
+ this.selectedControlNode.select();
+ this.freezeSimulation = true;
+ }
+ 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
+ */
+ _controlNodeDrag : function(event) {
+ var pointer = this._getPointer(event.gesture.center);
+ if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) {
+ this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x);
+ this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y);
+ }
+ this._redraw();
},
+ _releaseControlNode : function(pointer) {
+ var newNode = this._getNodeAt(pointer);
+ if (newNode != null) {
+ if (this.edgeBeingEdited.controlNodes.from.selected == true) {
+ this._editEdge(newNode.id, this.edgeBeingEdited.to.id);
+ this.edgeBeingEdited.controlNodes.from.unselect();
+ }
+ if (this.edgeBeingEdited.controlNodes.to.selected == true) {
+ this._editEdge(this.edgeBeingEdited.from.id, newNode.id);
+ this.edgeBeingEdited.controlNodes.to.unselect();
+ }
+ }
+ else {
+ this.edgeBeingEdited._restoreControlNodes();
+ }
+ this.freezeSimulation = false;
+ this._redraw();
+ },
/**
* the function bound to the selection event. It checks if you want to connect a cluster and changes the description
@@ -12953,6 +16560,36 @@ var manipulationMixin = {
}
},
+ /**
+ * connect two nodes with a new edge.
+ *
+ * @private
+ */
+ _editEdge : function(sourceNodeId,targetNodeId) {
+ if (this.editMode == true) {
+ var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId};
+ if (this.triggerFunctions.editEdge) {
+ if (this.triggerFunctions.editEdge.length == 2) {
+ var me = this;
+ this.triggerFunctions.editEdge(defaultData, function(finalizedData) {
+ me.edgesData.update(finalizedData);
+ me.moving = true;
+ me.start();
+ });
+ }
+ else {
+ alert(this.constants.labels["linkError"]);
+ this.moving = true;
+ this.start();
+ }
+ }
+ else {
+ this.edgesData.update(defaultData);
+ this.moving = true;
+ this.start();
+ }
+ }
+ },
/**
* Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
@@ -12993,6 +16630,8 @@ var manipulationMixin = {
},
+
+
/**
* delete everything in the selection
*
@@ -13036,8 +16675,8 @@ var manipulationMixin = {
/**
* Creation of the SectorMixin var.
*
- * This contains all the functions the Graph object can use to employ the sector system.
- * The sector system is always used by Graph, though the benefits only apply to the use of clustering.
+ * This contains all the functions the Network object can use to employ the sector system.
+ * The sector system is always used by Network, though the benefits only apply to the use of clustering.
* If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges.
*
* Alex de Mulder
@@ -13046,7 +16685,7 @@ var manipulationMixin = {
var SectorMixin = {
/**
- * This function is only called by the setData function of the Graph object.
+ * This function is only called by the setData function of the Network object.
* This loads the global references into the active sector. This initializes the sector.
*
* @private
@@ -13398,7 +17037,7 @@ var SectorMixin = {
*
* @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
+ * | instead of the Network object
* @param {*} [argument] | Optional: arguments to pass to the runFunction
* @private
*/
@@ -13437,7 +17076,7 @@ var SectorMixin = {
*
* @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
+ * | instead of the Network object
* @param {*} [argument] | Optional: arguments to pass to the runFunction
* @private
*/
@@ -13466,7 +17105,7 @@ var SectorMixin = {
*
* @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
+ * | instead of the Network object
* @param {*} [argument] | Optional: arguments to pass to the runFunction
* @private
*/
@@ -13504,7 +17143,7 @@ var SectorMixin = {
*
* @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
+ * | instead of the Network object
* @param {*} [argument] | Optional: arguments to pass to the runFunction
* @private
*/
@@ -13588,7 +17227,7 @@ var SectorMixin = {
/**
* Creation of the ClusterMixin var.
*
- * This contains all the functions the Graph object can use to employ clustering
+ * This contains all the functions the Network object can use to employ clustering
*
* Alex de Mulder
* 21-01-2013
@@ -13596,7 +17235,7 @@ var SectorMixin = {
var ClusterMixin = {
/**
- * This is only called in the constructor of the graph object
+ * This is only called in the constructor of the network object
*
*/
startWithClustering : function() {
@@ -14081,7 +17720,7 @@ var ClusterMixin = {
},
/**
- * This function forces the graph to cluster all nodes with only one connecting edge to their
+ * This function forces the network to cluster all nodes with only one connecting edge to their
* connected node.
*
* @private
@@ -14972,7 +18611,7 @@ var SelectionMixin = {
},
/**
- * return the number of selected nodes
+ * return the selected node
*
* @returns {number}
* @private
@@ -14986,6 +18625,21 @@ var SelectionMixin = {
return null;
},
+ /**
+ * return the selected edge
+ *
+ * @returns {number}
+ * @private
+ */
+ _getSelectedEdge : function() {
+ for (var edgeId in this.selectionObj.edges) {
+ if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
+ return this.selectionObj.edges[edgeId];
+ }
+ }
+ return null;
+ },
+
/**
* return the number of selected edges
@@ -15118,10 +18772,13 @@ var SelectionMixin = {
* @param {Boolean} [doNotTrigger] | ignore trigger
* @private
*/
- _selectObject : function(object, append, doNotTrigger) {
+ _selectObject : function(object, append, doNotTrigger, highlightEdges) {
if (doNotTrigger === undefined) {
doNotTrigger = false;
}
+ if (highlightEdges === undefined) {
+ highlightEdges = true;
+ }
if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) {
this._unselectAll(true);
@@ -15130,7 +18787,7 @@ var SelectionMixin = {
if (object.selected == false) {
object.select();
this._addToSelection(object);
- if (object instanceof Node && this.blockConnectingEdgeSelection == false) {
+ if (object instanceof Node && this.blockConnectingEdgeSelection == false && highlightEdges == true) {
this._selectConnectedEdges(object);
}
}
@@ -15189,7 +18846,6 @@ var SelectionMixin = {
* @private
*/
_handleTouch : function(pointer) {
-
},
@@ -15336,10 +18992,67 @@ var SelectionMixin = {
}
this._selectObject(node,true,true);
}
+
+ console.log("setSelection is deprecated. Please use selectNodes instead.")
+
+ this.redraw();
+ },
+
+
+ /**
+ * select zero or more nodes with the option to highlight edges
+ * @param {Number[] | String[]} selection An array with the ids of the
+ * selected nodes.
+ * @param {boolean} [highlightEdges]
+ */
+ selectNodes : function(selection, highlightEdges) {
+ var i, iMax, id;
+
+ if (!selection || (selection.length == undefined))
+ throw 'Selection must be an array with ids';
+
+ // first unselect any selected node
+ this._unselectAll(true);
+
+ for (i = 0, iMax = selection.length; i < iMax; i++) {
+ id = selection[i];
+
+ var node = this.nodes[id];
+ if (!node) {
+ throw new RangeError('Node with id "' + id + '" not found');
+ }
+ this._selectObject(node,true,true,highlightEdges);
+ }
this.redraw();
},
+ /**
+ * select zero or more edges
+ * @param {Number[] | String[]} selection An array with the ids of the
+ * selected nodes.
+ */
+ selectEdges : function(selection) {
+ var i, iMax, id;
+
+ if (!selection || (selection.length == undefined))
+ throw 'Selection must be an array with ids';
+
+ // first unselect any selected node
+ this._unselectAll(true);
+
+ for (i = 0, iMax = selection.length; i < iMax; i++) {
+ id = selection[i];
+
+ var edge = this.edges[id];
+ if (!edge) {
+ throw new RangeError('Edge with id "' + id + '" not found');
+ }
+ this._selectObject(edge,true,true,highlightEdges);
+ }
+ this.redraw();
+ },
+
/**
* Validate the selection: remove ids of nodes which no longer exist
* @private
@@ -15372,7 +19085,7 @@ var NavigationMixin = {
_cleanNavigation : function() {
// clean up previosu navigation items
- var wrapper = document.getElementById('graph-navigation_wrapper');
+ var wrapper = document.getElementById('network-navigation_wrapper');
if (wrapper != null) {
this.containerElement.removeChild(wrapper);
}
@@ -15395,7 +19108,7 @@ var NavigationMixin = {
var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent'];
this.navigationDivs['wrapper'] = document.createElement('div');
- this.navigationDivs['wrapper'].id = "graph-navigation_wrapper";
+ this.navigationDivs['wrapper'].id = "network-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";
@@ -15403,8 +19116,8 @@ var NavigationMixin = {
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[navigationDivs[i]].id = "network-navigation_" + navigationDivs[i];
+ this.navigationDivs[navigationDivs[i]].className = "network-navigation " + navigationDivs[i];
this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]);
this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this);
}
@@ -15575,10 +19288,10 @@ var NavigationMixin = {
*/
-var graphMixinLoaders = {
+var networkMixinLoaders = {
/**
- * Load a mixin into the graph object
+ * Load a mixin into the network object
*
* @param {Object} sourceVariable | this object has to contain functions.
* @private
@@ -15586,14 +19299,14 @@ var graphMixinLoaders = {
_loadMixin: function (sourceVariable) {
for (var mixinFunction in sourceVariable) {
if (sourceVariable.hasOwnProperty(mixinFunction)) {
- Graph.prototype[mixinFunction] = sourceVariable[mixinFunction];
+ Network.prototype[mixinFunction] = sourceVariable[mixinFunction];
}
}
},
/**
- * removes a mixin from the graph object.
+ * removes a mixin from the network object.
*
* @param {Object} sourceVariable | this object has to contain functions.
* @private
@@ -15601,7 +19314,7 @@ var graphMixinLoaders = {
_clearMixin: function (sourceVariable) {
for (var mixinFunction in sourceVariable) {
if (sourceVariable.hasOwnProperty(mixinFunction)) {
- Graph.prototype[mixinFunction] = undefined;
+ Network.prototype[mixinFunction] = undefined;
}
}
},
@@ -15686,8 +19399,8 @@ var graphMixinLoaders = {
// 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';
+ this.manipulationDiv.className = 'network-manipulationDiv';
+ this.manipulationDiv.id = 'network-manipulationDiv';
if (this.editMode == true) {
this.manipulationDiv.style.display = "block";
}
@@ -15699,8 +19412,8 @@ var graphMixinLoaders = {
if (this.editModeDiv === undefined) {
this.editModeDiv = document.createElement('div');
- this.editModeDiv.className = 'graph-manipulation-editMode';
- this.editModeDiv.id = 'graph-manipulation-editMode';
+ this.editModeDiv.className = 'network-manipulation-editMode';
+ this.editModeDiv.id = 'network-manipulation-editMode';
if (this.editMode == true) {
this.editModeDiv.style.display = "none";
}
@@ -15712,8 +19425,8 @@ var graphMixinLoaders = {
if (this.closeDiv === undefined) {
this.closeDiv = document.createElement('div');
- this.closeDiv.className = 'graph-manipulation-closeDiv';
- this.closeDiv.id = 'graph-manipulation-closeDiv';
+ this.closeDiv.className = 'network-manipulation-closeDiv';
+ this.closeDiv.id = 'network-manipulation-closeDiv';
this.closeDiv.style.display = this.manipulationDiv.style.display;
this.containerElement.insertBefore(this.closeDiv, this.frame);
}
@@ -15771,17 +19484,20 @@ var graphMixinLoaders = {
};
/**
- * @constructor Graph
- * Create a graph visualization, displaying nodes and edges.
+ * @constructor Network
+ * Create a network visualization, displaying nodes and edges.
*
- * @param {Element} container The DOM element in which the Graph will
+ * @param {Element} container The DOM element in which the Network will
* be created. Normally a div element.
* @param {Object} data An object containing parameters
* {Array} nodes
* {Array} edges
* @param {Object} options Options
*/
-function Graph (container, data, options) {
+function Network (container, data, options) {
+ if (!(this instanceof Network)) {
+ throw new SyntaxError('Constructor must be called with the new operator');
+ }
this._initializeMixinLoaders();
@@ -15797,12 +19513,13 @@ function Graph (container, data, options) {
this.maxPhysicsTicksPerRender = 3; // max amount of physics ticks per render step.
this.physicsDiscreteStepsize = 0.50; // discrete stepsize of the simulation
- this.stabilize = true; // stabilize before displaying the graph
+ this.stabilize = true; // stabilize before displaying the network
this.selectable = true;
this.initializing = true;
// these functions are triggered when the dataset is edited
- this.triggerFunctions = {add:null,edit:null,connect:null,del:null};
+ this.triggerFunctions = {add:null,edit:null,editEdge:null,connect:null,del:null};
+
// set constant values
this.constants = {
@@ -15840,6 +19557,7 @@ function Graph (container, data, options) {
widthMin: 1,
widthMax: 15,
width: 1,
+ widthSelectionMultiplier: 2,
hoverWidth: 1.5,
style: 'line',
color: {
@@ -15878,8 +19596,8 @@ function Graph (container, data, options) {
},
hierarchicalRepulsion: {
enabled: false,
- centralGravity: 0.0,
- springLength: 100,
+ centralGravity: 0.5,
+ springLength: 150,
springConstant: 0.01,
nodeDistance: 60,
damping: 0.09
@@ -15938,9 +19656,11 @@ function Graph (container, data, options) {
link:"Add Link",
del:"Delete selected",
editNode:"Edit Node",
+ editEdge:"Edit Edge",
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.",
+ editEdgeDescription:"Click on the control points and drag them to a node to connect to it.",
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).",
@@ -15958,7 +19678,7 @@ function Graph (container, data, options) {
background: '#FFFFC6'
}
},
- dragGraph: true,
+ dragNetwork: true,
dragNodes: true,
zoomable: true,
hover: false
@@ -15967,11 +19687,11 @@ function Graph (container, data, options) {
// Node variables
- var graph = this;
+ var network = this;
this.groups = new Groups(); // object with groups
this.images = new Images(); // object with images
this.images.setOnloadCallback(function () {
- graph._redraw();
+ network._redraw();
});
// keyboard navigation variables
@@ -15984,13 +19704,13 @@ function Graph (container, data, options) {
this._loadPhysicsSystem();
// create a frame and canvas
this._create();
- // load the sector system. (mandatory, fully integrated with Graph)
+ // load the sector system. (mandatory, fully integrated with Network)
this._loadSectorSystem();
// 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)
+ // load the selection system. (mandatory, required by Network)
this._loadSelectionSystem();
- // load the selection system. (mandatory, required by Graph)
+ // load the selection system. (mandatory, required by Network)
this._loadHierarchySystem();
// apply options
@@ -16024,30 +19744,30 @@ function Graph (container, data, options) {
// create event listeners used to subscribe on the DataSets of the nodes and edges
this.nodesListeners = {
'add': function (event, params) {
- graph._addNodes(params.items);
- graph.start();
+ network._addNodes(params.items);
+ network.start();
},
'update': function (event, params) {
- graph._updateNodes(params.items);
- graph.start();
+ network._updateNodes(params.items);
+ network.start();
},
'remove': function (event, params) {
- graph._removeNodes(params.items);
- graph.start();
+ network._removeNodes(params.items);
+ network.start();
}
};
this.edgesListeners = {
'add': function (event, params) {
- graph._addEdges(params.items);
- graph.start();
+ network._addEdges(params.items);
+ network.start();
},
'update': function (event, params) {
- graph._updateEdges(params.items);
- graph.start();
+ network._updateEdges(params.items);
+ network.start();
},
'remove': function (event, params) {
- graph._removeEdges(params.items);
- graph.start();
+ network._removeEdges(params.items);
+ network.start();
}
};
@@ -16076,8 +19796,8 @@ function Graph (container, data, options) {
}
}
-// Extend Graph with an Emitter mixin
-Emitter(Graph.prototype);
+// Extend Network with an Emitter mixin
+Emitter(Network.prototype);
/**
* Get the script path where the vis.js library is located
@@ -16086,7 +19806,7 @@ Emitter(Graph.prototype);
* end with a slash.
* @private
*/
-Graph.prototype._getScriptPath = function() {
+Network.prototype._getScriptPath = function() {
var scripts = document.getElementsByTagName( 'script' );
// find script named vis.js or vis.min.js
@@ -16104,10 +19824,10 @@ Graph.prototype._getScriptPath = function() {
/**
- * Find the center position of the graph
+ * Find the center position of the network
* @private
*/
-Graph.prototype._getRange = function() {
+Network.prototype._getRange = function() {
var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
for (var nodeId in this.nodes) {
if (this.nodes.hasOwnProperty(nodeId)) {
@@ -16130,18 +19850,18 @@ Graph.prototype._getRange = function() {
* @returns {{x: number, y: number}}
* @private
*/
-Graph.prototype._findCenter = function(range) {
+Network.prototype._findCenter = function(range) {
return {x: (0.5 * (range.maxX + range.minX)),
y: (0.5 * (range.maxY + range.minY))};
};
/**
- * center the graph
+ * center the network
*
* @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
*/
-Graph.prototype._centerGraph = function(range) {
+Network.prototype._centerNetwork = function(range) {
var center = this._findCenter(range);
center.x *= this.scale;
@@ -16159,7 +19879,7 @@ Graph.prototype._centerGraph = function(range) {
* @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
* @param {Boolean} [disableStart] | If true, start is not called.
*/
-Graph.prototype.zoomExtent = function(initialZoom, disableStart) {
+Network.prototype.zoomExtent = function(initialZoom, disableStart) {
if (initialZoom === undefined) {
initialZoom = false;
}
@@ -16211,7 +19931,7 @@ Graph.prototype.zoomExtent = function(initialZoom, disableStart) {
this._setScale(zoomLevel);
- this._centerGraph(range);
+ this._centerNetwork(range);
if (disableStart == false) {
this.moving = true;
this.start();
@@ -16223,7 +19943,7 @@ Graph.prototype.zoomExtent = function(initialZoom, disableStart) {
* Update the this.nodeIndices with the most recent node index list
* @private
*/
-Graph.prototype._updateNodeIndexList = function() {
+Network.prototype._updateNodeIndexList = function() {
this._clearNodeIndexList();
for (var idx in this.nodes) {
if (this.nodes.hasOwnProperty(idx)) {
@@ -16243,7 +19963,7 @@ Graph.prototype._updateNodeIndexList = function() {
* {Options} [options] Object with options
* @param {Boolean} [disableStart] | optional: disable the calling of the start function.
*/
-Graph.prototype.setData = function(data, disableStart) {
+Network.prototype.setData = function(data, disableStart) {
if (disableStart === undefined) {
disableStart = false;
}
@@ -16289,7 +20009,7 @@ Graph.prototype.setData = function(data, disableStart) {
* @param {Object} options
* @param {Boolean} [initializeView] | set zoom and translation to default.
*/
-Graph.prototype.setOptions = function (options) {
+Network.prototype.setOptions = function (options) {
if (options) {
var prop;
// retrieve parameter values
@@ -16301,11 +20021,16 @@ Graph.prototype.setOptions = function (options) {
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.dragGraph !== undefined) {this.constants.dragGraph = options.dragGraph;}
+ if (options.dragNetwork !== undefined) {this.constants.dragNetwork = options.dragNetwork;}
if (options.dragNodes !== undefined) {this.constants.dragNodes = options.dragNodes;}
if (options.zoomable !== undefined) {this.constants.zoomable = options.zoomable;}
if (options.hover !== undefined) {this.constants.hover = options.hover;}
+ // TODO: deprecated since version 3.0.0. Cleanup some day
+ if (options.dragGraph !== undefined) {
+ throw new Error('Option dragGraph is renamed to dragNetwork');
+ }
+
if (options.labels !== undefined) {
for (prop in options.labels) {
if (options.labels.hasOwnProperty(prop)) {
@@ -16322,6 +20047,10 @@ Graph.prototype.setOptions = function (options) {
this.triggerFunctions.edit = options.onEdit;
}
+ if (options.onEditEdge) {
+ this.triggerFunctions.editEdge = options.onEditEdge;
+ }
+
if (options.onConnect) {
this.triggerFunctions.connect = options.onConnect;
}
@@ -16528,24 +20257,24 @@ Graph.prototype.setOptions = function (options) {
};
/**
- * Create the main frame for the Graph.
- * This function is executed once when a Graph object is created. The frame
+ * Create the main frame for the Network.
+ * This function is executed once when a Network object is created. The frame
* contains a canvas, and this canvas contains all objects like the axis and
* nodes.
* @private
*/
-Graph.prototype._create = function () {
+Network.prototype._create = function () {
// remove all elements from the container element.
while (this.containerElement.hasChildNodes()) {
this.containerElement.removeChild(this.containerElement.firstChild);
}
this.frame = document.createElement('div');
- this.frame.className = 'graph-frame';
+ this.frame.className = 'network-frame';
this.frame.style.position = 'relative';
this.frame.style.overflow = 'hidden';
- // create the graph canvas (HTML canvas element)
+ // create the network canvas (HTML canvas element)
this.frame.canvas = document.createElement( 'canvas' );
this.frame.canvas.style.position = 'relative';
this.frame.appendChild(this.frame.canvas);
@@ -16587,7 +20316,7 @@ Graph.prototype._create = function () {
* Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin
* @private
*/
-Graph.prototype._createKeyBinds = function() {
+Network.prototype._createKeyBinds = function() {
var me = this;
this.mousetrap = mousetrap;
@@ -16628,7 +20357,7 @@ Graph.prototype._createKeyBinds = function() {
* @return {{x: Number, y: Number}} pointer
* @private
*/
-Graph.prototype._getPointer = function (touch) {
+Network.prototype._getPointer = function (touch) {
return {
x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas),
y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas)
@@ -16640,7 +20369,7 @@ Graph.prototype._getPointer = function (touch) {
* @param event
* @private
*/
-Graph.prototype._onTouch = function (event) {
+Network.prototype._onTouch = function (event) {
this.drag.pointer = this._getPointer(event.gesture.center);
this.drag.pinched = false;
this.pinch.scale = this._getScale();
@@ -16652,7 +20381,7 @@ Graph.prototype._onTouch = function (event) {
* handle drag start event
* @private
*/
-Graph.prototype._onDragStart = function () {
+Network.prototype._onDragStart = function () {
this._handleDragStart();
};
@@ -16663,7 +20392,7 @@ Graph.prototype._onDragStart = function () {
*
* @private
*/
-Graph.prototype._handleDragStart = function() {
+Network.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
@@ -16709,7 +20438,7 @@ Graph.prototype._handleDragStart = function() {
* handle drag event
* @private
*/
-Graph.prototype._onDrag = function (event) {
+Network.prototype._onDrag = function (event) {
this._handleOnDrag(event)
};
@@ -16720,7 +20449,7 @@ Graph.prototype._onDrag = function (event) {
*
* @private
*/
-Graph.prototype._handleOnDrag = function(event) {
+Network.prototype._handleOnDrag = function(event) {
if (this.drag.pinched) {
return;
}
@@ -16755,8 +20484,8 @@ Graph.prototype._handleOnDrag = function(event) {
}
}
else {
- if (this.constants.dragGraph == true) {
- // move the graph
+ if (this.constants.dragNetwork == true) {
+ // move the network
var diffX = pointer.x - this.drag.pointer.x;
var diffY = pointer.y - this.drag.pointer.y;
@@ -16774,7 +20503,7 @@ Graph.prototype._handleOnDrag = function(event) {
* handle drag start event
* @private
*/
-Graph.prototype._onDragEnd = function () {
+Network.prototype._onDragEnd = function () {
this.drag.dragging = false;
var selection = this.drag.selection;
if (selection) {
@@ -16790,7 +20519,7 @@ Graph.prototype._onDragEnd = function () {
* handle tap/click event: select/unselect a node
* @private
*/
-Graph.prototype._onTap = function (event) {
+Network.prototype._onTap = function (event) {
var pointer = this._getPointer(event.gesture.center);
this.pointerPosition = pointer;
this._handleTap(pointer);
@@ -16802,7 +20531,7 @@ Graph.prototype._onTap = function (event) {
* handle doubletap event
* @private
*/
-Graph.prototype._onDoubleTap = function (event) {
+Network.prototype._onDoubleTap = function (event) {
var pointer = this._getPointer(event.gesture.center);
this._handleDoubleTap(pointer);
};
@@ -16812,7 +20541,7 @@ Graph.prototype._onDoubleTap = function (event) {
* handle long tap event: multi select nodes
* @private
*/
-Graph.prototype._onHold = function (event) {
+Network.prototype._onHold = function (event) {
var pointer = this._getPointer(event.gesture.center);
this.pointerPosition = pointer;
this._handleOnHold(pointer);
@@ -16823,7 +20552,7 @@ Graph.prototype._onHold = function (event) {
*
* @private
*/
-Graph.prototype._onRelease = function (event) {
+Network.prototype._onRelease = function (event) {
var pointer = this._getPointer(event.gesture.center);
this._handleOnRelease(pointer);
};
@@ -16833,7 +20562,7 @@ Graph.prototype._onRelease = function (event) {
* @param event
* @private
*/
-Graph.prototype._onPinch = function (event) {
+Network.prototype._onPinch = function (event) {
var pointer = this._getPointer(event.gesture.center);
this.drag.pinched = true;
@@ -16847,13 +20576,13 @@ Graph.prototype._onPinch = function (event) {
};
/**
- * Zoom the graph in or out
+ * Zoom the network in or out
* @param {Number} scale a number around 1, and between 0.01 and 10
* @param {{x: Number, y: Number}} pointer Position on screen
* @return {Number} appliedScale scale is limited within the boundaries
* @private
*/
-Graph.prototype._zoom = function(scale, pointer) {
+Network.prototype._zoom = function(scale, pointer) {
if (this.constants.zoomable == true) {
var scaleOld = this._getScale();
if (scale < 0.00001) {
@@ -16896,7 +20625,7 @@ Graph.prototype._zoom = function(scale, pointer) {
* @param {MouseEvent} event
* @private
*/
-Graph.prototype._onMouseWheel = function(event) {
+Network.prototype._onMouseWheel = function(event) {
// retrieve delta
var delta = 0;
if (event.wheelDelta) { /* IE/Opera. */
@@ -16938,7 +20667,7 @@ Graph.prototype._onMouseWheel = function(event) {
* @param {Event} event
* @private
*/
-Graph.prototype._onMouseMoveTitle = function (event) {
+Network.prototype._onMouseMoveTitle = function (event) {
var gesture = util.fakeGesture(this, event);
var pointer = this._getPointer(gesture.center);
@@ -16996,14 +20725,14 @@ Graph.prototype._onMouseMoveTitle = function (event) {
};
/**
- * Check if there is an element on the given position in the graph
+ * Check if there is an element on the given position in the network
* (a node or edge). If so, and if this element has a title,
* show a popup window with its title.
*
* @param {{x:Number, y:Number}} pointer
* @private
*/
-Graph.prototype._checkShowPopup = function (pointer) {
+Network.prototype._checkShowPopup = function (pointer) {
var obj = {
left: this._XconvertDOMtoCanvas(pointer.x),
top: this._YconvertDOMtoCanvas(pointer.y),
@@ -17073,7 +20802,7 @@ Graph.prototype._checkShowPopup = function (pointer) {
* @param {{x:Number, y:Number}} pointer
* @private
*/
-Graph.prototype._checkHidePopup = function (pointer) {
+Network.prototype._checkHidePopup = function (pointer) {
if (!this.popupObj || !this._getNodeAt(pointer) ) {
this.popupObj = undefined;
if (this.popup) {
@@ -17084,13 +20813,13 @@ Graph.prototype._checkHidePopup = function (pointer) {
/**
- * Set a new size for the graph
+ * Set a new size for the network
* @param {string} width Width in pixels or percentage (for example '800px'
* or '50%')
* @param {string} height Height in pixels or percentage (for example '400px'
* or '30%')
*/
-Graph.prototype.setSize = function(width, height) {
+Network.prototype.setSize = function(width, height) {
this.frame.style.width = width;
this.frame.style.height = height;
@@ -17114,11 +20843,11 @@ Graph.prototype.setSize = function(width, height) {
};
/**
- * Set a data set with nodes for the graph
+ * Set a data set with nodes for the network
* @param {Array | DataSet | DataView} nodes The data containing the nodes.
* @private
*/
-Graph.prototype._setNodes = function(nodes) {
+Network.prototype._setNodes = function(nodes) {
var oldNodesData = this.nodesData;
if (nodes instanceof DataSet || nodes instanceof DataView) {
@@ -17164,7 +20893,7 @@ Graph.prototype._setNodes = function(nodes) {
* @param {Number[] | String[]} ids
* @private
*/
-Graph.prototype._addNodes = function(ids) {
+Network.prototype._addNodes = function(ids) {
var id;
for (var i = 0, len = ids.length; i < len; i++) {
id = ids[i];
@@ -17196,7 +20925,7 @@ Graph.prototype._addNodes = function(ids) {
* @param {Number[] | String[]} ids
* @private
*/
-Graph.prototype._updateNodes = function(ids) {
+Network.prototype._updateNodes = function(ids) {
var nodes = this.nodes,
nodesData = this.nodesData;
for (var i = 0, len = ids.length; i < len; i++) {
@@ -17228,7 +20957,7 @@ Graph.prototype._updateNodes = function(ids) {
* @param {Number[] | String[]} ids
* @private
*/
-Graph.prototype._removeNodes = function(ids) {
+Network.prototype._removeNodes = function(ids) {
var nodes = this.nodes;
for (var i = 0, len = ids.length; i < len; i++) {
var id = ids[i];
@@ -17251,7 +20980,7 @@ Graph.prototype._removeNodes = function(ids) {
* @private
* @private
*/
-Graph.prototype._setEdges = function(edges) {
+Network.prototype._setEdges = function(edges) {
var oldEdgesData = this.edgesData;
if (edges instanceof DataSet || edges instanceof DataView) {
@@ -17298,7 +21027,7 @@ Graph.prototype._setEdges = function(edges) {
* @param {Number[] | String[]} ids
* @private
*/
-Graph.prototype._addEdges = function (ids) {
+Network.prototype._addEdges = function (ids) {
var edges = this.edges,
edgesData = this.edgesData;
@@ -17329,7 +21058,7 @@ Graph.prototype._addEdges = function (ids) {
* @param {Number[] | String[]} ids
* @private
*/
-Graph.prototype._updateEdges = function (ids) {
+Network.prototype._updateEdges = function (ids) {
var edges = this.edges,
edgesData = this.edgesData;
for (var i = 0, len = ids.length; i < len; i++) {
@@ -17364,7 +21093,7 @@ Graph.prototype._updateEdges = function (ids) {
* @param {Number[] | String[]} ids
* @private
*/
-Graph.prototype._removeEdges = function (ids) {
+Network.prototype._removeEdges = function (ids) {
var edges = this.edges;
for (var i = 0, len = ids.length; i < len; i++) {
var id = ids[i];
@@ -17391,7 +21120,7 @@ Graph.prototype._removeEdges = function (ids) {
* Reconnect all edges
* @private
*/
-Graph.prototype._reconnectEdges = function() {
+Network.prototype._reconnectEdges = function() {
var id,
nodes = this.nodes,
edges = this.edges;
@@ -17419,7 +21148,7 @@ Graph.prototype._reconnectEdges = function() {
* setValueRange(min, max).
* @private
*/
-Graph.prototype._updateValueRange = function(obj) {
+Network.prototype._updateValueRange = function(obj) {
var id;
// determine the range of the objects
@@ -17446,20 +21175,19 @@ Graph.prototype._updateValueRange = function(obj) {
};
/**
- * Redraw the graph with the current data
+ * Redraw the network with the current data
* chart will be resized too.
*/
-Graph.prototype.redraw = function() {
+Network.prototype.redraw = function() {
this.setSize(this.width, this.height);
-
this._redraw();
};
/**
- * Redraw the graph with the current data
+ * Redraw the network with the current data
* @private
*/
-Graph.prototype._redraw = function() {
+Network.prototype._redraw = function() {
var ctx = this.frame.canvas.getContext('2d');
// clear the canvas
var w = this.frame.canvas.width;
@@ -17483,6 +21211,7 @@ Graph.prototype._redraw = function() {
this._doInAllSectors("_drawAllSectorNodes",ctx);
this._doInAllSectors("_drawEdges",ctx);
this._doInAllSectors("_drawNodes",ctx,false);
+ this._doInAllSectors("_drawControlNodes",ctx);
// this._doInSupportSector("_drawNodes",ctx,true);
// this._drawTree(ctx,"#F00F0F");
@@ -17492,12 +21221,12 @@ Graph.prototype._redraw = function() {
};
/**
- * Set the translation of the graph
+ * Set the translation of the network
* @param {Number} offsetX Horizontal offset
* @param {Number} offsetY Vertical offset
* @private
*/
-Graph.prototype._setTranslation = function(offsetX, offsetY) {
+Network.prototype._setTranslation = function(offsetX, offsetY) {
if (this.translation === undefined) {
this.translation = {
x: 0,
@@ -17516,11 +21245,11 @@ Graph.prototype._setTranslation = function(offsetX, offsetY) {
};
/**
- * Get the translation of the graph
+ * Get the translation of the network
* @return {Object} translation An object with parameters x and y, both a number
* @private
*/
-Graph.prototype._getTranslation = function() {
+Network.prototype._getTranslation = function() {
return {
x: this.translation.x,
y: this.translation.y
@@ -17528,20 +21257,20 @@ Graph.prototype._getTranslation = function() {
};
/**
- * Scale the graph
+ * Scale the network
* @param {Number} scale Scaling factor 1.0 is unscaled
* @private
*/
-Graph.prototype._setScale = function(scale) {
+Network.prototype._setScale = function(scale) {
this.scale = scale;
};
/**
- * Get the current scale of the graph
+ * Get the current scale of the network
* @return {Number} scale Scaling factor 1.0 is unscaled
* @private
*/
-Graph.prototype._getScale = function() {
+Network.prototype._getScale = function() {
return this.scale;
};
@@ -17552,7 +21281,7 @@ Graph.prototype._getScale = function() {
* @returns {number}
* @private
*/
-Graph.prototype._XconvertDOMtoCanvas = function(x) {
+Network.prototype._XconvertDOMtoCanvas = function(x) {
return (x - this.translation.x) / this.scale;
};
@@ -17563,7 +21292,7 @@ Graph.prototype._XconvertDOMtoCanvas = function(x) {
* @returns {number}
* @private
*/
-Graph.prototype._XconvertCanvasToDOM = function(x) {
+Network.prototype._XconvertCanvasToDOM = function(x) {
return x * this.scale + this.translation.x;
};
@@ -17574,7 +21303,7 @@ Graph.prototype._XconvertCanvasToDOM = function(x) {
* @returns {number}
* @private
*/
-Graph.prototype._YconvertDOMtoCanvas = function(y) {
+Network.prototype._YconvertDOMtoCanvas = function(y) {
return (y - this.translation.y) / this.scale;
};
@@ -17585,7 +21314,7 @@ Graph.prototype._YconvertDOMtoCanvas = function(y) {
* @returns {number}
* @private
*/
-Graph.prototype._YconvertCanvasToDOM = function(y) {
+Network.prototype._YconvertCanvasToDOM = function(y) {
return y * this.scale + this.translation.y ;
};
@@ -17596,7 +21325,7 @@ Graph.prototype._YconvertCanvasToDOM = function(y) {
* @returns {{x: number, y: number}}
* @constructor
*/
-Graph.prototype.canvasToDOM = function(pos) {
+Network.prototype.canvasToDOM = function(pos) {
return {x:this._XconvertCanvasToDOM(pos.x),y:this._YconvertCanvasToDOM(pos.y)};
}
@@ -17606,7 +21335,7 @@ Graph.prototype.canvasToDOM = function(pos) {
* @returns {{x: number, y: number}}
* @constructor
*/
-Graph.prototype.DOMtoCanvas = function(pos) {
+Network.prototype.DOMtoCanvas = function(pos) {
return {x:this._XconvertDOMtoCanvas(pos.x),y:this._YconvertDOMtoCanvas(pos.y)};
}
@@ -17617,7 +21346,7 @@ Graph.prototype.DOMtoCanvas = function(pos) {
* @param {Boolean} [alwaysShow]
* @private
*/
-Graph.prototype._drawNodes = function(ctx,alwaysShow) {
+Network.prototype._drawNodes = function(ctx,alwaysShow) {
if (alwaysShow === undefined) {
alwaysShow = false;
}
@@ -17654,7 +21383,7 @@ Graph.prototype._drawNodes = function(ctx,alwaysShow) {
* @param {CanvasRenderingContext2D} ctx
* @private
*/
-Graph.prototype._drawEdges = function(ctx) {
+Network.prototype._drawEdges = function(ctx) {
var edges = this.edges;
for (var id in edges) {
if (edges.hasOwnProperty(id)) {
@@ -17667,11 +21396,26 @@ Graph.prototype._drawEdges = function(ctx) {
}
};
+/**
+ * Redraw all edges
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
+ * @param {CanvasRenderingContext2D} ctx
+ * @private
+ */
+Network.prototype._drawControlNodes = function(ctx) {
+ var edges = this.edges;
+ for (var id in edges) {
+ if (edges.hasOwnProperty(id)) {
+ edges[id]._drawControlNodes(ctx);
+ }
+ }
+};
+
/**
* Find a stable position for all nodes
* @private
*/
-Graph.prototype._stabilize = function() {
+Network.prototype._stabilize = function() {
if (this.constants.freezeForStabilization == true) {
this._freezeDefinedNodes();
}
@@ -17695,7 +21439,7 @@ Graph.prototype._stabilize = function() {
*
* @private
*/
-Graph.prototype._freezeDefinedNodes = function() {
+Network.prototype._freezeDefinedNodes = function() {
var nodes = this.nodes;
for (var id in nodes) {
if (nodes.hasOwnProperty(id)) {
@@ -17714,7 +21458,7 @@ Graph.prototype._freezeDefinedNodes = function() {
*
* @private
*/
-Graph.prototype._restoreFrozenNodes = function() {
+Network.prototype._restoreFrozenNodes = function() {
var nodes = this.nodes;
for (var id in nodes) {
if (nodes.hasOwnProperty(id)) {
@@ -17733,7 +21477,7 @@ Graph.prototype._restoreFrozenNodes = function() {
* @return {boolean} true if moving, false if non of the nodes is moving
* @private
*/
-Graph.prototype._isMoving = function(vmin) {
+Network.prototype._isMoving = function(vmin) {
var nodes = this.nodes;
for (var id in nodes) {
if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
@@ -17750,7 +21494,7 @@ Graph.prototype._isMoving = function(vmin) {
*
* @private
*/
-Graph.prototype._discreteStepNodes = function() {
+Network.prototype._discreteStepNodes = function() {
var interval = this.physicsDiscreteStepsize;
var nodes = this.nodes;
var nodeId;
@@ -17789,7 +21533,7 @@ Graph.prototype._discreteStepNodes = function() {
*
* @private
*/
-Graph.prototype._physicsTick = function() {
+Network.prototype._physicsTick = function() {
if (!this.freezeSimulation) {
if (this.moving) {
this._doInAllActiveSectors("_initializeForceCalculation");
@@ -17809,7 +21553,7 @@ Graph.prototype._physicsTick = function() {
*
* @private
*/
-Graph.prototype._animationStep = function() {
+Network.prototype._animationStep = function() {
// reset the timer so a new scheduled animation step can be set
this.timer = undefined;
// handle the keyboad movement
@@ -17823,13 +21567,11 @@ Graph.prototype._animationStep = function() {
var maxSteps = 1;
this._physicsTick();
var timeRequired = Date.now() - calculationTime;
- while (timeRequired < (this.renderTimestep - this.renderTime) && maxSteps < this.maxPhysicsTicksPerRender) {
+ while (timeRequired < 0.9*(this.renderTimestep - this.renderTime) && maxSteps < this.maxPhysicsTicksPerRender) {
this._physicsTick();
timeRequired = Date.now() - calculationTime;
maxSteps++;
-
}
-
// start the rendering process
var renderTime = Date.now();
this._redraw();
@@ -17844,7 +21586,7 @@ if (typeof window !== 'undefined') {
/**
* Schedule a animation step with the refreshrate interval.
*/
-Graph.prototype.start = function() {
+Network.prototype.start = function() {
if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) {
if (!this.timer) {
var ua = navigator.userAgent.toLowerCase();
@@ -17874,11 +21616,11 @@ Graph.prototype.start = function() {
/**
- * Move the graph according to the keyboard presses.
+ * Move the network according to the keyboard presses.
*
* @private
*/
-Graph.prototype._handleNavigation = function() {
+Network.prototype._handleNavigation = function() {
if (this.xIncrement != 0 || this.yIncrement != 0) {
var translation = this._getTranslation();
this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement);
@@ -17896,7 +21638,7 @@ Graph.prototype._handleNavigation = function() {
/**
* Freeze the _animationStep
*/
-Graph.prototype.toggleFreeze = function() {
+Network.prototype.toggleFreeze = function() {
if (this.freezeSimulation == false) {
this.freezeSimulation = true;
}
@@ -17913,7 +21655,7 @@ Graph.prototype.toggleFreeze = function() {
* @param {boolean} [disableStart]
* @private
*/
-Graph.prototype._configureSmoothCurves = function(disableStart) {
+Network.prototype._configureSmoothCurves = function(disableStart) {
if (disableStart === undefined) {
disableStart = true;
}
@@ -17945,7 +21687,7 @@ Graph.prototype._configureSmoothCurves = function(disableStart) {
*
* @private
*/
-Graph.prototype._createBezierNodes = function() {
+Network.prototype._createBezierNodes = function() {
if (this.constants.smoothCurves == true) {
for (var edgeId in this.edges) {
if (this.edges.hasOwnProperty(edgeId)) {
@@ -17974,10 +21716,10 @@ Graph.prototype._createBezierNodes = function() {
*
* @private
*/
-Graph.prototype._initializeMixinLoaders = function () {
- for (var mixinFunction in graphMixinLoaders) {
- if (graphMixinLoaders.hasOwnProperty(mixinFunction)) {
- Graph.prototype[mixinFunction] = graphMixinLoaders[mixinFunction];
+Network.prototype._initializeMixinLoaders = function () {
+ for (var mixinFunction in networkMixinLoaders) {
+ if (networkMixinLoaders.hasOwnProperty(mixinFunction)) {
+ Network.prototype[mixinFunction] = networkMixinLoaders[mixinFunction];
}
}
};
@@ -17985,14 +21727,14 @@ Graph.prototype._initializeMixinLoaders = function () {
/**
* Load the XY positions of the nodes into the dataset.
*/
-Graph.prototype.storePosition = function() {
+Network.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)) {
+ 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});
}
}
@@ -18007,7 +21749,7 @@ Graph.prototype.storePosition = function() {
* @param {Number} nodeId
* @param {Number} [zoomLevel]
*/
-Graph.prototype.focusOnNode = function (nodeId, zoomLevel) {
+Network.prototype.focusOnNode = function (nodeId, zoomLevel) {
if (this.nodes.hasOwnProperty(nodeId)) {
if (zoomLevel === undefined) {
zoomLevel = this._getScale();
@@ -18043,16 +21785,20 @@ Graph.prototype.focusOnNode = function (nodeId, zoomLevel) {
/**
* @constructor Graph3d
- * The Graph is a visualization Graphs on a time line
+ * Graph3d displays data in 3d.
*
- * Graph is developed in javascript as a Google Visualization Chart.
+ * Graph3d is developed in javascript as a Google Visualization Chart.
*
- * @param {Element} container The DOM element in which the Graph will
+ * @param {Element} container The DOM element in which the Graph3d will
* be created. Normally a div element.
* @param {DataSet | DataView | Array} [data]
* @param {Object} [options]
*/
function Graph3d(container, data, options) {
+ if (!(this instanceof Graph3d)) {
+ throw new SyntaxError('Constructor must be called with the new operator');
+ }
+
// create variables and set default values
this.containerElement = container;
this.width = '400px';
@@ -18125,7 +21871,7 @@ function Graph3d(container, data, options) {
}
}
-// Extend Graph with an Emitter mixin
+// Extend Graph3d with an Emitter mixin
Emitter(Graph3d.prototype);
/**
@@ -21352,40 +25098,57 @@ getMouseY = function(event) {
* vis.js module exports
*/
var vis = {
- util: util,
moment: moment,
+ util: util,
+ DOMutil: DOMutil,
+
DataSet: DataSet,
DataView: DataView,
- Range: Range,
- stack: stack,
- TimeStep: TimeStep,
-
- components: {
- items: {
- Item: Item,
- ItemBox: ItemBox,
- ItemPoint: ItemPoint,
- ItemRange: ItemRange
- },
- Component: Component,
- Panel: Panel,
- RootPanel: RootPanel,
- ItemSet: ItemSet,
- TimeAxis: TimeAxis
+ Timeline: Timeline,
+ Graph2d: Graph2d,
+ timeline: {
+ DataStep: DataStep,
+ Range: Range,
+ stack: stack,
+ TimeStep: TimeStep,
+
+ components: {
+ items: {
+ Item: Item,
+ ItemBox: ItemBox,
+ ItemPoint: ItemPoint,
+ ItemRange: ItemRange
+ },
+
+ Component: Component,
+ CurrentTime: CurrentTime,
+ CustomTime: CustomTime,
+ DataAxis: DataAxis,
+ GraphGroup: GraphGroup,
+ Group: Group,
+ ItemSet: ItemSet,
+ Legend: Legend,
+ LineGraph: LineGraph,
+ TimeAxis: TimeAxis
+ }
},
- graph: {
- Node: Node,
+ Network: Network,
+ network: {
Edge: Edge,
- Popup: Popup,
Groups: Groups,
- Images: Images
+ Images: Images,
+ Node: Node,
+ Popup: Popup
+ },
+
+ // Deprecated since v3.0.0
+ Graph: function () {
+ throw new Error('Graph is renamed to Network. Please create a graph as new vis.Network(...)');
},
- Timeline: Timeline,
- Graph: Graph,
Graph3d: Graph3d
};
@@ -23007,7 +26770,7 @@ else {
})(this);
},{}],4:[function(require,module,exports){
var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {};//! moment.js
-//! version : 2.6.0
+//! version : 2.7.0
//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
//! license : MIT
//! momentjs.com
@@ -23019,7 +26782,7 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ?
************************************/
var moment,
- VERSION = "2.6.0",
+ VERSION = "2.7.0",
// the global-scope this is NOT the global object in Node.js
globalScope = typeof global !== 'undefined' ? global : this,
oldGlobalMoment,
@@ -23044,6 +26807,7 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ?
_f : null,
_l : null,
_strict : null,
+ _tzm : null,
_isUTC : null,
_offset : null, // optional. Combine with _isUTC
_pf : null,
@@ -23152,6 +26916,16 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ?
// format function strings
formatFunctions = {},
+ // default relative time thresholds
+ relativeTimeThresholds = {
+ s: 45, //seconds to minutes
+ m: 45, //minutes to hours
+ h: 22, //hours to days
+ dd: 25, //days to month (month == 1)
+ dm: 45, //days to months (months > 1)
+ dy: 345 //days to year
+ },
+
// tokens to ordinalize and pad
ordinalizeTokens = 'DDD w W M D d'.split(' '),
paddedTokens = 'M D H h m s w W'.split(' '),
@@ -23291,6 +27065,16 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ?
lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'];
+ // Pick the first defined of two or three arguments. dfl comes from
+ // default.
+ function dfl(a, b, c) {
+ switch (arguments.length) {
+ case 2: return a != null ? a : b;
+ case 3: return a != null ? a : b != null ? b : c;
+ default: throw new Error("Implement me");
+ }
+ }
+
function defaultParsingFlags() {
// We need to deep clone this object, and es5 standard is not very
// helpful.
@@ -24159,30 +27943,86 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ?
config._useUTC = true;
config._tzm = timezoneMinutesFromString(input);
break;
+ // WEEKDAY - human
+ case 'dd':
+ case 'ddd':
+ case 'dddd':
+ a = getLangDefinition(config._l).weekdaysParse(input);
+ // if we didn't get a weekday name, mark the date as invalid
+ if (a != null) {
+ config._w = config._w || {};
+ config._w['d'] = a;
+ } else {
+ config._pf.invalidWeekday = input;
+ }
+ break;
+ // WEEK, WEEK DAY - numeric
case 'w':
case 'ww':
case 'W':
case 'WW':
case 'd':
- case 'dd':
- case 'ddd':
- case 'dddd':
case 'e':
case 'E':
token = token.substr(0, 1);
/* falls through */
- case 'gg':
case 'gggg':
- case 'GG':
case 'GGGG':
case 'GGGGG':
token = token.substr(0, 2);
if (input) {
config._w = config._w || {};
- config._w[token] = input;
+ config._w[token] = toInt(input);
}
break;
+ case 'gg':
+ case 'GG':
+ config._w = config._w || {};
+ config._w[token] = moment.parseTwoDigitYear(input);
+ }
+ }
+
+ function dayOfYearFromWeekInfo(config) {
+ var w, weekYear, week, weekday, dow, doy, temp, lang;
+
+ w = config._w;
+ if (w.GG != null || w.W != null || w.E != null) {
+ dow = 1;
+ doy = 4;
+
+ // TODO: We need to take the current isoWeekYear, but that depends on
+ // how we interpret now (local, utc, fixed offset). So create
+ // a now version of current config (take local/utc/offset flags, and
+ // create now).
+ weekYear = dfl(w.GG, config._a[YEAR], weekOfYear(moment(), 1, 4).year);
+ week = dfl(w.W, 1);
+ weekday = dfl(w.E, 1);
+ } else {
+ lang = getLangDefinition(config._l);
+ dow = lang._week.dow;
+ doy = lang._week.doy;
+
+ weekYear = dfl(w.gg, config._a[YEAR], weekOfYear(moment(), dow, doy).year);
+ week = dfl(w.w, 1);
+
+ if (w.d != null) {
+ // weekday -- low day numbers are considered next week
+ weekday = w.d;
+ if (weekday < dow) {
+ ++week;
+ }
+ } else if (w.e != null) {
+ // local weekday -- counting starts from begining of week
+ weekday = w.e + dow;
+ } else {
+ // default to begining of week
+ weekday = dow;
+ }
}
+ temp = dayOfYearFromWeeks(weekYear, week, weekday, doy, dow);
+
+ config._a[YEAR] = temp.year;
+ config._dayOfYear = temp.dayOfYear;
}
// convert an array to a date.
@@ -24190,8 +28030,7 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ?
// note: all values past the year are optional and will default to the lowest possible value.
// [year, month, day , hour, minute, second, millisecond]
function dateFromConfig(config) {
- var i, date, input = [], currentDate,
- yearToUse, fixYear, w, temp, lang, weekday, week;
+ var i, date, input = [], currentDate, yearToUse;
if (config._d) {
return;
@@ -24201,39 +28040,12 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ?
//compute day of the year from weeks and weekdays
if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
- fixYear = function (val) {
- var intVal = parseInt(val, 10);
- return val ?
- (val.length < 3 ? (intVal > 68 ? 1900 + intVal : 2000 + intVal) : intVal) :
- (config._a[YEAR] == null ? moment().weekYear() : config._a[YEAR]);
- };
-
- w = config._w;
- if (w.GG != null || w.W != null || w.E != null) {
- temp = dayOfYearFromWeeks(fixYear(w.GG), w.W || 1, w.E, 4, 1);
- }
- else {
- lang = getLangDefinition(config._l);
- weekday = w.d != null ? parseWeekday(w.d, lang) :
- (w.e != null ? parseInt(w.e, 10) + lang._week.dow : 0);
-
- week = parseInt(w.w, 10) || 1;
-
- //if we're parsing 'd', then the low day numbers may be next week
- if (w.d != null && weekday < lang._week.dow) {
- week++;
- }
-
- temp = dayOfYearFromWeeks(fixYear(w.gg), week, weekday, lang._week.doy, lang._week.dow);
- }
-
- config._a[YEAR] = temp.year;
- config._dayOfYear = temp.dayOfYear;
+ dayOfYearFromWeekInfo(config);
}
//if the day of the year is set, figure out what it is
if (config._dayOfYear) {
- yearToUse = config._a[YEAR] == null ? currentDate[YEAR] : config._a[YEAR];
+ yearToUse = dfl(config._a[YEAR], currentDate[YEAR]);
if (config._dayOfYear > daysInYear(yearToUse)) {
config._pf._overflowDayOfYear = true;
@@ -24258,11 +28070,12 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ?
config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
}
- // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
- input[HOUR] += toInt((config._tzm || 0) / 60);
- input[MINUTE] += toInt((config._tzm || 0) % 60);
-
config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input);
+ // Apply timezone offset from input. The actual zone can be changed
+ // with parseZone.
+ if (config._tzm != null) {
+ config._d.setUTCMinutes(config._d.getUTCMinutes() + config._tzm);
+ }
}
function dateFromObject(config) {
@@ -24302,6 +28115,11 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ?
// date from string and format string
function makeDateFromStringAndFormat(config) {
+ if (config._f === moment.ISO_8601) {
+ parseISO(config);
+ return;
+ }
+
config._a = [];
config._pf.empty = true;
@@ -24414,7 +28232,7 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ?
}
// date from iso format
- function makeDateFromString(config) {
+ function parseISO(config) {
var i, l,
string = config._i,
match = isoRegex.exec(string);
@@ -24438,8 +28256,16 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ?
config._f += "Z";
}
makeDateFromStringAndFormat(config);
+ } else {
+ config._isValid = false;
}
- else {
+ }
+
+ // date from iso format or fallback
+ function makeDateFromString(config) {
+ parseISO(config);
+ if (config._isValid === false) {
+ delete config._isValid;
moment.createFromInputFallback(config);
}
}
@@ -24520,15 +28346,15 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ?
hours = round(minutes / 60),
days = round(hours / 24),
years = round(days / 365),
- args = seconds < 45 && ['s', seconds] ||
+ args = seconds < relativeTimeThresholds.s && ['s', seconds] ||
minutes === 1 && ['m'] ||
- minutes < 45 && ['mm', minutes] ||
+ minutes < relativeTimeThresholds.m && ['mm', minutes] ||
hours === 1 && ['h'] ||
- hours < 22 && ['hh', hours] ||
+ hours < relativeTimeThresholds.h && ['hh', hours] ||
days === 1 && ['d'] ||
- days <= 25 && ['dd', days] ||
- days <= 45 && ['M'] ||
- days < 345 && ['MM', round(days / 30)] ||
+ days <= relativeTimeThresholds.dd && ['dd', days] ||
+ days <= relativeTimeThresholds.dm && ['M'] ||
+ days < relativeTimeThresholds.dy && ['MM', round(days / 30)] ||
years === 1 && ['y'] || ['yy', years];
args[2] = withoutSuffix;
args[3] = milliseconds > 0;
@@ -24574,6 +28400,7 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ?
function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) {
var d = makeUTCDate(year, 0, 1).getUTCDay(), daysToAdd, dayOfYear;
+ d = d === 0 ? 7 : d;
weekday = weekday != null ? weekday : firstDayOfWeek;
daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0);
dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1;
@@ -24649,6 +28476,40 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ?
config._d = new Date(config._i);
});
+ // Pick a moment m from moments so that m[fn](other) is true for all
+ // other. This relies on the function fn to be transitive.
+ //
+ // moments should either be an array of moment objects or an array, whose
+ // first element is an array of moment objects.
+ function pickBy(fn, moments) {
+ var res, i;
+ if (moments.length === 1 && isArray(moments[0])) {
+ moments = moments[0];
+ }
+ if (!moments.length) {
+ return moment();
+ }
+ res = moments[0];
+ for (i = 1; i < moments.length; ++i) {
+ if (moments[i][fn](res)) {
+ res = moments[i];
+ }
+ }
+ return res;
+ }
+
+ moment.min = function () {
+ var args = [].slice.call(arguments, 0);
+
+ return pickBy('isBefore', args);
+ };
+
+ moment.max = function () {
+ var args = [].slice.call(arguments, 0);
+
+ return pickBy('isAfter', args);
+ };
+
// creating with utc
moment.utc = function (input, format, lang, strict) {
var c;
@@ -24745,6 +28606,9 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ?
// default format
moment.defaultFormat = isoFormat;
+ // constant that refers to the ISO standard
+ moment.ISO_8601 = function () {};
+
// Plugins that add properties should also add the key here (null value),
// so we can properly clone ourselves.
moment.momentProperties = momentProperties;
@@ -24753,6 +28617,15 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ?
// It is intended to keep the offset in sync with the timezone.
moment.updateOffset = function () {};
+ // This function allows you to set a threshold for relative time strings
+ moment.relativeTimeThreshold = function(threshold, limit) {
+ if (relativeTimeThresholds[threshold] === undefined) {
+ return false;
+ }
+ relativeTimeThresholds[threshold] = limit;
+ return true;
+ };
+
// This function will load languages and then set the global language. If
// no arguments are passed in, it will simply return the current global
// language key.
@@ -24908,7 +28781,9 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ?
add : function (input, val) {
var dur;
// switch args to support add('s', 1) and add(1, 's')
- if (typeof input === 'string') {
+ if (typeof input === 'string' && typeof val === 'string') {
+ dur = moment.duration(isNaN(+val) ? +input : +val, isNaN(+val) ? val : input);
+ } else if (typeof input === 'string') {
dur = moment.duration(+val, input);
} else {
dur = moment.duration(input, val);
@@ -24920,7 +28795,9 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ?
subtract : function (input, val) {
var dur;
// switch args to support subtract('s', 1) and subtract(1, 's')
- if (typeof input === 'string') {
+ if (typeof input === 'string' && typeof val === 'string') {
+ dur = moment.duration(isNaN(+val) ? +input : +val, isNaN(+val) ? val : input);
+ } else if (typeof input === 'string') {
dur = moment.duration(+val, input);
} else {
dur = moment.duration(input, val);
@@ -24971,10 +28848,11 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ?
return this.from(moment(), withoutSuffix);
},
- calendar : function () {
+ calendar : function (time) {
// We want to compare the start of today, vs this.
// Getting start-of-today depends on whether we're zone'd or not.
- var sod = makeAs(moment(), this).startOf('day'),
+ var now = time || moment(),
+ sod = makeAs(now, this).startOf('day'),
diff = this.diff(sod, 'days', true),
format = diff < -6 ? 'sameElse' :
diff < -1 ? 'lastWeek' :
@@ -25069,15 +28947,21 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ?
return +this.clone().startOf(units) === +makeAs(input, this).startOf(units);
},
- min: function (other) {
- other = moment.apply(null, arguments);
- return other < this ? this : other;
- },
-
- max: function (other) {
- other = moment.apply(null, arguments);
- return other > this ? this : other;
- },
+ min: deprecate(
+ "moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",
+ function (other) {
+ other = moment.apply(null, arguments);
+ return other < this ? this : other;
+ }
+ ),
+
+ max: deprecate(
+ "moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",
+ function (other) {
+ other = moment.apply(null, arguments);
+ return other > this ? this : other;
+ }
+ ),
// keepTime = true means only change the timezone, without affecting
// the local hour. So 5:31:26 +0300 --[zone(2, true)]--> 5:31:26 +0200
diff --git a/dist/vis.min.css b/dist/vis.min.css
index 6f9c3d0e..519c4d10 100644
--- a/dist/vis.min.css
+++ b/dist/vis.min.css
@@ -1 +1 @@
-.vis.timeline.rootpanel{position:relative;overflow:hidden;border:1px solid #bfbfbf;box-sizing:border-box}.vis.timeline .vpanel{position:absolute;overflow:hidden;box-sizing:border-box}.vis.timeline .vpanel.side{border-right:1px solid #bfbfbf}.vis.timeline .vpanel.side.hidden{display:none}.vis.timeline .labelset{position:relative;width:100%;overflow:hidden;box-sizing:border-box}.vis.timeline .labelset .vlabel{position:relative;left:0;top:0;width:100%;color:#4d4d4d;box-sizing:border-box}.vis.timeline.top .labelset .vlabel{border-top:1px solid #bfbfbf;border-bottom:none}.vis.timeline.bottom .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;box-sizing:border-box}.vis.timeline .axis{overflow:visible}.vis.timeline .group{position:relative;box-sizing:border-box}.vis.timeline.top .group{border-top:1px solid #bfbfbf;border-bottom:none}.vis.timeline.bottom .group{border-top:none;border-bottom:1px solid #bfbfbf}.vis.timeline .item{position:absolute;color:#1A1A1A;border-color:#97B0F8;border-width:1px;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}.vis.timeline .item.box{text-align:center;border-style:solid;border-radius:2px}.vis.timeline .item.point{background:0 0}.vis.timeline .item.dot{position:absolute;padding:0;border-width:4px;border-style:solid;border-radius:4px}.vis.timeline .item.range,.vis.timeline .item.rangeoverflow{border-style:solid;border-radius:2px;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
+.vis.timeline.root{position:relative;border:1px solid #bfbfbf;overflow:hidden;padding:0;margin:0;box-sizing:border-box}.vis.timeline .vispanel{position:absolute;padding:0;margin:0;box-sizing:border-box}.vis.timeline .vispanel.bottom,.vis.timeline .vispanel.center,.vis.timeline .vispanel.left,.vis.timeline .vispanel.right,.vis.timeline .vispanel.top{border:1px #bfbfbf}.vis.timeline .vispanel.center,.vis.timeline .vispanel.left,.vis.timeline .vispanel.right{border-top-style:solid;border-bottom-style:solid;overflow:hidden}.vis.timeline .vispanel.bottom,.vis.timeline .vispanel.center,.vis.timeline .vispanel.top{border-left-style:solid;border-right-style:solid}.vis.timeline .background{overflow:hidden}.vis.timeline .vispanel>.content{position:relative}.vis.timeline .vispanel .shadow{position:absolute;width:100%;height:1px;box-shadow:0 0 10px rgba(0,0,0,.8)}.vis.timeline .vispanel .shadow.top{top:-1px;left:0}.vis.timeline .vispanel .shadow.bottom{bottom:-1px;left:0}.vis.timeline .labelset{position:relative;width:100%;overflow:hidden;box-sizing:border-box}.vis.timeline .labelset .vlabel{position:relative;left:0;top:0;width:100%;color:#4d4d4d;box-sizing:border-box;border-bottom:1px solid #bfbfbf}.vis.timeline .labelset .vlabel:last-child{border-bottom:none}.vis.timeline .labelset .vlabel .inner{display:inline-block;padding:5px}.vis.timeline .labelset .vlabel .inner.hidden{padding:0}.vis.timeline .itemset{position:relative;padding:0;margin:0;box-sizing:border-box}.vis.timeline .itemset .background,.vis.timeline .itemset .foreground{position:absolute;width:100%;height:100%}.vis.timeline .axis{position:absolute;width:100%;height:0;left:1px;z-index:1}.vis.timeline .foreground .group{position:relative;box-sizing:border-box;border-bottom:1px solid #bfbfbf}.vis.timeline .foreground .group:last-child{border-bottom:none}.vis.timeline .item{position:absolute;color:#1A1A1A;border-color:#97B0F8;border-width:1px;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}.vis.timeline .item.box{text-align:center;border-style:solid;border-radius:2px}.vis.timeline .item.point{background:0 0}.vis.timeline .item.dot{position:absolute;padding:0;border-width:4px;border-style:solid;border-radius:4px}.vis.timeline .item.range{border-style:solid;border-radius:2px;box-sizing:border-box}.vis.timeline .item.range .content{position:relative;display:inline-block;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{position:absolute;width:24px;height:100%;top:0;left:-4px;cursor:w-resize;z-index:10000}.vis.timeline .item.range .drag-right{position:absolute;width:24px;height:100%;top:0;right:-4px;cursor:e-resize;z-index:10001}.vis.timeline .timeaxis{position:relative;overflow:hidden}.vis.timeline .timeaxis.foreground{top:0;left:0;width:100%}.vis.timeline .timeaxis.background{position:absolute;top:0;left:0;width:100%;height:100%}.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.minor{border-color:#e5e5e5}.vis.timeline .timeaxis .grid.major{border-color:#bfbfbf}.vis.timeline .currenttime{background-color:#FF7F6E;width:2px;z-index:1}.vis.timeline .customtime{background-color:#6E94FF;width:2px;cursor:move;z-index:1}.vis.timeline .vispanel.background.horizontal .grid.horizontal{position:absolute;width:100%;height:0;border-bottom:1px solid}.vis.timeline .vispanel.background.horizontal .grid.minor{border-color:#e5e5e5}.vis.timeline .vispanel.background.horizontal .grid.major{border-color:#bfbfbf}.vis.timeline .dataaxis .yAxis.major{width:100%;position:absolute;color:#4d4d4d;white-space:nowrap}.vis.timeline .dataaxis .yAxis.major.measure{padding:0;margin:0;visibility:hidden;width:auto}.vis.timeline .dataaxis .yAxis.minor{position:absolute;width:100%;color:#bebebe;white-space:nowrap}.vis.timeline .dataaxis .yAxis.minor.measure{padding:0;margin:0;visibility:hidden;width:auto}.vis.timeline .legend{background-color:rgba(247,252,255,.65);padding:5px;border-color:#b3b3b3;border-style:solid;border-width:1px;box-shadow:2px 2px 10px rgba(154,154,154,.55)}.vis.timeline .legendText{white-space:nowrap;display:inline-block}.vis.timeline .graphGroup0{fill:#4f81bd;fill-opacity:0;stroke-width:2px;stroke:#4f81bd}.vis.timeline .graphGroup1{fill:#f79646;fill-opacity:0;stroke-width:2px;stroke:#f79646}.vis.timeline .graphGroup2{fill:#8c51cf;fill-opacity:0;stroke-width:2px;stroke:#8c51cf}.vis.timeline .graphGroup3{fill:#75c841;fill-opacity:0;stroke-width:2px;stroke:#75c841}.vis.timeline .graphGroup4{fill:#ff0100;fill-opacity:0;stroke-width:2px;stroke:#ff0100}.vis.timeline .graphGroup5{fill:#37d8e6;fill-opacity:0;stroke-width:2px;stroke:#37d8e6}.vis.timeline .graphGroup6{fill:#042662;fill-opacity:0;stroke-width:2px;stroke:#042662}.vis.timeline .graphGroup7{fill:#00ff26;fill-opacity:0;stroke-width:2px;stroke:#00ff26}.vis.timeline .graphGroup8{fill:#f0f;fill-opacity:0;stroke-width:2px;stroke:#f0f}.vis.timeline .graphGroup9{fill:#8f3938;fill-opacity:0;stroke-width:2px;stroke:#8f3938}.vis.timeline .fill{fill-opacity:.1;stroke:none}.vis.timeline .bar{fill-opacity:.5;stroke-width:1px}.vis.timeline .point{stroke-width:2px;fill-opacity:1}.vis.timeline .legendBackground{stroke-width:1px;fill-opacity:.9;fill:#fff;stroke:#c2c2c2}.vis.timeline .outline{stroke-width:1px;fill-opacity:1;fill:#fff;stroke:#e5e5e5}.vis.timeline .iconFill{fill-opacity:.3;stroke:none}div.network-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.network-manipulation-editMode{height:30px;z-index:10;position:absolute;margin-top:20px}div.network-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/network/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.network-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.network-manipulationUI:hover{box-shadow:1px 1px 8px rgba(0,0,0,.2)}span.network-manipulationUI:active{box-shadow:1px 1px 8px rgba(0,0,0,.5)}span.network-manipulationUI.back{background-image:url(img/network/backIcon.png)}span.network-manipulationUI.none:hover{box-shadow:1px 1px 8px transparent;cursor:default}span.network-manipulationUI.none:active{box-shadow:1px 1px 8px transparent}span.network-manipulationUI.none{padding:0}span.network-manipulationUI.notification{margin:2px;font-weight:700}span.network-manipulationUI.add{background-image:url(img/network/addNodeIcon.png)}span.network-manipulationUI.edit{background-image:url(img/network/editIcon.png)}span.network-manipulationUI.edit.editmode{background-color:#fcfcfc;border-style:solid;border-width:1px;border-color:#ccc}span.network-manipulationUI.connect{background-image:url(img/network/connectIcon.png)}span.network-manipulationUI.delete{background-image:url(img/network/deleteIcon.png)}span.network-manipulationLabel{margin:0 0 0 23px;line-height:25px}div.network-seperatorLine{display:inline-block;width:1px;height:20px;background-color:#bdbdbd;margin:5px 7px 0 15px}div.network-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.network-navigation:hover{box-shadow:0 0 3px 3px rgba(56,207,21,.3)}div.network-navigation.active,div.network-navigation:active{box-shadow:0 0 1px 3px rgba(56,207,21,.95)}div.network-navigation.up{background-image:url(img/network/upArrow.png);bottom:50px;left:55px}div.network-navigation.down{background-image:url(img/network/downArrow.png);bottom:10px;left:55px}div.network-navigation.left{background-image:url(img/network/leftArrow.png);bottom:10px;left:15px}div.network-navigation.right{background-image:url(img/network/rightArrow.png);bottom:10px;left:95px}div.network-navigation.zoomIn{background-image:url(img/network/plus.png);bottom:10px;right:15px}div.network-navigation.zoomOut{background-image:url(img/network/minus.png);bottom:10px;right:55px}div.network-navigation.zoomExtends{background-image:url(img/network/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 3c8e58d8..f1a4ebb0 100644
--- a/dist/vis.min.js
+++ b/dist/vis.min.js
@@ -4,8 +4,8 @@
*
* A dynamic, browser-based visualization library.
*
- * @version 1.1.0
- * @date 2014-06-10
+ * @version 3.0.1-SNAPSHOT
+ * @date 2014-07-07
*
* @license
* Copyright (C) 2011-2014 Almende B.V, http://almende.com
@@ -22,13 +22,15 @@
* 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 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;ae?1:e>t?-1:0}),this.values.length>0&&this.selectValue(0),this.dataPoints=[],this.loaded=!1,this.onLoadCallback=void 0,i.animationPreload?(this.loaded=!1,this.loadInBackground()):this.loaded=!0}function Slider(t,e){if(void 0===t)throw"Error: No container element defined";if(this.container=t,this.visible=e&&void 0!=e.visible?e.visible:!0,this.visible){this.frame=document.createElement("DIV"),this.frame.style.width="100%",this.frame.style.position="relative",this.container.appendChild(this.frame),this.frame.prev=document.createElement("INPUT"),this.frame.prev.type="BUTTON",this.frame.prev.value="Prev",this.frame.appendChild(this.frame.prev),this.frame.play=document.createElement("INPUT"),this.frame.play.type="BUTTON",this.frame.play.value="Play",this.frame.appendChild(this.frame.play),this.frame.next=document.createElement("INPUT"),this.frame.next.type="BUTTON",this.frame.next.value="Next",this.frame.appendChild(this.frame.next),this.frame.bar=document.createElement("INPUT"),this.frame.bar.type="BUTTON",this.frame.bar.style.position="absolute",this.frame.bar.style.border="1px solid red",this.frame.bar.style.width="100px",this.frame.bar.style.height="6px",this.frame.bar.style.borderRadius="2px",this.frame.bar.style.MozBorderRadius="2px",this.frame.bar.style.border="1px solid #7F7F7F",this.frame.bar.style.backgroundColor="#E5E5E5",this.frame.appendChild(this.frame.bar),this.frame.slide=document.createElement("INPUT"),this.frame.slide.type="BUTTON",this.frame.slide.style.margin="0px",this.frame.slide.value=" ",this.frame.slide.style.position="relative",this.frame.slide.style.left="-100px",this.frame.appendChild(this.frame.slide);var i=this;this.frame.slide.onmousedown=function(t){i._onMouseDown(t)},this.frame.prev.onclick=function(t){i.prev(t)},this.frame.play.onclick=function(t){i.togglePlay(t)},this.frame.next.onclick=function(t){i.next(t)}}this.onChangeCallback=void 0,this.values=[],this.index=void 0,this.playTimeout=void 0,this.playInterval=1e3,this.playLoop=!0}var moment="undefined"!=typeof window&&window.moment||require("moment"),Emitter=require("emitter-component"),Hammer;Hammer="undefined"!=typeof window?window.Hammer||require("hammerjs"):function(){throw Error("hammer.js is only available in a browser, not in node.js.")};var mousetrap;if(mousetrap="undefined"!=typeof window?window.mousetrap||require("mousetrap"):function(){throw Error("mouseTrap is only available in a browser, not in node.js.")},!Array.prototype.indexOf){Array.prototype.indexOf=function(t){for(var e=0;ei;++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.deepExtend=function t(e,i){if(Array.isArray(i))throw new TypeError("Arrays are not supported by deepExtend");for(var s in i)if(i.hasOwnProperty(s))if(i[s]&&i[s].constructor===Object)void 0===e[s]&&(e[s]={}),e[s].constructor===Object?t(e[s],i[s]):e[s]=i[s];else{if(Array.isArray(i[s]))throw new TypeError("Arrays are not supported by deepExtend");e[s]=i[s]}return e},util.equalArray=function(t,e){if(t.length!=e.length)return!1;for(var i=0,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},hover:{background:a,border:o}}}else e={background:t,border:t,highlight:{background:t,border:t},hover:{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),util.isString(t.hover)?e.hover={border:t.hover,background:t.hover}:(e.hover={},e.hover.background=t.hover&&t.hover.background||e.background,e.hover.border=t.hover&&t.hover.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),l=i*(1-(1-r)*e);switch(a%6){case 0:s=i,n=l,o=h;break;case 1:s=d,n=i,o=h;break;case 2:s=h,n=i,o=l;break;case 3:s=h,n=d,o=i;break;case 4:s=l,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 l={},c=0,u=r.length;u>c;c++){var p=r[c];l[p]=t.getValue(h,c)}i=n._addItem(l),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),l=0,c=t.getNumberOfRows();c>l;l++){for(var u={},p=0,m=d.length;m>p;p++){var g=d[p];u[g]=t.getValue(l,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,l,c,u=i&&i.convert||this.options.convert,p=i&&i.filter,m=[];if(void 0!=t)h=n._getItem(t,u),p&&!p(h)&&(h=null);else if(void 0!=e)for(l=0,c=e.length;c>l;l++)h=n._getItem(e[l],u),(!p||p(h))&&m.push(h);else for(d in this.data)this.data.hasOwnProperty(d)&&(h=n._getItem(d,u),(!p||p(h))&&m.push(h));if(this.showInternalIds=o,i&&i.order&&void 0==t&&this._sort(m,i.order),i&&i.fields){var g=i.fields;if(void 0!=t)h=this._filterFields(h,g);else for(l=0,c=m.length;c>l;l++)m[l]=this._filterFields(m[l],g)}if("DataTable"==r){var f=this._getColumnNames(s);if(void 0!=t)n._appendRow(s,f,h);else for(l=0,c=m.length;c>l;l++)n._appendRow(s,f,m[l]);return s}if(void 0!=t)return h;if(s){for(l=0,c=m.length;c>l;l++)s.push(m[l]);return s}return m},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,l=[];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++)l[e]=o[e][this.fieldId]}else for(s in a)a.hasOwnProperty(s)&&(n=this._getItem(s,d),r(n)&&l.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++)l[e]=o[e][this.fieldId]}else for(s in a)a.hasOwnProperty(s)&&(n=a[s],l.push(n[this.fieldId]));return l},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||void 0===r||(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=[],l=[],c=[];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]?l.push(o):(this.ids[o]=!0,d.push(o)):this.ids[o]&&(delete this.ids[o],c.push(o));break;case"remove":for(s=0,n=r.length;n>s;s++)o=r[s],this.ids[o]&&(delete this.ids[o],c.push(o))}d.length&&this._trigger("add",{items:d},i),l.length&&this._trigger("update",{items:l},i),c.length&&this._trigger("remove",{items:c},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;var stack={};stack.orderByStart=function(t){t.sort(function(t,e){return t.data.start-e.data.start})},stack.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.stack=function(t,e,i){var s,n;if(i)for(s=0,n=t.length;n>s;s++)t[s].top=null;for(s=0,n=t.length;n>s;s++){var o=t[s];if(null===o.top){o.top=e.axis;do{for(var a=null,r=0,h=t.length;h>r;r++){var d=t[r];if(null!==d.top&&d!==o&&stack.collision(o,d,e.item)){a=d;break}}null!=a&&(o.top=a.top+a.height+e.item)}while(a)}}},stack.nostack=function(t,e){var i,s;for(i=0,s=t.length;s>i;i++)t[i].top=e.axis},stack.collision=function(t,e,i){return t.left-ie.left&&t.top-ie.top},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){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.WEEKDAY){switch(this.step){case 5:case 2:e.setHours(12*Math.round(e.getHours()/12));break;default:e.setHours(6*Math.round(e.getHours()/6))}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""}},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=t.editable.updateTime||t.editable.updateGroup,i="vis timeline rootpanel "+t.orientation+(e?" editable":"");t.className&&(i+=" "+util.option.asString(i)),this.frame.className=i;var s=this._repaintChilds();this.frame.style.maxHeight=util.option.asSize(this.options.maxHeight,""),this.frame.style.minHeight=util.option.asSize(this.options.minHeight,""),this._updateSize();var n=this._isResized()||s;n&&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(7*(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),l=n.isMajor();this.getOption("showMinorLabels")&&this._repaintMinorText(d,n.getLabelMinor(),t),l&&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 c=this.options.toTime(0),u=n.getLabelMajor(c),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(){this.dom.measureCharMinor||(this.dom.measureCharMinor=document.createElement("DIV"),this.dom.measureCharMinor.className="text minor measure",this.dom.measureCharMinor.style.position="absolute",this.dom.measureCharMinor.appendChild(document.createTextNode("0")),this.frame.appendChild(this.dom.measureCharMinor)),this.props.minorCharHeight=this.dom.measureCharMinor.clientHeight,this.props.minorCharWidth=this.dom.measureCharMinor.clientWidth,this.dom.measureCharMajor||(this.dom.measureCharMajor=document.createElement("DIV"),this.dom.measureCharMajor.className="text minor measure",this.dom.measureCharMajor.style.position="absolute",this.dom.measureCharMajor.appendChild(document.createTextNode("0")),this.frame.appendChild(this.dom.measureCharMajor)),this.props.majorCharHeight=this.dom.measureCharMajor.clientHeight,this.props.majorCharWidth=this.dom.measureCharMajor.clientWidth},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())};var UNGROUPED="__ungrouped__";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);var n=document.createElement("div");n.className="labelset",this.dom.labelSet=n,this.sidePanel.frame.appendChild(n),this._updateUngrouped(),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=function(t){Component.prototype.setOptions.call(this,t)},ItemSet.prototype.markDirty=function(){this.groupIds=[],this.stackDirty=!0},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),this.dom.labelSet.parentNode&&this.dom.labelSet.parentNode.removeChild(this.dom.labelSet)},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),this.dom.labelSet.parentNode||this.sidePanel.frame.appendChild(this.dom.labelSet)},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=this.options.margin,e=this.range,i=util.option.asSize,s=util.option.asString,n=this.options,o=this.getOption("orientation"),a=!1,r=this.frame;"number"==typeof t&&(t={item:t,axis:t}),r.className="itemset"+(n.className?" "+s(n.className):""),a=this._orderGroups()||a;var h=this.range.end-this.range.start,d=h!=this.lastVisibleInterval||this.width!=this.lastWidth;d&&(this.stackDirty=!0),this.lastVisibleInterval=h,this.lastWidth=this.width;var l=this.stackDirty,c=this._firstGroup(),u={item:t.item,axis:t.axis},p={item:t.item,axis:t.item/2},m=0,g=t.axis+t.item;return util.forEach(this.groups,function(t){var i=t==c?u:p;a=t.repaint(e,i,l)||a,m+=t.height}),m=Math.max(m,g),this.stackDirty=!1,r.style.left=i(n.left,""),r.style.right=i(n.right,""),r.style.top=i("top"==o?"0":""),r.style.bottom=i("top"==o?"":"0"),r.style.width=i(n.width,"100%"),r.style.height=i(m),this.top=r.offsetTop,this.left=r.offsetLeft,this.width=r.offsetWidth,this.height=m,this.dom.axis.style.left=i(n.left,"0"),this.dom.axis.style.right=i(n.right,""),this.dom.axis.style.width=i(n.width,"100%"),this.dom.axis.style.height=i(0),this.dom.axis.style.top=i("top"==o?"0":""),this.dom.axis.style.bottom=i("top"==o?"":"0"),a=this._isResized()||a},ItemSet.prototype._firstGroup=function(){var t="top"==this.options.orientation?0:this.groupIds.length-1,e=this.groupIds[t],i=this.groups[e]||this.groups[UNGROUPED];return i||null},ItemSet.prototype._updateUngrouped=function(){var t=this.groups[UNGROUPED];if(this.groupsData)t&&(t.hide(),delete this.groups[UNGROUPED]);else if(!t){var e=null,i=null;t=new Group(e,i,this),this.groups[UNGROUPED]=t;for(var s in this.items)this.items.hasOwnProperty(s)&&t.add(this.items[s]);t.show()}},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.getLabelSet=function(){return this.dom.labelSet},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 or DataView");this.itemsData=t}else this.itemsData=null;if(s&&(util.forEach(this.itemListeners,function(t,e){s.unsubscribe(e,t)}),e=s.getIds(),this._onRemove(e)),this.itemsData){var n=this.id;util.forEach(this.itemListeners,function(t,e){i.itemsData.on(e,t,n)}),e=this.itemsData.getIds(),this._onAdd(e),this._updateUngrouped()}},ItemSet.prototype.getItems=function(){return this.itemsData},ItemSet.prototype.setGroups=function(t){var e,i=this;if(this.groupsData&&(util.forEach(this.groupListeners,function(t,e){i.groupsData.unsubscribe(e,t)}),e=this.groupsData.getIds(),this.groupsData=null,this._onRemoveGroups(e)),t){if(!(t instanceof DataSet||t instanceof DataView))throw new TypeError("Data must be an instance of DataSet or DataView");this.groupsData=t}else this.groupsData=null;if(this.groupsData){var s=this.id;util.forEach(this.groupListeners,function(t,e){i.groupsData.on(e,t,s)}),e=this.groupsData.getIds(),this._onAddGroups(e)}this._updateUngrouped(),this._order(),this.emit("change")},ItemSet.prototype.getGroups=function(){return this.groupsData},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?e._updateItem(o,n):(e._removeItem(o),o=null)),!o){if(!r)throw new TypeError('Unknown item type "'+a+'"');o=new r(n,e.options,s),o.id=t,e._addItem(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];s&&(e++,i._removeItem(s))}),e&&(this._order(),this.stackDirty=!0,this.emit("change"))},ItemSet.prototype._order=function(){util.forEach(this.groups,function(t){t.order()})},ItemSet.prototype._onUpdateGroups=function(t){this._onAddGroups(t)},ItemSet.prototype._onAddGroups=function(t){var e=this;t.forEach(function(t){var i=e.groupsData.get(t),s=e.groups[t];if(s)s.setData(i);else{if(t==UNGROUPED)throw new Error("Illegal group id. "+t+" is a reserved id.");var n=Object.create(e.options);util.extend(n,{height:null}),s=new Group(t,i,e),e.groups[t]=s;for(var o in e.items)if(e.items.hasOwnProperty(o)){var a=e.items[o];a.data.group==t&&s.add(a)}s.order(),s.show()}}),this.emit("change")},ItemSet.prototype._onRemoveGroups=function(t){var e=this.groups;t.forEach(function(t){var i=e[t];i&&(i.hide(),delete e[t])}),this.markDirty(),this.emit("change")},ItemSet.prototype._orderGroups=function(){if(this.groupsData){var t=this.groupsData.getIds({order:this.options.groupOrder}),e=!util.equalArray(t,this.groupIds);if(e){var i=this.groups;t.forEach(function(t){i[t].hide()}),t.forEach(function(t){i[t].show()}),this.groupIds=t}return e}return!1},ItemSet.prototype._addItem=function(t){this.items[t.id]=t;var e=this.groupsData?t.data.group:UNGROUPED,i=this.groups[e];i&&i.add(t)},ItemSet.prototype._updateItem=function(t,e){var i=t.data.group;if(t.data=e,t.displayed&&t.repaint(),i!=t.data.group){var s=this.groups[i];s&&s.remove(t);var n=this.groupsData?t.data.group:UNGROUPED,o=this.groups[n];o&&o.add(t)}},ItemSet.prototype._removeItem=function(t){t.hide(),delete this.items[t.id];var e=this.selection.indexOf(t.id);-1!=e&&this.selection.splice(e,1);var i=this.groupsData?t.data.group:UNGROUPED,s=this.groups[i];s&&s.remove(t)},ItemSet.prototype._constructByEndArray=function(t){for(var e=[],i=0;it.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.updateTime&&!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.updateTime&&!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),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=n+this.props.content.width,this.dom.box.style.left=this.left+"px",this.dom.box.style.width=n+"px",this.dom.content.style.left=t+"px"},Group.prototype._create=function(){var t=document.createElement("div");t.className="vlabel",this.dom.label=t;var e=document.createElement("div");e.className="inner",t.appendChild(e),this.dom.inner=e;var i=document.createElement("div");i.className="group",i["timeline-group"]=this,this.dom.foreground=i,this.dom.background=document.createElement("div"),this.dom.axis=document.createElement("div"),this.dom.marker=document.createElement("div"),this.dom.marker.style.visibility="hidden",this.dom.marker.innerHTML="?",this.dom.background.appendChild(this.dom.marker)},Group.prototype.setData=function(t){var e=t&&t.content;e instanceof Element?this.dom.inner.appendChild(e):this.dom.inner.innerHTML=void 0!=e?e:this.groupId;var i=t&&t.className;i&&util.addClassName(this.dom.label,i)},Group.prototype.getForeground=function(){return this.dom.foreground},Group.prototype.getBackground=function(){return this.dom.background},Group.prototype.getAxis=function(){return this.dom.axis},Group.prototype.getLabelWidth=function(){return this.props.label.width},Group.prototype.repaint=function(t,e,i){var s=!1;this.visibleItems=this._updateVisibleItems(this.orderedItems,this.visibleItems,t);var n=this.dom.marker.clientHeight;n!=this.lastMarkerHeight&&(this.lastMarkerHeight=n,util.forEach(this.items,function(t){t.dirty=!0,t.displayed&&t.repaint()}),i=!0),this.itemSet.options.stack?stack.stack(this.visibleItems,e,i):stack.nostack(this.visibleItems,e);for(var o=0,a=this.visibleItems.length;a>o;o++){var r=this.visibleItems[o];r.repositionY()}var h,d=this.visibleItems;if(d.length){var l=d[0].top,c=d[0].top+d[0].height;util.forEach(d,function(t){l=Math.min(l,t.top),c=Math.max(c,t.top+t.height)}),h=c-l+e.axis+e.item}else h=e.axis+e.item;h=Math.max(h,this.props.label.height);var u=this.dom.foreground;return this.top=u.offsetTop,this.left=u.offsetLeft,this.width=u.offsetWidth,s=util.updateProperty(this,"height",h)||s,s=util.updateProperty(this.props.label,"width",this.dom.inner.clientWidth)||s,s=util.updateProperty(this.props.label,"height",this.dom.inner.clientHeight)||s,u.style.height=h+"px",this.dom.label.style.height=h+"px",s},Group.prototype.show=function(){this.dom.label.parentNode||this.itemSet.getLabelSet().appendChild(this.dom.label),this.dom.foreground.parentNode||this.itemSet.getForeground().appendChild(this.dom.foreground),this.dom.background.parentNode||this.itemSet.getBackground().appendChild(this.dom.background),this.dom.axis.parentNode||this.itemSet.getAxis().appendChild(this.dom.axis)},Group.prototype.hide=function(){var t=this.dom.label;t.parentNode&&t.parentNode.removeChild(t);var e=this.dom.foreground;e.parentNode&&e.parentNode.removeChild(e);var i=this.dom.background;i.parentNode&&i.parentNode.removeChild(i);var s=this.dom.axis;s.parentNode&&s.parentNode.removeChild(s)},Group.prototype.add=function(t){if(this.items[t.id]=t,t.setParent(this),t instanceof ItemRange&&-1==this.visibleItems.indexOf(t)){var e=this.itemSet.range;this._checkIfVisible(t,this.visibleItems,e)}},Group.prototype.remove=function(t){delete this.items[t.id],t.setParent(this.itemSet);var e=this.visibleItems.indexOf(t);-1!=e&&this.visibleItems.splice(e,1)},Group.prototype.removeFromDataSet=function(t){this.itemSet.removeItem(t.id)},Group.prototype.order=function(){var t=util.toArray(this.items);this.orderedItems.byStart=t,this.orderedItems.byEnd=this._constructByEndArray(t),stack.orderByStart(this.orderedItems.byStart),stack.orderByEnd(this.orderedItems.byEnd)},Group.prototype._constructByEndArray=function(t){for(var e=[],i=0;i0)for(n=0;n=0&&!this._checkIfInvisible(t.byStart[n],o,i);n--);for(n=s+1;n=0&&!this._checkIfInvisible(t.byEnd[n],o,i);n--);for(n=a+1;ne.start-a&&s[l].data[n]e.start-a&&s[l].data[n]=s&&(s=864e5),e=new Date(e.valueOf()-.05*s),i=new Date(i.valueOf()+.05*s)}(null!==e||null!==i)&&this.range.setRange(e,i)},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){this.itemSet.setSelection(t)},Timeline.prototype.getSelection=function(){return this.itemSet.getSelection()},Timeline.prototype.setWindow=function(t,e){if(1==arguments.length){var i=arguments[0];this.range.setRange(i.start,i.end)}else 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.redraw=function(){this.rootPanel.repaint()},Timeline.prototype.repaint=function(){throw new Error("Function repaint is deprecated. Use redraw instead.")},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();(a.length>0||s.length>0)&&this.emit("select",{items:this.getSelection()}),t.stopPropagation()}},Timeline.prototype._onAddItem=function(t){if(this.options.selectable&&this.options.editable.add){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.contentPanel.frame),o=t.gesture.center.pageX-n,a={start:this.timeAxis.snap(this._toTime(o)),content:"new item"};("range"===this.options.type||"rangeoverflow"==this.options.type)&&(a.end=this.timeAxis.snap(this._toTime(o+this.rootPanel.width/5)));var r=util.randomUUID();a[this.itemsData.fieldId]=r;var h=ItemSet.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 M=t,u()}function i(){D=0,C=M.charAt(0)}function s(){D++,C=M.charAt(D)}function n(){return M.charAt(D+1)}function o(t){return P.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 l(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 c(){for(O=T.NULL,I="";" "==C||" "==C||"\n"==C||"\r"==C;)s();do{var t=!1;if("#"==C){for(var e=D-1;" "==M.charAt(e)||" "==M.charAt(e);)e--;if("\n"==M.charAt(e)||""==M.charAt(e)){for(;""!=C&&"\n"!=C;)s();t=!0}}if("/"==C&&"/"==n()){for(;""!=C&&"\n"!=C;)s();t=!0}if("/"==C&&"*"==n()){for(;""!=C;){if("*"==C&&"/"==n()){s(),s();break}s()}t=!0}for(;" "==C||" "==C||"\n"==C||"\r"==C;)s()}while(t);if(""==C)return void(O=T.DELIMITER);var i=C+n();if(E[i])return O=T.DELIMITER,I=i,s(),void s();if(E[C])return O=T.DELIMITER,I=C,void s();if(o(C)||"-"==C){for(I+=C,s();o(C);)I+=C,s();return"false"==I?I=!1:"true"==I?I=!0:isNaN(Number(I))||(I=Number(I)),void(O=T.IDENTIFIER)}if('"'==C){for(s();""!=C&&('"'!=C||'"'==C&&'"'==n());)I+=C,'"'==C&&s(),s();if('"'!=C)throw b('End of string " expected');return s(),void(O=T.IDENTIFIER)}for(O=T.UNKNOWN;""!=C;)I+=C,s();throw new SyntaxError('Syntax error in part "'+x(I,30)+'"')}function u(){var t={};if(i(),c(),"strict"==I&&(t.strict=!0,c()),("graph"==I||"digraph"==I)&&(t.type=I,c()),O==T.IDENTIFIER&&(t.id=I,c()),"{"!=I)throw b("Angle bracket { expected");if(c(),p(t),"}"!=I)throw b("Angle bracket } expected");if(c(),""!==I)throw b("End of file expected");return c(),delete t.node,delete t.edge,delete t.graph,t}function p(t){for(;""!==I&&"}"!=I;)m(t),";"==I&&c()}function m(t){var e=g(t);if(e)return void y(t,e);var i=f(t);if(!i){if(O!=T.IDENTIFIER)throw b("Identifier expected");var s=I;if(c(),"="==I){if(c(),O!=T.IDENTIFIER)throw b("Identifier expected");t[s]=I,c()}else v(t,s)}}function g(t){var e=null;if("subgraph"==I&&(e={},e.type="subgraph",c(),O==T.IDENTIFIER&&(e.id=I,c())),"{"==I){if(c(),e||(e={}),e.parent=t,e.node=t.node,e.edge=t.edge,e.graph=t.graph,p(e),"}"!=I)throw b("Angle bracket } expected");c(),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"==I?(c(),t.node=_(),"node"):"edge"==I?(c(),t.edge=_(),"edge"):"graph"==I?(c(),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(;"->"==I||"--"==I;){var i,s=I;c();var n=g(t);if(n)i=n;else{if(O!=T.IDENTIFIER)throw b("Identifier or subgraph expected");i=I,h(t,{id:i}),c()}var o=_(),a=l(t,e,i,s,o);d(t,a),e=i}}function _(){for(var t=null;"["==I;){for(c(),t={};""!==I&&"]"!=I;){if(O!=T.IDENTIFIER)throw b("Attribute name expected");var e=I;if(c(),"="!=I)throw b("Equal sign = expected");if(c(),O!=T.IDENTIFIER)throw b("Attribute value expected");var i=I;r(t,e,i),c(),","==I&&c()}if("]"!=I)throw b("Bracket ] expected");c()}return t}function b(t){return new SyntaxError(t+', got "'+x(I,30)+'" (char '+D+")")}function x(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 S(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=l(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 T={NULL:0,DELIMITER:1,IDENTIFIER:2,UNKNOWN:3},E={"{":!0,"}":!0,"[":!0,"]":!0,";":!0,"=":!0,",":!0,"->":!0,"--":!0},M="",D=0,C="",I="",O=T.NULL,P=/[a-zA-Z_0-9.:#]/;t.parseDOT=e,t.DOTToGraph=S}("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,l=e+s/2;this.beginPath(),this.moveTo(t,l),this.bezierCurveTo(t,l-a,d-o,e,d,e),this.bezierCurveTo(d+o,e,r,l-a,r,l),this.bezierCurveTo(r,l+a,d+o,h,d,h),this.bezierCurveTo(d-o,h,t,l+a,t,l)},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,l=t+o,c=e+a,u=t+o/2,p=e+a/2,m=e+(s-a/2),g=e+s;this.beginPath(),this.moveTo(l,p),this.bezierCurveTo(l,p+d,u+h,c,u,c),this.bezierCurveTo(u-h,c,t,p+d,t,p),this.bezierCurveTo(t,p-d,u-h,e,u,e),this.bezierCurveTo(u+h,e,l,p-d,l,p),this.lineTo(l,m),this.bezierCurveTo(l,m+d,u+h,g,u,g),this.bezierCurveTo(u-h,g,t,m+d,t,m),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),l=n+s/3*Math.cos(i-.5*Math.PI),c=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(l,c),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),l=0,c=!0;d>=.1;){var u=n[l++%o];u>d&&(u=d);var p=Math.sqrt(u*u/(1+h*h));0>a&&(p=-p),t+=p,e+=h*p,this[c?"lineTo":"moveTo"](t,e),d-=u,c=!c}}),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.hover?this.color.hover.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.hover?this.color.hover.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.hover?this.color.hover.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.hover?this.color.hover.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.hover?this.color.hover.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.hover?this.color.hover.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.hover?this.color.hover.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.hover?this.color.hover.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,l=0;r>l;l++)t.fillText(a[l],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:1==this.hover?this.color.hover: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:1==this.hover?Math.min(this.hoverWidth,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:1==this.hover?this.color.hover: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):1==this.hover?(t.strokeStyle=this.color.hover,t.fillStyle=this.color.hover):(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)*this.arrowScaleFactor;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)*this.arrowScaleFactor;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):1==this.hover?(t.strokeStyle=this.color.hover,t.fillStyle=this.color.hover):(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 l,c,u=this.to.distanceToBorder(t,e),p=(o-u)/o;if(1==this.smooth?(l=(1-p)*this.via.x+p*this.to.x,c=(1-p)*this.via.y+p*this.to.y):(l=(1-p)*this.from.x+p*this.to.x,c=(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,l,c):t.lineTo(l,c),t.stroke(),i=(10+5*this.width)*this.arrowScaleFactor,t.arrow(l,c,e,i),t.fill(),t.stroke(),this.label){var m;if(1==this.smooth){var g=.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));m={x:g,y:f}}else m=this._pointOnLine(.5);this._label(t,this.label,m.x,m.y)}}else{var v,y,_,b=this.from,x=.25*Math.max(100,this.length);b.width||b.resize(t),b.width>b.height?(v=b.x+.5*b.width,y=b.y-x,_={x:v,y:b.y,angle:.9*Math.PI}):(v=b.x+x,y=b.y-.5*b.height,_={x:b.x,y:y,angle:.6*Math.PI}),t.beginPath(),t.arc(v,y,x,0,2*Math.PI,!1),t.stroke();var i=(10+5*this.width)*this.arrowScaleFactor;t.arrow(_.x,_.y,_.angle,i),t.fill(),t.stroke(),this.label&&(m=this._pointOnCircle(v,y,x,.5),this._label(t,this.label,m.x,m.y))}},Edge.prototype._getDistanceToEdge=function(t,e,i,s,n,o){if(1==this.smooth){var a,r,h,d,l,c,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,l=Math.abs(n-h),c=Math.abs(o-d),u=Math.min(u,Math.sqrt(l*l+c*c));return u}var p=i-t,m=s-e,g=p*p+m*m,f=((n-t)*p+(o-e)*m)/g;f>1?f=1:0>f&&(f=0);var h=t+f*p,d=e+f*m,l=h-n,c=d-o;return Math.sqrt(l*l+c*c)},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;n
Simulation Mode:
Barnes Hut
Repulsion
Hierarchical
Barnes Hut
gravitationalConstant
0
-20000
centralGravity
0
3
springLength
0
500
springConstant
0
0.5
damping
0
0.3
Repulsion
nodeDistance
0
300
centralGravity
0
3
springLength
0
500
springConstant
0
0.5
damping
0
0.3
Hierarchical
nodeDistance
0
300
centralGravity
0
3
springLength
0
500
springConstant
0
0.5
damping
0
0.3
direction
1
4
levelSeparation
1
500
nodeSpacing
1
500
Options:
',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,l=this.calculationNodes,c=this.calculationNodeIndices,u=5,p=.5*-u,m=this.constants.physics.hierarchicalRepulsion.nodeDistance,g=m;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,l=t[e[h]].y;n>d&&(n=d),d>a&&(a=d),o>l&&(o=l),l>r&&(r=l)}var c=Math.abs(a-n)-Math.abs(r-o);c>0?(o-=.5*c,r+=.5*c):(n+=.5*c,a-=.5*c);var u=1e-5,p=Math.max(u,Math.abs(a-n)),m=.5*p,g=.5*(n+a),f=.5*(o+r),v={root:{centerOfMass:{x:0,y:0},mass:0,range:{minX:g-m,maxX:g+m,minY:f-m,maxY:f+m},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,l,c=this.calculationNodes,u=this.calculationNodeIndices,p=-2/3,m=4/3,g=this.constants.physics.repulsion.nodeDistance,f=g;for(d=0;di&&(a=.5*f>i?1:v*i+m,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&&this.nodeIndices.length>0){"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._XconvertDOMtoCanvas(e.x),this.sectors.support.nodes.targetNode.y=this._YconvertDOMtoCanvas(e.y),this.sectors.support.nodes.targetViaNode.x=.5*(this._XconvertDOMtoCanvas(e.x)+this.edges.connectionEdge.from.x),this.sectors.support.nodes.targetViaNode.y=this._YconvertDOMtoCanvas(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=[],l=t.dynamicEdges.length,c=0;l>c;c++)d.push(t.dynamicEdges[c].id);if(0==e)for(h=!1,c=0;l>c;c++){var u=this.edges[d[c]];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(c=0;l>c;c++)if(u=this.edges[d[c]],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,e=0,i=1e9,s=0;for(t in this.nodes)this.nodes.hasOwnProperty(t)&&(s=this.nodes[t].clusterSessions.length,s>e&&(e=s),i>s&&(i=s));if(e-i>this.constants.clustering.clusterLevelDifference){var n=this.nodeIndices.length,o=e-this.constants.clustering.clusterLevelDifference;for(t in this.nodes)this.nodes.hasOwnProperty(t)&&this.nodes[t].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._XconvertDOMtoCanvas(t.x),i=this._YconvertDOMtoCanvas(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},_addToHover:function(t){t instanceof Node?this.hoverObj.nodes[t.id]=t:this.hoverObj.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);if(this._putDataInSector(),!e)if(this.stabilize){var s=this;setTimeout(function(){s._stabilize(),s.start()},0)}else 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.dragGraph&&(this.constants.dragGraph=t.dragGraph),void 0!==t.dragNodes&&(this.constants.dragNodes=t.dragNodes),void 0!==t.zoomable&&(this.constants.zoomable=t.zoomable),void 0!==t.hover&&(this.constants.hover=t.hover),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.physics.hierarchicalRepulsion){this.constants.hierarchicalLayout.enabled=!0,this.constants.physics.hierarchicalRepulsion.enabled=!0,this.constants.physics.barnesHut.enabled=!1;for(e in t.physics.hierarchicalRepulsion)t.physics.hierarchicalRepulsion.hasOwnProperty(e)&&(this.constants.physics.hierarchicalRepulsion[e]=t.physics.hierarchicalRepulsion[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]);this.editMode=this.constants.dataManipulation.initiallyVisible}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,this.constants.edges.color.hover=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),void 0!==t.edges.color.hover&&(this.constants.edges.color.hover=t.edges.color.hover))),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.moving=!0,this.start()},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&&1==this.constants.dragNodes){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._XconvertDOMtoCanvas(i._XconvertCanvasToDOM(t.x)+o)),t.yFixed||(e.y=i._YconvertDOMtoCanvas(i._YconvertCanvasToDOM(t.y)+a))}),this.moving||(this.moving=!0,this.start())}else if(1==this.constants.dragGraph){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.moving=!0,this.start()}}},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){if(1==this.constants.zoomable){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._XconvertDOMtoCanvas(e.x),y:this._YconvertDOMtoCanvas(e.y)},this._setScale(t),this._setTranslation(o,a),this.updateClustersDefault(),this._redraw(),t>i?this.emit("zoom",{direction:"+"}):this.emit("zoom",{direction:"-"}),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.popupObj&&this._checkHidePopup(i);var s=this,n=function(){s._checkShowPopup(i)};if(this.popupTimer&&clearInterval(this.popupTimer),this.drag.dragging||(this.popupTimer=setTimeout(n,this.constants.tooltip.delay)),1==this.constants.hover){for(var o in this.hoverObj.edges)this.hoverObj.edges.hasOwnProperty(o)&&(this.hoverObj.edges[o].hover=!1,delete this.hoverObj.edges[o]);var a=this._getNodeAt(i);null==a&&(a=this._getEdgeAt(i)),null!=a&&this._hoverObject(a);for(var r in this.hoverObj.nodes)this.hoverObj.nodes.hasOwnProperty(r)&&(a instanceof Node&&a.id!=r||a instanceof Edge||null==a)&&(this._blurObject(this.hoverObj.nodes[r]),delete this.hoverObj.nodes[r]);this.redraw()}},Graph.prototype._checkShowPopup=function(t){var e,i={left:this._XconvertDOMtoCanvas(t.x),top:this._YconvertDOMtoCanvas(t.y),right:this._XconvertDOMtoCanvas(t.x),bottom:this._YconvertDOMtoCanvas(t.y)},s=this.popupObj;if(void 0==this.popupObj){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.popupObj=o;break}}}if(void 0===this.popupObj){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.popupObj=r;break}}}if(this.popupObj){if(this.popupObj!=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.popupObj.getTitle()),h.popup.show()}}else this.popup&&this.popup.hide()},Graph.prototype._checkHidePopup=function(t){this.popupObj&&this._getNodeAt(t)||(this.popupObj=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)}this.moving=!0,1==this.constants.hierarchicalLayout.enabled&&0==this.initializing&&(this._resetLevels(),this._setupHierarchicalLayout()),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._XconvertDOMtoCanvas(0),y:this._YconvertDOMtoCanvas(0)},this.canvasBottomRight={x:this._XconvertDOMtoCanvas(this.frame.canvas.clientWidth),y:this._YconvertDOMtoCanvas(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),this.emit("viewChanged")},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._XconvertDOMtoCanvas=function(t){return(t-this.translation.x)/this.scale},Graph.prototype._XconvertCanvasToDOM=function(t){return t*this.scale+this.translation.x},Graph.prototype._YconvertDOMtoCanvas=function(t){return(t-this.translation.y)/this.scale},Graph.prototype._YconvertCanvasToDOM=function(t){return t*this.scale+this.translation.y},Graph.prototype.canvasToDOM=function(t){return{x:this._XconvertCanvasToDOM(t.x),y:this._YconvertCanvasToDOM(t.y)}},Graph.prototype.DOMtoCanvas=function(t){return{x:this._XconvertDOMtoCanvas(t.x),y:this._YconvertDOMtoCanvas(t.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;i.5*Math.PI&&(this.armRotation.vertical=.5*Math.PI)),(void 0!==t||void 0!==e)&&this.calculateCameraOrientation()},Graph3d.Camera.prototype.getArmRotation=function(){var t={};return t.horizontal=this.armRotation.horizontal,t.vertical=this.armRotation.vertical,t},Graph3d.Camera.prototype.setArmLength=function(t){void 0!==t&&(this.armLength=t,this.armLength<.71&&(this.armLength=.71),this.armLength>5&&(this.armLength=5),this.calculateCameraOrientation())},Graph3d.Camera.prototype.getArmLength=function(){return this.armLength},Graph3d.Camera.prototype.getCameraLocation=function(){return this.cameraLocation},Graph3d.Camera.prototype.getCameraRotation=function(){return this.cameraRotation},Graph3d.Camera.prototype.calculateCameraOrientation=function(){this.cameraLocation.x=this.armLocation.x-this.armLength*Math.sin(this.armRotation.horizontal)*Math.cos(this.armRotation.vertical),this.cameraLocation.y=this.armLocation.y-this.armLength*Math.cos(this.armRotation.horizontal)*Math.cos(this.armRotation.vertical),this.cameraLocation.z=this.armLocation.z+this.armLength*Math.sin(this.armRotation.vertical),this.cameraRotation.x=Math.PI/2-this.armRotation.vertical,this.cameraRotation.y=0,this.cameraRotation.z=-this.armRotation.horizontal},Graph3d.prototype._setScale=function(){this.scale=new Point3d(1/(this.xMax-this.xMin),1/(this.yMax-this.yMin),1/(this.zMax-this.zMin)),this.keepAspectRatio&&(this.scale.x3&&(this.colFilter=3);else{if(this.style!==Graph3d.STYLE.DOTCOLOR&&this.style!==Graph3d.STYLE.DOTSIZE&&this.style!==Graph3d.STYLE.BARCOLOR&&this.style!==Graph3d.STYLE.BARSIZE)throw'Unknown style "'+this.style+'"';this.colX=0,this.colY=1,this.colZ=2,this.colValue=3,t.getNumberOfColumns()>4&&(this.colFilter=4)}},Graph3d.prototype.getNumberOfRows=function(t){return t.length},Graph3d.prototype.getNumberOfColumns=function(t){var e=0;for(var i in t[0])t[0].hasOwnProperty(i)&&e++;return e},Graph3d.prototype.getDistinctValues=function(t,e){for(var i=[],s=0;st[s][e]&&(i.min=t[s][e]),i.maxt;t++){var p=(t-c)/(u-c),m=240*p,g=this._hsv2rgb(m,1,1);l.strokeStyle=g,l.beginPath(),l.moveTo(r,o+t),l.lineTo(a,o+t),l.stroke()}l.strokeStyle=this.colorAxis,l.strokeRect(r,o,i,n)}if(this.style===Graph3d.STYLE.DOTSIZE&&(l.strokeStyle=this.colorAxis,l.fillStyle=this.colorDot,l.beginPath(),l.moveTo(r,o),l.lineTo(a,o),l.lineTo(a-i+e,h),l.lineTo(r,h),l.closePath(),l.fill(),l.stroke()),this.style===Graph3d.STYLE.DOTCOLOR||this.style===Graph3d.STYLE.DOTSIZE){var f=5,v=new StepNumber(this.valueMin,this.valueMax,(this.valueMax-this.valueMin)/5,!0);for(v.start(),v.getCurrent()0?this.yMin:this.yMax,n=this._convert3Dto2D(new Point3d(_,a,this.zMin)),Math.cos(2*y)>0?(m.textAlign="center",m.textBaseline="top",n.y+=v):Math.sin(2*y)<0?(m.textAlign="right",m.textBaseline="middle"):(m.textAlign="left",m.textBaseline="middle"),m.fillStyle=this.colorAxis,m.fillText(" "+i.getCurrent()+" ",n.x,n.y),i.next()}for(m.lineWidth=1,s=void 0===this.defaultYStep,i=new StepNumber(this.yMin,this.yMax,this.yStep,s),i.start(),i.getCurrent()0?this.xMin:this.xMax,n=this._convert3Dto2D(new Point3d(o,i.getCurrent(),this.zMin)),Math.cos(2*y)<0?(m.textAlign="center",m.textBaseline="top",n.y+=v):Math.sin(2*y)>0?(m.textAlign="right",m.textBaseline="middle"):(m.textAlign="left",m.textBaseline="middle"),m.fillStyle=this.colorAxis,m.fillText(" "+i.getCurrent()+" ",n.x,n.y),i.next();for(m.lineWidth=1,s=void 0===this.defaultZStep,i=new StepNumber(this.zMin,this.zMax,this.zStep,s),i.start(),i.getCurrent()0?this.xMin:this.xMax,a=Math.sin(y)<0?this.yMin:this.yMax;!i.end();)t=this._convert3Dto2D(new Point3d(o,a,i.getCurrent())),m.strokeStyle=this.colorAxis,m.beginPath(),m.moveTo(t.x,t.y),m.lineTo(t.x-v,t.y),m.stroke(),m.textAlign="right",m.textBaseline="middle",m.fillStyle=this.colorAxis,m.fillText(i.getCurrent()+" ",t.x-5,t.y),i.next();m.lineWidth=1,t=this._convert3Dto2D(new Point3d(o,a,this.zMin)),e=this._convert3Dto2D(new Point3d(o,a,this.zMax)),m.strokeStyle=this.colorAxis,m.beginPath(),m.moveTo(t.x,t.y),m.lineTo(e.x,e.y),m.stroke(),m.lineWidth=1,c=this._convert3Dto2D(new Point3d(this.xMin,this.yMin,this.zMin)),u=this._convert3Dto2D(new Point3d(this.xMax,this.yMin,this.zMin)),m.strokeStyle=this.colorAxis,m.beginPath(),m.moveTo(c.x,c.y),m.lineTo(u.x,u.y),m.stroke(),c=this._convert3Dto2D(new Point3d(this.xMin,this.yMax,this.zMin)),u=this._convert3Dto2D(new Point3d(this.xMax,this.yMax,this.zMin)),m.strokeStyle=this.colorAxis,m.beginPath(),m.moveTo(c.x,c.y),m.lineTo(u.x,u.y),m.stroke(),m.lineWidth=1,t=this._convert3Dto2D(new Point3d(this.xMin,this.yMin,this.zMin)),e=this._convert3Dto2D(new Point3d(this.xMin,this.yMax,this.zMin)),m.strokeStyle=this.colorAxis,m.beginPath(),m.moveTo(t.x,t.y),m.lineTo(e.x,e.y),m.stroke(),t=this._convert3Dto2D(new Point3d(this.xMax,this.yMin,this.zMin)),e=this._convert3Dto2D(new Point3d(this.xMax,this.yMax,this.zMin)),m.strokeStyle=this.colorAxis,m.beginPath(),m.moveTo(t.x,t.y),m.lineTo(e.x,e.y),m.stroke();var b=this.xLabel;b.length>0&&(l=.1/this.scale.y,o=(this.xMin+this.xMax)/2,a=Math.cos(y)>0?this.yMin-l:this.yMax+l,n=this._convert3Dto2D(new Point3d(o,a,this.zMin)),Math.cos(2*y)>0?(m.textAlign="center",m.textBaseline="top"):Math.sin(2*y)<0?(m.textAlign="right",m.textBaseline="middle"):(m.textAlign="left",m.textBaseline="middle"),m.fillStyle=this.colorAxis,m.fillText(b,n.x,n.y));var x=this.yLabel;x.length>0&&(d=.1/this.scale.x,o=Math.sin(y)>0?this.xMin-d:this.xMax+d,a=(this.yMin+this.yMax)/2,n=this._convert3Dto2D(new Point3d(o,a,this.zMin)),Math.cos(2*y)<0?(m.textAlign="center",m.textBaseline="top"):Math.sin(2*y)>0?(m.textAlign="right",m.textBaseline="middle"):(m.textAlign="left",m.textBaseline="middle"),m.fillStyle=this.colorAxis,m.fillText(x,n.x,n.y));var w=this.zLabel;w.length>0&&(h=30,o=Math.cos(y)>0?this.xMin:this.xMax,a=Math.sin(y)<0?this.yMin:this.yMax,r=(this.zMin+this.zMax)/2,n=this._convert3Dto2D(new Point3d(o,a,r)),m.textAlign="right",m.textBaseline="middle",m.fillStyle=this.colorAxis,m.fillText(w,n.x-h,n.y))},Graph3d.prototype._hsv2rgb=function(t,e,i){var s,n,o,a,r,h;switch(a=i*e,r=Math.floor(t/60),h=a*(1-Math.abs(t/60%2-1)),r){case 0:s=a,n=h,o=0;break;case 1:s=h,n=a,o=0;break;case 2:s=0,n=a,o=h;break;case 3:s=0,n=h,o=a;break;case 4:s=h,n=0,o=a;break;case 5:s=a,n=0,o=h;break;default:s=0,n=0,o=0}return"RGB("+parseInt(255*s)+","+parseInt(255*n)+","+parseInt(255*o)+")"},Graph3d.prototype._redrawDataGrid=function(){var t,e,i,s,n,o,a,r,h,d,l,c,u,p=this.frame.canvas,m=p.getContext("2d");if(!(void 0===this.dataPoints||this.dataPoints.length<=0)){for(n=0;n0}else o=!0;o?(u=(t.point.z+e.point.z+i.point.z+s.point.z)/4,d=240*(1-(u-this.zMin)*this.scale.z/this.verticalRatio),l=1,this.showShadow?(c=Math.min(1+x.x/w/2,1),a=this._hsv2rgb(d,l,c),r=a):(c=1,a=this._hsv2rgb(d,l,c),r=this.colorAxis)):(a="gray",r=this.colorAxis),h=.5,m.lineWidth=h,m.fillStyle=a,m.strokeStyle=r,m.beginPath(),m.moveTo(t.screen.x,t.screen.y),m.lineTo(e.screen.x,e.screen.y),m.lineTo(s.screen.x,s.screen.y),m.lineTo(i.screen.x,i.screen.y),m.closePath(),m.fill(),m.stroke()}}else for(n=0;nc&&(c=0);var u,p,m;this.style===Graph3d.STYLE.DOTCOLOR?(u=240*(1-(h.point.value-this.valueMin)*this.scale.value),p=this._hsv2rgb(u,1,1),m=this._hsv2rgb(u,1,.8)):this.style===Graph3d.STYLE.DOTSIZE?(p=this.colorDot,m=this.colorDotBorder):(u=240*(1-(h.point.z-this.zMin)*this.scale.z/this.verticalRatio),p=this._hsv2rgb(u,1,1),m=this._hsv2rgb(u,1,.8)),i.lineWidth=1,i.strokeStyle=m,i.fillStyle=p,i.beginPath(),i.arc(h.screen.x,h.screen.y,c,0,2*Math.PI,!0),i.fill(),i.stroke()}}},Graph3d.prototype._redrawDataBar=function(){var t,e,i,s,n=this.frame.canvas,o=n.getContext("2d");if(!(void 0===this.dataPoints||this.dataPoints.length<=0)){for(t=0;t0&&(t=this.dataPoints[0],s.lineWidth=1,s.strokeStyle="blue",s.beginPath(),s.moveTo(t.screen.x,t.screen.y)),e=1;e0&&s.stroke()}},Graph3d.prototype._onMouseDown=function(t){if(t=t||window.event,this.leftButtonDown&&this._onMouseUp(t),this.leftButtonDown=t.which?1===t.which:1===t.button,this.leftButtonDown||this.touchDown){this.startMouseX=getMouseX(t),this.startMouseY=getMouseY(t),this.startStart=new Date(this.start),this.startEnd=new Date(this.end),this.startArmRotation=this.camera.getArmRotation(),this.frame.style.cursor="move";var e=this;this.onmousemove=function(t){e._onMouseMove(t)},this.onmouseup=function(t){e._onMouseUp(t)},G3DaddEventListener(document,"mousemove",e.onmousemove),G3DaddEventListener(document,"mouseup",e.onmouseup),G3DpreventDefault(t)}},Graph3d.prototype._onMouseMove=function(t){t=t||window.event;var e=parseFloat(getMouseX(t))-this.startMouseX,i=parseFloat(getMouseY(t))-this.startMouseY,s=this.startArmRotation.horizontal+e/200,n=this.startArmRotation.vertical+i/200,o=4,a=Math.sin(o/360*2*Math.PI);Math.abs(Math.sin(s))0?1:0>t?-1:0}var s=e[0],n=e[1],o=e[2],a=i((n.x-s.x)*(t.y-s.y)-(n.y-s.y)*(t.x-s.x)),r=i((o.x-n.x)*(t.y-n.y)-(o.y-n.y)*(t.x-n.x)),h=i((s.x-o.x)*(t.y-o.y)-(s.y-o.y)*(t.x-o.x));return!(0!=a&&0!=r&&a!=r||0!=r&&0!=h&&r!=h||0!=a&&0!=h&&a!=h)},Graph3d.prototype._dataPointFromXY=function(t,e){var i,s=100,n=null,o=null,a=null,r=new Point2d(t,e);if(this.style===Graph3d.STYLE.BAR||this.style===Graph3d.STYLE.BARCOLOR||this.style===Graph3d.STYLE.BARSIZE)for(i=this.dataPoints.length-1;i>=0;i--){n=this.dataPoints[i];var h=n.surfaces;if(h)for(var d=h.length-1;d>=0;d--){var l=h[d],c=l.corners,u=[c[0].screen,c[1].screen,c[2].screen],p=[c[2].screen,c[3].screen,c[0].screen];if(this._insideTriangle(r,u)||this._insideTriangle(r,p))return n}}else for(i=0;iv)&&s>v&&(a=v,o=n)}}return o},Graph3d.prototype._showTooltip=function(t){var e,i,s;this.tooltip?(e=this.tooltip.dom.content,i=this.tooltip.dom.line,s=this.tooltip.dom.dot):(e=document.createElement("div"),e.style.position="absolute",e.style.padding="10px",e.style.border="1px solid #4d4d4d",e.style.color="#1a1a1a",e.style.background="rgba(255,255,255,0.7)",e.style.borderRadius="2px",e.style.boxShadow="5px 5px 10px rgba(128,128,128,0.5)",i=document.createElement("div"),i.style.position="absolute",i.style.height="40px",i.style.width="0",i.style.borderLeft="1px solid #4d4d4d",s=document.createElement("div"),s.style.position="absolute",s.style.height="0",s.style.width="0",s.style.border="5px solid #4d4d4d",s.style.borderRadius="5px",this.tooltip={dataPoint:null,dom:{content:e,line:i,dot:s}}),this._hideTooltip(),this.tooltip.dataPoint=t,e.innerHTML="function"==typeof this.showTooltip?this.showTooltip(t.point):"
An object containing field names as key, and data types as value.
@@ -700,7 +711,7 @@ data.add([
// retrieve formatted items
var items = data.get({
fields: ['id', 'date', 'group'], // output the specified fields only
- convert: {
+ type: {
date: 'Date', // convert the date fields to Date objects
group: 'String' // convert the group fields to Strings
}
diff --git a/docs/dataview.html b/docs/dataview.html
index a1fd350f..3046391f 100644
--- a/docs/dataview.html
+++ b/docs/dataview.html
@@ -103,8 +103,6 @@ var data = new vis.DataView(dataset, options)
are exactly the same as the properties available in methods
DataSet.get and DataView.get.
-
-
+ Graph2d is an interactive visualization chart to draw data in a 2D graph.
+ You can freely move and zoom in the graph by dragging and scrolling in the
+ window.
+
+
+ Graph2d uses HTML DOM and SVG for rendering. This allows for flexible
+ customization using css styling.
+
+ The class name of the Graph2d is vis.Graph2d.
+ When constructing a Graph2d, an HTML DOM container must be provided to attach
+ the graph to. Optionally, data an options can be provided.
+ Data is a vis DataSet or an Array, described in
+ section Data Format.
+ Options is a name-value map in the JSON format. The available options
+ are described in section Configuration Options.
+ Groups is a vis DataSet containing groups. The available options and the method of construction
+ are described in section Data Format.
+
+
var graph = new vis.Graph2d(container [, data] [, options] [,groups]);
+
+
+ Data, options and groups can be set or changed later on using the functions
+ Graph2d.setData(data), Graph2d.setOptions(options) and Graph2d.setGroups(groups).
+
+
+
Data Format
+
+ Graph2d can load data from an Array, a DataSet or a DataView.
+ JSON objects are added to this DataSet by using the add() function.
+ Data points must have properties x, y, and z,
+ and can optionally have a property style and filter.
+
+ Graph2d can be provided with two types of data:
+
+ Like the items, groups are regular JavaScript Arrays and Objects.
+ Using groups, items can be grouped together.
+ Items are filtered per group, and displayed as individual graphs. Groups can contain the properties id,
+ content, className (optional) and options (optional).
+
+
+ Groups can be applied to a timeline using the method setGroups.
+ A table with groups can be created like:
+
+
+
+var groups = new vis.DataSet();
+groups.add({
+ id: 1,
+ content: 'Group 1'
+ // Optional: a field 'className'
+ // Optional: options
+ })
+groups.add({
+ // more groups...
+});
+
+
+
+
+ Groups can have the following properties:
+
+
+
+
+
Name
+
Type
+
Required
+
Description
+
+
+
id
+
String | Number
+
yes
+
An id for the group. The group will display all items having a
+ property group which matches the id
+ of the group.
+
+
+
content
+
String
+
yes
+
The contents of the group. This can be plain text or html code.
+
+
+
className
+
String
+
no
+
This field is optional. A className can be used to give groups
+ an individual css style.
+
+
+
+
options
+
JSON object
+
no
+
This field is optional. The options can be used to give a group a specific draw style.
+ Any options that are colored green in the Configuration Options can be used as options here.
+
+
+
+
Configuration Options
+
+
Graph2d Options
+
+Options can be used to customize the Graph2d to your purposes. These options can be passed to the Graph2d object either in
+the constructor, or by the setOptions function.
+
+
+
+The options colored in green can also be used as options for the groups. All options are optional.
+
+
+
+
Name
+
Type
+
Default
+
Description
+
+
+
yAxisOrientation
+
String
+
'left'
+
This defines with which axis, left or right, the graph is coupled. Example 5 shows groups with different Y axis. If no groups are coupled
+ with an axis, it will not be shown.
+
+
+
defaultGroup
+
String
+
'default'
+
This is the label for the default, ungrouped items when shown in a legend.
+
+
+
sort
+
Boolean
+
true
+
This determines if the items are sorted automatically.
+ They are sorted by the x value. If sort is enabled, more optimizations are possible, increasing the performance.
+
+
+
sampling
+
Boolean
+
true
+
If sampling is enabled, graph2D will automatically determine the amount of points per pixel.
+ If there are more than 1 point per pixel, not all points will be drawn. Disabling sampling will cause a decrease in performance.
+
+
+
graphHeight
+
Number | String
+
'400px'
+
This is the height of the graph SVG canvas.
+ If it is larger than the height of the outer frame, you can drag up and down
+ the vertical direction as well as the usual horizontal direction.
+
+
+
shaded
+
Boolean | Object
+
false
+
Toggle a shaded area with the default settings.
+
+
+
shaded.enabled
+
Boolean
+
false
+
This toggles the shading.
+
+
+
shaded.orientation
+
String
+
'bottom'
+
This determines if the shaded area is at the bottom or at the top of the curve. The options are 'bottom' or 'top'.
+
+
+
style
+
String
+
'line'
+
This allows the user to define if this should be a linegraph or a barchart. The options are: 'line' or 'bar'.
+
+
+
barChart.width
+
Number
+
50
+
The width of the bars.
+
+
+
barChart.align
+
String
+
'center'
+
The alignment of the bars with regards to the coordinate. The options are 'left', 'right' or 'center'.
+
+
+
catmullRom
+
Boolean | Object
+
true
+
Toggle the interpolation with the default settings. For more customization use the JSON format.
+
+
+
catmullRom.enabled
+
Boolean
+
true
+
Toggle the interpolation.
+
+
+
catmullRom.parametrization
+
String
+
'centripetal'
+
Define the type of parametrizaion. Example 7 shows the different methods. The options are 'centripetal' (best results), 'chordal' and 'uniform'. Uniform is the computationally cheapest variant.
+ If catmullRom is disabled, linear interpolation is used.
+
+
+
drawPoints
+
Boolean | Object
+
true
+
Toggle the drawing of the datapoints with the default settings.
+
+
+
drawPoints.enabled
+
Boolean
+
true
+
Toggle the drawing of the datapoints.
+
+
+
drawPoints.size
+
Number
+
6
+
Determine the size at which the data points are drawn.
+
+
+
drawPoints.style
+
String
+
'square'
+
Determine the shape of the data points. The options are 'square' or 'circle'.
+
+
+
dataAxis.showMinorLabels
+
Boolean
+
true
+
Toggle the drawing of the minor labels on the Y axis.
+
+
+
dataAxis.showMajorLabels
+
Boolean
+
true
+
Toggle the drawing of the major labels on the Y axis.
+
+
+
dataAxis.icons
+
Boolean
+
false
+
Toggle the drawing of automatically generated icons the Y axis.
+
+
+
dataAxis.width
+
Number | String
+
'40px'
+
Set the (minimal) width of the yAxis. The axis will resize to accomodate the labels of the Y values.
+
+
+
dataAxis.visible
+
Boolean
+
true
+
Show or hide the data axis.
+
+
+
legend
+
Boolean
+
false
+
Toggle the legend with the default settings.
+
+
+
legend.enabled
+
Boolean
+
false
+
Toggle the legend.
+
+
+
legend.icons
+
Boolean
+
true
+
Show automatically generated icons on the legend.
+
+
+
legend.left.visible
+
Boolean
+
true
+
Both axis, left and right, have a corresponding legend. This toggles the visibility of the legend that is coupled with the left axis.
+
+
+
legend.left.position
+
String
+
'top-left'
+
Determine the position of the legend coupled to the left axis. Options are 'top-left', 'top-right', 'bottom-left' or 'bottom-right'.
+
+
+
legend.right.visible
+
Boolean
+
true
+
This toggles the visibility of the legend that is coupled with the right axis.
+
+
+
legend.right.position
+
String
+
'top-right'
+
Determine the position of the legend coupled to the right axis. Options are 'top-left', 'top-right', 'bottom-left' or 'bottom-right'.
+
+
+
+
Timeline Options
+
+
+ Graph2d is built upon the framework of the timeline. These options from the timeline can be used with graph2D.
+ All options are optional.
+
+
+
+
+
Name
+
Type
+
Default
+
Description
+
+
+
+
autoResize
+
boolean
+
true
+
If true, the Timeline will automatically detect when its container is resized, and redraw itself accordingly. If false, the Timeline can be forced to repaint after its container has been resized using the function redraw().
+
+
+
+
end
+
Date | Number | String
+
none
+
The initial end date for the axis of the timeline.
+ If not provided, the latest date present in the items set is taken as
+ end date.
+
+
+
+
height
+
Number | String
+
none
+
The height of the timeline in pixels or as a percentage.
+ When height is undefined or null, the height of the timeline is automatically
+ adjusted to fit the contents.
+ It is possible to set a maximum height using option maxHeight
+ to prevent the timeline from getting too high in case of automatically
+ calculated height.
+
+
+
+
+
margin.axis
+
Number
+
20
+
The minimal margin in pixels between items and the time axis.
+
+
+
+
margin.item
+
Number
+
10
+
The minimal margin in pixels between items.
+
+
+
+
max
+
Date | Number | String
+
none
+
Set a maximum Date for the visible range.
+ It will not be possible to move beyond this maximum.
+
+
+
+
+
maxHeight
+
Number | String
+
none
+
Specifies the maximum height for the Timeline. Can be a number in pixels or a string like "300px".
+
+
+
+
min
+
Date | Number | String
+
none
+
Set a minimum Date for the visible range.
+ It will not be possible to move beyond this minimum.
+
+
+
+
+
minHeight
+
Number | String
+
none
+
Specifies the minimum height for the Timeline. Can be a number in pixels or a string like "300px".
+
+
+
+
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.
+
+
+
+
showCurrentTime
+
boolean
+
true
+
Show a vertical bar at the current time.
+
+
+
+
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. 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.
+
+
+
+
showMajorLabels
+
boolean
+
true
+
By default, the timeline shows both minor and major date labels on the
+ time axis.
+ For example the minor labels show minutes and the major labels show hours.
+ When showMajorLabels is false, no major labels
+ are shown.
+
+
+
+
showMinorLabels
+
boolean
+
true
+
By default, the timeline shows both minor and major date labels on the
+ time axis.
+ For example the minor labels show minutes and the major labels show hours.
+ When showMinorLabels is false, no minor labels
+ are shown. When both showMajorLabels and
+ showMinorLabels are false, no horizontal axis will be
+ visible.
+
+
+
+
start
+
Date | Number | String
+
none
+
The initial start date for the axis of the timeline.
+ If not provided, the earliest date present in the events is taken as start date.
+
+
+
+
width
+
String
+
'100%'
+
The width of the timeline in pixels or as a percentage.
+
+
+
+
zoomMax
+
Number
+
315360000000000
+
Set a maximum zoom interval for the visible range in milliseconds.
+ It will not be possible to zoom out further than this maximum.
+ Default value equals about 10000 years.
+
+
+
+
+
zoomMin
+
Number
+
10
+
Set a minimum zoom interval for the visible range in milliseconds.
+ It will not be possible to zoom in further than this minimum.
+
+
+
+
+
+
+
+
Methods
+
+ The Graph2d supports the following methods.
+
+
+
+
+
Method
+
Return Type
+
Description
+
+
+
+
clear([what])
+
none
+
+ Clear the Graph2d. An object can be passed specifying which sections to clear: items, groups,
+ and/or options. By Default, items, groups and options are cleared, i.e. what = {items: true, groups: true, options: true}. Example usage:
+
+
Graph2d.clear(); // clear items, groups, and options
+Graph2d.clear({options: true}); // clear options only
+
+
+
+
+
+
destroy()
+
none
+
Destroy the Graph2d. The Graph2d is removed from memory. all DOM elements and event listeners are cleaned up.
+
+
+
+
+
getCustomTime()
+
Date
+
Retrieve the custom time. Only applicable when the option showCustomTime is true.
+
+
+
+
+
setCustomTime(time)
+
none
+
Adjust the custom time bar. Only applicable when the option showCustomTime is true. time is a Date object.
+
+
+
+
+
getWindow()
+
Object
+
Get the current visible window. Returns an object with properties start: Date and end: Date.
+
+
+
+
on(event, callback)
+
none
+
Create an event listener. The callback function is invoked every time the event is triggered. Avialable events: rangechange, rangechanged, select. The callback function is invoked as callback(properties), where properties is an object containing event specific properties. See section Events for more information.
+
+
+
+
off(event, callback)
+
none
+
Remove an event listener created before via function on(event, callback). See section Events for more information.
+
+
+
+
redraw()
+
none
+
Force a redraw of the Graph2d. Can be useful to manually redraw when option autoResize=false.
+
+
+
+
+
setGroups(groups)
+
none
+
Set a data set with groups for the Graph2d.
+ groups can be an Array with Objects,
+ a DataSet, or a DataView. For each of the groups, the items of the
+ Graph2d are filtered on the property group, which
+ must correspond with the id of the group.
+
+
+
+
+
setItems(items)
+
none
+
Set a data set with items for the Graph2d.
+ items can be an Array with Objects,
+ a DataSet, or a DataView.
+
+
+
+
+
setOptions(options)
+
none
+
Set or update options. It is possible to change any option of the Graph2d at any time. You can for example switch orientation on the fly.
+
+
+
+
+
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
+
+ Graph2d fires events when changing the visible window by dragging, when
+ selecting items, and when dragging the custom time bar.
+
+
+
+ Here an example on how to listen for a rangeChanged event.
+
Fired repeatedly when the user is dragging the Graph2d window.
+
+
+
+
start (Number): timestamp of the current start of the window.
+
end (Number): timestamp of the current end of the window.
+
+
+
+
+
+
rangechanged
+
Fired once after the user has dragged the Graph2d window.
+
+
+
+
start (Number): timestamp of the current start of the window.
+
end (Number): timestamp of the current end of the window.
+
+
+
+
+
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.
+
+
+
+
+
+
+
Styles
+
+ All parts of the Graph2d have a class name and a default css style just like the Timeline.
+ The styles can be overwritten, which enables full customization of the layout
+ of the Graph2d.
+
+
+ Additionally, Graph2d has 10 preset styles for graphs, which are cycled through when loading groups. These styles can be overwritten
+ as well, along with defining your own classes to style the graphs! Example 4 and
+ example 5 show the usage of custom styles.
+
+
+
Data Policy
+
+ All code and data is processed and rendered in the browser.
+ No data is sent to any server.
+
Graph3d is an interactive visualization chart to draw data in a three dimensional
graph. You can freely move and zoom in the graph by dragging and scrolling in the
- window.
- Graph3d also supports animation of a graph.
+ window. Graph3d also supports animation of a graph.
+
+
+ Graph3d uses HTML canvas
+ to render graphs, and can render up to a few thousands of data points smoothly.
Contents
diff --git a/docs/index.html b/docs/index.html
index 1d6e3bd8..1ba97f9d 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -53,8 +53,12 @@
A filtered and/or formatted view on a DataSet.
- Graph.
- Display a graph or network with nodes and edges.
+ Network.
+ Display a network (force directed graph) with nodes and edges (previously called Graph).
+
+
+ Graph2d.
+ Plot data on a timeline with lines or barcharts.
Graph3d.
diff --git a/docs/graph.html b/docs/network.html
similarity index 84%
rename from docs/graph.html
rename to docs/network.html
index 20e96ed2..e2ffb70b 100644
--- a/docs/graph.html
+++ b/docs/network.html
@@ -2,7 +2,7 @@
- vis.js | graph documentation
+ vis.js | network documentation
@@ -13,29 +13,31 @@
-
Graph documentation
+
Network documentation
Overview
- Graph is a visualization to display graphs and networks consisting of nodes
+ Network is a visualization to display networks and networks consisting of nodes
and edges. The visualization is easy to use and supports custom shapes,
styles, colors, sizes, images, and more.
- The graph visualization works smooth on any modern browser for up to a
- few thousand nodes and edges. To handle a larger amount of nodes, Graph
- has clustering support.
+ The network visualization works smooth on any modern browser for up to a
+ few thousand nodes and edges. To handle a larger amount of nodes, Network
+ has clustering support. Network uses
+ HTML canvas
+ for rendering.
- 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
+ Every dataset is different. Nodes can have different sizes based on content, interconnectivity can be high or low etc. Because of this, network 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.
+ example 25.
- To get started with Graph, install or download the
+ To get started with Network, install or download the
vis.js library.
@@ -77,8 +79,8 @@
Example
- Here a basic graph example. Note that unlike the
- Timeline, the Graph does not need the vis.css
+ Here a basic network example. Note that unlike the
+ Timeline, the Network does not need the vis.css
file.
@@ -90,14 +92,14 @@
<!doctype html>
<html>
<head>
- <title>Graph | Basic usage</title>
+ <title>Network | Basic usage</title>
<script type="text/javascript" src="../../dist/vis.js"></script>
</head>
<body>
-<div id="mygraph"></div>
+<div id="mynetwork"></div>
<script type="text/javascript">
// create an array with nodes
@@ -117,8 +119,8 @@
{from: 2, to: 5}
];
- // create a graph
- var container = document.getElementById('mygraph');
+ // create a network
+ var container = document.getElementById('mynetwork');
var data= {
nodes: nodes,
edges: edges,
@@ -127,7 +129,7 @@
width: '400px',
height: '400px'
};
- var graph = new vis.Graph(container, data, options);
+ var network = new vis.Network(container, data, options);
</script>
</body>
@@ -146,13 +148,13 @@
-The constructor of the Graph is vis.Graph.
-
var graph = new vis.Graph(container, data, options);
+The constructor of the Network is vis.Network.
+
var network = new vis.Network(container, data, options);
The constructor accepts three parameters:
- container is the DOM element in which to create the graph.
+ container is the DOM element in which to create the network.
data is an Object containing properties nodes and
@@ -173,7 +175,7 @@ The constructor accepts three parameters:
Data format
- The data parameter of the Graph constructor is an object
+ The data parameter of the Network constructor is an object
which can contain different types of data.
The following properties are supported in the data object:
@@ -207,7 +209,7 @@ var data = {
A property options,
containing an object with global options.
- Options can be provided as third parameter in the graph constructor
+ Options can be provided as third parameter in the network constructor
as well. Section Configuration Options
describes the available options.
@@ -246,7 +248,7 @@ nodes.add([
// ... more nodes
]);
-When using a DataSet, the graph is automatically updating to changes in the DataSet.
+When using a DataSet, the network is automatically updating to changes in the DataSet.
Nodes support the following properties:
@@ -514,7 +516,7 @@ edges.add([
// ... more edges
]);
-When using a DataSet, the graph is automatically updating to changes in the DataSet.
+When using a DataSet, the network is automatically updating to changes in the DataSet.
Edges support the following properties:
@@ -527,45 +529,45 @@ When using a DataSet, the graph is automatically updating to changes in the Data
Required
Description
-
-
arrowScaleFactor
-
Number
-
no
-
If you are using arrows, this will scale the arrow. Values < 1 give smaller arrows, > 1 larger arrows. Default: 1.
-
-
-
color
-
String | Object
-
no
-
Color for the edge.
-
+
+
arrowScaleFactor
+
Number
+
no
+
If you are using arrows, this will scale the arrow. Values < 1 give smaller arrows, > 1 larger arrows. Default: 1.
+
+
+
color
+
String | Object
+
no
+
Color for the edge.
+
-
-
color.color
-
String
-
no
-
Color of the edge when not selected.
-
+
+
color.color
+
String
+
no
+
Color of the edge when not selected.
+
-
-
color.highlight
-
String
-
no
-
Color of the edge when selected.
-
+
+
color.highlight
+
String
+
no
+
Color of the edge when selected.
+
-
-
color.hover
-
String
-
no
-
Color of the edge when the edge is hovered over and the hover option is enabled.
-
-
-
hoverWidth
-
Number
-
1.5
-
This determines the thickness of the edge if it is hovered over. This will only manifest when the hover option is enabled.
-
+
+
color.hover
+
String
+
no
+
Color of the edge when the edge is hovered over and the hover option is enabled.
+
+
+
hoverWidth
+
Number
+
1.5
+
This determines the thickness of the edge if it is hovered over. This will only manifest when the hover option is enabled.
+
dash
@@ -714,7 +716,7 @@ When using a DataSet, the graph is automatically updating to changes in the Data
DOT language
- Graph supports data in the
+ Network supports data in the
DOT language.
To provide data in the DOT language, the data object must contain
a property dot with a String containing the data.
@@ -727,11 +729,11 @@ When using a DataSet, the graph is automatically updating to changes in the Data
// provide data in the DOT language
var data = {
- dot: 'digraph {1 -> 1 -> 2; 2 -> 3; 2 -- 4; 2 -> 1 }'
+ dot: 'dinetwork {1 -> 1 -> 2; 2 -> 3; 2 -- 4; 2 -> 1 }'
};
-// create a graph
-var graph = new vis.Graph(container, data);
+// create a network
+var network = new vis.Network(container, data);
@@ -739,7 +741,7 @@ var graph = new vis.Graph(container, data);
Configuration options
- Options can be used to customize the graph. Options are defined as a JSON object.
+ Options can be used to customize the network. Options are defined as a JSON object.
All options are optional.
@@ -780,7 +782,7 @@ var options = {
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.
+ Enabling this setting will create a physics configuration div above the network. 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.
@@ -818,7 +820,7 @@ var options = {
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
+ to speed up the initialization of the network 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.
@@ -838,7 +840,7 @@ var options = {
height
String
"400px"
-
The height of the graph in pixels or as a percentage.
+
The height of the network in pixels or as a percentage.
@@ -853,15 +855,15 @@ var options = {
Object
none
- Configuration options for shortcuts keys. Sortcut keys are turned off by default. See section Keyboard navigation for an overview of the available options.
+ Configuration options for shortcuts keys. Shortcut keys are turned off by default. See section Keyboard navigation for an overview of the available options.
-
dragGraph
+
dragNetwork
Boolean
true
- Toggle if the graph can be dragged. This will not affect the dragging of nodes.
+ Toggle if the network can be dragged. This will not affect the dragging of nodes.
@@ -869,7 +871,7 @@ var options = {
Boolean
true
- Toggle if the nodes can be dragged. This will not affect the dragging of the graph.
+ Toggle if the nodes can be dragged. This will not affect the dragging of the network.
@@ -902,7 +904,7 @@ var options = {
selectable
Boolean
true
-
If true, nodes in the graph can be selected by clicking them.
+
If true, nodes in the network can be selected by clicking them.
Long press can be used to select multiple nodes.
@@ -910,7 +912,7 @@ var options = {
stabilize
Boolean
true
-
If true, the graph is stabilized before displaying it. If false,
+
If true, the network is stabilized before displaying it. If false,
the nodes move to a stabe position visibly in an animated way.
@@ -919,21 +921,21 @@ var options = {
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.
+ before showing the result. If your simulation takes too long to stabilize, this number can be reduced. On the other hand, if your network is not stabilized after loading, this number can be increased.
width
String
"400px"
-
The width of the graph in pixels or as a percentage.
+
The width of the network in pixels or as a percentage.
zoomable
Boolean
true
- Toggle if the graph can be zoomed.
+ Toggle if the network can be zoomed.
@@ -943,7 +945,7 @@ var options = {
Nodes configuration
- Nodes can be configured with different styles and shapes. To configure nodes, provide an object named nodes in the options for the Graph.
+ Nodes can be configured with different styles and shapes. To configure nodes, provide an object named nodes in the options for the Network.
@@ -969,7 +971,7 @@ var options = {
The following options are available for nodes. These options must be created
- inside an object nodes in the graphs options object.
+ inside an object nodes in the networks options object.
@@ -1121,7 +1123,7 @@ var options = {
Edges configuration
- Edges can be configured with different length and styling. To configure edges, provide an object named edges in the options for the Graph.
+ Edges can be configured with different length and styling. To configure edges, provide an object named edges in the options for the Network.
@@ -1139,7 +1141,7 @@ var options = {
The following options are available for edges. These options must be created
- inside an object edges in the graphs options object.
+ inside an object edges in the networks options object.
@@ -1149,32 +1151,32 @@ var options = {
Default
Description
-
-
arrowScaleFactor
-
Number
-
1
-
If you are using arrows, this will scale the arrow. Values < 1 give smaller arrows, > 1 larger arrows. Default: 1.
-
-
-
color
-
String | Object
-
Object
-
Colors of the edge. This object contains both colors for the selected and unselected state.
-
+
+
arrowScaleFactor
+
Number
+
1
+
If you are using arrows, this will scale the arrow. Values < 1 give smaller arrows, > 1 larger arrows. Default: 1.
+
+
+
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.color
+
String
+
"#848484"
+
Color of the edge when not selected.
+
-
-
color.highlight
-
String
-
"#848484"
-
Color of the edge when selected.
-
+
+
color.highlight
+
String
+
"#848484"
+
Color of the edge when selected.
+
dash
@@ -1214,12 +1216,12 @@ 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
+
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.
+
style
@@ -1235,6 +1237,12 @@ var options = {
1
The default width of a edge.
+
+
widthSelectionMultiplier
+
Number
+
2
+
Determines the thickness scaling of an selected edge. This is applied when an edge, or a node connected to it, is selected.
+
Groups configuration
@@ -1378,9 +1386,10 @@ var nodes = [
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.
+ 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.
+ When using the hierarchical display option, hierarchicalRepulsion is automatically used as the physics solver. Similarly, if you use the hierarchicalRepulsion physics option, hierarchical display is automatically turned on with default settings.
-
Note: if the behaviour of your graph is not the way you want it, use configurePhysics as described below or by example 25.
+
Note: if the behaviour of your network 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.
@@ -1402,6 +1411,13 @@ var options = {
nodeDistance: 100,
damping: 0.09
},
+ hierarchicalRepulsion: {
+ centralGravity: 0.5,
+ springLength: 150,
+ springConstant: 0.01,
+ nodeDistance: 60,
+ damping: 0.09
+ }
}
barnesHut:
@@ -1465,6 +1481,12 @@ var options = {
0.1
The central gravity is a force that pulls all nodes to the center. This ensures independent groups do not float apart.
+
+
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.
+
springLength
Number
@@ -1472,17 +1494,55 @@ var options = {
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.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.
+
+
+
hierarchicalRepulsion:
+
+
+
Name
+
Type
+
Default
+
Description
+
+
+
+
centralGravity
+
Number
+
0.5
+
The central gravity is a force that pulls all nodes to the center. This ensures independent groups do not float apart.
+
nodeDistance
Number
-
100
+
60
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.
+
+
springLength
+
Number
+
100
+
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.05
+
0.01
This is the spring constant used to calculate the spring forces based on Hooke′s Law. More information is available here.
@@ -1493,9 +1553,9 @@ var options = {
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
+Every dataset is different. Nodes can have different sizes based on content, interconnectivity can be high or low etc. Because of this, network 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.
+settings you are happy with, you can supply them to network 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.
@@ -1505,9 +1565,9 @@ var options = {
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.
+ By using the data manipulation feature of the network 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,
+ 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.
@@ -1550,7 +1610,7 @@ var options: {
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
+ 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
@@ -1593,6 +1653,16 @@ var options: {
// all fields normally accepted by a node can be used.
callback(newData); // call the callback with the new data to edit the node.
}
+ onEditEdge: function(data,callback) {
+ /** data = {id: edgeID,
+ * from: nodeId1,
+ * to: nodeId2,
+ * };
+ */
+ var newData = {..}; // alter the data as you want, except for the ID.
+ // all fields normally accepted by an edge can be used.
+ callback(newData); // call the callback with the new data to edit the edge.
+ }
onConnect: function(data,callback) {
// data = {from: nodeId1, to: nodeId2};
var newData = {..}; // check or alter data as you see fit.
@@ -1612,12 +1682,12 @@ var options: {
An code snippet from example 21 is shown below.
- The graph now supports dynamic clustering of nodes. This allows a user to view a very large dataset (> 50.000 nodes) without
+ The network now supports dynamic clustering of nodes. This allows a user to view a very large dataset (> 50.000 nodes) without
sacrificing performance. When loading a large dataset, the nodes are clustered initially (this may take a small while) to have a
responsive visualization to work with. The clustering is both outside-in and inside-out. Outside-in means that nodes with only one
connection will be contained, or clustered, in the node it is connected to. Inside-out clustering first determines which nodes are hubs.
@@ -1629,7 +1699,7 @@ graph.on("resize", function(params) {console.log(params.width,params.height)});
to calculate the required forces. The contained nodes are removed from the global nodes index, greatly speeding up the system.
- The clustering has the following user-configurable settings. The default values have been tested with the Graph examples and work well.
+ The clustering has the following user-configurable settings. The default values have been tested with the Network examples and work well.
The default state for clustering is off.
@@ -1792,17 +1862,17 @@ var options: {
clusterLevelDifference
Number
2
-
At every clustering session, Graph will check if the difference between cluster levels is
+
At every clustering session, Network 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
+ If the highest level of your network 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.
+ Network has a menu with navigation controls, which is disabled by default.
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.
@@ -1817,8 +1887,8 @@ var options: {
Keyboard navigation
- The graph can be navigated using shortcut keys.
- The default state for the keyboard navigation is off. The predefined keys can be found in the example 20_navigation.html.
+ The network can be navigated using shortcut keys.
+ The default state for the keyboard navigation is off. The predefined keys can be found in the example 20_navigation.html.
@@ -1871,9 +1941,9 @@ 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.
+ The network 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.
+ 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.
@@ -1933,7 +2003,7 @@ var options: {
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).
+
This defines the direction the network 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.
@@ -1951,10 +2021,12 @@ var options: {
link:"Add Link",
del:"Delete selected",
editNode:"Edit Node",
+ editEdge:"Edit Edge",
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.",
+ editEdgeDescription:"Click on either one of the control points and drag them to another node to connect to it.".
addError:"The function for add does not support two arguments
(data,callback).",
linkError:"The function for connect does not support two arguments
@@ -2056,7 +2128,7 @@ var options: {
Methods
- Graph supports the following methods.
+ Network supports the following methods.
@@ -2085,19 +2157,19 @@ var options: {
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.
+ You can use this to stablize your network once, then save the positions in a database so the next time you load the nodes, stabilization will be near instantaneous.
DOMtoCanvas(pos)
object
-
This function converts DOM coordinates to coordinates on the canvas. Input and output are in the form of {x:xpos,y:ypos}. The DOM values are relative to the graph container.
+
This function converts DOM coordinates to coordinates on the canvas. Input and output are in the form of {x:xpos,y:ypos}. The DOM values are relative to the network container.
canvasToDOM(pos)
object
-
This function converts canvas coordinates to coordinates on the DOM. Input and output are in the form of {x:xpos,y:ypos}. The DOM values are relative to the graph container.
+
This function converts canvas coordinates to coordinates on the DOM. Input and output are in the form of {x:xpos,y:ypos}. The DOM values are relative to the network container.
@@ -2115,7 +2187,7 @@ var options: {
redraw()
none
-
Redraw the graph. Useful when the layout of the webpage changed.
+
Redraw the network. Useful when the layout of the webpage changed.
@@ -2132,21 +2204,41 @@ var options: {
setOptions(options)
none
-
Set options for the graph. The available options are described in
+
Set options for the network. The available options are described in
the section Configuration Options.
+
+
selectNodes(selection, [highlightEdges])
+
none
+
Select nodes.
+ selection is an array with ids of nodes to be selected.
+ The array selection can contain zero or multiple ids.
+ Example usage: network.selectNodes([3, 5]); will select
+ nodes with id 3 and 5. The highlisghEdges boolean can be used to automatically select the edges connected to the node.
+
+
+
+
selectEdges(selection)
+
none
+
Select Edges.
+ selection is an array with ids of edges to be selected.
+ The array selection can contain zero or multiple ids.
+ Example usage: network.selectEdges([3, 5]); will select
+ edges with id 3 and 5.
+
+
setSelection(selection)
none
-
Select nodes.
- selection is an array with ids of nodes to be selected.
- The array selection can contain zero or multiple ids.
- Example usage: graph.setSelection([3, 5]); will select
- nodes with id 3 and 5.
+
Select nodes [deprecated].
+ selection is an array with ids of nodes to be selected.
+ The array selection can contain zero or multiple ids.
+ Example usage: network.setSelection([3, 5]); will select
+ nodes with id 3 and 5.
-
+
setSize(width, height)
@@ -2159,14 +2251,14 @@ var options: {
zoomExtent()
none
-
Scales the graph so all the nodes are in center view.
+
Scales the network so all the nodes are in center view.
Events
- Graph fires events after one or multiple nodes are selected or deselected.
+ Network fires events after one or multiple nodes are selected or deselected.
The event can be catched by creating a listener.
@@ -2175,7 +2267,7 @@ var options: {
-graph.on('select', function (properties) {
+network.on('select', function (properties) {
alert('selected nodes: ' + properties.nodes);
});
Fired when the view has changed. This is when the graph has moved or zoomed.
+
Fired when the view has changed. This is when the network has moved or zoomed.
none
zoom
-
Fired when the graph has zoomed. This event can be used to trigger the .storePosition() function after stabilization.
+
Fired when the network has zoomed. This event can be used to trigger the .storePosition() function after stabilization.
direction: "+" or "-"
diff --git a/docs/timeline.html b/docs/timeline.html
index 544aa463..c280eb56 100644
--- a/docs/timeline.html
+++ b/docs/timeline.html
@@ -23,7 +23,10 @@
The time scale on the axis is adjusted automatically, and supports scales ranging
from milliseconds to years.
-
+
+ Timeline uses regular HTML DOM to render the timeline and items put on the
+ timeline. This allows for flexible customization using css styling.
+
Contents
@@ -68,16 +71,23 @@
<div id="visualization"></div>
<script type="text/javascript">
+ // DOM element where the Timeline will be attached
var container = document.getElementById('visualization');
- var items = [
+
+ // Create a DataSet (allows two way data-binding)
+ var items = new vis.DataSet([
{id: 1, content: 'item 1', start: '2013-04-20'},
{id: 2, content: 'item 2', start: '2013-04-14'},
{id: 3, content: 'item 3', start: '2013-04-18'},
{id: 4, content: 'item 4', start: '2013-04-16', end: '2013-04-19'},
{id: 5, content: 'item 5', start: '2013-04-25'},
{id: 6, content: 'item 6', start: '2013-04-27'}
- ];
+ ]);
+
+ // Configuration for the Timeline
var options = {};
+
+ // Create a Timeline
var timeline = new vis.Timeline(container, items, options);
</script>
</body>
@@ -104,7 +114,7 @@ The constructor of the Timeline is vis.Timeline
The constructor accepts three parameters:
- container is the DOM element in which to create the graph.
+ container is the DOM element in which to create the timeline.
items is an Array containing items. The properties of an
@@ -164,18 +174,27 @@ var items = [
Description
-
id
-
String | Number
+
className
+
String
no
-
An id for the item. Using an id is not required but highly
- recommended. An id is needed when dynamically adding, updating,
- and removing items in a DataSet.
+
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 like:
+
+ More details on how to style items can be found in the section
+ Styles.
+
-
start
-
Date
+
content
+
String
yes
-
The start date of the item, for example new Date(2010,09,23).
+
The contents of the item. This can be plain text or html code.
end
@@ -185,20 +204,6 @@ var items = [
If end date is provided, the item is displayed as a range.
If not, the item is displayed as a box.
-
-
content
-
String
-
yes
-
The contents of the item. This can be plain text or html code.
-
-
-
type
-
String
-
'box'
-
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'.
-
-
group
any type
@@ -211,20 +216,33 @@ var items = [
-
className
-
String
+
id
+
String | Number
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 like:
-
- More details on how to style items can be found in the section
- Styles.
+
An id for the item. Using an id is not required but highly
+ recommended. An id is needed when dynamically adding, updating,
+ and removing items in a DataSet.
+
+
+
start
+
Date
+
yes
+
The start date of the item, for example new Date(2010,9,23).
+
+
+
title
+
String
+
none
+
Add a title for the item, displayed when holding the mouse on the item.
+ The title can only contain plain text.
+
+
+
+
type
+
String
+
'box'
+
The type of the item. Can be 'box' (default), 'point', or 'range'.
+ Types 'box' and 'point' need a start date, and type 'range' needs both a start and end date.
@@ -266,20 +284,6 @@ var groups = [
Required
Description
-
-
id
-
String | Number
-
yes
-
An id for the group. The group will display all items having a
- property group which matches the id
- of the group.
-
-
-
content
-
String
-
yes
-
The contents of the group. This can be plain text or html code.
The contents of the group. This can be plain text or html code.
+
+
+
id
+
String | Number
+
yes
+
An id for the group. The group will display all items having a
+ property group which matches the id
+ of the group.
+
+
+
title
+
String
+
none
+
A title for the group, displayed when holding the mouse the groups label.
+ The title can only contain plain text.
+
+
@@ -323,132 +349,132 @@ var options = {
-
Name
-
Type
-
Default
-
Description
+
Name
+
Type
+
Default
+
Description
-
align
-
String
-
"center"
-
Alignment of items with type 'box'. Available values are
- 'center' (default), 'left', or 'right').
+
align
+
String
+
"center"
+
Alignment of items with type 'box'. Available values are
+ 'center' (default), 'left', or 'right').
-
autoResize
-
boolean
-
true
-
If true, the Timeline will automatically detect when its container is resized, and redraw itself accordingly. If false, the Timeline can be forced to repaint after its container has been resized using the function repaint().
+
autoResize
+
boolean
+
true
+
If true, the Timeline will automatically detect when its container is resized, and redraw itself accordingly. If false, the Timeline can be forced to repaint after its container has been resized using the function redraw().
-
editable
-
Boolean | Object
-
false
-
If true, the items in the timeline can be manipulated. Only applicable when option selectable is true. See also the callbacks onAdd, onUpdate, onMove, and onRemove. When editable is an object, one can enable or disable individual manipulation actions.
- See section Editing Items for a detailed explanation.
-
+
editable
+
Boolean | Object
+
false
+
If true, the items in the timeline can be manipulated. Only applicable when option selectable is true. See also the callbacks onAdd, onUpdate, onMove, and onRemove. When editable is an object, one can enable or disable individual manipulation actions.
+ See section Editing Items for a detailed explanation.
+
-
editable.add
-
Boolean
-
false
-
If true, new items can be created by double tapping an empty space in the Timeline. See section Editing Items for a detailed explanation.
+
editable.add
+
Boolean
+
false
+
If true, new items can be created by double tapping an empty space in the Timeline. See section Editing Items for a detailed explanation.
-
editable.remove
-
Boolean
-
false
-
If true, items can be deleted by first selecting them, and then clicking the delete button on the top right of the item. See section Editing Items for a detailed explanation.
+
editable.remove
+
Boolean
+
false
+
If true, items can be deleted by first selecting them, and then clicking the delete button on the top right of the item. See section Editing Items for a detailed explanation.
-
editable.updateGroup
-
Boolean
-
false
-
If true, items can be dragged from one group to another. Only applicable when the Timeline has groups. See section Editing Items for a detailed explanation.
+
editable.updateGroup
+
Boolean
+
false
+
If true, items can be dragged from one group to another. Only applicable when the Timeline has groups. See section Editing Items for a detailed explanation.
-
editable.updateTime
-
Boolean
-
false
-
If true, items can be dragged to another moment in time. See section Editing Items for a detailed explanation.
+
editable.updateTime
+
Boolean
+
false
+
If true, items can be dragged to another moment in time. See section Editing Items for a detailed explanation.
-
end
-
Date | Number | String
-
none
-
The initial end date for the axis of the timeline.
- If not provided, the latest date present in the items set is taken as
- end date.
+
end
+
Date | Number | String
+
none
+
The initial end date for the axis of the timeline.
+ If not provided, the latest date present in the items set is taken as
+ end date.
-
groupOrder
-
String | Function
-
none
-
Order the groups by a field name or custom sort function.
- By default, groups are not ordered.
-
+
groupOrder
+
String | Function
+
none
+
Order the groups by a field name or custom sort function.
+ By default, groups are not ordered.
+
-
height
-
Number | String
-
none
-
The height of the timeline in pixels or as a percentage.
- When height is undefined or null, the height of the timeline is automatically
- adjusted to fit the contents.
- It is possible to set a maximum height using option maxHeight
- to prevent the timeline from getting too high in case of automatically
- calculated height.
-
+
height
+
Number | String
+
none
+
The height of the timeline in pixels or as a percentage.
+ When height is undefined or null, the height of the timeline is automatically
+ adjusted to fit the contents.
+ It is possible to set a maximum height using option maxHeight
+ to prevent the timeline from getting too high in case of automatically
+ calculated height.
+
-
margin.axis
-
Number
-
20
-
The minimal margin in pixels between items and the time axis.
+
margin.axis
+
Number
+
20
+
The minimal margin in pixels between items and the time axis.
-
margin.item
-
Number
-
10
-
The minimal margin in pixels between items.
+
margin.item
+
Number
+
10
+
The minimal margin in pixels between items.
-
max
-
Date | Number | String
-
none
-
Set a maximum Date for the visible range.
- It will not be possible to move beyond this maximum.
-
+
max
+
Date | Number | String
+
none
+
Set a maximum Date for the visible range.
+ It will not be possible to move beyond this maximum.
+
-
maxHeight
-
Number | String
-
none
-
Specifies the maximum height for the Timeline. Can be a number in pixels or a string like "300px".
+
maxHeight
+
Number | String
+
none
+
Specifies the maximum height for the Timeline. Can be a number in pixels or a string like "300px".
-
min
-
Date | Number | String
-
none
-
Set a minimum Date for the visible range.
- It will not be possible to move beyond this minimum.
-
+
min
+
Date | Number | String
+
none
+
Set a minimum Date for the visible range.
+ It will not be possible to move beyond this minimum.
+
@@ -463,41 +489,41 @@ var options = {
Boolean
true
- Specifies whether the Timeline can be moved and zoomed by dragging the window.
- See also option zoomable.
+ Specifies whether the Timeline can be moved and zoomed by dragging the window.
+ See also option zoomable.
-
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.add are set true.
-
+
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.add 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.updateTime or editable.updateGroup 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.updateTime or editable.updateGroup 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.updateTime or editable.updateGroup 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.updateTime or editable.updateGroup 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.remove 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.remove 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
+
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.
-
padding
-
Number
-
5
-
The padding of items, needed to correctly calculate the size
- of item ranges. Must correspond with the css of items, for example when setting options.padding=10, corresponding css is:
+
padding
+
Number
+
5
+
The padding of items, needed to correctly calculate the size
+ 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).
+
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).
-
showCurrentTime
-
boolean
-
true
-
Show a vertical bar at the current time.
+
showCurrentTime
+
boolean
+
true
+
Show a vertical bar at the current time.
-
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. 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.
+
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. 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.
-
showMajorLabels
-
boolean
-
true
-
By default, the timeline shows both minor and major date labels on the
- time axis.
- For example the minor labels show minutes and the major labels show hours.
- When showMajorLabels is false, no major labels
- are shown.
+
showMajorLabels
+
boolean
+
true
+
By default, the timeline shows both minor and major date labels on the
+ time axis.
+ For example the minor labels show minutes and the major labels show hours.
+ When showMajorLabels is false, no major labels
+ are shown.
-
showMinorLabels
-
boolean
-
true
-
By default, the timeline shows both minor and major date labels on the
- time axis.
- For example the minor labels show minutes and the major labels show hours.
- When showMinorLabels is false, no minor labels
- are shown. When both showMajorLabels and
- showMinorLabels are false, no horizontal axis will be
- visible.
+
showMinorLabels
+
boolean
+
true
+
By default, the timeline shows both minor and major date labels on the
+ time axis.
+ For example the minor labels show minutes and the major labels show hours.
+ When showMinorLabels is false, no minor labels
+ are shown. When both showMajorLabels and
+ showMinorLabels are false, no horizontal axis will be
+ visible.
-
stack
-
Boolean
-
true
-
If true (default), items will be stacked on top of each other such that they do not overlap.
+
stack
+
Boolean
+
true
+
If true (default), items will be stacked on top of each other such that they do not overlap.
-
start
-
Date | Number | String
-
none
-
The initial start date for the axis of the timeline.
- If not provided, the earliest date present in the events is taken as start date.
+
start
+
Date | Number | String
+
none
+
The initial start date for the axis of the timeline.
+ If not provided, the earliest date present in the events is taken as start date.
-
type
-
String
-
'box'
-
Specifies the default type for the timeline items. Choose from 'box', 'point', 'range', and 'rangeoverflow'. Note that individual items can override this default type.
-
+
type
+
String
+
none
+
Specifies the default type for the timeline items. Choose from 'box', 'point', and 'range'. Note that individual items can override this default type. If undefined, the Timeline will auto detect the type from the items data: if a start and end date is available, a 'range' will be created, and else, a 'box' is created.
+
-
width
-
String
-
'100%'
-
The width of the timeline in pixels or as a percentage.
+
width
+
String
+
'100%'
+
The width of the timeline in pixels or as a percentage.
-
zoomable
-
Boolean
-
true
-
- Specifies whether the Timeline can be zoomed by pinching or scrolling in the window.
- Only applicable when option moveable is set true.
-
+
zoomable
+
Boolean
+
true
+
+ Specifies whether the Timeline can be zoomed by pinching or scrolling in the window.
+ Only applicable when option moveable is set true.
+
-
zoomMax
-
Number
-
315360000000000
-
Set a maximum zoom interval for the visible range in milliseconds.
- It will not be possible to zoom out further than this maximum.
- Default value equals about 10000 years.
-
+
zoomMax
+
Number
+
315360000000000
+
Set a maximum zoom interval for the visible range in milliseconds.
+ It will not be possible to zoom out further than this maximum.
+ Default value equals about 10000 years.
+
-
zoomMin
-
Number
-
10
-
Set a minimum zoom interval for the visible range in milliseconds.
- It will not be possible to zoom in further than this minimum.
-
+
zoomMin
+
Number
+
10
+
Set a minimum zoom interval for the visible range in milliseconds.
+ It will not be possible to zoom in further than this minimum.
+
Destroy the Timeline. The timeline is removed from memory. all DOM elements and event listeners are cleaned up.
+
+
+
fit()
none
@@ -915,7 +948,7 @@ var options = {
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.
+
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. Parameter `item` typically contains fields `content`, `start`, and optionally `end`. The type of `start` and `end` is determined by the DataSet type configuration and is `Date` by default. When invoked as callback(null), the action will be cancelled.
+ This example shows the most basic functionality of the vis.js Graph2d module. An array or a vis.Dataset can be used as input.
+ In the following examples we'll explore the options Graph2d offest for customization. This example uses all default settings.
+ There are 10 predefined styles that will be cycled through automatically when you add different groups. Alternatively you can
+ create your own styling.
+
+ Graph2d is built upon the framework of the newly refactored timeline. A lot of the timeline options will also apply to Graph2d.
+ In these examples however, we will focus on what's new in Graph2d!
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/graph2d/02_bars.html b/examples/graph2d/02_bars.html
new file mode 100644
index 00000000..1c0ec2b7
--- /dev/null
+++ b/examples/graph2d/02_bars.html
@@ -0,0 +1,57 @@
+
+
+
+ Graph2d | Bar Graph Example
+
+
+
+
+
+
+
+
Graph2d | Bar Graph Example
+
+ This example shows the most the same data as the first example, except we plot the data as bars! The
+ dataAxis (y-axis) icons have been enabled as well. These icons are generated automatically from the CSS
+ styling of the graphs. Finally, we've used the option from Timeline where we draw the x-axis (time-axis) on top.
+
+ The align option can be used to align the bar at the center of the datapoint or on the left or right side of it.
+ This example uses the default center alignment.
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/graph2d/03_groups.html b/examples/graph2d/03_groups.html
new file mode 100644
index 00000000..fb8bedb4
--- /dev/null
+++ b/examples/graph2d/03_groups.html
@@ -0,0 +1,112 @@
+
+
+
+ Graph2d | Groups Example
+
+
+
+
+
+
+
+
+
Graph2d | Groups Example
+
+ This example shows the groups functionality within Graph2d. This works in the same way as it does in Timeline,
+ We have however simplified the constructor to accept groups as well to shorten the code. These groups are the
+ method used in Graph2d to define individual graphs. These groups can be given an individual class as well as all the
+ styling options you can supply to Graph2d! This example, as well as the ones that follow will showcase a few different usages
+ of these options.
+
+ This example also introduces the automatically generated legend. The icons are automatically generated and the label is the
+ content as you define it in the groups. If you have datapoints that are not part of a group, a default group is created with the label: 'default'.
+ In this example, the setting defaultGroup is used to rename the default group to 'ungrouped'.
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/graph2d/04_rightAxis.html b/examples/graph2d/04_rightAxis.html
new file mode 100644
index 00000000..a111edda
--- /dev/null
+++ b/examples/graph2d/04_rightAxis.html
@@ -0,0 +1,126 @@
+
+
+
+ Graph2d | Right Axis Example
+
+
+
+
+
+
+
+
Graph2d | Right Axis Example
+
+ This example shows the all of the graphs outlined on the right side using the yAxisOrientation option.
+ We also show a few custom styles for the graph and show icons on the axis, which are adhering to the custom styling.
+ Finally, the legend is manually positioned. Both the left and right axis
+ have their own legend. If one of the axis is unused, the legend is not shown. The options for the legend have been split
+ in a left and a right segment. Since this example shows the right axis, the right legend is configured.
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/graph2d/05_bothAxis.html b/examples/graph2d/05_bothAxis.html
new file mode 100644
index 00000000..59442a10
--- /dev/null
+++ b/examples/graph2d/05_bothAxis.html
@@ -0,0 +1,138 @@
+
+
+
+ Graph2d | Both Axis Example
+
+
+
+
+
+
+
+
Graph2d | Both Axis Example
+
+ This example shows the some of the graphs outlined on the right side using the yAxisOrientation option within the groups.
+ We also show a few more custom styles for the graphs. Finally, the legend is manually positioned. Both the left and right axis
+ have their own legend. If one of the axis is unused, the legend is not shown. The options for the legend have been split
+ in a left and a right segment. The default position of the left axis has been changed.
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/graph2d/06_interpolation.html b/examples/graph2d/06_interpolation.html
new file mode 100644
index 00000000..0088b780
--- /dev/null
+++ b/examples/graph2d/06_interpolation.html
@@ -0,0 +1,101 @@
+
+
+
+ Graph2d | Interpolation
+
+
+
+
+
+
+
+
Graph2d | Interpolation
+
+ The Graph2d makes use of Catmull-Rom spline interpolation.
+ The user can configure these per group, or globally. In this example we show all 4 possiblities. The differences are in the parametrization of
+ the curves. The options are uniform, chordal and centripetal. Alternatively you can disable the Catmull-Rom interpolation and
+ a linear interpolation will be used. The centripetal parametrization produces the best result (no self intersection, yet follows the line closely) and is therefore the default setting.
+
+ For both the centripetal and chordal parametrization, the distances between the points have to be calculated and this makes these methods computationally intensive
+ if there are very many points. The uniform parametrization still has to do transformations, though it does not have to calculate the distance between point. Finally, the
+ linear interpolation is the fastest method. For more on the Catmull-Rom method, C. Yuksel et al. have an interesting paper titled ″On the parametrization of Catmull-Rom Curves″.
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/graph2d/07_scrollingAndSorting.html b/examples/graph2d/07_scrollingAndSorting.html
new file mode 100644
index 00000000..98e8629d
--- /dev/null
+++ b/examples/graph2d/07_scrollingAndSorting.html
@@ -0,0 +1,74 @@
+
+
+
+ Graph2d | Scrolling and Sorting
+
+
+
+
+
+
+
+
Graph2d | Scrolling and Sorting
+
+ You can determine the height of the Graph2d seperately from the height of the frame. If the graphHeight
+ is defined, and the height is not, the frame will auto-scale to accommodate the graphHeight. If the height
+ is defined as well, the user can scroll up and down vertically as well as horizontally to view the graph.
+
+ Vertical scrolling is planned, though not yet available. The graphHeight also does not conform if only the height is defined.
+
+ You can manually disable the automatic sorting of the datapoints by using the sort option. However, doing so does reduce the optimization
+ of the drawing so if you have a lot of points, keep sort turned on for the best results.
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/graph2d/08_performance.html b/examples/graph2d/08_performance.html
new file mode 100644
index 00000000..9a249f6c
--- /dev/null
+++ b/examples/graph2d/08_performance.html
@@ -0,0 +1,150 @@
+
+
+
+ Graph2d | Performance
+
+
+
+
+
+
+
+
+
+
+
Graph2d | Performance
+
+ This example is a test of the performance of the Graph2d. Select the amount of datapoints you want to plot and press draw.
+ You can choose between the style of the points as well as the interpolation method. This can only be toggled with the buttons.
+ The interpolation options may not look different for this dataset but you can see their effects clearly in example 7.
+
+ Linear interpolation and no points are the settings that will render quickest. By default, Graph2d will downsample when there are more
+ than 1 point per pixel. This can be manually disabled at the cost of performance by using the sampling option.
+
+
+
+ Number of items:
+ Click the draw button to load the data!
+
+
+ Interpolation method:
+
+
+ Points style:
+
+
+
This example shows how to serialize and deserialize JSON data, and load this in the Timeline via a DataSet. Serialization and deserialization is needed when loading or saving data from a server.
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/timeline/18_range_overflow.html b/examples/timeline/18_range_overflow.html
new file mode 100644
index 00000000..8f9f7506
--- /dev/null
+++ b/examples/timeline/18_range_overflow.html
@@ -0,0 +1,53 @@
+
+
+
+ Timeline | Range overflow
+
+
+
+
+
+
+
+
+
+ In case of ranges being spread over a wide range of time, it can be interesting to have the text contents of the ranges overflow the box. This can be achieved by changing the overflow property of the contents to visible with css:
+