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 9f771da0..9b46741f 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,46 @@ http://visjs.org +## 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 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 0eaf8f7a..e4b9c545 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "vis", - "version": "2.0.0", + "version": "3.0.0", "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 ff815c72..2a5d8a24 100644 --- a/dist/vis.css +++ b/dist/vis.css @@ -186,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; } @@ -227,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%; @@ -239,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%; @@ -344,7 +340,177 @@ transition: height .4s ease-in-out, top .4s ease-in-out; } /**/ -div.graph-manipulationDiv { + +.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; @@ -364,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; @@ -380,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; @@ -390,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; @@ -411,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; @@ -491,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 71d48208..525a40b8 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -4,8 +4,8 @@ * * A dynamic, browser-based visualization library. * - * @version 2.0.0 - * @date 2014-06-19 + * @version 3.0.0 + * @date 2014-07-07 * * @license * Copyright (C) 2011-2014 Almende B.V, http://almende.com @@ -420,17 +420,56 @@ util.selectiveExtend = function (props, a, b) { throw new Error('Array with property names expected as first argument'); } - for (var i = 1, len = arguments.length; i < len; i++) { + for (var i = 2; i < arguments.length; i++) { var other = arguments[i]; - for (var p = 0, pp = props.length; p < pp; p++) { + 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; }; @@ -1285,2362 +1324,5303 @@ util.isValidHex = function(hex) { return isOk; }; -util.copyObject = function(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]; + +/** + * This recursively redirects the prototype of JSON objects to the referenceObject + * This is used for default options. + * + * @param referenceObject + * @returns {*} + */ +util.selectiveBridgeObject = function(fields, referenceObject) { + if (typeof referenceObject == "object") { + var objectTo = Object.create(referenceObject); + for (var i = 0; i < fields.length; i++) { + if (referenceObject.hasOwnProperty(fields[i])) { + if (typeof referenceObject[fields[i]] == "object") { + objectTo[fields[i]] = util.bridgeObject(referenceObject[fields[i]]); + } } } + return objectTo; + } + else { + return null; } }; /** - * DataSet - * - * Usage: - * var dataSet = new DataSet({ - * fieldId: '_id', - * type: { - * // ... - * } - * }); - * - * 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 { + 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 guess; }; /** - * Add data. - * Adding an item will fail when there already is an item with the same id. - * @param {Object | Array | DataTable} data - * @param {String} [senderId] Optional sender id - * @return {Array} addedIds Array with the ids of the added 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.add = function (data, senderId) { - var addedIds = [], - id, - me = this; +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; - if (Array.isArray(data)) { - // Array - for (var i = 0, len = data.length; i < len; i++) { - id = me._addItem(data[i]); - addedIds.push(id); + if (high == 0) {guess = -1;} + else if (high == 1) { + value = array[guess][field]; + if (value == target) { + guess = 0; } - } - 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); - } - - id = me._addItem(item); - addedIds.push(id); + else { + guess = -1; } } - else if (data instanceof Object) { - // Single item - id = me._addItem(data); - addedIds.push(id); - } else { - throw new Error('Unknown dataType'); - } + 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 (addedIds.length) { - this._trigger('add', {items: addedIds}, senderId); + 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; + } + } + } } - - 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 + * Created by Alex on 6/20/14. */ -DataSet.prototype.update = function (data, senderId) { - var addedIds = [], - updatedIds = [], - me = this, - fieldId = me._fieldId; - var addOrUpdate = function (item) { - var id = item[fieldId]; - if (me._data[id]) { - // update item - id = me._updateItem(item); - updatedIds.push(id); - } - else { - // add new item - id = me._addItem(item); - addedIds.push(id); - } - }; +var DOMutil = {}; - if (Array.isArray(data)) { - // Array - for (var i = 0, len = data.length; i < len; i++) { - addOrUpdate(data[i]); +/** + * 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 (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); - } +}; - addOrUpdate(item); +/** + * 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 = []; + } } } - else if (data instanceof Object) { - // Single item - addOrUpdate(data); - } - else { - throw new Error('Unknown dataType'); - } - - if (addedIds.length) { - this._trigger('add', {items: addedIds}, senderId); - } - if (updatedIds.length) { - this._trigger('update', {items: updatedIds}, senderId); - } - - return addedIds.concat(updatedIds); }; /** - * Get a data item or multiple items. - * - * Usage: - * - * get() - * get(options: Object) - * get(options: Object, data: Array | DataTable) - * - * get(id: Number | String) - * get(id: Number | String, options: Object) - * get(id: Number | String, options: Object, data: Array | DataTable) - * - * get(ids: Number[] | String[]) - * get(ids: Number[] | String[], options: Object) - * get(ids: Number[] | String[], 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} [returnType] Type of data to be - * returned. Can be 'DataTable' or 'Array' (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. + * 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. * - * @throws Error + * @param elementType + * @param JSONcontainer + * @param svgContainer + * @returns {*} + * @private */ -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]; +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 { - // get([, options] [, data]) - options = arguments[0]; - data = arguments[1]; + // 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; +}; - // 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 + ')'); +/** + * 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(); } - if (returnType == 'DataTable' && !util.isDataTable(data)) { - throw new Error('Parameter "data" must be a DataTable ' + - 'when options.type is "DataTable"'); + else { + // create a new element and add it to the SVG + element = document.createElement(elementType); + DOMContainer.appendChild(element); } } - else if (data) { - returnType = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array'; - } else { - returnType = 'Array'; + // 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); } + JSONcontainer[elementType].used.push(element); + return element; +}; - // 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); - } - } + + +/** + * 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. + * + * @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 { - // 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); + 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; +}; - // 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; - } - } - } +/** + * draw a bar SVG element centered on the X coordinate + * + * @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); }; - /** - * 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 + * + * Usage: + * var dataSet = new DataSet({ + * fieldId: '_id', + * type: { + * // ... + * } + * }); + * + * 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 + * + * @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] 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); - } - } - } +DataSet.prototype.off = function(event, callback) { + var subscribers = this._subscribers[event]; + if (subscribers) { + this._subscribers[event] = subscribers.filter(function (listener) { + return (listener.callback != callback); + }); } }; +// TODO: make this function deprecated (replaced with `on` since version 0.5) +DataSet.prototype.unsubscribe = DataSet.prototype.off; + /** - * 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 + * Trigger an event + * @param {String} event + * @param {Object | null} params + * @param {String} [senderId] Optional id of the sender. + * @private */ -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)); - } - } +DataSet.prototype._trigger = function (event, params, senderId) { + if (event == '*') { + throw new Error('Cannot trigger event *'); } - // order items - if (options && options.order) { - this._sort(mappedItems, options.order); + var subscribers = []; + if (event in this._subscribers) { + subscribers = subscribers.concat(this._subscribers[event]); + } + if ('*' in this._subscribers) { + subscribers = subscribers.concat(this._subscribers['*']); } - return mappedItems; + for (var i = 0; i < subscribers.length; i++) { + var subscriber = subscribers[i]; + if (subscriber.callback) { + subscriber.callback(event, params, senderId || null); + } + } }; /** - * Filter the fields of an item - * @param {Object} item - * @param {String[]} fields Field names - * @return {Object} filteredItem - * @private + * Add data. + * Adding an item will fail when there already is an item with the same id. + * @param {Object | Array | DataTable} data + * @param {String} [senderId] Optional sender id + * @return {Array} addedIds Array with the ids of the added items */ -DataSet.prototype._filterFields = function (item, fields) { - var filteredItem = {}; +DataSet.prototype.add = function (data, senderId) { + var addedIds = [], + id, + me = this; - for (var field in item) { - if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) { - filteredItem[field] = item[field]; + if (Array.isArray(data)) { + // Array + for (var i = 0, len = data.length; i < len; i++) { + id = me._addItem(data[i]); + addedIds.push(id); } } + 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 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); - }); + id = me._addItem(item); + addedIds.push(id); + } } - else if (typeof order === 'function') { - // order by sort function - items.sort(order); + else if (data instanceof Object) { + // Single item + id = me._addItem(data); + addedIds.push(id); } - // 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'); + throw new Error('Unknown dataType'); + } + + if (addedIds.length) { + this._trigger('add', {items: addedIds}, senderId); } + + return addedIds; }; /** - * 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 + * 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} removedIds + * @return {Array} updatedIds The ids of the added or updated items */ -DataSet.prototype.remove = function (id, senderId) { - var removedIds = [], - i, len, removedId; +DataSet.prototype.update = function (data, senderId) { + var addedIds = [], + updatedIds = [], + me = this, + fieldId = me._fieldId; - if (Array.isArray(id)) { - for (i = 0, len = id.length; i < len; i++) { - removedId = this._remove(id[i]); - if (removedId != null) { - removedIds.push(removedId); - } + var addOrUpdate = function (item) { + var id = item[fieldId]; + if (me._data[id]) { + // update item + id = me._updateItem(item); + updatedIds.push(id); } - } - else { - removedId = this._remove(id); - if (removedId != null) { - removedIds.push(removedId); + else { + // add new item + id = me._addItem(item); + addedIds.push(id); } - } + }; - if (removedIds.length) { - this._trigger('remove', {items: removedIds}, senderId); + if (Array.isArray(data)) { + // Array + for (var i = 0, len = data.length; i < len; i++) { + addOrUpdate(data[i]); + } } + 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 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; + addOrUpdate(item); } } - else if (id instanceof Object) { - var itemId = id[this._fieldId]; - if (itemId && this._data[itemId]) { - delete this._data[itemId]; - return itemId; - } + else if (data instanceof Object) { + // Single item + addOrUpdate(data); + } + else { + throw new Error('Unknown dataType'); } - 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); + if (addedIds.length) { + this._trigger('add', {items: addedIds}, senderId); + } + if (updatedIds.length) { + this._trigger('update', {items: updatedIds}, senderId); + } - return ids; + return addedIds.concat(updatedIds); }; /** - * 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 + * Get a data item or multiple items. + * + * Usage: + * + * get() + * get(options: Object) + * get(options: Object, data: Array | DataTable) + * + * get(id: Number | String) + * get(id: Number | String, options: Object) + * get(id: Number | String, options: Object, data: Array | DataTable) + * + * get(ids: Number[] | String[]) + * get(ids: Number[] | String[], options: Object) + * get(ids: Number[] | String[], 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} [returnType] Type of data to be + * returned. Can be 'DataTable' or 'Array' (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.max = function (field) { - var data = this._data, - max = null, - maxField = null; +DataSet.prototype.get = function (args) { + var me = this; - 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; - } - } + // 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]; } - - 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; - } - } + 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]; } - 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; + // determine the return type + var returnType; + if (options && options.returnType) { + returnType = (options.returnType == 'DataTable') ? 'DataTable' : 'Array'; - 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 (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 (fieldType) { - for (i = 0; i < values.length; i++) { - values[i] = util.convert(values[i], fieldType); + 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'; + } - 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]; + // 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) { - // 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'); + // return a single item + item = me._getItem(id, type); + if (filter && !filter(item)) { + item = null; } } - else { - // generate an id - id = util.randomUUID(); - item[this._fieldId] = id; + 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); + } + } } - - 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); + 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); + } + } } } - 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; + // order the results + if (options && options.order && id == undefined) { + this._sort(items, options.order); + } - // get the item from the dataset - var raw = this._data[id]; - if (!raw) { - return null; + // 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); + } + } } - // 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]); + // 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 { - // no field types specified, no converting needed - for (field in raw) { - if (raw.hasOwnProperty(field)) { - value = raw[field]; - converted[field] = value; + // 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; } } } - 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 + * 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._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); - } +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]); + } + } - return 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; }; /** - * Get an array with the column names of a Google DataTable - * @param {DataTable} dataTable - * @return {String[]} columnNames - * @private + * Returns the DataSet itself. Is overwritten for example by the DataView, + * which returns the DataSet it is connected to instead. */ -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; +DataSet.prototype.getDataSet = function () { + return this; }; /** - * Append an item as a row to the dataTable - * @param dataTable - * @param columns - * @param item - * @private + * 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._appendRow = function (dataTable, columns, item) { - var row = dataTable.addRow(); +DataSet.prototype.forEach = function (callback, options) { + var filter = options && options.filter, + type = options && options.type || this._options.type, + data = this._data, + item, + id; - for (var col = 0, cols = columns.length; col < cols; col++) { - var field = columns[col]; - dataTable.setValue(row, col, item[field]); + 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); + } + } + } } }; /** - * 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 + * 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 */ -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 +DataSet.prototype.map = function (callback, options) { + var filter = options && options.filter, + type = options && options.type || this._options.type, + mappedItems = [], + data = this._data, + item; - var me = this; - this.listener = function () { - me._onEvent.apply(me, arguments); - }; + // 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)); + } + } + } - this.setData(data); -} + // order items + if (options && options.order) { + this._sort(mappedItems, options.order); + } -// TODO: implement a function .config() to dynamically update things like configured filter -// and trigger changes accordingly + return mappedItems; +}; /** - * Set a data source for the view - * @param {DataSet | DataView} data + * Filter the fields of an item + * @param {Object} item + * @param {String[]} fields Field names + * @return {Object} filteredItem + * @private */ -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); - } +DataSet.prototype._filterFields = function (item, fields) { + var filteredItem = {}; - // trigger a remove of all items in memory - ids = []; - for (var id in this._ids) { - if (this._ids.hasOwnProperty(id)) { - ids.push(id); - } + for (var field in item) { + if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) { + filteredItem[field] = item[field]; } - 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}); + return filteredItem; +}; - // subscribe to new dataset - if (this._data.on) { - this._data.on('*', this.listener); - } +/** + * 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'); } }; /** - * 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 + * 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 */ -DataView.prototype.get = function (args) { - var me = this; +DataSet.prototype.remove = function (id, senderId) { + var removedIds = [], + i, len, removedId; - // 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]; + 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 { - // get([, options] [, data]) - options = arguments[0]; - data = arguments[1]; + removedId = this._remove(id); + if (removedId != null) { + removedIds.push(removedId); + } } - // extend the options with the default options and provided options - var viewOptions = util.extend({}, this._options, options); + if (removedIds.length) { + this._trigger('remove', {items: removedIds}, senderId); + } - // 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); + 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; +}; - // build up the call to the linked data set - var getArguments = []; - if (ids != undefined) { - getArguments.push(ids); +/** + * 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; + } + } } - getArguments.push(viewOptions); - getArguments.push(data); - return this._data && this._data.get.apply(this._data, getArguments); + return max; }; /** - * 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 + * 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 */ -DataView.prototype.getIds = function (options) { - var ids; +DataSet.prototype.min = function (field) { + var data = this._data, + min = null, + minField = null; - if (this._data) { - var defaultFilter = this._options.filter; - var filter; + 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; + } + } + } - if (options && options.filter) { - if (defaultFilter) { - filter = function (item) { - return defaultFilter(item) && options.filter(item); + 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; } } - else { - filter = options.filter; + if (!exists && (value !== undefined)) { + values[count] = value; + count++; } } - else { - filter = defaultFilter; + } + + if (fieldType) { + for (i = 0; i < values.length; i++) { + values[i] = util.convert(values[i], fieldType); } + } - ids = this._data.getIds({ - filter: filter, - order: options && options.order - }); + 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 { - ids = []; + // generate an id + id = util.randomUUID(); + item[this._fieldId] = id; } - return ids; + 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; }; /** - * 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 + * 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 */ -DataView.prototype._onEvent = function (event, params, senderId) { - var i, len, id, item, - ids = params && params.items, - data = this._data, +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); - } - } + 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; + } + // 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 { + DOMutil.cleanupElements(this.DOMelements); + return false; + } +}; + +/** + * 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 (orientation == 'left') { + label.style.left = '-' + this.options.labelOffsetX + 'px'; + label.style.textAlign = "right"; + } + else { + label.style.right = '-' + this.options.labelOffsetX + 'px'; + label.style.textAlign = "left"; + } + + 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; + } +}; + +/** + * 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 = ''; + + if (orientation == 'left') { + line.style.left = (this.width - offset) + 'px'; + } + else { + line.style.right = (this.width - offset) + 'px'; + } + + line.style.width = width + 'px'; + line.style.top = y + 'px'; + } +}; + + +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); + } + + 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); + + this.props.majorCharHeight = measureCharMajor.clientHeight; + this.props.majorCharWidth = measureCharMajor.clientWidth; + + this.dom.frame.removeChild(measureCharMajor); + } +}; + +/** + * 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 + } + } + }; + + // 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); + } + }; + + // 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); + } + }; + + 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"; + } + } + }); + 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(); + +/** + * Create the HTML DOM for the ItemSet + */ +LineGraph.prototype._create = function(){ + var frame = document.createElement('div'); + frame.className = 'LineGraph'; + this.dom.frame = frame; + + // 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); + + // data axis + this.options.dataAxis.orientation = 'left'; + this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg); + + 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; + } + } + } + } + + if (this.yAxisLeft) { + if (options.dataAxis !== undefined) { + this.yAxisLeft.setOptions(this.options.dataAxis); + this.yAxisRight.setOptions(this.options.dataAxis); + } + } + + 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(); + } +}; + +/** + * 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); + } +}; + +/** + * Show the component in the DOM (when not already visible). + * @return {Boolean} changed + */ +LineGraph.prototype.show = function() { + // show frame containing the items + if (!this.dom.frame.parentNode) { + this.body.dom.center.appendChild(this.dom.frame); + } +}; + + +/** + * 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 { + 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(); +}; + +/** + * Set groups + * @param {vis.DataSet} groups + */ +LineGraph.prototype.setGroups = function(groups) { + var me = this, + ids; + + // 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 + } + + // 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'); + } + + 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(); +}; + + + +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]); + } + + 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(); +}; + +/** + * update a group object + * + * @param group + * @param groupId + * @private + */ +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]); + } + } + else { + 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(); +}; + +LineGraph.prototype._updateAllGroupData = function () { + if (this.itemsData != null) { + // ~450 ms @ 500k + + var groupsContent = {}; + for (var groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + groupsContent[groupId] = []; + } + } + 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"}} +// )); +// } +// } + } +}; + +/** + * 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 + */ +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; + } + } + } + } + + // 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); + } + 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(); +}; + + +/** + * Redraw the component, mandatory function + * @return {boolean} Returns true if the component is resized + */ +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; + + // calculate actual size and position + this.width = this.dom.frame.offsetWidth; + + // 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(); + } + + this.legendLeft.redraw(); + this.legendRight.redraw(); + + return resized; +}; + +/** + * Update and redraw the graph. + * + */ +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); + } + + // 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; + } + + // 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)) + } + + // 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); + } + } + } + } + + // cleanup unused svg elements + DOMutil.cleanupElements(this.svgElements); +}; + +/** + * 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 + */ +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'; + + // 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'; + } + + 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); + } + } + + 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; +}; + +/** + * 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 + */ +LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) { + var changed = false; + if (axisUsed == false) { + if (axis.dom.frame.parentNode) { + axis.hide(); + changed = true; + } + } + else { + if (!axis.dom.frame.parentNode) { + axis.show(); + changed = true; + } + } + return changed; +}; + + +/** + * draw a bar graph + * @param datapoints + * @param group + */ +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; + + if (group.options.barChart.align == 'left') {offset -= 0.5*width;} + else if (group.options.barChart.align == 'right') {offset += 0.5*width;} + + 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); + } + + // draw points + if (group.options.drawPoints.enabled == true) { + this._drawPoints(dataset, group, this.svgElements, this.svg, offset); + } + } + } +}; + + +/** + * draw a line graph + * + * @param datapoints + * @param group + */ +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); + + // 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); + } + } + } +}; + +/** + * draw the data points + * + * @param dataset + * @param JSONcontainer + * @param svg + * @param group + */ +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); + } +}; + + + +/** + * 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 + */ +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))); + } + + 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}; +}; + +/** + * 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 + */ +LineGraph.prototype._convertYvalues = function (datapoints, group) { + var extractedData = []; + var xValue, yValue; + var axis = this.yAxisLeft; + var svgHeight = Number(this.svg.style.height.replace("px","")); + + 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; +}; + + +/** + * 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 + */ +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++) { + + p0 = (i == 0) ? data[0] : data[i-1]; + p1 = data[i]; + p2 = data[i+1]; + p3 = (i + 2 < length) ? data[i+2] : p2; + + + // 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 + + // 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; +}; + +/** + * 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 + */ +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 ] + + // [ 0 1 0 0 ] + // [ -d2pow2a/N A/N d1pow2a/N 0 ] + // [ 0 d3pow2a/M B/M -d2pow2a/M ] + // [ 0 0 1 0 ] + + 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); + + 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;} + + bp1 = { x: ((-d2pow2A * p0.x + A*p1.x + d1pow2A * p2.x) * N), + y: ((-d2pow2A * p0.y + A*p1.y + d1pow2A * p2.y) * N)}; + + 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 + " "; + } + + 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; +}; + + + + + + +/** + * @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. + * + * 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 + * + * 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. + * + * Version: 1.2 + * + * @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 + */ +function DataStep(start, end, minimumStep, containerHeight, forcedStepSize) { + // variables + this.current = 0; + + this.autoScale = true; + this.stepIndex = 0; + this.step = 1; + this.scale = 1; + + this.marginStart; + this.marginEnd; + + this.majorSteps = [1, 2, 5, 10]; + this.minorSteps = [0.25, 0.5, 1, 2]; + + this.setRange(start, end, minimumStep, containerHeight, forcedStepSize); +} + + + +/** + * 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 + */ +DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight, forcedStepSize) { + this._start = start; + this._end = end; + + if (this.autoScale) { + this.setMinimumStep(minimumStep, containerHeight, forcedStepSize); + } + this.setFirst(); +}; + +/** + * 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; + } + } + if (solutionFound == true) { + break; + } + } + this.stepIndex = minorStepIdx; + this.scale = magnitudefactor; + this.step = magnitudefactor * this.minorSteps[minorStepIdx]; +}; + + +/** + * Set the range iterator to the start date. + */ +DataStep.prototype.first = function() { + this.setFirst(); +}; + +/** + * Round the current date to the first minor date value + * This must be executed once when the current date is set to start Date + */ +DataStep.prototype.setFirst = function() { + var niceStart = this._start - (this.scale * this.minorSteps[this.stepIndex]); + var niceEnd = this._end + (this.scale * this.minorSteps[this.stepIndex]); + + this.marginEnd = this.roundToMinor(niceEnd); + this.marginStart = this.roundToMinor(niceStart); + this.marginRange = this.marginEnd - this.marginStart; + + this.current = this.marginEnd; + +}; + +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; + } +} + + +/** + * 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); +}; + +/** + * Do the next step + */ +DataStep.prototype.next = function() { + var prev = this.current; + this.current -= this.step; + + // 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); + } + else if (toPrecision[i] == "." || toPrecision[i] == ",") { + toPrecision = toPrecision.slice(0,i); + break; + } + else{ + break; + } + } + + + return toPrecision; +}; + + + +/** + * 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 + */ +var stack = {}; + +/** + * Order items by their start data + * @param {Item[]} items + */ +stack.orderByStart = function(items) { + items.sort(function (a, b) { + return a.data.start - b.data.start; + }); +}; + +/** + * Order items by their end date. If they have no end date, their start date + * is used. + * @param {Item[]} 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; + + return aTime - bTime; + }); +}; + +/** + * Adjust vertical positions of the items such that they don't overlap each + * other. + * @param {Item[]} items + * All visible items + * @param {{item: number, axis: number}} margin + * Margins between items and between items and the axis. + * @param {boolean} [force=false] + * If true, all items will be repositioned. If false (default), only + * items having a top===null will be re-stacked + */ +stack.stack = function(items, margin, force) { + var i, iMax; + + if (force) { + // reset top position of all items + for (i = 0, iMax = items.length; i < iMax; i++) { + items[i].top = null; + } + } + + // calculate new, non-overlapping positions + for (i = 0, iMax = items.length; i < iMax; i++) { + var item = items[i]; + if (item.top === null) { + // initialize top position + item.top = margin.axis; + + do { + // TODO: optimize checking for overlap. when there is a gap without items, + // you only need to check for items from the next item on, not from zero + var collidingItem = null; + for (var j = 0, jj = items.length; j < jj; j++) { + var other = items[j]; + if (other.top !== null && other !== item && stack.collision(item, other, margin.item)) { + collidingItem = other; + break; + } + } + + if (collidingItem != null) { + // There is a collision. Reposition the items above the colliding element + item.top = collidingItem.top + collidingItem.height + margin.item; + } + } while (collidingItem); + } + } +}; + +/** + * Adjust vertical positions of the items without stacking them + * @param {Item[]} items + * All visible items + * @param {{item: number, axis: number}} margin + * Margins between items and between items and the axis. + */ +stack.nostack = function(items, margin) { + var i, iMax; + + // reset top position of all items + for (i = 0, iMax = items.length; i < iMax; i++) { + items[i].top = margin.axis; + } +}; + +/** + * Test if the two provided items collide + * The items must have parameters left, width, top, and height. + * @param {Item} a The first item + * @param {Item} b The second item + * @param {Number} margin A minimum required margin. + * If margin is provided, the two items will be + * marked colliding when they overlap or + * when the margin between the two is smaller than + * the requested margin. + * @return {boolean} true if a and b collide, else false + */ +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) && + (a.top + a.height + margin) > b.top); +}; + +/** + * @constructor TimeStep + * The class TimeStep is an iterator for dates. You provide a start date and an + * end date. The class itself determines the best scale (step size) based on the + * provided start Date, end Date, and minimumStep. + * + * 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 + * + * 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 TimeStep has scales ranging from milliseconds, seconds, minutes, hours, + * days, to years. + * + * Version: 1.2 + * + * @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 + */ +function TimeStep(start, end, minimumStep) { + // variables + this.current = new Date(); + this._start = new Date(); + this._end = new Date(); + + this.autoScale = true; + this.scale = TimeStep.SCALE.DAY; + this.step = 1; + + // initialize the range + this.setRange(start, end, minimumStep); +} + +/// enum scale +TimeStep.SCALE = { + MILLISECOND: 1, + SECOND: 2, + MINUTE: 3, + HOUR: 4, + DAY: 5, + WEEKDAY: 6, + MONTH: 7, + YEAR: 8 +}; + + +/** + * 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 {Date} [start] The start date and time. + * @param {Date} [end] The end date and time. + * @param {int} [minimumStep] Optional. Minimum step size in milliseconds + */ +TimeStep.prototype.setRange = function(start, end, minimumStep) { + if (!(start instanceof Date) || !(end instanceof Date)) { + throw "No legal start or end date in method setRange"; + } + + this._start = (start != undefined) ? new Date(start.valueOf()) : new Date(); + this._end = (end != undefined) ? new Date(end.valueOf()) : new Date(); + + if (this.autoScale) { + this.setMinimumStep(minimumStep); + } +}; + +/** + * Set the range iterator to the start date. + */ +TimeStep.prototype.first = function() { + this.current = new Date(this._start.valueOf()); + this.roundToMinor(); +}; + +/** + * Round the current date to the first minor date value + * This must be executed once when the current date is set to start Date + */ +TimeStep.prototype.roundToMinor = function() { + // round to floor + // IMPORTANT: we have no breaks in this switch! (this is no bug) + //noinspection FallthroughInSwitchStatementJS + 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: // intentional fall through + 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); + //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds + } + + if (this.step != 1) { + // round down to the first minor value that is a multiple of the current step size + 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: // intentional fall through + 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); break; + default: break; + } + } +}; + +/** + * Check if the there is a next step + * @return {boolean} true if the current date has not passed the end date + */ +TimeStep.prototype.hasNext = function () { + return (this.current.valueOf() <= this._end.valueOf()); +}; + +/** + * Do the next step + */ +TimeStep.prototype.next = function() { + var prev = this.current.valueOf(); + + // Two cases, needed to prevent issues with switching daylight savings + // (end of March and end of October) + 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() + this.step * 1000); break; + case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break; + case TimeStep.SCALE.HOUR: + this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60); + // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...) + var h = this.current.getHours(); + this.current.setHours(h - (h % this.step)); + break; + case TimeStep.SCALE.WEEKDAY: // intentional fall through + 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); break; + default: break; + } + } + 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: // intentional fall through + 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); break; + default: break; + } + } + + if (this.step != 1) { + // round down to the correct major value + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break; + case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break; + case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break; + case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break; + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break; + case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break; + case TimeStep.SCALE.YEAR: break; // nothing to do for year + default: break; + } + } + + // safety mechanism: if current time is still unchanged, move to the end + if (this.current.valueOf() == prev) { + this.current = new Date(this._end.valueOf()); + } +}; + + +/** + * Get the current datetime + * @return {Date} current The current date + */ +TimeStep.prototype.getCurrent = function() { + return this.current; +}; + +/** + * Set a custom scale. Autoscaling will be disabled. + * For example setScale(SCALE.MINUTES, 5) will result + * in minor steps of 5 minutes, and major steps of an hour. + * + * @param {TimeStep.SCALE} newScale + * A scale. Choose from SCALE.MILLISECOND, + * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR, + * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH, + * SCALE.YEAR. + * @param {Number} newStep A step size, by default 1. Choose for + * example 1, 2, 5, or 10. + */ +TimeStep.prototype.setScale = function(newScale, newStep) { + this.scale = newScale; + + if (newStep > 0) { + this.step = newStep; + } + + this.autoScale = false; +}; + +/** + * Enable or disable autoscaling + * @param {boolean} enable If true, autoascaling is set true + */ +TimeStep.prototype.setAutoScale = function (enable) { + this.autoScale = enable; +}; - 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); +/** + * Automatically determine the scale that bests fits the provided minimum step + * @param {Number} [minimumStep] The minimum step size in milliseconds + */ +TimeStep.prototype.setMinimumStep = function(minimumStep) { + if (minimumStep == undefined) { + return; + } - 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 :-( - } - } - } + var stepYear = (1000 * 60 * 60 * 24 * 30 * 12); + var stepMonth = (1000 * 60 * 60 * 24 * 30); + var stepDay = (1000 * 60 * 60 * 24); + var stepHour = (1000 * 60 * 60); + var stepMinute = (1000 * 60); + var stepSecond = (1000); + var stepMillisecond= (1); - break; + // find the smallest step that is larger than the provided minimumStep + if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;} + if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;} + if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;} + if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;} + if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;} + if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;} + if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;} + if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;} + if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;} + if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;} + if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;} + if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;} + if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;} + if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;} + if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;} + if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;} + if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;} + if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;} + if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;} + if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;} + if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;} + if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;} + if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;} + if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;} + if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;} + if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;} + if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;} + if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;} + if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;} +}; - 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); - } - } +/** + * Snap a date to a rounded value. + * The snap intervals are dependent on the current scale and step. + * @param {Date} date the date to be snapped. + * @return {Date} snappedDate + */ +TimeStep.prototype.snap = function(date) { + var clone = new Date(date.valueOf()); - break; + if (this.scale == TimeStep.SCALE.YEAR) { + var year = clone.getFullYear() + Math.round(clone.getMonth() / 12); + clone.setFullYear(Math.round(year / this.step) * this.step); + clone.setMonth(0); + clone.setDate(0); + clone.setHours(0); + clone.setMinutes(0); + clone.setSeconds(0); + clone.setMilliseconds(0); + } + else if (this.scale == TimeStep.SCALE.MONTH) { + if (clone.getDate() > 15) { + clone.setDate(1); + clone.setMonth(clone.getMonth() + 1); + // important: first set Date to 1, after that change the month. + } + else { + clone.setDate(1); } - if (added.length) { - this._trigger('add', {items: added}, senderId); + clone.setHours(0); + clone.setMinutes(0); + clone.setSeconds(0); + clone.setMilliseconds(0); + } + else if (this.scale == TimeStep.SCALE.DAY) { + //noinspection FallthroughInSwitchStatementJS + switch (this.step) { + case 5: + case 2: + clone.setHours(Math.round(clone.getHours() / 24) * 24); break; + default: + clone.setHours(Math.round(clone.getHours() / 12) * 12); break; } - if (updated.length) { - this._trigger('update', {items: updated}, senderId); + clone.setMinutes(0); + clone.setSeconds(0); + clone.setMilliseconds(0); + } + else if (this.scale == TimeStep.SCALE.WEEKDAY) { + //noinspection FallthroughInSwitchStatementJS + switch (this.step) { + case 5: + case 2: + clone.setHours(Math.round(clone.getHours() / 12) * 12); break; + default: + clone.setHours(Math.round(clone.getHours() / 6) * 6); break; } - if (removed.length) { - this._trigger('remove', {items: removed}, senderId); + clone.setMinutes(0); + clone.setSeconds(0); + clone.setMilliseconds(0); + } + else if (this.scale == TimeStep.SCALE.HOUR) { + switch (this.step) { + case 4: + clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break; + default: + clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break; + } + clone.setSeconds(0); + clone.setMilliseconds(0); + } else if (this.scale == TimeStep.SCALE.MINUTE) { + //noinspection FallthroughInSwitchStatementJS + switch (this.step) { + case 15: + case 10: + clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5); + clone.setSeconds(0); + break; + case 5: + clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break; + default: + clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break; + } + clone.setMilliseconds(0); + } + else if (this.scale == TimeStep.SCALE.SECOND) { + //noinspection FallthroughInSwitchStatementJS + switch (this.step) { + case 15: + case 10: + clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5); + clone.setMilliseconds(0); + break; + case 5: + clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break; + default: + clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break; } } + else if (this.scale == TimeStep.SCALE.MILLISECOND) { + var step = this.step > 5 ? this.step / 2 : 1; + clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step); + } + + return clone; +}; + +/** + * 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. + */ +TimeStep.prototype.isMajor = function() { + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: + return (this.current.getMilliseconds() == 0); + case TimeStep.SCALE.SECOND: + return (this.current.getSeconds() == 0); + case TimeStep.SCALE.MINUTE: + return (this.current.getHours() == 0) && (this.current.getMinutes() == 0); + // Note: this is no bug. Major label is equal for both minute and hour scale + case TimeStep.SCALE.HOUR: + return (this.current.getHours() == 0); + case TimeStep.SCALE.WEEKDAY: // intentional fall through + case TimeStep.SCALE.DAY: + return (this.current.getDate() == 1); + case TimeStep.SCALE.MONTH: + return (this.current.getMonth() == 0); + case TimeStep.SCALE.YEAR: + return false; + default: + return false; + } +}; + + +/** + * Returns formatted text for the minor axislabel, depending on the current + * date and the scale. For example when scale is MINUTE, the current time is + * formatted as "hh:mm". + * @param {Date} [date] custom date. if not provided, current date is taken + */ +TimeStep.prototype.getLabelMinor = function(date) { + if (date == undefined) { + date = this.current; + } + + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS'); + case TimeStep.SCALE.SECOND: return moment(date).format('s'); + case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm'); + case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm'); + case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D'); + case TimeStep.SCALE.DAY: return moment(date).format('D'); + case TimeStep.SCALE.MONTH: return moment(date).format('MMM'); + case TimeStep.SCALE.YEAR: return moment(date).format('YYYY'); + default: return ''; + } }; -// 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; /** - * Utility functions for ordering and stacking of items + * Returns formatted text for the major axis label, depending on the current + * date and the scale. For example when scale is MINUTE, the major scale is + * hours, and the hour will be formatted as "hh". + * @param {Date} [date] custom date. if not provided, current date is taken */ -var stack = {}; +TimeStep.prototype.getLabelMajor = function(date) { + if (date == undefined) { + date = this.current; + } -/** - * Order items by their start data - * @param {Item[]} items - */ -stack.orderByStart = function(items) { - items.sort(function (a, b) { - return a.data.start - b.data.start; - }); + //noinspection FallthroughInSwitchStatementJS + switch (this.scale) { + case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss'); + case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm'); + case TimeStep.SCALE.MINUTE: + case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM'); + case TimeStep.SCALE.WEEKDAY: + case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY'); + case TimeStep.SCALE.MONTH: return moment(date).format('YYYY'); + case TimeStep.SCALE.YEAR: return ''; + default: return ''; + } }; /** - * Order items by their end date. If they have no end date, their start date - * is used. - * @param {Item[]} items + * @constructor Range + * 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 {{dom: Object, domProps: Object, emitter: Emitter}} body + * @param {Object} [options] See description at Range.setOptions */ -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; +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 - return aTime - bTime; - }); -}; + this.body = body; -/** - * Adjust vertical positions of the items such that they don't overlap each - * other. - * @param {Item[]} items - * All visible items - * @param {{item: number, axis: number}} margin - * Margins between items and between items and the axis. - * @param {boolean} [force=false] - * If true, all items will be repositioned. If false (default), only - * items having a top===null will be re-stacked - */ -stack.stack = function(items, margin, force) { - var i, iMax; + // 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); - if (force) { - // reset top position of all items - for (i = 0, iMax = items.length; i < iMax; i++) { - items[i].top = null; - } - } + this.props = { + touch: {} + }; - // calculate new, non-overlapping positions - for (i = 0, iMax = items.length; i < iMax; i++) { - var item = items[i]; - if (item.top === null) { - // initialize top position - item.top = margin.axis; + // drag listeners for dragging + 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)); - do { - // TODO: optimize checking for overlap. when there is a gap without items, - // you only need to check for items from the next item on, not from zero - var collidingItem = null; - for (var j = 0, jj = items.length; j < jj; j++) { - var other = items[j]; - if (other.top !== null && other !== item && stack.collision(item, other, margin.item)) { - collidingItem = other; - break; - } - } + // ignore dragging when holding + this.body.emitter.on('hold', this._onHold.bind(this)); - if (collidingItem != null) { - // There is a collision. Reposition the items above the colliding element - item.top = collidingItem.top + collidingItem.height + margin.item; - } - } while (collidingItem); - } - } -}; + // mouse wheel for zooming + this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this)); + this.body.emitter.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF + + // pinch to zoom + this.body.emitter.on('touch', this._onTouch.bind(this)); + this.body.emitter.on('pinch', this._onPinch.bind(this)); + + this.setOptions(options); +} + +Range.prototype = new Component(); /** - * Adjust vertical positions of the items without stacking them - * @param {Item[]} items - * All visible items - * @param {{item: number, axis: number}} margin - * Margins between items and between items and the axis. + * 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 */ -stack.nostack = function(items, margin) { - var i, iMax; +Range.prototype.setOptions = function (options) { + if (options) { + // copy the options that we know + var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable']; + util.selectiveExtend(fields, this.options, options); - // reset top position of all items - for (i = 0, iMax = items.length; i < iMax; i++) { - items[i].top = margin.axis; + if ('start' in options || 'end' in options) { + // apply a new range. both start and end are optional + this.setRange(options.start, options.end); + } } }; /** - * Test if the two provided items collide - * The items must have parameters left, width, top, and height. - * @param {Item} a The first item - * @param {Item} b The second item - * @param {Number} margin A minimum required margin. - * If margin is provided, the two items will be - * marked colliding when they overlap or - * when the margin between the two is smaller than - * the requested margin. - * @return {boolean} true if a and b collide, else false + * Test whether direction has a valid value + * @param {String} direction 'horizontal' or 'vertical' */ -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) && - (a.top + a.height + margin) > b.top); -}; +function validateDirection (direction) { + if (direction != 'horizontal' && direction != 'vertical') { + throw new TypeError('Unknown direction "' + direction + '". ' + + 'Choose "horizontal" or "vertical".'); + } +} /** - * @constructor TimeStep - * The class TimeStep is an iterator for dates. You provide a start date and an - * end date. The class itself determines the best scale (step size) based on the - * provided start Date, end Date, and minimumStep. - * - * 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 - * - * 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 TimeStep has scales ranging from milliseconds, seconds, minutes, hours, - * days, to years. - * - * Version: 1.2 - * - * @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 + * Set a new start and end range + * @param {Number} [start] + * @param {Number} [end] */ -function TimeStep(start, end, minimumStep) { - // variables - this.current = new Date(); - this._start = new Date(); - this._end = new Date(); - - this.autoScale = true; - this.scale = TimeStep.SCALE.DAY; - this.step = 1; - - // initialize the range - this.setRange(start, end, minimumStep); -} - -/// enum scale -TimeStep.SCALE = { - MILLISECOND: 1, - SECOND: 2, - MINUTE: 3, - HOUR: 4, - DAY: 5, - WEEKDAY: 6, - MONTH: 7, - YEAR: 8 +Range.prototype.setRange = function(start, end) { + var changed = this._applyRange(start, end); + if (changed) { + var params = { + start: new Date(this.start), + end: new Date(this.end) + }; + this.body.emitter.emit('rangechange', params); + this.body.emitter.emit('rangechanged', params); + } }; - /** - * 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 {Date} [start] The start date and time. - * @param {Date} [end] The end date and time. - * @param {int} [minimumStep] Optional. Minimum step size in milliseconds + * Set a new start and end range. This method is the same as setRange, but + * does not trigger a range change and range changed event, and it returns + * true when the range is changed + * @param {Number} [start] + * @param {Number} [end] + * @return {Boolean} changed + * @private */ -TimeStep.prototype.setRange = function(start, end, minimumStep) { - if (!(start instanceof Date) || !(end instanceof Date)) { - throw "No legal start or end date in method setRange"; +Range.prototype._applyRange = function(start, end) { + var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start, + newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end, + max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null, + min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null, + diff; + + // check for valid number + if (isNaN(newStart) || newStart === null) { + throw new Error('Invalid start "' + start + '"'); + } + if (isNaN(newEnd) || newEnd === null) { + throw new Error('Invalid end "' + end + '"'); } - this._start = (start != undefined) ? new Date(start.valueOf()) : new Date(); - this._end = (end != undefined) ? new Date(end.valueOf()) : new Date(); + // prevent start < end + if (newEnd < newStart) { + newEnd = newStart; + } - if (this.autoScale) { - this.setMinimumStep(minimumStep); + // prevent start < min + if (min !== null) { + if (newStart < min) { + diff = (min - newStart); + newStart += diff; + newEnd += diff; + + // prevent end > max + if (max != null) { + if (newEnd > max) { + newEnd = max; + } + } + } } -}; -/** - * Set the range iterator to the start date. - */ -TimeStep.prototype.first = function() { - this.current = new Date(this._start.valueOf()); - this.roundToMinor(); -}; + // prevent end > max + if (max !== null) { + if (newEnd > max) { + diff = (newEnd - max); + newStart -= diff; + newEnd -= diff; -/** - * Round the current date to the first minor date value - * This must be executed once when the current date is set to start Date - */ -TimeStep.prototype.roundToMinor = function() { - // round to floor - // IMPORTANT: we have no breaks in this switch! (this is no bug) - //noinspection FallthroughInSwitchStatementJS - 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: // intentional fall through - 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); - //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds + // prevent start < min + if (min != null) { + if (newStart < min) { + newStart = min; + } + } + } } - if (this.step != 1) { - // round down to the first minor value that is a multiple of the current step size - 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: // intentional fall through - 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); break; - default: break; + // prevent (end-start) < zoomMin + if (this.options.zoomMin !== null) { + var zoomMin = parseFloat(this.options.zoomMin); + if (zoomMin < 0) { + zoomMin = 0; + } + if ((newEnd - newStart) < zoomMin) { + if ((this.end - this.start) === zoomMin) { + // ignore this action, we are already zoomed to the minimum + newStart = this.start; + newEnd = this.end; + } + else { + // zoom to the minimum + diff = (zoomMin - (newEnd - newStart)); + newStart -= diff / 2; + newEnd += diff / 2; + } + } + } + + // prevent (end-start) > zoomMax + if (this.options.zoomMax !== null) { + var zoomMax = parseFloat(this.options.zoomMax); + if (zoomMax < 0) { + zoomMax = 0; + } + if ((newEnd - newStart) > zoomMax) { + if ((this.end - this.start) === zoomMax) { + // ignore this action, we are already zoomed to the maximum + newStart = this.start; + newEnd = this.end; + } + else { + // zoom to the maximum + diff = ((newEnd - newStart) - zoomMax); + newStart += diff / 2; + newEnd -= diff / 2; + } } } + + var changed = (this.start != newStart || this.end != newEnd); + + this.start = newStart; + this.end = newEnd; + + return changed; }; /** - * Check if the there is a next step - * @return {boolean} true if the current date has not passed the end date + * Retrieve the current range. + * @return {Object} An object with start and end properties */ -TimeStep.prototype.hasNext = function () { - return (this.current.valueOf() <= this._end.valueOf()); +Range.prototype.getRange = function() { + return { + start: this.start, + end: this.end + }; }; /** - * Do the next step + * Calculate the conversion offset and scale for current range, based on + * the provided width + * @param {Number} width + * @returns {{offset: number, scale: number}} conversion */ -TimeStep.prototype.next = function() { - var prev = this.current.valueOf(); - - // Two cases, needed to prevent issues with switching daylight savings - // (end of March and end of October) - if (this.current.getMonth() < 6) { - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: +Range.prototype.conversion = function (width) { + return Range.conversion(this.start, this.end, width); +}; - this.current = new Date(this.current.valueOf() + this.step); break; - case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break; - case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break; - case TimeStep.SCALE.HOUR: - this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60); - // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...) - var h = this.current.getHours(); - this.current.setHours(h - (h % this.step)); - break; - case TimeStep.SCALE.WEEKDAY: // intentional fall through - 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); break; - default: break; +/** + * Static method to calculate the conversion offset and scale for a range, + * based on the provided start, end, and width + * @param {Number} start + * @param {Number} end + * @param {Number} width + * @returns {{offset: number, scale: number}} conversion + */ +Range.conversion = function (start, end, width) { + if (width != 0 && (end - start != 0)) { + return { + offset: start, + scale: width / (end - start) } } 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: // intentional fall through - 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); break; - default: break; - } + return { + offset: 0, + scale: 1 + }; } +}; - if (this.step != 1) { - // round down to the correct major value - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break; - case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break; - case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break; - case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break; - case TimeStep.SCALE.WEEKDAY: // intentional fall through - case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break; - case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break; - case TimeStep.SCALE.YEAR: break; // nothing to do for year - default: break; - } - } +/** + * 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; - // safety mechanism: if current time is still unchanged, move to the end - if (this.current.valueOf() == prev) { - this.current = new Date(this._end.valueOf()); + // 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.props.touch.allowDragging) return; + + this.props.touch.start = this.start; + this.props.touch.end = this.end; + + if (this.body.dom.root) { + this.body.dom.root.style.cursor = 'move'; } }; - /** - * Get the current datetime - * @return {Date} current The current date + * Perform dragging operation + * @param {Event} event + * @private */ -TimeStep.prototype.getCurrent = function() { - return this.current; +Range.prototype._onDrag = function (event) { + // only allow dragging when configured as movable + if (!this.options.moveable) return; + var direction = this.options.direction; + validateDirection(direction); + // 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.props.touch.allowDragging) return; + var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY, + 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(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) + }); }; /** - * Set a custom scale. Autoscaling will be disabled. - * For example setScale(SCALE.MINUTES, 5) will result - * in minor steps of 5 minutes, and major steps of an hour. - * - * @param {TimeStep.SCALE} newScale - * A scale. Choose from SCALE.MILLISECOND, - * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR, - * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH, - * SCALE.YEAR. - * @param {Number} newStep A step size, by default 1. Choose for - * example 1, 2, 5, or 10. + * Stop dragging operation + * @param {event} event + * @private */ -TimeStep.prototype.setScale = function(newScale, newStep) { - this.scale = newScale; +Range.prototype._onDragEnd = function (event) { + // only allow dragging when configured as movable + if (!this.options.moveable) return; - if (newStep > 0) { - this.step = newStep; + // 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.props.touch.allowDragging) return; + + if (this.body.dom.root) { + this.body.dom.root.style.cursor = 'auto'; } - this.autoScale = false; + // fire a rangechanged event + this.body.emitter.emit('rangechanged', { + start: new Date(this.start), + end: new Date(this.end) + }); }; /** - * Enable or disable autoscaling - * @param {boolean} enable If true, autoascaling is set true + * Event handler for mouse wheel event, used to zoom + * Code from http://adomas.org/javascript-mouse-wheel/ + * @param {Event} event + * @private */ -TimeStep.prototype.setAutoScale = function (enable) { - this.autoScale = enable; -}; - +Range.prototype._onMouseWheel = function(event) { + // only allow zooming when configured as zoomable and moveable + if (!(this.options.zoomable && this.options.moveable)) return; -/** - * Automatically determine the scale that bests fits the provided minimum step - * @param {Number} [minimumStep] The minimum step size in milliseconds - */ -TimeStep.prototype.setMinimumStep = function(minimumStep) { - if (minimumStep == undefined) { - return; + // retrieve delta + var delta = 0; + if (event.wheelDelta) { /* IE/Opera. */ + delta = event.wheelDelta / 120; + } else if (event.detail) { /* Mozilla case. */ + // In Mozilla, sign of delta is different than in IE. + // Also, delta is multiple of 3. + delta = -event.detail / 3; } - var stepYear = (1000 * 60 * 60 * 24 * 30 * 12); - var stepMonth = (1000 * 60 * 60 * 24 * 30); - var stepDay = (1000 * 60 * 60 * 24); - var stepHour = (1000 * 60 * 60); - var stepMinute = (1000 * 60); - var stepSecond = (1000); - var stepMillisecond= (1); - - // find the smallest step that is larger than the provided minimumStep - if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;} - if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;} - if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;} - if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;} - if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;} - if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;} - if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;} - if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;} - if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;} - if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;} - if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;} - if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;} - if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;} - if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;} - if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;} - if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;} - if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;} - if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;} - if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;} - if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;} - if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;} - if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;} - if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;} - if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;} - if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;} - if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;} - if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;} - if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;} - if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;} -}; - -/** - * Snap a date to a rounded value. - * The snap intervals are dependent on the current scale and step. - * @param {Date} date the date to be snapped. - * @return {Date} snappedDate - */ -TimeStep.prototype.snap = function(date) { - var clone = new Date(date.valueOf()); + // If delta is nonzero, handle it. + // Basically, delta is now positive if wheel was scrolled up, + // and negative, if wheel was scrolled down. + if (delta) { + // perform the zoom action. Delta is normally 1 or -1 - if (this.scale == TimeStep.SCALE.YEAR) { - var year = clone.getFullYear() + Math.round(clone.getMonth() / 12); - clone.setFullYear(Math.round(year / this.step) * this.step); - clone.setMonth(0); - clone.setDate(0); - clone.setHours(0); - clone.setMinutes(0); - clone.setSeconds(0); - clone.setMilliseconds(0); - } - else if (this.scale == TimeStep.SCALE.MONTH) { - if (clone.getDate() > 15) { - clone.setDate(1); - clone.setMonth(clone.getMonth() + 1); - // important: first set Date to 1, after that change the month. + // adjust a negative delta such that zooming in with delta 0.1 + // equals zooming out with a delta -0.1 + var scale; + if (delta < 0) { + scale = 1 - (delta / 5); } else { - clone.setDate(1); + scale = 1 / (1 + (delta / 5)) ; } - clone.setHours(0); - clone.setMinutes(0); - clone.setSeconds(0); - clone.setMilliseconds(0); - } - else if (this.scale == TimeStep.SCALE.DAY) { - //noinspection FallthroughInSwitchStatementJS - switch (this.step) { - case 5: - case 2: - clone.setHours(Math.round(clone.getHours() / 24) * 24); break; - default: - clone.setHours(Math.round(clone.getHours() / 12) * 12); break; - } - clone.setMinutes(0); - clone.setSeconds(0); - clone.setMilliseconds(0); - } - else if (this.scale == TimeStep.SCALE.WEEKDAY) { - //noinspection FallthroughInSwitchStatementJS - switch (this.step) { - case 5: - case 2: - clone.setHours(Math.round(clone.getHours() / 12) * 12); break; - default: - clone.setHours(Math.round(clone.getHours() / 6) * 6); break; - } - clone.setMinutes(0); - clone.setSeconds(0); - clone.setMilliseconds(0); - } - else if (this.scale == TimeStep.SCALE.HOUR) { - switch (this.step) { - case 4: - clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break; - default: - clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break; - } - clone.setSeconds(0); - clone.setMilliseconds(0); - } else if (this.scale == TimeStep.SCALE.MINUTE) { - //noinspection FallthroughInSwitchStatementJS - switch (this.step) { - case 15: - case 10: - clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5); - clone.setSeconds(0); - break; - case 5: - clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break; - default: - clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break; - } - clone.setMilliseconds(0); - } - else if (this.scale == TimeStep.SCALE.SECOND) { - //noinspection FallthroughInSwitchStatementJS - switch (this.step) { - case 15: - case 10: - clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5); - clone.setMilliseconds(0); - break; - case 5: - clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break; - default: - clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break; - } - } - else if (this.scale == TimeStep.SCALE.MILLISECOND) { - var step = this.step > 5 ? this.step / 2 : 1; - clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step); + // calculate center, the date to zoom around + var gesture = util.fakeGesture(this, event), + pointer = getPointer(gesture.center, this.body.dom.center), + pointerDate = this._pointerToDate(pointer); + + this.zoom(scale, pointerDate); } - - return clone; + + // Prevent default actions caused by mouse wheel + // (else the page and timeline both zoom and scroll) + event.preventDefault(); +}; + +/** + * Start of a touch gesture + * @private + */ +Range.prototype._onTouch = function (event) { + this.props.touch.start = this.start; + this.props.touch.end = this.end; + this.props.touch.allowDragging = true; + this.props.touch.center = null; }; /** - * 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. + * On start of a hold gesture + * @private */ -TimeStep.prototype.isMajor = function() { - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: - return (this.current.getMilliseconds() == 0); - case TimeStep.SCALE.SECOND: - return (this.current.getSeconds() == 0); - case TimeStep.SCALE.MINUTE: - return (this.current.getHours() == 0) && (this.current.getMinutes() == 0); - // Note: this is no bug. Major label is equal for both minute and hour scale - case TimeStep.SCALE.HOUR: - return (this.current.getHours() == 0); - case TimeStep.SCALE.WEEKDAY: // intentional fall through - case TimeStep.SCALE.DAY: - return (this.current.getDate() == 1); - case TimeStep.SCALE.MONTH: - return (this.current.getMonth() == 0); - case TimeStep.SCALE.YEAR: - return false; - default: - return false; - } +Range.prototype._onHold = function () { + this.props.touch.allowDragging = false; }; - /** - * Returns formatted text for the minor axislabel, depending on the current - * date and the scale. For example when scale is MINUTE, the current time is - * formatted as "hh:mm". - * @param {Date} [date] custom date. if not provided, current date is taken + * Handle pinch event + * @param {Event} event + * @private */ -TimeStep.prototype.getLabelMinor = function(date) { - if (date == undefined) { - date = this.current; - } +Range.prototype._onPinch = function (event) { + // only allow zooming when configured as zoomable and moveable + if (!(this.options.zoomable && this.options.moveable)) return; - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS'); - case TimeStep.SCALE.SECOND: return moment(date).format('s'); - case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm'); - case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm'); - case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D'); - case TimeStep.SCALE.DAY: return moment(date).format('D'); - case TimeStep.SCALE.MONTH: return moment(date).format('MMM'); - case TimeStep.SCALE.YEAR: return moment(date).format('YYYY'); - default: return ''; + this.props.touch.allowDragging = false; + + if (event.gesture.touches.length > 1) { + if (!this.props.touch.center) { + this.props.touch.center = getPointer(event.gesture.center, this.body.dom.center); + } + + var scale = 1 / event.gesture.scale, + initDate = this._pointerToDate(this.props.touch.center); + + // 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); + + // apply new range + this.setRange(newStart, newEnd); } }; - /** - * Returns formatted text for the major axis label, depending on the current - * date and the scale. For example when scale is MINUTE, the major scale is - * hours, and the hour will be formatted as "hh". - * @param {Date} [date] custom date. if not provided, current date is taken + * Helper function to calculate the center date for zooming + * @param {{x: Number, y: Number}} pointer + * @return {number} date + * @private */ -TimeStep.prototype.getLabelMajor = function(date) { - if (date == undefined) { - date = this.current; - } +Range.prototype._pointerToDate = function (pointer) { + var conversion; + var direction = this.options.direction; - //noinspection FallthroughInSwitchStatementJS - switch (this.scale) { - case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss'); - case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm'); - case TimeStep.SCALE.MINUTE: - case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM'); - case TimeStep.SCALE.WEEKDAY: - case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY'); - case TimeStep.SCALE.MONTH: return moment(date).format('YYYY'); - case TimeStep.SCALE.YEAR: return ''; - default: return ''; + validateDirection(direction); + + 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; } }; /** - * @constructor Range - * 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 {{dom: Object, domProps: Object, emitter: Emitter}} body - * @param {Object} [options] See description at Range.setOptions + * 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 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.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 +function getPointer (touch, element) { + return { + x: touch.pageX - vis.util.getAbsoluteLeft(element), + y: touch.pageY - vis.util.getAbsoluteTop(element) }; - this.options = util.extend({}, this.defaultOptions); +} - this.props = { - touch: {} - }; +/** + * 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; + } - // drag listeners for dragging - 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)); + // calculate new start and end + var newStart = center + (this.start - center) * scale; + var newEnd = center + (this.end - center) * scale; - // ignore dragging when holding - this.body.emitter.on('hold', this._onHold.bind(this)); + this.setRange(newStart, newEnd); +}; - // mouse wheel for zooming - this.body.emitter.on('mousewheel', this._onMouseWheel.bind(this)); - this.body.emitter.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF +/** + * 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); - // pinch to zoom - this.body.emitter.on('touch', this._onTouch.bind(this)); - this.body.emitter.on('pinch', this._onPinch.bind(this)); + // apply new values + var newStart = this.start + diff * delta; + var newEnd = this.end + diff * delta; - this.setOptions(options); -} + // TODO: reckon with min and max range -Range.prototype = new Component(); + this.start = newStart; + this.end = newEnd; +}; /** - * 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 + * Move the range to a new center point + * @param {Number} moveTo New center point of the range */ -Range.prototype.setOptions = function (options) { - if (options) { - // copy the options that we know - var fields = ['direction', 'min', 'max', 'zoomMin', 'zoomMax', 'moveable', 'zoomable']; - util.selectiveExtend(fields, this.options, options); +Range.prototype.moveTo = function(moveTo) { + var center = (this.start + this.end) / 2; - if ('start' in options || 'end' in options) { - // apply a new range. both start and end are optional - this.setRange(options.start, options.end); - } - } + var diff = center - moveTo; + + // calculate new start and end + var newStart = this.start - diff; + var newEnd = this.end - diff; + + this.setRange(newStart, newEnd); }; /** - * Test whether direction has a valid value - * @param {String} direction 'horizontal' or 'vertical' + * Prototype for visual components + * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} [body] + * @param {Object} [options] */ -function validateDirection (direction) { - if (direction != 'horizontal' && direction != 'vertical') { - throw new TypeError('Unknown direction "' + direction + '". ' + - 'Choose "horizontal" or "vertical".'); - } +function Component (body, options) { + this.options = null; + this.props = null; } /** - * Set a new start and end range - * @param {Number} [start] - * @param {Number} [end] + * Set options for the component. The new options will be merged into the + * current options. + * @param {Object} options + */ +Component.prototype.setOptions = function(options) { + if (options) { + util.extend(this.options, options); + } +}; + +/** + * Repaint the component + * @return {boolean} Returns true if the component is resized */ -Range.prototype.setRange = function(start, end) { - var changed = this._applyRange(start, end); - if (changed) { - var params = { - start: new Date(this.start), - end: new Date(this.end) - }; - this.body.emitter.emit('rangechange', params); - this.body.emitter.emit('rangechanged', params); - } +Component.prototype.redraw = function() { + // should be implemented by the component + return false; }; /** - * Set a new start and end range. This method is the same as setRange, but - * does not trigger a range change and range changed event, and it returns - * true when the range is changed - * @param {Number} [start] - * @param {Number} [end] - * @return {Boolean} changed - * @private + * Destroy the component. Cleanup DOM and event listeners */ -Range.prototype._applyRange = function(start, end) { - var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start, - newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end, - max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null, - min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null, - diff; - - // check for valid number - if (isNaN(newStart) || newStart === null) { - throw new Error('Invalid start "' + start + '"'); - } - if (isNaN(newEnd) || newEnd === null) { - throw new Error('Invalid end "' + end + '"'); - } - - // prevent start < end - if (newEnd < newStart) { - newEnd = newStart; - } +Component.prototype.destroy = function() { + // should be implemented by the component +}; - // prevent start < min - if (min !== null) { - if (newStart < min) { - diff = (min - newStart); - newStart += diff; - newEnd += diff; +/** + * 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() { + var resized = (this.props._previousWidth !== this.props.width || + this.props._previousHeight !== this.props.height); - // prevent end > max - if (max != null) { - if (newEnd > max) { - newEnd = max; - } - } - } - } + this.props._previousWidth = this.props.width; + this.props._previousHeight = this.props.height; - // prevent end > max - if (max !== null) { - if (newEnd > max) { - diff = (newEnd - max); - newStart -= diff; - newEnd -= diff; + return resized; +}; - // prevent start < min - if (min != null) { - if (newStart < min) { - newStart = min; - } - } +/** + * 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 (body, options) { + this.dom = { + foreground: null, + majorLines: [], + majorTexts: [], + minorLines: [], + minorTexts: [], + redundant: { + majorLines: [], + majorTexts: [], + minorLines: [], + minorTexts: [] } - } + }; + this.props = { + range: { + start: 0, + end: 0, + minimumStep: 0 + }, + lineTop: 0 + }; - // prevent (end-start) < zoomMin - if (this.options.zoomMin !== null) { - var zoomMin = parseFloat(this.options.zoomMin); - if (zoomMin < 0) { - zoomMin = 0; - } - if ((newEnd - newStart) < zoomMin) { - if ((this.end - this.start) === zoomMin) { - // ignore this action, we are already zoomed to the minimum - newStart = this.start; - newEnd = this.end; - } - else { - // zoom to the minimum - diff = (zoomMin - (newEnd - newStart)); - newStart -= diff / 2; - newEnd += diff / 2; - } - } - } + this.defaultOptions = { + orientation: 'bottom', // supported: 'top', 'bottom' + // TODO: implement timeaxis orientations 'left' and 'right' + showMinorLabels: true, + showMajorLabels: true + }; + this.options = util.extend({}, this.defaultOptions); - // prevent (end-start) > zoomMax - if (this.options.zoomMax !== null) { - var zoomMax = parseFloat(this.options.zoomMax); - if (zoomMax < 0) { - zoomMax = 0; - } - if ((newEnd - newStart) > zoomMax) { - if ((this.end - this.start) === zoomMax) { - // ignore this action, we are already zoomed to the maximum - newStart = this.start; - newEnd = this.end; - } - else { - // zoom to the maximum - diff = ((newEnd - newStart) - zoomMax); - newStart += diff / 2; - newEnd -= diff / 2; - } - } - } + this.body = body; - var changed = (this.start != newStart || this.end != newEnd); + // create the HTML DOM + this._create(); - this.start = newStart; - this.end = newEnd; + this.setOptions(options); +} - return changed; -}; +TimeAxis.prototype = new Component(); /** - * Retrieve the current range. - * @return {Object} An object with start and end properties + * Set options for the TimeAxis. + * Parameters will be merged in current options. + * @param {Object} options Available options: + * {string} [orientation] + * {boolean} [showMinorLabels] + * {boolean} [showMajorLabels] */ -Range.prototype.getRange = function() { - return { - start: this.start, - end: this.end - }; +TimeAxis.prototype.setOptions = function(options) { + if (options) { + // copy all options that we know + util.selectiveExtend(['orientation', 'showMinorLabels', 'showMajorLabels'], this.options, options); + } }; /** - * Calculate the conversion offset and scale for current range, based on - * the provided width - * @param {Number} width - * @returns {{offset: number, scale: number}} conversion + * Create the HTML DOM for the TimeAxis */ -Range.prototype.conversion = function (width) { - return Range.conversion(this.start, this.end, width); +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'; }; /** - * Static method to calculate the conversion offset and scale for a range, - * based on the provided start, end, and width - * @param {Number} start - * @param {Number} end - * @param {Number} width - * @returns {{offset: number, scale: number}} conversion + * Destroy the TimeAxis */ -Range.conversion = function (start, end, width) { - if (width != 0 && (end - start != 0)) { - return { - offset: start, - scale: width / (end - start) - } +TimeAxis.prototype.destroy = function() { + // remove from DOM + if (this.dom.foreground.parentNode) { + this.dom.foreground.parentNode.removeChild(this.dom.foreground); } - else { - return { - offset: 0, - scale: 1 - }; + if (this.dom.background.parentNode) { + this.dom.background.parentNode.removeChild(this.dom.background); } -}; - -/** - * 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 (!this.props.touch.allowDragging) return; - - this.props.touch.start = this.start; - this.props.touch.end = this.end; - - if (this.body.dom.root) { - this.body.dom.root.style.cursor = 'move'; - } + this.body = null; }; - -/** - * Perform dragging operation - * @param {Event} event - * @private + +/** + * Repaint the component + * @return {boolean} Returns true if the component is resized */ -Range.prototype._onDrag = function (event) { - // only allow dragging when configured as movable - if (!this.options.moveable) return; +TimeAxis.prototype.redraw = function () { + var options = this.options, + props = this.props, + foreground = this.dom.foreground, + background = this.dom.background; - var direction = this.options.direction; - validateDirection(direction); + // 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); - // 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.props.touch.allowDragging) return; + // calculate character width and height + this._calculateCharSize(); - var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY, - 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; + // 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; - this._applyRange(this.props.touch.start + diffRange, this.props.touch.end + diffRange); + // 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; - this.body.emitter.emit('rangechange', { - start: new Date(this.start), - end: new Date(this.end) - }); -}; + 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 -/** - * Stop dragging operation - * @param {event} event - * @private - */ -Range.prototype._onDragEnd = function (event) { - // only allow dragging when configured as movable - if (!this.options.moveable) return; + // 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); - // 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.props.touch.allowDragging) return; + foreground.style.height = this.props.height + 'px'; - if (this.body.dom.root) { - this.body.dom.root.style.cursor = 'auto'; + this._repaintLabels(); + + // 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) } - // fire a rangechanged event - this.body.emitter.emit('rangechanged', { - start: new Date(this.start), - end: new Date(this.end) - }); + return this._isResized() || parentChanged; }; /** - * Event handler for mouse wheel event, used to zoom - * Code from http://adomas.org/javascript-mouse-wheel/ - * @param {Event} event + * Repaint major and minor text labels and vertical grid lines * @private */ -Range.prototype._onMouseWheel = function(event) { - // only allow zooming when configured as zoomable and moveable - if (!(this.options.zoomable && this.options.moveable)) return; +TimeAxis.prototype._repaintLabels = function () { + var orientation = this.options.orientation; - // retrieve delta - var delta = 0; - if (event.wheelDelta) { /* IE/Opera. */ - delta = event.wheelDelta / 120; - } else if (event.detail) { /* Mozilla case. */ - // In Mozilla, sign of delta is different than in IE. - // Also, delta is multiple of 3. - delta = -event.detail / 3; - } + // calculate range and step (step such that we have space for 7 characters per label) + 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; - // If delta is nonzero, handle it. - // Basically, delta is now positive if wheel was scrolled up, - // and negative, if wheel was scrolled down. - if (delta) { - // perform the zoom action. Delta is normally 1 or -1 + // Move all DOM elements to a "redundant" list, where they + // can be picked for re-use, and clear the lists with lines and texts. + // At the end of the function _repaintLabels, left over elements will be cleaned up + var dom = this.dom; + dom.redundant.majorLines = dom.majorLines; + dom.redundant.majorTexts = dom.majorTexts; + dom.redundant.minorLines = dom.minorLines; + dom.redundant.minorTexts = dom.minorTexts; + dom.majorLines = []; + dom.majorTexts = []; + dom.minorLines = []; + dom.minorTexts = []; - // adjust a negative delta such that zooming in with delta 0.1 - // equals zooming out with a delta -0.1 - var scale; - if (delta < 0) { - scale = 1 - (delta / 5); + step.first(); + var xFirstMajorLabel = undefined; + var max = 0; + while (step.hasNext() && max < 1000) { + max++; + var cur = step.getCurrent(), + x = this.body.util.toScreen(cur), + isMajor = step.isMajor(); + + // TODO: lines must have a width, such that we can create css backgrounds + + if (this.options.showMinorLabels) { + this._repaintMinorText(x, step.getLabelMinor(), orientation); + } + + if (isMajor && this.options.showMajorLabels) { + if (x > 0) { + if (xFirstMajorLabel == undefined) { + xFirstMajorLabel = x; + } + this._repaintMajorText(x, step.getLabelMajor(), orientation); + } + this._repaintMajorLine(x, orientation); } else { - scale = 1 / (1 + (delta / 5)) ; + this._repaintMinorLine(x, orientation); } - // calculate center, the date to zoom around - var gesture = util.fakeGesture(this, event), - pointer = getPointer(gesture.center, this.body.dom.center), - pointerDate = this._pointerToDate(pointer); + step.next(); + } - this.zoom(scale, pointerDate); + // create a major label on the left when needed + 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 + + if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) { + this._repaintMajorText(0, leftText, orientation); + } } - // Prevent default actions caused by mouse wheel - // (else the page and timeline both zoom and scroll) - event.preventDefault(); + // Cleanup leftover DOM elements from the redundant list + util.forEach(this.dom.redundant, function (arr) { + while (arr.length) { + var elem = arr.pop(); + if (elem && elem.parentNode) { + elem.parentNode.removeChild(elem); + } + } + }); }; /** - * Start of a touch gesture + * Create a minor label for the axis at position x + * @param {Number} x + * @param {String} text + * @param {String} orientation "top" or "bottom" (default) * @private */ -Range.prototype._onTouch = function (event) { - this.props.touch.start = this.start; - this.props.touch.end = this.end; - this.props.touch.allowDragging = true; - this.props.touch.center = null; -}; +TimeAxis.prototype._repaintMinorText = function (x, text, orientation) { + // reuse redundant label + var label = this.dom.redundant.minorTexts.shift(); -/** - * On start of a hold gesture - * @private - */ -Range.prototype._onHold = function () { - this.props.touch.allowDragging = false; + if (!label) { + // create new label + var content = document.createTextNode(''); + label = document.createElement('div'); + label.appendChild(content); + label.className = 'text minor'; + this.dom.foreground.appendChild(label); + } + this.dom.minorTexts.push(label); + + label.childNodes[0].nodeValue = text; + + label.style.top = (orientation == 'top') ? (this.props.majorLabelHeight + 'px') : '0'; + label.style.left = x + 'px'; + //label.title = title; // TODO: this is a heavy operation }; /** - * Handle pinch event - * @param {Event} event + * Create a Major label for the axis at position x + * @param {Number} x + * @param {String} text + * @param {String} orientation "top" or "bottom" (default) * @private */ -Range.prototype._onPinch = function (event) { - // only allow zooming when configured as zoomable and moveable - if (!(this.options.zoomable && this.options.moveable)) return; +TimeAxis.prototype._repaintMajorText = function (x, text, orientation) { + // reuse redundant label + var label = this.dom.redundant.majorTexts.shift(); - this.props.touch.allowDragging = false; + if (!label) { + // create label + var content = document.createTextNode(text); + label = document.createElement('div'); + label.className = 'text major'; + label.appendChild(content); + this.dom.foreground.appendChild(label); + } + this.dom.majorTexts.push(label); - if (event.gesture.touches.length > 1) { - if (!this.props.touch.center) { - this.props.touch.center = getPointer(event.gesture.center, this.body.dom.center); - } + label.childNodes[0].nodeValue = text; + //label.title = title; // TODO: this is a heavy operation - var scale = 1 / event.gesture.scale, - initDate = this._pointerToDate(this.props.touch.center); + label.style.top = (orientation == 'top') ? '0' : (this.props.minorLabelHeight + 'px'); + label.style.left = x + 'px'; +}; + +/** + * Create a minor line for the axis at position x + * @param {Number} x + * @param {String} orientation "top" or "bottom" (default) + * @private + */ +TimeAxis.prototype._repaintMinorLine = function (x, orientation) { + // reuse redundant line + var line = this.dom.redundant.minorLines.shift(); - // 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); + if (!line) { + // create vertical line + line = document.createElement('div'); + line.className = 'grid vertical minor'; + this.dom.background.appendChild(line); + } + this.dom.minorLines.push(line); - // apply new range - this.setRange(newStart, newEnd); + var props = this.props; + if (orientation == 'top') { + line.style.top = props.majorLabelHeight + 'px'; + } + else { + line.style.top = this.body.domProps.top.height + 'px'; } + line.style.height = props.minorLineHeight + 'px'; + line.style.left = (x - props.minorLineWidth / 2) + 'px'; }; /** - * Helper function to calculate the center date for zooming - * @param {{x: Number, y: Number}} pointer - * @return {number} date + * Create a Major line for the axis at position x + * @param {Number} x + * @param {String} orientation "top" or "bottom" (default) * @private */ -Range.prototype._pointerToDate = function (pointer) { - var conversion; - var direction = this.options.direction; +TimeAxis.prototype._repaintMajorLine = function (x, orientation) { + // reuse redundant line + var line = this.dom.redundant.majorLines.shift(); - validateDirection(direction); + if (!line) { + // create vertical line + line = document.createElement('DIV'); + line.className = 'grid vertical major'; + this.dom.background.appendChild(line); + } + this.dom.majorLines.push(line); - if (direction == 'horizontal') { - var width = this.body.domProps.center.width; - conversion = this.conversion(width); - return pointer.x / conversion.scale + conversion.offset; + var props = this.props; + if (orientation == 'top') { + line.style.top = '0'; } else { - var height = this.body.domProps.center.height; - conversion = this.conversion(height); - return pointer.y / conversion.scale + conversion.offset; + line.style.top = this.body.domProps.top.height + 'px'; } + line.style.left = (x - props.majorLineWidth / 2) + 'px'; + line.style.height = props.majorLineHeight + 'px'; }; /** - * 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 + * 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 */ -function getPointer (touch, element) { - return { - x: touch.pageX - vis.util.getAbsoluteLeft(element), - y: touch.pageY - vis.util.getAbsoluteTop(element) - }; -} +TimeAxis.prototype._calculateCharSize = function () { + // Note: We calculate char size with every redraw. Size may change, for + // example when any of the timelines parents had display:none for example. -/** - * 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; + // determine the char width and height on the minor axis + if (!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.dom.foreground.appendChild(this.dom.measureCharMinor); } + this.props.minorCharHeight = this.dom.measureCharMinor.clientHeight; + this.props.minorCharWidth = this.dom.measureCharMinor.clientWidth; - // calculate new start and end - var newStart = center + (this.start - center) * scale; - var newEnd = center + (this.end - center) * scale; + // determine the char width and height on the major axis + if (!this.dom.measureCharMajor) { + this.dom.measureCharMajor = document.createElement('DIV'); + this.dom.measureCharMajor.className = 'text minor measure'; + this.dom.measureCharMajor.style.position = 'absolute'; - this.setRange(newStart, newEnd); + this.dom.measureCharMajor.appendChild(document.createTextNode('0')); + this.dom.foreground.appendChild(this.dom.measureCharMajor); + } + this.props.majorCharHeight = this.dom.measureCharMajor.clientHeight; + this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth; }; /** - * 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 + * 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 */ -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; +TimeAxis.prototype.snap = function(date) { + return this.step.snap(date); }; /** - * Move the range to a new center point - * @param {Number} moveTo New center point of the range + * A current time bar + * @param {{range: Range, dom: Object, domProps: Object}} body + * @param {Object} [options] Available parameters: + * {Boolean} [showCurrentTime] + * @constructor CurrentTime + * @extends Component */ -Range.prototype.moveTo = function(moveTo) { - var center = (this.start + this.end) / 2; - var diff = center - moveTo; +function CurrentTime (body, options) { + this.body = body; - // calculate new start and end - var newStart = this.start - diff; - var newEnd = this.end - diff; + // default options + this.defaultOptions = { + showCurrentTime: true + }; + this.options = util.extend({}, this.defaultOptions); - this.setRange(newStart, newEnd); + this._create(); + + this.setOptions(options); +} + +CurrentTime.prototype = new Component(); + +/** + * Create the HTML DOM for the current time bar + * @private + */ +CurrentTime.prototype._create = function() { + var bar = document.createElement('div'); + bar.className = 'currenttime'; + bar.style.position = 'absolute'; + bar.style.top = '0px'; + bar.style.height = '100%'; + + this.bar = bar; }; /** - * Prototype for visual components - * @param {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} [body] - * @param {Object} [options] + * Destroy the CurrentTime bar */ -function Component (body, options) { - this.options = null; - this.props = null; -} +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. The new options will be merged into the - * current options. - * @param {Object} options + * Set options for the component. Options will be merged in current options. + * @param {Object} options Available parameters: + * {boolean} [showCurrentTime] */ -Component.prototype.setOptions = function(options) { +CurrentTime.prototype.setOptions = function(options) { if (options) { - util.extend(this.options, options); + // copy all options that we know + util.selectiveExtend(['showCurrentTime'], this.options, options); } }; @@ -3648,120 +6628,149 @@ Component.prototype.setOptions = function(options) { * Repaint the component * @return {boolean} Returns true if the component is resized */ -Component.prototype.redraw = function() { - // should be implemented by the component +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.body.util.toScreen(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; }; /** - * Destroy the component. Cleanup DOM and event listeners + * Start auto refreshing the current time bar */ -Component.prototype.destroy = function() { - // should be implemented by the component -}; +CurrentTime.prototype.start = function() { + var me = this; -/** - * 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() { - var resized = (this.props._previousWidth !== this.props.width || - this.props._previousHeight !== this.props.height); + function update () { + me.stop(); + + // determine interval to refresh + 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.redraw(); - this.props._previousWidth = this.props.width; - this.props._previousHeight = this.props.height; + // start a timer to adjust for the new time + me.currentTimeTimer = setTimeout(update, interval); + } - return resized; + update(); }; /** - * 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 + * Stop auto refreshing the current time bar + */ +CurrentTime.prototype.stop = function() { + if (this.currentTimeTimer !== undefined) { + clearTimeout(this.currentTimeTimer); + delete this.currentTimeTimer; + } +}; + +/** + * A custom time bar + * @param {{range: Range, dom: Object}} body + * @param {Object} [options] Available parameters: + * {Boolean} [showCustomTime] + * @constructor CustomTime * @extends Component */ -function TimeAxis (body, options) { - this.dom = { - foreground: null, - majorLines: [], - majorTexts: [], - minorLines: [], - minorTexts: [], - redundant: { - majorLines: [], - majorTexts: [], - minorLines: [], - minorTexts: [] - } - }; - this.props = { - range: { - start: 0, - end: 0, - minimumStep: 0 - }, - lineTop: 0 - }; +function CustomTime (body, options) { + this.body = body; + + // default options this.defaultOptions = { - orientation: 'bottom', // supported: 'top', 'bottom' - // TODO: implement timeaxis orientations 'left' and 'right' - showMinorLabels: true, - showMajorLabels: true + showCustomTime: false }; this.options = util.extend({}, this.defaultOptions); - this.body = body; + this.customTime = new Date(); + this.eventParams = {}; // stores state parameters while dragging the bar - // create the HTML DOM + // create the DOM this._create(); this.setOptions(options); } -TimeAxis.prototype = new Component(); +CustomTime.prototype = new Component(); /** - * Set options for the TimeAxis. - * Parameters will be merged in current options. - * @param {Object} options Available options: - * {string} [orientation] - * {boolean} [showMinorLabels] - * {boolean} [showMajorLabels] + * Set options for the component. Options will be merged in current options. + * @param {Object} options Available parameters: + * {boolean} [showCustomTime] */ -TimeAxis.prototype.setOptions = function(options) { +CustomTime.prototype.setOptions = function(options) { if (options) { // copy all options that we know - util.selectiveExtend(['orientation', 'showMinorLabels', 'showMajorLabels'], this.options, options); + util.selectiveExtend(['showCustomTime'], this.options, options); } }; /** - * Create the HTML DOM for the TimeAxis + * Create the DOM for the custom time + * @private */ -TimeAxis.prototype._create = function() { - this.dom.foreground = document.createElement('div'); - this.dom.background = document.createElement('div'); +CustomTime.prototype._create = function() { + var bar = document.createElement('div'); + bar.className = 'customtime'; + bar.style.position = 'absolute'; + bar.style.top = '0px'; + bar.style.height = '100%'; + this.bar = bar; - this.dom.foreground.className = 'timeaxis foreground'; - this.dom.background.className = 'timeaxis background'; + var drag = document.createElement('div'); + drag.style.position = 'relative'; + drag.style.top = '0px'; + drag.style.left = '-10px'; + drag.style.height = '100%'; + drag.style.width = '20px'; + bar.appendChild(drag); + + // attach event listeners + this.hammer = Hammer(bar, { + prevent_default: true + }); + this.hammer.on('dragstart', this._onDragStart.bind(this)); + this.hammer.on('drag', this._onDrag.bind(this)); + this.hammer.on('dragend', this._onDragEnd.bind(this)); }; /** - * Destroy the TimeAxis + * Destroy the CustomTime bar */ -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); - } +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; }; @@ -3770,3308 +6779,3598 @@ TimeAxis.prototype.destroy = function() { * Repaint the component * @return {boolean} Returns true if the component is resized */ -TimeAxis.prototype.redraw = function () { - var options = this.options, - props = this.props, - foreground = this.dom.foreground, - background = this.dom.background; - - // 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); - - // calculate character width and height - this._calculateCharSize(); - - // 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; - - // 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; - - 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 - - // 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); - - foreground.style.height = this.props.height + 'px'; - - this._repaintLabels(); - - // 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() || parentChanged; -}; - -/** - * Repaint major and minor text labels and vertical grid lines - * @private - */ -TimeAxis.prototype._repaintLabels = function () { - 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.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; - - // Move all DOM elements to a "redundant" list, where they - // can be picked for re-use, and clear the lists with lines and texts. - // At the end of the function _repaintLabels, left over elements will be cleaned up - var dom = this.dom; - dom.redundant.majorLines = dom.majorLines; - dom.redundant.majorTexts = dom.majorTexts; - dom.redundant.minorLines = dom.minorLines; - dom.redundant.minorTexts = dom.minorTexts; - dom.majorLines = []; - dom.majorTexts = []; - dom.minorLines = []; - dom.minorTexts = []; - - step.first(); - var xFirstMajorLabel = undefined; - var max = 0; - while (step.hasNext() && max < 1000) { - max++; - var cur = step.getCurrent(), - x = this.body.util.toScreen(cur), - isMajor = step.isMajor(); - - // TODO: lines must have a width, such that we can create css backgrounds - - if (this.options.showMinorLabels) { - this._repaintMinorText(x, step.getLabelMinor(), orientation); - } - - if (isMajor && this.options.showMajorLabels) { - if (x > 0) { - if (xFirstMajorLabel == undefined) { - xFirstMajorLabel = x; - } - this._repaintMajorText(x, step.getLabelMajor(), orientation); +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); } - this._repaintMajorLine(x, orientation); - } - else { - this._repaintMinorLine(x, orientation); + parent.appendChild(this.bar); } - step.next(); - } - - // create a major label on the left when needed - 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 + var x = this.body.util.toScreen(this.customTime); - if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) { - this._repaintMajorText(0, leftText, orientation); + 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); } } - // Cleanup leftover DOM elements from the redundant list - util.forEach(this.dom.redundant, function (arr) { - while (arr.length) { - var elem = arr.pop(); - if (elem && elem.parentNode) { - elem.parentNode.removeChild(elem); - } - } - }); + return false; }; /** - * Create a minor label for the axis at position x - * @param {Number} x - * @param {String} text - * @param {String} orientation "top" or "bottom" (default) - * @private + * Set custom time. + * @param {Date} time */ -TimeAxis.prototype._repaintMinorText = function (x, text, orientation) { - // reuse redundant label - var label = this.dom.redundant.minorTexts.shift(); - - if (!label) { - // create new label - var content = document.createTextNode(''); - label = document.createElement('div'); - label.appendChild(content); - label.className = 'text minor'; - this.dom.foreground.appendChild(label); - } - this.dom.minorTexts.push(label); - - label.childNodes[0].nodeValue = text; +CustomTime.prototype.setCustomTime = function(time) { + this.customTime = new Date(time.valueOf()); + this.redraw(); +}; - label.style.top = (orientation == 'top') ? (this.props.majorLabelHeight + 'px') : '0'; - label.style.left = x + 'px'; - //label.title = title; // TODO: this is a heavy operation +/** + * Retrieve the current custom time. + * @return {Date} customTime + */ +CustomTime.prototype.getCustomTime = function() { + return new Date(this.customTime.valueOf()); }; /** - * Create a Major label for the axis at position x - * @param {Number} x - * @param {String} text - * @param {String} orientation "top" or "bottom" (default) + * Start moving horizontally + * @param {Event} event * @private */ -TimeAxis.prototype._repaintMajorText = function (x, text, orientation) { - // reuse redundant label - var label = this.dom.redundant.majorTexts.shift(); - - if (!label) { - // create label - var content = document.createTextNode(text); - label = document.createElement('div'); - label.className = 'text major'; - label.appendChild(content); - this.dom.foreground.appendChild(label); - } - this.dom.majorTexts.push(label); - - label.childNodes[0].nodeValue = text; - //label.title = title; // TODO: this is a heavy operation +CustomTime.prototype._onDragStart = function(event) { + this.eventParams.dragging = true; + this.eventParams.customTime = this.customTime; - label.style.top = (orientation == 'top') ? '0' : (this.props.minorLabelHeight + 'px'); - label.style.left = x + 'px'; + event.stopPropagation(); + event.preventDefault(); }; /** - * Create a minor line for the axis at position x - * @param {Number} x - * @param {String} orientation "top" or "bottom" (default) + * Perform moving operating. + * @param {Event} event * @private */ -TimeAxis.prototype._repaintMinorLine = function (x, orientation) { - // reuse redundant line - var line = this.dom.redundant.minorLines.shift(); +CustomTime.prototype._onDrag = function (event) { + if (!this.eventParams.dragging) return; - if (!line) { - // create vertical line - line = document.createElement('div'); - line.className = 'grid vertical minor'; - this.dom.background.appendChild(line); - } - this.dom.minorLines.push(line); + var deltaX = event.gesture.deltaX, + x = this.body.util.toScreen(this.eventParams.customTime) + deltaX, + time = this.body.util.toTime(x); - var props = this.props; - if (orientation == 'top') { - line.style.top = props.majorLabelHeight + 'px'; - } - else { - line.style.top = this.body.domProps.top.height + 'px'; - } - line.style.height = props.minorLineHeight + 'px'; - line.style.left = (x - props.minorLineWidth / 2) + 'px'; + this.setCustomTime(time); + + // fire a timechange event + this.body.emitter.emit('timechange', { + time: new Date(this.customTime.valueOf()) + }); + + event.stopPropagation(); + event.preventDefault(); }; /** - * Create a Major line for the axis at position x - * @param {Number} x - * @param {String} orientation "top" or "bottom" (default) + * Stop moving operating. + * @param {event} event * @private */ -TimeAxis.prototype._repaintMajorLine = function (x, orientation) { - // reuse redundant line - var line = this.dom.redundant.majorLines.shift(); +CustomTime.prototype._onDragEnd = function (event) { + if (!this.eventParams.dragging) return; - if (!line) { - // create vertical line - line = document.createElement('DIV'); - line.className = 'grid vertical major'; - this.dom.background.appendChild(line); - } - this.dom.majorLines.push(line); + // fire a timechanged event + this.body.emitter.emit('timechanged', { + time: new Date(this.customTime.valueOf()) + }); - var props = this.props; - if (orientation == 'top') { - line.style.top = '0'; - } - else { - line.style.top = this.body.domProps.top.height + 'px'; - } - line.style.left = (x - props.majorLineWidth / 2) + 'px'; - line.style.height = props.majorLineHeight + 'px'; + event.stopPropagation(); + event.preventDefault(); }; +var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items + /** - * 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 + * 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 {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body + * @param {Object} [options] See ItemSet.setOptions for the available options. + * @constructor ItemSet + * @extends Component */ -TimeAxis.prototype._calculateCharSize = function () { - // Note: We calculate char size with every redraw. Size may change, for - // example when any of the timelines parents had display:none for example. +function ItemSet(body, options) { + this.body = body; - // determine the char width and height on the minor axis - if (!this.dom.measureCharMinor) { - this.dom.measureCharMinor = document.createElement('DIV'); - this.dom.measureCharMinor.className = 'text minor measure'; - this.dom.measureCharMinor.style.position = 'absolute'; + 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 + + // 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); + } + }; + + // 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); + } + }; + + this.items = {}; // object with an Item for every data item + this.groups = {}; // Group object for every group + this.groupIds = []; + + this.selection = []; // list with the ids of all selected nodes + this.stackDirty = true; // if true, all items will be restacked on next redraw - this.dom.measureCharMinor.appendChild(document.createTextNode('0')); - this.dom.foreground.appendChild(this.dom.measureCharMinor); - } - this.props.minorCharHeight = this.dom.measureCharMinor.clientHeight; - this.props.minorCharWidth = this.dom.measureCharMinor.clientWidth; + this.touchParams = {}; // stores properties while dragging + // create the HTML DOM - // determine the char width and height on the major axis - if (!this.dom.measureCharMajor) { - this.dom.measureCharMajor = document.createElement('DIV'); - this.dom.measureCharMajor.className = 'text minor measure'; - this.dom.measureCharMajor.style.position = 'absolute'; + this._create(); - this.dom.measureCharMajor.appendChild(document.createTextNode('0')); - this.dom.foreground.appendChild(this.dom.measureCharMajor); - } - this.props.majorCharHeight = this.dom.measureCharMajor.clientHeight; - this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth; -}; + this.setOptions(options); +} -/** - * Snap a date to a rounded value. - * The snap intervals are dependent on the current scale and step. - * @param {Date} date the date to be snapped. - * @return {Date} snappedDate - */ -TimeAxis.prototype.snap = function(date) { - return this.step.snap(date); +ItemSet.prototype = new Component(); + +// available item types will be registered here +ItemSet.types = { + box: ItemBox, + range: ItemRange, + point: ItemPoint }; /** - * A current time bar - * @param {{range: Range, dom: Object, domProps: Object}} body - * @param {Object} [options] Available parameters: - * {Boolean} [showCurrentTime] - * @constructor CurrentTime - * @extends Component + * Create the HTML DOM for the ItemSet */ +ItemSet.prototype._create = function(){ + var frame = document.createElement('div'); + frame.className = 'itemset'; + frame['timeline-itemset'] = this; + this.dom.frame = frame; -function CurrentTime (body, options) { - this.body = body; + // create background panel + var background = document.createElement('div'); + background.className = 'background'; + frame.appendChild(background); + this.dom.background = background; - // default options - this.defaultOptions = { - showCurrentTime: true - }; - this.options = util.extend({}, this.defaultOptions); + // create foreground panel + var foreground = document.createElement('div'); + foreground.className = 'foreground'; + frame.appendChild(foreground); + this.dom.foreground = foreground; - this._create(); + // create axis panel + var axis = document.createElement('div'); + axis.className = 'axis'; + this.dom.axis = axis; - this.setOptions(options); -} + // create labelset + var labelSet = document.createElement('div'); + labelSet.className = 'labelset'; + this.dom.labelSet = labelSet; -CurrentTime.prototype = new Component(); + // create ungrouped Group + this._updateUngrouped(); -/** - * Create the HTML DOM for the current time bar - * @private - */ -CurrentTime.prototype._create = function() { - var bar = document.createElement('div'); - bar.className = 'currenttime'; - bar.style.position = 'absolute'; - bar.style.top = '0px'; - bar.style.height = '100%'; + // attach event listeners + // 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 + }); - this.bar = bar; -}; + // 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)); -/** - * Destroy the CurrentTime bar - */ -CurrentTime.prototype.destroy = function () { - this.options.showCurrentTime = false; - this.redraw(); // will remove the bar from the DOM and stop refreshing + // single select (or unselect) when tapping an item + this.hammer.on('tap', this._onSelectItem.bind(this)); - this.body = null; + // 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 component. Options will be merged in current options. - * @param {Object} options Available parameters: - * {boolean} [showCurrentTime] + * Set options for the ItemSet. Existing options will be extended/overwritten. + * @param {Object} [options] The following options are available: + * {String} type + * Default type for the items. Choose from 'box' + * (default), 'point', or 'range'. The default + * Style can be overwritten by individual items. + * {String} align + * Alignment for the items, only applicable for + * ItemBox. Choose 'center' (default), 'left', or + * 'right'. + * {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. + * {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. */ -CurrentTime.prototype.setOptions = function(options) { +ItemSet.prototype.setOptions = function(options) { if (options) { // copy all options that we know - util.selectiveExtend(['showCurrentTime'], this.options, options); - } -}; + var fields = ['type', 'align', 'orientation', 'padding', 'stack', 'selectable', 'groupOrder']; + util.selectiveExtend(fields, this.options, options); -/** - * Repaint the component - * @return {boolean} Returns true if the component is resized - */ -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); + 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); } - parent.appendChild(this.bar); - - this.start(); } - var now = new Date(); - var x = this.body.util.toScreen(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); + 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); + } } - this.stop(); - } - - return false; -}; - -/** - * Start auto refreshing the current time bar - */ -CurrentTime.prototype.start = function() { - var me = this; - - function update () { - me.stop(); - - // determine interval to refresh - 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.redraw(); + // 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); - // start a timer to adjust for the new time - me.currentTimeTimer = setTimeout(update, interval); + // force the itemSet to refresh: options like orientation and margins may be changed + this.markDirty(); } - - update(); }; /** - * Stop auto refreshing the current time bar + * Mark the ItemSet dirty so it will refresh everything with next redraw */ -CurrentTime.prototype.stop = function() { - if (this.currentTimeTimer !== undefined) { - clearTimeout(this.currentTimeTimer); - delete this.currentTimeTimer; - } +ItemSet.prototype.markDirty = function() { + this.groupIds = []; + this.stackDirty = true; }; /** - * A custom time bar - * @param {{range: Range, dom: Object}} body - * @param {Object} [options] Available parameters: - * {Boolean} [showCustomTime] - * @constructor CustomTime - * @extends Component + * Destroy the ItemSet */ +ItemSet.prototype.destroy = function() { + this.hide(); + this.setItems(null); + this.setGroups(null); -function CustomTime (body, options) { - this.body = body; - - // 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(); + this.hammer = null; -/** - * 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); - } + this.body = null; + this.conversion = null; }; /** - * Create the DOM for the custom time - * @private + * Hide the component from the DOM */ -CustomTime.prototype._create = function() { - var bar = document.createElement('div'); - bar.className = 'customtime'; - bar.style.position = 'absolute'; - bar.style.top = '0px'; - bar.style.height = '100%'; - this.bar = bar; +ItemSet.prototype.hide = function() { + // remove the frame containing the items + if (this.dom.frame.parentNode) { + this.dom.frame.parentNode.removeChild(this.dom.frame); + } - var drag = document.createElement('div'); - drag.style.position = 'relative'; - drag.style.top = '0px'; - drag.style.left = '-10px'; - drag.style.height = '100%'; - drag.style.width = '20px'; - bar.appendChild(drag); + // remove the axis with dots + if (this.dom.axis.parentNode) { + this.dom.axis.parentNode.removeChild(this.dom.axis); + } - // attach event listeners - this.hammer = Hammer(bar, { - prevent_default: true - }); - this.hammer.on('dragstart', this._onDragStart.bind(this)); - this.hammer.on('drag', this._onDrag.bind(this)); - this.hammer.on('dragend', this._onDragEnd.bind(this)); + // remove the labelset containing all group labels + if (this.dom.labelSet.parentNode) { + this.dom.labelSet.parentNode.removeChild(this.dom.labelSet); + } }; /** - * Destroy the CustomTime bar + * Show the component in the DOM (when not already visible). + * @return {Boolean} changed */ -CustomTime.prototype.destroy = function () { - this.options.showCustomTime = false; - this.redraw(); // will remove the bar from the DOM +ItemSet.prototype.show = function() { + // show frame containing the items + if (!this.dom.frame.parentNode) { + this.body.dom.center.appendChild(this.dom.frame); + } - this.hammer.enable(false); - this.hammer = null; + // show axis with dots + if (!this.dom.axis.parentNode) { + this.body.dom.backgroundVertical.appendChild(this.dom.axis); + } - this.body = null; + // show labelset containing labels + if (!this.dom.labelSet.parentNode) { + this.body.dom.left.appendChild(this.dom.labelSet); + } }; /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * 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. */ -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); +ItemSet.prototype.setSelection = function(ids) { + var i, ii, id, item; + + if (ids) { + if (!Array.isArray(ids)) { + throw new TypeError('Array expected'); } - var x = this.body.util.toScreen(this.customTime); + // unselect currently selected items + for (i = 0, ii = this.selection.length; i < ii; i++) { + id = this.selection[i]; + item = this.items[id]; + if (item) item.unselect(); + } - 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); + // select items + this.selection = []; + for (i = 0, ii = ids.length; i < ii; i++) { + id = ids[i]; + item = this.items[id]; + if (item) { + this.selection.push(id); + item.select(); + } } } +}; - return false; +/** + * Get the selected items by their id + * @return {Array} ids The ids of the selected items + */ +ItemSet.prototype.getSelection = function() { + return this.selection.concat([]); }; /** - * Set custom time. - * @param {Date} time + * Deselect a selected item + * @param {String | Number} id + * @private */ -CustomTime.prototype.setCustomTime = function(time) { - this.customTime = new Date(time.valueOf()); - this.redraw(); +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! + selection.splice(i, 1); + break; + } + } }; /** - * Retrieve the current custom time. - * @return {Date} customTime + * Repaint the component + * @return {boolean} Returns true if the component is resized */ -CustomTime.prototype.getCustomTime = function() { - return new Date(this.customTime.valueOf()); +ItemSet.prototype.redraw = function() { + var margin = this.options.margin, + range = this.body.range, + asSize = util.option.asSize, + options = this.options, + orientation = options.orientation, + resized = false, + frame = this.dom.frame, + editable = options.editable.updateTime || options.editable.updateGroup; + + // 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 = range.end - range.start; + var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.props.width != this.props.lastWidth); + if (zoomed) this.stackDirty = true; + this.lastVisibleInterval = visibleInterval; + this.props.lastWidth = this.props.width; + + // redraw all groups + var restack = this.stackDirty, + firstGroup = this._firstGroup(), + firstMargin = { + item: margin.item, + axis: margin.axis + }, + nonFirstMargin = { + item: margin.item, + axis: margin.item / 2 + }, + height = 0, + minHeight = margin.axis + margin.item; + util.forEach(this.groups, function (group) { + var groupMargin = (group == firstGroup) ? firstMargin : nonFirstMargin; + var groupResized = group.redraw(range, groupMargin, restack); + resized = groupResized || resized; + height += group.height; + }); + height = Math.max(height, minHeight); + this.stackDirty = false; + + // update frame height + frame.style.height = asSize(height); + + // calculate actual size and position + 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.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; + + return resized; }; /** - * Start moving horizontally - * @param {Event} event + * Get the first group, aligned with the axis + * @return {Group | null} firstGroup * @private */ -CustomTime.prototype._onDragStart = function(event) { - this.eventParams.dragging = true; - this.eventParams.customTime = this.customTime; +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]; - event.stopPropagation(); - event.preventDefault(); + return firstGroup || null; }; /** - * Perform moving operating. - * @param {Event} event - * @private + * Create or delete the group holding all ungrouped items. This group is used when + * there are no groups specified. + * @protected */ -CustomTime.prototype._onDrag = function (event) { - if (!this.eventParams.dragging) return; - - var deltaX = event.gesture.deltaX, - x = this.body.util.toScreen(this.eventParams.customTime) + deltaX, - time = this.body.util.toTime(x); +ItemSet.prototype._updateUngrouped = function() { + var ungrouped = this.groups[UNGROUPED]; - this.setCustomTime(time); + if (this.groupsData) { + // remove the group holding all ungrouped items + if (ungrouped) { + ungrouped.hide(); + delete this.groups[UNGROUPED]; + } + } + else { + // create a group holding all (unfiltered) items + if (!ungrouped) { + var id = null; + var data = null; + ungrouped = new Group(id, data, this); + this.groups[UNGROUPED] = ungrouped; - // fire a timechange event - this.body.emitter.emit('timechange', { - time: new Date(this.customTime.valueOf()) - }); + for (var itemId in this.items) { + if (this.items.hasOwnProperty(itemId)) { + ungrouped.add(this.items[itemId]); + } + } - event.stopPropagation(); - event.preventDefault(); + ungrouped.show(); + } + } }; /** - * Stop moving operating. - * @param {event} event - * @private + * Get the element for the labelset + * @return {HTMLElement} labelSet */ -CustomTime.prototype._onDragEnd = function (event) { - if (!this.eventParams.dragging) return; - - // fire a timechanged event - this.body.emitter.emit('timechanged', { - time: new Date(this.customTime.valueOf()) - }); - - event.stopPropagation(); - event.preventDefault(); +ItemSet.prototype.getLabelSet = function() { + return this.dom.labelSet; }; -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 {{dom: Object, domProps: Object, emitter: Emitter, range: Range}} body - * @param {Object} [options] See ItemSet.setOptions for the available options. - * @constructor ItemSet - * @extends Component + * Set items + * @param {vis.DataSet | null} items */ -function ItemSet(body, options) { - this.body = body; - - this.defaultOptions = { - type: 'box', - 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 - - // 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); - } - }; - - // 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); - } - }; +ItemSet.prototype.setItems = function(items) { + var me = this, + ids, + oldItemsData = this.itemsData; - this.items = {}; // object with an Item for every data item - this.groups = {}; // Group object for every group - this.groupIds = []; + // replace the dataset + if (!items) { + this.itemsData = null; + } + else if (items instanceof DataSet || items instanceof DataView) { + this.itemsData = items; + } + else { + throw new TypeError('Data must be an instance of DataSet or DataView'); + } - this.selection = []; // list with the ids of all selected nodes - this.stackDirty = true; // if true, all items will be restacked on next redraw + if (oldItemsData) { + // unsubscribe from old dataset + util.forEach(this.itemListeners, function (callback, event) { + oldItemsData.off(event, callback); + }); - this.touchParams = {}; // stores properties while dragging - // create the HTML DOM + // remove all drawn items + ids = oldItemsData.getIds(); + this._onRemove(ids); + } - this._create(); + if (this.itemsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.itemListeners, function (callback, event) { + me.itemsData.on(event, callback, id); + }); - this.setOptions(options); -} + // add all new items + ids = this.itemsData.getIds(); + this._onAdd(ids); -ItemSet.prototype = new Component(); + // update the group holding all ungrouped items + this._updateUngrouped(); + } +}; -// available item types will be registered here -ItemSet.types = { - box: ItemBox, - range: ItemRange, - rangeoverflow: ItemRangeOverflow, - point: ItemPoint +/** + * Get the current items + * @returns {vis.DataSet | null} + */ +ItemSet.prototype.getItems = function() { + return this.itemsData; }; /** - * Create the HTML DOM for the ItemSet + * Set groups + * @param {vis.DataSet} groups */ -ItemSet.prototype._create = function(){ - var frame = document.createElement('div'); - frame.className = 'itemset'; - frame['timeline-itemset'] = this; - this.dom.frame = frame; +ItemSet.prototype.setGroups = function(groups) { + var me = this, + ids; - // create background panel - var background = document.createElement('div'); - background.className = 'background'; - frame.appendChild(background); - this.dom.background = background; + // unsubscribe from current dataset + if (this.groupsData) { + util.forEach(this.groupListeners, function (callback, event) { + me.groupsData.unsubscribe(event, callback); + }); - // create foreground panel - var foreground = document.createElement('div'); - foreground.className = 'foreground'; - frame.appendChild(foreground); - this.dom.foreground = foreground; + // remove all drawn groups + ids = this.groupsData.getIds(); + this.groupsData = null; + this._onRemoveGroups(ids); // note: this will cause a redraw + } - // create axis panel - var axis = document.createElement('div'); - axis.className = 'axis'; - this.dom.axis = axis; + // 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'); + } - // create labelset - var labelSet = document.createElement('div'); - labelSet.className = 'labelset'; - this.dom.labelSet = labelSet; + if (this.groupsData) { + // subscribe to new dataset + var id = this.id; + util.forEach(this.groupListeners, function (callback, event) { + me.groupsData.on(event, callback, id); + }); - // create ungrouped Group - this._updateUngrouped(); + // draw all ms + ids = this.groupsData.getIds(); + this._onAddGroups(ids); + } - // attach event listeners - // 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 - }); + // update the group holding all ungrouped items + this._updateUngrouped(); - // 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)); + // update the order of all items in each group + this._order(); - // single select (or unselect) when tapping an item - this.hammer.on('tap', this._onSelectItem.bind(this)); + this.body.emitter.emit('change'); +}; - // multi select when holding mouse/touch, or on ctrl+click - this.hammer.on('hold', this._onMultiSelectItem.bind(this)); +/** + * Get the current groups + * @returns {vis.DataSet | null} groups + */ +ItemSet.prototype.getGroups = function() { + return this.groupsData; +}; - // add item on doubletap - this.hammer.on('doubletap', this._onAddItem.bind(this)); +/** + * Remove an item by its id + * @param {String | Number} id + */ +ItemSet.prototype.removeItem = function(id) { + var item = this.itemsData.get(id), + dataset = this.itemsData.getDataSet(); - // attach to the DOM - this.show(); + if (item) { + // confirm deletion + this.options.onRemove(item, function (item) { + if (item) { + // remove by id here, it is possible that an item has no id defined + // itself, so better not delete by the item itself + dataset.remove(id); + } + }); + } }; /** - * Set options for the ItemSet. Existing options will be extended/overwritten. - * @param {Object} [options] The following options are available: - * {String} type - * Default type for the items. Choose from 'box' - * (default), 'point', or 'range'. The default - * Style can be overwritten by individual items. - * {String} align - * Alignment for the items, only applicable for - * ItemBox. Choose 'center' (default), 'left', or - * 'right'. - * {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. - * {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. + * Handle updated items + * @param {Number[]} ids + * @protected */ -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); +ItemSet.prototype._onUpdate = function(ids) { + var me = this; - if ('margin' in options) { - if (typeof options.margin === 'number') { - this.options.margin.axis = options.margin; - this.options.margin.item = options.margin; + ids.forEach(function (id) { + 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]; + + if (item) { + // update item + if (!constructor || !(item instanceof constructor)) { + // item type has changed, delete the item and recreate it + me._removeItem(item); + item = null; } - else if (typeof options.margin === 'object'){ - util.selectiveExtend(['axis', 'item'], this.options.margin, options.margin); + else { + me._updateItem(item, itemData); } } - 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; + if (!item) { + // create item + if (constructor) { + item = new constructor(itemData, me.conversion, me.options); + item.id = id; // TODO: not so nice setting id afterwards + me._addItem(item); } - else if (typeof options.editable === 'object') { - util.selectiveExtend(['updateTime', 'updateGroup', 'add', 'remove'], this.options.editable, options.editable); + 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;}'); } - } - - // 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; + else { + throw new TypeError('Unknown item type "' + type + '"'); } - }).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 redraw - */ -ItemSet.prototype.markDirty = function() { - this.groupIds = []; - this.stackDirty = true; + this._order(); + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit('change'); }; /** - * Destroy the ItemSet + * Handle added items + * @param {Number[]} ids + * @protected */ -ItemSet.prototype.destroy = function() { - this.hide(); - this.setItems(null); - this.setGroups(null); - - this.hammer = null; - - this.body = null; - this.conversion = null; -}; +ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate; /** - * Hide the component from the DOM + * Handle removed items + * @param {Number[]} ids + * @protected */ -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); - } +ItemSet.prototype._onRemove = function(ids) { + var count = 0; + var me = this; + ids.forEach(function (id) { + var item = me.items[id]; + if (item) { + count++; + me._removeItem(item); + } + }); - // remove the labelset containing all group labels - if (this.dom.labelSet.parentNode) { - this.dom.labelSet.parentNode.removeChild(this.dom.labelSet); + if (count) { + // update order + this._order(); + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit('change'); } }; /** - * Show the component in the DOM (when not already visible). - * @return {Boolean} changed + * Update the order of item in all groups + * @private */ -ItemSet.prototype.show = function() { - // show frame containing the items - if (!this.dom.frame.parentNode) { - this.body.dom.center.appendChild(this.dom.frame); - } - - // show axis with dots - if (!this.dom.axis.parentNode) { - this.body.dom.backgroundVertical.appendChild(this.dom.axis); - } +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) { + group.order(); + }); +}; - // show labelset containing labels - if (!this.dom.labelSet.parentNode) { - this.body.dom.left.appendChild(this.dom.labelSet); - } +/** + * Handle updated groups + * @param {Number[]} ids + * @private + */ +ItemSet.prototype._onUpdateGroups = function(ids) { + this._onAddGroups(ids); }; /** - * 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. + * Handle changed groups + * @param {Number[]} ids + * @private */ -ItemSet.prototype.setSelection = function(ids) { - var i, ii, id, item; +ItemSet.prototype._onAddGroups = function(ids) { + var me = this; - if (ids) { - if (!Array.isArray(ids)) { - throw new TypeError('Array expected'); - } + ids.forEach(function (id) { + var groupData = me.groupsData.get(id); + var group = me.groups[id]; - // unselect currently selected items - for (i = 0, ii = this.selection.length; i < ii; i++) { - id = this.selection[i]; - item = this.items[id]; - if (item) item.unselect(); - } + if (!group) { + // check for reserved ids + if (id == UNGROUPED) { + throw new Error('Illegal group id. ' + id + ' is a reserved id.'); + } - // select items - this.selection = []; - for (i = 0, ii = ids.length; i < ii; i++) { - id = ids[i]; - item = this.items[id]; - if (item) { - this.selection.push(id); - item.select(); + var groupOptions = Object.create(me.options); + util.extend(groupOptions, { + height: null + }); + + group = new Group(id, groupData, me); + me.groups[id] = group; + + // add items with this groupId to the new group + for (var itemId in me.items) { + if (me.items.hasOwnProperty(itemId)) { + var item = me.items[itemId]; + if (item.data.group == id) { + group.add(item); + } + } } + + group.order(); + group.show(); } - } + else { + // update group + group.setData(groupData); + } + }); + + this.body.emitter.emit('change'); }; /** - * Get the selected items by their id - * @return {Array} ids The ids of the selected items + * Handle removed groups + * @param {Number[]} ids + * @private */ -ItemSet.prototype.getSelection = function() { - return this.selection.concat([]); +ItemSet.prototype._onRemoveGroups = function(ids) { + var groups = this.groups; + ids.forEach(function (id) { + var group = groups[id]; + + if (group) { + group.hide(); + delete groups[id]; + } + }); + + this.markDirty(); + + this.body.emitter.emit('change'); }; /** - * Deselect a selected item - * @param {String | Number} id + * Reorder the groups if needed + * @return {boolean} changed * @private */ -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! - selection.splice(i, 1); - break; +ItemSet.prototype._orderGroups = function () { + if (this.groupsData) { + // reorder the groups + var groupIds = this.groupsData.getIds({ + order: this.options.groupOrder + }); + + var changed = !util.equalArray(groupIds, this.groupIds); + if (changed) { + // hide all groups, removes them from the DOM + var groups = this.groups; + groupIds.forEach(function (groupId) { + groups[groupId].hide(); + }); + + // show the groups again, attach them to the DOM in correct order + groupIds.forEach(function (groupId) { + groups[groupId].show(); + }); + + this.groupIds = groupIds; } + + return changed; + } + else { + return false; } }; /** - * Repaint the component - * @return {boolean} Returns true if the component is resized + * Add a new item + * @param {Item} item + * @private */ -ItemSet.prototype.redraw = function() { - var margin = this.options.margin, - range = this.body.range, - asSize = util.option.asSize, - options = this.options, - orientation = options.orientation, - resized = false, - frame = this.dom.frame, - editable = options.editable.updateTime || options.editable.updateGroup; +ItemSet.prototype._addItem = function(item) { + this.items[item.id] = item; - // update class name - frame.className = 'itemset' + (editable ? ' editable' : ''); + // add to group + var groupId = this.groupsData ? item.data.group : UNGROUPED; + var group = this.groups[groupId]; + if (group) group.add(item); +}; - // reorder the groups (if needed) - resized = this._orderGroups() || resized; +/** + * Update an existing item + * @param {Item} item + * @param {Object} itemData + * @private + */ +ItemSet.prototype._updateItem = function(item, itemData) { + var oldGroupId = item.data.group; - // 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 = range.end - range.start; - var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.props.width != this.props.lastWidth); - if (zoomed) this.stackDirty = true; - this.lastVisibleInterval = visibleInterval; - this.props.lastWidth = this.props.width; + item.data = itemData; + if (item.displayed) { + item.redraw(); + } - // redraw all groups - var restack = this.stackDirty, - firstGroup = this._firstGroup(), - firstMargin = { - item: margin.item, - axis: margin.axis - }, - nonFirstMargin = { - item: margin.item, - axis: margin.item / 2 - }, - height = 0, - minHeight = margin.axis + margin.item; - util.forEach(this.groups, function (group) { - var groupMargin = (group == firstGroup) ? firstMargin : nonFirstMargin; - var groupResized = group.redraw(range, groupMargin, restack); - resized = groupResized || resized; - height += group.height; - }); - height = Math.max(height, minHeight); - this.stackDirty = false; + // update group + if (oldGroupId != item.data.group) { + var oldGroup = this.groups[oldGroupId]; + if (oldGroup) oldGroup.remove(item); - // update frame height - frame.style.height = asSize(height); + var groupId = this.groupsData ? item.data.group : UNGROUPED; + var group = this.groups[groupId]; + if (group) group.add(item); + } +}; - // calculate actual size and position - this.props.top = frame.offsetTop; - this.props.left = frame.offsetLeft; - this.props.width = frame.offsetWidth; - this.props.height = height; +/** + * Delete an item from the ItemSet: remove it from the DOM, from the map + * with items, and from the map with visible items, and from the selection + * @param {Item} item + * @private + */ +ItemSet.prototype._removeItem = function(item) { + // remove from DOM + item.hide(); - // reposition axis - 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'; + // remove from items + delete this.items[item.id]; - // check if this component is resized - resized = this._isResized() || resized; + // remove from selection + var index = this.selection.indexOf(item.id); + if (index != -1) this.selection.splice(index, 1); - return resized; + // remove from group + var groupId = this.groupsData ? item.data.group : UNGROUPED; + var group = this.groups[groupId]; + if (group) group.remove(item); }; /** - * Get the first group, aligned with the axis - * @return {Group | null} firstGroup + * Create an array containing all items being a range (having an end date) + * @param array + * @returns {Array} * @private */ -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]; +ItemSet.prototype._constructByEndArray = function(array) { + var endArray = []; - return firstGroup || null; + for (var i = 0; i < array.length; i++) { + if (array[i] instanceof ItemRange) { + endArray.push(array[i]); + } + } + return endArray; }; /** - * Create or delete the group holding all ungrouped items. This group is used when - * there are no groups specified. - * @protected + * 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._updateUngrouped = function() { - var ungrouped = this.groups[UNGROUPED]; +ItemSet.prototype._onTouch = function (event) { + // store the touched item, used in _onDragStart + this.touchParams.item = ItemSet.itemFromTarget(event); +}; - if (this.groupsData) { - // remove the group holding all ungrouped items - if (ungrouped) { - ungrouped.hide(); - delete this.groups[UNGROUPED]; - } +/** + * Start dragging the selected events + * @param {Event} event + * @private + */ +ItemSet.prototype._onDragStart = function (event) { + if (!this.options.editable.updateTime && !this.options.editable.updateGroup) { + return; } - else { - // create a group holding all (unfiltered) items - if (!ungrouped) { - var id = null; - var data = null; - ungrouped = new Group(id, data, this); - this.groups[UNGROUPED] = ungrouped; - for (var itemId in this.items) { - if (this.items.hasOwnProperty(itemId)) { - ungrouped.add(this.items[itemId]); - } + var item = this.touchParams.item || null, + me = this, + props; + + if (item && item.selected) { + var dragLeftItem = event.target.dragLeftItem; + var dragRightItem = event.target.dragRightItem; + + if (dragLeftItem) { + props = { + item: dragLeftItem + }; + + if (me.options.editable.updateTime) { + props.start = item.data.start.valueOf(); + } + if (me.options.editable.updateGroup) { + if ('group' in item.data) props.group = item.data.group; } - ungrouped.show(); + this.touchParams.itemProps = [props]; } - } -}; + else if (dragRightItem) { + props = { + item: dragRightItem + }; -/** - * Get the element for the labelset - * @return {HTMLElement} labelSet - */ -ItemSet.prototype.getLabelSet = function() { - return this.dom.labelSet; + if (me.options.editable.updateTime) { + props.end = item.data.end.valueOf(); + } + if (me.options.editable.updateGroup) { + if ('group' in item.data) props.group = item.data.group; + } + + this.touchParams.itemProps = [props]; + } + else { + this.touchParams.itemProps = this.getSelection().map(function (id) { + var item = me.items[id]; + var props = { + item: item + }; + + if (me.options.editable.updateTime) { + if ('start' in item.data) props.start = item.data.start.valueOf(); + if ('end' in item.data) props.end = item.data.end.valueOf(); + } + if (me.options.editable.updateGroup) { + if ('group' in item.data) props.group = item.data.group; + } + + return props; + }); + } + + event.stopPropagation(); + } }; /** - * Set items - * @param {vis.DataSet | null} items + * Drag selected items + * @param {Event} event + * @private */ -ItemSet.prototype.setItems = function(items) { - var me = this, - ids, - oldItemsData = this.itemsData; +ItemSet.prototype._onDrag = function (event) { + if (this.touchParams.itemProps) { + var range = this.body.range, + snap = this.body.util.snap || null, + deltaX = event.gesture.deltaX, + scale = (this.props.width / (range.end - range.start)), + offset = deltaX / scale; - // replace the dataset - if (!items) { - this.itemsData = null; - } - else if (items instanceof DataSet || items instanceof DataView) { - this.itemsData = items; - } - else { - throw new TypeError('Data must be an instance of DataSet or DataView'); - } + // move + this.touchParams.itemProps.forEach(function (props) { + if ('start' in props) { + var start = new Date(props.start + offset); + props.item.data.start = snap ? snap(start) : start; + } - if (oldItemsData) { - // unsubscribe from old dataset - util.forEach(this.itemListeners, function (callback, event) { - oldItemsData.off(event, callback); - }); + if ('end' in props) { + var end = new Date(props.end + offset); + props.item.data.end = snap ? snap(end) : end; + } - // remove all drawn items - ids = oldItemsData.getIds(); - this._onRemove(ids); - } + if ('group' in props) { + // drag from one group to another + var group = ItemSet.groupFromTarget(event); + if (group && group.groupId != props.item.data.group) { + var oldGroup = props.item.parent; + oldGroup.remove(props.item); + oldGroup.order(); + group.add(props.item); + group.order(); - if (this.itemsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.itemListeners, function (callback, event) { - me.itemsData.on(event, callback, id); + props.item.data.group = group.groupId; + } + } }); - // add all new items - ids = this.itemsData.getIds(); - this._onAdd(ids); + // TODO: implement onMoving handler - // update the group holding all ungrouped items - this._updateUngrouped(); + this.stackDirty = true; // force re-stacking of all items next redraw + this.body.emitter.emit('change'); + + event.stopPropagation(); } }; /** - * Get the current items - * @returns {vis.DataSet | null} + * End of dragging selected items + * @param {Event} event + * @private */ -ItemSet.prototype.getItems = function() { - return this.itemsData; +ItemSet.prototype._onDragEnd = function (event) { + if (this.touchParams.itemProps) { + // prepare a change set for the changed items + var changes = [], + me = this, + dataset = this.itemsData.getDataSet(); + + this.touchParams.itemProps.forEach(function (props) { + var id = props.item.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._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._options.type && dataset._options.type.end || 'Date'); + } + if ('group' in props.item.data) { + changed = changed || (props.group != props.item.data.group); + itemData.group = props.item.data.group; + } + + // only apply changes when start or end is actually changed + if (changed) { + me.options.onMove(itemData, function (itemData) { + if (itemData) { + // apply changes + itemData[dataset._fieldId] = id; // ensure the item contains its id (can be undefined) + changes.push(itemData); + } + else { + // restore original values + 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 redraw + me.body.emitter.emit('change'); + } + }); + } + }); + this.touchParams.itemProps = null; + + // apply the changes to the data (if there are changes) + if (changes.length) { + dataset.update(changes); + } + + event.stopPropagation(); + } }; /** - * Set groups - * @param {vis.DataSet} groups + * Handle selecting/deselecting an item when tapping it + * @param {Event} event + * @private */ -ItemSet.prototype.setGroups = function(groups) { - var me = this, - ids; - - // 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 - } +ItemSet.prototype._onSelectItem = function (event) { + if (!this.options.selectable) return; - // 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'); + 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; } - if (this.groupsData) { - // subscribe to new dataset - var id = this.id; - util.forEach(this.groupListeners, function (callback, event) { - me.groupsData.on(event, callback, id); - }); + var oldSelection = this.getSelection(); - // draw all ms - ids = this.groupsData.getIds(); - this._onAddGroups(ids); - } + var item = ItemSet.itemFromTarget(event); + var selection = item ? [item.id] : []; + this.setSelection(selection); - // update the group holding all ungrouped items - this._updateUngrouped(); + var newSelection = this.getSelection(); - // update the order of all items in each group - this._order(); + // 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() + }); + } - this.body.emitter.emit('change'); + event.stopPropagation(); }; /** - * Get the current groups - * @returns {vis.DataSet | null} groups + * Handle creation and updates of an item on double tap + * @param event + * @private */ -ItemSet.prototype.getGroups = function() { - return this.groupsData; -}; +ItemSet.prototype._onAddItem = function (event) { + if (!this.options.selectable) return; + if (!this.options.editable.add) return; -/** - * Remove an item by its id - * @param {String | Number} id - */ -ItemSet.prototype.removeItem = function(id) { - var item = this.itemsData.get(id), - dataset = this._myDataSet(); + var me = this, + snap = this.body.util.snap || null, + item = ItemSet.itemFromTarget(event); if (item) { - // confirm deletion - this.options.onRemove(item, function (item) { - if (item) { - // remove by id here, it is possible that an item has no id defined - // itself, so better not delete by the item itself - dataset.remove(id); + // 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); } }); } -}; - -/** - * Handle updated items - * @param {Number[]} ids - * @protected - */ -ItemSet.prototype._onUpdate = function(ids) { - var me = this; + 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' + }; - ids.forEach(function (id) { - var itemData = me.itemsData.get(id, me.itemOptions), - item = me.items[id], - type = itemData.type || - (itemData.start && itemData.end && 'range') || - me.options.type || - 'box'; + // 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; + } - var constructor = ItemSet.types[type]; + newItem[this.itemsData.fieldId] = util.randomUUID(); - if (item) { - // update item - if (!constructor || !(item instanceof constructor)) { - // item type has changed, delete the item and recreate it - me._removeItem(item); - item = null; - } - else { - me._updateItem(item, itemData); - } + var group = ItemSet.groupFromTarget(event); + if (group) { + newItem.group = group.groupId; } - if (!item) { - // create item - if (constructor) { - item = new constructor(itemData, me.conversion, me.options); - item.id = id; // TODO: not so nice setting id afterwards - me._addItem(item); - } - else { - throw new TypeError('Unknown item type "' + type + '"'); + // 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? } - } - }); - - this._order(); - this.stackDirty = true; // force re-stacking of all items next redraw - this.body.emitter.emit('change'); + }); + } }; /** - * Handle added items - * @param {Number[]} ids - * @protected + * Handle selecting/deselecting multiple items when holding an item + * @param {Event} event + * @private */ -ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate; +ItemSet.prototype._onMultiSelectItem = function (event) { + if (!this.options.selectable) return; -/** - * Handle removed items - * @param {Number[]} ids - * @protected - */ -ItemSet.prototype._onRemove = function(ids) { - var count = 0; - var me = this; - ids.forEach(function (id) { - var item = me.items[id]; - if (item) { - count++; - me._removeItem(item); - } - }); + var selection, + item = ItemSet.itemFromTarget(event); - if (count) { - // update order - this._order(); - this.stackDirty = true; // force re-stacking of all items next redraw - this.body.emitter.emit('change'); - } -}; + 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); -/** - * Update the order of item in all groups - * @private - */ -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) { - group.order(); - }); -}; + this.body.emitter.emit('select', { + items: this.getSelection() + }); -/** - * Handle updated groups - * @param {Number[]} ids - * @private - */ -ItemSet.prototype._onUpdateGroups = function(ids) { - this._onAddGroups(ids); + event.stopPropagation(); + } }; /** - * Handle changed groups - * @param {Number[]} ids - * @private + * 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.prototype._onAddGroups = function(ids) { - var me = this; - - ids.forEach(function (id) { - var groupData = me.groupsData.get(id); - var group = me.groups[id]; - - if (!group) { - // check for reserved ids - if (id == UNGROUPED) { - throw new Error('Illegal group id. ' + id + ' is a reserved id.'); - } +ItemSet.itemFromTarget = function(event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty('timeline-item')) { + return target['timeline-item']; + } + target = target.parentNode; + } - var groupOptions = Object.create(me.options); - util.extend(groupOptions, { - height: null - }); + return null; +}; - group = new Group(id, groupData, me); - me.groups[id] = group; +/** + * Find the Group from an event target: + * searches for the attribute 'timeline-group' in the event target's element tree + * @param {Event} event + * @return {Group | null} group + */ +ItemSet.groupFromTarget = function(event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty('timeline-group')) { + return target['timeline-group']; + } + target = target.parentNode; + } - // add items with this groupId to the new group - for (var itemId in me.items) { - if (me.items.hasOwnProperty(itemId)) { - var item = me.items[itemId]; - if (item.data.group == id) { - group.add(item); - } - } - } + return null; +}; - group.order(); - group.show(); - } - else { - // update group - group.setData(groupData); +/** + * Find the ItemSet from an event target: + * searches for the attribute 'timeline-itemset' in the event target's element tree + * @param {Event} event + * @return {ItemSet | null} item + */ +ItemSet.itemSetFromTarget = function(event) { + var target = event.target; + while (target) { + if (target.hasOwnProperty('timeline-itemset')) { + return target['timeline-itemset']; } - }); + target = target.parentNode; + } - this.body.emitter.emit('change'); + return null; }; + /** - * Handle removed groups - * @param {Number[]} ids - * @private + * @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 */ -ItemSet.prototype._onRemoveGroups = function(ids) { - var groups = this.groups; - ids.forEach(function (id) { - var group = groups[id]; +function Item (data, conversion, options) { + this.id = null; + this.parent = null; + this.data = data; + this.dom = null; + this.conversion = conversion || {}; + this.options = options || {}; - if (group) { - group.hide(); - delete groups[id]; - } - }); + this.selected = false; + this.displayed = false; + this.dirty = true; - this.markDirty(); + this.top = null; + this.left = null; + this.width = null; + this.height = null; +} - this.body.emitter.emit('change'); +/** + * Select current item + */ +Item.prototype.select = function() { + this.selected = true; + if (this.displayed) this.redraw(); }; /** - * Reorder the groups if needed - * @return {boolean} changed - * @private + * Unselect current item */ -ItemSet.prototype._orderGroups = function () { - if (this.groupsData) { - // reorder the groups - var groupIds = this.groupsData.getIds({ - order: this.options.groupOrder - }); - - var changed = !util.equalArray(groupIds, this.groupIds); - if (changed) { - // hide all groups, removes them from the DOM - var groups = this.groups; - groupIds.forEach(function (groupId) { - groups[groupId].hide(); - }); - - // show the groups again, attach them to the DOM in correct order - groupIds.forEach(function (groupId) { - groups[groupId].show(); - }); +Item.prototype.unselect = function() { + this.selected = false; + if (this.displayed) this.redraw(); +}; - this.groupIds = groupIds; +/** + * 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(); } - - return changed; } else { - return false; + this.parent = parent; } }; /** - * Add a new item - * @param {Item} item - * @private + * 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 */ -ItemSet.prototype._addItem = function(item) { - this.items[item.id] = item; +Item.prototype.isVisible = function(range) { + // Should be implemented by Item implementations + return false; +}; - // add to group - var groupId = this.groupsData ? item.data.group : UNGROUPED; - var group = this.groups[groupId]; - if (group) group.add(item); +/** + * Show the Item in the DOM (when not already visible) + * @return {Boolean} changed + */ +Item.prototype.show = function() { + return false; }; /** - * Update an existing item - * @param {Item} item - * @param {Object} itemData - * @private + * Hide the Item from the DOM (when visible) + * @return {Boolean} changed */ -ItemSet.prototype._updateItem = function(item, itemData) { - var oldGroupId = item.data.group; +Item.prototype.hide = function() { + return false; +}; - item.data = itemData; - if (item.displayed) { - item.redraw(); - } +/** + * Repaint the item + */ +Item.prototype.redraw = function() { + // should be implemented by the item +}; - // update group - if (oldGroupId != item.data.group) { - var oldGroup = this.groups[oldGroupId]; - if (oldGroup) oldGroup.remove(item); +/** + * Reposition the Item horizontally + */ +Item.prototype.repositionX = function() { + // should be implemented by the item +}; - var groupId = this.groupsData ? item.data.group : UNGROUPED; - var group = this.groups[groupId]; - if (group) group.add(item); - } +/** + * Reposition the Item vertically + */ +Item.prototype.repositionY = function() { + // should be implemented by the item }; /** - * Delete an item from the ItemSet: remove it from the DOM, from the map - * with items, and from the map with visible items, and from the selection - * @param {Item} item - * @private + * Repaint a delete button on the top right of the item when the item is selected + * @param {HTMLElement} anchor + * @protected */ -ItemSet.prototype._removeItem = function(item) { - // remove from DOM - item.hide(); +Item.prototype._repaintDeleteButton = function (anchor) { + if (this.selected && this.options.editable.remove && !this.dom.deleteButton) { + // create and show button + var me = this; - // remove from items - delete this.items[item.id]; + var deleteButton = document.createElement('div'); + deleteButton.className = 'delete'; + deleteButton.title = 'Delete this item'; - // remove from selection - var index = this.selection.indexOf(item.id); - if (index != -1) this.selection.splice(index, 1); + Hammer(deleteButton, { + preventDefault: true + }).on('tap', function (event) { + me.parent.removeFromDataSet(me); + event.stopPropagation(); + }); - // remove from group - var groupId = this.groupsData ? item.data.group : UNGROUPED; - var group = this.groups[groupId]; - if (group) group.remove(item); + 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; + } }; /** - * Create an array containing all items being a range (having an end date) - * @param array - * @returns {Array} - * @private + * @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 */ -ItemSet.prototype._constructByEndArray = function(array) { - var endArray = []; +function ItemBox (data, conversion, options) { + this.props = { + dot: { + width: 0, + height: 0 + }, + line: { + width: 0, + height: 0 + } + }; - for (var i = 0; i < array.length; i++) { - if (array[i] instanceof ItemRange) { - endArray.push(array[i]); + // validate data + if (data) { + if (data.start == undefined) { + throw new Error('Property "start" missing in item ' + data); } } - return endArray; -}; + + Item.call(this, data, conversion, options); +} + +ItemBox.prototype = new Item (null, null, null); /** - * 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 + * 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 */ -ItemSet.prototype._onTouch = function (event) { - // store the touched item, used in _onDragStart - this.touchParams.item = ItemSet.itemFromTarget(event); +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); }; /** - * Start dragging the selected events - * @param {Event} event - * @private + * Repaint the item */ -ItemSet.prototype._onDragStart = function (event) { - if (!this.options.editable.updateTime && !this.options.editable.updateGroup) { - return; - } +ItemBox.prototype.redraw = function() { + var dom = this.dom; + if (!dom) { + // create DOM + this.dom = {}; + dom = this.dom; - var item = this.touchParams.item || null, - me = this, - props; + // create main box + dom.box = document.createElement('DIV'); - if (item && item.selected) { - var dragLeftItem = event.target.dragLeftItem; - var dragRightItem = event.target.dragRightItem; + // contents box (inside the background box). used for making margins + dom.content = document.createElement('DIV'); + dom.content.className = 'content'; + dom.box.appendChild(dom.content); - if (dragLeftItem) { - props = { - item: dragLeftItem - }; + // line to axis + dom.line = document.createElement('DIV'); + dom.line.className = 'line'; - if (me.options.editable.updateTime) { - props.start = item.data.start.valueOf(); - } - if (me.options.editable.updateGroup) { - if ('group' in item.data) props.group = item.data.group; - } + // dot on axis + dom.dot = document.createElement('DIV'); + dom.dot.className = 'dot'; - this.touchParams.itemProps = [props]; - } - else if (dragRightItem) { - props = { - item: dragRightItem - }; + // attach this item as attribute + dom.box['timeline-item'] = this; + } - if (me.options.editable.updateTime) { - props.end = item.data.end.valueOf(); - } - if (me.options.editable.updateGroup) { - if ('group' in item.data) props.group = item.data.group; - } + // 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; - this.touchParams.itemProps = [props]; + // 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 { - this.touchParams.itemProps = this.getSelection().map(function (id) { - var item = me.items[id]; - var props = { - item: item - }; + throw new Error('Property "content" missing in item ' + this.data.id); + } - if (me.options.editable.updateTime) { - if ('start' in item.data) props.start = item.data.start.valueOf(); - if ('end' in item.data) props.end = item.data.end.valueOf(); - } - if (me.options.editable.updateGroup) { - if ('group' in item.data) props.group = item.data.group; - } + this.dirty = true; + } - return props; - }); - } + // update title + if (this.data.title != this.title) { + dom.box.title = this.data.title; + this.title = this.data.title; + } - event.stopPropagation(); + // 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); }; /** - * Drag selected items - * @param {Event} event - * @private + * Show the item in the DOM (when not already displayed). The items DOM will + * be created when needed. */ -ItemSet.prototype._onDrag = function (event) { - if (this.touchParams.itemProps) { - var range = this.body.range, - snap = this.body.util.snap || null, - deltaX = event.gesture.deltaX, - scale = (this.props.width / (range.end - range.start)), - offset = deltaX / scale; +ItemBox.prototype.show = function() { + if (!this.displayed) { + this.redraw(); + } +}; - // move - this.touchParams.itemProps.forEach(function (props) { - if ('start' in props) { - var start = new Date(props.start + offset); - props.item.data.start = snap ? snap(start) : start; - } +/** + * Hide the item from the DOM (when visible) + */ +ItemBox.prototype.hide = function() { + if (this.displayed) { + var dom = this.dom; - if ('end' in props) { - var end = new Date(props.end + offset); - props.item.data.end = snap ? snap(end) : end; - } + 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); - if ('group' in props) { - // drag from one group to another - var group = ItemSet.groupFromTarget(event); - if (group && group.groupId != props.item.data.group) { - var oldGroup = props.item.parent; - oldGroup.remove(props.item); - oldGroup.order(); - group.add(props.item); - group.order(); + this.top = null; + this.left = null; - props.item.data.group = group.groupId; - } - } - }); + this.displayed = false; + } +}; + +/** + * Reposition the item horizontally + * @Override + */ +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; + + // calculate left position of the box + if (align == 'right') { + this.left = start - this.width; + } + else if (align == 'left') { + this.left = start; + } + else { + // default or 'center' + this.left = start - this.width / 2; + } - // TODO: implement onMoving handler + // reposition box + box.style.left = this.left + 'px'; - this.stackDirty = true; // force re-stacking of all items next redraw - this.body.emitter.emit('change'); + // reposition line + line.style.left = (start - this.props.line.width / 2) + 'px'; - event.stopPropagation(); - } + // reposition dot + dot.style.left = (start - this.props.dot.width / 2) + 'px'; }; /** - * End of dragging selected items - * @param {Event} event - * @private + * Reposition the item vertically + * @Override */ -ItemSet.prototype._onDragEnd = function (event) { - if (this.touchParams.itemProps) { - // prepare a change set for the changed items - var changes = [], - me = this, - dataset = this._myDataSet(); - - this.touchParams.itemProps.forEach(function (props) { - var id = props.item.id, - 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._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._options.type && dataset._options.type.end || 'Date'); - } - if ('group' in props.item.data) { - changed = changed || (props.group != props.item.data.group); - itemData.group = props.item.data.group; - } - - // only apply changes when start or end is actually changed - if (changed) { - me.options.onMove(itemData, function (itemData) { - if (itemData) { - // apply changes - itemData[dataset._fieldId] = id; // ensure the item contains its id (can be undefined) - changes.push(itemData); - } - else { - // restore original values - if ('start' in props) props.item.data.start = props.start; - if ('end' in props) props.item.data.end = props.end; +ItemBox.prototype.repositionY = function() { + var orientation = this.options.orientation, + box = this.dom.box, + line = this.dom.line, + dot = this.dom.dot; - me.stackDirty = true; // force re-stacking of all items next redraw - me.body.emitter.emit('change'); - } - }); - } - }); - this.touchParams.itemProps = null; + if (orientation == 'top') { + box.style.top = (this.top || 0) + 'px'; - // apply the changes to the data (if there are changes) - if (changes.length) { - dataset.update(changes); - } + 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; - event.stopPropagation(); + 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'; }; /** - * Handle selecting/deselecting an item when tapping it - * @param {Event} event - * @private + * @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 */ -ItemSet.prototype._onSelectItem = function (event) { - if (!this.options.selectable) return; +function ItemPoint (data, conversion, options) { + this.props = { + dot: { + top: 0, + width: 0, + height: 0 + }, + content: { + height: 0, + marginLeft: 0 + } + }; - 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; + // validate data + if (data) { + if (data.start == undefined) { + throw new Error('Property "start" missing in item ' + data); + } } - var oldSelection = this.getSelection(); - - var item = ItemSet.itemFromTarget(event); - var selection = item ? [item.id] : []; - this.setSelection(selection); - - var newSelection = this.getSelection(); + Item.call(this, data, conversion, options); +} - // 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() - }); - } +ItemPoint.prototype = new Item (null, null, null); - event.stopPropagation(); +/** + * Check whether this item is visible inside given range + * @returns {{start: Number, end: Number}} range with a timestamp for start and end + * @returns {boolean} True if visible + */ +ItemPoint.prototype.isVisible = function(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); }; /** - * Handle creation and updates of an item on double tap - * @param event - * @private + * Repaint the item */ -ItemSet.prototype._onAddItem = function (event) { - if (!this.options.selectable) return; - if (!this.options.editable.add) return; +ItemPoint.prototype.redraw = function() { + var dom = this.dom; + if (!dom) { + // create DOM + this.dom = {}; + dom = this.dom; - var me = this, - snap = this.body.util.snap || null, - item = ItemSet.itemFromTarget(event); + // background box + dom.point = document.createElement('div'); + // className is updated in redraw() - if (item) { - // update item + // contents box, right from the dot + dom.content = document.createElement('div'); + dom.content.className = 'content'; + dom.point.appendChild(dom.content); - // 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); - } - }); + // dot at start + dom.dot = document.createElement('div'); + dom.point.appendChild(dom.dot); + + // attach this item as attribute + dom.point['timeline-item'] = this; } - 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' || this.options.type == 'rangeoverflow') { - var end = this.body.util.toTime(x + this.props.width / 5); - newItem.end = snap ? snap(end) : end; + // 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; - newItem[this.itemsData.fieldId] = util.randomUUID(); - - var group = ItemSet.groupFromTarget(event); - if (group) { - newItem.group = group.groupId; + // 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); } - // 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? - } - }); + this.dirty = true; } -}; -/** - * Handle selecting/deselecting multiple items when holding an item - * @param {Event} event - * @private - */ -ItemSet.prototype._onMultiSelectItem = function (event) { - if (!this.options.selectable) return; + // update title + if (this.data.title != this.title) { + dom.point.title = this.data.title; + this.title = this.data.title; + } - var selection, - item = ItemSet.itemFromTarget(event); + // 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; + } - 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); + // 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; - this.body.emitter.emit('select', { - items: this.getSelection() - }); + // resize contents + dom.content.style.marginLeft = 2 * this.props.dot.width + 'px'; + //dom.content.style.marginRight = ... + 'px'; // TODO: margin right - event.stopPropagation(); + 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); }; /** - * 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 + * Show the item in the DOM (when not already visible). The items DOM will + * be created when needed. */ -ItemSet.itemFromTarget = function(event) { - var target = event.target; - while (target) { - if (target.hasOwnProperty('timeline-item')) { - return target['timeline-item']; - } - target = target.parentNode; +ItemPoint.prototype.show = function() { + if (!this.displayed) { + this.redraw(); } - - return null; }; /** - * Find the Group from an event target: - * searches for the attribute 'timeline-group' in the event target's element tree - * @param {Event} event - * @return {Group | null} group + * Hide the item from the DOM (when visible) */ -ItemSet.groupFromTarget = function(event) { - var target = event.target; - while (target) { - if (target.hasOwnProperty('timeline-group')) { - return target['timeline-group']; +ItemPoint.prototype.hide = function() { + if (this.displayed) { + if (this.dom.point.parentNode) { + this.dom.point.parentNode.removeChild(this.dom.point); } - target = target.parentNode; - } - return null; + this.top = null; + this.left = null; + + this.displayed = false; + } }; /** - * Find the ItemSet from an event target: - * searches for the attribute 'timeline-itemset' in the event target's element tree - * @param {Event} event - * @return {ItemSet | null} item + * Reposition the item horizontally + * @Override */ -ItemSet.itemSetFromTarget = function(event) { - var target = event.target; - while (target) { - if (target.hasOwnProperty('timeline-itemset')) { - return target['timeline-itemset']; - } - target = target.parentNode; - } +ItemPoint.prototype.repositionX = function() { + var start = this.conversion.toScreen(this.data.start); - return null; + this.left = start - this.props.dot.width; + + // reposition point + this.dom.point.style.left = this.left + 'px'; }; /** - * Find the DataSet to which this ItemSet is connected - * @returns {null | DataSet} dataset - * @private + * Reposition the item vertically + * @Override */ -ItemSet.prototype._myDataSet = function() { - // find the root DataSet - var dataset = this.itemsData; - while (dataset instanceof DataView) { - dataset = dataset.data; +ItemPoint.prototype.repositionY = function() { + var orientation = this.options.orientation, + point = this.dom.point; + + if (orientation == 'top') { + point.style.top = this.top + 'px'; + } + else { + point.style.top = (this.parent.height - this.top - this.height) + 'px'; } - return dataset; }; + /** - * @constructor Item - * @param {Object} data Object containing (optional) parameters type, - * start, end, content, group, className. + * @constructor ItemRange + * @extends Item + * @param {Object} data Object containing parameters start, end + * 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 + * @param {Object} [options] Configuration options + * // TODO: describe options */ -function Item (data, conversion, options) { - this.id = null; - this.parent = null; - this.data = data; - this.dom = null; - this.conversion = conversion || {}; - this.options = options || {}; +function ItemRange (data, conversion, options) { + this.props = { + content: { + width: 0 + } + }; + this.overflow = false; // if contents can overflow (css styling), this flag is set to true - this.selected = false; - this.displayed = false; - this.dirty = true; + // 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); + } + } - this.top = null; - this.left = null; - this.width = null; - this.height = null; + Item.call(this, data, conversion, options); } -/** - * Select current item - */ -Item.prototype.select = function() { - this.selected = true; - if (this.displayed) this.redraw(); -}; +ItemRange.prototype = new Item (null, null, null); + +ItemRange.prototype.baseClassName = 'item range'; /** - * Unselect current item + * 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.unselect = function() { - this.selected = false; - if (this.displayed) this.redraw(); +ItemRange.prototype.isVisible = function(range) { + // determine visibility + return (this.data.start < range.end) && (this.data.end > range.start); }; /** - * Set a parent for the item - * @param {ItemSet | Group} parent + * Repaint the item */ -Item.prototype.setParent = function(parent) { - if (this.displayed) { - this.hide(); - this.parent = parent; - if (this.parent) { - this.show(); +ItemRange.prototype.redraw = function() { + var dom = this.dom; + if (!dom) { + // create DOM + this.dom = {}; + dom = this.dom; + + // background box + dom.box = document.createElement('div'); + // className is updated in redraw() + + // contents box + dom.content = document.createElement('div'); + dom.content.className = 'content'; + dom.box.appendChild(dom.content); + + // attach this item as attribute + dom.box['timeline-item'] = this; + } + + // 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); } - else { - this.parent = parent; + 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; } -}; -/** - * 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; + // 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 = this.baseClassName + className; + + this.dirty = true; + } + + // recalculate size + if (this.dirty) { + // 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._repaintDragLeft(); + this._repaintDragRight(); }; /** - * Show the Item in the DOM (when not already visible) - * @return {Boolean} changed + * Show the item in the DOM (when not already visible). The items DOM will + * be created when needed. */ -Item.prototype.show = function() { - return false; +ItemRange.prototype.show = function() { + if (!this.displayed) { + this.redraw(); + } }; /** - * Hide the Item from the DOM (when visible) + * Hide the item from the DOM (when visible) * @return {Boolean} changed */ -Item.prototype.hide = function() { - return false; +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; + } }; /** - * Repaint the item + * Reposition the item horizontally + * @Override */ -Item.prototype.redraw = function() { - // should be implemented by the item +// 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 horizontally + * Reposition the item vertically + * @Override */ -Item.prototype.repositionX = function() { - // should be implemented by the item +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'; + } }; /** - * Reposition the Item vertically + * Repaint a drag area on the left side of the range when the range is selected + * @protected */ -Item.prototype.repositionY = function() { - // should be implemented by the item +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 delete button on the top right of the item when the item is selected - * @param {HTMLElement} anchor + * Repaint a drag area on the right side of the range when the range is selected * @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'; +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; - Hammer(deleteButton, { + // TODO: this should be redundant? + Hammer(dragRight, { preventDefault: true - }).on('tap', function (event) { - me.parent.removeFromDataSet(me); - event.stopPropagation(); + }).on('drag', function () { + //console.log('drag right') }); - anchor.appendChild(deleteButton); - this.dom.deleteButton = deleteButton; + this.dom.box.appendChild(dragRight); + this.dom.dragRight = dragRight; } - else if (!this.selected && this.dom.deleteButton) { - // remove button - if (this.dom.deleteButton.parentNode) { - this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton); + 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.deleteButton = null; + this.dom.dragRight = 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 + * @constructor Group + * @param {Number | String} groupId + * @param {Object} data + * @param {ItemSet} itemSet */ -function ItemBox (data, conversion, options) { +function Group (groupId, data, itemSet) { + this.groupId = groupId; + + this.itemSet = itemSet; + + this.dom = {}; this.props = { - dot: { - width: 0, - height: 0 - }, - line: { + label: { width: 0, height: 0 } }; + this.className = null; - // validate data - if (data) { - if (data.start == undefined) { - throw new Error('Property "start" missing in item ' + data); + 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); +} + +/** + * Create DOM elements for the group + * @private + */ +Group.prototype._create = function() { + var label = document.createElement('div'); + label.className = 'vlabel'; + this.dom.label = label; + + var inner = document.createElement('div'); + inner.className = 'inner'; + label.appendChild(inner); + this.dom.inner = inner; + + var foreground = document.createElement('div'); + foreground.className = 'group'; + foreground['timeline-group'] = this; + this.dom.foreground = foreground; + + this.dom.background = document.createElement('div'); + this.dom.background.className = 'group'; + + 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); +}; + +/** + * Set the group data for this group + * @param {Object} data Group data, can contain properties content and className + */ +Group.prototype.setData = function(data) { + // update contents + var content = data && data.content; + if (content instanceof Element) { + this.dom.inner.appendChild(content); + } + else if (content != undefined) { + this.dom.inner.innerHTML = content; + } + else { + this.dom.inner.innerHTML = this.groupId; + } + + // update title + this.dom.label.title = data && data.title || ''; + + if (!this.dom.inner.firstChild) { + util.addClassName(this.dom.inner, 'hidden'); + } + else { + util.removeClassName(this.dom.inner, 'hidden'); + } + + // 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, 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 + * Get the width of the group label + * @return {number} width */ -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); +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 */ -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'); +Group.prototype.redraw = function(range, margin, restack) { + var resized = false; - // contents box (inside the background box). used for making margins - dom.content = document.createElement('DIV'); - dom.content.className = 'content'; - dom.box.appendChild(dom.content); + this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); - // line to axis - dom.line = document.createElement('DIV'); - dom.line.className = 'line'; + // 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 on axis - dom.dot = document.createElement('DIV'); - dom.dot.className = 'dot'; + util.forEach(this.items, function (item) { + item.dirty = true; + if (item.displayed) item.redraw(); + }); - // attach this item as attribute - dom.box['timeline-item'] = this; + restack = true; } - // 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); + // reposition visible items vertically + if (this.itemSet.options.stack) { // TODO: ugly way to access options... + stack.stack(this.visibleItems, margin, restack); } - 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); + 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.box.className = 'item box' + className; - dom.line.className = 'item line' + 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; - // 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; + // apply new height + this.dom.background.style.height = height + 'px'; + this.dom.foreground.style.height = height + 'px'; + this.dom.label.style.height = height + 'px'; - this.dirty = false; + // 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(); } - this._repaintDeleteButton(dom.box); + return resized; }; /** - * Show the item in the DOM (when not already displayed). The items DOM will - * be created when needed. + * Show this group: attach to the DOM */ -ItemBox.prototype.show = function() { - if (!this.displayed) { - this.redraw(); +Group.prototype.show = function() { + if (!this.dom.label.parentNode) { + this.itemSet.dom.labelSet.appendChild(this.dom.label); + } + + if (!this.dom.foreground.parentNode) { + this.itemSet.dom.foreground.appendChild(this.dom.foreground); + } + + if (!this.dom.background.parentNode) { + this.itemSet.dom.background.appendChild(this.dom.background); + } + + if (!this.dom.axis.parentNode) { + this.itemSet.dom.axis.appendChild(this.dom.axis); } }; /** - * Hide the item from the DOM (when visible) + * Hide this group: remove from the DOM */ -ItemBox.prototype.hide = function() { - if (this.displayed) { - var dom = this.dom; +Group.prototype.hide = function() { + var label = this.dom.label; + if (label.parentNode) { + label.parentNode.removeChild(label); + } - 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 = this.dom.foreground; + if (foreground.parentNode) { + foreground.parentNode.removeChild(foreground); + } - this.top = null; - this.left = null; + var background = this.dom.background; + if (background.parentNode) { + background.parentNode.removeChild(background); + } - this.displayed = false; + var axis = this.dom.axis; + if (axis.parentNode) { + axis.parentNode.removeChild(axis); } }; /** - * Reposition the item horizontally - * @Override + * Add an item to the group + * @param {Item} item */ -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; +Group.prototype.add = function(item) { + this.items[item.id] = item; + item.setParent(this); - // calculate left position of the box - if (align == 'right') { - this.left = start - this.width; - } - else if (align == 'left') { - this.left = start; - } - else { - // default or 'center' - this.left = start - this.width / 2; + 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 box - box.style.left = this.left + 'px'; +/** + * Remove an item from the group + * @param {Item} item + */ +Group.prototype.remove = function(item) { + delete this.items[item.id]; + item.setParent(this.itemSet); - // reposition line - line.style.left = (start - this.props.line.width / 2) + 'px'; + // remove from visible items + var index = this.visibleItems.indexOf(item); + if (index != -1) this.visibleItems.splice(index, 1); + + // TODO: also remove from ordered items? +}; + +/** + * Remove an item from the corresponding DataSet + * @param {Item} item + */ +Group.prototype.removeFromDataSet = function(item) { + this.itemSet.removeItem(item.id); +}; + +/** + * Reorder the items + */ +Group.prototype.order = function() { + var array = util.toArray(this.items); + this.orderedItems.byStart = array; + this.orderedItems.byEnd = this._constructByEndArray(array); - // reposition dot - dot.style.left = (start - this.props.dot.width / 2) + 'px'; + stack.orderByStart(this.orderedItems.byStart); + stack.orderByEnd(this.orderedItems.byEnd); }; /** - * Reposition the item vertically - * @Override + * Create an array containing all items being a range (having an end date) + * @param {Item[]} array + * @returns {ItemRange[]} + * @private */ -ItemBox.prototype.repositionY = function() { - var orientation = this.options.orientation, - box = this.dom.box, - line = this.dom.line, - dot = this.dom.dot; +Group.prototype._constructByEndArray = function(array) { + var endArray = []; - if (orientation == 'top') { - box.style.top = (this.top || 0) + 'px'; + for (var i = 0; i < array.length; i++) { + if (array[i] instanceof ItemRange) { + endArray.push(array[i]); + } + } + return endArray; +}; - line.style.top = '0'; - line.style.height = (this.parent.top + this.top + 1) + 'px'; - line.style.bottom = ''; +/** + * 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 + */ +Group.prototype._updateVisibleItems = function(orderedItems, visibleItems, range) { + var initialPosByStart, + newVisibleItems = [], + i; + + // 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); + } } - 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'; + // 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'); + } + else { + initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]); } - dot.style.top = (-this.props.dot.height / 2) + 'px'; -}; + // use visible search to find a visible ItemRange (only based on endTime) + var initialPosByEnd = util.binarySearch(orderedItems.byEnd, range, 'data','end'); -/** - * @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 - */ -function ItemPoint (data, conversion, options) { - this.props = { - dot: { - top: 0, - width: 0, - height: 0 - }, - content: { - height: 0, - marginLeft: 0 + // 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;} + } + } - // validate data - if (data) { - if (data.start == undefined) { - throw new Error('Property "start" missing in item ' + data); + // 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;} } } - Item.call(this, data, conversion, options); -} + return newVisibleItems; +}; + -ItemPoint.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 + * 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 */ -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); +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 { + return true; + } }; /** - * Repaint the item + * 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 */ -ItemPoint.prototype.redraw = function() { - var dom = this.dom; - if (!dom) { - // create DOM - this.dom = {}; - dom = this.dom; +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 { + if (item.displayed) item.hide(); + } +}; - // background box - dom.point = document.createElement('div'); - // className is updated in redraw() +/** + * 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 + */ +function Timeline (container, items, options) { + if (!(this instanceof Timeline)) { + throw new SyntaxError('Constructor must be called with the new operator'); + } - // contents box, right from the dot - dom.content = document.createElement('div'); - dom.content.className = 'content'; - dom.point.appendChild(dom.content); + var me = this; + this.defaultOptions = { + start: null, + end: null, - // dot at start - dom.dot = document.createElement('div'); - dom.point.appendChild(dom.dot); + autoResize: true, - // attach this item as attribute - dom.point['timeline-item'] = this; - } + orientation: 'bottom', + width: null, + height: null, + maxHeight: null, + minHeight: null + }; + this.options = util.deepExtend({}, this.defaultOptions); - // 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; + // Create the DOM, props, and emitter + this._create(container); - // 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); + // 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) + }, + 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) } + }; - this.dirty = true; - } + // range + this.range = new Range(this.body); + this.components.push(this.range); + this.body.range = this.range; - // 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; + // time axis + this.timeAxis = new TimeAxis(this.body); + this.components.push(this.timeAxis); + this.body.util.snap = this.timeAxis.snap.bind(this.timeAxis); - this.dirty = true; - } + // current time bar + this.currentTime = new CurrentTime(this.body); + this.components.push(this.currentTime); - // 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; + // 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); - // resize contents - dom.content.style.marginLeft = 2 * this.props.dot.width + 'px'; - //dom.content.style.marginRight = ... + 'px'; // TODO: margin right + // item set + this.itemSet = new ItemSet(this.body); + this.components.push(this.itemSet); - dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px'; - dom.dot.style.left = (this.props.dot.width / 2) + 'px'; + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet - this.dirty = false; + // apply options + if (options) { + this.setOptions(options); } - this._repaintDeleteButton(dom.point); -}; - -/** - * Show the item in the DOM (when not already visible). The items DOM will - * be created when needed. - */ -ItemPoint.prototype.show = function() { - if (!this.displayed) { + // create itemset + if (items) { + this.setItems(items); + } + else { this.redraw(); } -}; +} + +// turn Timeline into an event emitter +Emitter(Timeline.prototype); /** - * Hide the item from the DOM (when visible) + * 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 */ -ItemPoint.prototype.hide = function() { - if (this.displayed) { - if (this.dom.point.parentNode) { - this.dom.point.parentNode.removeChild(this.dom.point); - } +Timeline.prototype._create = function (container) { + this.dom = {}; - this.top = null; - this.left = null; + 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.displayed = false; - } -}; + 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'; -/** - * Reposition the item horizontally - * @Override - */ -ItemPoint.prototype.repositionX = function() { - var start = this.conversion.toScreen(this.data.start); + 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.left = start - this.props.dot.width; + this.dom.centerContainer.appendChild(this.dom.center); + this.dom.leftContainer.appendChild(this.dom.left); + this.dom.rightContainer.appendChild(this.dom.right); - // reposition point - this.dom.point.style.left = this.left + 'px'; + 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 = {}; + + 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; + }); + + // 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 + + // attach the root panel to the provided container + if (!container) throw new Error('No container provided'); + container.appendChild(this.dom.root); }; /** - * Reposition the item vertically - * @Override + * Destroy the Timeline, clean up all DOM elements and event listeners. */ -ItemPoint.prototype.repositionY = function() { - var orientation = this.options.orientation, - point = this.dom.point; +Timeline.prototype.destroy = function () { + // unbind datasets + this.clear(); - if (orientation == 'top') { - point.style.top = this.top + 'px'; + // remove all event listeners + this.off(); + + // stop checking for changed size + this._stopAutoResize(); + + // remove from DOM + if (this.dom.root.parentNode) { + this.dom.root.parentNode.removeChild(this.dom.root); } - else { - point.style.top = (this.parent.height - this.top - this.height) + 'px'; + this.dom = null; + + // 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; + + // give all components the opportunity to cleanup + this.components.forEach(function (component) { + component.destroy(); + }); + + this.body = null; }; /** - * @constructor ItemRange - * @extends Item - * @param {Object} data Object containing parameters start, end - * content, className. - * @param {{toScreen: function, toTime: function}} conversion - * Conversion functions from time to screen and vice versa - * @param {Object} [options] Configuration options - * // TODO: describe options - */ -function ItemRange (data, conversion, options) { - this.props = { - content: { - width: 0 - } - }; + * 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 + */ +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); - // 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); - } + // enable/disable autoResize + this._initAutoResize(); } - Item.call(this, data, conversion, options); -} + // propagate options to all components + this.components.forEach(function (component) { + component.setOptions(options); + }); -ItemRange.prototype = new Item (null, null, null); + // 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.'); + } -ItemRange.prototype.baseClassName = 'item range'; + // redraw everything + this.redraw(); +}; /** - * 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 + * Set a custom time bar + * @param {Date} time */ -ItemRange.prototype.isVisible = function(range) { - // determine visibility - return (this.data.start < range.end) && (this.data.end > range.start); +Timeline.prototype.setCustomTime = function (time) { + if (!this.customTime) { + throw new Error('Cannot get custom time: Custom time bar is not enabled'); + } + + this.customTime.setCustomTime(time); }; /** - * Repaint the item + * Retrieve the current custom time. + * @return {Date} customTime */ -ItemRange.prototype.redraw = function() { - var dom = this.dom; - if (!dom) { - // create DOM - this.dom = {}; - dom = this.dom; +Timeline.prototype.getCustomTime = function() { + if (!this.customTime) { + throw new Error('Cannot get custom time: Custom time bar is not enabled'); + } - // background box - dom.box = document.createElement('div'); - // className is updated in redraw() + return this.customTime.getCustomTime(); +}; - // contents box - dom.content = document.createElement('div'); - dom.content.className = 'content'; - dom.box.appendChild(dom.content); +/** + * Set items + * @param {vis.DataSet | Array | google.visualization.DataTable | null} items + */ +Timeline.prototype.setItems = function(items) { + var initialLoad = (this.itemsData == null); - // attach this item as attribute - dom.box['timeline-item'] = this; + // convert to type DataSet when needed + var newDataSet; + if (!items) { + newDataSet = null; } - - // append DOM to parent DOM - if (!this.parent) { - throw new Error('Cannot redraw item: no parent attached'); + else if (items instanceof DataSet || items instanceof DataView) { + newDataSet = items; } - 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); + else { + // turn an array into a dataset + newDataSet = new DataSet(items, { + type: { + start: 'Date', + end: 'Date' + } + }); } - 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); - } + // set items + this.itemsData = newDataSet; + this.itemSet && this.itemSet.setItems(newDataSet); - this.dirty = true; - } + if (initialLoad && ('start' in this.options || 'end' in this.options)) { + this.fit(); - // 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; + 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.dirty = true; + this.setWindow(start, end); } +}; - // recalculate size - if (this.dirty) { - this.props.content.width = this.dom.content.offsetWidth; - this.height = this.dom.box.offsetHeight; - - this.dirty = false; +/** + * 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 { + // turn an array into a dataset + newDataSet = new DataSet(groups); } - this._repaintDeleteButton(dom.box); - this._repaintDragLeft(); - this._repaintDragRight(); + this.groupsData = newDataSet; + this.itemSet.setGroups(newDataSet); }; /** - * Show the item in the DOM (when not already visible). The items DOM will - * be created when needed. + * 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} */ -ItemRange.prototype.show = function() { - if (!this.displayed) { - this.redraw(); +Timeline.prototype.clear = function(what) { + // clear items + if (!what || what.items) { + this.setItems(null); + } + + // clear groups + if (!what || what.groups) { + this.setGroups(null); + } + + // clear options of timeline and of each of the components + if (!what || what.options) { + this.components.forEach(function (component) { + component.setOptions(component.defaultOptions); + }); + + this.setOptions(this.defaultOptions); // this will also do a redraw } }; /** - * Hide the item from the DOM (when visible) - * @return {Boolean} changed + * Set Timeline window such that it fits all items */ -ItemRange.prototype.hide = function() { - if (this.displayed) { - var box = this.dom.box; +Timeline.prototype.fit = function() { + // apply the data range as range + var dataRange = this.getItemRange(); - if (box.parentNode) { - box.parentNode.removeChild(box); + // 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); + } - this.top = null; - this.left = null; - - this.displayed = false; + // skip range set if there is no start and end date + if (start === null && end === null) { + return; } + + this.range.setRange(start, end); }; /** - * Reposition the item horizontally - * @Override + * 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 */ -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; +Timeline.prototype.getItemRange = function() { + // calculate min from start filed + var dataset = this.itemsData.getDataSet(), + min = null, + max = null; - // 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; - } + 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 - // 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; + // 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()); + } + } } - 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'; + return { + min: (min != null) ? new Date(min) : null, + max: (max != null) ? new Date(max) : null + }; }; /** - * Reposition the item vertically - * @Override + * 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. */ -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'; - } +Timeline.prototype.setSelection = function(ids) { + this.itemSet && this.itemSet.setSelection(ids); }; /** - * Repaint a drag area on the left side of the range when the range is selected - * @protected + * Get the selected items by their id + * @return {Array} ids The ids of the selected items */ -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; - } +Timeline.prototype.getSelection = function() { + return this.itemSet && this.itemSet.getSelection() || []; }; /** - * Repaint a drag area on the right side of the range when the range is selected - * @protected + * 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 */ -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; +Timeline.prototype.setWindow = function(start, end) { + if (arguments.length == 1) { + var range = arguments[0]; + this.range.setRange(range.start, range.end); } - 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; + else { + this.range.setRange(start, end); } }; /** - * @constructor ItemRangeOverflow - * @extends ItemRange - * @param {Object} data Object containing parameters start, end - * content, className. - * @param {{toScreen: function, toTime: function}} conversion - * Conversion functions from time to screen and vice versa - * @param {Object} [options] Configuration options - * // TODO: describe options + * Get the visible window + * @return {{start: Date, end: Date}} Visible range */ -function ItemRangeOverflow (data, conversion, options) { - this.props = { - content: { - left: 0, - width: 0 - } +Timeline.prototype.getWindow = function() { + var range = this.range.getRange(); + return { + start: new Date(range.start), + end: new Date(range.end) }; - - ItemRange.call(this, data, conversion, options); -} - -ItemRangeOverflow.prototype = new ItemRange (null, null, null); - -ItemRangeOverflow.prototype.baseClassName = 'item rangeoverflow'; +}; /** - * Reposition the item horizontally - * @Override + * Force a redraw of the Timeline. Can be useful to manually redraw when + * option autoResize=false */ -ItemRangeOverflow.prototype.repositionX = function() { - var parentWidth = this.parent.width, - start = this.conversion.toScreen(this.data.start), - end = this.conversion.toScreen(this.data.end), - contentLeft; +Timeline.prototype.redraw = function() { + var resized = false, + options = this.options, + props = this.props, + dom = this.dom; - // 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; - } + if (!dom) return; // when destroyed - // when range exceeds left of the window, position the contents at the left of the visible area - contentLeft = Math.max(-start, 0); + // update class names + dom.root.className = 'vis timeline root ' + options.orientation; - 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 + // 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, ''); - this.dom.box.style.left = this.left + 'px'; - this.dom.box.style.width = boxWidth + 'px'; - this.dom.content.style.left = contentLeft + 'px'; -}; + // 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; -/** - * @constructor Group - * @param {Number | String} groupId - * @param {Object} data - * @param {ItemSet} itemSet - */ -function Group (groupId, data, itemSet) { - this.groupId = groupId; + // 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; - this.itemSet = itemSet; + // TODO: compensate borders when any of the panels is empty. - this.dom = {}; - this.props = { - label: { - width: 0, - height: 0 - } - }; - this.className = null; + // 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'); - 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: [] - }; + // 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; - this._create(); + // 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; - this.setData(data); -} + // 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'; -/** - * Create DOM elements for the group - * @private - */ -Group.prototype._create = function() { - var label = document.createElement('div'); - label.className = 'vlabel'; - this.dom.label = label; + 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'; - var inner = document.createElement('div'); - inner.className = 'inner'; - label.appendChild(inner); - this.dom.inner = inner; + // 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(); - var foreground = document.createElement('div'); - foreground.className = 'group'; - foreground['timeline-group'] = this; - this.dom.foreground = foreground; + // 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'; - this.dom.background = document.createElement('div'); - this.dom.background.className = 'group'; + // 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; - this.dom.axis = document.createElement('div'); - this.dom.axis.className = 'group'; + // redraw all components + this.components.forEach(function (component) { + resized = component.redraw() || resized; + }); + if (resized) { + // keep repainting until all sizes are settled + this.redraw(); + } +}; - // 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); +// TODO: deprecated since version 1.1.0, remove some day +Timeline.prototype.repaint = function () { + throw new Error('Function repaint is deprecated. Use redraw instead.'); }; /** - * Set the group data for this group - * @param {Object} data Group data, can contain properties content and className + * 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.setData = function(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; - } +// 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); +}; - if (!this.dom.inner.firstChild) { - util.addClassName(this.dom.inner, 'hidden'); - } - else { - util.removeClassName(this.dom.inner, 'hidden'); - } - // 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); - } +/** + * 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 + */ +// 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); }; /** - * Get the width of the group label - * @return {number} width + * 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.getLabelWidth = function() { - return this.props.label.width; +// 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; }; /** - * 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 + * 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.redraw = function(range, margin, restack) { - var resized = false; - - this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range); - - // 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; - - util.forEach(this.items, function (item) { - item.dirty = true; - if (item.displayed) item.redraw(); - }); - - restack = true; - } - - // 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); - } - - // 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; - } - else { - height = margin.axis + margin.item; - } - 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'; - - // 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(); - } - - return resized; +// 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; }; + /** - * Show this group: attach to the DOM + * Initialize watching when option autoResize is true + * @private */ -Group.prototype.show = function() { - if (!this.dom.label.parentNode) { - this.itemSet.dom.labelSet.appendChild(this.dom.label); - } - - if (!this.dom.foreground.parentNode) { - this.itemSet.dom.foreground.appendChild(this.dom.foreground); - } - - if (!this.dom.background.parentNode) { - this.itemSet.dom.background.appendChild(this.dom.background); +Timeline.prototype._initAutoResize = function () { + if (this.options.autoResize == true) { + this._startAutoResize(); } - - if (!this.dom.axis.parentNode) { - this.itemSet.dom.axis.appendChild(this.dom.axis); + else { + this._stopAutoResize(); } }; /** - * Hide this group: remove from the DOM + * Watch for changes in the size of the container. On resize, the Panel will + * automatically redraw itself. + * @private */ -Group.prototype.hide = function() { - var label = this.dom.label; - if (label.parentNode) { - label.parentNode.removeChild(label); - } +Timeline.prototype._startAutoResize = function () { + var me = this; - var foreground = this.dom.foreground; - if (foreground.parentNode) { - foreground.parentNode.removeChild(foreground); - } + this._stopAutoResize(); - var background = this.dom.background; - if (background.parentNode) { - background.parentNode.removeChild(background); - } + this._onResize = function() { + if (me.options.autoResize != true) { + // stop watching when the option autoResize is changed to false + me._stopAutoResize(); + return; + } - var axis = this.dom.axis; - if (axis.parentNode) { - axis.parentNode.removeChild(axis); - } -}; + 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; -/** - * Add an item to the group - * @param {Item} item - */ -Group.prototype.add = function(item) { - this.items[item.id] = item; - item.setParent(this); + me.emit('change'); + } + } + }; - 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); - } + // add event listener to window resize + util.addEventListener(window, 'resize', this._onResize); + + this.watchTimer = setInterval(this._onResize, 1000); }; /** - * Remove an item from the group - * @param {Item} item + * Stop watching for a resize of the frame. + * @private */ -Group.prototype.remove = function(item) { - delete this.items[item.id]; - item.setParent(this.itemSet); - - // remove from visible items - var index = this.visibleItems.indexOf(item); - if (index != -1) this.visibleItems.splice(index, 1); +Timeline.prototype._stopAutoResize = function () { + if (this.watchTimer) { + clearInterval(this.watchTimer); + this.watchTimer = undefined; + } - // TODO: also remove from ordered items? + // remove event listener on window.resize + util.removeEventListener(window, 'resize', this._onResize); + this._onResize = null; }; /** - * Remove an item from the corresponding DataSet - * @param {Item} item + * Start moving the timeline vertically + * @param {Event} event + * @private */ -Group.prototype.removeFromDataSet = function(item) { - this.itemSet.removeItem(item.id); +Timeline.prototype._onTouch = function (event) { + this.touch.allowDragging = true; }; /** - * Reorder the items + * Start moving the timeline vertically + * @param {Event} event + * @private */ -Group.prototype.order = function() { - 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); +Timeline.prototype._onPinch = function (event) { + this.touch.allowDragging = false; }; /** - * Create an array containing all items being a range (having an end date) - * @param {Item[]} array - * @returns {ItemRange[]} + * Start moving the timeline vertically + * @param {Event} event * @private */ -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; +Timeline.prototype._onDragStart = function (event) { + this.touch.initialScrollTop = this.props.scrollTop; }; /** - * 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. + * Move the timeline vertically + * @param {Event} event * @private */ -Group.prototype._updateVisibleItems = function(orderedItems, visibleItems, range) { - var initialPosByStart, - newVisibleItems = [], - i; - - // 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); - } - else { - initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]); - } +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; - // use visible search to find a visible ItemRange (only based on endTime) - var initialPosByEnd = this._binarySearch(orderedItems, range, true); + var delta = event.gesture.deltaY; - // 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;} - } - } + var oldScrollTop = this._getScrollTop(); + var newScrollTop = this._setScrollTop(this.touch.initialScrollTop + delta); - // 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;} - } + if (newScrollTop != oldScrollTop) { + this.redraw(); // TODO: this causes two redraws when dragging, the other is triggered by rangechange already } - - 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} + * Apply a scrollTop + * @param {Number} scrollTop + * @returns {Number} scrollTop Returns the applied scrollTop * @private */ -Group.prototype._binarySearch = function(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; - - var found = false; - var low = 0; - var high = array.length; - var guess = Math.floor(0.5*(high+low)); - var newGuess; - - 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; - } - } - 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; - } - } - } - } - return guess; +Timeline.prototype._setScrollTop = function (scrollTop) { + this.props.scrollTop = scrollTop; + this._updateScrollTop(); + return this.props.scrollTop; }; /** - * 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} + * Update the current scrollTop when the height of the containers has been changed + * @returns {Number} scrollTop Returns the applied scrollTop * @private */ -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); +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); } - return false; - } - else { - return true; + 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; }; /** - * 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 + * Get the current scrollTop + * @returns {number} scrollTop * @private */ -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 { - if (item.displayed) item.hide(); - } +Timeline.prototype._getScrollTop = function () { + return this.props.scrollTop; }; /** * 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. + * @param {Object} [options] See Graph2d.setOptions for the available options. * @constructor */ -function Timeline (container, items, options) { +function Graph2d (container, items, options, groups) { var me = this; this.defaultOptions = { start: null, @@ -7104,7 +10403,9 @@ function Timeline (container, items, options) { util: { snap: null, // will be specified after TimeAxis is created toScreen: me._toScreen.bind(me), - toTime: me._toTime.bind(me) + toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width + toTime: me._toTime.bind(me), + toGlobalTime : me._toGlobalTime.bind(me) } }; @@ -7127,9 +10428,9 @@ function Timeline (container, items, options) { this.customTime = new CustomTime(this.body); this.components.push(this.customTime); - // item set - this.itemSet = new ItemSet(this.body); - this.components.push(this.itemSet); + // item set + this.linegraph = new LineGraph(this.body); + this.components.push(this.linegraph); this.itemsData = null; // DataSet this.groupsData = null; // DataSet @@ -7139,6 +10440,11 @@ 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); @@ -7148,26 +10454,27 @@ function Timeline (container, items, options) { } } -// turn Timeline into an event emitter -Emitter(Timeline.prototype); +// turn Graph2d into an event emitter +Emitter(Graph2d.prototype); /** - * Create the main DOM for the Timeline: a root panel containing left, right, + * 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 Timeline will + * @param {Element} container The container element where the Graph2d will * be attached. * @private */ -Timeline.prototype._create = function (container) { +Graph2d.prototype._create = function (container) { this.dom = {}; 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.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'); @@ -7182,6 +10489,7 @@ Timeline.prototype._create = function (container) { 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'; @@ -7200,13 +10508,14 @@ Timeline.prototype._create = function (container) { 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.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); @@ -7272,9 +10581,9 @@ Timeline.prototype._create = function (container) { }; /** - * Destroy the Timeline, clean up all DOM elements and event listeners. + * Destroy the Graph2d, clean up all DOM elements and event listeners. */ -Timeline.prototype.destroy = function () { +Graph2d.prototype.destroy = function () { // unbind datasets this.clear(); @@ -7308,31 +10617,31 @@ Timeline.prototype.destroy = function () { }; /** - * Set options. Options will be passed to all components loaded in the Timeline. + * Set options. Options will be passed to all components loaded in the Graph2d. * @param {Object} [options] * {String} orientation - * Vertical orientation for the Timeline, + * 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 Timeline, a number in pixels or + * Fixed height for the Graph2d, a number in pixels or * a css string like '400px' or '75%'. If undefined, - * The Timeline will automatically size such that + * The Graph2d will automatically size such that * its contents fit. * {String | Number} minHeight - * Minimum height for the Timeline, a number in pixels or + * Minimum height for the Graph2d, 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 + * 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 */ -Timeline.prototype.setOptions = function (options) { +Graph2d.prototype.setOptions = function (options) { if (options) { // copy the known options var fields = ['width', 'height', 'minHeight', 'maxHeight', 'autoResize', 'start', 'end', 'orientation']; @@ -7360,7 +10669,7 @@ Timeline.prototype.setOptions = function (options) { * 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'); } @@ -7372,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'); } @@ -7384,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 @@ -7407,7 +10716,7 @@ Timeline.prototype.setItems = function(items) { // set items this.itemsData = newDataSet; - this.itemSet && this.itemSet.setItems(newDataSet); + this.linegraph && this.linegraph.setItems(newDataSet); if (initialLoad && ('start' in this.options || 'end' in this.options)) { this.fit(); @@ -7423,7 +10732,7 @@ Timeline.prototype.setItems = function(items) { * Set groups * @param {vis.DataSet | Array | google.visualization.DataTable} groups */ -Timeline.prototype.setGroups = function(groups) { +Graph2d.prototype.setGroups = function(groups) { // convert to type DataSet when needed var newDataSet; if (!groups) { @@ -7438,11 +10747,11 @@ Timeline.prototype.setGroups = function(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 @@ -7451,7 +10760,7 @@ Timeline.prototype.setGroups = function(groups) { * @param {Object} [what] Optionally specify what to clear. By default: * {items: true, groups: true, options: true} */ -Timeline.prototype.clear = function(what) { +Graph2d.prototype.clear = function(what) { // clear items if (!what || what.items) { this.setItems(null); @@ -7473,9 +10782,9 @@ Timeline.prototype.clear = function(what) { }; /** - * Set Timeline window such that it fits all items + * Set Graph2d window such that it fits all items */ -Timeline.prototype.fit = function() { +Graph2d.prototype.fit = function() { // apply the data range as range var dataRange = this.getItemRange(); @@ -7506,11 +10815,11 @@ Timeline.prototype.fit = function() { * When no minimum is found, min==null * When no maximum is found, max==null */ -Timeline.prototype.getItemRange = function() { +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' @@ -7541,25 +10850,6 @@ Timeline.prototype.getItemRange = function() { }; }; -/** - * 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(ids) { - this.itemSet && this.itemSet.setSelection(ids); -}; - -/** - * 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: @@ -7573,7 +10863,7 @@ Timeline.prototype.getSelection = function() { * @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) { +Graph2d.prototype.setWindow = function(start, end) { if (arguments.length == 1) { var range = arguments[0]; this.range.setRange(range.start, range.end); @@ -7587,7 +10877,7 @@ Timeline.prototype.setWindow = function(start, end) { * Get the visible window * @return {{start: Date, end: Date}} Visible range */ -Timeline.prototype.getWindow = function() { +Graph2d.prototype.getWindow = function() { var range = this.range.getRange(); return { start: new Date(range.start), @@ -7596,14 +10886,14 @@ Timeline.prototype.getWindow = function() { }; /** - * 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() { +Graph2d.prototype.redraw = function() { var resized = false, - options = this.options, - props = this.props, - dom = this.dom; + options = this.options, + props = this.props, + dom = this.dom; if (!dom) return; // when destroyed @@ -7637,14 +10927,14 @@ Timeline.prototype.redraw = function() { // 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; + 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; + borderRootHeight; props.centerContainer.height = containerHeight; props.leftContainer.height = containerHeight; props.rightContainer.height = props.leftContainer.height; @@ -7665,13 +10955,14 @@ Timeline.prototype.redraw = function() { // 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.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'; @@ -7682,8 +10973,8 @@ Timeline.prototype.redraw = function() { 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.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'; @@ -7696,16 +10987,19 @@ Timeline.prototype.redraw = function() { 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 + // 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, 0); + 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'; @@ -7726,16 +11020,11 @@ Timeline.prototype.redraw = function() { resized = component.redraw() || resized; }); if (resized) { - // keep repainting until all sizes are settled + // keep redrawing until all sizes are settled this.redraw(); } }; -// TODO: deprecated since version 1.1.0, remove some day -Timeline.prototype.repaint = function () { - throw new Error('Function repaint is deprecated. Use redraw instead.'); -}; - /** * Convert a position on screen (pixels) to a datetime * @param {int} x Position on the screen in pixels @@ -7743,11 +11032,25 @@ Timeline.prototype.repaint = function () { * @private */ // TODO: move this function to Range -Timeline.prototype._toTime = function(x) { +Graph2d.prototype._toTime = function(x) { var conversion = this.range.conversion(this.props.center.width); return new Date(x / conversion.scale + conversion.offset); }; +/** + * 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); +}; + /** * Convert a datetime (Date object) into a position on the screen * @param {Date} time A date @@ -7756,16 +11059,31 @@ Timeline.prototype._toTime = function(x) { * @private */ // TODO: move this function to Range -Timeline.prototype._toScreen = function(time) { +Graph2d.prototype._toScreen = function(time) { var conversion = this.range.conversion(this.props.center.width); return (time.valueOf() - conversion.offset) * conversion.scale; }; + +/** + * 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; +}; + /** * Initialize watching when option autoResize is true * @private */ -Timeline.prototype._initAutoResize = function () { +Graph2d.prototype._initAutoResize = function () { if (this.options.autoResize == true) { this._startAutoResize(); } @@ -7779,7 +11097,7 @@ Timeline.prototype._initAutoResize = function () { * automatically redraw itself. * @private */ -Timeline.prototype._startAutoResize = function () { +Graph2d.prototype._startAutoResize = function () { var me = this; this._stopAutoResize(); @@ -7794,7 +11112,7 @@ Timeline.prototype._startAutoResize = function () { 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.dom.root.clientHeight != me.props.lastHeight)) { me.props.lastWidth = me.dom.root.clientWidth; me.props.lastHeight = me.dom.root.clientHeight; @@ -7813,7 +11131,7 @@ Timeline.prototype._startAutoResize = function () { * Stop watching for a resize of the frame. * @private */ -Timeline.prototype._stopAutoResize = function () { +Graph2d.prototype._stopAutoResize = function () { if (this.watchTimer) { clearInterval(this.watchTimer); this.watchTimer = undefined; @@ -7829,7 +11147,7 @@ Timeline.prototype._stopAutoResize = function () { * @param {Event} event * @private */ -Timeline.prototype._onTouch = function (event) { +Graph2d.prototype._onTouch = function (event) { this.touch.allowDragging = true; }; @@ -7838,7 +11156,7 @@ Timeline.prototype._onTouch = function (event) { * @param {Event} event * @private */ -Timeline.prototype._onPinch = function (event) { +Graph2d.prototype._onPinch = function (event) { this.touch.allowDragging = false; }; @@ -7847,7 +11165,7 @@ Timeline.prototype._onPinch = function (event) { * @param {Event} event * @private */ -Timeline.prototype._onDragStart = function (event) { +Graph2d.prototype._onDragStart = function (event) { this.touch.initialScrollTop = this.props.scrollTop; }; @@ -7856,7 +11174,7 @@ Timeline.prototype._onDragStart = function (event) { * @param {Event} event * @private */ -Timeline.prototype._onDrag = function (event) { +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; @@ -7877,7 +11195,7 @@ Timeline.prototype._onDrag = function (event) { * @returns {Number} scrollTop Returns the applied scrollTop * @private */ -Timeline.prototype._setScrollTop = function (scrollTop) { +Graph2d.prototype._setScrollTop = function (scrollTop) { this.props.scrollTop = scrollTop; this._updateScrollTop(); return this.props.scrollTop; @@ -7888,7 +11206,7 @@ Timeline.prototype._setScrollTop = function (scrollTop) { * @returns {Number} scrollTop Returns the applied scrollTop * @private */ -Timeline.prototype._updateScrollTop = function () { +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) { @@ -7912,7 +11230,7 @@ Timeline.prototype._updateScrollTop = function () { * @returns {number} scrollTop * @private */ -Timeline.prototype._getScrollTop = function () { +Graph2d.prototype._getScrollTop = function () { return this.props.scrollTop; }; @@ -8747,7 +12065,7 @@ Timeline.prototype._getScrollTop = function () { })(typeof util !== 'undefined' ? util : exports); /** - * Canvas shapes used by the Graph + * Canvas shapes used by Network */ if (typeof CanvasRenderingContext2D !== 'undefined') { @@ -8989,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 @@ -9057,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; @@ -9499,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; @@ -9549,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; @@ -9598,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; @@ -9648,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; @@ -9698,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; @@ -9775,14 +13093,14 @@ 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; @@ -9791,7 +13109,7 @@ Node.prototype._drawShape = function (ctx, shape) { 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); } }; @@ -9819,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); @@ -9866,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; @@ -9888,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 @@ -9896,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; }; @@ -9949,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; @@ -9971,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; @@ -10040,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; @@ -10065,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;} } } @@ -10074,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; @@ -10090,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) { @@ -10251,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; } } }; @@ -10731,12 +14059,12 @@ Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is /** - * 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; }; @@ -10896,6 +14224,7 @@ Edge.prototype.getControlNodePositions = function(ctx) { 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. @@ -10922,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 @@ -11255,7 +14584,12 @@ var physicsMixin = { this._calculateSpringForcesWithSupport(); } else { - this._calculateSpringForces(); + if (this.constants.physics.hierarchicalRepulsion.enabled == true) { + this._calculateHierarchicalSpringForces(); + } + else { + this._calculateSpringForces(); + } } }, @@ -11335,6 +14669,8 @@ var physicsMixin = { }, + + /** * this function calculates the effects of the springs in the case of unsmooth curves. * @@ -11381,6 +14717,8 @@ var physicsMixin = { }, + + /** * This function calculates the springforces on the nodes, accounting for the support nodes. * @@ -11457,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'); @@ -11878,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 @@ -11886,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; + } + } } } } @@ -12172,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; @@ -12728,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"; @@ -12778,49 +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'] +""; + "
" + + "" + + ""+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("graph-manipulate-editEdge"); + 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); @@ -12828,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); } }, @@ -12851,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. @@ -12887,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. @@ -12928,14 +16335,14 @@ var manipulationMixin = { this.edgeBeingEdited._enableControlNodes(); this.manipulationDiv.innerHTML = "" + - "" + - "" + this.constants.labels['back'] + " " + - "
" + - "" + - "" + this.constants.labels['editEdgeDescription'] + ""; + "" + + "" + this.constants.labels['back'] + " " + + "
" + + "" + + "" + this.constants.labels['editEdgeDescription'] + ""; // bind the icon - var backButton = document.getElementById("graph-manipulate-back"); + var backButton = document.getElementById("network-manipulate-back"); backButton.onclick = this._createManipulatorBar.bind(this); // temporarily overload functions @@ -13268,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 @@ -13278,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 @@ -13630,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 */ @@ -13669,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 */ @@ -13698,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 */ @@ -13736,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 */ @@ -13821,7 +17228,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 @@ -13829,7 +17236,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() { @@ -14314,7 +17721,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 @@ -15366,10 +18773,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); @@ -15378,7 +18788,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); } } @@ -15583,10 +18993,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 @@ -15619,7 +19086,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); } @@ -15642,7 +19109,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"; @@ -15650,8 +19117,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); } @@ -15822,10 +19289,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 @@ -15833,14 +19300,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 @@ -15848,7 +19315,7 @@ var graphMixinLoaders = { _clearMixin: function (sourceVariable) { for (var mixinFunction in sourceVariable) { if (sourceVariable.hasOwnProperty(mixinFunction)) { - Graph.prototype[mixinFunction] = undefined; + Network.prototype[mixinFunction] = undefined; } } }, @@ -15933,8 +19400,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"; } @@ -15946,8 +19413,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"; } @@ -15959,8 +19426,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); } @@ -16018,17 +19485,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(); @@ -16044,13 +19514,14 @@ 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,editEdge:null,connect:null,del:null}; + // set constant values this.constants = { nodes: { @@ -16087,6 +19558,7 @@ function Graph (container, data, options) { widthMin: 1, widthMax: 15, width: 1, + widthSelectionMultiplier: 2, hoverWidth: 1.5, style: 'line', color: { @@ -16125,8 +19597,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 @@ -16207,7 +19679,7 @@ function Graph (container, data, options) { background: '#FFFFC6' } }, - dragGraph: true, + dragNetwork: true, dragNodes: true, zoomable: true, hover: false @@ -16216,11 +19688,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 @@ -16233,13 +19705,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 @@ -16273,30 +19745,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(); } }; @@ -16325,8 +19797,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 @@ -16335,7 +19807,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 @@ -16353,10 +19825,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)) { @@ -16379,18 +19851,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; @@ -16408,7 +19880,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; } @@ -16460,7 +19932,7 @@ Graph.prototype.zoomExtent = function(initialZoom, disableStart) { this._setScale(zoomLevel); - this._centerGraph(range); + this._centerNetwork(range); if (disableStart == false) { this.moving = true; this.start(); @@ -16472,7 +19944,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)) { @@ -16492,7 +19964,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; } @@ -16538,7 +20010,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 @@ -16550,11 +20022,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)) { @@ -16781,24 +20258,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); @@ -16840,7 +20317,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; @@ -16881,7 +20358,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) @@ -16893,7 +20370,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(); @@ -16905,7 +20382,7 @@ Graph.prototype._onTouch = function (event) { * handle drag start event * @private */ -Graph.prototype._onDragStart = function () { +Network.prototype._onDragStart = function () { this._handleDragStart(); }; @@ -16916,7 +20393,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 @@ -16962,7 +20439,7 @@ Graph.prototype._handleDragStart = function() { * handle drag event * @private */ -Graph.prototype._onDrag = function (event) { +Network.prototype._onDrag = function (event) { this._handleOnDrag(event) }; @@ -16973,7 +20450,7 @@ Graph.prototype._onDrag = function (event) { * * @private */ -Graph.prototype._handleOnDrag = function(event) { +Network.prototype._handleOnDrag = function(event) { if (this.drag.pinched) { return; } @@ -17008,8 +20485,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; @@ -17027,7 +20504,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) { @@ -17043,7 +20520,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); @@ -17055,7 +20532,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); }; @@ -17065,7 +20542,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); @@ -17076,7 +20553,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); }; @@ -17086,7 +20563,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; @@ -17100,13 +20577,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) { @@ -17149,7 +20626,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. */ @@ -17191,7 +20668,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); @@ -17249,14 +20726,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), @@ -17326,7 +20803,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) { @@ -17337,13 +20814,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; @@ -17367,11 +20844,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) { @@ -17417,7 +20894,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]; @@ -17449,7 +20926,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++) { @@ -17481,7 +20958,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]; @@ -17504,7 +20981,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) { @@ -17551,7 +21028,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; @@ -17582,7 +21059,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++) { @@ -17617,7 +21094,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]; @@ -17644,7 +21121,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; @@ -17672,7 +21149,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 @@ -17699,19 +21176,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; @@ -17745,12 +21222,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, @@ -17769,11 +21246,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 @@ -17781,20 +21258,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; }; @@ -17805,7 +21282,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; }; @@ -17816,7 +21293,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; }; @@ -17827,7 +21304,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; }; @@ -17838,7 +21315,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 ; }; @@ -17849,7 +21326,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)}; } @@ -17859,7 +21336,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)}; } @@ -17870,7 +21347,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; } @@ -17907,7 +21384,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)) { @@ -17926,7 +21403,7 @@ Graph.prototype._drawEdges = function(ctx) { * @param {CanvasRenderingContext2D} ctx * @private */ -Graph.prototype._drawControlNodes = function(ctx) { +Network.prototype._drawControlNodes = function(ctx) { var edges = this.edges; for (var id in edges) { if (edges.hasOwnProperty(id)) { @@ -17939,7 +21416,7 @@ Graph.prototype._drawControlNodes = function(ctx) { * Find a stable position for all nodes * @private */ -Graph.prototype._stabilize = function() { +Network.prototype._stabilize = function() { if (this.constants.freezeForStabilization == true) { this._freezeDefinedNodes(); } @@ -17963,7 +21440,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)) { @@ -17982,7 +21459,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)) { @@ -18001,7 +21478,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)) { @@ -18018,7 +21495,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; @@ -18057,7 +21534,7 @@ Graph.prototype._discreteStepNodes = function() { * * @private */ -Graph.prototype._physicsTick = function() { +Network.prototype._physicsTick = function() { if (!this.freezeSimulation) { if (this.moving) { this._doInAllActiveSectors("_initializeForceCalculation"); @@ -18077,7 +21554,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 @@ -18091,13 +21568,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(); @@ -18112,7 +21587,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(); @@ -18142,11 +21617,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); @@ -18164,7 +21639,7 @@ Graph.prototype._handleNavigation = function() { /** * Freeze the _animationStep */ -Graph.prototype.toggleFreeze = function() { +Network.prototype.toggleFreeze = function() { if (this.freezeSimulation == false) { this.freezeSimulation = true; } @@ -18181,7 +21656,7 @@ Graph.prototype.toggleFreeze = function() { * @param {boolean} [disableStart] * @private */ -Graph.prototype._configureSmoothCurves = function(disableStart) { +Network.prototype._configureSmoothCurves = function(disableStart) { if (disableStart === undefined) { disableStart = true; } @@ -18213,7 +21688,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)) { @@ -18242,10 +21717,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]; } } }; @@ -18253,14 +21728,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}); } } @@ -18275,7 +21750,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(); @@ -18311,16 +21786,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'; @@ -18393,7 +21872,7 @@ function Graph3d(container, data, options) { } } -// Extend Graph with an Emitter mixin +// Extend Graph3d with an Emitter mixin Emitter(Graph3d.prototype); /** @@ -21620,38 +25099,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, - 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 }; diff --git a/dist/vis.min.css b/dist/vis.min.css index e08fd306..519c4d10 100644 --- a/dist/vis.min.css +++ b/dist/vis.min.css @@ -1 +1 @@ -.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,.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: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}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 transparent;cursor:default}span.graph-manipulationUI.none:active{box-shadow:1px 1px 8px transparent}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 7ade499b..d1a39d5c 100644 --- a/dist/vis.min.js +++ b/dist/vis.min.js @@ -4,8 +4,8 @@ * * A dynamic, browser-based visualization library. * - * @version 2.0.0 - * @date 2014-06-19 + * @version 3.0.0 + * @date 2014-07-07 * * @license * Copyright (C) 2011-2014 Almende B.V, http://almende.com @@ -22,14 +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(r,a){if(!i[r]){if(!e[r]){var h="function"==typeof require&&require;if(!a&&h)return h(r,!0);if(o)return o(r,!0);throw new Error("Cannot find module '"+r+"'")}var d=i[r]={exports:{}};e[r][0].call(d.exports,function(t){var i=e[r][1][t];return n(i?i:t)},d,d.exports,t,e,i,s)}return i[r].exports}for(var o="function"==typeof require&&require,r=0;re?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),r=o.length>>>0;if("function"!=typeof t)throw new TypeError(t+" is not a function");for(e&&(i=e),s=new Array(r),n=0;r>n;){var a,h;n in o&&(a=o[n],h=t.call(i,a,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 r=e[o];t.call(n,r,o,e)&&s.push(r)}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 r in n)t.call(n,r)&&o.push(r);if(e)for(var a=0;s>a;a++)t.call(n,i[a])&&o.push(i[a]);return o}}()),Array.isArray||(Array.isArray=function(t){return"[object Array]"===Object.prototype.toString.call(t)}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,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)&&(t[n]=s[n])}return t},util.selectiveExtend=function(t,e){if(!Array.isArray(t))throw new Error("Array with property names expected as first argument");for(var i=1,s=arguments.length;s>i;i++)for(var n=arguments[i],o=0,r=t.length;r>o;o++){var a=t[o];n.hasOwnProperty(a)&&(e[a]=n[a])}return e},util.deepExtend=function(t,e){if(Array.isArray(e))throw new TypeError("Arrays are not supported by deepExtend");for(var i in e)if(e.hasOwnProperty(i))if(e[i]&&e[i].constructor===Object)void 0===t[i]&&(t[i]={}),t[i].constructor===Object?util.deepExtend(t[i],e[i]):t[i]=e[i];else{if(Array.isArray(e[i]))throw new TypeError("Arrays are not supported by deepExtend");t[i]=e[i]}return t},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('Unknown 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(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),r=util.HSVToHex(s.h,s.s,s.v);e={background:t,border:o,highlight:{background:r,border:o},hover:{background:r,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)),r=util.GiveDec(t.substring(5,6)),a=16*e+i,h=16*s+n,i=16*o+r;return{r:a,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)),r=util.GiveHex(e%16),a=util.GiveHex(Math.floor(i/16)),h=util.GiveHex(i%16),d=s+n+o+r+a+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,r=t==s?3:i==s?1:5,a=60*(r-o/(n-s))/360,h=(n-s)/n,d=n;return{h:a,s:h,v:d}},util.HSVToRGB=function(t,e,i){var s,n,o,r=Math.floor(6*t),a=6*t-r,h=i*(1-e),d=i*(1-a*e),l=i*(1-(1-a)*e);switch(r%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 a=this._getColumnNames(t),h=0,d=t.getNumberOfRows();d>h;h++){for(var l={},c=0,u=a.length;u>c;c++){var p=a[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,r=function(t){var e=t[o];n._data[e]?(e=n._updateItem(t),s.push(e)):(e=n._addItem(t),i.push(e))};if(Array.isArray(t))for(var a=0,h=t.length;h>a;a++)r(t[a]);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)}r(u)}else{if(!(t instanceof Object))throw new Error("Unknown dataType");r(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=util.getType(arguments[0]);"String"==o||"Number"==o?(t=arguments[0],i=arguments[1],s=arguments[2]):"Array"==o?(e=arguments[0],i=arguments[1],s=arguments[2]):(i=arguments[0],s=arguments[1]);var r;if(i&&i.returnType){if(r="DataTable"==i.returnType?"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";var a,h,d,l,c=i&&i.type||this._options.type,u=i&&i.filter,p=[];if(void 0!=t)a=n._getItem(t,c),u&&!u(a)&&(a=null);else if(void 0!=e)for(d=0,l=e.length;l>d;d++)a=n._getItem(e[d],c),(!u||u(a))&&p.push(a);else for(h in this._data)this._data.hasOwnProperty(h)&&(a=n._getItem(h,c),(!u||u(a))&&p.push(a));if(i&&i.order&&void 0==t&&this._sort(p,i.order),i&&i.fields){var m=i.fields;if(void 0!=t)a=this._filterFields(a,m);else for(d=0,l=p.length;l>d;d++)p[d]=this._filterFields(p[d],m)}if("DataTable"==r){var g=this._getColumnNames(s);if(void 0!=t)n._appendRow(s,g,a);else for(d=0,l=p.length;l>d;d++)n._appendRow(s,g,p[d]);return s}if(void 0!=t)return a;if(s){for(d=0,l=p.length;l>d;d++)s.push(p[d]);return s}return p},DataSet.prototype.getIds=function(t){var e,i,s,n,o,r=this._data,a=t&&t.filter,h=t&&t.order,d=t&&t.type||this._options.type,l=[];if(a)if(h){o=[];for(s in r)r.hasOwnProperty(s)&&(n=this._getItem(s,d),a(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 r)r.hasOwnProperty(s)&&(n=this._getItem(s,d),a(n)&&l.push(n[this._fieldId]));else if(h){o=[];for(s in r)r.hasOwnProperty(s)&&o.push(r[s]);for(this._sort(o,h),e=0,i=o.length;i>e;e++)l[e]=o[e][this._fieldId]}else for(s in r)r.hasOwnProperty(s)&&(n=r[s],l.push(n[this._fieldId]));return l},DataSet.prototype.forEach=function(t,e){var i,s,n=e&&e.filter,o=e&&e.type||this._options.type,r=this._data;if(e&&e.order)for(var a=this.get(e),h=0,d=a.length;d>h;h++)i=a[h],s=i[this._fieldId],t(i,s);else for(s in r)r.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.type||this._options.type,o=[],r=this._data;for(var a in r)r.hasOwnProperty(a)&&(i=this._getItem(a,n),(!s||s(i))&&o.push(t(i,a)));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(Array.isArray(t))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],t}else if(t instanceof Object){var e=t[this._fieldId];if(e&&this._data[e])return delete this._data[e],e}return null},DataSet.prototype.clear=function(t){var e=Object.keys(this._data);return this._data={},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],r=o[t];null!=r&&(!i||r>s)&&(i=o,s=r)}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],r=o[t];null!=r&&(!i||s>r)&&(i=o,s=r)}return i},DataSet.prototype.distinct=function(t){var e,i=this._data,s=[],n=this._options.type&&this._options.type[t]||null,o=0;for(var r in i)if(i.hasOwnProperty(r)){var a=i[r],h=a[t],d=!1;for(e=0;o>e;e++)if(s[e]==h){d=!0;break}d||void 0===h||(s[o]=h,o++)}if(n)for(e=0;ei;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 r=e[n];t.setValue(s,n,i[r])}},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 r=[];return void 0!=t&&r.push(t),r.push(o),r.push(i),this._data&&this._data.get.apply(this._data,r)},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,r,a=e&&e.items,h=this._data,d=[],l=[],c=[];if(a&&h){switch(t){case"add":for(s=0,n=a.length;n>s;s++)o=a[s],r=this.get(o),r&&(this._ids[o]=!0,d.push(o));break;case"update":for(s=0,n=a.length;n>s;s++)o=a[s],r=this.get(o),r?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=a.length;n>s;s++)o=a[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 r=null,a=0,h=t.length;h>a;a++){var d=t[a];if(null!==d.top&&d!==o&&stack.collision(o,d,e.item)){r=d;break}}null!=r&&(o.top=r.top+r.height+e.item)}while(r)}}},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,r=1e3,a=1;1e3*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1e3),500*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=500),100*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=100),50*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=50),10*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=10),5*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=5),e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1),3*i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=3),i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=1),5*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*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=15),10*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=10),5*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=5),r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=1),200*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=200),100*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=100),50*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=50),10*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=10),5*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=5),a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=1)}},TimeStep.prototype.snap=function(t){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""}},Range.prototype=new Component,Range.prototype.setOptions=function(t){if(t){var e=["direction","min","max","zoomMin","zoomMax","moveable","zoomable"];util.selectiveExtend(e,this.options,t),("start"in t||"end"in t)&&this.setRange(t.start,t.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.body.emitter.emit("rangechange",s),this.body.emitter.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,r=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!==r&&r>s&&(i=r-s,s+=i,n+=i,null!=o&&n>o&&(n=o)),null!==o&&n>o&&(i=n-o,s-=i,n-=i,null!=r&&r>s&&(s=r)),null!==this.options.zoomMin){var a=parseFloat(this.options.zoomMin);0>a&&(a=0),a>n-s&&(this.end-this.start===a?(s=this.start,n=this.end):(i=a-(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}},Range.prototype._onDragStart=function(){this.options.moveable&&this.props.touch.allowDragging&&(this.props.touch.start=this.start,this.props.touch.end=this.end,this.body.dom.root&&(this.body.dom.root.style.cursor="move"))},Range.prototype._onDrag=function(t){if(this.options.moveable){var e=this.options.direction;if(validateDirection(e),this.props.touch.allowDragging){var i="horizontal"==e?t.gesture.deltaX:t.gesture.deltaY,s=this.props.touch.end-this.props.touch.start,n="horizontal"==e?this.body.domProps.center.width:this.body.domProps.center.height,o=-i/n*s;this._applyRange(this.props.touch.start+o,this.props.touch.end+o),this.body.emitter.emit("rangechange",{start:new Date(this.start),end:new Date(this.end)})}}},Range.prototype._onDragEnd=function(){this.options.moveable&&this.props.touch.allowDragging&&(this.body.dom.root&&(this.body.dom.root.style.cursor="auto"),this.body.emitter.emit("rangechanged",{start:new Date(this.start),end:new Date(this.end)}))},Range.prototype._onMouseWheel=function(t){if(this.options.zoomable&&this.options.moveable){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.body.dom.center),o=this._pointerToDate(n);this.zoom(i,o)}t.preventDefault()}},Range.prototype._onTouch=function(){this.props.touch.start=this.start,this.props.touch.end=this.end,this.props.touch.allowDragging=!0,this.props.touch.center=null},Range.prototype._onHold=function(){this.props.touch.allowDragging=!1},Range.prototype._onPinch=function(t){if(this.options.zoomable&&this.options.moveable&&(this.props.touch.allowDragging=!1,t.gesture.touches.length>1)){this.props.touch.center||(this.props.touch.center=getPointer(t.gesture.center,this.body.dom.center));var e=1/t.gesture.scale,i=this._pointerToDate(this.props.touch.center),s=parseInt(i+(this.props.touch.start-i)*e),n=parseInt(i+(this.props.touch.end-i)*e);this.setRange(s,n)}},Range.prototype._pointerToDate=function(t){var e,i=this.options.direction;if(validateDirection(i),"horizontal"==i){var s=this.body.domProps.center.width;return e=this.conversion(s),t.x/e.scale+e.offset}var n=this.body.domProps.center.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)},Component.prototype.setOptions=function(t){t&&util.extend(this.options,t)},Component.prototype.redraw=function(){return!1},Component.prototype.destroy=function(){},Component.prototype._isResized=function(){var t=this.props._previousWidth!==this.props.width||this.props._previousHeight!==this.props.height;return this.props._previousWidth=this.props.width,this.props._previousHeight=this.props.height,t},TimeAxis.prototype=new Component,TimeAxis.prototype.setOptions=function(t){t&&util.selectiveExtend(["orientation","showMinorLabels","showMajorLabels"],this.options,t)},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"},TimeAxis.prototype.destroy=function(){this.dom.foreground.parentNode&&this.dom.foreground.parentNode.removeChild(this.dom.foreground),this.dom.background.parentNode&&this.dom.background.parentNode.removeChild(this.dom.background),this.body=null},TimeAxis.prototype.redraw=function(){var t=this.options,e=this.props,i=this.dom.foreground,s=this.dom.background,n="top"==t.orientation?this.body.dom.top:this.body.dom.bottom,o=i.parentNode!==n;this._calculateCharSize();var r=(this.options.orientation,this.options.showMinorLabels),a=this.options.showMajorLabels;e.minorLabelHeight=r?e.minorCharHeight:0,e.majorLabelHeight=a?e.majorCharHeight:0,e.height=e.minorLabelHeight+e.majorLabelHeight,e.width=i.offsetWidth,e.minorLineHeight=this.body.domProps.root.height-e.majorLabelHeight-("top"==t.orientation?this.body.domProps.bottom.height:this.body.domProps.top.height),e.minorLineWidth=1,e.majorLineHeight=e.minorLineHeight+e.majorLabelHeight,e.majorLineWidth=1;var h=i.nextSibling,d=s.nextSibling;return i.parentNode&&i.parentNode.removeChild(i),s.parentNode&&s.parentNode.removeChild(s),i.style.height=this.props.height+"px",this._repaintLabels(),h?n.insertBefore(i,h):n.appendChild(i),d?this.body.dom.backgroundVertical.insertBefore(s,d):this.body.dom.backgroundVertical.appendChild(s),this._isResized()||o},TimeAxis.prototype._repaintLabels=function(){var t=this.options.orientation,e=util.convert(this.body.range.start,"Number"),i=util.convert(this.body.range.end,"Number"),s=this.body.util.toTime(7*(this.props.minorCharWidth||10)).valueOf()-this.body.util.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 r=void 0,a=0;n.hasNext()&&1e3>a;){a++;var h=n.getCurrent(),d=this.body.util.toScreen(h),l=n.isMajor();this.options.showMinorLabels&&this._repaintMinorText(d,n.getLabelMinor(),t),l&&this.options.showMajorLabels?(d>0&&(void 0==r&&(r=d),this._repaintMajorText(d,n.getLabelMajor(),t)),this._repaintMajorLine(d,t)):this._repaintMinorLine(d,t),n.next()}if(this.options.showMajorLabels){var c=this.body.util.toTime(0),u=n.getLabelMajor(c),p=u.length*(this.props.majorCharWidth||10)+10;(void 0==r||r>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.dom.foreground.appendChild(s)}this.dom.minorTexts.push(s),s.childNodes[0].nodeValue=e,s.style.top="top"==i?this.props.majorLabelHeight+"px":"0",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.dom.foreground.appendChild(s) -}this.dom.majorTexts.push(s),s.childNodes[0].nodeValue=e,s.style.top="top"==i?"0":this.props.minorLabelHeight+"px",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.dom.background.appendChild(i)),this.dom.minorLines.push(i);var s=this.props;i.style.top="top"==e?s.majorLabelHeight+"px":this.body.domProps.top.height+"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.dom.background.appendChild(i)),this.dom.majorLines.push(i);var s=this.props;i.style.top="top"==e?"0":this.body.domProps.top.height+"px",i.style.left=t-s.majorLineWidth/2+"px",i.style.height=s.majorLineHeight+"px"},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.dom.foreground.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.dom.foreground.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._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.destroy=function(){this.options.showCurrentTime=!1,this.redraw(),this.body=null},CurrentTime.prototype.setOptions=function(t){t&&util.selectiveExtend(["showCurrentTime"],this.options,t)},CurrentTime.prototype.redraw=function(){if(this.options.showCurrentTime){var t=this.body.dom.backgroundVertical;this.bar.parentNode!=t&&(this.bar.parentNode&&this.bar.parentNode.removeChild(this.bar),t.appendChild(this.bar),this.start());var e=new Date,i=this.body.util.toScreen(e);this.bar.style.left=i+"px",this.bar.title="Current time: "+e}else this.bar.parentNode&&this.bar.parentNode.removeChild(this.bar),this.stop();return!1},CurrentTime.prototype.start=function(){function t(){e.stop();var i=e.body.range.conversion(e.body.domProps.center.width).scale,s=1/i/10;30>s&&(s=30),s>1e3&&(s=1e3),e.redraw(),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=function(t){t&&util.selectiveExtend(["showCustomTime"],this.options,t)},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.destroy=function(){this.options.showCustomTime=!1,this.redraw(),this.hammer.enable(!1),this.hammer=null,this.body=null},CustomTime.prototype.redraw=function(){if(this.options.showCustomTime){var t=this.body.dom.backgroundVertical;this.bar.parentNode!=t&&(this.bar.parentNode&&this.bar.parentNode.removeChild(this.bar),t.appendChild(this.bar));var e=this.body.util.toScreen(this.customTime);this.bar.style.left=e+"px",this.bar.title="Time: "+this.customTime}else this.bar.parentNode&&this.bar.parentNode.removeChild(this.bar);return!1},CustomTime.prototype.setCustomTime=function(t){this.customTime=new Date(t.valueOf()),this.redraw()},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.body.util.toScreen(this.eventParams.customTime)+e,s=this.body.util.toTime(i);this.setCustomTime(s),this.body.emitter.emit("timechange",{time:new Date(this.customTime.valueOf())}),t.stopPropagation(),t.preventDefault()}},CustomTime.prototype._onDragEnd=function(t){this.eventParams.dragging&&(this.body.emitter.emit("timechanged",{time:new Date(this.customTime.valueOf())}),t.stopPropagation(),t.preventDefault())};var UNGROUPED="__ungrouped__";ItemSet.prototype=new Component,ItemSet.types={box:ItemBox,range:ItemRange,rangeoverflow:ItemRangeOverflow,point:ItemPoint},ItemSet.prototype._create=function(){var t=document.createElement("div");t.className="itemset",t["timeline-itemset"]=this,this.dom.frame=t;var e=document.createElement("div");e.className="background",t.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;var n=document.createElement("div");n.className="labelset",this.dom.labelSet=n,this._updateUngrouped(),this.hammer=Hammer(this.body.dom.centerContainer,{prevent_default:!0}),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)),this.hammer.on("tap",this._onSelectItem.bind(this)),this.hammer.on("hold",this._onMultiSelectItem.bind(this)),this.hammer.on("doubletap",this._onAddItem.bind(this)),this.show()},ItemSet.prototype.setOptions=function(t){if(t){var e=["type","align","orientation","padding","stack","selectable","groupOrder"];util.selectiveExtend(e,this.options,t),"margin"in t&&("number"==typeof t.margin?(this.options.margin.axis=t.margin,this.options.margin.item=t.margin):"object"==typeof t.margin&&util.selectiveExtend(["axis","item"],this.options.margin,t.margin)),"editable"in t&&("boolean"==typeof t.editable?(this.options.editable.updateTime=t.editable,this.options.editable.updateGroup=t.editable,this.options.editable.add=t.editable,this.options.editable.remove=t.editable):"object"==typeof t.editable&&util.selectiveExtend(["updateTime","updateGroup","add","remove"],this.options.editable,t.editable));var i=function(e){if(e in t){var i=t[e];if(!(i instanceof Function)||2!=i.length)throw new Error("option "+e+" must be a function "+e+"(item, callback)");this.options[e]=i}}.bind(this);["onAdd","onUpdate","onRemove","onMove"].forEach(i),this.markDirty()}},ItemSet.prototype.markDirty=function(){this.groupIds=[],this.stackDirty=!0},ItemSet.prototype.destroy=function(){this.hide(),this.setItems(null),this.setGroups(null),this.hammer=null,this.body=null,this.conversion=null},ItemSet.prototype.hide=function(){this.dom.frame.parentNode&&this.dom.frame.parentNode.removeChild(this.dom.frame),this.dom.axis.parentNode&&this.dom.axis.parentNode.removeChild(this.dom.axis),this.dom.labelSet.parentNode&&this.dom.labelSet.parentNode.removeChild(this.dom.labelSet)},ItemSet.prototype.show=function(){this.dom.frame.parentNode||this.body.dom.center.appendChild(this.dom.frame),this.dom.axis.parentNode||this.body.dom.backgroundVertical.appendChild(this.dom.axis),this.dom.labelSet.parentNode||this.body.dom.left.appendChild(this.dom.labelSet)},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.redraw=function(){var t=this.options.margin,e=this.body.range,i=util.option.asSize,s=this.options,n=s.orientation,o=!1,r=this.dom.frame,a=s.editable.updateTime||s.editable.updateGroup;r.className="itemset"+(a?" editable":""),o=this._orderGroups()||o;var h=e.end-e.start,d=h!=this.lastVisibleInterval||this.props.width!=this.props.lastWidth;d&&(this.stackDirty=!0),this.lastVisibleInterval=h,this.props.lastWidth=this.props.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,s=t.redraw(e,i,l);o=s||o,m+=t.height}),m=Math.max(m,g),this.stackDirty=!1,r.style.height=i(m),this.props.top=r.offsetTop,this.props.left=r.offsetLeft,this.props.width=r.offsetWidth,this.props.height=m,this.dom.axis.style.top=i("top"==n?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",o=this._isResized()||o},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.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.off(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.body.emitter.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;t.forEach(function(t){var i=e.itemsData.get(t,e.itemOptions),s=e.items[t],n=i.type||i.start&&i.end&&"range"||e.options.type||"box",o=ItemSet.types[n];if(s&&(o&&s instanceof o?e._updateItem(s,i):(e._removeItem(s),s=null)),!s){if(!o)throw new TypeError('Unknown item type "'+n+'"');s=new o(i,e.conversion,e.options),s.id=t,e._addItem(s)}}),this._order(),this.stackDirty=!0,this.body.emitter.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.body.emitter.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 r=e.items[o];r.data.group==t&&s.add(r)}s.order(),s.show()}}),this.body.emitter.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.body.emitter.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.redraw(),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;i0||s.length>0)&&this.body.emitter.emit("select",{items:this.getSelection()}),t.stopPropagation()}},ItemSet.prototype._onAddItem=function(t){if(this.options.selectable&&this.options.editable.add){var e=this,i=this.body.util.snap||null,s=ItemSet.itemFromTarget(t);if(s){var n=e.itemsData.get(s.id);this.options.onUpdate(n,function(t){t&&e.itemsData.update(t)})}else{var o=vis.util.getAbsoluteLeft(this.dom.frame),r=t.gesture.center.pageX-o,a=this.body.util.toTime(r),h={start:i?i(a):a,content:"new item"};if("range"===this.options.type||"rangeoverflow"==this.options.type){var d=this.body.util.toTime(r+this.props.width/5);h.end=i?i(d):d}h[this.itemsData.fieldId]=util.randomUUID();var l=ItemSet.groupFromTarget(t);l&&(h.group=l.groupId),this.options.onAdd(h,function(t){t&&e.itemsData.add(h)})}}},ItemSet.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.body.emitter.emit("select",{items:this.getSelection()}),t.stopPropagation()}}},ItemSet.itemFromTarget=function(t){for(var e=t.target;e;){if(e.hasOwnProperty("timeline-item"))return e["timeline-item"];e=e.parentNode}return null},ItemSet.groupFromTarget=function(t){for(var e=t.target;e;){if(e.hasOwnProperty("timeline-group"))return e["timeline-group"];e=e.parentNode}return null},ItemSet.itemSetFromTarget=function(t){for(var e=t.target;e;){if(e.hasOwnProperty("timeline-itemset"))return e["timeline-itemset"];e=e.parentNode}return null},ItemSet.prototype._myDataSet=function(){for(var t=this.itemsData;t instanceof DataView;)t=t.data;return t},Item.prototype.select=function(){this.selected=!0,this.displayed&&this.redraw()},Item.prototype.unselect=function(){this.selected=!1,this.displayed&&this.redraw()},Item.prototype.setParent=function(t){this.displayed?(this.hide(),this.parent=t,this.parent&&this.show()):this.parent=t},Item.prototype.isVisible=function(){return!1},Item.prototype.show=function(){return!1},Item.prototype.hide=function(){return!1},Item.prototype.redraw=function(){},Item.prototype.repositionX=function(){},Item.prototype.repositionY=function(){},Item.prototype._repaintDeleteButton=function(t){if(this.selected&&this.options.editable.remove&&!this.dom.deleteButton){var e=this,i=document.createElement("div");i.className="delete",i.title="Delete this item",Hammer(i,{preventDefault:!0}).on("tap",function(t){e.parent.removeFromDataSet(e),t.stopPropagation()}),t.appendChild(i),this.dom.deleteButton=i}else!this.selected&&this.dom.deleteButton&&(this.dom.deleteButton.parentNode&&this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton),this.dom.deleteButton=null)},ItemBox.prototype=new Item(null,null,null),ItemBox.prototype.isVisible=function(t){var e=(t.end-t.start)/4;return this.data.start>t.start-e&&this.data.startt.start-e&&this.data.startt.start},ItemRange.prototype.redraw=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 redraw item: no parent attached");if(!t.box.parentNode){var e=this.parent.dom.foreground;if(!e)throw new Error("Cannot redraw 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.redraw()},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.conversion.toScreen(this.data.start),n=this.conversion.toScreen(this.data.end),o=this.options.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,e=this.dom.box;e.style.top="top"==t?this.top+"px":this.parent.height-this.top-this.height+"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,null,null),ItemRangeOverflow.prototype.baseClassName="item rangeoverflow",ItemRangeOverflow.prototype.repositionX=function(){var t,e=this.parent.width,i=this.conversion.toScreen(this.data.start),s=this.conversion.toScreen(this.data.end);-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.background.className="group",this.dom.axis=document.createElement("div"),this.dom.axis.className="group",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,this.dom.inner.firstChild?util.removeClassName(this.dom.inner,"hidden"):util.addClassName(this.dom.inner,"hidden");var i=t&&t.className||null;i!=this.className&&(this.className&&(util.removeClassName(this.dom.label,i),util.removeClassName(this.dom.foreground,i),util.removeClassName(this.dom.background,i),util.removeClassName(this.dom.axis,i)),util.addClassName(this.dom.label,i),util.addClassName(this.dom.foreground,i),util.addClassName(this.dom.background,i),util.addClassName(this.dom.axis,i))},Group.prototype.getLabelWidth=function(){return this.props.label.width},Group.prototype.redraw=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.redraw()}),i=!0),this.itemSet.options.stack?stack.stack(this.visibleItems,e,i):stack.nostack(this.visibleItems,e);var o,r=this.visibleItems;if(r.length){var a=r[0].top,h=r[0].top+r[0].height;util.forEach(r,function(t){a=Math.min(a,t.top),h=Math.max(h,t.top+t.height)}),o=h-a+e.axis+e.item}else o=e.axis+e.item;o=Math.max(o,this.props.label.height);var d=this.dom.foreground;this.top=d.offsetTop,this.left=d.offsetLeft,this.width=d.offsetWidth,s=util.updateProperty(this,"height",o)||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,d.style.height=o+"px",this.dom.label.style.height=o+"px";for(var l=0,c=this.visibleItems.length;c>l;l++){var u=this.visibleItems[l];u.repositionY()}return s},Group.prototype.show=function(){this.dom.label.parentNode||this.itemSet.dom.labelSet.appendChild(this.dom.label),this.dom.foreground.parentNode||this.itemSet.dom.foreground.appendChild(this.dom.foreground),this.dom.background.parentNode||this.itemSet.dom.background.appendChild(this.dom.background),this.dom.axis.parentNode||this.itemSet.dom.axis.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.body.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=r+1;ne.start-r&&s[l].data[n]e.start-r&&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?util.convert(s.start,"Date").valueOf():null;var n=t.max("start");n&&(i=util.convert(n.start,"Date").valueOf());var o=t.max("end");o&&(i=null==i?util.convert(o.end,"Date").valueOf():Math.max(i,util.convert(o.end,"Date").valueOf()))}return{min:null!=e?new Date(e):null,max:null!=i?new Date(i):null}},Timeline.prototype.setSelection=function(t){this.itemSet&&this.itemSet.setSelection(t)},Timeline.prototype.getSelection=function(){return this.itemSet&&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(){var t=!1,e=this.options,i=this.props,s=this.dom;if(s){s.root.className="vis timeline root "+e.orientation,s.root.style.maxHeight=util.option.asSize(e.maxHeight,""),s.root.style.minHeight=util.option.asSize(e.minHeight,""),s.root.style.width=util.option.asSize(e.width,""),i.border.left=(s.centerContainer.offsetWidth-s.centerContainer.clientWidth)/2,i.border.right=i.border.left,i.border.top=(s.centerContainer.offsetHeight-s.centerContainer.clientHeight)/2,i.border.bottom=i.border.top;var n=s.root.offsetHeight-s.root.clientHeight,o=s.root.offsetWidth-s.root.clientWidth;i.center.height=s.center.offsetHeight,i.left.height=s.left.offsetHeight,i.right.height=s.right.offsetHeight,i.top.height=s.top.clientHeight||-i.border.top,i.bottom.height=s.bottom.clientHeight||-i.border.bottom;var r=Math.max(i.left.height,i.center.height,i.right.height),a=i.top.height+r+i.bottom.height+n+i.border.top+i.border.bottom;s.root.style.height=util.option.asSize(e.height,a+"px"),i.root.height=s.root.offsetHeight,i.background.height=i.root.height-n;var h=i.root.height-i.top.height-i.bottom.height-n;i.centerContainer.height=h,i.leftContainer.height=h,i.rightContainer.height=i.leftContainer.height,i.root.width=s.root.offsetWidth,i.background.width=i.root.width-o,i.left.width=s.leftContainer.clientWidth||-i.border.left,i.leftContainer.width=i.left.width,i.right.width=s.rightContainer.clientWidth||-i.border.right,i.rightContainer.width=i.right.width;var d=i.root.width-i.left.width-i.right.width-o;i.center.width=d,i.centerContainer.width=d,i.top.width=d,i.bottom.width=d,s.background.style.height=i.background.height+"px",s.backgroundVertical.style.height=i.background.height+"px",s.backgroundHorizontal.style.height=i.centerContainer.height+"px",s.centerContainer.style.height=i.centerContainer.height+"px",s.leftContainer.style.height=i.leftContainer.height+"px",s.rightContainer.style.height=i.rightContainer.height+"px",s.background.style.width=i.background.width+"px",s.backgroundVertical.style.width=i.centerContainer.width+"px",s.backgroundHorizontal.style.width=i.background.width+"px",s.centerContainer.style.width=i.center.width+"px",s.top.style.width=i.top.width+"px",s.bottom.style.width=i.bottom.width+"px",s.background.style.left="0",s.background.style.top="0",s.backgroundVertical.style.left=i.left.width+"px",s.backgroundVertical.style.top="0",s.backgroundHorizontal.style.left="0",s.backgroundHorizontal.style.top=i.top.height+"px",s.centerContainer.style.left=i.left.width+"px",s.centerContainer.style.top=i.top.height+"px",s.leftContainer.style.left="0",s.leftContainer.style.top=i.top.height+"px",s.rightContainer.style.left=i.left.width+i.center.width+"px",s.rightContainer.style.top=i.top.height+"px",s.top.style.left=i.left.width+"px",s.top.style.top="0",s.bottom.style.left=i.left.width+"px",s.bottom.style.top=i.top.height+i.centerContainer.height+"px",this._updateScrollTop();var l=this.props.scrollTop;"bottom"==e.orientation&&(l+=Math.max(this.props.centerContainer.height-this.props.center.height,0)),s.center.style.left="0",s.center.style.top=l+"px",s.left.style.left="0",s.left.style.top=l+"px",s.right.style.left="0",s.right.style.top=l+"px";var c=0==this.props.scrollTop?"hidden":"",u=this.props.scrollTop==this.props.scrollTopMin?"hidden":"";s.shadowTop.style.visibility=c,s.shadowBottom.style.visibility=u,s.shadowTopLeft.style.visibility=c,s.shadowBottomLeft.style.visibility=u,s.shadowTopRight.style.visibility=c,s.shadowBottomRight.style.visibility=u,this.components.forEach(function(e){t=e.redraw()||t}),t&&this.redraw()}},Timeline.prototype.repaint=function(){throw new Error("Function repaint is deprecated. Use redraw instead.")},Timeline.prototype._toTime=function(t){var e=this.range.conversion(this.props.center.width);return new Date(t/e.scale+e.offset)},Timeline.prototype._toScreen=function(t){var e=this.range.conversion(this.props.center.width);return(t.valueOf()-e.offset)*e.scale},Timeline.prototype._initAutoResize=function(){1==this.options.autoResize?this._startAutoResize():this._stopAutoResize()},Timeline.prototype._startAutoResize=function(){var t=this;this._stopAutoResize(),this._onResize=function(){return 1!=t.options.autoResize?void t._stopAutoResize():void(t.dom.root&&(t.dom.root.clientWidth!=t.props.lastWidth||t.dom.root.clientHeight!=t.props.lastHeight)&&(t.props.lastWidth=t.dom.root.clientWidth,t.props.lastHeight=t.dom.root.clientHeight,t.emit("change")))},util.addEventListener(window,"resize",this._onResize),this.watchTimer=setInterval(this._onResize,1e3)},Timeline.prototype._stopAutoResize=function(){this.watchTimer&&(clearInterval(this.watchTimer),this.watchTimer=void 0),util.removeEventListener(window,"resize",this._onResize),this._onResize=null},Timeline.prototype._onTouch=function(){this.touch.allowDragging=!0},Timeline.prototype._onPinch=function(){this.touch.allowDragging=!1},Timeline.prototype._onDragStart=function(){this.touch.initialScrollTop=this.props.scrollTop},Timeline.prototype._onDrag=function(t){if(this.touch.allowDragging){var e=t.gesture.deltaY,i=this._getScrollTop(),s=this._setScrollTop(this.touch.initialScrollTop+e);s!=i&&this.redraw()}},Timeline.prototype._setScrollTop=function(t){return this.props.scrollTop=t,this._updateScrollTop(),this.props.scrollTop},Timeline.prototype._updateScrollTop=function(){var t=Math.min(this.props.centerContainer.height-this.props.center.height,0);return t!=this.props.scrollTopMin&&("bottom"==this.options.orientation&&(this.props.scrollTop+=t-this.props.scrollTopMin),this.props.scrollTopMin=t),this.props.scrollTop>0&&(this.props.scrollTop=0),this.props.scrollTopi;i++)if(e.id===a.nodes[i].id){n=a.nodes[i];break}for(n||(n={id:e.id},t.node&&(n.attr=r(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=r(n.attr,e.attr))}function d(t,e){if(t.edges||(t.edges=[]),t.edges.push(e),t.edge){var i=r({},t.edge);e.attr=r(i,e.attr)}}function l(t,e,i,s,n){var o={from:e,to:i,type:s};return t.edge&&(o.attr=r({},t.edge)),o.attr=r(o.attr||{},n),o}function c(){for(I=T.NULL,N="";" "==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(I=T.DELIMITER);var i=C+n();if(E[i])return I=T.DELIMITER,N=i,s(),void s();if(E[C])return I=T.DELIMITER,N=C,void s();if(o(C)||"-"==C){for(N+=C,s();o(C);)N+=C,s();return"false"==N?N=!1:"true"==N?N=!0:isNaN(Number(N))||(N=Number(N)),void(I=T.IDENTIFIER)}if('"'==C){for(s();""!=C&&('"'!=C||'"'==C&&'"'==n());)N+=C,'"'==C&&s(),s();if('"'!=C)throw b('End of string " expected');return s(),void(I=T.IDENTIFIER)}for(I=T.UNKNOWN;""!=C;)N+=C,s();throw new SyntaxError('Syntax error in part "'+w(N,30)+'"')}function u(){var t={};if(i(),c(),"strict"==N&&(t.strict=!0,c()),("graph"==N||"digraph"==N)&&(t.type=N,c()),I==T.IDENTIFIER&&(t.id=N,c()),"{"!=N)throw b("Angle bracket { expected");if(c(),p(t),"}"!=N)throw b("Angle bracket } expected");if(c(),""!==N)throw b("End of file expected");return c(),delete t.node,delete t.edge,delete t.graph,t}function p(t){for(;""!==N&&"}"!=N;)m(t),";"==N&&c()}function m(t){var e=g(t);if(e)return void y(t,e);var i=f(t);if(!i){if(I!=T.IDENTIFIER)throw b("Identifier expected");var s=N;if(c(),"="==N){if(c(),I!=T.IDENTIFIER)throw b("Identifier expected");t[s]=N,c()}else v(t,s)}}function g(t){var e=null;if("subgraph"==N&&(e={},e.type="subgraph",c(),I==T.IDENTIFIER&&(e.id=N,c())),"{"==N){if(c(),e||(e={}),e.parent=t,e.node=t.node,e.edge=t.edge,e.graph=t.graph,p(e),"}"!=N)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"==N?(c(),t.node=_(),"node"):"edge"==N?(c(),t.edge=_(),"edge"):"graph"==N?(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(;"->"==N||"--"==N;){var i,s=N;c();var n=g(t);if(n)i=n;else{if(I!=T.IDENTIFIER)throw b("Identifier or subgraph expected");i=N,h(t,{id:i}),c()}var o=_(),r=l(t,e,i,s,o);d(t,r),e=i}}function _(){for(var t=null;"["==N;){for(c(),t={};""!==N&&"]"!=N;){if(I!=T.IDENTIFIER)throw b("Attribute name expected");var e=N;if(c(),"="!=N)throw b("Equal sign = expected");if(c(),I!=T.IDENTIFIER)throw b("Attribute value expected");var i=N;a(t,e,i),c(),","==N&&c()}if("]"!=N)throw b("Bracket ] expected");c()}return t}function b(t){return new SyntaxError(t+', got "'+w(N,30)+'" (char '+D+")")}function w(t,e){return t.length<=e?t:t.substr(0,27)+"..."}function x(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 r(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)};r(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)}),x(e,s,function(e,s){var o=l(n,e.id,s.id,t.type,t.attr),r=i(o);n.edges.push(r)}),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="",N="",I=T.NULL,O=/[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,r=Math.sqrt(s*s-n*n);this.moveTo(t,e-(r-o)),this.lineTo(t+n,e+o),this.lineTo(t-n,e+o),this.lineTo(t,e-(r-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,r=Math.sqrt(s*s-n*n);this.moveTo(t,e+(r-o)),this.lineTo(t+n,e-o),this.lineTo(t-n,e-o),this.lineTo(t,e+(r-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,r=s/2*n,a=t+i,h=e+s,d=t+i/2,l=e+s/2;this.beginPath(),this.moveTo(t,l),this.bezierCurveTo(t,l-r,d-o,e,d,e),this.bezierCurveTo(d+o,e,a,l-r,a,l),this.bezierCurveTo(a,l+r,d+o,h,d,h),this.bezierCurveTo(d-o,h,t,l+r,t,l)},CanvasRenderingContext2D.prototype.database=function(t,e,i,s){var n=1/3,o=i,r=s*n,a=.5522848,h=o/2*a,d=r/2*a,l=t+o,c=e+r,u=t+o/2,p=e+r/2,m=e+(s-r/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),r=t-.9*s*Math.cos(i),a=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(r,a),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 r=i-t,a=s-e,h=a/r,d=Math.sqrt(r*r+a*a),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>r&&(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,r=Math.cos(e)*n;return s*n/Math.sqrt(o*o+r*r);case"box":case"image":case"text":default:return this.width?Math.min(Math.abs(this.width/2/Math.cos(e)),Math.abs(this.height/2/Math.sin(e)))+i:0}},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 r=e.split("\n"),a=r.length,h=this.fontSize+4,d=s+(1-a)/2*h,l=0;a>l;l++)t.fillText(r[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,r=this.length/4,a=this.from;a.width||a.resize(t),a.width>a.height?(n=a.x+a.width/2,o=a.y-r):(n=a.x+r,o=a.y-a.height/2),this._circle(t,n,o,r),e=this._pointOnCircle(n,o,r,.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,r=i-n/2,a=s-o/2;t.fillRect(r,a,n,o),t.fillStyle=this.fontColor||"black",t.textAlign="left",t.textBaseline="top",t.fillText(e,r,a)}},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 r,a,h=.25*Math.max(100,this.length),d=this.from;d.width||d.resize(t),d.width>d.height?(r=d.x+.5*d.width,a=d.y-h):(r=d.x+h,a=d.y-.5*d.height),this._circle(t,r,a,h);var i=.2*Math.PI,s=(10+5*this.width)*this.arrowScaleFactor;e=this._pointOnCircle(r,a,h,.5),t.arrow(e.x,e.y,i,s),t.fill(),t.stroke(),this.label&&(e=this._pointOnCircle(r,a,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),r=this.from.distanceToBorder(t,e+Math.PI),a=(o-r)/o,h=a*this.from.x+(1-a)*this.to.x,d=a*this.from.y+(1-a)*this.to.y;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,w=.25*Math.max(100,this.length);b.width||b.resize(t),b.width>b.height?(v=b.x+.5*b.width,y=b.y-w,_={x:v,y:b.y,angle:.9*Math.PI}):(v=b.x+w,y=b.y-.5*b.height,_={x:b.x,y:y,angle:.6*Math.PI}),t.beginPath(),t.arc(v,y,w,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,w,.5),this._label(t,this.label,m.x,m.y))}},Edge.prototype._getDistanceToEdge=function(t,e,i,s,n,o){if(this.from!=this.to){if(1==this.smooth){var r,a,h,d,l,c,u=1e9;for(r=0;10>r;r++)a=.1*r,h=Math.pow(1-a,2)*t+2*a*(1-a)*this.via.x+Math.pow(a,2)*i,d=Math.pow(1-a,2)*e+2*a*(1-a)*this.via.y+Math.pow(a,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)}var h,d,l,c,v=this.length/4,y=this.from;return y.width||y.resize(ctx),y.width>y.height?(h=y.x+y.width/2,d=y.y-v):(h=y.x+v,d=y.y-y.height/2),l=h-n,c=d-o,Math.abs(Math.sqrt(l*l+c*c)-v)},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))},Edge.prototype._drawControlNodes=function(t){if(1==this.controlNodesEnabled){if(null===this.controlNodes.from&&null===this.controlNodes.to){var e="edgeIdFrom:".concat(this.id),i="edgeIdTo:".concat(this.id),s={nodes:{group:"",radius:8},physics:{damping:0},clustering:{maxNodeSizeIncrements:0,nodeScaling:{width:0,height:0,radius:0}}};this.controlNodes.from=new Node({id:e,shape:"dot",color:{background:"#ff4e00",border:"#3c3c3c",highlight:{background:"#07f968"}}},{},{},s),this.controlNodes.to=new Node({id:i,shape:"dot",color:{background:"#ff4e00",border:"#3c3c3c",highlight:{background:"#07f968"}}},{},{},s)}0==this.controlNodes.from.selected&&0==this.controlNodes.to.selected&&(this.controlNodes.positions=this.getControlNodePositions(t),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(t),this.controlNodes.to.draw(t)}else this.controlNodes={from:null,to:null,positions:{}}},Edge.prototype._enableControlNodes=function(){this.controlNodesEnabled=!0},Edge.prototype._disableControlNodes=function(){this.controlNodesEnabled=!1},Edge.prototype._getSelectedControlNode=function(t,e){var i=this.controlNodes.positions,s=Math.sqrt(Math.pow(t-i.from.x,2)+Math.pow(e-i.from.y,2)),n=Math.sqrt(Math.pow(t-i.to.x,2)+Math.pow(e-i.to.y,2));return 15>s?(this.connectedNode=this.from,this.from=this.controlNodes.from,this.controlNodes.from):15>n?(this.connectedNode=this.to,this.to=this.controlNodes.to,this.controlNodes.to):null},Edge.prototype._restoreControlNodes=function(){1==this.controlNodes.from.selected&&(this.from=this.connectedNode,this.connectedNode=null,this.controlNodes.from.unselect()),1==this.controlNodes.to.selected&&(this.to=this.connectedNode,this.connectedNode=null,this.controlNodes.to.unselect())},Edge.prototype.getControlNodePositions=function(t){var e=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x),i=this.to.x-this.from.x,s=this.to.y-this.from.y,n=Math.sqrt(i*i+s*s),o=this.from.distanceToBorder(t,e+Math.PI),r=(n-o)/n,a=r*this.from.x+(1-r)*this.to.x,h=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),i=this.to.x-this.via.x,s=this.to.y-this.via.y,n=Math.sqrt(i*i+s*s));var d,l,c=this.to.distanceToBorder(t,e),u=(n-c)/n;return 1==this.smooth?(d=(1-u)*this.via.x+u*this.to.x,l=(1-u)*this.via.y+u*this.to.y):(d=(1-u)*this.from.x+u*this.to.x,l=(1-u)*this.from.y+u*this.to.y),{from:{x:a,y:h},to:{x:d,y:l}}},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&&(r=n-i-this.padding),rthis.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,r=this.constants.physics.centralGravity,a=0;for(n=0;nSimulation Mode:Barnes HutRepulsionHierarchical
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"),r=document.getElementById("graph_repositionNodes"),a=document.getElementById("graph_generateOptions");o.onclick=graphToggleSmoothCurves.bind(this),r.onclick=graphRepositionNodes.bind(this),a.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,r,a,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,r.fx-=s,r.fy-=n,a.fx+=s,a.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),r=i*o,a=s*o;e.fx+=r,e.fy+=a}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),r=i*o,a=s*o;e.fx+=r,e.fy+=a}}},_formBarnesHutTree:function(t,e){for(var i,s=e.length,n=Number.MAX_VALUE,o=Number.MAX_VALUE,r=-Number.MAX_VALUE,a=-Number.MAX_VALUE,h=0;s>h;h++){var d=t[e[h]].x,l=t[e[h]].y;n>d&&(n=d),d>r&&(r=d),o>l&&(o=l),l>a&&(a=l)}var c=Math.abs(r-n)-Math.abs(a-o);c>0?(o-=.5*c,a+=.5*c):(n+=.5*c,r-=.5*c);var u=1e-5,p=Math.max(u,Math.abs(r-n)),m=.5*p,g=.5*(n+r),f=.5*(o+a),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,r=.5*t.size;switch(e){case"NW":i=t.range.minX,s=t.range.minX+r,n=t.range.minY,o=t.range.minY+r;break;case"NE":i=t.range.minX+r,s=t.range.maxX,n=t.range.minY,o=t.range.minY+r;break;case"SW":i=t.range.minX,s=t.range.minX+r,n=t.range.minY+r,o=t.range.maxY;break;case"SE":i=t.range.minX+r,s=t.range.maxX,n=t.range.minY+r,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,r,a,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&&(r=.5*f>i?1:v*i+m,r*=0==o?1:1+o*this.constants.clustering.forceAmplification,r/=i,s=t*r,n=e*r,a.fx-=s,a.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,r=!0):o.yFixed&&o.level>s&&(o.yFixed=!1,o.y=i[o.level].minPos,r=!0),1==r&&(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),void 0!==this.edgeBeingEdited&&(this.edgeBeingEdited._disableControlNodes(),this.edgeBeingEdited=void 0,this.selectedControlNode=null),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+"":1==this._getSelectedEdgeCount()&&0==this._getSelectedNodeCount()&&(this.manipulationDiv.innerHTML+="
"+this.constants.labels.editEdge+""),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)}else if(1==this._getSelectedEdgeCount()&&0==this._getSelectedNodeCount()){var i=document.getElementById("graph-manipulate-editEdge");i.onclick=this._createEditEdgeToolbar.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()},_createEditEdgeToolbar:function(){this._clearManipulatorBar(),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+"";var t=document.getElementById("graph-manipulate-back");t.onclick=this._createManipulatorBar.bind(this),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,this._redraw()},_selectControlNode:function(t){this.edgeBeingEdited.controlNodes.from.unselect(),this.edgeBeingEdited.controlNodes.to.unselect(),this.selectedControlNode=this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(t.x),this._YconvertDOMtoCanvas(t.y)),null!==this.selectedControlNode&&(this.selectedControlNode.select(),this.freezeSimulation=!0),this._redraw()},_controlNodeDrag:function(t){var e=this._getPointer(t.gesture.center);null!==this.selectedControlNode&&void 0!==this.selectedControlNode&&(this.selectedControlNode.x=this._XconvertDOMtoCanvas(e.x),this.selectedControlNode.y=this._YconvertDOMtoCanvas(e.y)),this._redraw()},_releaseControlNode:function(t){var e=this._getNodeAt(t);null!=e?(1==this.edgeBeingEdited.controlNodes.from.selected&&(this._editEdge(e.id,this.edgeBeingEdited.to.id),this.edgeBeingEdited.controlNodes.from.unselect()),1==this.edgeBeingEdited.controlNodes.to.selected&&(this._editEdge(this.edgeBeingEdited.from.id,e.id),this.edgeBeingEdited.controlNodes.to.unselect())):this.edgeBeingEdited._restoreControlNodes(),this.freezeSimulation=!1,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()}},_editEdge:function(t,e){if(1==this.editMode){var i={id:this.edgeBeingEdited.id,from:t,to:e};if(this.triggerFunctions.editEdge)if(2==this.triggerFunctions.editEdge.length){var s=this;this.triggerFunctions.editEdge(i,function(t){s.edgesData.update(t),s.moving=!0,s.start()})}else alert(this.constants.labels.linkError),this.moving=!0,this.start();else this.edgesData.update(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,r=-1e9;for(var a in this.sectors[e])if(this.sectors[e].hasOwnProperty(a)&&void 0!==this.sectors[e][a].drawingNode){this._switchToSector(a,e),s=1e9,n=-1e9,o=1e9,r=-1e9;for(var h in this.nodes)this.nodes.hasOwnProperty(h)&&(i=this.nodes[h],i.resize(t),o>i.x-.5*i.width&&(o=i.x-.5*i.width),ri.y-.5*i.height&&(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 r=o.from,a=o.to;o.to.mass>o.from.mass&&(r=o.to,a=o.from),1==a.dynamicEdgesLength?this._addToCluster(r,a,!1):1==r.dynamicEdgesLength&&this._addToCluster(a,r,!1)}}},_forceClustersByZoom:function(){for(var t in this.nodes)if(this.nodes.hasOwnProperty(t)){var e=this.nodes[t];if(1==e.dynamicEdgesLength&&0!=e.dynamicEdges.length){var i=e.dynamicEdges[0],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,r,a=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,r=Math.sqrt(n*n+o*o),a>r)){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 r=e-Math.pow(t,2),a=Math.sqrt(r);this.hubThreshold=Math.floor(t+2*a),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},_getSelectedEdge:function(){for(var t in this.selectionObj.edges)if(this.selectionObj.edges.hasOwnProperty(t))return this.selectionObj.edges[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 r=1.1*(Math.abs(s.minX)+Math.abs(s.maxX)),a=1.1*(Math.abs(s.minY)+Math.abs(s.maxY)),h=this.frame.canvas.clientWidth/r,d=this.frame.canvas.clientHeight/a;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.onEditEdge&&(this.triggerFunctions.editEdge=t.onEditEdge),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,r=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)+r))}),this.moving||(this.moving=!0,this.start())}else if(1==this.constants.dragGraph){var a=e.x-this.drag.pointer.x,h=e.y-this.drag.pointer.y;this._setTranslation(this.drag.translation.x+a,this.drag.translation.y+h),this._redraw(),this.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,r=(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,r),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 r=this._getNodeAt(i);null==r&&(r=this._getEdgeAt(i)),null!=r&&this._hoverObject(r);for(var a in this.hoverObj.nodes)this.hoverObj.nodes.hasOwnProperty(a)&&(r instanceof Node&&r.id!=a||r instanceof Edge||null==r)&&(this._blurObject(this.hoverObj.nodes[a]),delete this.hoverObj.nodes[a]);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 r=this.edges;for(e in r)if(r.hasOwnProperty(e)){var a=r[e];if(a.connected&&void 0!==a.getTitle()&&a.isOverlappingWith(i)){this.popupObj=a;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 r=1*t.length,a=2*Math.PI*Math.random();0==o.xFixed&&(o.x=r*Math.cos(a)),0==o.yFixed&&(o.y=r*Math.sin(a))}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],r=e[o],a=i.get(o);r?r.setProperties(a,this.constants):(r=new Node(properties,this.images,this.groups,this.constants),e[o]=r)}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],r=e[o];r&&r.disconnect();var a=i.get(o,{showInternalIds:!0});e[o]=new Edge(a,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],r=i.get(o),a=e[o];a?(a.disconnect(),a.setProperties(r,this.constants),a.connect()):(a=new Edge(r,this,this.constants),this.edges[o]=a)}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),this._doInAllSectors("_drawControlNodes",t),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,r=s.length;r>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._drawControlNodes=function(t){var e=this.edges;for(var i in e)e.hasOwnProperty(i)&&e[i]._drawControlNodes(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(a,o+t),l.lineTo(r,o+t),l.stroke() -}l.strokeStyle=this.colorAxis,l.strokeRect(a,o,i,n)}if(this.style===Graph3d.STYLE.DOTSIZE&&(l.strokeStyle=this.colorAxis,l.fillStyle=this.colorDot,l.beginPath(),l.moveTo(a,o),l.lineTo(r,o),l.lineTo(r-i+e,h),l.lineTo(a,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(_,r,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,r=Math.sin(y)<0?this.yMin:this.yMax;!i.end();)t=this._convert3Dto2D(new Point3d(o,r,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,r,this.zMin)),e=this._convert3Dto2D(new Point3d(o,r,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,r=Math.cos(y)>0?this.yMin-l:this.yMax+l,n=this._convert3Dto2D(new Point3d(o,r,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 w=this.yLabel;w.length>0&&(d=.1/this.scale.x,o=Math.sin(y)>0?this.xMin-d:this.xMax+d,r=(this.yMin+this.yMax)/2,n=this._convert3Dto2D(new Point3d(o,r,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(w,n.x,n.y));var x=this.zLabel;x.length>0&&(h=30,o=Math.cos(y)>0?this.xMin:this.xMax,r=Math.sin(y)<0?this.yMin:this.yMax,a=(this.zMin+this.zMax)/2,n=this._convert3Dto2D(new Point3d(o,r,a)),m.textAlign="right",m.textBaseline="middle",m.fillStyle=this.colorAxis,m.fillText(x,n.x-h,n.y))},Graph3d.prototype._hsv2rgb=function(t,e,i){var s,n,o,r,a,h;switch(r=i*e,a=Math.floor(t/60),h=r*(1-Math.abs(t/60%2-1)),a){case 0:s=r,n=h,o=0;break;case 1:s=h,n=r,o=0;break;case 2:s=0,n=r,o=h;break;case 3:s=0,n=h,o=r;break;case 4:s=h,n=0,o=r;break;case 5:s=r,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,r,a,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+w.x/x/2,1),r=this._hsv2rgb(d,l,c),a=r):(c=1,r=this._hsv2rgb(d,l,c),a=this.colorAxis)):(r="gray",a=this.colorAxis),h=.5,m.lineWidth=h,m.fillStyle=r,m.strokeStyle=a,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,r=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],r=i((n.x-s.x)*(t.y-s.y)-(n.y-s.y)*(t.x-s.x)),a=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!=r&&0!=a&&r!=a||0!=a&&0!=h&&a!=h||0!=r&&0!=h&&r!=h)},Graph3d.prototype._dataPointFromXY=function(t,e){var i,s=100,n=null,o=null,r=null,a=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(a,u)||this._insideTriangle(a,p))return n}}else for(i=0;iv)&&s>v&&(r=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):"
x:"+t.point.x+"
y:"+t.point.y+"
z:"+t.point.z+"
",e.style.left="0",e.style.top="0",this.frame.appendChild(e),this.frame.appendChild(i),this.frame.appendChild(s);var n=e.offsetWidth,o=e.offsetHeight,r=i.offsetHeight,a=s.offsetWidth,h=s.offsetHeight,d=t.screen.x-n/2;d=Math.min(Math.max(d,10),this.frame.clientWidth-10-n),i.style.left=t.screen.x+"px",i.style.top=t.screen.y-r+"px",e.style.left=d+"px",e.style.top=t.screen.y-r-o+"px",s.style.left=t.screen.x-a/2+"px",s.style.top=t.screen.y-h/2+"px"},Graph3d.prototype._hideTooltip=function(){if(this.tooltip){this.tooltip.dataPoint=null;for(var t in this.tooltip.dom)if(this.tooltip.dom.hasOwnProperty(t)){var e=this.tooltip.dom[t];e&&e.parentNode&&e.parentNode.removeChild(e)}}},G3DaddEventListener=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)},G3DremoveEventListener=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)},G3DstopPropagation=function(t){t||(t=window.event),t.stopPropagation?t.stopPropagation():t.cancelBubble=!0},G3DpreventDefault=function(t){t||(t=window.event),t.preventDefault?t.preventDefault():t.returnValue=!1},Point3d.subtract=function(t,e){var i=new Point3d;return i.x=t.x-e.x,i.y=t.y-e.y,i.z=t.z-e.z,i},Point3d.add=function(t,e){var i=new Point3d;return i.x=t.x+e.x,i.y=t.y+e.y,i.z=t.z+e.z,i},Point3d.avg=function(t,e){return new Point3d((t.x+e.x)/2,(t.y+e.y)/2,(t.z+e.z)/2)},Point3d.crossProduct=function(t,e){var i=new Point3d;return i.x=t.y*e.z-t.z*e.y,i.y=t.z*e.x-t.x*e.z,i.z=t.x*e.y-t.y*e.x,i},Point3d.prototype.length=function(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z)},Point2d=function(t,e){this.x=void 0!==t?t:0,this.y=void 0!==e?e:0},Filter.prototype.isLoaded=function(){return this.loaded},Filter.prototype.getLoadedProgress=function(){for(var t=this.values.length,e=0;this.dataPoints[e];)e++;return Math.round(e/t*100)},Filter.prototype.getLabel=function(){return this.graph.filterLabel},Filter.prototype.getColumn=function(){return this.column},Filter.prototype.getSelectedValue=function(){return void 0===this.index?void 0:this.values[this.index]},Filter.prototype.getValues=function(){return this.values},Filter.prototype.getValue=function(t){if(t>=this.values.length)throw"Error: index out of range";return this.values[t]},Filter.prototype._getDataPoints=function(t){if(void 0===t&&(t=this.index),void 0===t)return[];var e;if(this.dataPoints[t])e=this.dataPoints[t];else{var i={};i.column=this.column,i.value=this.values[t];var s=new DataView(this.data,{filter:function(t){return t[i.column]==i.value}}).get();e=this.graph._getDataPoints(s),this.dataPoints[t]=e}return e},Filter.prototype.setOnLoadCallback=function(t){this.onLoadCallback=t},Filter.prototype.selectValue=function(t){if(t>=this.values.length)throw"Error: index out of range";this.index=t,this.value=this.values[t]},Filter.prototype.loadInBackground=function(t){void 0===t&&(t=0);var e=this.graph.frame;if(t=t||(void 0!==e&&(this.prettyStep=e),this._step=this.prettyStep===!0?StepNumber.calculatePrettyStep(t):t)},StepNumber.calculatePrettyStep=function(t){var e=function(t){return Math.log(t)/Math.LN10},i=Math.pow(10,Math.round(e(t))),s=2*Math.pow(10,Math.round(e(t/2))),n=5*Math.pow(10,Math.round(e(t/5))),o=i;return Math.abs(s-t)<=Math.abs(o-t)&&(o=s),Math.abs(n-t)<=Math.abs(o-t)&&(o=n),0>=o&&(o=1),o},StepNumber.prototype.getCurrent=function(){return parseFloat(this._current.toPrecision(this.precision))},StepNumber.prototype.getStep=function(){return this._step},StepNumber.prototype.start=function(){this._current=this._start-this._start%this._step},StepNumber.prototype.next=function(){this._current+=this._step},StepNumber.prototype.end=function(){return this._current>this._end},Slider.prototype.prev=function(){var t=this.getIndex();t>0&&(t--,this.setIndex(t))},Slider.prototype.next=function(){var t=this.getIndex();t0?this.setIndex(0):this.index=void 0},Slider.prototype.setIndex=function(t){if(!(ts&&(s=0),s>this.values.length-1&&(s=this.values.length-1),s},Slider.prototype.indexToLeft=function(t){var e=parseFloat(this.frame.bar.style.width)-this.frame.slide.clientWidth-10,i=t/(this.values.length-1)*e,s=i+3;return s},Slider.prototype._onMouseMove=function(t){var e=t.clientX-this.startClientX,i=this.startSlideX+e,s=this.leftToIndex(i);this.setIndex(s),G3DpreventDefault()},Slider.prototype._onMouseUp=function(){this.frame.style.cursor="auto",G3DremoveEventListener(document,"mousemove",this.onmousemove),G3DremoveEventListener(document,"mouseup",this.onmouseup),G3DpreventDefault()},getAbsoluteLeft=function(t){for(var e=0;null!==t;)e+=t.offsetLeft,e-=t.scrollLeft,t=t.offsetParent;return e},getAbsoluteTop=function(t){for(var e=0;null!==t;)e+=t.offsetTop,e-=t.scrollTop,t=t.offsetParent;return e},getMouseX=function(t){return"clientX"in t?t.clientX:t.targetTouches[0]&&t.targetTouches[0].clientX||0},getMouseY=function(t){return"clientY"in t?t.clientY:t.targetTouches[0]&&t.targetTouches[0].clientY||0};var vis={util:util,moment:moment,DataSet:DataSet,DataView:DataView,Range:Range,stack:stack,TimeStep:TimeStep,components:{items:{Item:Item,ItemBox:ItemBox,ItemPoint:ItemPoint,ItemRange:ItemRange},Component:Component,ItemSet:ItemSet,TimeAxis:TimeAxis},graph:{Node:Node,Edge:Edge,Popup:Popup,Groups:Groups,Images:Images},Timeline:Timeline,Graph:Graph,Graph3d:Graph3d};"undefined"!=typeof exports&&(exports=vis),"undefined"!=typeof module&&"undefined"!=typeof module.exports&&(module.exports=vis),"function"==typeof define&&define(function(){return vis}),"undefined"!=typeof window&&(window.vis=vis)},{"emitter-component":2,hammerjs:3,moment:4,mousetrap:5}],2:[function(t,e){function i(t){return t?s(t):void 0}function s(t){for(var e in i.prototype)t[e]=i.prototype[e];return t}e.exports=i,i.prototype.on=i.prototype.addEventListener=function(t,e){return this._callbacks=this._callbacks||{},(this._callbacks[t]=this._callbacks[t]||[]).push(e),this},i.prototype.once=function(t,e){function i(){s.off(t,i),e.apply(this,arguments)}var s=this;return this._callbacks=this._callbacks||{},i.fn=e,this.on(t,i),this},i.prototype.off=i.prototype.removeListener=i.prototype.removeAllListeners=i.prototype.removeEventListener=function(t,e){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var i=this._callbacks[t];if(!i)return this;if(1==arguments.length)return delete this._callbacks[t],this;for(var s,n=0;ns;++s)i[s].apply(this,e)}return this},i.prototype.listeners=function(t){return this._callbacks=this._callbacks||{},this._callbacks[t]||[]},i.prototype.hasListeners=function(t){return!!this.listeners(t).length}},{}],3:[function(t,e){!function(t,i){"use strict";function s(){if(!n.READY){n.event.determineEventTypes();for(var t in n.gestures)n.gestures.hasOwnProperty(t)&&n.detection.register(n.gestures[t]);n.event.onTouch(n.DOCUMENT,n.EVENT_MOVE,n.detection.detect),n.event.onTouch(n.DOCUMENT,n.EVENT_END,n.detection.detect),n.READY=!0}}var n=function(t,e){return new n.Instance(t,e||{})};n.defaults={stop_browser_behavior:{userSelect:"none",touchAction:"none",touchCallout:"none",contentZooming:"none",userDrag:"none",tapHighlightColor:"rgba(0,0,0,0)"}},n.HAS_POINTEREVENTS=navigator.pointerEnabled||navigator.msPointerEnabled,n.HAS_TOUCHEVENTS="ontouchstart"in t,n.MOBILE_REGEX=/mobile|tablet|ip(ad|hone|od)|android/i,n.NO_MOUSEEVENTS=n.HAS_TOUCHEVENTS&&navigator.userAgent.match(n.MOBILE_REGEX),n.EVENT_TYPES={},n.DIRECTION_DOWN="down",n.DIRECTION_LEFT="left",n.DIRECTION_UP="up",n.DIRECTION_RIGHT="right",n.POINTER_MOUSE="mouse",n.POINTER_TOUCH="touch",n.POINTER_PEN="pen",n.EVENT_START="start",n.EVENT_MOVE="move",n.EVENT_END="end",n.DOCUMENT=document,n.plugins={},n.READY=!1,n.Instance=function(t,e){var i=this;return s(),this.element=t,this.enabled=!0,this.options=n.utils.extend(n.utils.extend({},n.defaults),e||{}),this.options.stop_browser_behavior&&n.utils.stopDefaultBrowserBehavior(this.element,this.options.stop_browser_behavior),n.event.onTouch(t,n.EVENT_START,function(t){i.enabled&&n.detection.startDetect(i,t)}),this},n.Instance.prototype={on:function(t,e){for(var i=t.split(" "),s=0;s0&&e==n.EVENT_END?e=n.EVENT_MOVE:l||(e=n.EVENT_END),l||null===o?o=h:h=o,i.call(n.detection,s.collectEventData(t,e,h)),n.HAS_POINTEREVENTS&&e==n.EVENT_END&&(l=n.PointerEvent.updatePointer(e,h))),l||(o=null,r=!1,a=!1,n.PointerEvent.reset()) -}})},determineEventTypes:function(){var t;t=n.HAS_POINTEREVENTS?n.PointerEvent.getEvents():n.NO_MOUSEEVENTS?["touchstart","touchmove","touchend touchcancel"]:["touchstart mousedown","touchmove mousemove","touchend touchcancel mouseup"],n.EVENT_TYPES[n.EVENT_START]=t[0],n.EVENT_TYPES[n.EVENT_MOVE]=t[1],n.EVENT_TYPES[n.EVENT_END]=t[2]},getTouchList:function(t){return n.HAS_POINTEREVENTS?n.PointerEvent.getTouchList():t.touches?t.touches:[{identifier:1,pageX:t.pageX,pageY:t.pageY,target:t.target}]},collectEventData:function(t,e,i){var s=this.getTouchList(i,e),o=n.POINTER_TOUCH;return(i.type.match(/mouse/)||n.PointerEvent.matchType(n.POINTER_MOUSE,i))&&(o=n.POINTER_MOUSE),{center:n.utils.getCenter(s),timeStamp:(new Date).getTime(),target:i.target,touches:s,eventType:e,pointerType:o,srcEvent:i,preventDefault:function(){this.srcEvent.preventManipulation&&this.srcEvent.preventManipulation(),this.srcEvent.preventDefault&&this.srcEvent.preventDefault()},stopPropagation:function(){this.srcEvent.stopPropagation()},stopDetect:function(){return n.detection.stopDetect()}}}},n.PointerEvent={pointers:{},getTouchList:function(){var t=this,e=[];return Object.keys(t.pointers).sort().forEach(function(i){e.push(t.pointers[i])}),e},updatePointer:function(t,e){return t==n.EVENT_END?this.pointers={}:(e.identifier=e.pointerId,this.pointers[e.pointerId]=e),Object.keys(this.pointers).length},matchType:function(t,e){if(!e.pointerType)return!1;var i={};return i[n.POINTER_MOUSE]=e.pointerType==e.MSPOINTER_TYPE_MOUSE||e.pointerType==n.POINTER_MOUSE,i[n.POINTER_TOUCH]=e.pointerType==e.MSPOINTER_TYPE_TOUCH||e.pointerType==n.POINTER_TOUCH,i[n.POINTER_PEN]=e.pointerType==e.MSPOINTER_TYPE_PEN||e.pointerType==n.POINTER_PEN,i[t]},getEvents:function(){return["pointerdown MSPointerDown","pointermove MSPointerMove","pointerup pointercancel MSPointerUp MSPointerCancel"]},reset:function(){this.pointers={}}},n.utils={extend:function(t,e,s){for(var n in e)t[n]!==i&&s||(t[n]=e[n]);return t},hasParent:function(t,e){for(;t;){if(t==e)return!0;t=t.parentNode}return!1},getCenter:function(t){for(var e=[],i=[],s=0,n=t.length;n>s;s++)e.push(t[s].pageX),i.push(t[s].pageY);return{pageX:(Math.min.apply(Math,e)+Math.max.apply(Math,e))/2,pageY:(Math.min.apply(Math,i)+Math.max.apply(Math,i))/2}},getVelocity:function(t,e,i){return{x:Math.abs(e/t)||0,y:Math.abs(i/t)||0}},getAngle:function(t,e){var i=e.pageY-t.pageY,s=e.pageX-t.pageX;return 180*Math.atan2(i,s)/Math.PI},getDirection:function(t,e){var i=Math.abs(t.pageX-e.pageX),s=Math.abs(t.pageY-e.pageY);return i>=s?t.pageX-e.pageX>0?n.DIRECTION_LEFT:n.DIRECTION_RIGHT:t.pageY-e.pageY>0?n.DIRECTION_UP:n.DIRECTION_DOWN},getDistance:function(t,e){var i=e.pageX-t.pageX,s=e.pageY-t.pageY;return Math.sqrt(i*i+s*s)},getScale:function(t,e){return t.length>=2&&e.length>=2?this.getDistance(e[0],e[1])/this.getDistance(t[0],t[1]):1},getRotation:function(t,e){return t.length>=2&&e.length>=2?this.getAngle(e[1],e[0])-this.getAngle(t[1],t[0]):0},isVertical:function(t){return t==n.DIRECTION_UP||t==n.DIRECTION_DOWN},stopDefaultBrowserBehavior:function(t,e){var i,s=["webkit","khtml","moz","ms","o",""];if(e&&t.style){for(var n=0;ni;i++){var o=this.gestures[i];if(!this.stopped&&e[o.name]!==!1&&o.handler.call(o,t,this.current.inst)===!1){this.stopDetect();break}}return this.current&&(this.current.lastEvent=t),t.eventType==n.EVENT_END&&!t.touches.length-1&&this.stopDetect(),t}},stopDetect:function(){this.previous=n.utils.extend({},this.current),this.current=null,this.stopped=!0},extendEventData:function(t){var e=this.current.startEvent;if(e&&(t.touches.length!=e.touches.length||t.touches===e.touches)){e.touches=[];for(var i=0,s=t.touches.length;s>i;i++)e.touches.push(n.utils.extend({},t.touches[i]))}var o=t.timeStamp-e.timeStamp,r=t.center.pageX-e.center.pageX,a=t.center.pageY-e.center.pageY,h=n.utils.getVelocity(o,r,a);return n.utils.extend(t,{deltaTime:o,deltaX:r,deltaY:a,velocityX:h.x,velocityY:h.y,distance:n.utils.getDistance(e.center,t.center),angle:n.utils.getAngle(e.center,t.center),direction:n.utils.getDirection(e.center,t.center),scale:n.utils.getScale(e.touches,t.touches),rotation:n.utils.getRotation(e.touches,t.touches),startEvent:e}),t},register:function(t){var e=t.defaults||{};return e[t.name]===i&&(e[t.name]=!0),n.utils.extend(n.defaults,e,!0),t.index=t.index||1e3,this.gestures.push(t),this.gestures.sort(function(t,e){return t.indexe.index?1:0}),this.gestures}},n.gestures=n.gestures||{},n.gestures.Hold={name:"hold",index:10,defaults:{hold_timeout:500,hold_threshold:1},timer:null,handler:function(t,e){switch(t.eventType){case n.EVENT_START:clearTimeout(this.timer),n.detection.current.name=this.name,this.timer=setTimeout(function(){"hold"==n.detection.current.name&&e.trigger("hold",t)},e.options.hold_timeout);break;case n.EVENT_MOVE:t.distance>e.options.hold_threshold&&clearTimeout(this.timer);break;case n.EVENT_END:clearTimeout(this.timer)}}},n.gestures.Tap={name:"tap",index:100,defaults:{tap_max_touchtime:250,tap_max_distance:10,tap_always:!0,doubletap_distance:20,doubletap_interval:300},handler:function(t,e){if(t.eventType==n.EVENT_END){var i=n.detection.previous,s=!1;if(t.deltaTime>e.options.tap_max_touchtime||t.distance>e.options.tap_max_distance)return;i&&"tap"==i.name&&t.timeStamp-i.lastEvent.timeStamp0&&t.touches.length>e.options.swipe_max_touches)return;(t.velocityX>e.options.swipe_velocity||t.velocityY>e.options.swipe_velocity)&&(e.trigger(this.name,t),e.trigger(this.name+t.direction,t))}}},n.gestures.Drag={name:"drag",index:50,defaults:{drag_min_distance:10,drag_max_touches:1,drag_block_horizontal:!1,drag_block_vertical:!1,drag_lock_to_axis:!1,drag_lock_min_distance:25},triggered:!1,handler:function(t,e){if(n.detection.current.name!=this.name&&this.triggered)return e.trigger(this.name+"end",t),void(this.triggered=!1);if(!(e.options.drag_max_touches>0&&t.touches.length>e.options.drag_max_touches))switch(t.eventType){case n.EVENT_START:this.triggered=!1;break;case n.EVENT_MOVE:if(t.distancee.options.transform_min_rotation&&e.trigger("rotate",t),i>e.options.transform_min_scale&&(e.trigger("pinch",t),e.trigger("pinch"+(t.scale<1?"in":"out"),t));break;case n.EVENT_END:this.triggered&&e.trigger(this.name+"end",t),this.triggered=!1}}},n.gestures.Touch={name:"touch",index:-1/0,defaults:{prevent_default:!1,prevent_mouseevents:!1},handler:function(t,e){return e.options.prevent_mouseevents&&t.pointerType==n.POINTER_MOUSE?void t.stopDetect():(e.options.prevent_default&&t.preventDefault(),void(t.eventType==n.EVENT_START&&e.trigger(this.name,t)))}},n.gestures.Release={name:"release",index:1/0,handler:function(t,e){t.eventType==n.EVENT_END&&e.trigger(this.name,t)}},"object"==typeof e&&"object"==typeof e.exports?e.exports=n:(t.Hammer=n,"function"==typeof t.define&&t.define.amd&&t.define("hammer",[],function(){return n}))}(this)},{}],4:[function(t,e){var i="undefined"!=typeof self?self:"undefined"!=typeof window?window:{};(function(s){function n(t,e,i){switch(arguments.length){case 2:return null!=t?t:e;case 3:return null!=t?t:null!=e?e:i;default:throw new Error("Implement me")}}function o(){return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function r(t,e){function i(){ge.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+t)}var s=!0;return u(function(){return s&&(i(),s=!1),e.apply(this,arguments)},e)}function a(t,e){return function(i){return g(t.call(this,i),e)}}function h(t,e){return function(i){return this.lang().ordinal(t.call(this,i),e)}}function d(){}function l(t){C(t),u(this,t)}function c(t){var e=w(t),i=e.year||0,s=e.quarter||0,n=e.month||0,o=e.week||0,r=e.day||0,a=e.hour||0,h=e.minute||0,d=e.second||0,l=e.millisecond||0;this._milliseconds=+l+1e3*d+6e4*h+36e5*a,this._days=+r+7*o,this._months=+n+3*s+12*i,this._data={},this._bubble()}function u(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return e.hasOwnProperty("toString")&&(t.toString=e.toString),e.hasOwnProperty("valueOf")&&(t.valueOf=e.valueOf),t}function p(t){var e,i={};for(e in t)t.hasOwnProperty(e)&&Ne.hasOwnProperty(e)&&(i[e]=t[e]);return i}function m(t){return 0>t?Math.ceil(t):Math.floor(t)}function g(t,e,i){for(var s=""+Math.abs(t),n=t>=0;s.lengths;s++)(i&&t[s]!==e[s]||!i&&S(t[s])!==S(e[s]))&&r++;return r+o}function b(t){if(t){var e=t.toLowerCase().replace(/(.)s$/,"$1");t=ni[t]||oi[e]||e}return t}function w(t){var e,i,s={};for(i in t)t.hasOwnProperty(i)&&(e=b(i),e&&(s[e]=t[i]));return s}function x(t){var e,i;if(0===t.indexOf("week"))e=7,i="day";else{if(0!==t.indexOf("month"))return;e=12,i="month"}ge[t]=function(n,o){var r,a,h=ge.fn._lang[t],d=[];if("number"==typeof n&&(o=n,n=s),a=function(t){var e=ge().utc().set(i,t);return h.call(ge.fn._lang,e,n||"")},null!=o)return a(o);for(r=0;e>r;r++)d.push(a(r));return d}}function S(t){var e=+t,i=0;return 0!==e&&isFinite(e)&&(i=e>=0?Math.floor(e):Math.ceil(e)),i}function T(t,e){return new Date(Date.UTC(t,e+1,0)).getUTCDate()}function E(t,e,i){return ne(ge([t,11,31+e-i]),e,i).week}function M(t){return D(t)?366:365}function D(t){return t%4===0&&t%100!==0||t%400===0}function C(t){var e;t._a&&-2===t._pf.overflow&&(e=t._a[xe]<0||t._a[xe]>11?xe:t._a[Se]<1||t._a[Se]>T(t._a[we],t._a[xe])?Se:t._a[Te]<0||t._a[Te]>23?Te:t._a[Ee]<0||t._a[Ee]>59?Ee:t._a[Me]<0||t._a[Me]>59?Me:t._a[De]<0||t._a[De]>999?De:-1,t._pf._overflowDayOfYear&&(we>e||e>Se)&&(e=Se),t._pf.overflow=e)}function N(t){return null==t._isValid&&(t._isValid=!isNaN(t._d.getTime())&&t._pf.overflow<0&&!t._pf.empty&&!t._pf.invalidMonth&&!t._pf.nullInput&&!t._pf.invalidFormat&&!t._pf.userInvalidated,t._strict&&(t._isValid=t._isValid&&0===t._pf.charsLeftOver&&0===t._pf.unusedTokens.length)),t._isValid}function I(t){return t?t.toLowerCase().replace("_","-"):t}function O(t,e){return e._isUTC?ge(t).zone(e._offset||0):ge(t).local()}function L(t,e){return e.abbr=t,Ce[t]||(Ce[t]=new d),Ce[t].set(e),Ce[t]}function k(t){delete Ce[t]}function P(e){var i,s,n,o,r=0,a=function(e){if(!Ce[e]&&Ie)try{t("./lang/"+e)}catch(i){}return Ce[e]};if(!e)return ge.fn._lang;if(!v(e)){if(s=a(e))return s;e=[e]}for(;r0;){if(s=a(o.slice(0,i).join("-")))return s;if(n&&n.length>=i&&_(o,n,!0)>=i-1)break;i--}r++}return ge.fn._lang}function z(t){return t.match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function A(t){var e,i,s=t.match(Pe);for(e=0,i=s.length;i>e;e++)s[e]=li[s[e]]?li[s[e]]:z(s[e]);return function(n){var o="";for(e=0;i>e;e++)o+=s[e]instanceof Function?s[e].call(n,t):s[e];return o}}function R(t,e){return t.isValid()?(e=F(e,t.lang()),ri[e]||(ri[e]=A(e)),ri[e](t)):t.lang().invalidDate()}function F(t,e){function i(t){return e.longDateFormat(t)||t}var s=5;for(ze.lastIndex=0;s>=0&&ze.test(t);)t=t.replace(ze,i),ze.lastIndex=0,s-=1;return t}function G(t,e){var i,s=e._strict;switch(t){case"Q":return Ue;case"DDDD":return qe;case"YYYY":case"GGGG":case"gggg":return s?Ze:Fe;case"Y":case"G":case"g":return $e;case"YYYYYY":case"YYYYY":case"GGGGG":case"ggggg":return s?Ke:Ge;case"S":if(s)return Ue;case"SS":if(s)return Xe;case"SSS":if(s)return qe;case"DDD":return Re;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return Ye;case"a":case"A":return P(e._l)._meridiemParse;case"X":return Ve;case"Z":case"ZZ":return Be;case"T":return We;case"SSSS":return He;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"ww":case"WW":return s?Xe:Ae;case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"W":case"e":case"E":return Ae;case"Do":return je;default:return i=new RegExp(q(X(t.replace("\\","")),"i"))}}function H(t){t=t||"";var e=t.match(Be)||[],i=e[e.length-1]||[],s=(i+"").match(ii)||["-",0,0],n=+(60*s[1])+S(s[2]);return"+"===s[0]?-n:n}function Y(t,e,i){var s,n=i._a;switch(t){case"Q":null!=e&&(n[xe]=3*(S(e)-1));break;case"M":case"MM":null!=e&&(n[xe]=S(e)-1);break;case"MMM":case"MMMM":s=P(i._l).monthsParse(e),null!=s?n[xe]=s:i._pf.invalidMonth=e;break;case"D":case"DD":null!=e&&(n[Se]=S(e));break;case"Do":null!=e&&(n[Se]=S(parseInt(e,10)));break;case"DDD":case"DDDD":null!=e&&(i._dayOfYear=S(e));break;case"YY":n[we]=ge.parseTwoDigitYear(e);break;case"YYYY":case"YYYYY":case"YYYYYY":n[we]=S(e);break;case"a":case"A":i._isPm=P(i._l).isPM(e);break;case"H":case"HH":case"h":case"hh":n[Te]=S(e);break;case"m":case"mm":n[Ee]=S(e);break;case"s":case"ss":n[Me]=S(e);break;case"S":case"SS":case"SSS":case"SSSS":n[De]=S(1e3*("0."+e));break;case"X":i._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":i._useUTC=!0,i._tzm=H(e);break;case"dd":case"ddd":case"dddd":s=P(i._l).weekdaysParse(e),null!=s?(i._w=i._w||{},i._w.d=s):i._pf.invalidWeekday=e;break;case"w":case"ww":case"W":case"WW":case"d":case"e":case"E":t=t.substr(0,1);case"gggg":case"GGGG":case"GGGGG":t=t.substr(0,2),e&&(i._w=i._w||{},i._w[t]=S(e));break;case"gg":case"GG":i._w=i._w||{},i._w[t]=ge.parseTwoDigitYear(e)}}function B(t){var e,i,s,o,r,a,h,d;e=t._w,null!=e.GG||null!=e.W||null!=e.E?(r=1,a=4,i=n(e.GG,t._a[we],ne(ge(),1,4).year),s=n(e.W,1),o=n(e.E,1)):(d=P(t._l),r=d._week.dow,a=d._week.doy,i=n(e.gg,t._a[we],ne(ge(),r,a).year),s=n(e.w,1),null!=e.d?(o=e.d,r>o&&++s):o=null!=e.e?e.e+r:r),h=oe(i,s,o,a,r),t._a[we]=h.year,t._dayOfYear=h.dayOfYear}function W(t){var e,i,s,o,r=[];if(!t._d){for(s=j(t),t._w&&null==t._a[Se]&&null==t._a[xe]&&B(t),t._dayOfYear&&(o=n(t._a[we],s[we]),t._dayOfYear>M(o)&&(t._pf._overflowDayOfYear=!0),i=te(o,0,t._dayOfYear),t._a[xe]=i.getUTCMonth(),t._a[Se]=i.getUTCDate()),e=0;3>e&&null==t._a[e];++e)t._a[e]=r[e]=s[e];for(;7>e;e++)t._a[e]=r[e]=null==t._a[e]?2===e?1:0:t._a[e];t._d=(t._useUTC?te:Q).apply(null,r),null!=t._tzm&&t._d.setUTCMinutes(t._d.getUTCMinutes()+t._tzm)}}function V(t){var e;t._d||(e=w(t._i),t._a=[e.year,e.month,e.day,e.hour,e.minute,e.second,e.millisecond],W(t))}function j(t){var e=new Date;return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}function U(t){if(t._f===ge.ISO_8601)return void K(t);t._a=[],t._pf.empty=!0;var e,i,s,n,o,r=P(t._l),a=""+t._i,h=a.length,d=0;for(s=F(t._f,r).match(Pe)||[],e=0;e0&&t._pf.unusedInput.push(o),a=a.slice(a.indexOf(i)+i.length),d+=i.length),li[n]?(i?t._pf.empty=!1:t._pf.unusedTokens.push(n),Y(n,i,t)):t._strict&&!i&&t._pf.unusedTokens.push(n);t._pf.charsLeftOver=h-d,a.length>0&&t._pf.unusedInput.push(a),t._isPm&&t._a[Te]<12&&(t._a[Te]+=12),t._isPm===!1&&12===t._a[Te]&&(t._a[Te]=0),W(t),C(t)}function X(t){return t.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,i,s,n){return e||i||s||n})}function q(t){return t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function Z(t){var e,i,s,n,r;if(0===t._f.length)return t._pf.invalidFormat=!0,void(t._d=new Date(0/0));for(n=0;nr)&&(s=r,i=e));u(t,i||e)}function K(t){var e,i,s=t._i,n=Je.exec(s);if(n){for(t._pf.iso=!0,e=0,i=ti.length;i>e;e++)if(ti[e][1].exec(s)){t._f=ti[e][0]+(n[6]||" ");break}for(e=0,i=ei.length;i>e;e++)if(ei[e][1].exec(s)){t._f+=ei[e][0];break}s.match(Be)&&(t._f+="Z"),U(t)}else t._isValid=!1}function $(t){K(t),t._isValid===!1&&(delete t._isValid,ge.createFromInputFallback(t))}function J(t){var e=t._i,i=Oe.exec(e);e===s?t._d=new Date:i?t._d=new Date(+i[1]):"string"==typeof e?$(t):v(e)?(t._a=e.slice(0),W(t)):y(e)?t._d=new Date(+e):"object"==typeof e?V(t):"number"==typeof e?t._d=new Date(e):ge.createFromInputFallback(t)}function Q(t,e,i,s,n,o,r){var a=new Date(t,e,i,s,n,o,r);return 1970>t&&a.setFullYear(t),a}function te(t){var e=new Date(Date.UTC.apply(null,arguments));return 1970>t&&e.setUTCFullYear(t),e}function ee(t,e){if("string"==typeof t)if(isNaN(t)){if(t=e.weekdaysParse(t),"number"!=typeof t)return null}else t=parseInt(t,10);return t}function ie(t,e,i,s,n){return n.relativeTime(e||1,!!i,t,s)}function se(t,e,i){var s=be(Math.abs(t)/1e3),n=be(s/60),o=be(n/60),r=be(o/24),a=be(r/365),h=s0,h[4]=i,ie.apply({},h)}function ne(t,e,i){var s,n=i-e,o=i-t.day();return o>n&&(o-=7),n-7>o&&(o+=7),s=ge(t).add("d",o),{week:Math.ceil(s.dayOfYear()/7),year:s.year()}}function oe(t,e,i,s,n){var o,r,a=te(t,0,1).getUTCDay();return a=0===a?7:a,i=null!=i?i:n,o=n-a+(a>s?7:0)-(n>a?7:0),r=7*(e-1)+(i-n)+o+1,{year:r>0?t:t-1,dayOfYear:r>0?r:M(t-1)+r}}function re(t){var e=t._i,i=t._f;return null===e||i===s&&""===e?ge.invalid({nullInput:!0}):("string"==typeof e&&(t._i=e=P().preparse(e)),ge.isMoment(e)?(t=p(e),t._d=new Date(+e._d)):i?v(i)?Z(t):U(t):J(t),new l(t))}function ae(t,e){var i,s;if(1===e.length&&v(e[0])&&(e=e[0]),!e.length)return ge();for(i=e[0],s=1;s=0?"+":"-";return e+g(Math.abs(t),6)},gg:function(){return g(this.weekYear()%100,2)},gggg:function(){return g(this.weekYear(),4)},ggggg:function(){return g(this.weekYear(),5)},GG:function(){return g(this.isoWeekYear()%100,2)},GGGG:function(){return g(this.isoWeekYear(),4)},GGGGG:function(){return g(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return S(this.milliseconds()/100)},SS:function(){return g(S(this.milliseconds()/10),2)},SSS:function(){return g(this.milliseconds(),3)},SSSS:function(){return g(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+g(S(t/60),2)+":"+g(S(t)%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+g(S(t/60),2)+g(S(t)%60,2)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()},Q:function(){return this.quarter()}},ci=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"];hi.length;)ve=hi.pop(),li[ve+"o"]=h(li[ve],ve);for(;di.length;)ve=di.pop(),li[ve+ve]=a(li[ve],2);for(li.DDDD=a(li.DDD,3),u(d.prototype,{set:function(t){var e,i;for(i in t)e=t[i],"function"==typeof e?this[i]=e:this["_"+i]=e},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(t){return this._months[t.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(t){return this._monthsShort[t.month()]},monthsParse:function(t){var e,i,s;for(this._monthsParse||(this._monthsParse=[]),e=0;12>e;e++)if(this._monthsParse[e]||(i=ge.utc([2e3,e]),s="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[e]=new RegExp(s.replace(".",""),"i")),this._monthsParse[e].test(t))return e},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(t){return this._weekdays[t.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(t){return this._weekdaysShort[t.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(t){return this._weekdaysMin[t.day()]},weekdaysParse:function(t){var e,i,s;for(this._weekdaysParse||(this._weekdaysParse=[]),e=0;7>e;e++)if(this._weekdaysParse[e]||(i=ge([2e3,1]).day(e),s="^"+this.weekdays(i,"")+"|^"+this.weekdaysShort(i,"")+"|^"+this.weekdaysMin(i,""),this._weekdaysParse[e]=new RegExp(s.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(t){var e=this._longDateFormat[t];return!e&&this._longDateFormat[t.toUpperCase()]&&(e=this._longDateFormat[t.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t]=e),e},isPM:function(t){return"p"===(t+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(t,e,i){return t>11?i?"pm":"PM":i?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(t,e){var i=this._calendar[t];return"function"==typeof i?i.apply(e):i},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(t,e,i,s){var n=this._relativeTime[i];return"function"==typeof n?n(t,e,i,s):n.replace(/%d/i,t)},pastFuture:function(t,e){var i=this._relativeTime[t>0?"future":"past"];return"function"==typeof i?i(e):i.replace(/%s/i,e)},ordinal:function(t){return this._ordinal.replace("%d",t)},_ordinal:"%d",preparse:function(t){return t},postformat:function(t){return t},week:function(t){return ne(t,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),ge=function(t,e,i,n){var r;return"boolean"==typeof i&&(n=i,i=s),r={},r._isAMomentObject=!0,r._i=t,r._f=e,r._l=i,r._strict=n,r._isUTC=!1,r._pf=o(),re(r)},ge.suppressDeprecationWarnings=!1,ge.createFromInputFallback=r("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(t){t._d=new Date(t._i)}),ge.min=function(){var t=[].slice.call(arguments,0);return ae("isBefore",t)},ge.max=function(){var t=[].slice.call(arguments,0);return ae("isAfter",t)},ge.utc=function(t,e,i,n){var r;return"boolean"==typeof i&&(n=i,i=s),r={},r._isAMomentObject=!0,r._useUTC=!0,r._isUTC=!0,r._l=i,r._i=t,r._f=e,r._strict=n,r._pf=o(),re(r).utc()},ge.unix=function(t){return ge(1e3*t)},ge.duration=function(t,e){var i,s,n,o=t,r=null;return ge.isDuration(t)?o={ms:t._milliseconds,d:t._days,M:t._months}:"number"==typeof t?(o={},e?o[e]=t:o.milliseconds=t):(r=Le.exec(t))?(i="-"===r[1]?-1:1,o={y:0,d:S(r[Se])*i,h:S(r[Te])*i,m:S(r[Ee])*i,s:S(r[Me])*i,ms:S(r[De])*i}):(r=ke.exec(t))&&(i="-"===r[1]?-1:1,n=function(t){var e=t&&parseFloat(t.replace(",","."));return(isNaN(e)?0:e)*i},o={y:n(r[2]),M:n(r[3]),d:n(r[4]),h:n(r[5]),m:n(r[6]),s:n(r[7]),w:n(r[8])}),s=new c(o),ge.isDuration(t)&&t.hasOwnProperty("_lang")&&(s._lang=t._lang),s},ge.version=ye,ge.defaultFormat=Qe,ge.ISO_8601=function(){},ge.momentProperties=Ne,ge.updateOffset=function(){},ge.relativeTimeThreshold=function(t,e){return ai[t]===s?!1:(ai[t]=e,!0)},ge.lang=function(t,e){var i;return t?(e?L(I(t),e):null===e?(k(t),t="en"):Ce[t]||P(t),i=ge.duration.fn._lang=ge.fn._lang=P(t),i._abbr):ge.fn._lang._abbr},ge.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),P(t)},ge.isMoment=function(t){return t instanceof l||null!=t&&t.hasOwnProperty("_isAMomentObject")},ge.isDuration=function(t){return t instanceof c},ve=ci.length-1;ve>=0;--ve)x(ci[ve]);ge.normalizeUnits=function(t){return b(t)},ge.invalid=function(t){var e=ge.utc(0/0);return null!=t?u(e._pf,t):e._pf.userInvalidated=!0,e},ge.parseZone=function(){return ge.apply(null,arguments).parseZone()},ge.parseTwoDigitYear=function(t){return S(t)+(S(t)>68?1900:2e3)},u(ge.fn=l.prototype,{clone:function(){return ge(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().lang("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){var t=ge(this).utc();return 00:!1},parsingFlags:function(){return u({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(t){var e=R(this,t||ge.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var i;return i="string"==typeof t&&"string"==typeof e?ge.duration(isNaN(+e)?+t:+e,isNaN(+e)?e:t):"string"==typeof t?ge.duration(+e,t):ge.duration(t,e),f(this,i,1),this},subtract:function(t,e){var i;return i="string"==typeof t&&"string"==typeof e?ge.duration(isNaN(+e)?+t:+e,isNaN(+e)?e:t):"string"==typeof t?ge.duration(+e,t):ge.duration(t,e),f(this,i,-1),this},diff:function(t,e,i){var s,n,o=O(t,this),r=6e4*(this.zone()-o.zone());return e=b(e),"year"===e||"month"===e?(s=432e5*(this.daysInMonth()+o.daysInMonth()),n=12*(this.year()-o.year())+(this.month()-o.month()),n+=(this-ge(this).startOf("month")-(o-ge(o).startOf("month")))/s,n-=6e4*(this.zone()-ge(this).startOf("month").zone()-(o.zone()-ge(o).startOf("month").zone()))/s,"year"===e&&(n/=12)):(s=this-o,n="second"===e?s/1e3:"minute"===e?s/6e4:"hour"===e?s/36e5:"day"===e?(s-r)/864e5:"week"===e?(s-r)/6048e5:s),i?n:m(n)},from:function(t,e){return ge.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(ge(),t) -},calendar:function(t){var e=t||ge(),i=O(e,this).startOf("day"),s=this.diff(i,"days",!0),n=-6>s?"sameElse":-1>s?"lastWeek":0>s?"lastDay":1>s?"sameDay":2>s?"nextDay":7>s?"nextWeek":"sameElse";return this.format(this.lang().calendar(n,this))},isLeapYear:function(){return D(this.year())},isDST:function(){return this.zone()+ge(t).startOf(e)},isBefore:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)<+ge(t).startOf(e)},isSame:function(t,e){return e=e||"ms",+this.clone().startOf(e)===+O(t,this).startOf(e)},min:r("moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",function(t){return t=ge.apply(null,arguments),this>t?this:t}),max:r("moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(t){return t=ge.apply(null,arguments),t>this?this:t}),zone:function(t,e){var i=this._offset||0;return null==t?this._isUTC?i:this._d.getTimezoneOffset():("string"==typeof t&&(t=H(t)),Math.abs(t)<16&&(t=60*t),this._offset=t,this._isUTC=!0,i!==t&&(!e||this._changeInProgress?f(this,ge.duration(i-t,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,ge.updateOffset(this,!0),this._changeInProgress=null)),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return this._tzm?this.zone(this._tzm):"string"==typeof this._i&&this.zone(this._i),this},hasAlignedHourOffset:function(t){return t=t?ge(t).zone():0,(this.zone()-t)%60===0},daysInMonth:function(){return T(this.year(),this.month())},dayOfYear:function(t){var e=be((ge(this).startOf("day")-ge(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},quarter:function(t){return null==t?Math.ceil((this.month()+1)/3):this.month(3*(t-1)+this.month()%3)},weekYear:function(t){var e=ne(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==t?e:this.add("y",t-e)},isoWeekYear:function(t){var e=ne(this,1,4).year;return null==t?e:this.add("y",t-e)},week:function(t){var e=this.lang().week(this);return null==t?e:this.add("d",7*(t-e))},isoWeek:function(t){var e=ne(this,1,4).week;return null==t?e:this.add("d",7*(t-e))},weekday:function(t){var e=(this.day()+7-this.lang()._week.dow)%7;return null==t?e:this.add("d",t-e)},isoWeekday:function(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)},isoWeeksInYear:function(){return E(this.year(),1,4)},weeksInYear:function(){var t=this._lang._week;return E(this.year(),t.dow,t.doy)},get:function(t){return t=b(t),this[t]()},set:function(t,e){return t=b(t),"function"==typeof this[t]&&this[t](e),this},lang:function(t){return t===s?this._lang:(this._lang=P(t),this)}}),ge.fn.millisecond=ge.fn.milliseconds=ce("Milliseconds",!1),ge.fn.second=ge.fn.seconds=ce("Seconds",!1),ge.fn.minute=ge.fn.minutes=ce("Minutes",!1),ge.fn.hour=ge.fn.hours=ce("Hours",!0),ge.fn.date=ce("Date",!0),ge.fn.dates=r("dates accessor is deprecated. Use date instead.",ce("Date",!0)),ge.fn.year=ce("FullYear",!0),ge.fn.years=r("years accessor is deprecated. Use year instead.",ce("FullYear",!0)),ge.fn.days=ge.fn.day,ge.fn.months=ge.fn.month,ge.fn.weeks=ge.fn.week,ge.fn.isoWeeks=ge.fn.isoWeek,ge.fn.quarters=ge.fn.quarter,ge.fn.toJSON=ge.fn.toISOString,u(ge.duration.fn=c.prototype,{_bubble:function(){var t,e,i,s,n=this._milliseconds,o=this._days,r=this._months,a=this._data;a.milliseconds=n%1e3,t=m(n/1e3),a.seconds=t%60,e=m(t/60),a.minutes=e%60,i=m(e/60),a.hours=i%24,o+=m(i/24),a.days=o%30,r+=m(o/30),a.months=r%12,s=m(r/12),a.years=s},weeks:function(){return m(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*S(this._months/12)},humanize:function(t){var e=+this,i=se(e,!t,this.lang());return t&&(i=this.lang().pastFuture(e,i)),this.lang().postformat(i)},add:function(t,e){var i=ge.duration(t,e);return this._milliseconds+=i._milliseconds,this._days+=i._days,this._months+=i._months,this._bubble(),this},subtract:function(t,e){var i=ge.duration(t,e);return this._milliseconds-=i._milliseconds,this._days-=i._days,this._months-=i._months,this._bubble(),this},get:function(t){return t=b(t),this[t.toLowerCase()+"s"]()},as:function(t){return t=b(t),this["as"+t.charAt(0).toUpperCase()+t.slice(1)+"s"]()},lang:ge.fn.lang,toIsoString:function(){var t=Math.abs(this.years()),e=Math.abs(this.months()),i=Math.abs(this.days()),s=Math.abs(this.hours()),n=Math.abs(this.minutes()),o=Math.abs(this.seconds()+this.milliseconds()/1e3);return this.asSeconds()?(this.asSeconds()<0?"-":"")+"P"+(t?t+"Y":"")+(e?e+"M":"")+(i?i+"D":"")+(s||n||o?"T":"")+(s?s+"H":"")+(n?n+"M":"")+(o?o+"S":""):"P0D"}});for(ve in si)si.hasOwnProperty(ve)&&(pe(ve,si[ve]),ue(ve.toLowerCase()));pe("Weeks",6048e5),ge.duration.fn.asMonths=function(){return(+this-31536e6*this.years())/2592e6+12*this.years()},ge.lang("en",{ordinal:function(t){var e=t%10,i=1===S(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th";return t+i}}),Ie?e.exports=ge:"function"==typeof define&&define.amd?(define("moment",function(t,e,i){return i.config&&i.config()&&i.config().noGlobal===!0&&(_e.moment=fe),ge}),me(!0)):me()}).call(this)},{}],5:[function(t,e){function i(t,e,i){return t.addEventListener?t.addEventListener(e,i,!1):void t.attachEvent("on"+e,i)}function s(t){return"keypress"==t.type?String.fromCharCode(t.which):w[t.which]?w[t.which]:x[t.which]?x[t.which]:String.fromCharCode(t.which).toLowerCase()}function n(t){var e=t.target||t.srcElement,i=e.tagName;return(" "+e.className+" ").indexOf(" mousetrap ")>-1?!1:"INPUT"==i||"SELECT"==i||"TEXTAREA"==i||e.contentEditable&&"true"==e.contentEditable}function o(t,e){return t.sort().join(",")===e.sort().join(",")}function r(t){t=t||{};var e,i=!1;for(e in D)t[e]?i=!0:D[e]=0;i||(N=!1)}function a(t,e,i,s,n){var r,a,h=[];if(!E[t])return[];for("keyup"==i&&u(t)&&(e=[t]),r=0;r95&&112>t||w.hasOwnProperty(t)&&(_[w[t]]=t)}return _}function g(t,e,i){return i||(i=m()[t]?"keydown":"keypress"),"keypress"==i&&e.length&&(i="keydown"),i}function f(t,e,i,n){D[t]=0,n||(n=g(e[0],[]));var o,a=function(){N=n,++D[t],p()},h=function(t){d(i,t),"keyup"!==n&&(C=s(t)),setTimeout(r,10)};for(o=0;o1)return f(t,d,e,i);for(h="+"===t?["+"]:t.split("+"),o=0;o":".","?":"/","|":"\\"},T={option:"alt",command:"meta","return":"enter",escape:"esc"},E={},M={},D={},C=!1,N=!1,I=1;20>I;++I)w[111+I]="f"+I;for(I=0;9>=I;++I)w[I+96]=I;i(document,"keypress",c),i(document,"keydown",c),i(document,"keyup",c);var O={bind:function(t,e,i){return y(t instanceof Array?t:[t],e,i),M[t+":"+i]=e,this},unbind:function(t,e){return M[t+":"+e]&&(delete M[t+":"+e],this.bind(t,function(){},e)),this},trigger:function(t,e){return M[t+":"+e](),this},reset:function(){return E={},M={},this}};e.exports=O},{}]},{},[1])(1)}); \ No newline at end of file +!function(t){if("object"==typeof exports)module.exports=t();else if("function"==typeof define&&define.amd)define(t);else{var e;"undefined"!=typeof window?e=window:"undefined"!=typeof global?e=global:"undefined"!=typeof self&&(e=self),e.vis=t()}}(function(){var define,module,exports;return function t(e,i,s){function o(r,a){if(!i[r]){if(!e[r]){var h="function"==typeof require&&require;if(!a&&h)return h(r,!0);if(n)return n(r,!0);throw new Error("Cannot find module '"+r+"'")}var d=i[r]={exports:{}};e[r][0].call(d.exports,function(t){var i=e[r][1][t];return o(i?i:t)},d,d.exports,t,e,i,s)}return i[r].exports}for(var n="function"==typeof require&&require,r=0;re?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,o;if(null==this)throw new TypeError(" this is null or not defined");var n=Object(this),r=n.length>>>0;if("function"!=typeof t)throw new TypeError(t+" is not a function");for(e&&(i=e),s=new Array(r),o=0;r>o;){var a,h;o in n&&(a=n[o],h=t.call(i,a,o,n),s[o]=h),o++}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=[],o=arguments[1],n=0;i>n;n++)if(n in e){var r=e[n];t.call(o,r,n,e)&&s.push(r)}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(o){if("object"!=typeof o&&"function"!=typeof o||null===o)throw new TypeError("Object.keys called on non-object");var n=[];for(var r in o)t.call(o,r)&&n.push(r);if(e)for(var a=0;s>a;a++)t.call(o,i[a])&&n.push(i[a]);return n}}()),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(){},o=function(){return i.apply(this instanceof s&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return s.prototype=this.prototype,o.prototype=new s,o}),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(){},o=function(){return i.apply(this instanceof s&&t?this:t,e.concat(Array.prototype.slice.call(arguments))) +};return s.prototype=this.prototype,o.prototype=new s,o});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 o in s)s.hasOwnProperty(o)&&(t[o]=s[o])}return t},util.selectiveExtend=function(t,e){if(!Array.isArray(t))throw new Error("Array with property names expected as first argument");for(var i=2;ii;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('Unknown 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,o=t.offsetParent;null!=o&&o!=i&&o!=e;)s+=o.offsetLeft,s-=o.scrollLeft,o=o.offsetParent;return s},util.getAbsoluteTop=function(t){for(var e=document.documentElement,i=document.body,s=t.offsetTop,o=t.offsetParent;null!=o&&o!=i&&o!=e;)s+=o.offsetTop,s-=o.scrollTop,o=o.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(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)},o={h:i.h,s:Math.min(1,1.25*i.v),v:.6*i.v},n=util.HSVToHex(o.h,o.h,o.v),r=util.HSVToHex(s.h,s.s,s.v);e={background:t,border:n,highlight:{background:r,border:n},hover:{background:r,border:n}}}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)),o=util.GiveDec(t.substring(3,4)),n=util.GiveDec(t.substring(4,5)),r=util.GiveDec(t.substring(5,6)),a=16*e+i,h=16*s+o,i=16*n+r;return{r:a,g:h,b:i}},util.RGBToHex=function(t,e,i){var s=util.GiveHex(Math.floor(t/16)),o=util.GiveHex(t%16),n=util.GiveHex(Math.floor(e/16)),r=util.GiveHex(e%16),a=util.GiveHex(Math.floor(i/16)),h=util.GiveHex(i%16),d=s+o+n+r+a+h;return"#"+d},util.RGBToHSV=function(t,e,i){t/=255,e/=255,i/=255;var s=Math.min(t,Math.min(e,i)),o=Math.max(t,Math.max(e,i));if(s==o)return{h:0,s:0,v:s};var n=t==s?e-i:i==s?t-e:i-t,r=t==s?3:i==s?1:5,a=60*(r-n/(o-s))/360,h=(o-s)/o,d=o;return{h:a,s:h,v:d}},util.HSVToRGB=function(t,e,i){var s,o,n,r=Math.floor(6*t),a=6*t-r,h=i*(1-e),d=i*(1-a*e),l=i*(1-(1-a)*e);switch(r%6){case 0:s=i,o=l,n=h;break;case 1:s=d,o=i,n=h;break;case 2:s=h,o=i,n=l;break;case 3:s=h,o=d,n=i;break;case 4:s=l,o=h,n=i;break;case 5:s=i,o=h,n=d}return{r:Math.floor(255*s),g:Math.floor(255*o),b:Math.floor(255*n)}},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.selectiveBridgeObject=function(t,e){if("object"==typeof e){for(var i=Object.create(e),s=0;se.start-a&&ne.start-a&&nn&&r>e||e>r&&a>e?(d=!0,r!=e&&("before"==s?e>n&&r>e&&(p=Math.max(0,p-1)):e>r&&a>e&&(p=Math.min(h.length-1,p+1)))):(e>r?l=Math.floor(.5*(c+l)):c=Math.floor(.5*(c+l)),o=Math.floor(.5*(c+l)),p==o?(p=-2,d=!0):p=o);return p};var DOMutil={};DOMutil.prepareElements=function(t){for(var e in t)t.hasOwnProperty(e)&&(t[e].redundant=t[e].used,t[e].used=[])},DOMutil.cleanupElements=function(t){for(var e in t)if(t.hasOwnProperty(e)&&t[e].redundant){for(var i=0;i0?(s=e[t].redundant[0],e[t].redundant.shift()):(s=document.createElementNS("http://www.w3.org/2000/svg",t),i.appendChild(s)):(s=document.createElementNS("http://www.w3.org/2000/svg",t),e[t]={used:[],redundant:[]},i.appendChild(s)),e[t].used.push(s),s},DOMutil.getDOMElement=function(t,e,i){var s;return e.hasOwnProperty(t)?e[t].redundant.length>0?(s=e[t].redundant[0],e[t].redundant.shift()):(s=document.createElement(t),i.appendChild(s)):(s=document.createElement(t),e[t]={used:[],redundant:[]},i.appendChild(s)),e[t].used.push(s),s},DOMutil.drawPoint=function(t,e,i,s,o){var n;return"circle"==i.options.drawPoints.style?(n=DOMutil.getSVGElement("circle",s,o),n.setAttributeNS(null,"cx",t),n.setAttributeNS(null,"cy",e),n.setAttributeNS(null,"r",.5*i.options.drawPoints.size),n.setAttributeNS(null,"class",i.className+" point")):(n=DOMutil.getSVGElement("rect",s,o),n.setAttributeNS(null,"x",t-.5*i.options.drawPoints.size),n.setAttributeNS(null,"y",e-.5*i.options.drawPoints.size),n.setAttributeNS(null,"width",i.options.drawPoints.size),n.setAttributeNS(null,"height",i.options.drawPoints.size),n.setAttributeNS(null,"class",i.className+" point")),n},DOMutil.drawBar=function(t,e,i,s,o,n,r){var a=DOMutil.getSVGElement("rect",n,r);a.setAttributeNS(null,"x",t-.5*i),a.setAttributeNS(null,"y",e),a.setAttributeNS(null,"width",i),a.setAttributeNS(null,"height",s),a.setAttributeNS(null,"class",o)},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 o=0;on;n++)i=o._addItem(t[n]),s.push(i);else if(util.isDataTable(t))for(var a=this._getColumnNames(t),h=0,d=t.getNumberOfRows();d>h;h++){for(var l={},c=0,p=a.length;p>c;c++){var u=a[c];l[u]=t.getValue(h,c)}i=o._addItem(l),s.push(i)}else{if(!(t instanceof Object))throw new Error("Unknown dataType");i=o._addItem(t),s.push(i)}return s.length&&this._trigger("add",{items:s},e),s},DataSet.prototype.update=function(t,e){var i=[],s=[],o=this,n=o._fieldId,r=function(t){var e=t[n];o._data[e]?(e=o._updateItem(t),s.push(e)):(e=o._addItem(t),i.push(e))};if(Array.isArray(t))for(var a=0,h=t.length;h>a;a++)r(t[a]);else if(util.isDataTable(t))for(var d=this._getColumnNames(t),l=0,c=t.getNumberOfRows();c>l;l++){for(var p={},u=0,m=d.length;m>u;u++){var g=d[u];p[g]=t.getValue(l,u)}r(p)}else{if(!(t instanceof Object))throw new Error("Unknown dataType");r(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,o=this,n=util.getType(arguments[0]);"String"==n||"Number"==n?(t=arguments[0],i=arguments[1],s=arguments[2]):"Array"==n?(e=arguments[0],i=arguments[1],s=arguments[2]):(i=arguments[0],s=arguments[1]);var r;if(i&&i.returnType){if(r="DataTable"==i.returnType?"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";var a,h,d,l,c=i&&i.type||this._options.type,p=i&&i.filter,u=[];if(void 0!=t)a=o._getItem(t,c),p&&!p(a)&&(a=null);else if(void 0!=e)for(d=0,l=e.length;l>d;d++)a=o._getItem(e[d],c),(!p||p(a))&&u.push(a);else for(h in this._data)this._data.hasOwnProperty(h)&&(a=o._getItem(h,c),(!p||p(a))&&u.push(a));if(i&&i.order&&void 0==t&&this._sort(u,i.order),i&&i.fields){var m=i.fields;if(void 0!=t)a=this._filterFields(a,m);else for(d=0,l=u.length;l>d;d++)u[d]=this._filterFields(u[d],m)}if("DataTable"==r){var g=this._getColumnNames(s);if(void 0!=t)o._appendRow(s,g,a);else for(d=0,l=u.length;l>d;d++)o._appendRow(s,g,u[d]);return s}if(void 0!=t)return a;if(s){for(d=0,l=u.length;l>d;d++)s.push(u[d]);return s}return u},DataSet.prototype.getIds=function(t){var e,i,s,o,n,r=this._data,a=t&&t.filter,h=t&&t.order,d=t&&t.type||this._options.type,l=[];if(a)if(h){n=[];for(s in r)r.hasOwnProperty(s)&&(o=this._getItem(s,d),a(o)&&n.push(o));for(this._sort(n,h),e=0,i=n.length;i>e;e++)l[e]=n[e][this._fieldId]}else for(s in r)r.hasOwnProperty(s)&&(o=this._getItem(s,d),a(o)&&l.push(o[this._fieldId]));else if(h){n=[];for(s in r)r.hasOwnProperty(s)&&n.push(r[s]);for(this._sort(n,h),e=0,i=n.length;i>e;e++)l[e]=n[e][this._fieldId]}else for(s in r)r.hasOwnProperty(s)&&(o=r[s],l.push(o[this._fieldId]));return l},DataSet.prototype.getDataSet=function(){return this},DataSet.prototype.forEach=function(t,e){var i,s,o=e&&e.filter,n=e&&e.type||this._options.type,r=this._data;if(e&&e.order)for(var a=this.get(e),h=0,d=a.length;d>h;h++)i=a[h],s=i[this._fieldId],t(i,s);else for(s in r)r.hasOwnProperty(s)&&(i=this._getItem(s,n),(!o||o(i))&&t(i,s))},DataSet.prototype.map=function(t,e){var i,s=e&&e.filter,o=e&&e.type||this._options.type,n=[],r=this._data;for(var a in r)r.hasOwnProperty(a)&&(i=this._getItem(a,o),(!s||s(i))&&n.push(t(i,a)));return e&&e.order&&this._sort(n,e.order),n},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],o=e[i];return s>o?1:o>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,o,n=[];if(Array.isArray(t))for(i=0,s=t.length;s>i;i++)o=this._remove(t[i]),null!=o&&n.push(o);else o=this._remove(t),null!=o&&n.push(o);return n.length&&this._trigger("remove",{items:n},e),n},DataSet.prototype._remove=function(t){if(util.isNumber(t)||util.isString(t)){if(this._data[t])return delete this._data[t],t}else if(t instanceof Object){var e=t[this._fieldId];if(e&&this._data[e])return delete this._data[e],e}return null},DataSet.prototype.clear=function(t){var e=Object.keys(this._data);return this._data={},this._trigger("remove",{items:e},t),e},DataSet.prototype.max=function(t){var e=this._data,i=null,s=null;for(var o in e)if(e.hasOwnProperty(o)){var n=e[o],r=n[t];null!=r&&(!i||r>s)&&(i=n,s=r)}return i},DataSet.prototype.min=function(t){var e=this._data,i=null,s=null;for(var o in e)if(e.hasOwnProperty(o)){var n=e[o],r=n[t];null!=r&&(!i||s>r)&&(i=n,s=r)}return i},DataSet.prototype.distinct=function(t){var e,i=this._data,s=[],o=this._options.type&&this._options.type[t]||null,n=0;for(var r in i)if(i.hasOwnProperty(r)){var a=i[r],h=a[t],d=!1;for(e=0;n>e;e++)if(s[e]==h){d=!0;break}d||void 0===h||(s[n]=h,n++)}if(o)for(e=0;ei;i++)e[i]=t.getColumnId(i)||t.getColumnLabel(i);return e},DataSet.prototype._appendRow=function(t,e,i){for(var s=t.addRow(),o=0,n=e.length;n>o;o++){var r=e[o];t.setValue(s,o,i[r])}},DataView.prototype.setData=function(t){var e,i,s;if(this._data){this._data.unsubscribe&&this._data.unsubscribe("*",this.listener),e=[];for(var o in this._ids)this._ids.hasOwnProperty(o)&&e.push(o);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++)o=e[i],this._ids[o]=!0;this._trigger("add",{items:e}),this._data.on&&this._data.on("*",this.listener)}},DataView.prototype.get=function(){var t,e,i,s=this,o=util.getType(arguments[0]);"String"==o||"Number"==o||"Array"==o?(t=arguments[0],e=arguments[1],i=arguments[2]):(e=arguments[0],i=arguments[1]);var n=util.extend({},this._options,e);this._options.filter&&e&&e.filter&&(n.filter=function(t){return s._options.filter(t)&&e.filter(t)});var r=[];return void 0!=t&&r.push(t),r.push(n),r.push(i),this._data&&this._data.get.apply(this._data,r)},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.getDataSet=function(){for(var t=this;t instanceof DataView;)t=t._data;return t||null},DataView.prototype._onEvent=function(t,e,i){var s,o,n,r,a=e&&e.items,h=this._data,d=[],l=[],c=[];if(a&&h){switch(t){case"add":for(s=0,o=a.length;o>s;s++)n=a[s],r=this.get(n),r&&(this._ids[n]=!0,d.push(n));break;case"update":for(s=0,o=a.length;o>s;s++)n=a[s],r=this.get(n),r?this._ids[n]?l.push(n):(this._ids[n]=!0,d.push(n)):this._ids[n]&&(delete this._ids[n],c.push(n));break;case"remove":for(s=0,o=a.length;o>s;s++)n=a[s],this._ids[n]&&(delete this._ids[n],c.push(n))}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,GraphGroup.prototype.setItems=function(t){null!=t?(this.itemsData=t,1==this.options.sort&&this.itemsData.sort(function(t,e){return t.x-e.x})):this.itemsData=[]},GraphGroup.prototype.setZeroPosition=function(t){this.zeroPosition=t},GraphGroup.prototype.setOptions=function(t){if(void 0!==t){var e=["sampling","style","sort","yAxisOrientation","barChart"];util.selectiveDeepExtend(e,this.options,t),util.mergeOptions(this.options,t,"catmullRom"),util.mergeOptions(this.options,t,"drawPoints"),util.mergeOptions(this.options,t,"shaded"),t.catmullRom&&"object"==typeof t.catmullRom&&t.catmullRom.parametrization&&("uniform"==t.catmullRom.parametrization?this.options.catmullRom.alpha=0:"chordal"==t.catmullRom.parametrization?this.options.catmullRom.alpha=1:(this.options.catmullRom.parametrization="centripetal",this.options.catmullRom.alpha=.5))}},GraphGroup.prototype.update=function(t){this.group=t,this.content=t.content||"graph",this.className=t.className||this.className||"graphGroup"+this.groupsUsingDefaultStyles[0]%10,this.setOptions(t.options)},GraphGroup.prototype.drawIcon=function(t,e,i,s,o,n){var r,a,h=.5*n,d=DOMutil.getSVGElement("rect",i,s);if(d.setAttributeNS(null,"x",t),d.setAttributeNS(null,"y",e-h),d.setAttributeNS(null,"width",o),d.setAttributeNS(null,"height",2*h),d.setAttributeNS(null,"class","outline"),"line"==this.options.style)r=DOMutil.getSVGElement("path",i,s),r.setAttributeNS(null,"class",this.className),r.setAttributeNS(null,"d","M"+t+","+e+" L"+(t+o)+","+e),1==this.options.shaded.enabled&&(a=DOMutil.getSVGElement("path",i,s),"top"==this.options.shaded.orientation?a.setAttributeNS(null,"d","M"+t+", "+(e-h)+"L"+t+","+e+" L"+(t+o)+","+e+" L"+(t+o)+","+(e-h)):a.setAttributeNS(null,"d","M"+t+","+e+" L"+t+","+(e+h)+" L"+(t+o)+","+(e+h)+"L"+(t+o)+","+e),a.setAttributeNS(null,"class",this.className+" iconFill")),1==this.options.drawPoints.enabled&&DOMutil.drawPoint(t+.5*o,e,this,i,s);else{var l=Math.round(.3*o),c=Math.round(.4*n),p=Math.round(.75*n),u=Math.round((o-2*l)/3);DOMutil.drawBar(t+.5*l+u,e+h-c-1,l,c,this.className+" bar",i,s),DOMutil.drawBar(t+1.5*l+u+2,e+h-p-1,l,p,this.className+" bar",i,s)}},Legend.prototype=new Component,Legend.prototype.addGroup=function(t,e){this.groups.hasOwnProperty(t)||(this.groups[t]=e),this.amountOfGroups+=1},Legend.prototype.updateGroup=function(t,e){this.groups[t]=e},Legend.prototype.removeGroup=function(t){this.groups.hasOwnProperty(t)&&(delete this.groups[t],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="0px",this.svg.style.width=this.options.iconSize+5+"px",this.dom.frame.appendChild(this.svg),this.dom.frame.appendChild(this.dom.textArea)},Legend.prototype.hide=function(){this.dom.frame.parentNode&&this.dom.frame.parentNode.removeChild(this.dom.frame)},Legend.prototype.show=function(){this.dom.frame.parentNode||this.body.dom.center.appendChild(this.dom.frame)},Legend.prototype.setOptions=function(t){var e=["enabled","orientation","icons","left","right"];util.selectiveDeepExtend(e,this.options,t)},Legend.prototype.redraw=function(){if(0==this.options[this.side].visible||0==this.amountOfGroups||0==this.options.enabled)this.hide();else{this.show(),"top-left"==this.options[this.side].position||"bottom-left"==this.options[this.side].position?(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="0px",this.svg.style.right=""):(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="0px",this.svg.style.left=""),"top-left"==this.options[this.side].position||"top-right"==this.options[this.side].position?(this.dom.frame.style.top=4-Number(this.body.dom.center.style.top.replace("px",""))+"px",this.dom.frame.style.bottom=""):(this.dom.frame.style.bottom=4-Number(this.body.dom.center.style.top.replace("px",""))+"px",this.dom.frame.style.top=""),0==this.options.icons?(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"):(this.dom.frame.style.width=this.options.iconSize+15+this.dom.textArea.offsetWidth+10+"px",this.drawLegendIcons());var t="";for(var e in this.groups)this.groups.hasOwnProperty(e)&&(t+=this.groups[e].content+"
");this.dom.textArea.innerHTML=t,this.dom.textArea.style.lineHeight=.75*this.options.iconSize+this.options.iconSpacing+"px"}},Legend.prototype.drawLegendIcons=function(){if(this.dom.frame.parentNode){DOMutil.prepareElements(this.svgElements);var t=window.getComputedStyle(this.dom.frame).paddingTop,e=Number(t.replace("px","")),i=e,s=this.options.iconSize,o=.75*this.options.iconSize,n=e+.5*o+3;this.svg.style.width=s+5+e+"px";for(var r in this.groups)this.groups.hasOwnProperty(r)&&(this.groups[r].drawIcon(i,n,this.svgElements,this.svg,s,o),n+=o+this.options.iconSpacing);DOMutil.cleanupElements(this.svgElements)}},DataAxis.prototype=new Component,DataAxis.prototype.addGroup=function(t,e){this.groups.hasOwnProperty(t)||(this.groups[t]=e),this.amountOfGroups+=1},DataAxis.prototype.updateGroup=function(t,e){this.groups[t]=e},DataAxis.prototype.removeGroup=function(t){this.groups.hasOwnProperty(t)&&(delete this.groups[t],this.amountOfGroups-=1)},DataAxis.prototype.setOptions=function(t){if(t){var e=!1;this.options.orientation!=t.orientation&&void 0!==t.orientation&&(e=!0);var i=["orientation","showMinorLabels","showMajorLabels","icons","majorLinesOffset","minorLinesOffset","labelOffsetX","labelOffsetY","iconWidth","width","visible"];util.selectiveExtend(i,this.options,t),this.minWidth=Number((""+this.options.width).replace("px","")),1==e&&this.dom.frame&&(this.hide(),this.show())}},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,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 t,e=this.options.iconWidth,i=15,s=4,o=s+.5*i;t="left"==this.options.orientation?s:this.width-e-s;for(var n in this.groups)this.groups.hasOwnProperty(n)&&(this.groups[n].drawIcon(t,o,this.svgElements,this.svg,e,i),o+=i+s);DOMutil.cleanupElements(this.svgElements)},DataAxis.prototype.show=function(){this.dom.frame.parentNode||("left"==this.options.orientation?this.body.dom.left.appendChild(this.dom.frame):this.body.dom.right.appendChild(this.dom.frame)),this.dom.lineContainer.parentNode||this.body.dom.backgroundHorizontal.appendChild(this.dom.lineContainer)},DataAxis.prototype.hide=function(){this.dom.frame.parentNode&&this.dom.frame.parentNode.removeChild(this.dom.frame),this.dom.lineContainer.parentNode&&this.dom.lineContainer.parentNode.removeChild(this.dom.lineContainer)},DataAxis.prototype.setRange=function(t,e){this.range.start=t,this.range.end=e},DataAxis.prototype.redraw=function(){var t=!1;if(0==this.amountOfGroups)this.hide();else{this.show(),this.height=Number(this.linegraphSVG.style.height.replace("px","")),this.dom.lineContainer.style.height=this.height+"px",this.width=1==this.options.visible?Number((""+this.options.width).replace("px","")):0;var e=this.props,i=this.dom.frame;i.className="dataaxis",this._calculateCharSize();var s=this.options.orientation,o=this.options.showMinorLabels,n=this.options.showMajorLabels;e.minorLabelHeight=o?e.minorCharHeight:0,e.majorLabelHeight=n?e.majorCharHeight:0,e.minorLineWidth=this.body.dom.backgroundHorizontal.offsetWidth-this.lineOffset-this.width+2*this.options.minorLinesOffset,e.minorLineHeight=1,e.majorLineWidth=this.body.dom.backgroundHorizontal.offsetWidth-this.lineOffset-this.width+2*this.options.majorLinesOffset,e.majorLineHeight=1,"left"==s?(i.style.top="0",i.style.left="0",i.style.bottom="",i.style.width=this.width+"px",i.style.height=this.height+"px"):(i.style.top="",i.style.bottom="0",i.style.left="0",i.style.width=this.width+"px",i.style.height=this.height+"px"),t=this._redrawLabels(),1==this.options.icons&&this._redrawGroupIcons()}return t},DataAxis.prototype._redrawLabels=function(){DOMutil.prepareElements(this.DOMelements);var t=this.options.orientation,e=this.master?this.props.majorCharHeight||10:this.stepPixelsForced,i=new DataStep(this.range.start,this.range.end,e,this.dom.frame.offsetHeight);this.step=i,i.first();var s=this.dom.frame.offsetHeight/(i.marginRange/i.step+1);this.stepPixels=s;var o=this.height/s,n=0;if(0==this.master){s=this.stepPixelsForced,n=Math.round(this.height/s-o);for(var r=0;.5*n>r;r++)i.previous();o=this.height/s}this.valueAtZero=i.marginEnd;var a=0,h=1;i.next(),this.maxLabelSize=0;for(var d=0;h=0&&this._redrawLabel(d-2,i.getCurrent(),t,"yAxis major",this.props.majorCharHeight),this._redrawLine(d,t,"grid horizontal major",this.options.majorLinesOffset,this.props.majorLineWidth)):this._redrawLine(d,t,"grid horizontal minor",this.options.minorLinesOffset,this.props.minorLineWidth),i.next(),h++ +}this.conversionFactor=a/((o-1)*i.step);var c=1==this.options.icons?this.options.iconWidth+this.options.labelOffsetX+15:this.options.labelOffsetX+15;return this.maxLabelSize>this.width-c&&1==this.options.visible?(this.width=this.maxLabelSize+c,this.options.width=this.width+"px",DOMutil.cleanupElements(this.DOMelements),this.redraw(),!0):this.maxLabelSizethis.minWidth?(this.width=Math.max(this.minWidth,this.maxLabelSize+c),this.options.width=this.width+"px",DOMutil.cleanupElements(this.DOMelements),this.redraw(),!0):(DOMutil.cleanupElements(this.DOMelements),!1)},DataAxis.prototype._redrawLabel=function(t,e,i,s,o){var n=DOMutil.getDOMElement("div",this.DOMelements,this.dom.frame);n.className=s,n.innerHTML=e,"left"==i?(n.style.left="-"+this.options.labelOffsetX+"px",n.style.textAlign="right"):(n.style.right="-"+this.options.labelOffsetX+"px",n.style.textAlign="left"),n.style.top=t-.5*o+this.options.labelOffsetY+"px",e+="";var r=Math.max(this.props.majorCharWidth,this.props.minorCharWidth);this.maxLabelSize0){for(s=0;sc){e.push(m);break}e.push(m)}}else for(var u=0;ul&&m.x0){for(var p=0;pi?i:a,d=s>d?s:d):(r=!0,h=h>i?i:h,l=s>l?s:l)}1==n&&this.yAxisLeft.setRange(a,d),1==r&&this.yAxisRight.setRange(h,l)}return o=this._toggleAxisVisiblity(n,this.yAxisLeft)||o,o=this._toggleAxisVisiblity(r,this.yAxisRight)||o,1==r&&1==n?(this.yAxisLeft.drawIcons=!0,this.yAxisRight.drawIcons=!0):(this.yAxisLeft.drawIcons=!1,this.yAxisRight.drawIcons=!1),this.yAxisRight.master=!n,0==this.yAxisRight.master?(1==r&&(this.yAxisLeft.lineOffset=this.yAxisRight.width),o=this.yAxisLeft.redraw()||o,this.yAxisRight.stepPixelsForced=this.yAxisLeft.stepPixels,o=this.yAxisRight.redraw()||o):o=this.yAxisRight.redraw()||o,o},LineGraph.prototype._toggleAxisVisiblity=function(t,e){var i=!1;return 0==t?e.dom.frame.parentNode&&(e.hide(),i=!0):e.dom.frame.parentNode||(e.show(),i=!0),i},LineGraph.prototype._drawBarGraph=function(t,e){if(null!=t&&t.length>0){var i,s=.1*e.options.barChart.width,o=0,n=e.options.barChart.width;"left"==e.options.barChart.align?o-=.5*n:"right"==e.options.barChart.align&&(o+=.5*n);for(var r=0;r0&&(i=Math.min(i,Math.abs(t[r-1].x-t[r].x))),n>i&&(n=s>i?s:i),DOMutil.drawBar(t[r].x+o,t[r].y,n,e.zeroPosition-t[r].y,e.className+" bar",this.svgElements,this.svg);1==e.options.drawPoints.enabled&&this._drawPoints(t,e,this.svgElements,this.svg,o)}},LineGraph.prototype._drawLineGraph=function(t,e){if(null!=t&&t.length>0){var i,s,o=Number(this.svg.style.height.replace("px",""));if(i=DOMutil.getSVGElement("path",this.svgElements,this.svg),i.setAttributeNS(null,"class",e.className),s=1==e.options.catmullRom.enabled?this._catmullRom(t,e):this._linear(t),1==e.options.shaded.enabled){var n,r=DOMutil.getSVGElement("path",this.svgElements,this.svg);n="top"==e.options.shaded.orientation?"M"+t[0].x+",0 "+s+"L"+t[t.length-1].x+",0":"M"+t[0].x+","+o+" "+s+"L"+t[t.length-1].x+","+o,r.setAttributeNS(null,"class",e.className+" fill"),r.setAttributeNS(null,"d",n)}i.setAttributeNS(null,"d","M"+s),1==e.options.drawPoints.enabled&&this._drawPoints(t,e,this.svgElements,this.svg)}},LineGraph.prototype._drawPoints=function(t,e,i,s,o){void 0===o&&(o=0);for(var n=0;np;p+=r)i=n(t[p].x)+this.width-1,s=t[p].y,o.push({x:i,y:s}),h=h>s?s:h,d=s>d?s:d;return{min:h,max:d,data:o}},LineGraph.prototype._convertYvalues=function(t,e){var i,s,o=[],n=this.yAxisLeft,r=Number(this.svg.style.height.replace("px",""));"right"==e.options.yAxisOrientation&&(n=this.yAxisRight);for(var a=0;al;l++)e=0==l?t[0]:t[l-1],i=t[l],s=t[l+1],o=d>l+2?t[l+2]:s,n={x:(-e.x+6*i.x+s.x)*h,y:(-e.y+6*i.y+s.y)*h},r={x:(i.x+6*s.x-o.x)*h,y:(i.y+6*s.y-o.y)*h},a+="C"+n.x+","+n.y+" "+r.x+","+r.y+" "+s.x+","+s.y+" ";return a},LineGraph.prototype._catmullRom=function(t,e){var i=e.options.catmullRom.alpha;if(0==i||void 0===i)return this._catmullRomUniform(t);for(var s,o,n,r,a,h,d,l,c,p,u,m,g,f,v,y,b,_,w,x=Math.round(t[0].x)+","+Math.round(t[0].y)+" ",S=t.length,D=0;S-1>D;D++)s=0==D?t[0]:t[D-1],o=t[D],n=t[D+1],r=S>D+2?t[D+2]:n,d=Math.sqrt(Math.pow(s.x-o.x,2)+Math.pow(s.y-o.y,2)),l=Math.sqrt(Math.pow(o.x-n.x,2)+Math.pow(o.y-n.y,2)),c=Math.sqrt(Math.pow(n.x-r.x,2)+Math.pow(n.y-r.y,2)),f=Math.pow(c,i),y=Math.pow(c,2*i),v=Math.pow(l,i),b=Math.pow(l,2*i),w=Math.pow(d,i),_=Math.pow(d,2*i),p=2*_+3*w*v+b,u=2*y+3*f*v+b,m=3*w*(w+v),m>0&&(m=1/m),g=3*f*(f+v),g>0&&(g=1/g),a={x:(-b*s.x+p*o.x+_*n.x)*m,y:(-b*s.y+p*o.y+_*n.y)*m},h={x:(y*o.x+u*n.x-b*r.x)*g,y:(y*o.y+u*n.y-b*r.y)*g},0==a.x&&0==a.y&&(a=o),0==h.x&&0==h.y&&(h=n),x+="C"+a.x+","+a.y+" "+h.x+","+h.y+" "+n.x+","+n.y+" ";return x},LineGraph.prototype._linear=function(t){for(var e="",i=0;in&&(h=n);for(var d=!1,l=h;Math.abs(l)<=Math.abs(n);l++){a=Math.pow(10,l);for(var c=0;c=o){d=!0,r=c;break}}if(1==d)break}this.stepIndex=r,this.scale=a,this.step=a*this.minorSteps[r]},DataStep.prototype.first=function(){this.setFirst()},DataStep.prototype.setFirst=function(){var t=this._start-this.scale*this.minorSteps[this.stepIndex],e=this._end+this.scale*this.minorSteps[this.stepIndex];this.marginEnd=this.roundToMinor(e),this.marginStart=this.roundToMinor(t),this.marginRange=this.marginEnd-this.marginStart,this.current=this.marginEnd},DataStep.prototype.roundToMinor=function(t){var e=t-t%(this.scale*this.minorSteps[this.stepIndex]);return t%(this.scale*this.minorSteps[this.stepIndex])>.5*this.scale*this.minorSteps[this.stepIndex]?e+this.scale*this.minorSteps[this.stepIndex]:e},DataStep.prototype.hasNext=function(){return this.current>=this.marginStart},DataStep.prototype.next=function(){var t=this.current;this.current-=this.step,this.current==t&&(this.current=this._end)},DataStep.prototype.previous=function(){this.current+=this.step,this.marginEnd+=this.step,this.marginRange=this.marginEnd-this.marginStart},DataStep.prototype.getCurrent=function(){for(var t=""+Number(this.current).toPrecision(5),e=t.length-1;e>0;e--){if("0"!=t[e]){if("."==t[e]||","==t[e]){t=t.slice(0,e);break}break}t=t.slice(0,e)}return t},DataStep.prototype.snap=function(){},DataStep.prototype.isMajor=function(){return this.current%(this.scale*this.majorSteps[this.stepIndex])==0};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,o;if(i)for(s=0,o=t.length;o>s;s++)t[s].top=null;for(s=0,o=t.length;o>s;s++){var n=t[s];if(null===n.top){n.top=e.axis;do{for(var r=null,a=0,h=t.length;h>a;a++){var d=t[a];if(null!==d.top&&d!==n&&stack.collision(n,d,e.item)){r=d;break}}null!=r&&(n.top=r.top+r.height+e.item)}while(r)}}},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,o=36e5,n=6e4,r=1e3,a=1;1e3*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1e3),500*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=500),100*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=100),50*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=50),10*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=10),5*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=5),e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1),3*i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=3),i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=1),5*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*o>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=4),o>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=1),15*n>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=15),10*n>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=10),5*n>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=5),n>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=1),15*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=15),10*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=10),5*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=5),r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=1),200*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=200),100*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=100),50*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=50),10*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=10),5*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=5),a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=1)}},TimeStep.prototype.snap=function(t){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""}},Range.prototype=new Component,Range.prototype.setOptions=function(t){if(t){var e=["direction","min","max","zoomMin","zoomMax","moveable","zoomable"];util.selectiveExtend(e,this.options,t),("start"in t||"end"in t)&&this.setRange(t.start,t.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.body.emitter.emit("rangechange",s),this.body.emitter.emit("rangechanged",s)}},Range.prototype._applyRange=function(t,e){var i,s=null!=t?util.convert(t,"Date").valueOf():this.start,o=null!=e?util.convert(e,"Date").valueOf():this.end,n=null!=this.options.max?util.convert(this.options.max,"Date").valueOf():null,r=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(o)||null===o)throw new Error('Invalid end "'+e+'"');if(s>o&&(o=s),null!==r&&r>s&&(i=r-s,s+=i,o+=i,null!=n&&o>n&&(o=n)),null!==n&&o>n&&(i=o-n,s-=i,o-=i,null!=r&&r>s&&(s=r)),null!==this.options.zoomMin){var a=parseFloat(this.options.zoomMin);0>a&&(a=0),a>o-s&&(this.end-this.start===a?(s=this.start,o=this.end):(i=a-(o-s),s-=i/2,o+=i/2))}if(null!==this.options.zoomMax){var h=parseFloat(this.options.zoomMax);0>h&&(h=0),o-s>h&&(this.end-this.start===h?(s=this.start,o=this.end):(i=o-s-h,s+=i/2,o-=i/2))}var d=this.start!=s||this.end!=o;return this.start=s,this.end=o,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}},Range.prototype._onDragStart=function(){this.options.moveable&&this.props.touch.allowDragging&&(this.props.touch.start=this.start,this.props.touch.end=this.end,this.body.dom.root&&(this.body.dom.root.style.cursor="move"))},Range.prototype._onDrag=function(t){if(this.options.moveable){var e=this.options.direction;if(validateDirection(e),this.props.touch.allowDragging){var i="horizontal"==e?t.gesture.deltaX:t.gesture.deltaY,s=this.props.touch.end-this.props.touch.start,o="horizontal"==e?this.body.domProps.center.width:this.body.domProps.center.height,n=-i/o*s;this._applyRange(this.props.touch.start+n,this.props.touch.end+n),this.body.emitter.emit("rangechange",{start:new Date(this.start),end:new Date(this.end)})}}},Range.prototype._onDragEnd=function(){this.options.moveable&&this.props.touch.allowDragging&&(this.body.dom.root&&(this.body.dom.root.style.cursor="auto"),this.body.emitter.emit("rangechanged",{start:new Date(this.start),end:new Date(this.end)}))},Range.prototype._onMouseWheel=function(t){if(this.options.zoomable&&this.options.moveable){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),o=getPointer(s.center,this.body.dom.center),n=this._pointerToDate(o);this.zoom(i,n)}t.preventDefault()}},Range.prototype._onTouch=function(){this.props.touch.start=this.start,this.props.touch.end=this.end,this.props.touch.allowDragging=!0,this.props.touch.center=null},Range.prototype._onHold=function(){this.props.touch.allowDragging=!1},Range.prototype._onPinch=function(t){if(this.options.zoomable&&this.options.moveable&&(this.props.touch.allowDragging=!1,t.gesture.touches.length>1)){this.props.touch.center||(this.props.touch.center=getPointer(t.gesture.center,this.body.dom.center));var e=1/t.gesture.scale,i=this._pointerToDate(this.props.touch.center),s=parseInt(i+(this.props.touch.start-i)*e),o=parseInt(i+(this.props.touch.end-i)*e);this.setRange(s,o)}},Range.prototype._pointerToDate=function(t){var e,i=this.options.direction;if(validateDirection(i),"horizontal"==i){var s=this.body.domProps.center.width;return e=this.conversion(s),t.x/e.scale+e.offset}var o=this.body.domProps.center.height;return e=this.conversion(o),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,o=this.end-i;this.setRange(s,o)},Component.prototype.setOptions=function(t){t&&util.extend(this.options,t)},Component.prototype.redraw=function(){return!1},Component.prototype.destroy=function(){},Component.prototype._isResized=function(){var t=this.props._previousWidth!==this.props.width||this.props._previousHeight!==this.props.height;return this.props._previousWidth=this.props.width,this.props._previousHeight=this.props.height,t},TimeAxis.prototype=new Component,TimeAxis.prototype.setOptions=function(t){t&&util.selectiveExtend(["orientation","showMinorLabels","showMajorLabels"],this.options,t)},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"},TimeAxis.prototype.destroy=function(){this.dom.foreground.parentNode&&this.dom.foreground.parentNode.removeChild(this.dom.foreground),this.dom.background.parentNode&&this.dom.background.parentNode.removeChild(this.dom.background),this.body=null +},TimeAxis.prototype.redraw=function(){var t=this.options,e=this.props,i=this.dom.foreground,s=this.dom.background,o="top"==t.orientation?this.body.dom.top:this.body.dom.bottom,n=i.parentNode!==o;this._calculateCharSize();var r=(this.options.orientation,this.options.showMinorLabels),a=this.options.showMajorLabels;e.minorLabelHeight=r?e.minorCharHeight:0,e.majorLabelHeight=a?e.majorCharHeight:0,e.height=e.minorLabelHeight+e.majorLabelHeight,e.width=i.offsetWidth,e.minorLineHeight=this.body.domProps.root.height-e.majorLabelHeight-("top"==t.orientation?this.body.domProps.bottom.height:this.body.domProps.top.height),e.minorLineWidth=1,e.majorLineHeight=e.minorLineHeight+e.majorLabelHeight,e.majorLineWidth=1;var h=i.nextSibling,d=s.nextSibling;return i.parentNode&&i.parentNode.removeChild(i),s.parentNode&&s.parentNode.removeChild(s),i.style.height=this.props.height+"px",this._repaintLabels(),h?o.insertBefore(i,h):o.appendChild(i),d?this.body.dom.backgroundVertical.insertBefore(s,d):this.body.dom.backgroundVertical.appendChild(s),this._isResized()||n},TimeAxis.prototype._repaintLabels=function(){var t=this.options.orientation,e=util.convert(this.body.range.start,"Number"),i=util.convert(this.body.range.end,"Number"),s=this.body.util.toTime(7*(this.props.minorCharWidth||10)).valueOf()-this.body.util.toTime(0).valueOf(),o=new TimeStep(new Date(e),new Date(i),s);this.step=o;var n=this.dom;n.redundant.majorLines=n.majorLines,n.redundant.majorTexts=n.majorTexts,n.redundant.minorLines=n.minorLines,n.redundant.minorTexts=n.minorTexts,n.majorLines=[],n.majorTexts=[],n.minorLines=[],n.minorTexts=[],o.first();for(var r=void 0,a=0;o.hasNext()&&1e3>a;){a++;var h=o.getCurrent(),d=this.body.util.toScreen(h),l=o.isMajor();this.options.showMinorLabels&&this._repaintMinorText(d,o.getLabelMinor(),t),l&&this.options.showMajorLabels?(d>0&&(void 0==r&&(r=d),this._repaintMajorText(d,o.getLabelMajor(),t)),this._repaintMajorLine(d,t)):this._repaintMinorLine(d,t),o.next()}if(this.options.showMajorLabels){var c=this.body.util.toTime(0),p=o.getLabelMajor(c),u=p.length*(this.props.majorCharWidth||10)+10;(void 0==r||r>u)&&this._repaintMajorText(0,p,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 o=document.createTextNode("");s=document.createElement("div"),s.appendChild(o),s.className="text minor",this.dom.foreground.appendChild(s)}this.dom.minorTexts.push(s),s.childNodes[0].nodeValue=e,s.style.top="top"==i?this.props.majorLabelHeight+"px":"0",s.style.left=t+"px"},TimeAxis.prototype._repaintMajorText=function(t,e,i){var s=this.dom.redundant.majorTexts.shift();if(!s){var o=document.createTextNode(e);s=document.createElement("div"),s.className="text major",s.appendChild(o),this.dom.foreground.appendChild(s)}this.dom.majorTexts.push(s),s.childNodes[0].nodeValue=e,s.style.top="top"==i?"0":this.props.minorLabelHeight+"px",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.dom.background.appendChild(i)),this.dom.minorLines.push(i);var s=this.props;i.style.top="top"==e?s.majorLabelHeight+"px":this.body.domProps.top.height+"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.dom.background.appendChild(i)),this.dom.majorLines.push(i);var s=this.props;i.style.top="top"==e?"0":this.body.domProps.top.height+"px",i.style.left=t-s.majorLineWidth/2+"px",i.style.height=s.majorLineHeight+"px"},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.dom.foreground.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.dom.foreground.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._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.destroy=function(){this.options.showCurrentTime=!1,this.redraw(),this.body=null},CurrentTime.prototype.setOptions=function(t){t&&util.selectiveExtend(["showCurrentTime"],this.options,t)},CurrentTime.prototype.redraw=function(){if(this.options.showCurrentTime){var t=this.body.dom.backgroundVertical;this.bar.parentNode!=t&&(this.bar.parentNode&&this.bar.parentNode.removeChild(this.bar),t.appendChild(this.bar),this.start());var e=new Date,i=this.body.util.toScreen(e);this.bar.style.left=i+"px",this.bar.title="Current time: "+e}else this.bar.parentNode&&this.bar.parentNode.removeChild(this.bar),this.stop();return!1},CurrentTime.prototype.start=function(){function t(){e.stop();var i=e.body.range.conversion(e.body.domProps.center.width).scale,s=1/i/10;30>s&&(s=30),s>1e3&&(s=1e3),e.redraw(),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=function(t){t&&util.selectiveExtend(["showCustomTime"],this.options,t)},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.destroy=function(){this.options.showCustomTime=!1,this.redraw(),this.hammer.enable(!1),this.hammer=null,this.body=null},CustomTime.prototype.redraw=function(){if(this.options.showCustomTime){var t=this.body.dom.backgroundVertical;this.bar.parentNode!=t&&(this.bar.parentNode&&this.bar.parentNode.removeChild(this.bar),t.appendChild(this.bar));var e=this.body.util.toScreen(this.customTime);this.bar.style.left=e+"px",this.bar.title="Time: "+this.customTime}else this.bar.parentNode&&this.bar.parentNode.removeChild(this.bar);return!1},CustomTime.prototype.setCustomTime=function(t){this.customTime=new Date(t.valueOf()),this.redraw()},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.body.util.toScreen(this.eventParams.customTime)+e,s=this.body.util.toTime(i);this.setCustomTime(s),this.body.emitter.emit("timechange",{time:new Date(this.customTime.valueOf())}),t.stopPropagation(),t.preventDefault()}},CustomTime.prototype._onDragEnd=function(t){this.eventParams.dragging&&(this.body.emitter.emit("timechanged",{time:new Date(this.customTime.valueOf())}),t.stopPropagation(),t.preventDefault())};var UNGROUPED="__ungrouped__";ItemSet.prototype=new Component,ItemSet.types={box:ItemBox,range:ItemRange,point:ItemPoint},ItemSet.prototype._create=function(){var t=document.createElement("div");t.className="itemset",t["timeline-itemset"]=this,this.dom.frame=t;var e=document.createElement("div");e.className="background",t.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;var o=document.createElement("div");o.className="labelset",this.dom.labelSet=o,this._updateUngrouped(),this.hammer=Hammer(this.body.dom.centerContainer,{prevent_default:!0}),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)),this.hammer.on("tap",this._onSelectItem.bind(this)),this.hammer.on("hold",this._onMultiSelectItem.bind(this)),this.hammer.on("doubletap",this._onAddItem.bind(this)),this.show()},ItemSet.prototype.setOptions=function(t){if(t){var e=["type","align","orientation","padding","stack","selectable","groupOrder"];util.selectiveExtend(e,this.options,t),"margin"in t&&("number"==typeof t.margin?(this.options.margin.axis=t.margin,this.options.margin.item=t.margin):"object"==typeof t.margin&&util.selectiveExtend(["axis","item"],this.options.margin,t.margin)),"editable"in t&&("boolean"==typeof t.editable?(this.options.editable.updateTime=t.editable,this.options.editable.updateGroup=t.editable,this.options.editable.add=t.editable,this.options.editable.remove=t.editable):"object"==typeof t.editable&&util.selectiveExtend(["updateTime","updateGroup","add","remove"],this.options.editable,t.editable));var i=function(e){if(e in t){var i=t[e];if(!(i instanceof Function)||2!=i.length)throw new Error("option "+e+" must be a function "+e+"(item, callback)");this.options[e]=i}}.bind(this);["onAdd","onUpdate","onRemove","onMove"].forEach(i),this.markDirty()}},ItemSet.prototype.markDirty=function(){this.groupIds=[],this.stackDirty=!0},ItemSet.prototype.destroy=function(){this.hide(),this.setItems(null),this.setGroups(null),this.hammer=null,this.body=null,this.conversion=null},ItemSet.prototype.hide=function(){this.dom.frame.parentNode&&this.dom.frame.parentNode.removeChild(this.dom.frame),this.dom.axis.parentNode&&this.dom.axis.parentNode.removeChild(this.dom.axis),this.dom.labelSet.parentNode&&this.dom.labelSet.parentNode.removeChild(this.dom.labelSet)},ItemSet.prototype.show=function(){this.dom.frame.parentNode||this.body.dom.center.appendChild(this.dom.frame),this.dom.axis.parentNode||this.body.dom.backgroundVertical.appendChild(this.dom.axis),this.dom.labelSet.parentNode||this.body.dom.left.appendChild(this.dom.labelSet)},ItemSet.prototype.setSelection=function(t){var e,i,s,o;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],o=this.items[s],o&&o.unselect();for(this.selection=[],e=0,i=t.length;i>e;e++)s=t[e],o=this.items[s],o&&(this.selection.push(s),o.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.redraw=function(){var t=this.options.margin,e=this.body.range,i=util.option.asSize,s=this.options,o=s.orientation,n=!1,r=this.dom.frame,a=s.editable.updateTime||s.editable.updateGroup;r.className="itemset"+(a?" editable":""),n=this._orderGroups()||n;var h=e.end-e.start,d=h!=this.lastVisibleInterval||this.props.width!=this.props.lastWidth;d&&(this.stackDirty=!0),this.lastVisibleInterval=h,this.props.lastWidth=this.props.width;var l=this.stackDirty,c=this._firstGroup(),p={item:t.item,axis:t.axis},u={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?p:u,s=t.redraw(e,i,l);n=s||n,m+=t.height}),m=Math.max(m,g),this.stackDirty=!1,r.style.height=i(m),this.props.top=r.offsetTop,this.props.left=r.offsetLeft,this.props.width=r.offsetWidth,this.props.height=m,this.dom.axis.style.top=i("top"==o?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",n=this._isResized()||n},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.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.off(e,t)}),e=s.getIds(),this._onRemove(e)),this.itemsData){var o=this.id;util.forEach(this.itemListeners,function(t,e){i.itemsData.on(e,t,o)}),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.body.emitter.emit("change")},ItemSet.prototype.getGroups=function(){return this.groupsData},ItemSet.prototype.removeItem=function(t){var e=this.itemsData.get(t),i=this.itemsData.getDataSet();e&&this.options.onRemove(e,function(e){e&&i.remove(t)})},ItemSet.prototype._onUpdate=function(t){var e=this;t.forEach(function(t){var i=e.itemsData.get(t,e.itemOptions),s=e.items[t],o=i.type||e.options.type||(i.end?"range":"box"),n=ItemSet.types[o];if(s&&(n&&s instanceof n?e._updateItem(s,i):(e._removeItem(s),s=null)),!s){if(!n)throw new TypeError("rangeoverflow"==o?'Item type "rangeoverflow" is deprecated. Use css styling instead: .vis.timeline .item.range .content {overflow: visible;}':'Unknown item type "'+o+'"');s=new n(i,e.conversion,e.options),s.id=t,e._addItem(s)}}),this._order(),this.stackDirty=!0,this.body.emitter.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.body.emitter.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 o=Object.create(e.options);util.extend(o,{height:null}),s=new Group(t,i,e),e.groups[t]=s;for(var n in e.items)if(e.items.hasOwnProperty(n)){var r=e.items[n];r.data.group==t&&s.add(r)}s.order(),s.show()}}),this.body.emitter.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.body.emitter.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.redraw(),i!=t.data.group){var s=this.groups[i];s&&s.remove(t);var o=this.groupsData?t.data.group:UNGROUPED,n=this.groups[o];n&&n.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;i0||s.length>0)&&this.body.emitter.emit("select",{items:this.getSelection()}),t.stopPropagation()}},ItemSet.prototype._onAddItem=function(t){if(this.options.selectable&&this.options.editable.add){var e=this,i=this.body.util.snap||null,s=ItemSet.itemFromTarget(t);if(s){var o=e.itemsData.get(s.id);this.options.onUpdate(o,function(t){t&&e.itemsData.update(t)})}else{var n=vis.util.getAbsoluteLeft(this.dom.frame),r=t.gesture.center.pageX-n,a=this.body.util.toTime(r),h={start:i?i(a):a,content:"new item"};if("range"===this.options.type){var d=this.body.util.toTime(r+this.props.width/5);h.end=i?i(d):d}h[this.itemsData.fieldId]=util.randomUUID();var l=ItemSet.groupFromTarget(t);l&&(h.group=l.groupId),this.options.onAdd(h,function(t){t&&e.itemsData.add(h)})}}},ItemSet.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.body.emitter.emit("select",{items:this.getSelection()}),t.stopPropagation()}}},ItemSet.itemFromTarget=function(t){for(var e=t.target;e;){if(e.hasOwnProperty("timeline-item"))return e["timeline-item"];e=e.parentNode}return null},ItemSet.groupFromTarget=function(t){for(var e=t.target;e;){if(e.hasOwnProperty("timeline-group"))return e["timeline-group"];e=e.parentNode}return null},ItemSet.itemSetFromTarget=function(t){for(var e=t.target;e;){if(e.hasOwnProperty("timeline-itemset"))return e["timeline-itemset"];e=e.parentNode}return null},Item.prototype.select=function(){this.selected=!0,this.displayed&&this.redraw()},Item.prototype.unselect=function(){this.selected=!1,this.displayed&&this.redraw()},Item.prototype.setParent=function(t){this.displayed?(this.hide(),this.parent=t,this.parent&&this.show()):this.parent=t},Item.prototype.isVisible=function(){return!1},Item.prototype.show=function(){return!1},Item.prototype.hide=function(){return!1},Item.prototype.redraw=function(){},Item.prototype.repositionX=function(){},Item.prototype.repositionY=function(){},Item.prototype._repaintDeleteButton=function(t){if(this.selected&&this.options.editable.remove&&!this.dom.deleteButton){var e=this,i=document.createElement("div");i.className="delete",i.title="Delete this item",Hammer(i,{preventDefault:!0}).on("tap",function(t){e.parent.removeFromDataSet(e),t.stopPropagation()}),t.appendChild(i),this.dom.deleteButton=i}else!this.selected&&this.dom.deleteButton&&(this.dom.deleteButton.parentNode&&this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton),this.dom.deleteButton=null)},ItemBox.prototype=new Item(null,null,null),ItemBox.prototype.isVisible=function(t){var e=(t.end-t.start)/4;return this.data.start>t.start-e&&this.data.startt.start-e&&this.data.startt.start},ItemRange.prototype.redraw=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 redraw item: no parent attached");if(!t.box.parentNode){var e=this.parent.dom.foreground;if(!e)throw new Error("Cannot redraw 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}this.data.title!=this.title&&(t.box.title=this.data.title,this.title=this.data.title);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.overflow="hidden"!==window.getComputedStyle(t.content).overflow,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.redraw()},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.conversion.toScreen(this.data.start),o=this.conversion.toScreen(this.data.end),n=this.options.padding;-i>s&&(s=-i),o>2*i&&(o=2*i);var r=Math.max(o-s,1);this.overflow?(t=Math.max(-s,0),this.left=s,this.width=r+this.props.content.width):(t=0>s?Math.min(-s,o-s-e.content.width-2*n):0,this.left=s,this.width=r),this.dom.box.style.left=this.left+"px",this.dom.box.style.width=r+"px",this.dom.content.style.left=t+"px"},ItemRange.prototype.repositionY=function(){var t=this.options.orientation,e=this.dom.box;e.style.top="top"==t?this.top+"px":this.parent.height-this.top-this.height+"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)},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.background.className="group",this.dom.axis=document.createElement("div"),this.dom.axis.className="group",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,this.dom.label.title=t&&t.title||"",this.dom.inner.firstChild?util.removeClassName(this.dom.inner,"hidden"):util.addClassName(this.dom.inner,"hidden");var i=t&&t.className||null;i!=this.className&&(this.className&&(util.removeClassName(this.dom.label,i),util.removeClassName(this.dom.foreground,i),util.removeClassName(this.dom.background,i),util.removeClassName(this.dom.axis,i)),util.addClassName(this.dom.label,i),util.addClassName(this.dom.foreground,i),util.addClassName(this.dom.background,i),util.addClassName(this.dom.axis,i))},Group.prototype.getLabelWidth=function(){return this.props.label.width},Group.prototype.redraw=function(t,e,i){var s=!1;this.visibleItems=this._updateVisibleItems(this.orderedItems,this.visibleItems,t);var o=this.dom.marker.clientHeight;o!=this.lastMarkerHeight&&(this.lastMarkerHeight=o,util.forEach(this.items,function(t){t.dirty=!0,t.displayed&&t.redraw()}),i=!0),this.itemSet.options.stack?stack.stack(this.visibleItems,e,i):stack.nostack(this.visibleItems,e);var n,r=this.visibleItems;if(r.length){var a=r[0].top,h=r[0].top+r[0].height;if(util.forEach(r,function(t){a=Math.min(a,t.top),h=Math.max(h,t.top+t.height)}),a>e.axis){var d=a-e.axis;h-=d,util.forEach(r,function(t){t.top-=d})}n=h+e.item/2}else n=e.axis+e.item;n=Math.max(n,this.props.label.height);var l=this.dom.foreground;this.top=l.offsetTop,this.left=l.offsetLeft,this.width=l.offsetWidth,s=util.updateProperty(this,"height",n)||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,this.dom.background.style.height=n+"px",this.dom.foreground.style.height=n+"px",this.dom.label.style.height=n+"px";for(var c=0,p=this.visibleItems.length;p>c;c++){var u=this.visibleItems[c];u.repositionY()}return s},Group.prototype.show=function(){this.dom.label.parentNode||this.itemSet.dom.labelSet.appendChild(this.dom.label),this.dom.foreground.parentNode||this.itemSet.dom.foreground.appendChild(this.dom.foreground),this.dom.background.parentNode||this.itemSet.dom.background.appendChild(this.dom.background),this.dom.axis.parentNode||this.itemSet.dom.axis.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.body.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(o=0;o=0&&!this._checkIfInvisible(t.byStart[o],n,i);o--);for(o=s+1;o=0&&!this._checkIfInvisible(t.byEnd[o],n,i);o--);for(o=r+1;o=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.getDataSet(),e=null,i=null;if(t){var s=t.min("start");e=s?util.convert(s.start,"Date").valueOf():null;var o=t.max("start");o&&(i=util.convert(o.start,"Date").valueOf());var n=t.max("end");n&&(i=null==i?util.convert(n.end,"Date").valueOf():Math.max(i,util.convert(n.end,"Date").valueOf()))}return{min:null!=e?new Date(e):null,max:null!=i?new Date(i):null}},Timeline.prototype.setSelection=function(t){this.itemSet&&this.itemSet.setSelection(t)},Timeline.prototype.getSelection=function(){return this.itemSet&&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(){var t=!1,e=this.options,i=this.props,s=this.dom;if(s){s.root.className="vis timeline root "+e.orientation,s.root.style.maxHeight=util.option.asSize(e.maxHeight,""),s.root.style.minHeight=util.option.asSize(e.minHeight,""),s.root.style.width=util.option.asSize(e.width,""),i.border.left=(s.centerContainer.offsetWidth-s.centerContainer.clientWidth)/2,i.border.right=i.border.left,i.border.top=(s.centerContainer.offsetHeight-s.centerContainer.clientHeight)/2,i.border.bottom=i.border.top;var o=s.root.offsetHeight-s.root.clientHeight,n=s.root.offsetWidth-s.root.clientWidth;i.center.height=s.center.offsetHeight,i.left.height=s.left.offsetHeight,i.right.height=s.right.offsetHeight,i.top.height=s.top.clientHeight||-i.border.top,i.bottom.height=s.bottom.clientHeight||-i.border.bottom;var r=Math.max(i.left.height,i.center.height,i.right.height),a=i.top.height+r+i.bottom.height+o+i.border.top+i.border.bottom;s.root.style.height=util.option.asSize(e.height,a+"px"),i.root.height=s.root.offsetHeight,i.background.height=i.root.height-o;var h=i.root.height-i.top.height-i.bottom.height-o;i.centerContainer.height=h,i.leftContainer.height=h,i.rightContainer.height=i.leftContainer.height,i.root.width=s.root.offsetWidth,i.background.width=i.root.width-n,i.left.width=s.leftContainer.clientWidth||-i.border.left,i.leftContainer.width=i.left.width,i.right.width=s.rightContainer.clientWidth||-i.border.right,i.rightContainer.width=i.right.width;var d=i.root.width-i.left.width-i.right.width-n;i.center.width=d,i.centerContainer.width=d,i.top.width=d,i.bottom.width=d,s.background.style.height=i.background.height+"px",s.backgroundVertical.style.height=i.background.height+"px",s.backgroundHorizontal.style.height=i.centerContainer.height+"px",s.centerContainer.style.height=i.centerContainer.height+"px",s.leftContainer.style.height=i.leftContainer.height+"px",s.rightContainer.style.height=i.rightContainer.height+"px",s.background.style.width=i.background.width+"px",s.backgroundVertical.style.width=i.centerContainer.width+"px",s.backgroundHorizontal.style.width=i.background.width+"px",s.centerContainer.style.width=i.center.width+"px",s.top.style.width=i.top.width+"px",s.bottom.style.width=i.bottom.width+"px",s.background.style.left="0",s.background.style.top="0",s.backgroundVertical.style.left=i.left.width+"px",s.backgroundVertical.style.top="0",s.backgroundHorizontal.style.left="0",s.backgroundHorizontal.style.top=i.top.height+"px",s.centerContainer.style.left=i.left.width+"px",s.centerContainer.style.top=i.top.height+"px",s.leftContainer.style.left="0",s.leftContainer.style.top=i.top.height+"px",s.rightContainer.style.left=i.left.width+i.center.width+"px",s.rightContainer.style.top=i.top.height+"px",s.top.style.left=i.left.width+"px",s.top.style.top="0",s.bottom.style.left=i.left.width+"px",s.bottom.style.top=i.top.height+i.centerContainer.height+"px",this._updateScrollTop();var l=this.props.scrollTop;"bottom"==e.orientation&&(l+=Math.max(this.props.centerContainer.height-this.props.center.height-this.props.border.top-this.props.border.bottom,0)),s.center.style.left="0",s.center.style.top=l+"px",s.left.style.left="0",s.left.style.top=l+"px",s.right.style.left="0",s.right.style.top=l+"px";var c=0==this.props.scrollTop?"hidden":"",p=this.props.scrollTop==this.props.scrollTopMin?"hidden":"";s.shadowTop.style.visibility=c,s.shadowBottom.style.visibility=p,s.shadowTopLeft.style.visibility=c,s.shadowBottomLeft.style.visibility=p,s.shadowTopRight.style.visibility=c,s.shadowBottomRight.style.visibility=p,this.components.forEach(function(e){t=e.redraw()||t}),t&&this.redraw()}},Timeline.prototype.repaint=function(){throw new Error("Function repaint is deprecated. Use redraw instead.")},Timeline.prototype._toTime=function(t){var e=this.range.conversion(this.props.center.width);return new Date(t/e.scale+e.offset)},Timeline.prototype._toGlobalTime=function(t){var e=this.range.conversion(this.props.root.width);return new Date(t/e.scale+e.offset)},Timeline.prototype._toScreen=function(t){var e=this.range.conversion(this.props.center.width);return(t.valueOf()-e.offset)*e.scale},Timeline.prototype._toGlobalScreen=function(t){var e=this.range.conversion(this.props.root.width);return(t.valueOf()-e.offset)*e.scale},Timeline.prototype._initAutoResize=function(){1==this.options.autoResize?this._startAutoResize():this._stopAutoResize()},Timeline.prototype._startAutoResize=function(){var t=this;this._stopAutoResize(),this._onResize=function(){return 1!=t.options.autoResize?void t._stopAutoResize():void(t.dom.root&&(t.dom.root.clientWidth!=t.props.lastWidth||t.dom.root.clientHeight!=t.props.lastHeight)&&(t.props.lastWidth=t.dom.root.clientWidth,t.props.lastHeight=t.dom.root.clientHeight,t.emit("change")))},util.addEventListener(window,"resize",this._onResize),this.watchTimer=setInterval(this._onResize,1e3)},Timeline.prototype._stopAutoResize=function(){this.watchTimer&&(clearInterval(this.watchTimer),this.watchTimer=void 0),util.removeEventListener(window,"resize",this._onResize),this._onResize=null},Timeline.prototype._onTouch=function(){this.touch.allowDragging=!0},Timeline.prototype._onPinch=function(){this.touch.allowDragging=!1},Timeline.prototype._onDragStart=function(){this.touch.initialScrollTop=this.props.scrollTop},Timeline.prototype._onDrag=function(t){if(this.touch.allowDragging){var e=t.gesture.deltaY,i=this._getScrollTop(),s=this._setScrollTop(this.touch.initialScrollTop+e);s!=i&&this.redraw()}},Timeline.prototype._setScrollTop=function(t){return this.props.scrollTop=t,this._updateScrollTop(),this.props.scrollTop},Timeline.prototype._updateScrollTop=function(){var t=Math.min(this.props.centerContainer.height-this.props.center.height,0);return t!=this.props.scrollTopMin&&("bottom"==this.options.orientation&&(this.props.scrollTop+=t-this.props.scrollTopMin),this.props.scrollTopMin=t),this.props.scrollTop>0&&(this.props.scrollTop=0),this.props.scrollTop=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)},Graph2d.prototype.getItemRange=function(){var t=this.itemsData,e=null,i=null;if(t){var s=t.min("start");e=s?util.convert(s.start,"Date").valueOf():null;var o=t.max("start");o&&(i=util.convert(o.start,"Date").valueOf());var n=t.max("end");n&&(i=null==i?util.convert(n.end,"Date").valueOf():Math.max(i,util.convert(n.end,"Date").valueOf()))}return{min:null!=e?new Date(e):null,max:null!=i?new Date(i):null}},Graph2d.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)},Graph2d.prototype.getWindow=function(){var t=this.range.getRange();return{start:new Date(t.start),end:new Date(t.end)}},Graph2d.prototype.redraw=function(){var t=!1,e=this.options,i=this.props,s=this.dom;if(s){s.root.className="vis timeline root "+e.orientation,s.root.style.maxHeight=util.option.asSize(e.maxHeight,""),s.root.style.minHeight=util.option.asSize(e.minHeight,""),s.root.style.width=util.option.asSize(e.width,""),i.border.left=(s.centerContainer.offsetWidth-s.centerContainer.clientWidth)/2,i.border.right=i.border.left,i.border.top=(s.centerContainer.offsetHeight-s.centerContainer.clientHeight)/2,i.border.bottom=i.border.top;var o=s.root.offsetHeight-s.root.clientHeight,n=s.root.offsetWidth-s.root.clientWidth;i.center.height=s.center.offsetHeight,i.left.height=s.left.offsetHeight,i.right.height=s.right.offsetHeight,i.top.height=s.top.clientHeight||-i.border.top,i.bottom.height=s.bottom.clientHeight||-i.border.bottom;var r=Math.max(i.left.height,i.center.height,i.right.height),a=i.top.height+r+i.bottom.height+o+i.border.top+i.border.bottom;s.root.style.height=util.option.asSize(e.height,a+"px"),i.root.height=s.root.offsetHeight,i.background.height=i.root.height-o;var h=i.root.height-i.top.height-i.bottom.height-o;i.centerContainer.height=h,i.leftContainer.height=h,i.rightContainer.height=i.leftContainer.height,i.root.width=s.root.offsetWidth,i.background.width=i.root.width-n,i.left.width=s.leftContainer.clientWidth||-i.border.left,i.leftContainer.width=i.left.width,i.right.width=s.rightContainer.clientWidth||-i.border.right,i.rightContainer.width=i.right.width;var d=i.root.width-i.left.width-i.right.width-n;i.center.width=d,i.centerContainer.width=d,i.top.width=d,i.bottom.width=d,s.background.style.height=i.background.height+"px",s.backgroundVertical.style.height=i.background.height+"px",s.backgroundHorizontalContainer.style.height=i.centerContainer.height+"px",s.centerContainer.style.height=i.centerContainer.height+"px",s.leftContainer.style.height=i.leftContainer.height+"px",s.rightContainer.style.height=i.rightContainer.height+"px",s.background.style.width=i.background.width+"px",s.backgroundVertical.style.width=i.centerContainer.width+"px",s.backgroundHorizontalContainer.style.width=i.background.width+"px",s.backgroundHorizontal.style.width=i.background.width+"px",s.centerContainer.style.width=i.center.width+"px",s.top.style.width=i.top.width+"px",s.bottom.style.width=i.bottom.width+"px",s.background.style.left="0",s.background.style.top="0",s.backgroundVertical.style.left=i.left.width+"px",s.backgroundVertical.style.top="0",s.backgroundHorizontalContainer.style.left="0",s.backgroundHorizontalContainer.style.top=i.top.height+"px",s.centerContainer.style.left=i.left.width+"px",s.centerContainer.style.top=i.top.height+"px",s.leftContainer.style.left="0",s.leftContainer.style.top=i.top.height+"px",s.rightContainer.style.left=i.left.width+i.center.width+"px",s.rightContainer.style.top=i.top.height+"px",s.top.style.left=i.left.width+"px",s.top.style.top="0",s.bottom.style.left=i.left.width+"px",s.bottom.style.top=i.top.height+i.centerContainer.height+"px",this._updateScrollTop();var l=this.props.scrollTop;"bottom"==e.orientation&&(l+=Math.max(this.props.centerContainer.height-this.props.center.height-this.props.border.top-this.props.border.bottom,0)),s.center.style.left="0",s.center.style.top=l+"px",s.backgroundHorizontal.style.left="0",s.backgroundHorizontal.style.top=l+"px",s.left.style.left="0",s.left.style.top=l+"px",s.right.style.left="0",s.right.style.top=l+"px";var c=0==this.props.scrollTop?"hidden":"",p=this.props.scrollTop==this.props.scrollTopMin?"hidden":"";s.shadowTop.style.visibility=c,s.shadowBottom.style.visibility=p,s.shadowTopLeft.style.visibility=c,s.shadowBottomLeft.style.visibility=p,s.shadowTopRight.style.visibility=c,s.shadowBottomRight.style.visibility=p,this.components.forEach(function(e){t=e.redraw()||t}),t&&this.redraw()}},Graph2d.prototype._toTime=function(t){var e=this.range.conversion(this.props.center.width);return new Date(t/e.scale+e.offset)},Graph2d.prototype._toGlobalTime=function(t){var e=this.range.conversion(this.props.root.width);return new Date(t/e.scale+e.offset)},Graph2d.prototype._toScreen=function(t){var e=this.range.conversion(this.props.center.width);return(t.valueOf()-e.offset)*e.scale},Graph2d.prototype._toGlobalScreen=function(t){var e=this.range.conversion(this.props.root.width);return(t.valueOf()-e.offset)*e.scale},Graph2d.prototype._initAutoResize=function(){1==this.options.autoResize?this._startAutoResize():this._stopAutoResize()},Graph2d.prototype._startAutoResize=function(){var t=this;this._stopAutoResize(),this._onResize=function(){return 1!=t.options.autoResize?void t._stopAutoResize():void(t.dom.root&&(t.dom.root.clientWidth!=t.props.lastWidth||t.dom.root.clientHeight!=t.props.lastHeight)&&(t.props.lastWidth=t.dom.root.clientWidth,t.props.lastHeight=t.dom.root.clientHeight,t.emit("change")))},util.addEventListener(window,"resize",this._onResize),this.watchTimer=setInterval(this._onResize,1e3)},Graph2d.prototype._stopAutoResize=function(){this.watchTimer&&(clearInterval(this.watchTimer),this.watchTimer=void 0),util.removeEventListener(window,"resize",this._onResize),this._onResize=null},Graph2d.prototype._onTouch=function(){this.touch.allowDragging=!0},Graph2d.prototype._onPinch=function(){this.touch.allowDragging=!1},Graph2d.prototype._onDragStart=function(){this.touch.initialScrollTop=this.props.scrollTop},Graph2d.prototype._onDrag=function(t){if(this.touch.allowDragging){var e=t.gesture.deltaY,i=this._getScrollTop(),s=this._setScrollTop(this.touch.initialScrollTop+e);s!=i&&this.redraw()}},Graph2d.prototype._setScrollTop=function(t){return this.props.scrollTop=t,this._updateScrollTop(),this.props.scrollTop},Graph2d.prototype._updateScrollTop=function(){var t=Math.min(this.props.centerContainer.height-this.props.center.height,0);return t!=this.props.scrollTopMin&&("bottom"==this.options.orientation&&(this.props.scrollTop+=t-this.props.scrollTopMin),this.props.scrollTopMin=t),this.props.scrollTop>0&&(this.props.scrollTop=0),this.props.scrollTopi;i++)if(e.id===a.nodes[i].id){o=a.nodes[i];break}for(o||(o={id:e.id},t.node&&(o.attr=r(o.attr,t.node))),i=n.length-1;i>=0;i--){var h=n[i];h.nodes||(h.nodes=[]),-1==h.nodes.indexOf(o)&&h.nodes.push(o)}e.attr&&(o.attr=r(o.attr,e.attr))}function d(t,e){if(t.edges||(t.edges=[]),t.edges.push(e),t.edge){var i=r({},t.edge);e.attr=r(i,e.attr)}}function l(t,e,i,s,o){var n={from:e,to:i,type:s};return t.edge&&(n.attr=r({},t.edge)),n.attr=r(n.attr||{},o),n}function c(){for(O=D.NULL,N="";" "==C||" "==C||"\n"==C||"\r"==C;)s();do{var t=!1;if("#"==C){for(var e=M-1;" "==T.charAt(e)||" "==T.charAt(e);)e--;if("\n"==T.charAt(e)||""==T.charAt(e)){for(;""!=C&&"\n"!=C;)s();t=!0}}if("/"==C&&"/"==o()){for(;""!=C&&"\n"!=C;)s();t=!0}if("/"==C&&"*"==o()){for(;""!=C;){if("*"==C&&"/"==o()){s(),s();break}s()}t=!0}for(;" "==C||" "==C||"\n"==C||"\r"==C;)s()}while(t);if(""==C)return void(O=D.DELIMITER);var i=C+o();if(E[i])return O=D.DELIMITER,N=i,s(),void s();if(E[C])return O=D.DELIMITER,N=C,void s();if(n(C)||"-"==C){for(N+=C,s();n(C);)N+=C,s();return"false"==N?N=!1:"true"==N?N=!0:isNaN(Number(N))||(N=Number(N)),void(O=D.IDENTIFIER)}if('"'==C){for(s();""!=C&&('"'!=C||'"'==C&&'"'==o());)N+=C,'"'==C&&s(),s();if('"'!=C)throw _('End of string " expected');return s(),void(O=D.IDENTIFIER)}for(O=D.UNKNOWN;""!=C;)N+=C,s();throw new SyntaxError('Syntax error in part "'+w(N,30)+'"')}function p(){var t={};if(i(),c(),"strict"==N&&(t.strict=!0,c()),("graph"==N||"digraph"==N)&&(t.type=N,c()),O==D.IDENTIFIER&&(t.id=N,c()),"{"!=N)throw _("Angle bracket { expected");if(c(),u(t),"}"!=N)throw _("Angle bracket } expected");if(c(),""!==N)throw _("End of file expected");return c(),delete t.node,delete t.edge,delete t.graph,t +}function u(t){for(;""!==N&&"}"!=N;)m(t),";"==N&&c()}function m(t){var e=g(t);if(e)return void y(t,e);var i=f(t);if(!i){if(O!=D.IDENTIFIER)throw _("Identifier expected");var s=N;if(c(),"="==N){if(c(),O!=D.IDENTIFIER)throw _("Identifier expected");t[s]=N,c()}else v(t,s)}}function g(t){var e=null;if("subgraph"==N&&(e={},e.type="subgraph",c(),O==D.IDENTIFIER&&(e.id=N,c())),"{"==N){if(c(),e||(e={}),e.parent=t,e.node=t.node,e.edge=t.edge,e.graph=t.graph,u(e),"}"!=N)throw _("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"==N?(c(),t.node=b(),"node"):"edge"==N?(c(),t.edge=b(),"edge"):"graph"==N?(c(),t.graph=b(),"graph"):null}function v(t,e){var i={id:e},s=b();s&&(i.attr=s),h(t,i),y(t,e)}function y(t,e){for(;"->"==N||"--"==N;){var i,s=N;c();var o=g(t);if(o)i=o;else{if(O!=D.IDENTIFIER)throw _("Identifier or subgraph expected");i=N,h(t,{id:i}),c()}var n=b(),r=l(t,e,i,s,n);d(t,r),e=i}}function b(){for(var t=null;"["==N;){for(c(),t={};""!==N&&"]"!=N;){if(O!=D.IDENTIFIER)throw _("Attribute name expected");var e=N;if(c(),"="!=N)throw _("Equal sign = expected");if(c(),O!=D.IDENTIFIER)throw _("Attribute value expected");var i=N;a(t,e,i),c(),","==N&&c()}if("]"!=N)throw _("Bracket ] expected");c()}return t}function _(t){return new SyntaxError(t+', got "'+w(N,30)+'" (char '+M+")")}function w(t,e){return t.length<=e?t:t.substr(0,27)+"..."}function x(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 r(e,t.attr),e.style="->"==t.type?"arrow":"line",e}var s=e(t),o={nodes:[],edges:[],options:{}};return s.nodes&&s.nodes.forEach(function(t){var e={id:t.id,label:String(t.label||t.id)};r(e,t.attr),e.image&&(e.shape="image"),o.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);o.edges.push(e)}),x(e,s,function(e,s){var n=l(o,e.id,s.id,t.type,t.attr),r=i(n);o.edges.push(r)}),t.to instanceof Object&&t.to.edges&&t.to.edges.forEach(function(t){var e=i(t);o.edges.push(e)})}),s.attr&&(o.options=s.attr),o}var D={NULL:0,DELIMITER:1,IDENTIFIER:2,UNKNOWN:3},E={"{":!0,"}":!0,"[":!0,"]":!0,";":!0,"=":!0,",":!0,"->":!0,"--":!0},T="",M=0,C="",N="",O=D.NULL,k=/[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,o=s/2,n=Math.sqrt(3)/6*s,r=Math.sqrt(s*s-o*o);this.moveTo(t,e-(r-n)),this.lineTo(t+o,e+n),this.lineTo(t-o,e+n),this.lineTo(t,e-(r-n)),this.closePath()},CanvasRenderingContext2D.prototype.triangleDown=function(t,e,i){this.beginPath();var s=2*i,o=s/2,n=Math.sqrt(3)/6*s,r=Math.sqrt(s*s-o*o);this.moveTo(t,e+(r-n)),this.lineTo(t+o,e-n),this.lineTo(t-o,e-n),this.lineTo(t,e+(r-n)),this.closePath()},CanvasRenderingContext2D.prototype.star=function(t,e,i){this.beginPath();for(var s=0;10>s;s++){var o=s%2===0?1.3*i:.5*i;this.lineTo(t+o*Math.sin(2*s*Math.PI/10),e-o*Math.cos(2*s*Math.PI/10))}this.closePath()},CanvasRenderingContext2D.prototype.roundRect=function(t,e,i,s,o){var n=Math.PI/180;0>i-2*o&&(o=i/2),0>s-2*o&&(o=s/2),this.beginPath(),this.moveTo(t+o,e),this.lineTo(t+i-o,e),this.arc(t+i-o,e+o,o,270*n,360*n,!1),this.lineTo(t+i,e+s-o),this.arc(t+i-o,e+s-o,o,0,90*n,!1),this.lineTo(t+o,e+s),this.arc(t+o,e+s-o,o,90*n,180*n,!1),this.lineTo(t,e+o),this.arc(t+o,e+o,o,180*n,270*n,!1)},CanvasRenderingContext2D.prototype.ellipse=function(t,e,i,s){var o=.5522848,n=i/2*o,r=s/2*o,a=t+i,h=e+s,d=t+i/2,l=e+s/2;this.beginPath(),this.moveTo(t,l),this.bezierCurveTo(t,l-r,d-n,e,d,e),this.bezierCurveTo(d+n,e,a,l-r,a,l),this.bezierCurveTo(a,l+r,d+n,h,d,h),this.bezierCurveTo(d-n,h,t,l+r,t,l)},CanvasRenderingContext2D.prototype.database=function(t,e,i,s){var o=1/3,n=i,r=s*o,a=.5522848,h=n/2*a,d=r/2*a,l=t+n,c=e+r,p=t+n/2,u=e+r/2,m=e+(s-r/2),g=e+s;this.beginPath(),this.moveTo(l,u),this.bezierCurveTo(l,u+d,p+h,c,p,c),this.bezierCurveTo(p-h,c,t,u+d,t,u),this.bezierCurveTo(t,u-d,p-h,e,p,e),this.bezierCurveTo(p+h,e,l,u-d,l,u),this.lineTo(l,m),this.bezierCurveTo(l,m+d,p+h,g,p,g),this.bezierCurveTo(p-h,g,t,m+d,t,m),this.lineTo(t,u)},CanvasRenderingContext2D.prototype.arrow=function(t,e,i,s){var o=t-s*Math.cos(i),n=e-s*Math.sin(i),r=t-.9*s*Math.cos(i),a=e-.9*s*Math.sin(i),h=o+s/3*Math.cos(i+.5*Math.PI),d=n+s/3*Math.sin(i+.5*Math.PI),l=o+s/3*Math.cos(i-.5*Math.PI),c=n+s/3*Math.sin(i-.5*Math.PI);this.beginPath(),this.moveTo(t,e),this.lineTo(h,d),this.lineTo(r,a),this.lineTo(l,c),this.closePath()},CanvasRenderingContext2D.prototype.dashedLine=function(t,e,i,s,o){o||(o=[10,5]),0==p&&(p=.001);var n=o.length;this.moveTo(t,e);for(var r=i-t,a=s-e,h=a/r,d=Math.sqrt(r*r+a*a),l=0,c=!0;d>=.1;){var p=o[l++%n];p>d&&(p=d);var u=Math.sqrt(p*p/(1+h*h));0>r&&(u=-u),t+=u,e+=h*u,this[c?"lineTo":"moveTo"](t,e),d-=p,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,o=this.height/2,n=Math.sin(e)*s,r=Math.cos(e)*o;return s*o/Math.sqrt(n*n+r*r);case"box":case"image":case"text":default:return this.width?Math.min(Math.abs(this.width/2/Math.cos(e)),Math.abs(this.height/2/Math.sin(e)))+i:0}},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,o=(this.fy-s)/this.mass;this.vy+=o*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 o=this.damping*this.vy,n=(this.fy-o)/this.mass;this.vy+=n*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.networkScaleInv,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.networkScaleInv,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.networkScaleInv,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.networkScaleInv,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.networkScaleInv,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.networkScaleInv,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.networkScaleInv,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.networkScaleInv,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.networkScaleInv,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,o=2;switch(e){case"dot":o=2;break;case"square":o=2;break;case"triangle":o=3;break;case"triangleDown":o=3;break;case"star":o=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.networkScaleInv,t.lineWidth=Math.min(.1*this.width,t.lineWidth),t[e](this.x,this.y,this.radius+o*t.lineWidth),t.stroke()),t.lineWidth=(this.selected?s:1)+(this.clusterSize>1?i:0),t.lineWidth*=this.networkScaleInv,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",!0)},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,o,n,r){if(e&&this.fontSize*this.networkScale>this.fontDrawThreshold){t.font=(this.selected?"bold ":"")+this.fontSize+"px "+this.fontFace,t.fillStyle=this.fontColor||"black",t.textAlign=o||"center",t.textBaseline=n||"middle";var a=e.split("\n"),h=a.length,d=this.fontSize+4,l=s+(1-h)/2*d;1==r&&(l=s+(1-h)/(2*d));for(var c=0;h>c;c++)t.fillText(a[c],i,l),l+=d}},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,o=0,n=e.length;n>o;o++)s=Math.max(s,t.measureText(e[o]).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.networkScaleInv>=this.canvasTopLeft.x&&this.x-this.width*this.networkScaleInv=this.canvasTopLeft.y&&this.y-this.height*this.networkScaleInv=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 o,n,r=this.length/4,a=this.from;a.width||a.resize(t),a.width>a.height?(o=a.x+a.width/2,n=a.y-r):(o=a.x+r,n=a.y-a.height/2),this._circle(t,o,n,r),e=this._pointOnCircle(o,n,r,.5),this._label(t,this.label,e.x,e.y)}},Edge.prototype._getLineWidth=function(){return 1==this.selected?Math.min(this.widthSelected,this.widthMax)*this.networkScaleInv:1==this.hover?Math.min(this.hoverWidth,this.widthMax)*this.networkScaleInv:this.width*this.networkScaleInv},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 o=t.measureText(e).width,n=this.fontSize,r=i-o/2,a=s-n/2;t.fillRect(r,a,o,n),t.fillStyle=this.fontColor||"black",t.textAlign="left",t.textBaseline="top",t.fillText(e,r,a)}},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)),o=.5*(.5*(this.from.y+this.via.y)+.5*(this.to.y+this.via.y));i={x:s,y:o}}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 o=2*(s-3/8)*Math.PI;return{x:t+i*Math.cos(o),y:e-i*Math.sin(o)}},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 o=.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));e={x:o,y:n}}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 r,a,h=.25*Math.max(100,this.length),d=this.from;d.width||d.resize(t),d.width>d.height?(r=d.x+.5*d.width,a=d.y-h):(r=d.x+h,a=d.y-.5*d.height),this._circle(t,r,a,h);var i=.2*Math.PI,s=(10+5*this.width)*this.arrowScaleFactor;e=this._pointOnCircle(r,a,h,.5),t.arrow(e.x,e.y,i,s),t.fill(),t.stroke(),this.label&&(e=this._pointOnCircle(r,a,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,o=this.to.y-this.from.y,n=Math.sqrt(s*s+o*o),r=this.from.distanceToBorder(t,e+Math.PI),a=(n-r)/n,h=a*this.from.x+(1-a)*this.to.x,d=a*this.from.y+(1-a)*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,o=this.to.y-this.via.y,n=Math.sqrt(s*s+o*o));var l,c,p=this.to.distanceToBorder(t,e),u=(n-p)/n;if(1==this.smooth?(l=(1-u)*this.via.x+u*this.to.x,c=(1-u)*this.via.y+u*this.to.y):(l=(1-u)*this.from.x+u*this.to.x,c=(1-u)*this.from.y+u*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,w=.25*Math.max(100,this.length);_.width||_.resize(t),_.width>_.height?(v=_.x+.5*_.width,y=_.y-w,b={x:v,y:_.y,angle:.9*Math.PI}):(v=_.x+w,y=_.y-.5*_.height,b={x:_.x,y:y,angle:.6*Math.PI}),t.beginPath(),t.arc(v,y,w,0,2*Math.PI,!1),t.stroke();var i=(10+5*this.width)*this.arrowScaleFactor;t.arrow(b.x,b.y,b.angle,i),t.fill(),t.stroke(),this.label&&(m=this._pointOnCircle(v,y,w,.5),this._label(t,this.label,m.x,m.y))}},Edge.prototype._getDistanceToEdge=function(t,e,i,s,o,n){if(this.from!=this.to){if(1==this.smooth){var r,a,h,d,l,c,p=1e9;for(r=0;10>r;r++)a=.1*r,h=Math.pow(1-a,2)*t+2*a*(1-a)*this.via.x+Math.pow(a,2)*i,d=Math.pow(1-a,2)*e+2*a*(1-a)*this.via.y+Math.pow(a,2)*s,l=Math.abs(o-h),c=Math.abs(n-d),p=Math.min(p,Math.sqrt(l*l+c*c));return p}var u=i-t,m=s-e,g=u*u+m*m,f=((o-t)*u+(n-e)*m)/g;f>1?f=1:0>f&&(f=0);var h=t+f*u,d=e+f*m,l=h-o,c=d-n;return Math.sqrt(l*l+c*c)}var h,d,l,c,v=this.length/4,y=this.from;return y.width||y.resize(ctx),y.width>y.height?(h=y.x+y.width/2,d=y.y-v):(h=y.x+v,d=y.y-y.height/2),l=h-o,c=d-n,Math.abs(Math.sqrt(l*l+c*c)-v)},Edge.prototype.setScale=function(t){this.networkScaleInv=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))},Edge.prototype._drawControlNodes=function(t){if(1==this.controlNodesEnabled){if(null===this.controlNodes.from&&null===this.controlNodes.to){var e="edgeIdFrom:".concat(this.id),i="edgeIdTo:".concat(this.id),s={nodes:{group:"",radius:8},physics:{damping:0},clustering:{maxNodeSizeIncrements:0,nodeScaling:{width:0,height:0,radius:0}}};this.controlNodes.from=new Node({id:e,shape:"dot",color:{background:"#ff4e00",border:"#3c3c3c",highlight:{background:"#07f968"}}},{},{},s),this.controlNodes.to=new Node({id:i,shape:"dot",color:{background:"#ff4e00",border:"#3c3c3c",highlight:{background:"#07f968"}}},{},{},s)}0==this.controlNodes.from.selected&&0==this.controlNodes.to.selected&&(this.controlNodes.positions=this.getControlNodePositions(t),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(t),this.controlNodes.to.draw(t) +}else this.controlNodes={from:null,to:null,positions:{}}},Edge.prototype._enableControlNodes=function(){this.controlNodesEnabled=!0},Edge.prototype._disableControlNodes=function(){this.controlNodesEnabled=!1},Edge.prototype._getSelectedControlNode=function(t,e){var i=this.controlNodes.positions,s=Math.sqrt(Math.pow(t-i.from.x,2)+Math.pow(e-i.from.y,2)),o=Math.sqrt(Math.pow(t-i.to.x,2)+Math.pow(e-i.to.y,2));return 15>s?(this.connectedNode=this.from,this.from=this.controlNodes.from,this.controlNodes.from):15>o?(this.connectedNode=this.to,this.to=this.controlNodes.to,this.controlNodes.to):null},Edge.prototype._restoreControlNodes=function(){1==this.controlNodes.from.selected&&(this.from=this.connectedNode,this.connectedNode=null,this.controlNodes.from.unselect()),1==this.controlNodes.to.selected&&(this.to=this.connectedNode,this.connectedNode=null,this.controlNodes.to.unselect())},Edge.prototype.getControlNodePositions=function(t){var e=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x),i=this.to.x-this.from.x,s=this.to.y-this.from.y,o=Math.sqrt(i*i+s*s),n=this.from.distanceToBorder(t,e+Math.PI),r=(o-n)/o,a=r*this.from.x+(1-r)*this.to.x,h=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),i=this.to.x-this.via.x,s=this.to.y-this.via.y,o=Math.sqrt(i*i+s*s));var d,l,c=this.to.distanceToBorder(t,e),p=(o-c)/o;return 1==this.smooth?(d=(1-p)*this.via.x+p*this.to.x,l=(1-p)*this.via.y+p*this.to.y):(d=(1-p)*this.from.x+p*this.to.x,l=(1-p)*this.from.y+p*this.to.y),{from:{x:a,y:h},to:{x:d,y:l}}},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,o=this.frame.parentNode.clientWidth,n=this.y-e;n+e+this.padding>s&&(n=s-e-this.padding),no&&(r=o-i-this.padding),rthis.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():1==this.constants.physics.hierarchicalRepulsion.enabled?this._calculateHierarchicalSpringForces():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,o,n=this.calculationNodes,r=this.constants.physics.centralGravity,a=0;for(o=0;oSimulation Mode:Barnes HutRepulsionHierarchical
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"),o=document.getElementById("graph_physicsMethod3");s.checked=!0,this.constants.physics.barnesHut.enabled&&(i.checked=!0),this.constants.hierarchicalLayout.enabled&&(o.checked=!0);var n=document.getElementById("graph_toggleSmooth"),r=document.getElementById("graph_repositionNodes"),a=document.getElementById("graph_generateOptions");n.onclick=graphToggleSmoothCurves.bind(this),r.onclick=graphRepositionNodes.bind(this),a.onclick=graphGenerateOptions.bind(this),n.style.background=1==this.constants.smoothCurves?"#A4FF56":"#FF8532",switchConfigurations.apply(this),i.onchange=switchConfigurations.bind(this),s.onchange=switchConfigurations.bind(this),o.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,o,n,r,a,h,d,l=this.calculationNodes,c=this.calculationNodeIndices,p=5,u=.5*-p,m=this.constants.physics.hierarchicalRepulsion.nodeDistance,g=m,f=u/g;for(h=0;hi)){n=f*i+p;var v=.05,y=2*g*2*v;n=v*Math.pow(i,2)-y*i+y*y/(4*v),0==i?i=.01:n/=i,s=t*n,o=e*n,r.fx-=s,r.fy-=o,a.fx+=s,a.fy+=o}},_calculateHierarchicalSpringForces:function(){var t,e,i,s,o,n,r,a,h,d=this.edges;for(i in d)if(d.hasOwnProperty(i)&&(e=d[i],e.connected&&this.nodes.hasOwnProperty(e.toId)&&this.nodes.hasOwnProperty(e.fromId))){t=e.customLength?e.length:this.constants.physics.springLength,t+=(e.to.clusterSize+e.from.clusterSize-2)*this.constants.clustering.edgeGrowth,s=e.from.x-e.to.x,o=e.from.y-e.to.y,h=Math.sqrt(s*s+o*o),0==h&&(h=.01),h=Math.max(.8*t,Math.min(5*t,h)),a=this.constants.physics.springConstant*(t-h)/h,n=s*a,r=o*a,e.to.fx-=n,e.to.fy-=r,e.from.fx+=n,e.from.fy+=r;var l=5;h>t&&(l=25),e.from.level>e.to.level?(e.to.fx-=l*n,e.to.fy-=l*r):e.from.leveln;n++)t=e[i[n]],this._getForceContribution(o.root.children.NW,t),this._getForceContribution(o.root.children.NE,t),this._getForceContribution(o.root.children.SW,t),this._getForceContribution(o.root.children.SE,t)}},_getForceContribution:function(t,e){if(t.childrenCount>0){var i,s,o;if(i=t.centerOfMass.x-e.x,s=t.centerOfMass.y-e.y,o=Math.sqrt(i*i+s*s),o*t.calcSize>this.constants.physics.barnesHut.theta){0==o&&(o=.1*Math.random(),i=o);var n=this.constants.physics.barnesHut.gravitationalConstant*t.mass*e.mass/(o*o*o),r=i*n,a=s*n;e.fx+=r,e.fy+=a}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==o&&(o=.5*Math.random(),i=o);var n=this.constants.physics.barnesHut.gravitationalConstant*t.mass*e.mass/(o*o*o),r=i*n,a=s*n;e.fx+=r,e.fy+=a}}},_formBarnesHutTree:function(t,e){for(var i,s=e.length,o=Number.MAX_VALUE,n=Number.MAX_VALUE,r=-Number.MAX_VALUE,a=-Number.MAX_VALUE,h=0;s>h;h++){var d=t[e[h]].x,l=t[e[h]].y;o>d&&(o=d),d>r&&(r=d),n>l&&(n=l),l>a&&(a=l)}var c=Math.abs(r-o)-Math.abs(a-n);c>0?(n-=.5*c,a+=.5*c):(o+=.5*c,r-=.5*c);var p=1e-5,u=Math.max(p,Math.abs(r-o)),m=.5*u,g=.5*(o+r),f=.5*(n+a),v={root:{centerOfMass:{x:0,y:0},mass:0,range:{minX:g-m,maxX:g+m,minY:f-m,maxY:f+m},size:u,calcSize:1/u,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 o=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,o,n,r=.5*t.size;switch(e){case"NW":i=t.range.minX,s=t.range.minX+r,o=t.range.minY,n=t.range.minY+r;break;case"NE":i=t.range.minX+r,s=t.range.maxX,o=t.range.minY,n=t.range.minY+r;break;case"SW":i=t.range.minX,s=t.range.minX+r,o=t.range.minY+r,n=t.range.maxY;break;case"SE":i=t.range.minX+r,s=t.range.maxX,o=t.range.minY+r,n=t.range.maxY}t.children[e]={centerOfMass:{x:0,y:0},mass:0,range:{minX:i,maxX:s,minY:o,maxY:n},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,o,n,r,a,h,d,l,c=this.calculationNodes,p=this.calculationNodeIndices,u=-2/3,m=4/3,g=this.constants.physics.repulsion.nodeDistance,f=g;for(d=0;di&&(r=.5*f>i?1:v*i+m,r*=0==n?1:1+n*this.constants.clustering.forceAmplification,r/=i,s=t*r,o=e*r,a.fx-=s,a.fy-=o,h.fx+=s,h.fy+=o)}}},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,o=!1;for(e in this.nodes)this.nodes.hasOwnProperty(e)&&(t=this.nodes[e],-1!=t.level?s=!0:o=!0,is&&(n.xFixed=!1,n.x=i[n.level].minPos,r=!0):n.yFixed&&n.level>s&&(n.yFixed=!1,n.y=i[n.level].minPos,r=!0),1==r&&(i[n.level].minPos+=i[n.level].nodeSpacing,n.edges.length>1&&this._placeBranchNodes(n.edges,n.id,i,n.level))}},_setLevel:function(t,e,i){for(var s=0;st)&&(o.level=t,e.length>1&&this._setLevel(t+1,o.edges,o.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("network-manipulationDiv"),e=document.getElementById("network-manipulation-closeDiv"),i=document.getElementById("network-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),void 0!==this.edgeBeingEdited&&(this.edgeBeingEdited._disableControlNodes(),this.edgeBeingEdited=void 0,this.selectedControlNode=null),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+"":1==this._getSelectedEdgeCount()&&0==this._getSelectedNodeCount()&&(this.manipulationDiv.innerHTML+="
"+this.constants.labels.editEdge+""),0==this._selectionIsEmpty()&&(this.manipulationDiv.innerHTML+="
"+this.constants.labels.del+"");var t=document.getElementById("network-manipulate-addNode");t.onclick=this._createAddNodeToolbar.bind(this);var e=document.getElementById("network-manipulate-connectNode");if(e.onclick=this._createAddEdgeToolbar.bind(this),1==this._getSelectedNodeCount()&&this.triggerFunctions.edit){var i=document.getElementById("network-manipulate-editNode"); +i.onclick=this._editNode.bind(this)}else if(1==this._getSelectedEdgeCount()&&0==this._getSelectedNodeCount()){var i=document.getElementById("network-manipulate-editEdge");i.onclick=this._createEditEdgeToolbar.bind(this)}if(0==this._selectionIsEmpty()){var s=document.getElementById("network-manipulate-delete");s.onclick=this._deleteSelected.bind(this)}var o=document.getElementById("network-manipulation-closeDiv");o.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 n=document.getElementById("network-manipulate-editModeButton");n.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("network-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("network-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()},_createEditEdgeToolbar:function(){this._clearManipulatorBar(),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+"";var t=document.getElementById("network-manipulate-back");t.onclick=this._createManipulatorBar.bind(this),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,this._redraw()},_selectControlNode:function(t){this.edgeBeingEdited.controlNodes.from.unselect(),this.edgeBeingEdited.controlNodes.to.unselect(),this.selectedControlNode=this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(t.x),this._YconvertDOMtoCanvas(t.y)),null!==this.selectedControlNode&&(this.selectedControlNode.select(),this.freezeSimulation=!0),this._redraw()},_controlNodeDrag:function(t){var e=this._getPointer(t.gesture.center);null!==this.selectedControlNode&&void 0!==this.selectedControlNode&&(this.selectedControlNode.x=this._XconvertDOMtoCanvas(e.x),this.selectedControlNode.y=this._YconvertDOMtoCanvas(e.y)),this._redraw()},_releaseControlNode:function(t){var e=this._getNodeAt(t);null!=e?(1==this.edgeBeingEdited.controlNodes.from.selected&&(this._editEdge(e.id,this.edgeBeingEdited.to.id),this.edgeBeingEdited.controlNodes.from.unselect()),1==this.edgeBeingEdited.controlNodes.to.selected&&(this._editEdge(this.edgeBeingEdited.from.id,e.id),this.edgeBeingEdited.controlNodes.to.unselect())):this.edgeBeingEdited._restoreControlNodes(),this.freezeSimulation=!1,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()}},_editEdge:function(t,e){if(1==this.editMode){var i={id:this.edgeBeingEdited.id,from:t,to:e};if(this.triggerFunctions.editEdge)if(2==this.triggerFunctions.editEdge.length){var s=this;this.triggerFunctions.editEdge(i,function(t){s.edgesData.update(t),s.moving=!0,s.start()})}else alert(this.constants.labels.linkError),this.moving=!0,this.start();else this.edgesData.update(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,o=-1e9,n=1e9,r=-1e9;for(var a in this.sectors[e])if(this.sectors[e].hasOwnProperty(a)&&void 0!==this.sectors[e][a].drawingNode){this._switchToSector(a,e),s=1e9,o=-1e9,n=1e9,r=-1e9;for(var h in this.nodes)this.nodes.hasOwnProperty(h)&&(i=this.nodes[h],i.resize(t),n>i.x-.5*i.width&&(n=i.x-.5*i.width),ri.y-.5*i.height&&(s=i.y-.5*i.height),ot&&s>o;)o%3==0?(this.forceAggregateHubs(!0),this.normalizeClusterLevels()):this.increaseClusterLevel(),i=this.nodeIndices.length,o+=1;o>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 o=this.moving,n=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 r=n.from,a=n.to;n.to.mass>n.from.mass&&(r=n.to,a=n.from),1==a.dynamicEdgesLength?this._addToCluster(r,a,!1):1==r.dynamicEdgesLength&&this._addToCluster(a,r,!1)}}},_forceClustersByZoom:function(){for(var t in this.nodes)if(this.nodes.hasOwnProperty(t)){var e=this.nodes[t];if(1==e.dynamicEdgesLength&&0!=e.dynamicEdges.length){var i=e.dynamicEdges[0],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;so.clusterSessions.length&&(e=o.clusterSessions.length,i=o)}null!=o&&void 0!==this.nodes[o.id]&&this._addToCluster(o,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 o,n,r,a=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 p=this.edges[d[c]];if(void 0!==p&&p.connected&&p.toId!=p.fromId&&(o=p.to.x-p.from.x,n=p.to.y-p.from.y,r=Math.sqrt(o*o+n*n),a>r)){h=!0;break}}if(!e&&h||e)for(c=0;l>c;c++)if(p=this.edges[d[c]],void 0!==p){var u=this.nodes[p.fromId==t.id?p.toId:p.fromId];u.dynamicEdges.length<=this.hubThreshold+s&&u.id!=t.id&&this._addToCluster(t,u,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 o=this.nodeIndices.length,n=e-this.constants.clustering.clusterLevelDifference;for(t in this.nodes)this.nodes.hasOwnProperty(t)&&this.nodes[t].clusterSessions.lengths&&(s=n.dynamicEdgesLength),t+=n.dynamicEdgesLength,e+=Math.pow(n.dynamicEdgesLength,2),i+=1}t/=i,e/=i;var r=e-Math.pow(t,2),a=Math.sqrt(r);this.hubThreshold=Math.floor(t+2*a),this.hubThreshold>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},_getSelectedEdge:function(){for(var t in this.selectionObj.edges)if(this.selectionObj.edges.hasOwnProperty(t))return this.selectionObj.edges[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 o=this.nodes[s];if(!o)throw new RangeError('Node with id "'+s+'" not found');this._selectObject(o,!0,!0)}console.log("setSelection is deprecated. Please use selectNodes instead."),this.redraw()},selectNodes:function(t,e){var i,s,o;if(!t||void 0==t.length)throw"Selection must be an array with ids";for(this._unselectAll(!0),i=0,s=t.length;s>i;i++){o=t[i];var n=this.nodes[o];if(!n)throw new RangeError('Node with id "'+o+'" not found');this._selectObject(n,!0,!0,e)}this.redraw()},selectEdges:function(t){var e,i,s;if(!t||void 0==t.length)throw"Selection must be an array with ids";for(this._unselectAll(!0),e=0,i=t.length;i>e;e++){s=t[e];var o=this.edges[s];if(!o)throw new RangeError('Edge with id "'+s+'" not found');this._selectObject(o,!0,!0,highlightEdges)}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("network-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="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",this.containerElement.insertBefore(this.navigationDivs.wrapper,this.frame);for(var i=0;it.x&&(s=t.x),ot.y&&(e=t.y),i=this.constants.clustering.initialMaxNodes?49.07548/(o+142.05338)+91444e-8:12.662/(o+7.4147)+.0964822:1==this.constants.clustering.enabled&&o>=this.constants.clustering.initialMaxNodes?77.5271985/(o+187.266146)+476710517e-13:30.5062972/(o+19.93597763)+.08413486;var n=Math.min(this.frame.canvas.clientWidth/600,this.frame.canvas.clientHeight/600);i*=n}else{var r=1.1*(Math.abs(s.minX)+Math.abs(s.maxX)),a=1.1*(Math.abs(s.minY)+Math.abs(s.maxY)),h=this.frame.canvas.clientWidth/r,d=this.frame.canvas.clientHeight/a;i=d>=h?h:d}i>1&&(i=1),this._setScale(i),this._centerNetwork(s),0==e&&(this.moving=!0,this.start())},Network.prototype._updateNodeIndexList=function(){this._clearNodeIndexList();for(var t in this.nodes)this.nodes.hasOwnProperty(t)&&this.nodeIndices.push(t)},Network.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()},Network.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.dragNetwork&&(this.constants.dragNetwork=t.dragNetwork),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.dragGraph)throw new Error("Option dragGraph is renamed to dragNetwork");if(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.onEditEdge&&(this.triggerFunctions.editEdge=t.onEditEdge),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()},Network.prototype._create=function(){for(;this.containerElement.hasChildNodes();)this.containerElement.removeChild(this.containerElement.firstChild);if(this.frame=document.createElement("div"),this.frame.className="network-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)},Network.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)))},Network.prototype._getPointer=function(t){return{x:t.pageX-vis.util.getAbsoluteLeft(this.frame.canvas),y:t.pageY-vis.util.getAbsoluteTop(this.frame.canvas)}},Network.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)},Network.prototype._onDragStart=function(){this._handleDragStart()},Network.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],o={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(o)}}},Network.prototype._onDrag=function(t){this._handleOnDrag(t)},Network.prototype._handleOnDrag=function(t){if(!this.drag.pinched){var e=this._getPointer(t.gesture.center),i=this,s=this.drag,o=s.selection;if(o&&o.length&&1==this.constants.dragNodes){var n=e.x-s.pointer.x,r=e.y-s.pointer.y;o.forEach(function(t){var e=t.node;t.xFixed||(e.x=i._XconvertDOMtoCanvas(i._XconvertCanvasToDOM(t.x)+n)),t.yFixed||(e.y=i._YconvertDOMtoCanvas(i._YconvertCanvasToDOM(t.y)+r))}),this.moving||(this.moving=!0,this.start())}else if(1==this.constants.dragNetwork){var a=e.x-this.drag.pointer.x,h=e.y-this.drag.pointer.y;this._setTranslation(this.drag.translation.x+a,this.drag.translation.y+h),this._redraw(),this.moving=!0,this.start()}}},Network.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})},Network.prototype._onTap=function(t){var e=this._getPointer(t.gesture.center);this.pointerPosition=e,this._handleTap(e)},Network.prototype._onDoubleTap=function(t){var e=this._getPointer(t.gesture.center);this._handleDoubleTap(e)},Network.prototype._onHold=function(t){var e=this._getPointer(t.gesture.center);this.pointerPosition=e,this._handleOnHold(e)},Network.prototype._onRelease=function(t){var e=this._getPointer(t.gesture.center);this._handleOnRelease(e)},Network.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)},Network.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(),o=t/i,n=(1-o)*e.x+s.x*o,r=(1-o)*e.y+s.y*o;return this.areaCenter={x:this._XconvertDOMtoCanvas(e.x),y:this._YconvertDOMtoCanvas(e.y)},this._setScale(t),this._setTranslation(n,r),this.updateClustersDefault(),this._redraw(),t>i?this.emit("zoom",{direction:"+"}):this.emit("zoom",{direction:"-"}),t}},Network.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 o=util.fakeGesture(this,t),n=this._getPointer(o.center);this._zoom(i,n)}t.preventDefault()},Network.prototype._onMouseMoveTitle=function(t){var e=util.fakeGesture(this,t),i=this._getPointer(e.center);this.popupObj&&this._checkHidePopup(i);var s=this,o=function(){s._checkShowPopup(i)};if(this.popupTimer&&clearInterval(this.popupTimer),this.drag.dragging||(this.popupTimer=setTimeout(o,this.constants.tooltip.delay)),1==this.constants.hover){for(var n in this.hoverObj.edges)this.hoverObj.edges.hasOwnProperty(n)&&(this.hoverObj.edges[n].hover=!1,delete this.hoverObj.edges[n]);var r=this._getNodeAt(i);null==r&&(r=this._getEdgeAt(i)),null!=r&&this._hoverObject(r);for(var a in this.hoverObj.nodes)this.hoverObj.nodes.hasOwnProperty(a)&&(r instanceof Node&&r.id!=a||r instanceof Edge||null==r)&&(this._blurObject(this.hoverObj.nodes[a]),delete this.hoverObj.nodes[a]);this.redraw()}},Network.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 o=this.nodes;for(e in o)if(o.hasOwnProperty(e)){var n=o[e];if(void 0!==n.getTitle()&&n.isOverlappingWith(i)){this.popupObj=n;break}}}if(void 0===this.popupObj){var r=this.edges;for(e in r)if(r.hasOwnProperty(e)){var a=r[e];if(a.connected&&void 0!==a.getTitle()&&a.isOverlappingWith(i)){this.popupObj=a;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()},Network.prototype._checkHidePopup=function(t){this.popupObj&&this._getNodeAt(t)||(this.popupObj=void 0,this.popup&&this.popup.hide())},Network.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})},Network.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()},Network.prototype._addNodes=function(t){for(var e,i=0,s=t.length;s>i;i++){e=t[i];var o=this.nodesData.get(e),n=new Node(o,this.images,this.groups,this.constants);if(this.nodes[e]=n,!(0!=n.xFixed&&0!=n.yFixed||null!==n.x&&null!==n.y)){var r=1*t.length,a=2*Math.PI*Math.random();0==n.xFixed&&(n.x=r*Math.cos(a)),0==n.yFixed&&(n.y=r*Math.sin(a))}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()},Network.prototype._updateNodes=function(t){for(var e=this.nodes,i=this.nodesData,s=0,o=t.length;o>s;s++){var n=t[s],r=e[n],a=i.get(n);r?r.setProperties(a,this.constants):(r=new Node(properties,this.images,this.groups,this.constants),e[n]=r)}this.moving=!0,1==this.constants.hierarchicalLayout.enabled&&0==this.initializing&&(this._resetLevels(),this._setupHierarchicalLayout()),this._updateNodeIndexList(),this._reconnectEdges(),this._updateValueRange(e)},Network.prototype._removeNodes=function(t){for(var e=this.nodes,i=0,s=t.length;s>i;i++){var o=t[i];delete e[o]}this._updateNodeIndexList(),1==this.constants.hierarchicalLayout.enabled&&0==this.initializing&&(this._resetLevels(),this._setupHierarchicalLayout()),this._updateCalculationNodes(),this._reconnectEdges(),this._updateSelection(),this._updateValueRange(e)},Network.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()},Network.prototype._addEdges=function(t){for(var e=this.edges,i=this.edgesData,s=0,o=t.length;o>s;s++){var n=t[s],r=e[n];r&&r.disconnect();var a=i.get(n,{showInternalIds:!0});e[n]=new Edge(a,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()},Network.prototype._updateEdges=function(t){for(var e=this.edges,i=this.edgesData,s=0,o=t.length;o>s;s++){var n=t[s],r=i.get(n),a=e[n];a?(a.disconnect(),a.setProperties(r,this.constants),a.connect()):(a=new Edge(r,this,this.constants),this.edges[n]=a)}this._createBezierNodes(),1==this.constants.hierarchicalLayout.enabled&&0==this.initializing&&(this._resetLevels(),this._setupHierarchicalLayout()),this.moving=!0,this._updateValueRange(e)},Network.prototype._removeEdges=function(t){for(var e=this.edges,i=0,s=t.length;s>i;i++){var o=t[i],n=e[o];n&&(null!=n.via&&delete this.sectors.support.nodes[n.via.id],n.disconnect(),delete e[o])}this.moving=!0,this._updateValueRange(e),1==this.constants.hierarchicalLayout.enabled&&0==this.initializing&&(this._resetLevels(),this._setupHierarchicalLayout()),this._updateCalculationNodes()},Network.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()}},Network.prototype._updateValueRange=function(t){var e,i=void 0,s=void 0;for(e in t)if(t.hasOwnProperty(e)){var o=t[e].getValue();void 0!==o&&(i=void 0===i?o:Math.min(o,i),s=void 0===s?o:Math.max(o,s))}if(void 0!==i&&void 0!==s)for(e in t)t.hasOwnProperty(e)&&t[e].setValueRange(i,s)},Network.prototype.redraw=function(){this.setSize(this.width,this.height),this._redraw()},Network.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),this._doInAllSectors("_drawControlNodes",t),t.restore()},Network.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")},Network.prototype._getTranslation=function(){return{x:this.translation.x,y:this.translation.y}},Network.prototype._setScale=function(t){this.scale=t},Network.prototype._getScale=function(){return this.scale},Network.prototype._XconvertDOMtoCanvas=function(t){return(t-this.translation.x)/this.scale},Network.prototype._XconvertCanvasToDOM=function(t){return t*this.scale+this.translation.x},Network.prototype._YconvertDOMtoCanvas=function(t){return(t-this.translation.y)/this.scale},Network.prototype._YconvertCanvasToDOM=function(t){return t*this.scale+this.translation.y},Network.prototype.canvasToDOM=function(t){return{x:this._XconvertCanvasToDOM(t.x),y:this._YconvertCanvasToDOM(t.y)}},Network.prototype.DOMtoCanvas=function(t){return{x:this._XconvertDOMtoCanvas(t.x),y:this._YconvertDOMtoCanvas(t.y)}},Network.prototype._drawNodes=function(t,e){void 0===e&&(e=!1);var i=this.nodes,s=[];for(var o in i)i.hasOwnProperty(o)&&(i[o].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight),i[o].isSelected()?s.push(o):(i[o].inArea()||e)&&i[o].draw(t));for(var n=0,r=s.length;r>n;n++)(i[s[n]].inArea()||e)&&i[s[n]].draw(t)},Network.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)}},Network.prototype._drawControlNodes=function(t){var e=this.edges;for(var i in e)e.hasOwnProperty(i)&&e[i]._drawControlNodes(t)},Network.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 o=this.constants.minVelocity/Math.max(this.scale,.05);this.moving=o>.5*this.constants.maxVelocity?!0:this._isMoving(o)}},Network.prototype._physicsTick=function(){this.freezeSimulation||this.moving&&(this._doInAllActiveSectors("_initializeForceCalculation"),this._doInAllActiveSectors("_discreteStepNodes"),this.constants.smoothCurves&&this._doInSupportSector("_discreteStepNodes"),this._findCenter(this._getRange()))},Network.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<.9*(this.renderTimestep-this.renderTime)&&e.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 u=(t-c)/(p-c),m=240*u,g=this._hsv2rgb(m,1,1);l.strokeStyle=g,l.beginPath(),l.moveTo(a,n+t),l.lineTo(r,n+t),l.stroke()}l.strokeStyle=this.colorAxis,l.strokeRect(a,n,i,o)}if(this.style===Graph3d.STYLE.DOTSIZE&&(l.strokeStyle=this.colorAxis,l.fillStyle=this.colorDot,l.beginPath(),l.moveTo(a,n),l.lineTo(r,n),l.lineTo(r-i+e,h),l.lineTo(a,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,o=this._convert3Dto2D(new Point3d(b,r,this.zMin)),Math.cos(2*y)>0?(m.textAlign="center",m.textBaseline="top",o.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()+" ",o.x,o.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,o=this._convert3Dto2D(new Point3d(n,i.getCurrent(),this.zMin)),Math.cos(2*y)<0?(m.textAlign="center",m.textBaseline="top",o.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()+" ",o.x,o.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,r=Math.sin(y)<0?this.yMin:this.yMax;!i.end();)t=this._convert3Dto2D(new Point3d(n,r,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(n,r,this.zMin)),e=this._convert3Dto2D(new Point3d(n,r,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)),p=this._convert3Dto2D(new Point3d(this.xMax,this.yMin,this.zMin)),m.strokeStyle=this.colorAxis,m.beginPath(),m.moveTo(c.x,c.y),m.lineTo(p.x,p.y),m.stroke(),c=this._convert3Dto2D(new Point3d(this.xMin,this.yMax,this.zMin)),p=this._convert3Dto2D(new Point3d(this.xMax,this.yMax,this.zMin)),m.strokeStyle=this.colorAxis,m.beginPath(),m.moveTo(c.x,c.y),m.lineTo(p.x,p.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 _=this.xLabel;_.length>0&&(l=.1/this.scale.y,n=(this.xMin+this.xMax)/2,r=Math.cos(y)>0?this.yMin-l:this.yMax+l,o=this._convert3Dto2D(new Point3d(n,r,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(_,o.x,o.y));var w=this.yLabel;w.length>0&&(d=.1/this.scale.x,n=Math.sin(y)>0?this.xMin-d:this.xMax+d,r=(this.yMin+this.yMax)/2,o=this._convert3Dto2D(new Point3d(n,r,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(w,o.x,o.y));var x=this.zLabel;x.length>0&&(h=30,n=Math.cos(y)>0?this.xMin:this.xMax,r=Math.sin(y)<0?this.yMin:this.yMax,a=(this.zMin+this.zMax)/2,o=this._convert3Dto2D(new Point3d(n,r,a)),m.textAlign="right",m.textBaseline="middle",m.fillStyle=this.colorAxis,m.fillText(x,o.x-h,o.y))},Graph3d.prototype._hsv2rgb=function(t,e,i){var s,o,n,r,a,h;switch(r=i*e,a=Math.floor(t/60),h=r*(1-Math.abs(t/60%2-1)),a){case 0:s=r,o=h,n=0;break;case 1:s=h,o=r,n=0;break;case 2:s=0,o=r,n=h;break;case 3:s=0,o=h,n=r;break;case 4:s=h,o=0,n=r;break;case 5:s=r,o=0,n=h;break;default:s=0,o=0,n=0}return"RGB("+parseInt(255*s)+","+parseInt(255*o)+","+parseInt(255*n)+")"},Graph3d.prototype._redrawDataGrid=function(){var t,e,i,s,o,n,r,a,h,d,l,c,p,u=this.frame.canvas,m=u.getContext("2d");if(!(void 0===this.dataPoints||this.dataPoints.length<=0)){for(o=0;o0}else n=!0;n?(p=(t.point.z+e.point.z+i.point.z+s.point.z)/4,d=240*(1-(p-this.zMin)*this.scale.z/this.verticalRatio),l=1,this.showShadow?(c=Math.min(1+w.x/x/2,1),r=this._hsv2rgb(d,l,c),a=r):(c=1,r=this._hsv2rgb(d,l,c),a=this.colorAxis)):(r="gray",a=this.colorAxis),h=.5,m.lineWidth=h,m.fillStyle=r,m.strokeStyle=a,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(o=0;oc&&(c=0);var p,u,m;this.style===Graph3d.STYLE.DOTCOLOR?(p=240*(1-(h.point.value-this.valueMin)*this.scale.value),u=this._hsv2rgb(p,1,1),m=this._hsv2rgb(p,1,.8)):this.style===Graph3d.STYLE.DOTSIZE?(u=this.colorDot,m=this.colorDotBorder):(p=240*(1-(h.point.z-this.zMin)*this.scale.z/this.verticalRatio),u=this._hsv2rgb(p,1,1),m=this._hsv2rgb(p,1,.8)),i.lineWidth=1,i.strokeStyle=m,i.fillStyle=u,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,o=this.frame.canvas,n=o.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,o=this.startArmRotation.vertical+i/200,n=4,r=Math.sin(n/360*2*Math.PI);Math.abs(Math.sin(s))0?1:0>t?-1:0}var s=e[0],o=e[1],n=e[2],r=i((o.x-s.x)*(t.y-s.y)-(o.y-s.y)*(t.x-s.x)),a=i((n.x-o.x)*(t.y-o.y)-(n.y-o.y)*(t.x-o.x)),h=i((s.x-n.x)*(t.y-n.y)-(s.y-n.y)*(t.x-n.x));return!(0!=r&&0!=a&&r!=a||0!=a&&0!=h&&a!=h||0!=r&&0!=h&&r!=h)},Graph3d.prototype._dataPointFromXY=function(t,e){var i,s=100,o=null,n=null,r=null,a=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--){o=this.dataPoints[i];var h=o.surfaces;if(h)for(var d=h.length-1;d>=0;d--){var l=h[d],c=l.corners,p=[c[0].screen,c[1].screen,c[2].screen],u=[c[2].screen,c[3].screen,c[0].screen];if(this._insideTriangle(a,p)||this._insideTriangle(a,u))return o}}else for(i=0;iv)&&s>v&&(r=v,n=o)}}return n},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):"
x:"+t.point.x+"
y:"+t.point.y+"
z:"+t.point.z+"
",e.style.left="0",e.style.top="0",this.frame.appendChild(e),this.frame.appendChild(i),this.frame.appendChild(s);var o=e.offsetWidth,n=e.offsetHeight,r=i.offsetHeight,a=s.offsetWidth,h=s.offsetHeight,d=t.screen.x-o/2;d=Math.min(Math.max(d,10),this.frame.clientWidth-10-o),i.style.left=t.screen.x+"px",i.style.top=t.screen.y-r+"px",e.style.left=d+"px",e.style.top=t.screen.y-r-n+"px",s.style.left=t.screen.x-a/2+"px",s.style.top=t.screen.y-h/2+"px"},Graph3d.prototype._hideTooltip=function(){if(this.tooltip){this.tooltip.dataPoint=null;for(var t in this.tooltip.dom)if(this.tooltip.dom.hasOwnProperty(t)){var e=this.tooltip.dom[t];e&&e.parentNode&&e.parentNode.removeChild(e)}}},G3DaddEventListener=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)},G3DremoveEventListener=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)},G3DstopPropagation=function(t){t||(t=window.event),t.stopPropagation?t.stopPropagation():t.cancelBubble=!0},G3DpreventDefault=function(t){t||(t=window.event),t.preventDefault?t.preventDefault():t.returnValue=!1},Point3d.subtract=function(t,e){var i=new Point3d;return i.x=t.x-e.x,i.y=t.y-e.y,i.z=t.z-e.z,i},Point3d.add=function(t,e){var i=new Point3d;return i.x=t.x+e.x,i.y=t.y+e.y,i.z=t.z+e.z,i},Point3d.avg=function(t,e){return new Point3d((t.x+e.x)/2,(t.y+e.y)/2,(t.z+e.z)/2)},Point3d.crossProduct=function(t,e){var i=new Point3d;return i.x=t.y*e.z-t.z*e.y,i.y=t.z*e.x-t.x*e.z,i.z=t.x*e.y-t.y*e.x,i},Point3d.prototype.length=function(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z)},Point2d=function(t,e){this.x=void 0!==t?t:0,this.y=void 0!==e?e:0},Filter.prototype.isLoaded=function(){return this.loaded},Filter.prototype.getLoadedProgress=function(){for(var t=this.values.length,e=0;this.dataPoints[e];)e++;return Math.round(e/t*100)},Filter.prototype.getLabel=function(){return this.graph.filterLabel},Filter.prototype.getColumn=function(){return this.column},Filter.prototype.getSelectedValue=function(){return void 0===this.index?void 0:this.values[this.index]},Filter.prototype.getValues=function(){return this.values},Filter.prototype.getValue=function(t){if(t>=this.values.length)throw"Error: index out of range";return this.values[t]},Filter.prototype._getDataPoints=function(t){if(void 0===t&&(t=this.index),void 0===t)return[];var e;if(this.dataPoints[t])e=this.dataPoints[t];else{var i={};i.column=this.column,i.value=this.values[t];var s=new DataView(this.data,{filter:function(t){return t[i.column]==i.value}}).get();e=this.graph._getDataPoints(s),this.dataPoints[t]=e}return e},Filter.prototype.setOnLoadCallback=function(t){this.onLoadCallback=t},Filter.prototype.selectValue=function(t){if(t>=this.values.length)throw"Error: index out of range";this.index=t,this.value=this.values[t]},Filter.prototype.loadInBackground=function(t){void 0===t&&(t=0);var e=this.graph.frame;if(t=t||(void 0!==e&&(this.prettyStep=e),this._step=this.prettyStep===!0?StepNumber.calculatePrettyStep(t):t)},StepNumber.calculatePrettyStep=function(t){var e=function(t){return Math.log(t)/Math.LN10},i=Math.pow(10,Math.round(e(t))),s=2*Math.pow(10,Math.round(e(t/2))),o=5*Math.pow(10,Math.round(e(t/5))),n=i;return Math.abs(s-t)<=Math.abs(n-t)&&(n=s),Math.abs(o-t)<=Math.abs(n-t)&&(n=o),0>=n&&(n=1),n},StepNumber.prototype.getCurrent=function(){return parseFloat(this._current.toPrecision(this.precision))},StepNumber.prototype.getStep=function(){return this._step},StepNumber.prototype.start=function(){this._current=this._start-this._start%this._step},StepNumber.prototype.next=function(){this._current+=this._step},StepNumber.prototype.end=function(){return this._current>this._end},Slider.prototype.prev=function(){var t=this.getIndex();t>0&&(t--,this.setIndex(t))},Slider.prototype.next=function(){var t=this.getIndex();t0?this.setIndex(0):this.index=void 0},Slider.prototype.setIndex=function(t){if(!(ts&&(s=0),s>this.values.length-1&&(s=this.values.length-1),s},Slider.prototype.indexToLeft=function(t){var e=parseFloat(this.frame.bar.style.width)-this.frame.slide.clientWidth-10,i=t/(this.values.length-1)*e,s=i+3;return s},Slider.prototype._onMouseMove=function(t){var e=t.clientX-this.startClientX,i=this.startSlideX+e,s=this.leftToIndex(i);this.setIndex(s),G3DpreventDefault()},Slider.prototype._onMouseUp=function(){this.frame.style.cursor="auto",G3DremoveEventListener(document,"mousemove",this.onmousemove),G3DremoveEventListener(document,"mouseup",this.onmouseup),G3DpreventDefault()},getAbsoluteLeft=function(t){for(var e=0;null!==t;)e+=t.offsetLeft,e-=t.scrollLeft,t=t.offsetParent;return e},getAbsoluteTop=function(t){for(var e=0;null!==t;)e+=t.offsetTop,e-=t.scrollTop,t=t.offsetParent;return e},getMouseX=function(t){return"clientX"in t?t.clientX:t.targetTouches[0]&&t.targetTouches[0].clientX||0},getMouseY=function(t){return"clientY"in t?t.clientY:t.targetTouches[0]&&t.targetTouches[0].clientY||0};var vis={moment:moment,util:util,DOMutil:DOMutil,DataSet:DataSet,DataView:DataView,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}},Network:Network,network:{Edge:Edge,Groups:Groups,Images:Images,Node:Node,Popup:Popup},Graph:function(){throw new Error("Graph is renamed to Network. Please create a graph as new vis.Network(...)")},Graph3d:Graph3d};"undefined"!=typeof exports&&(exports=vis),"undefined"!=typeof module&&"undefined"!=typeof module.exports&&(module.exports=vis),"function"==typeof define&&define(function(){return vis}),"undefined"!=typeof window&&(window.vis=vis)},{"emitter-component":2,hammerjs:3,moment:4,mousetrap:5}],2:[function(t,e){function i(t){return t?s(t):void 0}function s(t){for(var e in i.prototype)t[e]=i.prototype[e];return t}e.exports=i,i.prototype.on=i.prototype.addEventListener=function(t,e){return this._callbacks=this._callbacks||{},(this._callbacks[t]=this._callbacks[t]||[]).push(e),this},i.prototype.once=function(t,e){function i(){s.off(t,i),e.apply(this,arguments)}var s=this;return this._callbacks=this._callbacks||{},i.fn=e,this.on(t,i),this},i.prototype.off=i.prototype.removeListener=i.prototype.removeAllListeners=i.prototype.removeEventListener=function(t,e){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var i=this._callbacks[t];if(!i)return this;if(1==arguments.length)return delete this._callbacks[t],this;for(var s,o=0;os;++s)i[s].apply(this,e)}return this},i.prototype.listeners=function(t){return this._callbacks=this._callbacks||{},this._callbacks[t]||[]},i.prototype.hasListeners=function(t){return!!this.listeners(t).length}},{}],3:[function(t,e){!function(t,i){"use strict";function s(){if(!o.READY){o.event.determineEventTypes();for(var t in o.gestures)o.gestures.hasOwnProperty(t)&&o.detection.register(o.gestures[t]);o.event.onTouch(o.DOCUMENT,o.EVENT_MOVE,o.detection.detect),o.event.onTouch(o.DOCUMENT,o.EVENT_END,o.detection.detect),o.READY=!0}}var o=function(t,e){return new o.Instance(t,e||{})};o.defaults={stop_browser_behavior:{userSelect:"none",touchAction:"none",touchCallout:"none",contentZooming:"none",userDrag:"none",tapHighlightColor:"rgba(0,0,0,0)"}},o.HAS_POINTEREVENTS=navigator.pointerEnabled||navigator.msPointerEnabled,o.HAS_TOUCHEVENTS="ontouchstart"in t,o.MOBILE_REGEX=/mobile|tablet|ip(ad|hone|od)|android/i,o.NO_MOUSEEVENTS=o.HAS_TOUCHEVENTS&&navigator.userAgent.match(o.MOBILE_REGEX),o.EVENT_TYPES={},o.DIRECTION_DOWN="down",o.DIRECTION_LEFT="left",o.DIRECTION_UP="up",o.DIRECTION_RIGHT="right",o.POINTER_MOUSE="mouse",o.POINTER_TOUCH="touch",o.POINTER_PEN="pen",o.EVENT_START="start",o.EVENT_MOVE="move",o.EVENT_END="end",o.DOCUMENT=document,o.plugins={},o.READY=!1,o.Instance=function(t,e){var i=this;return s(),this.element=t,this.enabled=!0,this.options=o.utils.extend(o.utils.extend({},o.defaults),e||{}),this.options.stop_browser_behavior&&o.utils.stopDefaultBrowserBehavior(this.element,this.options.stop_browser_behavior),o.event.onTouch(t,o.EVENT_START,function(t){i.enabled&&o.detection.startDetect(i,t)}),this},o.Instance.prototype={on:function(t,e){for(var i=t.split(" "),s=0;s0&&e==o.EVENT_END?e=o.EVENT_MOVE:l||(e=o.EVENT_END),l||null===n?n=h:h=n,i.call(o.detection,s.collectEventData(t,e,h)),o.HAS_POINTEREVENTS&&e==o.EVENT_END&&(l=o.PointerEvent.updatePointer(e,h))),l||(n=null,r=!1,a=!1,o.PointerEvent.reset())}})},determineEventTypes:function(){var t;t=o.HAS_POINTEREVENTS?o.PointerEvent.getEvents():o.NO_MOUSEEVENTS?["touchstart","touchmove","touchend touchcancel"]:["touchstart mousedown","touchmove mousemove","touchend touchcancel mouseup"],o.EVENT_TYPES[o.EVENT_START]=t[0],o.EVENT_TYPES[o.EVENT_MOVE]=t[1],o.EVENT_TYPES[o.EVENT_END]=t[2]},getTouchList:function(t){return o.HAS_POINTEREVENTS?o.PointerEvent.getTouchList():t.touches?t.touches:[{identifier:1,pageX:t.pageX,pageY:t.pageY,target:t.target}]},collectEventData:function(t,e,i){var s=this.getTouchList(i,e),n=o.POINTER_TOUCH;return(i.type.match(/mouse/)||o.PointerEvent.matchType(o.POINTER_MOUSE,i))&&(n=o.POINTER_MOUSE),{center:o.utils.getCenter(s),timeStamp:(new Date).getTime(),target:i.target,touches:s,eventType:e,pointerType:n,srcEvent:i,preventDefault:function(){this.srcEvent.preventManipulation&&this.srcEvent.preventManipulation(),this.srcEvent.preventDefault&&this.srcEvent.preventDefault()},stopPropagation:function(){this.srcEvent.stopPropagation()},stopDetect:function(){return o.detection.stopDetect()}}}},o.PointerEvent={pointers:{},getTouchList:function(){var t=this,e=[];return Object.keys(t.pointers).sort().forEach(function(i){e.push(t.pointers[i])}),e},updatePointer:function(t,e){return t==o.EVENT_END?this.pointers={}:(e.identifier=e.pointerId,this.pointers[e.pointerId]=e),Object.keys(this.pointers).length},matchType:function(t,e){if(!e.pointerType)return!1;var i={};return i[o.POINTER_MOUSE]=e.pointerType==e.MSPOINTER_TYPE_MOUSE||e.pointerType==o.POINTER_MOUSE,i[o.POINTER_TOUCH]=e.pointerType==e.MSPOINTER_TYPE_TOUCH||e.pointerType==o.POINTER_TOUCH,i[o.POINTER_PEN]=e.pointerType==e.MSPOINTER_TYPE_PEN||e.pointerType==o.POINTER_PEN,i[t]},getEvents:function(){return["pointerdown MSPointerDown","pointermove MSPointerMove","pointerup pointercancel MSPointerUp MSPointerCancel"]},reset:function(){this.pointers={}}},o.utils={extend:function(t,e,s){for(var o in e)t[o]!==i&&s||(t[o]=e[o]);return t},hasParent:function(t,e){for(;t;){if(t==e)return!0;t=t.parentNode}return!1},getCenter:function(t){for(var e=[],i=[],s=0,o=t.length;o>s;s++)e.push(t[s].pageX),i.push(t[s].pageY);return{pageX:(Math.min.apply(Math,e)+Math.max.apply(Math,e))/2,pageY:(Math.min.apply(Math,i)+Math.max.apply(Math,i))/2}},getVelocity:function(t,e,i){return{x:Math.abs(e/t)||0,y:Math.abs(i/t)||0}},getAngle:function(t,e){var i=e.pageY-t.pageY,s=e.pageX-t.pageX;return 180*Math.atan2(i,s)/Math.PI},getDirection:function(t,e){var i=Math.abs(t.pageX-e.pageX),s=Math.abs(t.pageY-e.pageY);return i>=s?t.pageX-e.pageX>0?o.DIRECTION_LEFT:o.DIRECTION_RIGHT:t.pageY-e.pageY>0?o.DIRECTION_UP:o.DIRECTION_DOWN},getDistance:function(t,e){var i=e.pageX-t.pageX,s=e.pageY-t.pageY;return Math.sqrt(i*i+s*s)},getScale:function(t,e){return t.length>=2&&e.length>=2?this.getDistance(e[0],e[1])/this.getDistance(t[0],t[1]):1},getRotation:function(t,e){return t.length>=2&&e.length>=2?this.getAngle(e[1],e[0])-this.getAngle(t[1],t[0]):0},isVertical:function(t){return t==o.DIRECTION_UP||t==o.DIRECTION_DOWN},stopDefaultBrowserBehavior:function(t,e){var i,s=["webkit","khtml","moz","ms","o",""];if(e&&t.style){for(var o=0;oi;i++){var n=this.gestures[i];if(!this.stopped&&e[n.name]!==!1&&n.handler.call(n,t,this.current.inst)===!1){this.stopDetect();break}}return this.current&&(this.current.lastEvent=t),t.eventType==o.EVENT_END&&!t.touches.length-1&&this.stopDetect(),t}},stopDetect:function(){this.previous=o.utils.extend({},this.current),this.current=null,this.stopped=!0},extendEventData:function(t){var e=this.current.startEvent;if(e&&(t.touches.length!=e.touches.length||t.touches===e.touches)){e.touches=[];for(var i=0,s=t.touches.length;s>i;i++)e.touches.push(o.utils.extend({},t.touches[i]))}var n=t.timeStamp-e.timeStamp,r=t.center.pageX-e.center.pageX,a=t.center.pageY-e.center.pageY,h=o.utils.getVelocity(n,r,a);return o.utils.extend(t,{deltaTime:n,deltaX:r,deltaY:a,velocityX:h.x,velocityY:h.y,distance:o.utils.getDistance(e.center,t.center),angle:o.utils.getAngle(e.center,t.center),direction:o.utils.getDirection(e.center,t.center),scale:o.utils.getScale(e.touches,t.touches),rotation:o.utils.getRotation(e.touches,t.touches),startEvent:e}),t},register:function(t){var e=t.defaults||{};return e[t.name]===i&&(e[t.name]=!0),o.utils.extend(o.defaults,e,!0),t.index=t.index||1e3,this.gestures.push(t),this.gestures.sort(function(t,e){return t.indexe.index?1:0}),this.gestures}},o.gestures=o.gestures||{},o.gestures.Hold={name:"hold",index:10,defaults:{hold_timeout:500,hold_threshold:1},timer:null,handler:function(t,e){switch(t.eventType){case o.EVENT_START:clearTimeout(this.timer),o.detection.current.name=this.name,this.timer=setTimeout(function(){"hold"==o.detection.current.name&&e.trigger("hold",t)},e.options.hold_timeout);break;case o.EVENT_MOVE:t.distance>e.options.hold_threshold&&clearTimeout(this.timer);break;case o.EVENT_END:clearTimeout(this.timer)}}},o.gestures.Tap={name:"tap",index:100,defaults:{tap_max_touchtime:250,tap_max_distance:10,tap_always:!0,doubletap_distance:20,doubletap_interval:300},handler:function(t,e){if(t.eventType==o.EVENT_END){var i=o.detection.previous,s=!1;if(t.deltaTime>e.options.tap_max_touchtime||t.distance>e.options.tap_max_distance)return;i&&"tap"==i.name&&t.timeStamp-i.lastEvent.timeStamp0&&t.touches.length>e.options.swipe_max_touches)return;(t.velocityX>e.options.swipe_velocity||t.velocityY>e.options.swipe_velocity)&&(e.trigger(this.name,t),e.trigger(this.name+t.direction,t))}}},o.gestures.Drag={name:"drag",index:50,defaults:{drag_min_distance:10,drag_max_touches:1,drag_block_horizontal:!1,drag_block_vertical:!1,drag_lock_to_axis:!1,drag_lock_min_distance:25},triggered:!1,handler:function(t,e){if(o.detection.current.name!=this.name&&this.triggered)return e.trigger(this.name+"end",t),void(this.triggered=!1);if(!(e.options.drag_max_touches>0&&t.touches.length>e.options.drag_max_touches))switch(t.eventType){case o.EVENT_START:this.triggered=!1;break;case o.EVENT_MOVE:if(t.distancee.options.transform_min_rotation&&e.trigger("rotate",t),i>e.options.transform_min_scale&&(e.trigger("pinch",t),e.trigger("pinch"+(t.scale<1?"in":"out"),t));break;case o.EVENT_END:this.triggered&&e.trigger(this.name+"end",t),this.triggered=!1}}},o.gestures.Touch={name:"touch",index:-1/0,defaults:{prevent_default:!1,prevent_mouseevents:!1},handler:function(t,e){return e.options.prevent_mouseevents&&t.pointerType==o.POINTER_MOUSE?void t.stopDetect():(e.options.prevent_default&&t.preventDefault(),void(t.eventType==o.EVENT_START&&e.trigger(this.name,t)))}},o.gestures.Release={name:"release",index:1/0,handler:function(t,e){t.eventType==o.EVENT_END&&e.trigger(this.name,t)}},"object"==typeof e&&"object"==typeof e.exports?e.exports=o:(t.Hammer=o,"function"==typeof t.define&&t.define.amd&&t.define("hammer",[],function(){return o}))}(this)},{}],4:[function(t,e){var i="undefined"!=typeof self?self:"undefined"!=typeof window?window:{};(function(s){function o(t,e,i){switch(arguments.length){case 2:return null!=t?t:e;case 3:return null!=t?t:null!=e?e:i;default:throw new Error("Implement me")}}function n(){return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function r(t,e){function i(){ge.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+t)}var s=!0;return p(function(){return s&&(i(),s=!1),e.apply(this,arguments)},e)}function a(t,e){return function(i){return g(t.call(this,i),e)}}function h(t,e){return function(i){return this.lang().ordinal(t.call(this,i),e)}}function d(){}function l(t){C(t),p(this,t)}function c(t){var e=w(t),i=e.year||0,s=e.quarter||0,o=e.month||0,n=e.week||0,r=e.day||0,a=e.hour||0,h=e.minute||0,d=e.second||0,l=e.millisecond||0;this._milliseconds=+l+1e3*d+6e4*h+36e5*a,this._days=+r+7*n,this._months=+o+3*s+12*i,this._data={},this._bubble()}function p(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return e.hasOwnProperty("toString")&&(t.toString=e.toString),e.hasOwnProperty("valueOf")&&(t.valueOf=e.valueOf),t}function u(t){var e,i={};for(e in t)t.hasOwnProperty(e)&&Ne.hasOwnProperty(e)&&(i[e]=t[e]);return i}function m(t){return 0>t?Math.ceil(t):Math.floor(t)}function g(t,e,i){for(var s=""+Math.abs(t),o=t>=0;s.lengths;s++)(i&&t[s]!==e[s]||!i&&S(t[s])!==S(e[s]))&&r++;return r+n}function _(t){if(t){var e=t.toLowerCase().replace(/(.)s$/,"$1");t=oi[t]||ni[e]||e}return t}function w(t){var e,i,s={};for(i in t)t.hasOwnProperty(i)&&(e=_(i),e&&(s[e]=t[i]));return s}function x(t){var e,i;if(0===t.indexOf("week"))e=7,i="day";else{if(0!==t.indexOf("month"))return;e=12,i="month"}ge[t]=function(o,n){var r,a,h=ge.fn._lang[t],d=[];if("number"==typeof o&&(n=o,o=s),a=function(t){var e=ge().utc().set(i,t);return h.call(ge.fn._lang,e,o||"")},null!=n)return a(n);for(r=0;e>r;r++)d.push(a(r));return d}}function S(t){var e=+t,i=0;return 0!==e&&isFinite(e)&&(i=e>=0?Math.floor(e):Math.ceil(e)),i}function D(t,e){return new Date(Date.UTC(t,e+1,0)).getUTCDate()}function E(t,e,i){return oe(ge([t,11,31+e-i]),e,i).week}function T(t){return M(t)?366:365}function M(t){return t%4===0&&t%100!==0||t%400===0}function C(t){var e;t._a&&-2===t._pf.overflow&&(e=t._a[xe]<0||t._a[xe]>11?xe:t._a[Se]<1||t._a[Se]>D(t._a[we],t._a[xe])?Se:t._a[De]<0||t._a[De]>23?De:t._a[Ee]<0||t._a[Ee]>59?Ee:t._a[Te]<0||t._a[Te]>59?Te:t._a[Me]<0||t._a[Me]>999?Me:-1,t._pf._overflowDayOfYear&&(we>e||e>Se)&&(e=Se),t._pf.overflow=e)}function N(t){return null==t._isValid&&(t._isValid=!isNaN(t._d.getTime())&&t._pf.overflow<0&&!t._pf.empty&&!t._pf.invalidMonth&&!t._pf.nullInput&&!t._pf.invalidFormat&&!t._pf.userInvalidated,t._strict&&(t._isValid=t._isValid&&0===t._pf.charsLeftOver&&0===t._pf.unusedTokens.length)),t._isValid}function O(t){return t?t.toLowerCase().replace("_","-"):t}function k(t,e){return e._isUTC?ge(t).zone(e._offset||0):ge(t).local()}function L(t,e){return e.abbr=t,Ce[t]||(Ce[t]=new d),Ce[t].set(e),Ce[t]}function I(t){delete Ce[t]}function P(e){var i,s,o,n,r=0,a=function(e){if(!Ce[e]&&Oe)try{t("./lang/"+e)}catch(i){}return Ce[e]};if(!e)return ge.fn._lang;if(!v(e)){if(s=a(e))return s;e=[e]}for(;r0;){if(s=a(n.slice(0,i).join("-")))return s;if(o&&o.length>=i&&b(n,o,!0)>=i-1)break;i--}r++}return ge.fn._lang}function A(t){return t.match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function z(t){var e,i,s=t.match(Pe);for(e=0,i=s.length;i>e;e++)s[e]=li[s[e]]?li[s[e]]:A(s[e]);return function(o){var n=""; +for(e=0;i>e;e++)n+=s[e]instanceof Function?s[e].call(o,t):s[e];return n}}function R(t,e){return t.isValid()?(e=G(e,t.lang()),ri[e]||(ri[e]=z(e)),ri[e](t)):t.lang().invalidDate()}function G(t,e){function i(t){return e.longDateFormat(t)||t}var s=5;for(Ae.lastIndex=0;s>=0&&Ae.test(t);)t=t.replace(Ae,i),Ae.lastIndex=0,s-=1;return t}function F(t,e){var i,s=e._strict;switch(t){case"Q":return je;case"DDDD":return qe;case"YYYY":case"GGGG":case"gggg":return s?Ze:Ge;case"Y":case"G":case"g":return $e;case"YYYYYY":case"YYYYY":case"GGGGG":case"ggggg":return s?Ke:Fe;case"S":if(s)return je;case"SS":if(s)return Xe;case"SSS":if(s)return qe;case"DDD":return Re;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return Ye;case"a":case"A":return P(e._l)._meridiemParse;case"X":return Ve;case"Z":case"ZZ":return Be;case"T":return We;case"SSSS":return He;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"ww":case"WW":return s?Xe:ze;case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"W":case"e":case"E":return ze;case"Do":return Ue;default:return i=new RegExp(q(X(t.replace("\\","")),"i"))}}function H(t){t=t||"";var e=t.match(Be)||[],i=e[e.length-1]||[],s=(i+"").match(ii)||["-",0,0],o=+(60*s[1])+S(s[2]);return"+"===s[0]?-o:o}function Y(t,e,i){var s,o=i._a;switch(t){case"Q":null!=e&&(o[xe]=3*(S(e)-1));break;case"M":case"MM":null!=e&&(o[xe]=S(e)-1);break;case"MMM":case"MMMM":s=P(i._l).monthsParse(e),null!=s?o[xe]=s:i._pf.invalidMonth=e;break;case"D":case"DD":null!=e&&(o[Se]=S(e));break;case"Do":null!=e&&(o[Se]=S(parseInt(e,10)));break;case"DDD":case"DDDD":null!=e&&(i._dayOfYear=S(e));break;case"YY":o[we]=ge.parseTwoDigitYear(e);break;case"YYYY":case"YYYYY":case"YYYYYY":o[we]=S(e);break;case"a":case"A":i._isPm=P(i._l).isPM(e);break;case"H":case"HH":case"h":case"hh":o[De]=S(e);break;case"m":case"mm":o[Ee]=S(e);break;case"s":case"ss":o[Te]=S(e);break;case"S":case"SS":case"SSS":case"SSSS":o[Me]=S(1e3*("0."+e));break;case"X":i._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":i._useUTC=!0,i._tzm=H(e);break;case"dd":case"ddd":case"dddd":s=P(i._l).weekdaysParse(e),null!=s?(i._w=i._w||{},i._w.d=s):i._pf.invalidWeekday=e;break;case"w":case"ww":case"W":case"WW":case"d":case"e":case"E":t=t.substr(0,1);case"gggg":case"GGGG":case"GGGGG":t=t.substr(0,2),e&&(i._w=i._w||{},i._w[t]=S(e));break;case"gg":case"GG":i._w=i._w||{},i._w[t]=ge.parseTwoDigitYear(e)}}function B(t){var e,i,s,n,r,a,h,d;e=t._w,null!=e.GG||null!=e.W||null!=e.E?(r=1,a=4,i=o(e.GG,t._a[we],oe(ge(),1,4).year),s=o(e.W,1),n=o(e.E,1)):(d=P(t._l),r=d._week.dow,a=d._week.doy,i=o(e.gg,t._a[we],oe(ge(),r,a).year),s=o(e.w,1),null!=e.d?(n=e.d,r>n&&++s):n=null!=e.e?e.e+r:r),h=ne(i,s,n,a,r),t._a[we]=h.year,t._dayOfYear=h.dayOfYear}function W(t){var e,i,s,n,r=[];if(!t._d){for(s=U(t),t._w&&null==t._a[Se]&&null==t._a[xe]&&B(t),t._dayOfYear&&(n=o(t._a[we],s[we]),t._dayOfYear>T(n)&&(t._pf._overflowDayOfYear=!0),i=te(n,0,t._dayOfYear),t._a[xe]=i.getUTCMonth(),t._a[Se]=i.getUTCDate()),e=0;3>e&&null==t._a[e];++e)t._a[e]=r[e]=s[e];for(;7>e;e++)t._a[e]=r[e]=null==t._a[e]?2===e?1:0:t._a[e];t._d=(t._useUTC?te:Q).apply(null,r),null!=t._tzm&&t._d.setUTCMinutes(t._d.getUTCMinutes()+t._tzm)}}function V(t){var e;t._d||(e=w(t._i),t._a=[e.year,e.month,e.day,e.hour,e.minute,e.second,e.millisecond],W(t))}function U(t){var e=new Date;return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}function j(t){if(t._f===ge.ISO_8601)return void K(t);t._a=[],t._pf.empty=!0;var e,i,s,o,n,r=P(t._l),a=""+t._i,h=a.length,d=0;for(s=G(t._f,r).match(Pe)||[],e=0;e0&&t._pf.unusedInput.push(n),a=a.slice(a.indexOf(i)+i.length),d+=i.length),li[o]?(i?t._pf.empty=!1:t._pf.unusedTokens.push(o),Y(o,i,t)):t._strict&&!i&&t._pf.unusedTokens.push(o);t._pf.charsLeftOver=h-d,a.length>0&&t._pf.unusedInput.push(a),t._isPm&&t._a[De]<12&&(t._a[De]+=12),t._isPm===!1&&12===t._a[De]&&(t._a[De]=0),W(t),C(t)}function X(t){return t.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,i,s,o){return e||i||s||o})}function q(t){return t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function Z(t){var e,i,s,o,r;if(0===t._f.length)return t._pf.invalidFormat=!0,void(t._d=new Date(0/0));for(o=0;or)&&(s=r,i=e));p(t,i||e)}function K(t){var e,i,s=t._i,o=Je.exec(s);if(o){for(t._pf.iso=!0,e=0,i=ti.length;i>e;e++)if(ti[e][1].exec(s)){t._f=ti[e][0]+(o[6]||" ");break}for(e=0,i=ei.length;i>e;e++)if(ei[e][1].exec(s)){t._f+=ei[e][0];break}s.match(Be)&&(t._f+="Z"),j(t)}else t._isValid=!1}function $(t){K(t),t._isValid===!1&&(delete t._isValid,ge.createFromInputFallback(t))}function J(t){var e=t._i,i=ke.exec(e);e===s?t._d=new Date:i?t._d=new Date(+i[1]):"string"==typeof e?$(t):v(e)?(t._a=e.slice(0),W(t)):y(e)?t._d=new Date(+e):"object"==typeof e?V(t):"number"==typeof e?t._d=new Date(e):ge.createFromInputFallback(t)}function Q(t,e,i,s,o,n,r){var a=new Date(t,e,i,s,o,n,r);return 1970>t&&a.setFullYear(t),a}function te(t){var e=new Date(Date.UTC.apply(null,arguments));return 1970>t&&e.setUTCFullYear(t),e}function ee(t,e){if("string"==typeof t)if(isNaN(t)){if(t=e.weekdaysParse(t),"number"!=typeof t)return null}else t=parseInt(t,10);return t}function ie(t,e,i,s,o){return o.relativeTime(e||1,!!i,t,s)}function se(t,e,i){var s=_e(Math.abs(t)/1e3),o=_e(s/60),n=_e(o/60),r=_e(n/24),a=_e(r/365),h=s0,h[4]=i,ie.apply({},h)}function oe(t,e,i){var s,o=i-e,n=i-t.day();return n>o&&(n-=7),o-7>n&&(n+=7),s=ge(t).add("d",n),{week:Math.ceil(s.dayOfYear()/7),year:s.year()}}function ne(t,e,i,s,o){var n,r,a=te(t,0,1).getUTCDay();return a=0===a?7:a,i=null!=i?i:o,n=o-a+(a>s?7:0)-(o>a?7:0),r=7*(e-1)+(i-o)+n+1,{year:r>0?t:t-1,dayOfYear:r>0?r:T(t-1)+r}}function re(t){var e=t._i,i=t._f;return null===e||i===s&&""===e?ge.invalid({nullInput:!0}):("string"==typeof e&&(t._i=e=P().preparse(e)),ge.isMoment(e)?(t=u(e),t._d=new Date(+e._d)):i?v(i)?Z(t):j(t):J(t),new l(t))}function ae(t,e){var i,s;if(1===e.length&&v(e[0])&&(e=e[0]),!e.length)return ge();for(i=e[0],s=1;s=0?"+":"-";return e+g(Math.abs(t),6)},gg:function(){return g(this.weekYear()%100,2)},gggg:function(){return g(this.weekYear(),4)},ggggg:function(){return g(this.weekYear(),5)},GG:function(){return g(this.isoWeekYear()%100,2)},GGGG:function(){return g(this.isoWeekYear(),4)},GGGGG:function(){return g(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return S(this.milliseconds()/100)},SS:function(){return g(S(this.milliseconds()/10),2)},SSS:function(){return g(this.milliseconds(),3)},SSSS:function(){return g(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+g(S(t/60),2)+":"+g(S(t)%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+g(S(t/60),2)+g(S(t)%60,2)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()},Q:function(){return this.quarter()}},ci=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"];hi.length;)ve=hi.pop(),li[ve+"o"]=h(li[ve],ve);for(;di.length;)ve=di.pop(),li[ve+ve]=a(li[ve],2);for(li.DDDD=a(li.DDD,3),p(d.prototype,{set:function(t){var e,i;for(i in t)e=t[i],"function"==typeof e?this[i]=e:this["_"+i]=e},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(t){return this._months[t.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(t){return this._monthsShort[t.month()]},monthsParse:function(t){var e,i,s;for(this._monthsParse||(this._monthsParse=[]),e=0;12>e;e++)if(this._monthsParse[e]||(i=ge.utc([2e3,e]),s="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[e]=new RegExp(s.replace(".",""),"i")),this._monthsParse[e].test(t))return e},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(t){return this._weekdays[t.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(t){return this._weekdaysShort[t.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(t){return this._weekdaysMin[t.day()]},weekdaysParse:function(t){var e,i,s;for(this._weekdaysParse||(this._weekdaysParse=[]),e=0;7>e;e++)if(this._weekdaysParse[e]||(i=ge([2e3,1]).day(e),s="^"+this.weekdays(i,"")+"|^"+this.weekdaysShort(i,"")+"|^"+this.weekdaysMin(i,""),this._weekdaysParse[e]=new RegExp(s.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(t){var e=this._longDateFormat[t];return!e&&this._longDateFormat[t.toUpperCase()]&&(e=this._longDateFormat[t.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t]=e),e},isPM:function(t){return"p"===(t+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(t,e,i){return t>11?i?"pm":"PM":i?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(t,e){var i=this._calendar[t];return"function"==typeof i?i.apply(e):i},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(t,e,i,s){var o=this._relativeTime[i];return"function"==typeof o?o(t,e,i,s):o.replace(/%d/i,t)},pastFuture:function(t,e){var i=this._relativeTime[t>0?"future":"past"];return"function"==typeof i?i(e):i.replace(/%s/i,e)},ordinal:function(t){return this._ordinal.replace("%d",t)},_ordinal:"%d",preparse:function(t){return t},postformat:function(t){return t},week:function(t){return oe(t,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),ge=function(t,e,i,o){var r;return"boolean"==typeof i&&(o=i,i=s),r={},r._isAMomentObject=!0,r._i=t,r._f=e,r._l=i,r._strict=o,r._isUTC=!1,r._pf=n(),re(r)},ge.suppressDeprecationWarnings=!1,ge.createFromInputFallback=r("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(t){t._d=new Date(t._i)}),ge.min=function(){var t=[].slice.call(arguments,0);return ae("isBefore",t)},ge.max=function(){var t=[].slice.call(arguments,0);return ae("isAfter",t)},ge.utc=function(t,e,i,o){var r;return"boolean"==typeof i&&(o=i,i=s),r={},r._isAMomentObject=!0,r._useUTC=!0,r._isUTC=!0,r._l=i,r._i=t,r._f=e,r._strict=o,r._pf=n(),re(r).utc()},ge.unix=function(t){return ge(1e3*t)},ge.duration=function(t,e){var i,s,o,n=t,r=null;return ge.isDuration(t)?n={ms:t._milliseconds,d:t._days,M:t._months}:"number"==typeof t?(n={},e?n[e]=t:n.milliseconds=t):(r=Le.exec(t))?(i="-"===r[1]?-1:1,n={y:0,d:S(r[Se])*i,h:S(r[De])*i,m:S(r[Ee])*i,s:S(r[Te])*i,ms:S(r[Me])*i}):(r=Ie.exec(t))&&(i="-"===r[1]?-1:1,o=function(t){var e=t&&parseFloat(t.replace(",","."));return(isNaN(e)?0:e)*i},n={y:o(r[2]),M:o(r[3]),d:o(r[4]),h:o(r[5]),m:o(r[6]),s:o(r[7]),w:o(r[8])}),s=new c(n),ge.isDuration(t)&&t.hasOwnProperty("_lang")&&(s._lang=t._lang),s},ge.version=ye,ge.defaultFormat=Qe,ge.ISO_8601=function(){},ge.momentProperties=Ne,ge.updateOffset=function(){},ge.relativeTimeThreshold=function(t,e){return ai[t]===s?!1:(ai[t]=e,!0)},ge.lang=function(t,e){var i;return t?(e?L(O(t),e):null===e?(I(t),t="en"):Ce[t]||P(t),i=ge.duration.fn._lang=ge.fn._lang=P(t),i._abbr):ge.fn._lang._abbr},ge.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),P(t)},ge.isMoment=function(t){return t instanceof l||null!=t&&t.hasOwnProperty("_isAMomentObject")},ge.isDuration=function(t){return t instanceof c},ve=ci.length-1;ve>=0;--ve)x(ci[ve]);ge.normalizeUnits=function(t){return _(t)},ge.invalid=function(t){var e=ge.utc(0/0);return null!=t?p(e._pf,t):e._pf.userInvalidated=!0,e},ge.parseZone=function(){return ge.apply(null,arguments).parseZone()},ge.parseTwoDigitYear=function(t){return S(t)+(S(t)>68?1900:2e3)},p(ge.fn=l.prototype,{clone:function(){return ge(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().lang("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){var t=ge(this).utc();return 00:!1},parsingFlags:function(){return p({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(t){var e=R(this,t||ge.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var i;return i="string"==typeof t&&"string"==typeof e?ge.duration(isNaN(+e)?+t:+e,isNaN(+e)?e:t):"string"==typeof t?ge.duration(+e,t):ge.duration(t,e),f(this,i,1),this},subtract:function(t,e){var i;return i="string"==typeof t&&"string"==typeof e?ge.duration(isNaN(+e)?+t:+e,isNaN(+e)?e:t):"string"==typeof t?ge.duration(+e,t):ge.duration(t,e),f(this,i,-1),this},diff:function(t,e,i){var s,o,n=k(t,this),r=6e4*(this.zone()-n.zone());return e=_(e),"year"===e||"month"===e?(s=432e5*(this.daysInMonth()+n.daysInMonth()),o=12*(this.year()-n.year())+(this.month()-n.month()),o+=(this-ge(this).startOf("month")-(n-ge(n).startOf("month")))/s,o-=6e4*(this.zone()-ge(this).startOf("month").zone()-(n.zone()-ge(n).startOf("month").zone()))/s,"year"===e&&(o/=12)):(s=this-n,o="second"===e?s/1e3:"minute"===e?s/6e4:"hour"===e?s/36e5:"day"===e?(s-r)/864e5:"week"===e?(s-r)/6048e5:s),i?o:m(o)},from:function(t,e){return ge.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(ge(),t)},calendar:function(t){var e=t||ge(),i=k(e,this).startOf("day"),s=this.diff(i,"days",!0),o=-6>s?"sameElse":-1>s?"lastWeek":0>s?"lastDay":1>s?"sameDay":2>s?"nextDay":7>s?"nextWeek":"sameElse";return this.format(this.lang().calendar(o,this))},isLeapYear:function(){return M(this.year())},isDST:function(){return this.zone()+ge(t).startOf(e)},isBefore:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)<+ge(t).startOf(e)},isSame:function(t,e){return e=e||"ms",+this.clone().startOf(e)===+k(t,this).startOf(e)},min:r("moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",function(t){return t=ge.apply(null,arguments),this>t?this:t}),max:r("moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(t){return t=ge.apply(null,arguments),t>this?this:t}),zone:function(t,e){var i=this._offset||0;return null==t?this._isUTC?i:this._d.getTimezoneOffset():("string"==typeof t&&(t=H(t)),Math.abs(t)<16&&(t=60*t),this._offset=t,this._isUTC=!0,i!==t&&(!e||this._changeInProgress?f(this,ge.duration(i-t,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,ge.updateOffset(this,!0),this._changeInProgress=null)),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return this._tzm?this.zone(this._tzm):"string"==typeof this._i&&this.zone(this._i),this},hasAlignedHourOffset:function(t){return t=t?ge(t).zone():0,(this.zone()-t)%60===0},daysInMonth:function(){return D(this.year(),this.month())},dayOfYear:function(t){var e=_e((ge(this).startOf("day")-ge(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},quarter:function(t){return null==t?Math.ceil((this.month()+1)/3):this.month(3*(t-1)+this.month()%3)},weekYear:function(t){var e=oe(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==t?e:this.add("y",t-e)},isoWeekYear:function(t){var e=oe(this,1,4).year;return null==t?e:this.add("y",t-e)},week:function(t){var e=this.lang().week(this);return null==t?e:this.add("d",7*(t-e))},isoWeek:function(t){var e=oe(this,1,4).week;return null==t?e:this.add("d",7*(t-e))},weekday:function(t){var e=(this.day()+7-this.lang()._week.dow)%7;return null==t?e:this.add("d",t-e)},isoWeekday:function(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)},isoWeeksInYear:function(){return E(this.year(),1,4)},weeksInYear:function(){var t=this._lang._week;return E(this.year(),t.dow,t.doy)},get:function(t){return t=_(t),this[t]()},set:function(t,e){return t=_(t),"function"==typeof this[t]&&this[t](e),this},lang:function(t){return t===s?this._lang:(this._lang=P(t),this)}}),ge.fn.millisecond=ge.fn.milliseconds=ce("Milliseconds",!1),ge.fn.second=ge.fn.seconds=ce("Seconds",!1),ge.fn.minute=ge.fn.minutes=ce("Minutes",!1),ge.fn.hour=ge.fn.hours=ce("Hours",!0),ge.fn.date=ce("Date",!0),ge.fn.dates=r("dates accessor is deprecated. Use date instead.",ce("Date",!0)),ge.fn.year=ce("FullYear",!0),ge.fn.years=r("years accessor is deprecated. Use year instead.",ce("FullYear",!0)),ge.fn.days=ge.fn.day,ge.fn.months=ge.fn.month,ge.fn.weeks=ge.fn.week,ge.fn.isoWeeks=ge.fn.isoWeek,ge.fn.quarters=ge.fn.quarter,ge.fn.toJSON=ge.fn.toISOString,p(ge.duration.fn=c.prototype,{_bubble:function(){var t,e,i,s,o=this._milliseconds,n=this._days,r=this._months,a=this._data;a.milliseconds=o%1e3,t=m(o/1e3),a.seconds=t%60,e=m(t/60),a.minutes=e%60,i=m(e/60),a.hours=i%24,n+=m(i/24),a.days=n%30,r+=m(n/30),a.months=r%12,s=m(r/12),a.years=s},weeks:function(){return m(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*S(this._months/12)},humanize:function(t){var e=+this,i=se(e,!t,this.lang());return t&&(i=this.lang().pastFuture(e,i)),this.lang().postformat(i)},add:function(t,e){var i=ge.duration(t,e);return this._milliseconds+=i._milliseconds,this._days+=i._days,this._months+=i._months,this._bubble(),this},subtract:function(t,e){var i=ge.duration(t,e);return this._milliseconds-=i._milliseconds,this._days-=i._days,this._months-=i._months,this._bubble(),this},get:function(t){return t=_(t),this[t.toLowerCase()+"s"]()},as:function(t){return t=_(t),this["as"+t.charAt(0).toUpperCase()+t.slice(1)+"s"]()},lang:ge.fn.lang,toIsoString:function(){var t=Math.abs(this.years()),e=Math.abs(this.months()),i=Math.abs(this.days()),s=Math.abs(this.hours()),o=Math.abs(this.minutes()),n=Math.abs(this.seconds()+this.milliseconds()/1e3);return this.asSeconds()?(this.asSeconds()<0?"-":"")+"P"+(t?t+"Y":"")+(e?e+"M":"")+(i?i+"D":"")+(s||o||n?"T":"")+(s?s+"H":"")+(o?o+"M":"")+(n?n+"S":""):"P0D"}});for(ve in si)si.hasOwnProperty(ve)&&(ue(ve,si[ve]),pe(ve.toLowerCase()));ue("Weeks",6048e5),ge.duration.fn.asMonths=function(){return(+this-31536e6*this.years())/2592e6+12*this.years()},ge.lang("en",{ordinal:function(t){var e=t%10,i=1===S(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th";return t+i}}),Oe?e.exports=ge:"function"==typeof define&&define.amd?(define("moment",function(t,e,i){return i.config&&i.config()&&i.config().noGlobal===!0&&(be.moment=fe),ge}),me(!0)):me()}).call(this)},{}],5:[function(t,e){function i(t,e,i){return t.addEventListener?t.addEventListener(e,i,!1):void t.attachEvent("on"+e,i)}function s(t){return"keypress"==t.type?String.fromCharCode(t.which):w[t.which]?w[t.which]:x[t.which]?x[t.which]:String.fromCharCode(t.which).toLowerCase()}function o(t){var e=t.target||t.srcElement,i=e.tagName;return(" "+e.className+" ").indexOf(" mousetrap ")>-1?!1:"INPUT"==i||"SELECT"==i||"TEXTAREA"==i||e.contentEditable&&"true"==e.contentEditable}function n(t,e){return t.sort().join(",")===e.sort().join(",")}function r(t){t=t||{};var e,i=!1;for(e in M)t[e]?i=!0:M[e]=0;i||(N=!1)}function a(t,e,i,s,o){var r,a,h=[];if(!E[t])return[];for("keyup"==i&&p(t)&&(e=[t]),r=0;r95&&112>t||w.hasOwnProperty(t)&&(b[w[t]]=t)}return b}function g(t,e,i){return i||(i=m()[t]?"keydown":"keypress"),"keypress"==i&&e.length&&(i="keydown"),i}function f(t,e,i,o){M[t]=0,o||(o=g(e[0],[]));var n,a=function(){N=o,++M[t],u()},h=function(t){d(i,t),"keyup"!==o&&(C=s(t)),setTimeout(r,10)};for(n=0;n1)return f(t,d,e,i);for(h="+"===t?["+"]:t.split("+"),n=0;n":".","?":"/","|":"\\"},D={option:"alt",command:"meta","return":"enter",escape:"esc"},E={},T={},M={},C=!1,N=!1,O=1;20>O;++O)w[111+O]="f"+O;for(O=0;9>=O;++O)w[O+96]=O;i(document,"keypress",c),i(document,"keydown",c),i(document,"keyup",c);var k={bind:function(t,e,i){return y(t instanceof Array?t:[t],e,i),T[t+":"+i]=e,this},unbind:function(t,e){return T[t+":"+e]&&(delete T[t+":"+e],this.bind(t,function(){},e)),this},trigger:function(t,e){return T[t+":"+e](),this},reset:function(){return E={},T={},this}};e.exports=k},{}]},{},[1])(1)}); \ No newline at end of file diff --git a/docs/dataset.html b/docs/dataset.html index 723affae..220c2bd3 100644 --- a/docs/dataset.html +++ b/docs/dataset.html @@ -220,6 +220,17 @@ var data = new vis.DataSet([data] [, options]) + + + getDataSet() + + DataSet + + Get the DataSet itself. In case of a DataView, this function does not + return the DataSet to which the DataView is connected. + + + getIds([options]) 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. - - diff --git a/docs/graph2d.html b/docs/graph2d.html new file mode 100644 index 00000000..9c3f6cfc --- /dev/null +++ b/docs/graph2d.html @@ -0,0 +1,859 @@ + + + + vis.js | Graph2d documentation + + + + + + + + +
+ +

Graph2d documentation

+ +

Overview

+

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

+ +

Contents

+ + +

Example

+

+ The following code shows how to create a Graph2d and provide it with data. + More examples can be found in the examples directory. +

+ +
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Graph2d | Basic Example</title>
+
+  <style type="text/css">
+    body, html {
+      font-family: sans-serif;
+    }
+  </style>
+
+  <script src="../../dist/vis.js"></script>
+  <link href="../../dist/vis.css" rel="stylesheet" type="text/css" />
+</head>
+<body>
+<div id="visualization"></div>
+
+<script type="text/javascript">
+  var container = document.getElementById('visualization');
+  var items = [
+      {x: '2014-06-11', y: 10},
+      {x: '2014-06-12', y: 25},
+      {x: '2014-06-13', y: 30},
+      {x: '2014-06-14', y: 10},
+      {x: '2014-06-15', y: 15},
+      {x: '2014-06-16', y: 30}
+  ];
+
+  var dataset = new vis.DataSet(items);
+  var options = {
+      start: '2014-06-10',
+      end: '2014-06-18'
+  };
+  var graph2d = new vis.Graph2d(container, dataset, options);
+</script>
+</body>
+</html>
+
+
+ + +

Loading

+ +

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

+
    +
  • Items containing a set of points to be displayed.
  • +
  • Groups containing a set of groups used to group items + together. All items belonging to a group will be drawn as a single graph.
  • +
+ +

Items

+ +
+var items = [
+    {x: '2014-06-13', y: 30, group: 0},
+    {x: '2014-06-14', y: 10, group: 0},
+    {x: '2014-06-15', y: 15, group: 1},
+    {x: '2014-06-16', y: 30, group: 1},
+    {x: '2014-06-17', y: 10, group: 1},
+    {x: '2014-06-18', y: 15, group: 1}
+];
+
+ +
Name
+ + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeRequiredDescription
xnumberyesLocation on the x-axis.
ynumberyesLocation on the y-axis.
groupnumber | stringnoThe ID of the group this point belongs to.
+ +

Groups

+ +

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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeRequiredDescription
idString | NumberyesAn id for the group. The group will display all items having a + property group which matches the id + of the group.
contentStringyesThe contents of the group. This can be plain text or html code.
classNameStringnoThis field is optional. A className can be used to give groups + an individual css style. +
optionsJSON objectnoThis 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. + +
+var options = {
+    width:  '100%',
+    height: '400px',
+    style: 'surface'
+};
+
+ +The options colored in green can also be used as options for the groups. All options are optional. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
yAxisOrientationString'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.
defaultGroupString'default'This is the label for the default, ungrouped items when shown in a legend.
sortBooleantrueThis 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.
samplingBooleantrueIf 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.
graphHeightNumber | 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.
shadedBoolean | ObjectfalseToggle a shaded area with the default settings.
shaded.enabledBooleanfalseThis toggles the shading.
shaded.orientationString'bottom'This determines if the shaded area is at the bottom or at the top of the curve. The options are 'bottom' or 'top'.
styleString'line'This allows the user to define if this should be a linegraph or a barchart. The options are: 'line' or 'bar'.
barChart.widthNumber50The width of the bars.
barChart.alignString'center'The alignment of the bars with regards to the coordinate. The options are 'left', 'right' or 'center'.
catmullRomBoolean | ObjecttrueToggle the interpolation with the default settings. For more customization use the JSON format.
catmullRom.enabledBooleantrueToggle the interpolation.
catmullRom.parametrizationString'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.
drawPointsBoolean | ObjecttrueToggle the drawing of the datapoints with the default settings.
drawPoints.enabledBooleantrueToggle the drawing of the datapoints.
drawPoints.sizeNumber6Determine the size at which the data points are drawn.
drawPoints.styleString'square'Determine the shape of the data points. The options are 'square' or 'circle'.
dataAxis.showMinorLabelsBooleantrueToggle the drawing of the minor labels on the Y axis.
dataAxis.showMajorLabelsBooleantrueToggle the drawing of the major labels on the Y axis.
dataAxis.iconsBooleanfalseToggle the drawing of automatically generated icons the Y axis.
dataAxis.widthNumber | String'40px'Set the (minimal) width of the yAxis. The axis will resize to accomodate the labels of the Y values.
dataAxis.visibleBooleantrueShow or hide the data axis.
legendBooleanfalseToggle the legend with the default settings.
legend.enabledBooleanfalseToggle the legend.
legend.iconsBooleantrueShow automatically generated icons on the legend.
legend.left.visibleBooleantrueBoth axis, left and right, have a corresponding legend. This toggles the visibility of the legend that is coupled with the left axis.
legend.left.positionString'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.visibleBooleantrueThis toggles the visibility of the legend that is coupled with the right axis.
legend.right.positionString'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. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDefaultDescription
autoResizebooleantrueIf 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().
endDate | Number | StringnoneThe 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.
heightNumber | StringnoneThe 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.axisNumber20The minimal margin in pixels between items and the time axis.
margin.itemNumber10The minimal margin in pixels between items.
maxDate | Number | StringnoneSet a maximum Date for the visible range. + It will not be possible to move beyond this maximum. +
maxHeightNumber | StringnoneSpecifies the maximum height for the Timeline. Can be a number in pixels or a string like "300px".
minDate | Number | StringnoneSet a minimum Date for the visible range. + It will not be possible to move beyond this minimum. +
minHeightNumber | StringnoneSpecifies the minimum height for the Timeline. Can be a number in pixels or a string like "300px".
orientationString'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.
showCurrentTimebooleantrueShow a vertical bar at the current time.
showCustomTimebooleanfalseShow 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.
showMajorLabelsbooleantrueBy 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.
showMinorLabelsbooleantrueBy 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.
startDate | Number | StringnoneThe initial start date for the axis of the timeline. + If not provided, the earliest date present in the events is taken as start date.
widthString'100%'The width of the timeline in pixels or as a percentage.
zoomMaxNumber315360000000000Set 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. +
zoomMinNumber10Set 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. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodReturn TypeDescription
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()noneDestroy the Graph2d. The Graph2d is removed from memory. all DOM elements and event listeners are cleaned up. +
getCustomTime()DateRetrieve the custom time. Only applicable when the option showCustomTime is true. +
setCustomTime(time)noneAdjust the custom time bar. Only applicable when the option showCustomTime is true. time is a Date object. +
getWindow()ObjectGet the current visible window. Returns an object with properties start: Date and end: Date.
on(event, callback)noneCreate 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)noneRemove an event listener created before via function on(event, callback). See section Events for more information.
redraw()noneForce a redraw of the Graph2d. Can be useful to manually redraw when option autoResize=false. +
setGroups(groups)noneSet 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)noneSet a data set with items for the Graph2d. + items can be an Array with Objects, + a DataSet, or a DataView. +
setOptions(options)noneSet 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)noneSet 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. +

+ +
+Graph2d.on('select', function (properties) {
+  alert('selected items: ' + properties.nodes);
+});
+
+ +

+ A listener can be removed via the function off: +

+ +
+function onChange (properties) {
+  alert('changed!');
+}
+
+// add event listener
+Graph2d.on('rangechanged', onChange);
+
+// do stuff...
+
+// remove event listener
+Graph2d.off('rangechanged', onChange);
+
+ + +

+ The following events are available. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
nameDescriptionProperties
rangechangeFired 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.
  • +
+
rangechangedFired 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.
  • +
+
timechangeFired repeatedly when the user is dragging the custom time bar. + Only available when the custom time bar is enabled. + +
    +
  • time (Date): the current time.
  • +
+
timechangedFired 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. +

+ + + + diff --git a/docs/graph3d.html b/docs/graph3d.html index d2bda8cb..e103aefd 100644 --- a/docs/graph3d.html +++ b/docs/graph3d.html @@ -18,8 +18,11 @@

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 85% rename from docs/graph.html rename to docs/network.html index 23d49b6a..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 = { - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + @@ -1214,12 +1216,12 @@ var options = { - - - - - - + + + + + + @@ -1235,6 +1237,12 @@ var options = { + + + + + +
      Default Description
      arrowScaleFactorNumber1If you are using arrows, this will scale the arrow. Values < 1 give smaller arrows, > 1 larger arrows. Default: 1.
      colorString | ObjectObjectColors of the edge. This object contains both colors for the selected and unselected state.
      arrowScaleFactorNumber1If you are using arrows, this will scale the arrow. Values < 1 give smaller arrows, > 1 larger arrows. Default: 1.
      colorString | ObjectObjectColors of the edge. This object contains both colors for the selected and unselected state.
      color.colorString"#848484"Color of the edge when not selected.
      color.colorString"#848484"Color of the edge when not selected.
      color.highlightString"#848484"Color of the edge when selected.
      color.highlightString"#848484"Color of the edge when selected.
      dashDefault length of a gap in pixels on a dashed line. Only applicable when the line style is dash-line.
      lengthnumberphysics.[method].springLengthThe 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.
      lengthnumberphysics.[method].springLengthThe 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.
      style1 The default width of a edge.
      widthSelectionMultiplierNumber2Determines 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:
      + + + + + + + + + + + + + + - + + + + + + + + - + @@ -1493,9 +1553,9 @@ var options = {
      NameTypeDefaultDescription
      centralGravityNumber0.5The central gravity is a force that pulls all nodes to the center. This ensures independent groups do not float apart.
      nodeDistance Number10060 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.
      springLengthNumber100In 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 Number0.050.01 This is the spring constant used to calculate the spring forces based on Hooke′s Law. More information is available here.

      Configuration:

      -Every dataset is different. Nodes can have different sizes based on content, interconnectivity can be high or low etc. Because of this, graph has a special option +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 @@ -1622,12 +1682,12 @@ var options: { An code snippet from example 21 is shown below.

      -graph.on("resize", function(params) {console.log(params.width,params.height)});
      +network.on("resize", function(params) {console.log(params.width,params.height)});
       

      Clustering

      - The graph now supports dynamic clustering of nodes. This allows a user to view a very large dataset (> 50.000 nodes) without + 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. @@ -1639,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.

      @@ -1802,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.

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

      @@ -1827,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.

      @@ -1881,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.

      @@ -1943,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. @@ -2068,7 +2128,7 @@ var options: {

      Methods

      - Graph supports the following methods. + Network supports the following methods.

      @@ -2097,19 +2157,19 @@ var options: { - - @@ -2127,7 +2187,7 @@ var options: { - + @@ -2144,21 +2204,41 @@ var options: { - + + + + + + + + + + - - + @@ -2171,14 +2251,14 @@ 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) objectThis 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) objectThis 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.
      redraw() noneRedraw the graph. Useful when the layout of the webpage changed.Redraw the network. Useful when the layout of the webpage changed.
      setOptions(options) noneSet 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])noneSelect 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)noneSelect 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) noneSelect 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)
      zoomExtent() noneScales 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.

      @@ -2187,7 +2267,7 @@ var options: {

      -graph.on('select', function (properties) {
      +network.on('select', function (properties) {
         alert('selected nodes: ' + properties.nodes);
       });
       
      @@ -2202,12 +2282,12 @@ function onSelect (properties) { } // add event listener -graph.on('select', onSelect); +network.on('select', onSelect); // do stuff... // remove event listener -graph.off('select', onSelect); +network.off('select', onSelect);
      @@ -2286,7 +2366,7 @@ graph.off('select', onSelect); stabilized - Fired when the graph has been stabilized after initialization. This event can be used to trigger the .storePosition() function after stabilization. + Fired when the network has been stabilized after initialization. This event can be used to trigger the .storePosition() function after stabilization.
      • iterations: number of iterations used to stabilize
      • @@ -2295,14 +2375,14 @@ graph.off('select', onSelect); viewChanged - 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 6907887d..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

            @@ -111,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 @@ -171,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: +
              +.vis.timeline .red {
              +  color: white;
              +  background-color: red;
              +  border-color: darkred;
              +}
              + More details on how to style items can be found in the section + Styles. + - 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 @@ -192,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 @@ -218,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: -
              -.vis.timeline .red {
              -  color: white;
              -  background-color: red;
              -  border-color: darkred;
              -}
              - More details on how to style items can be found in the section - Styles. + 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. @@ -273,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. - className String @@ -303,6 +300,28 @@ var groups = [ Styles. + + content + String + yes + 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. + + @@ -330,132 +349,132 @@ var options = { - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + @@ -470,41 +489,41 @@ var options = { - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + diff --git a/examples/graph/14_dot_language.html b/examples/graph/14_dot_language.html deleted file mode 100644 index 11bf5763..00000000 --- a/examples/graph/14_dot_language.html +++ /dev/null @@ -1,18 +0,0 @@ - - - Graph | DOT Language - - - - -
              - - - - diff --git a/examples/graph2d/01_basic.html b/examples/graph2d/01_basic.html new file mode 100644 index 00000000..45619ca0 --- /dev/null +++ b/examples/graph2d/01_basic.html @@ -0,0 +1,53 @@ + + + + + + + Graph2d | Basic Example + + + + + + + +

              Graph2d | Basic Example

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

              +
              + + + + \ No newline at end of file diff --git a/examples/graph2d/default.css b/examples/graph2d/default.css new file mode 100644 index 00000000..f7afb828 --- /dev/null +++ b/examples/graph2d/default.css @@ -0,0 +1,87 @@ +html, body { + width: 100%; + height: 100%; + padding: 0; + margin: 0; +} + +body, td, th { + font-family: arial, sans-serif; + font-size: 11pt; + color: #4D4D4D; + line-height: 1.7em; +} + +#container { + margin: 0 auto; + padding-bottom: 50px; + width: 900px; +} + +h1 { + font-size: 180%; + font-weight: bold; + padding: 0; + margin: 1em 0 1em 0; +} + +h2 { + padding-top: 20px; + padding-bottom: 10px; + border-bottom: 1px solid #a0c0f0; + color: #2B7CE9; +} + +h3 { + font-size: 140%; +} + + +a { + color: #2B7CE9; + text-decoration: none; +} +a:visited { + color: #2E60A4; +} +a:hover { + color: red; + text-decoration: underline; +} + +hr { + border: none 0; + border-top: 1px solid #abc; + height: 1px; +} + +pre { + display: block; + font-size: 10pt; + line-height: 1.5em; + font-family: monospace; +} + +pre, code { + background-color: #f5f5f5; +} + +table +{ + border-collapse: collapse; +} + +th { + font-weight: bold; + border: 1px solid lightgray; + background-color: #E5E5E5; + text-align: left; + vertical-align: top; + padding: 5px; +} + +td { + border: 1px solid lightgray; + padding: 5px; + vertical-align: top; +} diff --git a/examples/graph2d/index.html b/examples/graph2d/index.html new file mode 100644 index 00000000..784f56f6 --- /dev/null +++ b/examples/graph2d/index.html @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/examples/index.html b/examples/index.html index cd8638ad..65d5c2d0 100644 --- a/examples/index.html +++ b/examples/index.html @@ -12,7 +12,8 @@

              vis.js examples

              -

              graph

              +

              network

              +

              graph2d

              graph3d

              timeline

              diff --git a/examples/graph/01_basic_usage.html b/examples/network/01_basic_usage.html similarity index 76% rename from examples/graph/01_basic_usage.html rename to examples/network/01_basic_usage.html index 83b15d06..b22b11b1 100644 --- a/examples/graph/01_basic_usage.html +++ b/examples/network/01_basic_usage.html @@ -1,12 +1,12 @@ - Graph | Basic usage + Network | Basic usage + + + +

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

              +
              +.vis.timeline .item.range .content {
              +  overflow: visible;
              +}
              +
              + +
              + + + + \ No newline at end of file diff --git a/examples/timeline/index.html b/examples/timeline/index.html index 88ba04b7..79b67820 100644 --- a/examples/timeline/index.html +++ b/examples/timeline/index.html @@ -29,6 +29,7 @@

              15_item_class_names.html

              16_navigation_menu.html

              17_data_serialization.html

              +

              18_range_overflow.html

              requirejs_example.html

              diff --git a/misc/how_to_publish.md b/misc/how_to_publish.md index 0c91d03b..b7b4414c 100644 --- a/misc/how_to_publish.md +++ b/misc/how_to_publish.md @@ -21,7 +21,7 @@ This generates the vis.js library in the folder `./dist`. npm test -- Open some of the example in your browser and visually check if it works as expected. +- Open some of the examples in your browser and visually check if it works as expected. ## Commit diff --git a/package.json b/package.json index 801a1dfc..d4a877c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vis", - "version": "2.0.0", + "version": "3.0.0", "description": "A dynamic, browser-based visualization library.", "homepage": "http://visjs.org/", "repository": { diff --git a/src/DOMutil.js b/src/DOMutil.js new file mode 100644 index 00000000..a7bf81e1 --- /dev/null +++ b/src/DOMutil.js @@ -0,0 +1,163 @@ +/** + * Created by Alex on 6/20/14. + */ + +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 = []; + } + } +}; + +/** + * 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 { + // 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; +}; + + +/** + * 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); + } + } + 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); + } + JSONcontainer[elementType].used.push(element); + return element; +}; + + + + +/** + * 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. + * + * @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 + * + * @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); +}; \ No newline at end of file diff --git a/src/DataSet.js b/src/DataSet.js index 996622d3..1ce20953 100644 --- a/src/DataSet.js +++ b/src/DataSet.js @@ -511,6 +511,14 @@ DataSet.prototype.getIds = function (options) { 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 diff --git a/src/DataView.js b/src/DataView.js index e7ff81ec..3a934f89 100644 --- a/src/DataView.js +++ b/src/DataView.js @@ -187,6 +187,19 @@ DataView.prototype.getIds = function (options) { 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 diff --git a/src/graph/graphMixins/physics/HierarchialRepulsion.js b/src/graph/graphMixins/physics/HierarchialRepulsion.js deleted file mode 100644 index 8ccb40b8..00000000 --- a/src/graph/graphMixins/physics/HierarchialRepulsion.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Created by Alex on 2/10/14. - */ - -var hierarchalRepulsionMixin = { - - - /** - * Calculate the forces the nodes apply on eachother based on a repulsion field. - * This field is linearly approximated. - * - * @private - */ - _calculateNodeForces: function () { - var dx, dy, distance, fx, fy, combinedClusterSize, - repulsingForce, node1, node2, i, j; - - var nodes = this.calculationNodes; - var nodeIndices = this.calculationNodeIndices; - - // approximation constants - var b = 5; - var a_base = 0.5 * -b; - - - // repulsing forces between nodes - var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance; - var minimumDistance = nodeDistance; - - // we loop from i over all but the last entree in the array - // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j - for (i = 0; i < nodeIndices.length - 1; i++) { - - node1 = nodes[nodeIndices[i]]; - for (j = i + 1; j < nodeIndices.length; j++) { - node2 = nodes[nodeIndices[j]]; - - dx = node2.x - node1.x; - dy = node2.y - node1.y; - distance = Math.sqrt(dx * dx + dy * dy); - - var a = a_base / minimumDistance; - if (distance < 2 * minimumDistance) { - repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)) - - // normalize force with - if (distance == 0) { - distance = 0.01; - } - else { - repulsingForce = repulsingForce / distance; - } - fx = dx * repulsingForce; - fy = dy * repulsingForce; - - node1.fx -= fx; - node1.fy -= fy; - node2.fx += fx; - node2.fy += fy; - } - } - } - } -}; \ No newline at end of file diff --git a/src/graph3d/Graph3d.js b/src/graph3d/Graph3d.js index 21e3f5d1..038efe8b 100644 --- a/src/graph3d/Graph3d.js +++ b/src/graph3d/Graph3d.js @@ -1,15 +1,19 @@ /** * @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'; @@ -82,7 +86,7 @@ function Graph3d(container, data, options) { } } -// Extend Graph with an Emitter mixin +// Extend Graph3d with an Emitter mixin Emitter(Graph3d.prototype); /** diff --git a/src/module/exports.js b/src/module/exports.js index 1c5c90d5..076e04cf 100644 --- a/src/module/exports.js +++ b/src/module/exports.js @@ -2,38 +2,57 @@ * 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 - }, + 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, - ItemSet: ItemSet, - TimeAxis: TimeAxis + 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 }; diff --git a/src/graph/Edge.js b/src/network/Edge.js similarity index 96% rename from src/graph/Edge.js rename to src/network/Edge.js index 6bd94b03..305a7fe8 100644 --- a/src/graph/Edge.js +++ b/src/network/Edge.js @@ -8,16 +8,16 @@ * 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; @@ -30,6 +30,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; @@ -99,6 +101,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; @@ -124,6 +128,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;} } } @@ -133,6 +138,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; @@ -149,8 +156,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) { @@ -310,14 +317,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; } } }; @@ -790,12 +797,12 @@ Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is /** - * 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; }; @@ -954,4 +961,4 @@ Edge.prototype.getControlNodePositions = function(ctx) { } return {from:{x:xFrom,y:yFrom},to:{x:xTo,y:yTo}}; -} \ No newline at end of file +} diff --git a/src/graph/Groups.js b/src/network/Groups.js similarity index 100% rename from src/graph/Groups.js rename to src/network/Groups.js diff --git a/src/graph/Images.js b/src/network/Images.js similarity index 100% rename from src/graph/Images.js rename to src/network/Images.js diff --git a/src/graph/Graph.js b/src/network/Network.js similarity index 91% rename from src/graph/Graph.js rename to src/network/Network.js index ee68da20..462ee806 100644 --- a/src/graph/Graph.js +++ b/src/network/Network.js @@ -1,15 +1,18 @@ /** - * @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(); @@ -25,13 +28,14 @@ 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,editEdge:null,connect:null,del:null}; + // set constant values this.constants = { nodes: { @@ -68,6 +72,7 @@ function Graph (container, data, options) { widthMin: 1, widthMax: 15, width: 1, + widthSelectionMultiplier: 2, hoverWidth: 1.5, style: 'line', color: { @@ -106,8 +111,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 @@ -188,7 +193,7 @@ function Graph (container, data, options) { background: '#FFFFC6' } }, - dragGraph: true, + dragNetwork: true, dragNodes: true, zoomable: true, hover: false @@ -197,11 +202,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 @@ -214,13 +219,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 @@ -254,30 +259,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(); } }; @@ -306,8 +311,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 @@ -316,7 +321,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 @@ -334,10 +339,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)) { @@ -360,18 +365,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; @@ -389,7 +394,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; } @@ -441,7 +446,7 @@ Graph.prototype.zoomExtent = function(initialZoom, disableStart) { this._setScale(zoomLevel); - this._centerGraph(range); + this._centerNetwork(range); if (disableStart == false) { this.moving = true; this.start(); @@ -453,7 +458,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)) { @@ -473,7 +478,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; } @@ -519,7 +524,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 @@ -531,11 +536,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)) { @@ -762,24 +772,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); @@ -821,7 +831,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; @@ -862,7 +872,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) @@ -874,7 +884,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(); @@ -886,7 +896,7 @@ Graph.prototype._onTouch = function (event) { * handle drag start event * @private */ -Graph.prototype._onDragStart = function () { +Network.prototype._onDragStart = function () { this._handleDragStart(); }; @@ -897,7 +907,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 @@ -943,7 +953,7 @@ Graph.prototype._handleDragStart = function() { * handle drag event * @private */ -Graph.prototype._onDrag = function (event) { +Network.prototype._onDrag = function (event) { this._handleOnDrag(event) }; @@ -954,7 +964,7 @@ Graph.prototype._onDrag = function (event) { * * @private */ -Graph.prototype._handleOnDrag = function(event) { +Network.prototype._handleOnDrag = function(event) { if (this.drag.pinched) { return; } @@ -989,8 +999,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; @@ -1008,7 +1018,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) { @@ -1024,7 +1034,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); @@ -1036,7 +1046,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); }; @@ -1046,7 +1056,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); @@ -1057,7 +1067,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); }; @@ -1067,7 +1077,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; @@ -1081,13 +1091,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) { @@ -1130,7 +1140,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. */ @@ -1172,7 +1182,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); @@ -1230,14 +1240,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), @@ -1307,7 +1317,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) { @@ -1318,13 +1328,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; @@ -1348,11 +1358,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) { @@ -1398,7 +1408,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]; @@ -1430,7 +1440,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++) { @@ -1462,7 +1472,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]; @@ -1485,7 +1495,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) { @@ -1532,7 +1542,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; @@ -1563,7 +1573,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++) { @@ -1598,7 +1608,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]; @@ -1625,7 +1635,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; @@ -1653,7 +1663,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 @@ -1680,19 +1690,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; @@ -1726,12 +1736,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, @@ -1750,11 +1760,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 @@ -1762,20 +1772,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; }; @@ -1786,7 +1796,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; }; @@ -1797,7 +1807,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; }; @@ -1808,7 +1818,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; }; @@ -1819,7 +1829,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 ; }; @@ -1830,7 +1840,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)}; } @@ -1840,7 +1850,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)}; } @@ -1851,7 +1861,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; } @@ -1888,7 +1898,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)) { @@ -1907,7 +1917,7 @@ Graph.prototype._drawEdges = function(ctx) { * @param {CanvasRenderingContext2D} ctx * @private */ -Graph.prototype._drawControlNodes = function(ctx) { +Network.prototype._drawControlNodes = function(ctx) { var edges = this.edges; for (var id in edges) { if (edges.hasOwnProperty(id)) { @@ -1920,7 +1930,7 @@ Graph.prototype._drawControlNodes = function(ctx) { * Find a stable position for all nodes * @private */ -Graph.prototype._stabilize = function() { +Network.prototype._stabilize = function() { if (this.constants.freezeForStabilization == true) { this._freezeDefinedNodes(); } @@ -1944,7 +1954,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)) { @@ -1963,7 +1973,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)) { @@ -1982,7 +1992,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)) { @@ -1999,7 +2009,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; @@ -2038,7 +2048,7 @@ Graph.prototype._discreteStepNodes = function() { * * @private */ -Graph.prototype._physicsTick = function() { +Network.prototype._physicsTick = function() { if (!this.freezeSimulation) { if (this.moving) { this._doInAllActiveSectors("_initializeForceCalculation"); @@ -2058,7 +2068,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 @@ -2072,13 +2082,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(); @@ -2093,7 +2101,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(); @@ -2123,11 +2131,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); @@ -2145,7 +2153,7 @@ Graph.prototype._handleNavigation = function() { /** * Freeze the _animationStep */ -Graph.prototype.toggleFreeze = function() { +Network.prototype.toggleFreeze = function() { if (this.freezeSimulation == false) { this.freezeSimulation = true; } @@ -2162,7 +2170,7 @@ Graph.prototype.toggleFreeze = function() { * @param {boolean} [disableStart] * @private */ -Graph.prototype._configureSmoothCurves = function(disableStart) { +Network.prototype._configureSmoothCurves = function(disableStart) { if (disableStart === undefined) { disableStart = true; } @@ -2194,7 +2202,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)) { @@ -2223,10 +2231,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]; } } }; @@ -2234,14 +2242,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}); } } @@ -2256,7 +2264,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(); diff --git a/src/graph/Node.js b/src/network/Node.js similarity index 94% rename from src/graph/Node.js rename to src/network/Node.js index b3a57295..1de217d0 100644 --- a/src/graph/Node.js +++ b/src/network/Node.js @@ -15,9 +15,9 @@ * {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 @@ -83,9 +83,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; @@ -525,7 +525,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; @@ -575,14 +575,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; @@ -624,14 +624,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; @@ -674,14 +674,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; @@ -724,14 +724,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; @@ -801,14 +801,14 @@ 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; @@ -817,7 +817,7 @@ Node.prototype._drawShape = function (ctx, shape) { 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); } }; @@ -845,17 +845,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); @@ -892,10 +895,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; @@ -914,7 +917,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 @@ -922,21 +925,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; }; diff --git a/src/graph/Popup.js b/src/network/Popup.js similarity index 98% rename from src/graph/Popup.js rename to src/network/Popup.js index d9397f5e..2dd20404 100644 --- a/src/graph/Popup.js +++ b/src/network/Popup.js @@ -24,7 +24,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 diff --git a/src/graph/css/graph-manipulation.css b/src/network/css/network-manipulation.css similarity index 73% rename from src/graph/css/graph-manipulation.css rename to src/network/css/network-manipulation.css index 4d25df2f..946c9480 100644 --- a/src/graph/css/graph-manipulation.css +++ b/src/network/css/network-manipulation.css @@ -1,4 +1,4 @@ -div.graph-manipulationDiv { +div.network-manipulationDiv { border-width:0px; border-bottom: 1px; border-style:solid; @@ -18,14 +18,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; @@ -34,7 +34,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; @@ -44,7 +44,7 @@ div.graph-manipulation-closeDiv { user-select: none; } -span.graph-manipulationUI { +span.network-manipulationUI { font-family: verdana; font-size: 12px; -moz-border-radius: 15px; @@ -65,61 +65,61 @@ 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; diff --git a/src/graph/css/graph-navigation.css b/src/network/css/network-navigation.css similarity index 53% rename from src/graph/css/graph-navigation.css rename to src/network/css/network-navigation.css index 1a318390..aa05519a 100644 --- a/src/graph/css/graph-navigation.css +++ b/src/network/css/network-navigation.css @@ -1,4 +1,4 @@ -div.graph-navigation { +div.network-navigation { width:34px; height:34px; z-index:10; @@ -17,50 +17,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; } \ No newline at end of file diff --git a/src/graph/dotparser.js b/src/network/dotparser.js similarity index 100% rename from src/graph/dotparser.js rename to src/network/dotparser.js diff --git a/src/graph/img/acceptDeleteIcon.png b/src/network/img/acceptDeleteIcon.png similarity index 100% rename from src/graph/img/acceptDeleteIcon.png rename to src/network/img/acceptDeleteIcon.png diff --git a/src/graph/img/addNodeIcon.png b/src/network/img/addNodeIcon.png similarity index 100% rename from src/graph/img/addNodeIcon.png rename to src/network/img/addNodeIcon.png diff --git a/src/graph/img/backIcon.png b/src/network/img/backIcon.png similarity index 100% rename from src/graph/img/backIcon.png rename to src/network/img/backIcon.png diff --git a/src/graph/img/connectIcon.png b/src/network/img/connectIcon.png similarity index 100% rename from src/graph/img/connectIcon.png rename to src/network/img/connectIcon.png diff --git a/src/graph/img/cross.png b/src/network/img/cross.png similarity index 100% rename from src/graph/img/cross.png rename to src/network/img/cross.png diff --git a/src/graph/img/cross2.png b/src/network/img/cross2.png similarity index 100% rename from src/graph/img/cross2.png rename to src/network/img/cross2.png diff --git a/src/graph/img/deleteIcon.png b/src/network/img/deleteIcon.png similarity index 100% rename from src/graph/img/deleteIcon.png rename to src/network/img/deleteIcon.png diff --git a/src/graph/img/downArrow.png b/src/network/img/downArrow.png similarity index 100% rename from src/graph/img/downArrow.png rename to src/network/img/downArrow.png diff --git a/src/graph/img/editIcon.png b/src/network/img/editIcon.png similarity index 100% rename from src/graph/img/editIcon.png rename to src/network/img/editIcon.png diff --git a/src/graph/img/leftArrow.png b/src/network/img/leftArrow.png similarity index 100% rename from src/graph/img/leftArrow.png rename to src/network/img/leftArrow.png diff --git a/src/graph/img/minus.png b/src/network/img/minus.png similarity index 100% rename from src/graph/img/minus.png rename to src/network/img/minus.png diff --git a/src/graph/img/plus.png b/src/network/img/plus.png similarity index 100% rename from src/graph/img/plus.png rename to src/network/img/plus.png diff --git a/src/graph/img/rightArrow.png b/src/network/img/rightArrow.png similarity index 100% rename from src/graph/img/rightArrow.png rename to src/network/img/rightArrow.png diff --git a/src/graph/img/upArrow.png b/src/network/img/upArrow.png similarity index 100% rename from src/graph/img/upArrow.png rename to src/network/img/upArrow.png diff --git a/src/graph/img/zoomExtends.png b/src/network/img/zoomExtends.png similarity index 100% rename from src/graph/img/zoomExtends.png rename to src/network/img/zoomExtends.png diff --git a/src/graph/graphMixins/ClusterMixin.js b/src/network/networkMixins/ClusterMixin.js similarity index 99% rename from src/graph/graphMixins/ClusterMixin.js rename to src/network/networkMixins/ClusterMixin.js index 03da8b05..512ceb2a 100644 --- a/src/graph/graphMixins/ClusterMixin.js +++ b/src/network/networkMixins/ClusterMixin.js @@ -1,7 +1,7 @@ /** * 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 @@ -9,7 +9,7 @@ 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() { @@ -494,7 +494,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 diff --git a/src/graph/graphMixins/HierarchicalLayoutMixin.js b/src/network/networkMixins/HierarchicalLayoutMixin.js similarity index 100% rename from src/graph/graphMixins/HierarchicalLayoutMixin.js rename to src/network/networkMixins/HierarchicalLayoutMixin.js diff --git a/src/graph/graphMixins/ManipulationMixin.js b/src/network/networkMixins/ManipulationMixin.js similarity index 81% rename from src/graph/graphMixins/ManipulationMixin.js rename to src/network/networkMixins/ManipulationMixin.js index af4669fb..83947515 100644 --- a/src/graph/graphMixins/ManipulationMixin.js +++ b/src/network/networkMixins/ManipulationMixin.js @@ -37,9 +37,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"; @@ -87,49 +87,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'] +""; + "
              " + + "" + + ""+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("graph-manipulate-editEdge"); + 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); @@ -137,9 +137,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); } }, @@ -160,14 +160,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. @@ -196,14 +196,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. @@ -237,14 +237,14 @@ var manipulationMixin = { this.edgeBeingEdited._enableControlNodes(); this.manipulationDiv.innerHTML = "" + - "" + - "" + this.constants.labels['back'] + " " + - "
              " + - "" + - "" + this.constants.labels['editEdgeDescription'] + ""; + "" + + "" + this.constants.labels['back'] + " " + + "
              " + + "" + + "" + this.constants.labels['editEdgeDescription'] + ""; // bind the icon - var backButton = document.getElementById("graph-manipulate-back"); + var backButton = document.getElementById("network-manipulate-back"); backButton.onclick = this._createManipulatorBar.bind(this); // temporarily overload functions diff --git a/src/graph/graphMixins/MixinLoader.js b/src/network/networkMixins/MixinLoader.js similarity index 88% rename from src/graph/graphMixins/MixinLoader.js rename to src/network/networkMixins/MixinLoader.js index a4b36126..d3b31f4e 100644 --- a/src/graph/graphMixins/MixinLoader.js +++ b/src/network/networkMixins/MixinLoader.js @@ -3,10 +3,10 @@ */ -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 @@ -14,14 +14,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 @@ -29,7 +29,7 @@ var graphMixinLoaders = { _clearMixin: function (sourceVariable) { for (var mixinFunction in sourceVariable) { if (sourceVariable.hasOwnProperty(mixinFunction)) { - Graph.prototype[mixinFunction] = undefined; + Network.prototype[mixinFunction] = undefined; } } }, @@ -114,8 +114,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"; } @@ -127,8 +127,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"; } @@ -140,8 +140,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); } diff --git a/src/graph/graphMixins/NavigationMixin.js b/src/network/networkMixins/NavigationMixin.js similarity index 94% rename from src/graph/graphMixins/NavigationMixin.js rename to src/network/networkMixins/NavigationMixin.js index efa8b9c6..375daf5e 100644 --- a/src/graph/graphMixins/NavigationMixin.js +++ b/src/network/networkMixins/NavigationMixin.js @@ -6,7 +6,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); } @@ -29,7 +29,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"; @@ -37,8 +37,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); } diff --git a/src/graph/graphMixins/SectorsMixin.js b/src/network/networkMixins/SectorsMixin.js similarity index 97% rename from src/graph/graphMixins/SectorsMixin.js rename to src/network/networkMixins/SectorsMixin.js index c865c3d8..b23bc584 100644 --- a/src/graph/graphMixins/SectorsMixin.js +++ b/src/network/networkMixins/SectorsMixin.js @@ -1,8 +1,8 @@ /** * 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 @@ -11,7 +11,7 @@ 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 @@ -363,7 +363,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 */ @@ -402,7 +402,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 */ @@ -431,7 +431,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 */ @@ -469,7 +469,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 */ diff --git a/src/graph/graphMixins/SelectionMixin.js b/src/network/networkMixins/SelectionMixin.js similarity index 89% rename from src/graph/graphMixins/SelectionMixin.js rename to src/network/networkMixins/SelectionMixin.js index f663a332..8acb29c6 100644 --- a/src/graph/graphMixins/SelectionMixin.js +++ b/src/network/networkMixins/SelectionMixin.js @@ -402,10 +402,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); @@ -414,7 +417,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); } } @@ -619,10 +622,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 diff --git a/src/graph/graphMixins/physics/BarnesHut.js b/src/network/networkMixins/physics/BarnesHut.js similarity index 99% rename from src/graph/graphMixins/physics/BarnesHut.js rename to src/network/networkMixins/physics/BarnesHut.js index 4b9f039c..d0c5f8da 100644 --- a/src/graph/graphMixins/physics/BarnesHut.js +++ b/src/network/networkMixins/physics/BarnesHut.js @@ -256,7 +256,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; diff --git a/src/network/networkMixins/physics/HierarchialRepulsion.js b/src/network/networkMixins/physics/HierarchialRepulsion.js new file mode 100644 index 00000000..6cc39b55 --- /dev/null +++ b/src/network/networkMixins/physics/HierarchialRepulsion.js @@ -0,0 +1,133 @@ +/** + * Created by Alex on 2/10/14. + */ + +var hierarchalRepulsionMixin = { + + + /** + * Calculate the forces the nodes apply on eachother based on a repulsion field. + * This field is linearly approximated. + * + * @private + */ + _calculateNodeForces: function () { + var dx, dy, distance, fx, fy, combinedClusterSize, + repulsingForce, node1, node2, i, j; + + var nodes = this.calculationNodes; + var nodeIndices = this.calculationNodeIndices; + + // approximation constants + var b = 5; + var a_base = 0.5 * -b; + + + // repulsing forces between nodes + var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance; + var minimumDistance = nodeDistance; + 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 + for (i = 0; i < nodeIndices.length - 1; i++) { + + 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); + + + 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; + } + } + } + } + }, + + + /** + * 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; + } + } + } + } + } + } +}; \ No newline at end of file diff --git a/src/graph/graphMixins/physics/PhysicsMixin.js b/src/network/networkMixins/physics/PhysicsMixin.js similarity index 99% rename from src/graph/graphMixins/physics/PhysicsMixin.js rename to src/network/networkMixins/physics/PhysicsMixin.js index 2a419591..b492b462 100644 --- a/src/graph/graphMixins/physics/PhysicsMixin.js +++ b/src/network/networkMixins/physics/PhysicsMixin.js @@ -101,7 +101,12 @@ var physicsMixin = { this._calculateSpringForcesWithSupport(); } else { - this._calculateSpringForces(); + if (this.constants.physics.hierarchicalRepulsion.enabled == true) { + this._calculateHierarchicalSpringForces(); + } + else { + this._calculateSpringForces(); + } } }, @@ -181,6 +186,8 @@ var physicsMixin = { }, + + /** * this function calculates the effects of the springs in the case of unsmooth curves. * @@ -227,6 +234,8 @@ var physicsMixin = { }, + + /** * This function calculates the springforces on the nodes, accounting for the support nodes. * @@ -303,7 +312,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'); diff --git a/src/graph/graphMixins/physics/Repulsion.js b/src/network/networkMixins/physics/Repulsion.js similarity index 100% rename from src/graph/graphMixins/physics/Repulsion.js rename to src/network/networkMixins/physics/Repulsion.js diff --git a/src/graph/shapes.js b/src/network/shapes.js similarity index 99% rename from src/graph/shapes.js rename to src/network/shapes.js index ed80372b..791f37b7 100644 --- a/src/graph/shapes.js +++ b/src/network/shapes.js @@ -1,5 +1,5 @@ /** - * Canvas shapes used by the Graph + * Canvas shapes used by Network */ if (typeof CanvasRenderingContext2D !== 'undefined') { diff --git a/src/timeline/DataStep.js b/src/timeline/DataStep.js new file mode 100644 index 00000000..e57062cd --- /dev/null +++ b/src/timeline/DataStep.js @@ -0,0 +1,215 @@ +/** + * @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. + * + * 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 + * + * 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. + * + * Version: 1.2 + * + * @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 + */ +function DataStep(start, end, minimumStep, containerHeight, forcedStepSize) { + // variables + this.current = 0; + + this.autoScale = true; + this.stepIndex = 0; + this.step = 1; + this.scale = 1; + + this.marginStart; + this.marginEnd; + + this.majorSteps = [1, 2, 5, 10]; + this.minorSteps = [0.25, 0.5, 1, 2]; + + this.setRange(start, end, minimumStep, containerHeight, forcedStepSize); +} + + + +/** + * 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 + */ +DataStep.prototype.setRange = function(start, end, minimumStep, containerHeight, forcedStepSize) { + this._start = start; + this._end = end; + + if (this.autoScale) { + this.setMinimumStep(minimumStep, containerHeight, forcedStepSize); + } + this.setFirst(); +}; + +/** + * 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; + } + } + if (solutionFound == true) { + break; + } + } + this.stepIndex = minorStepIdx; + this.scale = magnitudefactor; + this.step = magnitudefactor * this.minorSteps[minorStepIdx]; +}; + + +/** + * Set the range iterator to the start date. + */ +DataStep.prototype.first = function() { + this.setFirst(); +}; + +/** + * Round the current date to the first minor date value + * This must be executed once when the current date is set to start Date + */ +DataStep.prototype.setFirst = function() { + var niceStart = this._start - (this.scale * this.minorSteps[this.stepIndex]); + var niceEnd = this._end + (this.scale * this.minorSteps[this.stepIndex]); + + this.marginEnd = this.roundToMinor(niceEnd); + this.marginStart = this.roundToMinor(niceStart); + this.marginRange = this.marginEnd - this.marginStart; + + this.current = this.marginEnd; + +}; + +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; + } +} + + +/** + * 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); +}; + +/** + * Do the next step + */ +DataStep.prototype.next = function() { + var prev = this.current; + this.current -= this.step; + + // 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); + } + else if (toPrecision[i] == "." || toPrecision[i] == ",") { + toPrecision = toPrecision.slice(0,i); + break; + } + else{ + break; + } + } + + + return toPrecision; +}; + + + +/** + * 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); +}; diff --git a/src/timeline/Graph2d.js b/src/timeline/Graph2d.js new file mode 100644 index 00000000..39ffbb07 --- /dev/null +++ b/src/timeline/Graph2d.js @@ -0,0 +1,870 @@ +/** + * 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, + + autoResize: true, + + orientation: 'bottom', + width: null, + height: null, + maxHeight: null, + minHeight: null + }; + this.options = util.deepExtend({}, this.defaultOptions); + + // Create the DOM, props, and emitter + this._create(container); + + // 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) + }, + 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 + 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(this.body); + this.components.push(this.customTime); + + // item set + this.linegraph = new LineGraph(this.body); + this.components.push(this.linegraph); + + this.itemsData = null; // DataSet + this.groupsData = null; // DataSet + + // apply options + if (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 Graph2d into an event emitter +Emitter(Graph2d.prototype); + +/** + * 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 + */ +Graph2d.prototype._create = function (container) { + this.dom = {}; + + 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 = {}; + + 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; + }); + + // 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 + + // attach the root panel to the provided container + if (!container) throw new Error('No container provided'); + container.appendChild(this.dom.root); +}; + +/** + * Destroy the Graph2d, clean up all DOM elements and event listeners. + */ +Graph2d.prototype.destroy = function () { + // unbind datasets + this.clear(); + + // remove all event listeners + this.off(); + + // 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; + + // 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; + + // 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.'); + } + + // redraw everything + this.redraw(); +}; + +/** + * Set a custom time bar + * @param {Date} time + */ +Graph2d.prototype.setCustomTime = function (time) { + if (!this.customTime) { + throw new Error('Cannot get custom time: Custom time bar is not enabled'); + } + + this.customTime.setCustomTime(time); +}; + +/** + * Retrieve the current custom time. + * @return {Date} customTime + */ +Graph2d.prototype.getCustomTime = function() { + if (!this.customTime) { + throw new Error('Cannot get custom time: Custom time bar is not enabled'); + } + + return this.customTime.getCustomTime(); +}; + +/** + * Set items + * @param {vis.DataSet | Array | google.visualization.DataTable | null} items + */ +Graph2d.prototype.setItems = function(items) { + var initialLoad = (this.itemsData == null); + + // 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' + } + }); + } + + // set items + this.itemsData = newDataSet; + this.linegraph && this.linegraph.setItems(newDataSet); + + if (initialLoad && ('start' in this.options || 'end' in this.options)) { + this.fit(); + + 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); + } +}; + +/** + * Set groups + * @param {vis.DataSet | Array | google.visualization.DataTable} groups + */ +Graph2d.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 { + // turn an array into a dataset + newDataSet = new DataSet(groups); + } + + this.groupsData = newDataSet; + this.linegraph.setGroups(newDataSet); +}; + +/** + * Clear the Graph2d. 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} + */ +Graph2d.prototype.clear = function(what) { + // clear items + if (!what || what.items) { + this.setItems(null); + } + + // clear groups + if (!what || what.groups) { + this.setGroups(null); + } + + // clear options of timeline and of each of the components + if (!what || what.options) { + this.components.forEach(function (component) { + component.setOptions(component.defaultOptions); + }); + + this.setOptions(this.defaultOptions); // this will also do a redraw + } +}; + +/** + * Set Graph2d window such that it fits all items + */ +Graph2d.prototype.fit = function() { + // apply the data range as range + var dataRange = this.getItemRange(); + + // 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); + } + + // skip range set if there is no start and end date + if (start === null && end === null) { + return; + } + + 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 + */ +Graph2d.prototype.getItemRange = function() { + // calculate min from start filed + var itemsData = this.itemsData, + min = null, + max = null; + + if (itemsData) { + // calculate the minimum value of the field 'start' + var minItem = itemsData.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 = itemsData.max('start'); + if (maxStartItem) { + max = util.convert(maxStartItem.start, 'Date').valueOf(); + } + var maxEndItem = itemsData.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 + }; +}; + +/** + * 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 + */ +Graph2d.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); + } +}; + +/** + * Get the visible window + * @return {{start: Date, end: Date}} Visible range + */ +Graph2d.prototype.getWindow = function() { + var range = this.range.getRange(); + return { + start: new Date(range.start), + end: new Date(range.end) + }; +}; + +/** + * Force a redraw of the Graph2d. Can be useful to manually redraw when + * option autoResize=false + */ +Graph2d.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.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(); + } +}; + +/** + * 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 Range +Graph2d.prototype._toTime = function(x) { + var conversion = this.range.conversion(this.props.center.width); + return new Date(x / conversion.scale + conversion.offset); +}; + +/** + * 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); +}; + +/** + * 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 + */ +// 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; +}; + + +/** + * 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; +}; + +/** + * Initialize watching when option autoResize is true + * @private + */ +Graph2d.prototype._initAutoResize = function () { + if (this.options.autoResize == true) { + this._startAutoResize(); + } + else { + 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; + + this._stopAutoResize(); + + this._onResize = function() { + if (me.options.autoResize != true) { + // stop watching when the option autoResize is changed to false + me._stopAutoResize(); + return; + } + + 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'); + } + } + }; + + // 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; +}; + +/** + * Start moving the timeline vertically + * @param {Event} event + * @private + */ +Graph2d.prototype._onTouch = function (event) { + this.touch.allowDragging = true; +}; + +/** + * Start moving the timeline vertically + * @param {Event} event + * @private + */ +Graph2d.prototype._onPinch = function (event) { + this.touch.allowDragging = false; +}; + +/** + * Start moving the timeline vertically + * @param {Event} event + * @private + */ +Graph2d.prototype._onDragStart = function (event) { + this.touch.initialScrollTop = this.props.scrollTop; +}; + +/** + * 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; + + 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 + } +}; + +/** + * Apply a scrollTop + * @param {Number} scrollTop + * @returns {Number} scrollTop Returns the applied scrollTop + * @private + */ +Graph2d.prototype._setScrollTop = function (scrollTop) { + this.props.scrollTop = scrollTop; + this._updateScrollTop(); + return this.props.scrollTop; +}; + +/** + * Update the current scrollTop when the height of the containers has been changed + * @returns {Number} scrollTop Returns the applied scrollTop + * @private + */ +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; +}; diff --git a/src/timeline/Range.js b/src/timeline/Range.js index 135d992a..8f878541 100644 --- a/src/timeline/Range.js +++ b/src/timeline/Range.js @@ -293,21 +293,16 @@ Range.prototype._onDragStart = function(event) { Range.prototype._onDrag = function (event) { // only allow dragging when configured as movable if (!this.options.moveable) return; - var direction = this.options.direction; validateDirection(direction); - // 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.props.touch.allowDragging) return; - var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY, 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(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) diff --git a/src/timeline/stack.js b/src/timeline/Stack.js similarity index 100% rename from src/timeline/stack.js rename to src/timeline/Stack.js diff --git a/src/timeline/Timeline.js b/src/timeline/Timeline.js index 38299f77..64b1652d 100644 --- a/src/timeline/Timeline.js +++ b/src/timeline/Timeline.js @@ -6,6 +6,10 @@ * @constructor */ function Timeline (container, items, options) { + if (!(this instanceof Timeline)) { + throw new SyntaxError('Constructor must be called with the new operator'); + } + var me = this; this.defaultOptions = { start: null, @@ -38,7 +42,9 @@ function Timeline (container, items, options) { util: { snap: null, // will be specified after TimeAxis is created toScreen: me._toScreen.bind(me), - toTime: me._toTime.bind(me) + toGlobalScreen: me._toGlobalScreen.bind(me), // this refers to the root.width + toTime: me._toTime.bind(me), + toGlobalTime : me._toGlobalTime.bind(me) } }; @@ -442,23 +448,23 @@ Timeline.prototype.fit = function() { */ Timeline.prototype.getItemRange = function() { // calculate min from start filed - var itemsData = this.itemsData, + var dataset = this.itemsData.getDataSet(), min = null, max = null; - if (itemsData) { + if (dataset) { // calculate the minimum value of the field 'start' - var minItem = itemsData.min('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 = itemsData.max('start'); + var maxStartItem = dataset.max('start'); if (maxStartItem) { max = util.convert(maxStartItem.start, 'Date').valueOf(); } - var maxEndItem = itemsData.max('end'); + var maxEndItem = dataset.max('end'); if (maxEndItem) { if (max == null) { max = util.convert(maxEndItem.end, 'Date').valueOf(); @@ -636,7 +642,8 @@ Timeline.prototype.redraw = function() { // reposition the scrollable contents var offset = this.props.scrollTop; if (options.orientation == 'bottom') { - offset += Math.max(this.props.centerContainer.height - this.props.center.height, 0); + 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'; @@ -682,6 +689,19 @@ Timeline.prototype._toTime = function(x) { return new Date(x / conversion.scale + conversion.offset); }; + +/** + * 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 + */ +// 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); +}; + /** * Convert a datetime (Date object) into a position on the screen * @param {Date} time A date @@ -695,6 +715,22 @@ Timeline.prototype._toScreen = function(time) { return (time.valueOf() - conversion.offset) * conversion.scale; }; + +/** + * 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 +Timeline.prototype._toGlobalScreen = function(time) { + var conversion = this.range.conversion(this.props.root.width); + return (time.valueOf() - conversion.offset) * conversion.scale; +}; + + /** * Initialize watching when option autoResize is true * @private diff --git a/src/timeline/component/DataAxis.js b/src/timeline/component/DataAxis.js new file mode 100644 index 00000000..81ea5cb6 --- /dev/null +++ b/src/timeline/component/DataAxis.js @@ -0,0 +1,468 @@ +/** + * 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; + } + // 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 { + DOMutil.cleanupElements(this.DOMelements); + return false; + } +}; + +/** + * 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 (orientation == 'left') { + label.style.left = '-' + this.options.labelOffsetX + 'px'; + label.style.textAlign = "right"; + } + else { + label.style.right = '-' + this.options.labelOffsetX + 'px'; + label.style.textAlign = "left"; + } + + 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; + } +}; + +/** + * 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 = ''; + + if (orientation == 'left') { + line.style.left = (this.width - offset) + 'px'; + } + else { + line.style.right = (this.width - offset) + 'px'; + } + + line.style.width = width + 'px'; + line.style.top = y + 'px'; + } +}; + + +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); + } + + 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); + + this.props.majorCharHeight = measureCharMajor.clientHeight; + this.props.majorCharWidth = measureCharMajor.clientWidth; + + this.dom.frame.removeChild(measureCharMajor); + } +}; + +/** + * 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); +}; diff --git a/src/timeline/component/GraphGroup.js b/src/timeline/component/GraphGroup.js new file mode 100644 index 00000000..0da3d235 --- /dev/null +++ b/src/timeline/component/GraphGroup.js @@ -0,0 +1,116 @@ +/** + * @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); + } +} diff --git a/src/timeline/component/Group.js b/src/timeline/component/Group.js index 6184c750..bfc63e71 100644 --- a/src/timeline/component/Group.js +++ b/src/timeline/component/Group.js @@ -81,6 +81,9 @@ Group.prototype.setData = function(data) { this.dom.inner.innerHTML = this.groupId; } + // update title + this.dom.label.title = data && data.title || ''; + if (!this.dom.inner.firstChild) { util.addClassName(this.dom.inner, 'hidden'); } @@ -157,7 +160,15 @@ Group.prototype.redraw = function(range, margin, restack) { min = Math.min(min, item.top); max = Math.max(max, (item.top + item.height)); }); - height = (max - min) + margin.axis + margin.item; + 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; + }); + } + height = max + margin.item / 2; } else { height = margin.axis + margin.item; @@ -176,7 +187,8 @@ Group.prototype.redraw = function(range, margin, restack) { resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized; // apply new height - foreground.style.height = height + 'px'; + 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 @@ -323,14 +335,14 @@ Group.prototype._updateVisibleItems = function(orderedItems, visibleItems, 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); + initialPosByStart = util.binarySearch(orderedItems.byStart, range, 'data','start'); } 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); + 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) { @@ -355,72 +367,7 @@ Group.prototype._updateVisibleItems = function(orderedItems, visibleItems, range 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} - * @private - */ -Group.prototype._binarySearch = function(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; - - var found = false; - var low = 0; - var high = array.length; - var guess = Math.floor(0.5*(high+low)); - var newGuess; - - 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; - } - } - 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; - } - } - } - } - return guess; -}; + /** * this function checks if an item is invisible. If it is NOT we make it visible diff --git a/src/timeline/component/ItemSet.js b/src/timeline/component/ItemSet.js index 56dba106..1a5171cd 100644 --- a/src/timeline/component/ItemSet.js +++ b/src/timeline/component/ItemSet.js @@ -13,7 +13,7 @@ function ItemSet(body, options) { this.body = body; this.defaultOptions = { - type: 'box', + type: null, // 'box', 'point', 'range' orientation: 'bottom', // 'top' or 'bottom' align: 'center', // alignment of box items stack: true, @@ -114,7 +114,6 @@ ItemSet.prototype = new Component(); ItemSet.types = { box: ItemBox, range: ItemRange, - rangeoverflow: ItemRangeOverflow, point: ItemPoint }; @@ -648,7 +647,7 @@ ItemSet.prototype.getGroups = function() { */ ItemSet.prototype.removeItem = function(id) { var item = this.itemsData.get(id), - dataset = this._myDataSet(); + dataset = this.itemsData.getDataSet(); if (item) { // confirm deletion @@ -673,10 +672,7 @@ ItemSet.prototype._onUpdate = function(ids) { ids.forEach(function (id) { var itemData = me.itemsData.get(id, me.itemOptions), item = me.items[id], - type = itemData.type || - (itemData.start && itemData.end && 'range') || - me.options.type || - 'box'; + type = itemData.type || me.options.type || (itemData.end ? 'range' : 'box'); var constructor = ItemSet.types[type]; @@ -699,6 +695,11 @@ ItemSet.prototype._onUpdate = function(ids) { 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 + '"'); } @@ -1087,7 +1088,7 @@ 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, @@ -1208,7 +1209,7 @@ ItemSet.prototype._onAddItem = function (event) { }; // when default type is a range, add a default end date to the new item - if (this.options.type === 'range' || this.options.type == 'rangeoverflow') { + if (this.options.type === 'range') { var end = this.body.util.toTime(x + this.props.width / 5); newItem.end = snap ? snap(end) : end; } @@ -1317,16 +1318,3 @@ ItemSet.itemSetFromTarget = function(event) { return null; }; -/** - * Find the DataSet to which this ItemSet is connected - * @returns {null | DataSet} dataset - * @private - */ -ItemSet.prototype._myDataSet = function() { - // find the root DataSet - var dataset = this.itemsData; - while (dataset instanceof DataView) { - dataset = dataset.data; - } - return dataset; -}; \ No newline at end of file diff --git a/src/timeline/component/Legend.js b/src/timeline/component/Legend.js new file mode 100644 index 00000000..d879d0de --- /dev/null +++ b/src/timeline/component/Legend.js @@ -0,0 +1,177 @@ +/** + * 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); + } +} \ No newline at end of file diff --git a/src/timeline/component/LineGraph.js b/src/timeline/component/LineGraph.js new file mode 100644 index 00000000..6ffed716 --- /dev/null +++ b/src/timeline/component/LineGraph.js @@ -0,0 +1,1065 @@ +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 + } + } + }; + + // 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); + } + }; + + // 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); + } + }; + + 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"; + } + } + }); + 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(); + +/** + * Create the HTML DOM for the ItemSet + */ +LineGraph.prototype._create = function(){ + var frame = document.createElement('div'); + frame.className = 'LineGraph'; + this.dom.frame = frame; + + // 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); + + // data axis + this.options.dataAxis.orientation = 'left'; + this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg); + + 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; + } + } + } + } + + if (this.yAxisLeft) { + if (options.dataAxis !== undefined) { + this.yAxisLeft.setOptions(this.options.dataAxis); + this.yAxisRight.setOptions(this.options.dataAxis); + } + } + + 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(); + } +}; + +/** + * 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); + } +}; + +/** + * Show the component in the DOM (when not already visible). + * @return {Boolean} changed + */ +LineGraph.prototype.show = function() { + // show frame containing the items + if (!this.dom.frame.parentNode) { + this.body.dom.center.appendChild(this.dom.frame); + } +}; + + +/** + * 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 { + 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(); +}; + +/** + * Set groups + * @param {vis.DataSet} groups + */ +LineGraph.prototype.setGroups = function(groups) { + var me = this, + ids; + + // 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 + } + + // 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'); + } + + 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(); +}; + + + +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]); + } + + 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(); +}; + +/** + * update a group object + * + * @param group + * @param groupId + * @private + */ +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]); + } + } + else { + 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(); +}; + +LineGraph.prototype._updateAllGroupData = function () { + if (this.itemsData != null) { + // ~450 ms @ 500k + + var groupsContent = {}; + for (var groupId in this.groups) { + if (this.groups.hasOwnProperty(groupId)) { + groupsContent[groupId] = []; + } + } + 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"}} +// )); +// } +// } + } +}; + +/** + * 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 + */ +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; + } + } + } + } + + // 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); + } + 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(); +}; + + +/** + * Redraw the component, mandatory function + * @return {boolean} Returns true if the component is resized + */ +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; + + // calculate actual size and position + this.width = this.dom.frame.offsetWidth; + + // 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(); + } + + this.legendLeft.redraw(); + this.legendRight.redraw(); + + return resized; +}; + +/** + * Update and redraw the graph. + * + */ +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); + } + + // 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; + } + + // 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)) + } + + // 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); + } + } + } + } + + // cleanup unused svg elements + DOMutil.cleanupElements(this.svgElements); +}; + +/** + * 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 + */ +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'; + + // 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'; + } + + 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); + } + } + + 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; +}; + +/** + * 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 + */ +LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) { + var changed = false; + if (axisUsed == false) { + if (axis.dom.frame.parentNode) { + axis.hide(); + changed = true; + } + } + else { + if (!axis.dom.frame.parentNode) { + axis.show(); + changed = true; + } + } + return changed; +}; + + +/** + * draw a bar graph + * @param datapoints + * @param group + */ +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; + + if (group.options.barChart.align == 'left') {offset -= 0.5*width;} + else if (group.options.barChart.align == 'right') {offset += 0.5*width;} + + 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); + } + + // draw points + if (group.options.drawPoints.enabled == true) { + this._drawPoints(dataset, group, this.svgElements, this.svg, offset); + } + } + } +}; + + +/** + * draw a line graph + * + * @param datapoints + * @param group + */ +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); + + // 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); + } + } + } +}; + +/** + * draw the data points + * + * @param dataset + * @param JSONcontainer + * @param svg + * @param group + */ +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); + } +}; + + + +/** + * 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 + */ +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))); + } + + 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}; +}; + +/** + * 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 + */ +LineGraph.prototype._convertYvalues = function (datapoints, group) { + var extractedData = []; + var xValue, yValue; + var axis = this.yAxisLeft; + var svgHeight = Number(this.svg.style.height.replace("px","")); + + 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; +}; + + +/** + * 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 + */ +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++) { + + p0 = (i == 0) ? data[0] : data[i-1]; + p1 = data[i]; + p2 = data[i+1]; + p3 = (i + 2 < length) ? data[i+2] : p2; + + + // 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 + + // 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; +}; + +/** + * 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 + */ +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 ] + + // [ 0 1 0 0 ] + // [ -d2pow2a/N A/N d1pow2a/N 0 ] + // [ 0 d3pow2a/M B/M -d2pow2a/M ] + // [ 0 0 1 0 ] + + 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); + + 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;} + + bp1 = { x: ((-d2pow2A * p0.x + A*p1.x + d1pow2A * p2.x) * N), + y: ((-d2pow2A * p0.y + A*p1.y + d1pow2A * p2.y) * N)}; + + 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 + " "; + } + + 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; +}; + + + + + diff --git a/src/timeline/component/css/dataaxis.css b/src/timeline/component/css/dataaxis.css new file mode 100644 index 00000000..1fe4d71d --- /dev/null +++ b/src/timeline/component/css/dataaxis.css @@ -0,0 +1,61 @@ + +.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 +} \ No newline at end of file diff --git a/src/timeline/component/css/item.css b/src/timeline/component/css/item.css index 1291a49f..fd248b81 100644 --- a/src/timeline/component/css/item.css +++ b/src/timeline/component/css/item.css @@ -41,15 +41,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; } @@ -82,8 +80,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%; @@ -94,8 +91,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%; diff --git a/src/timeline/component/css/pathStyles.css b/src/timeline/component/css/pathStyles.css new file mode 100644 index 00000000..22d97ef2 --- /dev/null +++ b/src/timeline/component/css/pathStyles.css @@ -0,0 +1,108 @@ +.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; +} + + diff --git a/src/timeline/component/item/ItemBox.js b/src/timeline/component/item/ItemBox.js index 6a067826..04c94439 100644 --- a/src/timeline/component/item/ItemBox.js +++ b/src/timeline/component/item/ItemBox.js @@ -112,6 +112,12 @@ ItemBox.prototype.redraw = function() { 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' : ''); diff --git a/src/timeline/component/item/ItemPoint.js b/src/timeline/component/item/ItemPoint.js index ebb74c28..bd6fd09f 100644 --- a/src/timeline/component/item/ItemPoint.js +++ b/src/timeline/component/item/ItemPoint.js @@ -102,6 +102,12 @@ ItemPoint.prototype.redraw = function() { 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' : ''); diff --git a/src/timeline/component/item/ItemRange.js b/src/timeline/component/item/ItemRange.js index 00a2b241..b6bc2572 100644 --- a/src/timeline/component/item/ItemRange.js +++ b/src/timeline/component/item/ItemRange.js @@ -14,6 +14,7 @@ function ItemRange (data, conversion, options) { width: 0 } }; + this.overflow = false; // if contents can overflow (css styling), this flag is set to true // validate data if (data) { @@ -95,6 +96,12 @@ ItemRange.prototype.redraw = function() { 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' : ''); @@ -107,6 +114,9 @@ ItemRange.prototype.redraw = function() { // recalculate size if (this.dirty) { + // 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; @@ -151,6 +161,7 @@ ItemRange.prototype.hide = function() { * Reposition the item horizontally * @Override */ +// TODO: delete the old function ItemRange.prototype.repositionX = function() { var props = this.props, parentWidth = this.parent.width, @@ -166,22 +177,35 @@ ItemRange.prototype.repositionX = function() { if (end > 2 * parentWidth) { end = 2 * parentWidth; } + var boxWidth = Math.max(end - start, 1); - // 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; + 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 = Math.max(end - start, 1); + this.left = start; + this.width = boxWidth; + } this.dom.box.style.left = this.left + 'px'; - this.dom.box.style.width = this.width + 'px'; + this.dom.box.style.width = boxWidth + 'px'; this.dom.content.style.left = contentLeft + 'px'; }; diff --git a/src/timeline/component/item/ItemRangeOverflow.js b/src/timeline/component/item/ItemRangeOverflow.js deleted file mode 100644 index 3d5d474e..00000000 --- a/src/timeline/component/item/ItemRangeOverflow.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @constructor ItemRangeOverflow - * @extends ItemRange - * @param {Object} data Object containing parameters start, end - * content, className. - * @param {{toScreen: function, toTime: function}} conversion - * Conversion functions from time to screen and vice versa - * @param {Object} [options] Configuration options - * // TODO: describe options - */ -function ItemRangeOverflow (data, conversion, options) { - this.props = { - content: { - left: 0, - width: 0 - } - }; - - ItemRange.call(this, data, conversion, options); -} - -ItemRangeOverflow.prototype = new ItemRange (null, null, null); - -ItemRangeOverflow.prototype.baseClassName = 'item rangeoverflow'; - -/** - * Reposition the item horizontally - * @Override - */ -ItemRangeOverflow.prototype.repositionX = function() { - var parentWidth = this.parent.width, - start = this.conversion.toScreen(this.data.start), - end = this.conversion.toScreen(this.data.end), - 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 - contentLeft = Math.max(-start, 0); - - 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 - - this.dom.box.style.left = this.left + 'px'; - this.dom.box.style.width = boxWidth + 'px'; - this.dom.content.style.left = contentLeft + 'px'; -}; diff --git a/src/util.js b/src/util.js index 522b5241..abf577f2 100644 --- a/src/util.js +++ b/src/util.js @@ -110,17 +110,56 @@ util.selectiveExtend = function (props, a, b) { throw new Error('Array with property names expected as first argument'); } - for (var i = 1, len = arguments.length; i < len; i++) { + for (var i = 2; i < arguments.length; i++) { var other = arguments[i]; - for (var p = 0, pp = props.length; p < pp; p++) { + 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; }; @@ -975,16 +1014,251 @@ util.isValidHex = function(hex) { return isOk; }; -util.copyObject = function(objectFrom, objectTo) { - for (var i in objectFrom) { - if (objectFrom.hasOwnProperty(i)) { - if (typeof objectFrom[i] == "object") { - objectTo[i] = {}; - util.copyObject(objectFrom[i], objectTo[i]); + +/** + * This recursively redirects the prototype of JSON objects to the referenceObject + * This is used for default options. + * + * @param referenceObject + * @returns {*} + */ +util.selectiveBridgeObject = function(fields, referenceObject) { + if (typeof referenceObject == "object") { + var objectTo = Object.create(referenceObject); + for (var i = 0; i < fields.length; i++) { + if (referenceObject.hasOwnProperty(fields[i])) { + if (typeof referenceObject[fields[i]] == "object") { + objectTo[fields[i]] = util.bridgeObject(referenceObject[fields[i]]); + } + } + } + return objectTo; + } + else { + return null; + } +}; + +/** + * This recursively redirects the prototype of JSON objects to the referenceObject + * This is used for default options. + * + * @param referenceObject + * @returns {*} + */ +util.bridgeObject = function(referenceObject) { + if (typeof referenceObject == "object") { + var objectTo = Object.create(referenceObject); + for (var i in referenceObject) { + if (referenceObject.hasOwnProperty(i)) { + if (typeof referenceObject[i] == "object") { + objectTo[i] = util.bridgeObject(referenceObject[i]); + } + } + } + return objectTo; + } + else { + return null; + } +}; + + +/** + * this is used to set the options of subobjects in the options object. A requirement of these subobjects + * is that they have an 'enabled' element which is optional for the user but mandatory for the program. + * + * @param [object] mergeTarget | this is either this.options or the options used for the groups. + * @param [object] options | options + * @param [String] option | this is the option key in the options argument + * @private + */ +util.mergeOptions = function (mergeTarget, options, option) { + if (options[option] !== undefined) { + if (typeof options[option] == 'boolean') { + mergeTarget[option].enabled = options[option]; + } + else { + mergeTarget[option].enabled = true; + for (prop in options[option]) { + if (options[option].hasOwnProperty(prop)) { + mergeTarget[option][prop] = options[option][prop]; + } + } + } + } +} + + +/** + * this is used to set the options of subobjects in the options object. A requirement of these subobjects + * is that they have an 'enabled' element which is optional for the user but mandatory for the program. + * + * @param [object] mergeTarget | this is either this.options or the options used for the groups. + * @param [object] options | options + * @param [String] option | this is the option key in the options argument + * @private + */ +util.mergeOptions = function (mergeTarget, options, option) { + if (options[option] !== undefined) { + if (typeof options[option] == 'boolean') { + mergeTarget[option].enabled = options[option]; + } + else { + mergeTarget[option].enabled = true; + for (prop in options[option]) { + if (options[option].hasOwnProperty(prop)) { + mergeTarget[option][prop] = options[option][prop]; + } + } + } + } +} + + + + +/** + * 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} + * @private + */ +util.binarySearch = function(orderedItems, range, field, field2) { + var array = orderedItems; + var interval = range.end - range.start; + + var found = false; + var low = 0; + var high = array.length; + var guess = Math.floor(0.5*(high+low)); + var newGuess; + var value; + + if (high == 0) {guess = -1;} + else if (high == 1) { + value = field2 === undefined ? array[guess][field] : array[guess][field][field2]; + if ((value > range.start - interval) && (value < range.end)) { + guess = 0; + } + else { + guess = -1; + } + } + else { + 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 { - objectTo[i] = objectFrom[i]; + 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 guess; }; + +/** + * 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 + */ +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; + + if (high == 0) {guess = -1;} + else if (high == 1) { + value = array[guess][field]; + if (value == target) { + guess = 0; + } + else { + 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 (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; + } + } + } + } + return guess; +}; \ No newline at end of file diff --git a/test/dotparser.js b/test/dotparser.js index 5fccb7e9..48832813 100644 --- a/test/dotparser.js +++ b/test/dotparser.js @@ -1,6 +1,6 @@ var assert = require('assert'), fs = require('fs'), - dot = require('../src/graph/dotparser.js'); + dot = require('../src/network/dotparser.js'); fs.readFile('test/dot.txt', function (err, data) { data = String(data); diff --git a/test/timeline.html b/test/timeline.html index 14ce8d91..94607b91 100644 --- a/test/timeline.html +++ b/test/timeline.html @@ -65,16 +65,17 @@ fieldId: '_id' }); items.add([ - {_id: 0, content: 'item 0', start: now.clone().add('days', 3).toDate()}, + {_id: 0, content: 'item 0', start: now.clone().add('days', 3).toDate(), title: 'hello title!'}, {_id: 1, content: 'item 1
              start', start: now.clone().add('days', 4).toDate()}, {_id: 2, content: 'item 2', start: now.clone().add('days', -2).toDate() }, {_id: 3, content: 'item 3', start: now.clone().add('days', 2).toDate()}, { - _id: 4, content: 'item 4', + _id: 4, content: 'item 4 ', start: now.clone().add('days', 0).toDate(), - end: now.clone().add('days', 7).toDate() + end: now.clone().add('days', 7).toDate(), + title: 'hello title!' }, - {_id: 5, content: 'item 5', start: now.clone().add('days', 9).toDate(), type:'point'}, + {_id: 5, content: 'item 5', start: now.clone().add('days', 9).toDate(), type:'point', title: 'hello title!'}, {_id: 6, content: 'item 6', start: now.clone().add('days', 11).toDate()} ]); diff --git a/test/timeline_groups.html b/test/timeline_groups.html index de2f5b16..aae53b62 100644 --- a/test/timeline_groups.html +++ b/test/timeline_groups.html @@ -51,7 +51,7 @@ var names = ['John (0)', 'Alston (1)', 'Lee (2)', 'Grant (3)']; var groups = new vis.DataSet(); for (var g = 0; g < groupCount; g++) { - groups.add({id: g, content: names[g]}); + groups.add({id: g, content: names[g], title: 'Title of group ' + g}); } // create a dataset with items @@ -65,6 +65,7 @@ content: 'item ' + i + ' (' + names[group] + ')', start: start, + title: 'Title for item ' + i, type: 'box' }); }
              NameTypeDefaultDescriptionNameTypeDefaultDescription
              alignString"center"Alignment of items with type 'box'. Available values are - 'center' (default), 'left', or 'right').alignString"center"Alignment of items with type 'box'. Available values are + 'center' (default), 'left', or 'right').
              autoResizebooleantrueIf 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().autoResizebooleantrueIf 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().
              editableBoolean | ObjectfalseIf 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. - editableBoolean | ObjectfalseIf 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.addBooleanfalseIf true, new items can be created by double tapping an empty space in the Timeline. See section Editing Items for a detailed explanation.editable.addBooleanfalseIf true, new items can be created by double tapping an empty space in the Timeline. See section Editing Items for a detailed explanation.
              editable.removeBooleanfalseIf 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.removeBooleanfalseIf 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.updateGroupBooleanfalseIf 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.updateGroupBooleanfalseIf 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.updateTimeBooleanfalseIf true, items can be dragged to another moment in time. See section Editing Items for a detailed explanation.editable.updateTimeBooleanfalseIf true, items can be dragged to another moment in time. See section Editing Items for a detailed explanation.
              endDate | Number | StringnoneThe 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.endDate | Number | StringnoneThe 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.
              groupOrderString | FunctionnoneOrder the groups by a field name or custom sort function. - By default, groups are not ordered. - groupOrderString | FunctionnoneOrder the groups by a field name or custom sort function. + By default, groups are not ordered. +
              heightNumber | StringnoneThe 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. - heightNumber | StringnoneThe 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.axisNumber20The minimal margin in pixels between items and the time axis.margin.axisNumber20The minimal margin in pixels between items and the time axis.
              margin.itemNumber10The minimal margin in pixels between items.margin.itemNumber10The minimal margin in pixels between items.
              maxDate | Number | StringnoneSet a maximum Date for the visible range. - It will not be possible to move beyond this maximum. - maxDate | Number | StringnoneSet a maximum Date for the visible range. + It will not be possible to move beyond this maximum. +
              maxHeightNumber | StringnoneSpecifies the maximum height for the Timeline. Can be a number in pixels or a string like "300px".maxHeightNumber | StringnoneSpecifies the maximum height for the Timeline. Can be a number in pixels or a string like "300px".
              minDate | Number | StringnoneSet a minimum Date for the visible range. - It will not be possible to move beyond this minimum. - minDate | Number | StringnoneSet a minimum Date for the visible range. + It will not be possible to move beyond this minimum. +
              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.
              onAddFunctionnoneCallback 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. - onAddFunctionnoneCallback 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. +
              onUpdateFunctionnoneCallback 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. - onUpdateFunctionnoneCallback 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. +
              onMoveFunctionnoneCallback 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. - onMoveFunctionnoneCallback 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. +
              onRemoveFunctionnoneCallback 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. - onRemoveFunctionnoneCallback 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. +
              orientationString'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.orientationString'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.
              paddingNumber5The 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: + paddingNumber5The 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;
               }
              -
              selectableBooleantrueIf 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).selectableBooleantrueIf 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).
              showCurrentTimebooleantrueShow a vertical bar at the current time.showCurrentTimebooleantrueShow a vertical bar at the current time.
              showCustomTimebooleanfalseShow 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.showCustomTimebooleanfalseShow 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.
              showMajorLabelsbooleantrueBy 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.showMajorLabelsbooleantrueBy 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.
              showMinorLabelsbooleantrueBy 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.showMinorLabelsbooleantrueBy 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.
              stackBooleantrueIf true (default), items will be stacked on top of each other such that they do not overlap.stackBooleantrueIf true (default), items will be stacked on top of each other such that they do not overlap.
              startDate | Number | StringnoneThe initial start date for the axis of the timeline. - If not provided, the earliest date present in the events is taken as start date.startDate | Number | StringnoneThe initial start date for the axis of the timeline. + If not provided, the earliest date present in the events is taken as start date.
              typeString'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. - typeStringnoneSpecifies 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. +
              widthString'100%'The width of the timeline in pixels or as a percentage.widthString'100%'The width of the timeline in pixels or as a percentage.
              zoomableBooleantrue - Specifies whether the Timeline can be zoomed by pinching or scrolling in the window. - Only applicable when option moveable is set true. - zoomableBooleantrue + Specifies whether the Timeline can be zoomed by pinching or scrolling in the window. + Only applicable when option moveable is set true. +
              zoomMaxNumber315360000000000Set 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. - zoomMaxNumber315360000000000Set 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. +
              zoomMinNumber10Set a minimum zoom interval for the visible range in milliseconds. - It will not be possible to zoom in further than this minimum. - zoomMinNumber10Set a minimum zoom interval for the visible range in milliseconds. + It will not be possible to zoom in further than this minimum. +