diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..7a1537ba
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.idea
+node_modules
diff --git a/Jakefile.js b/Jakefile.js
new file mode 100644
index 00000000..4f210bf6
--- /dev/null
+++ b/Jakefile.js
@@ -0,0 +1,114 @@
+/**
+ * Jake build script
+ */
+var jake = require('jake'),
+ fs = require('fs'),
+ path = require('path');
+
+require('jake-utils');
+
+/**
+ * default task
+ */
+desc('Execute all tasks: build all libraries');
+task('default', ['timeline'], function () {
+ console.log('done');
+});
+
+/**
+ * timeline
+ */
+desc('Build the timeline visualization');
+task('timeline', function () {
+ var TIMELINE = './bin/timeline/timeline.js';
+ var TIMELINE_MIN = './bin/timeline/timeline.min.js';
+ var DIR = './bin/timeline';
+ jake.rmRf(DIR);
+ jake.mkdirP(DIR);
+
+ // concatenate the script files
+ concat({
+ dest: TIMELINE,
+ src: [
+ './src/header.js',
+ './src/util.js',
+ './src/events.js',
+ './src/timestep.js',
+ './src/dataset.js',
+ './src/stack.js',
+ './src/range.js',
+ './src/controller.js',
+
+ './src/component/component.js',
+ './src/component/panel.js',
+ './src/component/rootpanel.js',
+ './src/component/timeaxis.js',
+ './src/component/itemset.js',
+ './src/component/item/*.js',
+
+ './src/visualization/timeline.js',
+
+ './lib/moment.js'
+ ],
+ separator: '\n'
+ });
+
+ // concatenate the css files
+ concat({
+ dest: './bin/timeline/timeline.css',
+ src: [
+ './src/component/css/panel.css',
+ './src/component/css/item.css',
+ './src/component/css/timeaxis.css'
+ ],
+ separator: '\n'
+ });
+
+ // minify javascript
+ minify({
+ src: TIMELINE,
+ dest: TIMELINE_MIN,
+ header: read('./src/header.js')
+ });
+
+ // update version number and stuff in the javascript files
+ [TIMELINE, TIMELINE_MIN].forEach(function (file) {
+ replace({
+ replacements: [
+ {pattern: '@@name', replacement: 'timeline'},
+ {pattern: '@@date', replacement: today()},
+ {pattern: '@@version', replacement: version()}
+ ],
+ src: file
+ });
+ });
+
+ // copy examples
+ jake.cpR('./src/examples/timeline', './bin/timeline/examples/');
+
+ console.log('created timeline library');
+});
+
+/**
+ * Recursively remove a directory and its files
+ * https://gist.github.com/tkihira/2367067
+ * @param {String} dir
+ */
+var rmdir = function(dir) {
+ var list = fs.readdirSync(dir);
+ for(var i = 0; i < list.length; i++) {
+ var filename = path.join(dir, list[i]);
+ var stat = fs.statSync(filename);
+
+ if(filename == "." || filename == "..") {
+ // pass these files
+ } else if(stat.isDirectory()) {
+ // rmdir recursively
+ rmdir(filename);
+ } else {
+ // rm fiilename
+ fs.unlinkSync(filename);
+ }
+ }
+ fs.rmdirSync(dir);
+};
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..ea2712c0
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,176 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 00000000..f8f5cecc
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,14 @@
+Vis.js
+Copyright 2010-2013 Almende B.V.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/README.md b/README.md
index 70fd9c66..54e2c2ca 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,10 @@
-vis
-===
+vis.js
+==================
-Dynamic, browser based visualization library
\ No newline at end of file
+Vis.js is a dynamic, browser based visualization library.
+The library is designed to be easy to use, to handle large amounts
+of dynamic data, and to enable manipulation of the data.
+The library consists of Timeline, LineChart, LineChart3d, Graph, and Treegrid.
+
+vis.js Library is part of [CHAP](http://chap.almende.com),
+the Common Hybrid Agent Platform, developed by [Almende B.V](http://almende.com).
diff --git a/bin/timeline/examples/01_basics.html b/bin/timeline/examples/01_basics.html
new file mode 100644
index 00000000..42bf45de
--- /dev/null
+++ b/bin/timeline/examples/01_basics.html
@@ -0,0 +1,62 @@
+
+
+
+ Timeline demo
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bin/timeline/timeline.css b/bin/timeline/timeline.css
new file mode 100644
index 00000000..5acc923b
--- /dev/null
+++ b/bin/timeline/timeline.css
@@ -0,0 +1,142 @@
+
+.graph {
+ position: relative;
+ border: 1px solid #bfbfbf;
+}
+
+.graph .panel {
+ position: absolute;
+}
+
+.graph .itemset {
+ position: absolute;
+}
+
+
+.graph .item {
+ position: absolute;
+ color: #1A1A1A;
+ border-color: #97B0F8;
+ background-color: #D5DDF6;
+ display: inline-block;
+}
+
+.graph .item.selected {
+ border-color: #FFC200;
+ background-color: #FFF785;
+ z-index: 999;
+}
+
+.graph .item.cluster {
+ /* TODO: use another color or pattern? */
+ background: #97B0F8 url('img/cluster_bg.png');
+ color: white;
+}
+.graph .item.cluster.point {
+ border-color: #D5DDF6;
+}
+
+.graph .item.box {
+ text-align: center;
+ border-style: solid;
+ border-width: 1px;
+ border-radius: 5px;
+ -moz-border-radius: 5px; /* For Firefox 3.6 and older */
+}
+
+.graph .item.point {
+ background: none;
+}
+
+.graph .dot {
+ border: 5px solid #97B0F8;
+ position: absolute;
+ border-radius: 5px;
+ -moz-border-radius: 5px; /* For Firefox 3.6 and older */
+}
+
+.graph .item.range {
+ overflow: hidden;
+ border-style: solid;
+ border-width: 1px;
+ border-radius: 2px;
+ -moz-border-radius: 2px; /* For Firefox 3.6 and older */
+}
+
+.graph .item.range .drag-left {
+ cursor: w-resize;
+ z-index: 1000;
+}
+
+.graph .item.range .drag-right {
+ cursor: e-resize;
+ z-index: 1000;
+}
+
+.graph .item.range .content {
+ position: relative;
+ display: inline-block;
+}
+
+.graph .item.line {
+ position: absolute;
+ width: 0;
+ border-left-width: 1px;
+ border-left-style: solid;
+ z-index: -1;
+}
+
+.graph .item .content {
+ margin: 5px;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+/* TODO: better css name, 'graph' is way to generic */
+
+.graph {
+ overflow: hidden;
+}
+
+.graph .axis {
+ position: relative;
+}
+
+.graph .axis .text {
+ position: absolute;
+ color: #4d4d4d;
+ padding: 3px;
+ white-space: nowrap;
+}
+
+.graph .axis .text.measure {
+ position: absolute;
+ padding-left: 0;
+ padding-right: 0;
+ margin-left: 0;
+ margin-right: 0;
+ visibility: hidden;
+}
+
+.graph .axis .grid.vertical {
+ position: absolute;
+ width: 0;
+ border-right: 1px solid;
+}
+
+.graph .axis .grid.horizontal {
+ position: absolute;
+ left: 0;
+ width: 100%;
+ height: 0;
+ border-bottom: 1px solid;
+}
+
+.graph .axis .grid.minor {
+ border-color: #e5e5e5;
+}
+
+.graph .axis .grid.major {
+ border-color: #bfbfbf;
+}
+
diff --git a/bin/timeline/timeline.js b/bin/timeline/timeline.js
new file mode 100644
index 00000000..59966ac1
--- /dev/null
+++ b/bin/timeline/timeline.js
@@ -0,0 +1,6267 @@
+/**
+ * timeline
+ * https://github.com/almende/vis
+ *
+ * A dynamic, browser-based visualization library.
+ *
+ * @version 3.0.0-SNAPSHOT
+ * @date 2013-04-16
+ *
+ * @license
+ * Copyright (C) 2011-2013 Almende B.V, http://almende.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+
+// create namespace
+var util = {};
+
+/**
+ * Test whether given object is a number
+ * @param {*} object
+ * @return {Boolean} isNumber
+ */
+util.isNumber = function isNumber(object) {
+ return (object instanceof Number || typeof object == 'number');
+};
+
+/**
+ * Test whether given object is a string
+ * @param {*} object
+ * @return {Boolean} isString
+ */
+util.isString = function isString(object) {
+ return (object instanceof String || typeof object == 'string');
+};
+
+/**
+ * Test whether given object is a Date, or a String containing a Date
+ * @param {Date | String} object
+ * @return {Boolean} isDate
+ */
+util.isDate = function isDate(object) {
+ if (object instanceof Date) {
+ return true;
+ }
+ else if (util.isString(object)) {
+ // test whether this string contains a date
+ var match = ASPDateRegex.exec(object);
+ if (match) {
+ return true;
+ }
+ else if (!isNaN(Date.parse(object))) {
+ return true;
+ }
+ }
+
+ return false;
+};
+
+/**
+ * Test whether given object is an instance of google.visualization.DataTable
+ * @param {*} object
+ * @return {Boolean} isDataTable
+ */
+util.isDataTable = function isDataTable(object) {
+ return (typeof (google) !== 'undefined') &&
+ (google.visualization) &&
+ (google.visualization.DataTable) &&
+ (object instanceof google.visualization.DataTable);
+};
+
+/**
+ * Create a semi UUID
+ * source: http://stackoverflow.com/a/105074/1262753
+ * @return {String} uuid
+ */
+util.randomUUID = function randomUUID () {
+ var S4 = function () {
+ return Math.floor(
+ Math.random() * 0x10000 /* 65536 */
+ ).toString(16);
+ };
+
+ return (
+ S4() + S4() + '-' +
+ S4() + '-' +
+ S4() + '-' +
+ S4() + '-' +
+ S4() + S4() + S4()
+ );
+};
+
+/**
+ * Extend object a with the properties of object b
+ * @param {Object} a
+ * @param {Object} b
+ * @return {Object} a
+ */
+util.extend = function (a, b) {
+ for (var prop in b) {
+ if (b.hasOwnProperty(prop)) {
+ a[prop] = b[prop];
+ }
+ }
+ return a;
+};
+
+/**
+ * Cast an object to another type
+ * @param {Boolean | Number | String | Date | Null | undefined} object
+ * @param {String | function | undefined} type Name of the type or a cast
+ * function. Available types:
+ * 'Boolean', 'Number', 'String',
+ * 'Date', ISODate', 'ASPDate'.
+ * @return {*} object
+ * @throws Error
+ */
+util.cast = function cast(object, type) {
+ if (object === undefined) {
+ return undefined;
+ }
+ if (object === null) {
+ return null;
+ }
+
+ if (!type) {
+ return object;
+ }
+ if (typeof type == 'function') {
+ return type(object);
+ }
+
+ //noinspection FallthroughInSwitchStatementJS
+ switch (type) {
+ case 'boolean':
+ case 'Boolean':
+ return Boolean(object);
+
+ case 'number':
+ case 'Number':
+ return Number(object);
+
+ case 'string':
+ case 'String':
+ return String(object);
+
+ case 'Date':
+ if (util.isNumber(object)) {
+ return new Date(object);
+ }
+ if (object instanceof Date) {
+ return new Date(object.valueOf());
+ }
+ if (util.isString(object)) {
+ // parse ASP.Net Date pattern,
+ // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
+ // code from http://momentjs.com/
+ var match = ASPDateRegex.exec(object);
+ if (match) {
+ return new Date(Number(match[1]));
+ }
+ else {
+ return new Date(object);
+ }
+ }
+ else {
+ throw new Error(
+ 'Cannot cast object of type ' + util.getType(object) +
+ ' to type Date');
+ }
+
+ case 'ISODate':
+ if (object instanceof Date) {
+ return object.toISOString();
+ }
+ else if (util.isNumber(object) || util.isString(Object)) {
+ return (new Date(object)).toISOString()
+ }
+ else {
+ throw new Error(
+ 'Cannot cast object of type ' + util.getType(object) +
+ ' to type ISODate');
+ }
+
+ case 'ASPDate':
+ if (object instanceof Date) {
+ return '/Date(' + object.valueOf() + ')/';
+ }
+ else if (util.isNumber(object) || util.isString(Object)) {
+ return '/Date(' + (new Date(object)).valueOf() + ')/';
+ }
+ else {
+ throw new Error(
+ 'Cannot cast object of type ' + util.getType(object) +
+ ' to type ASPDate');
+ }
+
+ default:
+ throw new Error('Cannot cast object of type ' + util.getType(object) +
+ ' to type "' + type + '"');
+ }
+};
+
+var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
+
+/**
+ * Get the type of an object
+ * @param {*} object
+ * @return {String} type
+ */
+util.getType = function getType(object) {
+ var type = typeof object;
+
+ if (type == 'object') {
+ if (object == null) {
+ return 'null';
+ }
+ if (object && object.constructor && object.constructor.name) {
+ return object.constructor.name;
+ }
+ }
+
+ return type;
+};
+
+/**
+ * Retrieve the absolute left value of a DOM element
+ * @param {Element} elem A dom element, for example a div
+ * @return {number} left The absolute left position of this element
+ * in the browser page.
+ */
+util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
+ var doc = document.documentElement;
+ var body = document.body;
+
+ var left = elem.offsetLeft;
+ var e = elem.offsetParent;
+ while (e != null && e != body && e != doc) {
+ left += e.offsetLeft;
+ left -= e.scrollLeft;
+ e = e.offsetParent;
+ }
+ return left;
+};
+
+/**
+ * Retrieve the absolute top value of a DOM element
+ * @param {Element} elem A dom element, for example a div
+ * @return {number} top The absolute top position of this element
+ * in the browser page.
+ */
+util.getAbsoluteTop = function getAbsoluteTop (elem) {
+ var doc = document.documentElement;
+ var body = document.body;
+
+ var top = elem.offsetTop;
+ var e = elem.offsetParent;
+ while (e != null && e != body && e != doc) {
+ top += e.offsetTop;
+ top -= e.scrollTop;
+ e = e.offsetParent;
+ }
+ return top;
+};
+
+/**
+ * Get the absolute, vertical mouse position from an event.
+ * @param {Event} event
+ * @return {Number} pageY
+ */
+util.getPageY = function getPageY (event) {
+ if ('pageY' in event) {
+ return event.pageY;
+ }
+ else {
+ var clientY;
+ if (('targetTouches' in event) && event.targetTouches.length) {
+ clientY = event.targetTouches[0].clientY;
+ }
+ else {
+ clientY = event.clientY;
+ }
+
+ var doc = document.documentElement;
+ var body = document.body;
+ return clientY +
+ ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
+ ( doc && doc.clientTop || body && body.clientTop || 0 );
+ }
+};
+
+/**
+ * Get the absolute, horizontal mouse position from an event.
+ * @param {Event} event
+ * @return {Number} pageX
+ */
+util.getPageX = function getPageX (event) {
+ if ('pageY' in event) {
+ return event.pageX;
+ }
+ else {
+ var clientX;
+ if (('targetTouches' in event) && event.targetTouches.length) {
+ clientX = event.targetTouches[0].clientX;
+ }
+ else {
+ clientX = event.clientX;
+ }
+
+ var doc = document.documentElement;
+ var body = document.body;
+ return clientX +
+ ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
+ ( doc && doc.clientLeft || body && body.clientLeft || 0 );
+ }
+};
+
+/**
+ * add a className to the given elements style
+ * @param {Element} elem
+ * @param {String} className
+ */
+util.addClassName = function addClassName(elem, className) {
+ var classes = elem.className.split(' ');
+ if (classes.indexOf(className) == -1) {
+ classes.push(className); // add the class to the array
+ elem.className = classes.join(' ');
+ }
+};
+
+/**
+ * add a className to the given elements style
+ * @param {Element} elem
+ * @param {String} className
+ */
+util.removeClassName = function removeClassname(elem, className) {
+ var classes = elem.className.split(' ');
+ var index = classes.indexOf(className);
+ if (index != -1) {
+ classes.splice(index, 1); // remove the class from the array
+ elem.className = classes.join(' ');
+ }
+};
+
+/**
+ * For each method for both arrays and objects.
+ * In case of an array, the built-in Array.forEach() is applied.
+ * In case of an Object, the method loops over all properties of the object.
+ * @param {Object | Array} object An Object or Array
+ * @param {function} callback Callback method, called for each item in
+ * the object or array with three parameters:
+ * callback(value, index, object)
+ */
+util.forEach = function forEach (object, callback) {
+ if (object instanceof Array) {
+ // array
+ object.forEach(callback);
+ }
+ else {
+ // object
+ for (var key in object) {
+ if (object.hasOwnProperty(key)) {
+ callback(object[key], key, object);
+ }
+ }
+ }
+};
+
+/**
+ * Update a property in an object
+ * @param {Object} object
+ * @param {String} key
+ * @param {*} value
+ * @return {Boolean} changed
+ */
+util.updateProperty = function updateProp (object, key, value) {
+ if (object[key] !== value) {
+ object[key] = value;
+ return true;
+ }
+ else {
+ return false;
+ }
+};
+
+/**
+ * Add and event listener. Works for all browsers
+ * @param {Element} element An html element
+ * @param {string} action The action, for example "click",
+ * without the prefix "on"
+ * @param {function} listener The callback function to be executed
+ * @param {boolean} [useCapture]
+ */
+util.addEventListener = function addEventListener(element, action, listener, useCapture) {
+ if (element.addEventListener) {
+ if (useCapture === undefined)
+ useCapture = false;
+
+ if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
+ action = "DOMMouseScroll"; // For Firefox
+ }
+
+ element.addEventListener(action, listener, useCapture);
+ } else {
+ element.attachEvent("on" + action, listener); // IE browsers
+ }
+};
+
+/**
+ * Remove an event listener from an element
+ * @param {Element} element An html dom element
+ * @param {string} action The name of the event, for example "mousedown"
+ * @param {function} listener The listener function
+ * @param {boolean} [useCapture]
+ */
+util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
+ if (element.removeEventListener) {
+ // non-IE browsers
+ if (useCapture === undefined)
+ useCapture = false;
+
+ if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
+ action = "DOMMouseScroll"; // For Firefox
+ }
+
+ element.removeEventListener(action, listener, useCapture);
+ } else {
+ // IE browsers
+ element.detachEvent("on" + action, listener);
+ }
+};
+
+
+/**
+ * Get HTML element which is the target of the event
+ * @param {Event} event
+ * @return {Element} target element
+ */
+util.getTarget = function getTarget(event) {
+ // code from http://www.quirksmode.org/js/events_properties.html
+ if (!event) {
+ event = window.event;
+ }
+
+ var target;
+
+ if (event.target) {
+ target = event.target;
+ }
+ else if (event.srcElement) {
+ target = event.srcElement;
+ }
+
+ if (target.nodeType != undefined && target.nodeType == 3) {
+ // defeat Safari bug
+ target = target.parentNode;
+ }
+
+ return target;
+};
+
+/**
+ * Stop event propagation
+ */
+util.stopPropagation = function stopPropagation(event) {
+ if (!event)
+ event = window.event;
+
+ if (event.stopPropagation) {
+ event.stopPropagation(); // non-IE browsers
+ }
+ else {
+ event.cancelBubble = true; // IE browsers
+ }
+};
+
+
+/**
+ * Cancels the event if it is cancelable, without stopping further propagation of the event.
+ */
+util.preventDefault = function preventDefault (event) {
+ if (!event)
+ event = window.event;
+
+ if (event.preventDefault) {
+ event.preventDefault(); // non-IE browsers
+ }
+ else {
+ event.returnValue = false; // IE browsers
+ }
+};
+
+
+util.option = {};
+
+/**
+ * Cast a value as boolean
+ * @param {Boolean | function | undefined} value
+ * @param {Boolean} [defaultValue]
+ * @returns {Boolean} bool
+ */
+util.option.asBoolean = function (value, defaultValue) {
+ if (typeof value == 'function') {
+ value = value();
+ }
+
+ if (value != null) {
+ return (value != false);
+ }
+
+ return defaultValue || null;
+};
+
+/**
+ * Cast a value as string
+ * @param {String | function | undefined} value
+ * @param {String} [defaultValue]
+ * @returns {String} str
+ */
+util.option.asString = function (value, defaultValue) {
+ if (typeof value == 'function') {
+ value = value();
+ }
+
+ if (value != null) {
+ return String(value);
+ }
+
+ return defaultValue || null;
+};
+
+/**
+ * Cast a size or location in pixels or a percentage
+ * @param {String | Number | function | undefined} value
+ * @param {String} [defaultValue]
+ * @returns {String} size
+ */
+util.option.asSize = function (value, defaultValue) {
+ if (typeof value == 'function') {
+ value = value();
+ }
+
+ if (util.isString(value)) {
+ return value;
+ }
+ else if (util.isNumber(value)) {
+ return value + 'px';
+ }
+ else {
+ return defaultValue || null;
+ }
+};
+
+/**
+ * Cast a value as DOM element
+ * @param {HTMLElement | function | undefined} value
+ * @param {HTMLElement} [defaultValue]
+ * @returns {HTMLElement | null} dom
+ */
+util.option.asElement = function (value, defaultValue) {
+ if (typeof value == 'function') {
+ value = value();
+ }
+
+ return value || defaultValue || null;
+};
+
+
+// Internet Explorer 8 and older does not support Array.indexOf, so we define
+// it here in that case.
+// http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
+if(!Array.prototype.indexOf) {
+ Array.prototype.indexOf = function(obj){
+ for(var i = 0; i < this.length; i++){
+ if(this[i] == obj){
+ return i;
+ }
+ }
+ return -1;
+ };
+
+ try {
+ console.log("Warning: Ancient browser detected. Please update your browser");
+ }
+ catch (err) {
+ }
+}
+
+// Internet Explorer 8 and older does not support Array.forEach, so we define
+// it here in that case.
+// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
+if (!Array.prototype.forEach) {
+ Array.prototype.forEach = function(fn, scope) {
+ for(var i = 0, len = this.length; i < len; ++i) {
+ fn.call(scope || this, this[i], i, this);
+ }
+ }
+}
+
+// Internet Explorer 8 and older does not support Array.map, so we define it
+// here in that case.
+// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map
+// Production steps of ECMA-262, Edition 5, 15.4.4.19
+// Reference: http://es5.github.com/#x15.4.4.19
+if (!Array.prototype.map) {
+ Array.prototype.map = function(callback, thisArg) {
+
+ var T, A, k;
+
+ if (this == null) {
+ throw new TypeError(" this is null or not defined");
+ }
+
+ // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
+ var O = Object(this);
+
+ // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
+ // 3. Let len be ToUint32(lenValue).
+ var len = O.length >>> 0;
+
+ // 4. If IsCallable(callback) is false, throw a TypeError exception.
+ // See: http://es5.github.com/#x9.11
+ if (typeof callback !== "function") {
+ throw new TypeError(callback + " is not a function");
+ }
+
+ // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
+ if (thisArg) {
+ T = thisArg;
+ }
+
+ // 6. Let A be a new array created as if by the expression new Array(len) where Array is
+ // the standard built-in constructor with that name and len is the value of len.
+ A = new Array(len);
+
+ // 7. Let k be 0
+ k = 0;
+
+ // 8. Repeat, while k < len
+ while(k < len) {
+
+ var kValue, mappedValue;
+
+ // a. Let Pk be ToString(k).
+ // This is implicit for LHS operands of the in operator
+ // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
+ // This step can be combined with c
+ // c. If kPresent is true, then
+ if (k in O) {
+
+ // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
+ kValue = O[ k ];
+
+ // ii. Let mappedValue be the result of calling the Call internal method of callback
+ // with T as the this value and argument list containing kValue, k, and O.
+ mappedValue = callback.call(T, kValue, k, O);
+
+ // iii. Call the DefineOwnProperty internal method of A with arguments
+ // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true},
+ // and false.
+
+ // In browsers that support Object.defineProperty, use the following:
+ // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true });
+
+ // For best browser support, use the following:
+ A[ k ] = mappedValue;
+ }
+ // d. Increase k by 1.
+ k++;
+ }
+
+ // 9. return A
+ return A;
+ };
+}
+
+// Internet Explorer 8 and older does not support Array.filter, so we define it
+// here in that case.
+// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter
+if (!Array.prototype.filter) {
+ Array.prototype.filter = function(fun /*, thisp */) {
+ "use strict";
+
+ if (this == null) {
+ throw new TypeError();
+ }
+
+ var t = Object(this);
+ var len = t.length >>> 0;
+ if (typeof fun != "function") {
+ throw new TypeError();
+ }
+
+ var res = [];
+ var thisp = arguments[1];
+ for (var i = 0; i < len; i++) {
+ if (i in t) {
+ var val = t[i]; // in case fun mutates this
+ if (fun.call(thisp, val, i, t))
+ res.push(val);
+ }
+ }
+
+ return res;
+ };
+}
+
+
+// Internet Explorer 8 and older does not support Object.keys, so we define it
+// here in that case.
+// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys
+if (!Object.keys) {
+ Object.keys = (function () {
+ var hasOwnProperty = Object.prototype.hasOwnProperty,
+ hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
+ dontEnums = [
+ 'toString',
+ 'toLocaleString',
+ 'valueOf',
+ 'hasOwnProperty',
+ 'isPrototypeOf',
+ 'propertyIsEnumerable',
+ 'constructor'
+ ],
+ dontEnumsLength = dontEnums.length;
+
+ return function (obj) {
+ if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) {
+ throw new TypeError('Object.keys called on non-object');
+ }
+
+ var result = [];
+
+ for (var prop in obj) {
+ if (hasOwnProperty.call(obj, prop)) result.push(prop);
+ }
+
+ if (hasDontEnumBug) {
+ for (var i=0; i < dontEnumsLength; i++) {
+ if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]);
+ }
+ }
+ return result;
+ }
+ })()
+}
+
+
+/**
+ * Event listener (singleton)
+ */
+var events = {
+ 'listeners': [],
+
+ /**
+ * Find a single listener by its object
+ * @param {Object} object
+ * @return {Number} index -1 when not found
+ */
+ 'indexOf': function (object) {
+ var listeners = this.listeners;
+ for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
+ var listener = listeners[i];
+ if (listener && listener.object == object) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ /**
+ * Add an event listener
+ * @param {Object} object
+ * @param {String} event The name of an event, for example 'select'
+ * @param {function} callback The callback method, called when the
+ * event takes place
+ */
+ 'addListener': function (object, event, callback) {
+ var index = this.indexOf(object);
+ var listener = this.listeners[index];
+ if (!listener) {
+ listener = {
+ 'object': object,
+ 'events': {}
+ };
+ this.listeners.push(listener);
+ }
+
+ var callbacks = listener.events[event];
+ if (!callbacks) {
+ callbacks = [];
+ listener.events[event] = callbacks;
+ }
+
+ // add the callback if it does not yet exist
+ if (callbacks.indexOf(callback) == -1) {
+ callbacks.push(callback);
+ }
+ },
+
+ /**
+ * Remove an event listener
+ * @param {Object} object
+ * @param {String} event The name of an event, for example 'select'
+ * @param {function} callback The registered callback method
+ */
+ 'removeListener': function (object, event, callback) {
+ var index = this.indexOf(object);
+ var listener = this.listeners[index];
+ if (listener) {
+ var callbacks = listener.events[event];
+ if (callbacks) {
+ index = callbacks.indexOf(callback);
+ if (index != -1) {
+ callbacks.splice(index, 1);
+ }
+
+ // remove the array when empty
+ if (callbacks.length == 0) {
+ delete listener.events[event];
+ }
+ }
+
+ // count the number of registered events. remove listener when empty
+ var count = 0;
+ var events = listener.events;
+ for (var e in events) {
+ if (events.hasOwnProperty(e)) {
+ count++;
+ }
+ }
+ if (count == 0) {
+ delete this.listeners[index];
+ }
+ }
+ },
+
+ /**
+ * Remove all registered event listeners
+ */
+ 'removeAllListeners': function () {
+ this.listeners = [];
+ },
+
+ /**
+ * Trigger an event. All registered event handlers will be called
+ * @param {Object} object
+ * @param {String} event
+ * @param {Object} properties (optional)
+ */
+ 'trigger': function (object, event, properties) {
+ var index = this.indexOf(object);
+ var listener = this.listeners[index];
+ if (listener) {
+ var callbacks = listener.events[event];
+ if (callbacks) {
+ for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
+ callbacks[i](properties);
+ }
+ }
+ }
+ }
+};
+
+/**
+ * @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
+ */
+TimeStep = function(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";
+ return;
+ }
+
+ 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;
+};
+
+
+/**
+ * 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;
+ }
+
+ 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
+ */
+TimeStep.prototype.snap = function(date) {
+ if (this.scale == TimeStep.SCALE.YEAR) {
+ var year = date.getFullYear() + Math.round(date.getMonth() / 12);
+ date.setFullYear(Math.round(year / this.step) * this.step);
+ date.setMonth(0);
+ date.setDate(0);
+ date.setHours(0);
+ date.setMinutes(0);
+ date.setSeconds(0);
+ date.setMilliseconds(0);
+ }
+ else if (this.scale == TimeStep.SCALE.MONTH) {
+ if (date.getDate() > 15) {
+ date.setDate(1);
+ date.setMonth(date.getMonth() + 1);
+ // important: first set Date to 1, after that change the month.
+ }
+ else {
+ date.setDate(1);
+ }
+
+ date.setHours(0);
+ date.setMinutes(0);
+ date.setSeconds(0);
+ date.setMilliseconds(0);
+ }
+ else if (this.scale == TimeStep.SCALE.DAY ||
+ this.scale == TimeStep.SCALE.WEEKDAY) {
+ //noinspection FallthroughInSwitchStatementJS
+ switch (this.step) {
+ case 5:
+ case 2:
+ date.setHours(Math.round(date.getHours() / 24) * 24); break;
+ default:
+ date.setHours(Math.round(date.getHours() / 12) * 12); break;
+ }
+ date.setMinutes(0);
+ date.setSeconds(0);
+ date.setMilliseconds(0);
+ }
+ else if (this.scale == TimeStep.SCALE.HOUR) {
+ switch (this.step) {
+ case 4:
+ date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
+ default:
+ date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
+ }
+ date.setSeconds(0);
+ date.setMilliseconds(0);
+ } else if (this.scale == TimeStep.SCALE.MINUTE) {
+ //noinspection FallthroughInSwitchStatementJS
+ switch (this.step) {
+ case 15:
+ case 10:
+ date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
+ date.setSeconds(0);
+ break;
+ case 5:
+ date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
+ default:
+ date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
+ }
+ date.setMilliseconds(0);
+ }
+ else if (this.scale == TimeStep.SCALE.SECOND) {
+ //noinspection FallthroughInSwitchStatementJS
+ switch (this.step) {
+ case 15:
+ case 10:
+ date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
+ date.setMilliseconds(0);
+ break;
+ case 5:
+ date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
+ default:
+ date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
+ }
+ }
+ else if (this.scale == TimeStep.SCALE.MILLISECOND) {
+ var step = this.step > 5 ? this.step / 2 : 1;
+ date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
+ }
+};
+
+/**
+ * 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 '';
+ }
+};
+
+
+/**
+ * Returns formatted text for the major axislabel, 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
+ */
+TimeStep.prototype.getLabelMajor = function(date) {
+ if (date == undefined) {
+ date = this.current;
+ }
+
+ //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 '';
+ }
+};
+
+/**
+ * DataSet
+ *
+ * Usage:
+ * var dataSet = new DataSet({
+ * fieldId: '_id',
+ * fieldTypes: {
+ * // ...
+ * }
+ * });
+ *
+ * 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 {Object} [options] Available options:
+ * {String} fieldId Field name of the id in the
+ * items, 'id' by default.
+ * {Object.} [fieldTypes]
+ * {String[]} [fields] filter fields
+ * @param {Array | DataTable} [data] If provided, items will be appended
+ * to this array or table. Required
+ * in case of Google DataTable
+ * @return {Array | Object | DataTable | null} data
+ * @throws Error
+ */
+DataSet.prototype.get = function (ids, options, data) {
+ var me = this;
+
+ // shift arguments when first argument contains the options
+ if (util.getType(ids) == 'Object') {
+ data = options;
+ options = ids;
+ ids = undefined;
+ }
+
+ // merge field types
+ var fieldTypes = {};
+ if (this.options && this.options.fieldTypes) {
+ util.forEach(this.options.fieldTypes, function (value, field) {
+ fieldTypes[field] = value;
+ });
+ }
+ if (options && options.fieldTypes) {
+ util.forEach(options.fieldTypes, function (value, field) {
+ fieldTypes[field] = value;
+ });
+ }
+
+ var fields = options ? options.fields : undefined;
+
+ // determine the return type
+ var type;
+ if (options && options.type) {
+ type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
+
+ if (data && (type != util.getType(data))) {
+ throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
+ 'does not correspond with specified options.type (' + options.type + ')');
+ }
+ if (type == 'DataTable' && !util.isDataTable(data)) {
+ throw new Error('Parameter "data" must be a DataTable ' +
+ 'when options.type is "DataTable"');
+ }
+ }
+ else if (data) {
+ type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
+ }
+ else {
+ type = 'Array';
+ }
+
+ if (type == 'DataTable') {
+ // return a Google DataTable
+ var columns = this._getColumnNames(data);
+ if (ids == undefined) {
+ // return all data
+ util.forEach(this.data, function (item) {
+ me._appendRow(data, columns, me._castItem(item));
+ });
+ }
+ else if (util.isNumber(ids) || util.isString(ids)) {
+ var item = me._castItem(me.data[ids], fieldTypes, fields);
+ this._appendRow(data, columns, item);
+ }
+ else if (ids instanceof Array) {
+ ids.forEach(function (id) {
+ var item = me._castItem(me.data[id], fieldTypes, fields);
+ me._appendRow(data, columns, item);
+ });
+ }
+ else {
+ throw new TypeError('Parameter "ids" must be ' +
+ 'undefined, a String, Number, or Array');
+ }
+ }
+ else {
+ // return an array
+ data = data || [];
+ if (ids == undefined) {
+ // return all data
+ util.forEach(this.data, function (item) {
+ data.push(me._castItem(item, fieldTypes, fields));
+ });
+ }
+ else if (util.isNumber(ids) || util.isString(ids)) {
+ // return a single item
+ return this._castItem(me.data[ids], fieldTypes, fields);
+ }
+ else if (ids instanceof Array) {
+ ids.forEach(function (id) {
+ data.push(me._castItem(me.data[id], fieldTypes, fields));
+ });
+ }
+ else {
+ throw new TypeError('Parameter "ids" must be ' +
+ 'undefined, a String, Number, or Array');
+ }
+ }
+
+ return data;
+};
+
+/**
+ * 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, used to trigger events for
+ * all but this sender's event subscribers.
+ */
+DataSet.prototype.remove = function (id, senderId) {
+ var items = [],
+ me = this;
+
+ if (util.isNumber(id) || util.isString(id)) {
+ delete this.data[id];
+ delete this.internalIds[id];
+ items.push(id);
+ }
+ else if (id instanceof Array) {
+ id.forEach(function (id) {
+ me.remove(id);
+ });
+ items = items.concat(id);
+ }
+ else if (id instanceof Object) {
+ // search for the object
+ for (var i in this.data) {
+ if (this.data.hasOwnProperty(i)) {
+ if (this.data[i] == id) {
+ delete this.data[i];
+ delete this.internalIds[i];
+ items.push(i);
+ }
+ }
+ }
+ }
+
+ this._trigger('remove', {items: items}, senderId);
+};
+
+/**
+ * Clear the data
+ * @param {String} [senderId] Optional sender id, used to trigger events for
+ * all but this sender's event subscribers.
+ */
+DataSet.prototype.clear = function (senderId) {
+ var items = Object.keys(this.data);
+
+ this.data = [];
+ this.internalIds = {};
+
+ this._trigger('remove', {items: items}, senderId);
+};
+
+/**
+ * Add a single item
+ * @param {Object} item
+ * @return {String} id
+ * @private
+ */
+DataSet.prototype._addItem = function (item) {
+ var id = item[this.fieldId];
+ if (id == undefined) {
+ // generate an id
+ id = util.randomUUID();
+ item[this.fieldId] = id;
+
+ this.internalIds[id] = item;
+ }
+
+ var d = {};
+ for (var field in item) {
+ if (item.hasOwnProperty(field)) {
+ var type = this.fieldTypes[field]; // type may be undefined
+ d[field] = util.cast(item[field], type);
+ }
+ }
+ this.data[id] = d;
+ //TODO: fail when an item with this id already exists?
+
+ return id;
+};
+
+/**
+ * Cast and filter the fields of an item
+ * @param {Object | undefined} item
+ * @param {Object.} [fieldTypes]
+ * @param {String[]} [fields]
+ * @return {Object | null} castedItem
+ * @private
+ */
+DataSet.prototype._castItem = function (item, fieldTypes, fields) {
+ var clone,
+ fieldId = this.fieldId,
+ internalIds = this.internalIds;
+
+ if (item) {
+ clone = {};
+ fieldTypes = fieldTypes || {};
+
+ if (fields) {
+ // output filtered fields
+ util.forEach(item, function (value, field) {
+ if (fields.indexOf(field) != -1) {
+ clone[field] = util.cast(value, fieldTypes[field]);
+ }
+ });
+ }
+ else {
+ // output all fields, except internal ids
+ util.forEach(item, function (value, field) {
+ if (field != fieldId || !(value in internalIds)) {
+ clone[field] = util.cast(value, fieldTypes[field]);
+ }
+ });
+ }
+ }
+ else {
+ clone = null;
+ }
+
+ return clone;
+};
+
+/**
+ * Update a single item: merge with existing item
+ * @param {Object} item
+ * @return {String} id
+ * @private
+ */
+DataSet.prototype._updateItem = function (item) {
+ var id = item[this.fieldId];
+ if (id == undefined) {
+ throw new Error('Item has no id (item: ' + JSON.stringify(item) + ')');
+ }
+ var d = this.data[id];
+ if (d) {
+ // merge with current item
+ for (var field in item) {
+ if (item.hasOwnProperty(field)) {
+ var type = this.fieldTypes[field]; // type may be undefined
+ d[field] = util.cast(item[field], type);
+ }
+ }
+ }
+ else {
+ // create new item
+ this._addItem(item);
+ }
+
+ return id;
+};
+
+/**
+ * Get an array with the column names of a Google DataTable
+ * @param {DataTable} dataTable
+ * @return {Array} 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();
+ columns.forEach(function (field, col) {
+ dataTable.setValue(row, col, item[field]);
+ });
+};
+
+/**
+ * @constructor Stack
+ * Stacks items on top of each other.
+ * @param {ItemSet} parent
+ * @param {Object} [options]
+ */
+function Stack (parent, options) {
+ this.parent = parent;
+ this.options = {
+ order: function (a, b) {
+ return (b.width - a.width) || (a.left - b.left);
+ }
+ };
+
+ this.ordered = []; // ordered items
+
+ this.setOptions(options);
+}
+
+/**
+ * Set options for the stack
+ * @param {Object} options Available options:
+ * {ItemSet} parent
+ * {Number} margin
+ * {function} order Stacking order
+ */
+Stack.prototype.setOptions = function (options) {
+ util.extend(this.options, options);
+
+ // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately
+};
+
+/**
+ * Stack the items such that they don't overlap. The items will have a minimal
+ * distance equal to options.margin.item.
+ */
+Stack.prototype.update = function() {
+ this._order();
+ this._stack();
+};
+
+/**
+ * Order the items. The items are ordered by width first, and by left position
+ * second.
+ * If a custom order function has been provided via the options, then this will
+ * be used.
+ * @private
+ */
+Stack.prototype._order = function() {
+ var items = this.parent.items;
+ if (!items) {
+ throw new Error('Cannot stack items: parent does not contain items');
+ }
+
+ // TODO: store the sorted items, to have less work later on
+ var ordered = [];
+ var index = 0;
+ util.forEach(items, function (item, id) {
+ ordered[index] = item;
+ index++;
+ });
+
+ //if a customer stack order function exists, use it.
+ var order = this.options.order;
+ if (!(typeof this.options.order === 'function')) {
+ throw new Error('Option order must be a function');
+ }
+
+ ordered.sort(order);
+
+ this.ordered = ordered;
+};
+
+/**
+ * Adjust vertical positions of the events such that they don't overlap each
+ * other.
+ * @private
+ */
+Stack.prototype._stack = function() {
+ var i,
+ iMax,
+ ordered = this.ordered,
+ options = this.options,
+ axisOnTop = (options.orientation == 'top'),
+ margin = options.margin && options.margin.item || 0;
+
+ // calculate new, non-overlapping positions
+ for (i = 0, iMax = ordered.length; i < iMax; i++) {
+ var item = ordered[i];
+ var collidingItem = null;
+ do {
+ // TODO: optimize checking for overlap. when there is a gap without items,
+ // you only need to check for items from the next item on, not from zero
+ collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin);
+ if (collidingItem != null) {
+ // There is a collision. Reposition the event above the colliding element
+ if (axisOnTop) {
+ item.top = collidingItem.top + collidingItem.height + margin;
+ }
+ else {
+ item.top = collidingItem.top - item.height - margin;
+ }
+ }
+ } while (collidingItem);
+ }
+};
+
+/**
+ * Check if the destiny position of given item overlaps with any
+ * of the other items from index itemStart to itemEnd.
+ * @param {Array} items Array with items
+ * @param {int} itemIndex Number of the item to be checked for overlap
+ * @param {int} itemStart First item to be checked.
+ * @param {int} itemEnd Last item to be checked.
+ * @return {Object | null} colliding item, or undefined when no collisions
+ * @param {Number} margin A minimum required margin.
+ * If margin is provided, the two items will be
+ * marked colliding when they overlap or
+ * when the margin between the two is smaller than
+ * the requested margin.
+ */
+Stack.prototype.checkOverlap = function(items, itemIndex, itemStart, itemEnd, margin) {
+ var collision = this.collision;
+
+ // we loop from end to start, as we suppose that the chance of a
+ // collision is larger for items at the end, so check these first.
+ var a = items[itemIndex];
+ for (var i = itemEnd; i >= itemStart; i--) {
+ var b = items[i];
+ if (collision(a, b, margin)) {
+ if (i != itemIndex) {
+ return b;
+ }
+ }
+ }
+
+ return null;
+};
+
+/**
+ * Test if the two provided items collide
+ * The items must have parameters left, width, top, and height.
+ * @param {Component} a The first item
+ * @param {Component} 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.prototype.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 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 {Object} [options] See description at Range.setOptions
+ * @extends Controller
+ */
+function Range(options) {
+ this.id = util.randomUUID();
+ this.start = 0; // Number
+ this.end = 0; // Number
+
+ this.options = {
+ min: null,
+ max: null,
+ zoomMin: null,
+ zoomMax: null
+ };
+
+ this.setOptions(options);
+
+ this.listeners = [];
+}
+
+/**
+ * Set options for the range controller
+ * @param {Object} options Available options:
+ * {Number} start Set start value of the range
+ * {Number} end Set end value of 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).
+ */
+Range.prototype.setOptions = function (options) {
+ util.extend(this.options, options);
+
+ if (options.start != null || options.end != null) {
+ this.setRange(options.start, options.end);
+ }
+};
+
+/**
+ * Add listeners for mouse and touch events to the component
+ * @param {Component} component
+ * @param {String} event Available events: 'move', 'zoom'
+ * @param {String} direction Available directions: 'horizontal', 'vertical'
+ */
+Range.prototype.subscribe = function (component, event, direction) {
+ var me = this;
+ var listener;
+
+ if (direction != 'horizontal' && direction != 'vertical') {
+ throw new TypeError('Unknown direction "' + direction + '". ' +
+ 'Choose "horizontal" or "vertical".');
+ }
+
+ //noinspection FallthroughInSwitchStatementJS
+ if (event == 'move') {
+ listener = {
+ component: component,
+ event: event,
+ direction: direction,
+ callback: function (event) {
+ me._onMouseDown(event, listener);
+ },
+ params: {}
+ };
+
+ component.on('mousedown', listener.callback);
+ me.listeners.push(listener);
+ }
+ else if (event == 'zoom') {
+ listener = {
+ component: component,
+ event: event,
+ direction: direction,
+ callback: function (event) {
+ me._onMouseWheel(event, listener);
+ },
+ params: {}
+ };
+
+ component.on('mousewheel', listener.callback);
+ me.listeners.push(listener);
+ }
+ else {
+ throw new TypeError('Unknown event "' + event + '". ' +
+ 'Choose "move" or "zoom".');
+ }
+};
+
+/**
+ * Event handler
+ * @param {String} event name of the event, for example 'click', 'mousemove'
+ * @param {function} callback callback handler, invoked with the raw HTML Event
+ * as parameter.
+ */
+Range.prototype.on = function (event, callback) {
+ events.addListener(this, event, callback);
+};
+
+/**
+ * Trigger an event
+ * @param {String} event name of the event, available events: 'rangechange',
+ * 'rangechanged'
+ * @private
+ */
+Range.prototype._trigger = function (event) {
+ events.trigger(this, event, {
+ start: this.start,
+ end: this.end
+ });
+};
+
+/**
+ * Set a new start and end range
+ * @param {Number} start
+ * @param {Number} end
+ */
+Range.prototype.setRange = function(start, end) {
+ var changed = this._applyRange(start, end);
+ if (changed) {
+ this._trigger('rangechange');
+ this._trigger('rangechanged');
+ }
+};
+
+/**
+ * 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
+ */
+Range.prototype._applyRange = function(start, end) {
+ var newStart = util.cast(start, 'Number');
+ var newEnd = util.cast(end, 'Number');
+ var diff;
+
+ // check for valid number
+ if (newStart == null || isNaN(newStart)) {
+ throw new Error('Invalid start "' + start + '"');
+ }
+ if (newEnd == null || isNaN(newEnd)) {
+ throw new Error('Invalid end "' + end + '"');
+ }
+
+ // prevent start < end
+ if (newEnd < newStart) {
+ newEnd = newStart;
+ }
+
+ // prevent start < min
+ if (this.options.min != null) {
+ var min = this.options.min.valueOf();
+ if (newStart < min) {
+ diff = (min - newStart);
+ newStart += diff;
+ newEnd += diff;
+ }
+ }
+
+ // prevent end > max
+ if (this.options.max != null) {
+ var max = this.options.max.valueOf();
+ if (newEnd > max) {
+ diff = (newEnd - max);
+ newStart -= diff;
+ newEnd -= diff;
+ }
+ }
+
+ // prevent (end-start) > zoomMin
+ if (this.options.zoomMin != null) {
+ var zoomMin = this.options.zoomMin.valueOf();
+ if (zoomMin < 0) {
+ zoomMin = 0;
+ }
+ if ((newEnd - newStart) < zoomMin) {
+ if ((this.end - this.start) > zoomMin) {
+ // zoom to the minimum
+ diff = (zoomMin - (newEnd - newStart));
+ newStart -= diff / 2;
+ newEnd += diff / 2;
+ }
+ else {
+ // ingore this action, we are already zoomed to the minimum
+ newStart = this.start;
+ newEnd = this.end;
+ }
+ }
+ }
+
+ // prevent (end-start) > zoomMin
+ if (this.options.zoomMax != null) {
+ var zoomMax = this.options.zoomMax.valueOf();
+ if (zoomMax < 0) {
+ zoomMax = 0;
+ }
+ if ((newEnd - newStart) > zoomMax) {
+ if ((this.end - this.start) < zoomMax) {
+ // zoom to the maximum
+ diff = ((newEnd - newStart) - zoomMax);
+ newStart += diff / 2;
+ newEnd -= diff / 2;
+ }
+ else {
+ // ingore this action, we are already zoomed to the maximum
+ newStart = this.start;
+ newEnd = this.end;
+ }
+ }
+ }
+
+ var changed = (this.start != newStart || this.end != newEnd);
+
+ this.start = newStart;
+ this.end = newEnd;
+
+ return changed;
+};
+
+/**
+ * Retrieve the current range.
+ * @return {Object} An object with start and end properties
+ */
+Range.prototype.getRange = function() {
+ return {
+ start: this.start,
+ end: this.end
+ };
+};
+
+/**
+ * Calculate the conversion offset and factor for current range, based on
+ * the provided width
+ * @param {Number} width
+ * @returns {{offset: number, factor: number}} conversion
+ */
+Range.prototype.conversion = function (width) {
+ var start = this.start;
+ var end = this.end;
+
+ return Range.conversion(this.start, this.end, width);
+};
+
+/**
+ * Static method to calculate the conversion offset and factor for a range,
+ * based on the provided start, end, and width
+ * @param {Number} start
+ * @param {Number} end
+ * @param {Number} width
+ * @returns {{offset: number, factor: number}} conversion
+ */
+Range.conversion = function (start, end, width) {
+ if (width != 0 && (end - start != 0)) {
+ return {
+ offset: start,
+ factor: width / (end - start)
+ }
+ }
+ else {
+ return {
+ offset: 0,
+ factor: 1
+ };
+ }
+};
+
+/**
+ * Start moving horizontally or vertically
+ * @param {Event} event
+ * @param {Object} listener Listener containing the component and params
+ * @private
+ */
+Range.prototype._onMouseDown = function(event, listener) {
+ event = event || window.event;
+ var params = listener.params;
+
+ // only react on left mouse button down
+ var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
+ if (!leftButtonDown) {
+ return;
+ }
+
+ // get mouse position
+ params.mouseX = util.getPageX(event);
+ params.mouseY = util.getPageY(event);
+ params.previousLeft = 0;
+ params.previousOffset = 0;
+
+ params.moved = false;
+ params.start = this.start;
+ params.end = this.end;
+
+ var frame = listener.component.frame;
+ if (frame) {
+ frame.style.cursor = 'move';
+ }
+
+ // add event listeners to handle moving the contents
+ // we store the function onmousemove and onmouseup in the timeaxis,
+ // so we can remove the eventlisteners lateron in the function onmouseup
+ var me = this;
+ if (!params.onMouseMove) {
+ params.onMouseMove = function (event) {
+ me._onMouseMove(event, listener);
+ };
+ util.addEventListener(document, "mousemove", params.onMouseMove);
+ }
+ if (!params.onMouseUp) {
+ params.onMouseUp = function (event) {
+ me._onMouseUp(event, listener);
+ };
+ util.addEventListener(document, "mouseup", params.onMouseUp);
+ }
+
+ util.preventDefault(event);
+};
+
+/**
+ * Perform moving operating.
+ * This function activated from within the funcion TimeAxis._onMouseDown().
+ * @param {Event} event
+ * @param {Object} listener
+ * @private
+ */
+Range.prototype._onMouseMove = function (event, listener) {
+ event = event || window.event;
+
+ var params = listener.params;
+
+ // calculate change in mouse position
+ var mouseX = util.getPageX(event);
+ var mouseY = util.getPageY(event);
+
+ if (params.mouseX == undefined) {
+ params.mouseX = mouseX;
+ }
+ if (params.mouseY == undefined) {
+ params.mouseY = mouseY;
+ }
+
+ var diffX = mouseX - params.mouseX;
+ var diffY = mouseY - params.mouseY;
+ var diff = (listener.direction == 'horizontal') ? diffX : diffY;
+
+ // if mouse movement is big enough, register it as a "moved" event
+ if (Math.abs(diff) >= 1) {
+ params.moved = true;
+ }
+
+ var interval = (params.end - params.start);
+ var width = (listener.direction == 'horizontal') ?
+ listener.component.width : listener.component.height;
+ var diffRange = -diff / width * interval;
+ this._applyRange(params.start + diffRange, params.end + diffRange);
+
+ // fire a rangechange event
+ this._trigger('rangechange');
+
+ util.preventDefault(event);
+};
+
+/**
+ * Stop moving operating.
+ * This function activated from within the function Range._onMouseDown().
+ * @param {event} event
+ * @param {Object} listener
+ * @private
+ */
+Range.prototype._onMouseUp = function (event, listener) {
+ event = event || window.event;
+
+ var params = listener.params;
+
+ if (listener.component.frame) {
+ listener.component.frame.style.cursor = 'auto';
+ }
+
+ // remove event listeners here, important for Safari
+ if (params.onMouseMove) {
+ util.removeEventListener(document, "mousemove", params.onMouseMove);
+ params.onMouseMove = null;
+ }
+ if (params.onMouseUp) {
+ util.removeEventListener(document, "mouseup", params.onMouseUp);
+ params.onMouseUp = null;
+ }
+ //util.preventDefault(event);
+
+ if (params.moved) {
+ // fire a rangechanged event
+ this._trigger('rangechanged');
+ }
+};
+
+/**
+ * Event handler for mouse wheel event, used to zoom
+ * Code from http://adomas.org/javascript-mouse-wheel/
+ * @param {Event} event
+ * @param {Object} listener
+ * @private
+ */
+Range.prototype._onMouseWheel = function(event, listener) {
+ event = event || window.event;
+
+ // 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;
+ }
+
+ // 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) {
+ var me = this;
+ var zoom = function () {
+ // perform the zoom action. Delta is normally 1 or -1
+ var zoomFactor = delta / 5.0;
+ var zoomAround = null;
+ var frame = listener.component.frame;
+ if (frame) {
+ var size, conversion;
+ if (listener.direction == 'horizontal') {
+ size = listener.component.width;
+ conversion = me.conversion(size);
+ var frameLeft = util.getAbsoluteLeft(frame);
+ var mouseX = util.getPageX(event);
+ zoomAround = (mouseX - frameLeft) / conversion.factor + conversion.offset;
+ }
+ else {
+ size = listener.component.height;
+ conversion = me.conversion(size);
+ var frameTop = util.getAbsoluteTop(frame);
+ var mouseY = util.getPageY(event);
+ zoomAround = ((frameTop + size - mouseY) - frameTop) / conversion.factor + conversion.offset;
+ }
+ }
+
+ me.zoom(zoomFactor, zoomAround);
+ };
+
+ zoom();
+ }
+
+ // Prevent default actions caused by mouse wheel.
+ // That might be ugly, but we handle scrolls somehow
+ // anyway, so don't bother here...
+ util.preventDefault(event);
+};
+
+
+/**
+ * Zoom the range the given zoomfactor 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 zoomfactor = 0.1 or -0.1
+ * @param {Number} zoomFactor Zooming amount. Positive value will zoom in,
+ * negative value will zoom out
+ * @param {Number} zoomAround Value around which will be zoomed. Optional
+ */
+Range.prototype.zoom = function(zoomFactor, zoomAround) {
+ // if zoomAroundDate is not provided, take it half between start Date and end Date
+ if (zoomAround == null) {
+ zoomAround = (this.start + this.end) / 2;
+ }
+
+ // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will
+ // result in a start>=end )
+ if (zoomFactor >= 1) {
+ zoomFactor = 0.9;
+ }
+ if (zoomFactor <= -1) {
+ zoomFactor = -0.9;
+ }
+
+ // adjust a negative factor such that zooming in with 0.1 equals zooming
+ // out with a factor -0.1
+ if (zoomFactor < 0) {
+ zoomFactor = zoomFactor / (1 + zoomFactor);
+ }
+
+ // zoom start and end relative to the zoomAround value
+ var startDiff = (this.start - zoomAround);
+ var endDiff = (this.end - zoomAround);
+
+ // calculate new start and end
+ var newStart = this.start - startDiff * zoomFactor;
+ var newEnd = this.end - endDiff * zoomFactor;
+
+ this.setRange(newStart, newEnd);
+};
+
+/**
+ * Move the range with a given factor to the left or right. Start and end
+ * value will be adjusted. For example, try moveFactor = 0.1 or -0.1
+ * @param {Number} moveFactor Moving amount. Positive value will move right,
+ * negative value will move left
+ */
+Range.prototype.move = function(moveFactor) {
+ // zoom start Date and end Date relative to the zoomAroundDate
+ var diff = (this.end - this.start);
+
+ // apply new values
+ var newStart = this.start + diff * moveFactor;
+ var newEnd = this.end + diff * moveFactor;
+
+ // TODO: reckon with min and max range
+
+ this.start = newStart;
+ this.end = newEnd;
+};
+
+/**
+ * @constructor Controller
+ *
+ * A Controller controls the reflows and repaints of all visual components
+ */
+function Controller () {
+ this.id = util.randomUUID();
+ this.components = {};
+
+ this.repaintTimer = undefined;
+ this.reflowTimer = undefined;
+}
+
+/**
+ * Add a component to the controller
+ * @param {Component | Controller} component
+ */
+Controller.prototype.add = function (component) {
+ // validate the component
+ if (component.id == undefined) {
+ throw new Error('Component has no field id');
+ }
+ if (!(component instanceof Component) && !(component instanceof Controller)) {
+ throw new TypeError('Component must be an instance of ' +
+ 'prototype Component or Controller');
+ }
+
+ // add the component
+ component.controller = this;
+ this.components[component.id] = component;
+};
+
+/**
+ * Request a reflow. The controller will schedule a reflow
+ */
+Controller.prototype.requestReflow = function () {
+ if (!this.reflowTimer) {
+ var me = this;
+ this.reflowTimer = setTimeout(function () {
+ me.reflowTimer = undefined;
+ me.reflow();
+ }, 0);
+ }
+};
+
+/**
+ * Request a repaint. The controller will schedule a repaint
+ */
+Controller.prototype.requestRepaint = function () {
+ if (!this.repaintTimer) {
+ var me = this;
+ this.repaintTimer = setTimeout(function () {
+ me.repaintTimer = undefined;
+ me.repaint();
+ }, 0);
+ }
+};
+
+/**
+ * Repaint all components
+ */
+Controller.prototype.repaint = function () {
+ var changed = false;
+
+ // cancel any running repaint request
+ if (this.repaintTimer) {
+ clearTimeout(this.repaintTimer);
+ this.repaintTimer = undefined;
+ }
+
+ var done = {};
+
+ function repaint(component, id) {
+ if (!(id in done)) {
+ // first repaint the components on which this component is dependent
+ if (component.depends) {
+ component.depends.forEach(function (dep) {
+ repaint(dep, dep.id);
+ });
+ }
+ if (component.parent) {
+ repaint(component.parent, component.parent.id);
+ }
+
+ // repaint the component itself and mark as done
+ changed = component.repaint() || changed;
+ done[id] = true;
+ }
+ }
+
+ util.forEach(this.components, repaint);
+
+ // immediately reflow when needed
+ if (changed) {
+ this.reflow();
+ }
+ // TODO: limit the number of nested reflows/repaints, prevent loop
+};
+
+/**
+ * Reflow all components
+ */
+Controller.prototype.reflow = function () {
+ var resized = false;
+
+ // cancel any running repaint request
+ if (this.reflowTimer) {
+ clearTimeout(this.reflowTimer);
+ this.reflowTimer = undefined;
+ }
+
+ var done = {};
+
+ function reflow(component, id) {
+ if (!(id in done)) {
+ // first reflow the components on which this component is dependent
+ if (component.depends) {
+ component.depends.forEach(function (dep) {
+ reflow(dep, dep.id);
+ });
+ }
+ if (component.parent) {
+ reflow(component.parent, component.parent.id);
+ }
+
+ // reflow the component itself and mark as done
+ resized = component.reflow() || resized;
+ done[id] = true;
+ }
+ }
+
+ util.forEach(this.components, reflow);
+
+ // immediately repaint when needed
+ if (resized) {
+ this.repaint();
+ }
+ // TODO: limit the number of nested reflows/repaints, prevent loop
+};
+
+/**
+ * Prototype for visual components
+ */
+function Component () {
+ this.id = null;
+ this.parent = null;
+ this.depends = null;
+ this.controller = null;
+ this.options = null;
+
+ this.frame = null; // main DOM element
+ this.top = 0;
+ this.left = 0;
+ this.width = 0;
+ this.height = 0;
+}
+
+/**
+ * Set parameters for the frame. Parameters will be merged in current parameter
+ * set.
+ * @param {Object} options Available parameters:
+ * {String | function} [className]
+ * {String | Number | function} [left]
+ * {String | Number | function} [top]
+ * {String | Number | function} [width]
+ * {String | Number | function} [height]
+ */
+Component.prototype.setOptions = function(options) {
+ if (options) {
+ util.extend(this.options, options);
+ }
+
+ if (this.controller) {
+ this.requestRepaint();
+ this.requestReflow();
+ }
+};
+
+/**
+ * Get the container element of the component, which can be used by a child to
+ * add its own widgets. Not all components do have a container for childs, in
+ * that case null is returned.
+ * @returns {HTMLElement | null} container
+ */
+Component.prototype.getContainer = function () {
+ // should be implemented by the component
+ return null;
+};
+
+/**
+ * Get the frame element of the component, the outer HTML DOM element.
+ * @returns {HTMLElement | null} frame
+ */
+Component.prototype.getFrame = function () {
+ return this.frame;
+};
+
+/**
+ * Repaint the component
+ * @return {Boolean} changed
+ */
+Component.prototype.repaint = function () {
+ // should be implemented by the component
+ return false;
+};
+
+/**
+ * Reflow the component
+ * @return {Boolean} resized
+ */
+Component.prototype.reflow = function () {
+ // should be implemented by the component
+ return false;
+};
+
+/**
+ * Request a repaint. The controller will schedule a repaint
+ */
+Component.prototype.requestRepaint = function () {
+ if (this.controller) {
+ this.controller.requestRepaint();
+ }
+ else {
+ throw new Error('Cannot request a repaint: no controller configured');
+ // TODO: just do a repaint when no parent is configured?
+ }
+};
+
+/**
+ * Request a reflow. The controller will schedule a reflow
+ */
+Component.prototype.requestReflow = function () {
+ if (this.controller) {
+ this.controller.requestReflow();
+ }
+ else {
+ throw new Error('Cannot request a reflow: no controller configured');
+ // TODO: just do a reflow when no parent is configured?
+ }
+};
+
+/**
+ * Event handler
+ * @param {String} event name of the event, for example 'click', 'mousemove'
+ * @param {function} callback callback handler, invoked with the raw HTML Event
+ * as parameter.
+ */
+Component.prototype.on = function (event, callback) {
+ // TODO: rethink the way of event delegation
+ if (this.parent) {
+ this.parent.on(event, callback);
+ }
+ else {
+ throw new Error('Cannot attach event: no root panel found');
+ }
+};
+
+/**
+ * A panel can contain components
+ * @param {Component} [parent]
+ * @param {Component[]} [depends] Components on which this components depends
+ * (except for the parent)
+ * @param {Object} [options] Available parameters:
+ * {String | Number | function} [left]
+ * {String | Number | function} [top]
+ * {String | Number | function} [width]
+ * {String | Number | function} [height]
+ * {String | function} [className]
+ * @constructor Panel
+ * @extends Component
+ */
+function Panel(parent, depends, options) {
+ this.id = util.randomUUID();
+ this.parent = parent;
+ this.depends = depends;
+ this.options = {};
+
+ this.setOptions(options);
+}
+
+Panel.prototype = new Component();
+
+/**
+ * Get the container element of the panel, which can be used by a child to
+ * add its own widgets.
+ * @returns {HTMLElement} container
+ */
+Panel.prototype.getContainer = function () {
+ return this.frame;
+};
+
+/**
+ * Repaint the component
+ * @return {Boolean} changed
+ */
+Panel.prototype.repaint = function () {
+ var changed = 0,
+ update = util.updateProperty,
+ asSize = util.option.asSize,
+ options = this.options,
+ frame = this.frame;
+ if (!frame) {
+ frame = document.createElement('div');
+ frame.className = 'panel';
+
+ if (options.className) {
+ if (typeof options.className == 'function') {
+ util.addClassName(frame, String(options.className()));
+ }
+ else {
+ util.addClassName(frame, String(options.className));
+ }
+ }
+
+ this.frame = frame;
+ changed += 1;
+ }
+ if (!frame.parentNode) {
+ if (!this.parent) {
+ throw new Error('Cannot repaint panel: no parent attached');
+ }
+ var parentContainer = this.parent.getContainer();
+ if (!parentContainer) {
+ throw new Error('Cannot repaint panel: parent has no container element');
+ }
+ parentContainer.appendChild(frame);
+ changed += 1;
+ }
+
+ changed += update(frame.style, 'top', asSize(options.top, '0px'));
+ changed += update(frame.style, 'left', asSize(options.left, '0px'));
+ changed += update(frame.style, 'width', asSize(options.width, '100%'));
+ changed += update(frame.style, 'height', asSize(options.height, '100%'));
+
+ return (changed > 0);
+};
+
+/**
+ * Reflow the component
+ * @return {Boolean} resized
+ */
+Panel.prototype.reflow = function () {
+ var changed = 0,
+ update = util.updateProperty,
+ frame = this.frame;
+
+ if (frame) {
+ changed += update(this, 'top', frame.offsetTop);
+ changed += update(this, 'left', frame.offsetLeft);
+ changed += update(this, 'width', frame.offsetWidth);
+ changed += update(this, 'height', frame.offsetHeight);
+ }
+ else {
+ changed += 1;
+ }
+
+ return (changed > 0);
+};
+
+/**
+ * A root panel can hold components. The root panel must be initialized with
+ * a DOM element as container.
+ * @param {HTMLElement} container
+ * @param {Object} [options] Available parameters: see RootPanel.setOptions.
+ * @constructor RootPanel
+ * @extends Panel
+ */
+function RootPanel(container, options) {
+ this.id = util.randomUUID();
+ this.container = container;
+ this.options = {
+ autoResize: true
+ };
+
+ this.listeners = {}; // event listeners
+
+ this.setOptions(options);
+}
+
+RootPanel.prototype = new Panel();
+
+/**
+ * Set options. Will extend the current options.
+ * @param {Object} [options] Available parameters:
+ * {String | function} [className]
+ * {String | Number | function} [left]
+ * {String | Number | function} [top]
+ * {String | Number | function} [width]
+ * {String | Number | function} [height]
+ * {String | Number | function} [height]
+ * {Boolean | function} [autoResize]
+ */
+RootPanel.prototype.setOptions = function (options) {
+ util.extend(this.options, options);
+
+ if (this.options.autoResize) {
+ this._watch();
+ }
+ else {
+ this._unwatch();
+ }
+};
+
+/**
+ * Repaint the component
+ * @return {Boolean} changed
+ */
+RootPanel.prototype.repaint = function () {
+ var changed = 0,
+ update = util.updateProperty,
+ asSize = util.option.asSize,
+ options = this.options,
+ frame = this.frame;
+ if (!frame) {
+ frame = document.createElement('div');
+ frame.className = 'graph panel';
+
+ if (options.className) {
+ util.addClassName(frame, util.option.asString(options.className));
+ }
+
+ this.frame = frame;
+ changed += 1;
+ }
+ if (!frame.parentNode) {
+ if (!this.container) {
+ throw new Error('Cannot repaint root panel: no container attached');
+ }
+ this.container.appendChild(frame);
+ changed += 1;
+ }
+
+ changed += update(frame.style, 'top', asSize(options.top, '0px'));
+ changed += update(frame.style, 'left', asSize(options.left, '0px'));
+ changed += update(frame.style, 'width', asSize(options.width, '100%'));
+ changed += update(frame.style, 'height', asSize(options.height, '100%'));
+
+ this._updateEventEmitters();
+
+ return (changed > 0);
+};
+
+/**
+ * Reflow the component
+ * @return {Boolean} resized
+ */
+RootPanel.prototype.reflow = function () {
+ var changed = 0,
+ update = util.updateProperty,
+ frame = this.frame;
+
+ if (frame) {
+ changed += update(this, 'top', frame.offsetTop);
+ changed += update(this, 'left', frame.offsetLeft);
+ changed += update(this, 'width', frame.offsetWidth);
+ changed += update(this, 'height', frame.offsetHeight);
+ }
+ else {
+ changed += 1;
+ }
+
+ return (changed > 0);
+};
+
+/**
+ * Watch for changes in the size of the frame. On resize, the Panel will
+ * automatically redraw itself.
+ * @private
+ */
+RootPanel.prototype._watch = function () {
+ var me = this;
+
+ this._unwatch();
+
+ var checkSize = function () {
+ if (!me.options.autoResize) {
+ // stop watching when the option autoResize is changed to false
+ me._unwatch();
+ return;
+ }
+
+ if (me.frame) {
+ // check whether the frame is resized
+ if ((me.frame.clientWidth != me.width) ||
+ (me.frame.clientHeight != me.height)) {
+ me.requestReflow();
+ }
+ }
+ };
+
+ // TODO: automatically cleanup the event listener when the frame is deleted
+ util.addEventListener(window, 'resize', checkSize);
+
+ this.watchTimer = setInterval(checkSize, 1000);
+};
+
+/**
+ * Stop watching for a resize of the frame.
+ * @private
+ */
+RootPanel.prototype._unwatch = function () {
+ if (this.watchTimer) {
+ clearInterval(this.watchTimer);
+ this.watchTimer = undefined;
+ }
+
+ // TODO: remove event listener on window.resize
+};
+
+/**
+ * Event handler
+ * @param {String} event name of the event, for example 'click', 'mousemove'
+ * @param {function} callback callback handler, invoked with the raw HTML Event
+ * as parameter.
+ */
+RootPanel.prototype.on = function (event, callback) {
+ // register the listener at this component
+ var arr = this.listeners[event];
+ if (!arr) {
+ arr = [];
+ this.listeners[event] = arr;
+ }
+ arr.push(callback);
+
+ this._updateEventEmitters();
+};
+
+/**
+ * Update the event listeners for all event emitters
+ * @private
+ */
+RootPanel.prototype._updateEventEmitters = function () {
+ if (this.listeners) {
+ var me = this;
+ util.forEach(this.listeners, function (listeners, event) {
+ if (!me.emitters) {
+ me.emitters = {};
+ }
+ if (!(event in me.emitters)) {
+ // create event
+ var frame = me.frame;
+ if (frame) {
+ //console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging
+ var callback = function(event) {
+ listeners.forEach(function (listener) {
+ // TODO: filter on event target!
+ listener(event);
+ });
+ };
+ me.emitters[event] = callback;
+ util.addEventListener(frame, event, callback);
+ }
+ }
+ });
+
+ // TODO: be able to delete event listeners
+ // TODO: be able to move event listeners to a parent when available
+ }
+};
+/**
+ * A horizontal time axis
+ * @param {Component} parent
+ * @param {Component[]} [depends] Components on which this components depends
+ * (except for the parent)
+ * @param {Object} [options] See TimeAxis.setOptions for the available
+ * options.
+ * @constructor TimeAxis
+ * @extends Component
+ */
+function TimeAxis (parent, depends, options) {
+ this.id = util.randomUUID();
+ this.parent = parent;
+ this.depends = depends;
+
+ this.dom = {
+ majorLines: [],
+ majorTexts: [],
+ minorLines: [],
+ minorTexts: [],
+ redundant: {
+ majorLines: [],
+ majorTexts: [],
+ minorLines: [],
+ minorTexts: []
+ }
+ };
+ this.props = {
+ range: {
+ start: 0,
+ end: 0,
+ minimumStep: 0
+ }
+ };
+
+ this.options = {
+ orientation: 'bottom', // supported: 'top', 'bottom'
+ // TODO: implement timeaxis orientations 'left' and 'right'
+ showMinorLabels: true,
+ showMajorLabels: true
+ };
+
+ this.conversion = null;
+ this.range = null;
+
+ this.setOptions(options);
+}
+
+TimeAxis.prototype = new Component();
+
+// TODO: comment options
+TimeAxis.prototype.setOptions = function (options) {
+ util.extend(this.options, options);
+};
+
+/**
+ * Set a range (start and end)
+ * @param {Range | Object} range A Range or an object containing start and end.
+ */
+TimeAxis.prototype.setRange = function (range) {
+ if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
+ throw new TypeError('Range must be an instance of Range, ' +
+ 'or an object containing start and end.');
+ }
+ this.range = range;
+};
+
+/**
+ * Convert a position on screen (pixels) to a datetime
+ * @param {int} x Position on the screen in pixels
+ * @return {Date} time The datetime the corresponds with given position x
+ */
+TimeAxis.prototype.toTime = function(x) {
+ var conversion = this.conversion;
+ return new Date(x / conversion.factor + 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
+ */
+TimeAxis.prototype.toScreen = function(time) {
+ var conversion = this.conversion;
+ return (time.valueOf() - conversion.offset) * conversion.factor;
+};
+
+/**
+ * Repaint the component
+ * @return {Boolean} changed
+ */
+TimeAxis.prototype.repaint = function () {
+ var changed = 0,
+ update = util.updateProperty,
+ asSize = util.option.asSize,
+ options = this.options,
+ props = this.props,
+ step = this.step;
+
+ var frame = this.frame;
+ if (!frame) {
+ frame = document.createElement('div');
+ this.frame = frame;
+ changed += 1;
+ }
+ frame.className = 'axis ' + options.orientation;
+ // TODO: custom className?
+
+ if (!frame.parentNode) {
+ if (!this.parent) {
+ throw new Error('Cannot repaint time axis: no parent attached');
+ }
+ var parentContainer = this.parent.getContainer();
+ if (!parentContainer) {
+ throw new Error('Cannot repaint time axis: parent has no container element');
+ }
+ parentContainer.appendChild(frame);
+
+ changed += 1;
+ }
+
+ var parent = frame.parentNode;
+ if (parent) {
+ var beforeChild = frame.nextSibling;
+ parent.removeChild(frame); // take frame offline while updating (is almost twice as fast)
+
+ var orientation = options.orientation;
+ var defaultTop = (orientation == 'bottom') ? (this.props.parentHeight - this.height) + 'px' : '0px';
+ changed += update(frame.style, 'top', asSize(options.top, defaultTop));
+ changed += update(frame.style, 'left', asSize(options.left, '0px'));
+ changed += update(frame.style, 'width', asSize(options.width, '100%'));
+ changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
+
+ // get characters width and height
+ this._repaintMeasureChars();
+
+ if (this.step) {
+ this._repaintStart();
+
+ step.first();
+ var xFirstMajorLabel = undefined;
+ var max = 0;
+ while (step.hasNext() && max < 1000) {
+ max++;
+ var cur = step.getCurrent(),
+ x = this.toScreen(cur),
+ isMajor = step.isMajor();
+
+ // TODO: lines must have a width, such that we can create css backgrounds
+
+ if (options.showMinorLabels) {
+ this._repaintMinorText(x, step.getLabelMinor());
+ }
+
+ if (isMajor && options.showMajorLabels) {
+ if (x > 0) {
+ if (xFirstMajorLabel == undefined) {
+ xFirstMajorLabel = x;
+ }
+ this._repaintMajorText(x, step.getLabelMajor());
+ }
+ this._repaintMajorLine(x);
+ }
+ else {
+ this._repaintMinorLine(x);
+ }
+
+ step.next();
+ }
+
+ // create a major label on the left when needed
+ if (options.showMajorLabels) {
+ var leftTime = this.toTime(0),
+ leftText = step.getLabelMajor(leftTime),
+ widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation
+
+ if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
+ this._repaintMajorText(0, leftText);
+ }
+ }
+
+ this._repaintEnd();
+ }
+
+ this._repaintLine();
+
+ // put frame online again
+ if (beforeChild) {
+ parent.insertBefore(frame, beforeChild);
+ }
+ else {
+ parent.appendChild(frame)
+ }
+ }
+
+ return (changed > 0);
+};
+
+/**
+ * Start a repaint. Move all DOM elements to a redundant list, where they
+ * can be picked for re-use, or can be cleaned up in the end
+ * @private
+ */
+TimeAxis.prototype._repaintStart = function () {
+ var dom = this.dom,
+ redundant = dom.redundant;
+
+ redundant.majorLines = dom.majorLines;
+ redundant.majorTexts = dom.majorTexts;
+ redundant.minorLines = dom.minorLines;
+ redundant.minorTexts = dom.minorTexts;
+
+ dom.majorLines = [];
+ dom.majorTexts = [];
+ dom.minorLines = [];
+ dom.minorTexts = [];
+};
+
+/**
+ * End a repaint. Cleanup leftover DOM elements in the redundant list
+ * @private
+ */
+TimeAxis.prototype._repaintEnd = function () {
+ util.forEach(this.dom.redundant, function (arr) {
+ while (arr.length) {
+ var elem = arr.pop();
+ if (elem && elem.parentNode) {
+ elem.parentNode.removeChild(elem);
+ }
+ }
+ });
+};
+
+
+/**
+ * Create a minor label for the axis at position x
+ * @param {Number} x
+ * @param {String} text
+ * @private
+ */
+TimeAxis.prototype._repaintMinorText = function (x, text) {
+ // 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.frame.appendChild(label);
+ }
+ this.dom.minorTexts.push(label);
+
+ label.childNodes[0].nodeValue = text;
+ label.style.left = x + 'px';
+ label.style.top = this.props.minorLabelTop + 'px';
+ //label.title = title; // TODO: this is a heavy operation
+};
+
+/**
+ * Create a Major label for the axis at position x
+ * @param {Number} x
+ * @param {String} text
+ * @private
+ */
+TimeAxis.prototype._repaintMajorText = function (x, text) {
+ // 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.frame.appendChild(label);
+ }
+ this.dom.majorTexts.push(label);
+
+ label.childNodes[0].nodeValue = text;
+ label.style.top = this.props.majorLabelTop + 'px';
+ label.style.left = x + 'px';
+ //label.title = title; // TODO: this is a heavy operation
+};
+
+/**
+ * Create a minor line for the axis at position x
+ * @param {Number} x
+ * @private
+ */
+TimeAxis.prototype._repaintMinorLine = function (x) {
+ // reuse redundant line
+ var line = this.dom.redundant.minorLines.shift();
+
+ if (!line) {
+ // create vertical line
+ line = document.createElement('div');
+ line.className = 'grid vertical minor';
+ this.frame.appendChild(line);
+ }
+ this.dom.minorLines.push(line);
+
+ var props = this.props;
+ line.style.top = props.minorLineTop + 'px';
+ line.style.height = props.minorLineHeight + 'px';
+ line.style.left = (x - props.minorLineWidth / 2) + 'px';
+};
+
+/**
+ * Create a Major line for the axis at position x
+ * @param {Number} x
+ * @private
+ */
+TimeAxis.prototype._repaintMajorLine = function (x) {
+ // reuse redundant line
+ var line = this.dom.redundant.majorLines.shift();
+
+ if (!line) {
+ // create vertical line
+ line = document.createElement('DIV');
+ line.className = 'grid vertical major';
+ this.frame.appendChild(line);
+ }
+ this.dom.majorLines.push(line);
+
+ var props = this.props;
+ line.style.top = props.majorLineTop + 'px';
+ line.style.left = (x - props.majorLineWidth / 2) + 'px';
+ line.style.height = props.majorLineHeight + 'px';
+};
+
+
+/**
+ * Repaint the horizontal line for the axis
+ * @private
+ */
+TimeAxis.prototype._repaintLine = function() {
+ var line = this.dom.line,
+ frame = this.frame,
+ options = this.options;
+
+ // line before all axis elements
+ if (options.showMinorLabels || options.showMajorLabels) {
+ if (line) {
+ // put this line at the end of all childs
+ frame.removeChild(line);
+ frame.appendChild(line);
+ }
+ else {
+ // create the axis line
+ line = document.createElement('div');
+ line.className = 'grid horizontal major';
+ frame.appendChild(line);
+ this.dom.line = line;
+ }
+
+ line.style.top = this.props.lineTop + 'px';
+ }
+ else {
+ if (line && axis.parentElement) {
+ frame.removeChild(axis.line);
+ delete this.dom.line;
+ }
+ }
+};
+
+/**
+ * Create characters used to determine the size of text on the axis
+ * @private
+ */
+TimeAxis.prototype._repaintMeasureChars = function () {
+ // calculate the width and height of a single character
+ // this is used to calculate the step size, and also the positioning of the
+ // axis
+ var dom = this.dom,
+ text;
+
+ if (!dom.characterMinor) {
+ text = document.createTextNode('0');
+ var measureCharMinor = document.createElement('DIV');
+ measureCharMinor.className = 'text minor measure';
+ measureCharMinor.appendChild(text);
+ this.frame.appendChild(measureCharMinor);
+
+ dom.measureCharMinor = measureCharMinor;
+ }
+
+ if (!dom.characterMajor) {
+ text = document.createTextNode('0');
+ var measureCharMajor = document.createElement('DIV');
+ measureCharMajor.className = 'text major measure';
+ measureCharMajor.appendChild(text);
+ this.frame.appendChild(measureCharMajor);
+
+ dom.measureCharMajor = measureCharMajor;
+ }
+};
+
+/**
+ * Reflow the component
+ * @return {Boolean} resized
+ */
+TimeAxis.prototype.reflow = function () {
+ var changed = 0,
+ update = util.updateProperty,
+ frame = this.frame,
+ range = this.range;
+
+ if (!range) {
+ throw new Error('Cannot repaint time axis: no range configured');
+ }
+
+ if (frame) {
+ changed += update(this, 'top', frame.offsetTop);
+ changed += update(this, 'left', frame.offsetLeft);
+
+ // calculate size of a character
+ var props = this.props,
+ showMinorLabels = this.options.showMinorLabels,
+ showMajorLabels = this.options.showMajorLabels,
+ measureCharMinor = this.dom.measureCharMinor,
+ measureCharMajor = this.dom.measureCharMajor;
+ if (measureCharMinor) {
+ props.minorCharHeight = measureCharMinor.clientHeight;
+ props.minorCharWidth = measureCharMinor.clientWidth;
+ }
+ if (measureCharMajor) {
+ props.majorCharHeight = measureCharMajor.clientHeight;
+ props.majorCharWidth = measureCharMajor.clientWidth;
+ }
+
+ var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0;
+ if (parentHeight != props.parentHeight) {
+ props.parentHeight = parentHeight;
+ changed += 1;
+ }
+ switch (this.options.orientation) {
+ case 'bottom':
+ props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
+ props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
+
+ props.minorLabelTop = 0;
+ props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight;
+
+ props.minorLineTop = -this.top;
+ props.minorLineHeight = this.top + props.majorLabelHeight;
+ props.minorLineWidth = 1; // TODO: really calculate width
+
+ props.majorLineTop = -this.top;
+ props.majorLineHeight = this.top + props.minorLabelHeight + props.majorLabelHeight;
+ props.majorLineWidth = 1; // TODO: really calculate width
+
+ props.lineTop = 0;
+
+ break;
+
+ case 'top':
+ props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
+ props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
+
+ props.majorLabelTop = 0;
+ props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight;
+
+ props.minorLineTop = props.minorLabelTop;
+ props.minorLineHeight = parentHeight - props.majorLabelHeight - this.top;
+ props.minorLineWidth = 1; // TODO: really calculate width
+
+ props.majorLineTop = 0;
+ props.majorLineHeight = parentHeight - this.top;
+ props.majorLineWidth = 1; // TODO: really calculate width
+
+ props.lineTop = props.majorLabelHeight + props.minorLabelHeight;
+
+ break;
+
+ default:
+ throw new Error('Unkown orientation "' + this.options.orientation + '"');
+ }
+
+ var height = props.minorLabelHeight + props.majorLabelHeight;
+ changed += update(this, 'width', frame.offsetWidth);
+ changed += update(this, 'height', height);
+
+ // calculate range and step
+ this._updateConversion();
+
+ var start = util.cast(range.start, 'Date'),
+ end = util.cast(range.end, 'Date'),
+ minimumStep = this.toTime((props.minorCharWidth || 10) * 5) - this.toTime(0);
+ this.step = new TimeStep(start, end, minimumStep);
+ changed += update(props.range, 'start', start.valueOf());
+ changed += update(props.range, 'end', end.valueOf());
+ changed += update(props.range, 'minimumStep', minimumStep.valueOf());
+ }
+
+ return (changed > 0);
+};
+
+/**
+ * Calculate the factor and offset to convert a position on screen to the
+ * corresponding date and vice versa.
+ * After the method _updateConversion is executed once, the methods toTime
+ * and toScreen can be used.
+ * @private
+ */
+TimeAxis.prototype._updateConversion = function() {
+ var range = this.range;
+ if (!range) {
+ throw new Error('No range configured');
+ }
+
+ if (range.conversion) {
+ this.conversion = range.conversion(this.width);
+ }
+ else {
+ this.conversion = Range.conversion(range.start, range.end, this.width);
+ }
+};
+
+/**
+ * An ItemSet holds a set of items and ranges which can be displayed in a
+ * range. The width is determined by the parent of the ItemSet, and the height
+ * is determined by the size of the items.
+ * @param {Component} parent
+ * @param {Component[]} [depends] Components on which this components depends
+ * (except for the parent)
+ * @param {Object} [options] See ItemSet.setOptions for the available
+ * options.
+ * @constructor ItemSet
+ * @extends Panel
+ */
+function ItemSet(parent, depends, options) {
+ this.id = util.randomUUID();
+ this.parent = parent;
+ this.depends = depends;
+
+ // one options object is shared by this itemset and all its items
+ this.options = {
+ style: 'box',
+ align: 'center',
+ orientation: 'bottom',
+ margin: {
+ axis: 20,
+ item: 10
+ },
+ padding: 5
+ };
+
+ var me = this;
+ this.data = null; // DataSet
+ this.range = null; // Range or Object {start: number, end: number}
+ this.listeners = {
+ 'add': function (event, params) {
+ me._onAdd(params.items);
+ },
+ 'update': function (event, params) {
+ me._onUpdate(params.items);
+ },
+ 'remove': function (event, params) {
+ me._onRemove(params.items);
+ }
+ };
+
+ this.items = {};
+ this.queue = {}; // queue with items to be added/updated/removed
+ this.stack = new Stack(this);
+ this.conversion = null;
+
+ this.setOptions(options);
+}
+
+ItemSet.prototype = new Panel();
+
+/**
+ * Set options for the ItemSet. Existing options will be extended/overwritten.
+ * @param {Object} [options] The following options are available:
+ * {String | function} [className]
+ * class name for the itemset
+ * {String} [style]
+ * Default style 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).
+ * {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} padding
+ * Padding of the contents of an item in pixels.
+ * Must correspond with the items css. Default is 5.
+ */
+ItemSet.prototype.setOptions = function (options) {
+ util.extend(this.options, options);
+
+ // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis
+
+ this.stack.setOptions(this.options);
+};
+
+/**
+ * Set range (start and end).
+ * @param {Range | Object} range A Range or an object containing start and end.
+ */
+ItemSet.prototype.setRange = function (range) {
+ if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
+ throw new TypeError('Range must be an instance of Range, ' +
+ 'or an object containing start and end.');
+ }
+ this.range = range;
+};
+
+/**
+ * Repaint the component
+ * @return {Boolean} changed
+ */
+ItemSet.prototype.repaint = function () {
+ var changed = 0,
+ update = util.updateProperty,
+ asSize = util.option.asSize,
+ options = this.options,
+ frame = this.frame;
+
+ if (!frame) {
+ frame = document.createElement('div');
+ frame.className = 'itemset';
+
+ if (options.className) {
+ util.addClassName(frame, util.option.asString(options.className));
+ }
+
+ this.frame = frame;
+ changed += 1;
+ }
+ if (!frame.parentNode) {
+ if (!this.parent) {
+ throw new Error('Cannot repaint itemset: no parent attached');
+ }
+ var parentContainer = this.parent.getContainer();
+ if (!parentContainer) {
+ throw new Error('Cannot repaint itemset: parent has no container element');
+ }
+ parentContainer.appendChild(frame);
+ changed += 1;
+ }
+
+ changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
+ changed += update(frame.style, 'top', asSize(options.top, '0px'));
+ changed += update(frame.style, 'left', asSize(options.left, '0px'));
+ changed += update(frame.style, 'width', asSize(options.width, '100%'));
+
+ this._updateConversion();
+
+ var me = this,
+ queue = this.queue,
+ data = this.data,
+ items = this.items,
+ dataOptions = {
+ fields: ['id', 'start', 'end', 'content', 'type']
+ };
+ // TODO: copy options from the itemset itself?
+ // TODO: make orientation dynamically changable for the items
+
+ // show/hide added/changed/removed items
+ Object.keys(queue).forEach(function (id) {
+ var entry = queue[id];
+ var item = entry.item;
+ //noinspection FallthroughInSwitchStatementJS
+ switch (entry.action) {
+ case 'add':
+ case 'update':
+ var itemData = data.get(id, dataOptions);
+ var type = itemData.type ||
+ (itemData.start && itemData.end && 'range') ||
+ 'box';
+ var constructor = itemTypes[type];
+
+ // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
+ if (item) {
+ // update item
+ if (!constructor || !(item instanceof constructor)) {
+ // item type has changed, delete the item
+ item.visible = false;
+ changed += item.repaint();
+ item = null;
+ }
+ else {
+ item.data = itemData; // TODO: create a method item.setData ?
+ changed += item.repaint();
+ }
+ }
+
+ if (!item) {
+ // create item
+ if (constructor) {
+ item = new constructor(me, itemData, options);
+ changed += item.repaint();
+ }
+ else {
+ throw new TypeError('Unknown item type "' + type + '"');
+ }
+ }
+
+ // update lists
+ items[id] = item;
+ delete queue[id];
+ break;
+
+ case 'remove':
+ if (item) {
+ // TODO: remove dom of the item
+ item.visible = false;
+ changed += item.repaint();
+ }
+
+ // update lists
+ delete items[id];
+ delete queue[id];
+ break;
+
+ default:
+ console.log('Error: unknown action "' + entry.action + '"');
+ }
+ });
+
+ // reposition all items
+ util.forEach(this.items, function (item) {
+ item.reposition();
+ });
+
+ return (changed > 0);
+};
+
+/**
+ * Reflow the component
+ * @return {Boolean} resized
+ */
+ItemSet.prototype.reflow = function () {
+ var changed = 0,
+ options = this.options,
+ update = util.updateProperty,
+ frame = this.frame;
+
+ if (frame) {
+ this._updateConversion();
+
+ util.forEach(this.items, function (item) {
+ changed += item.reflow();
+ });
+
+ // TODO: stack.update should be triggered via an event, in stack itself
+ // TODO: only update the stack when there are changed items
+ this.stack.update();
+
+ if (options.height != null) {
+ changed += update(this, 'height', frame.offsetHeight);
+ }
+ else {
+ // height is not specified, determine the height from the height and positioned items
+ var frameHeight = this.height;
+ var maxHeight = 0;
+ if (options.orientation == 'top') {
+ util.forEach(this.items, function (item) {
+ maxHeight = Math.max(maxHeight, item.top + item.height);
+ });
+ }
+ else {
+ // orientation == 'bottom'
+ util.forEach(this.items, function (item) {
+ maxHeight = Math.max(maxHeight, frameHeight - item.top);
+ });
+ }
+
+ changed += update(this, 'height', maxHeight + options.margin.axis);
+ }
+
+ // calculate height from items
+ changed += update(this, 'top', frame.offsetTop);
+ changed += update(this, 'left', frame.offsetLeft);
+ changed += update(this, 'width', frame.offsetWidth);
+ }
+ else {
+ changed += 1;
+ }
+
+ return (changed > 0);
+};
+
+/**
+ * Set data
+ * @param {DataSet | Array | DataTable} data
+ */
+ItemSet.prototype.setData = function(data) {
+ // unsubscribe from current dataset
+ var current = this.data;
+ if (current) {
+ util.forEach(this.listeners, function (callback, event) {
+ current.unsubscribe(event, callback);
+ });
+ }
+
+ if (data instanceof DataSet) {
+ this.data = data;
+ }
+ else {
+ this.data = new DataSet({
+ fieldTypes: {
+ start: 'Date',
+ end: 'Date'
+ }
+ });
+ this.data.add(data);
+ }
+
+ var id = this.id;
+ var me = this;
+ util.forEach(this.listeners, function (callback, event) {
+ me.data.subscribe(event, callback, id);
+ });
+
+ var dataItems = this.data.get({filter: ['id']});
+ var ids = [];
+ util.forEach(dataItems, function (dataItem, index) {
+ ids[index] = dataItem.id;
+ });
+ this._onAdd(ids);
+};
+
+/**
+ * Handle updated items
+ * @param {Number[]} ids
+ * @private
+ */
+ItemSet.prototype._onUpdate = function(ids) {
+ this._toQueue(ids, 'update');
+};
+
+/**
+ * Handle changed items
+ * @param {Number[]} ids
+ * @private
+ */
+ItemSet.prototype._onAdd = function(ids) {
+ this._toQueue(ids, 'add');
+};
+
+/**
+ * Handle removed items
+ * @param {Number[]} ids
+ * @private
+ */
+ItemSet.prototype._onRemove = function(ids) {
+ this._toQueue(ids, 'remove');
+};
+
+/**
+ * Put items in the queue to be added/updated/remove
+ * @param {Number[]} ids
+ * @param {String} action can be 'add', 'update', 'remove'
+ */
+ItemSet.prototype._toQueue = function (ids, action) {
+ var items = this.items;
+ var queue = this.queue;
+ ids.forEach(function (id) {
+ var entry = queue[id];
+ if (entry) {
+ // already queued, update the action of the entry
+ entry.action = action;
+ }
+ else {
+ // not yet queued, add an entry to the queue
+ queue[id] = {
+ item: items[id] || null,
+ action: action
+ };
+ }
+ });
+
+ if (this.controller) {
+ //this.requestReflow();
+ this.requestRepaint();
+ }
+};
+
+/**
+ * Calculate the factor and offset to convert a position on screen to the
+ * corresponding date and vice versa.
+ * After the method _updateConversion is executed once, the methods toTime
+ * and toScreen can be used.
+ * @private
+ */
+ItemSet.prototype._updateConversion = function() {
+ var range = this.range;
+ if (!range) {
+ throw new Error('No range configured');
+ }
+
+ if (range.conversion) {
+ this.conversion = range.conversion(this.width);
+ }
+ else {
+ this.conversion = Range.conversion(range.start, range.end, this.width);
+ }
+};
+
+/**
+ * Convert a position on screen (pixels) to a datetime
+ * Before this method can be used, the method _updateConversion must be
+ * executed once.
+ * @param {int} x Position on the screen in pixels
+ * @return {Date} time The datetime the corresponds with given position x
+ */
+ItemSet.prototype.toTime = function(x) {
+ var conversion = this.conversion;
+ return new Date(x / conversion.factor + conversion.offset);
+};
+
+/**
+ * Convert a datetime (Date object) into a position on the screen
+ * Before this method can be used, the method _updateConversion must be
+ * executed once.
+ * @param {Date} time A date
+ * @return {int} x The position on the screen in pixels which corresponds
+ * with the given date.
+ */
+ItemSet.prototype.toScreen = function(time) {
+ var conversion = this.conversion;
+ return (time.valueOf() - conversion.offset) * conversion.factor;
+};
+
+
+/**
+ * @constructor Item
+ * @param {ItemSet} parent
+ * @param {Object} data Object containing (optional) parameters type,
+ * start, end, content, group, className.
+ * @param {Object} [options] Options to set initial property values
+ * // TODO: describe available options
+ */
+function Item (parent, data, options) {
+ this.parent = parent;
+ this.data = data;
+ this.selected = false;
+ this.visible = true;
+ this.dom = null;
+ this.options = options;
+}
+
+Item.prototype = new Component();
+
+/**
+ * Select current item
+ */
+Item.prototype.select = function () {
+ this.selected = true;
+};
+
+/**
+ * Unselect current item
+ */
+Item.prototype.unselect = function () {
+ this.selected = false;
+};
+
+// create a namespace for all item types
+var itemTypes = {};
+/**
+ * @constructor ItemBox
+ * @extends Item
+ * @param {ItemSet} parent
+ * @param {Object} data Object containing parameters start
+ * content, className.
+ * @param {Object} [options] Options to set initial property values
+ * // TODO: describe available options
+ */
+function ItemBox (parent, data, options) {
+ this.props = {
+ dot: {
+ left: 0,
+ top: 0,
+ width: 0,
+ height: 0
+ },
+ line: {
+ top: 0,
+ left: 0,
+ width: 0,
+ height: 0
+ }
+ };
+
+ Item.call(this, parent, data, options);
+}
+
+ItemBox.prototype = new Item (null, null);
+
+// register the ItemBox in the item types
+itemTypes['box'] = ItemBox;
+
+/**
+ * Select the item
+ * @override
+ */
+ItemBox.prototype.select = function () {
+ this.selected = true;
+ // TODO: select and unselect
+};
+
+/**
+ * Unselect the item
+ * @override
+ */
+ItemBox.prototype.unselect = function () {
+ this.selected = false;
+ // TODO: select and unselect
+};
+
+/**
+ * Repaint the item
+ * @return {Boolean} changed
+ */
+ItemBox.prototype.repaint = function () {
+ // TODO: make an efficient repaint
+ var changed = false;
+ var dom = this.dom;
+
+ if (this.visible) {
+ if (!dom) {
+ this._create();
+ changed = true;
+ }
+ dom = this.dom;
+
+ if (dom) {
+ if (!this.options && !this.parent) {
+ throw new Error('Cannot repaint item: no parent attached');
+ }
+ var parentContainer = this.parent.getContainer();
+ if (!parentContainer) {
+ throw new Error('Cannot repaint time axis: parent has no container element');
+ }
+
+ if (!dom.box.parentNode) {
+ parentContainer.appendChild(dom.box);
+ changed = true;
+ }
+ if (!dom.line.parentNode) {
+ parentContainer.appendChild(dom.line);
+ changed = true;
+ }
+ if (!dom.dot.parentNode) {
+ parentContainer.appendChild(dom.dot);
+ changed = true;
+ }
+
+ // update contents
+ if (this.data.content != this.content) {
+ this.content = this.data.content;
+ if (this.content instanceof Element) {
+ dom.content.innerHTML = '';
+ dom.content.appendChild(this.content);
+ }
+ else if (this.data.content != undefined) {
+ dom.content.innerHTML = this.content;
+ }
+ else {
+ throw new Error('Property "content" missing in item ' + this.data.id);
+ }
+ changed = true;
+ }
+
+ // update class
+ var className = (this.data.className? ' ' + this.data.className : '') +
+ (this.selected ? ' selected' : '');
+ if (this.className != className) {
+ this.className = className;
+ dom.box.className = 'item box' + className;
+ dom.line.className = 'item line' + className;
+ dom.dot.className = 'item dot' + className;
+ changed = true;
+ }
+ }
+ }
+ else {
+ // hide when visible
+ if (dom) {
+ if (dom.box.parentNode) {
+ dom.box.parentNode.removeChild(dom.box);
+ changed = true;
+ }
+ if (dom.line.parentNode) {
+ dom.line.parentNode.removeChild(dom.line);
+ changed = true;
+ }
+ if (dom.dot.parentNode) {
+ dom.dot.parentNode.removeChild(dom.dot);
+ changed = true;
+ }
+ }
+ }
+
+ return changed;
+};
+
+/**
+ * Reflow the item: calculate its actual size and position from the DOM
+ * @return {boolean} resized returns true if the axis is resized
+ * @override
+ */
+ItemBox.prototype.reflow = function () {
+ if (this.data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + this.data.id);
+ }
+
+ var update = util.updateProperty,
+ dom = this.dom,
+ props = this.props,
+ options = this.options,
+ start = this.parent.toScreen(this.data.start),
+ align = options && options.align,
+ orientation = options.orientation,
+ changed = 0,
+ top,
+ left;
+
+ if (dom) {
+ changed += update(props.dot, 'height', dom.dot.offsetHeight);
+ changed += update(props.dot, 'width', dom.dot.offsetWidth);
+ changed += update(props.line, 'width', dom.line.offsetWidth);
+ changed += update(props.line, 'width', dom.line.offsetWidth);
+ changed += update(this, 'width', dom.box.offsetWidth);
+ changed += update(this, 'height', dom.box.offsetHeight);
+
+ if (align == 'right') {
+ left = start - this.width;
+ }
+ else if (align == 'left') {
+ left = start;
+ }
+ else {
+ // default or 'center'
+ left = start - this.width / 2;
+ }
+ changed += update(this, 'left', left);
+
+ changed += update(props.line, 'left', start - props.line.width / 2);
+ changed += update(props.dot, 'left', start - props.dot.width / 2);
+ if (orientation == 'top') {
+ top = options.margin.axis;
+
+ changed += update(this, 'top', top);
+ changed += update(props.line, 'top', 0);
+ changed += update(props.line, 'height', top);
+ changed += update(props.dot, 'top', -props.dot.height / 2);
+ }
+ else {
+ // default or 'bottom'
+ var parentHeight = this.parent.height;
+ top = parentHeight - this.height - options.margin.axis;
+
+ changed += update(this, 'top', top);
+ changed += update(props.line, 'top', top + this.height);
+ changed += update(props.line, 'height', Math.max(options.margin.axis, 0));
+ changed += update(props.dot, 'top', parentHeight - props.dot.height / 2);
+ }
+ }
+ else {
+ changed += 1;
+ }
+
+ return (changed > 0);
+};
+
+/**
+ * Create an items DOM
+ * @private
+ */
+ItemBox.prototype._create = function () {
+ var dom = this.dom;
+ if (!dom) {
+ this.dom = dom = {};
+
+ // create the box
+ dom.box = document.createElement('DIV');
+ // className is updated in repaint()
+
+ // contents box (inside the background box). used for making margins
+ dom.content = document.createElement('DIV');
+ dom.content.className = 'content';
+ dom.box.appendChild(dom.content);
+
+ // line to axis
+ dom.line = document.createElement('DIV');
+ dom.line.className = 'line';
+
+ // dot on axis
+ dom.dot = document.createElement('DIV');
+ dom.dot.className = 'dot';
+ }
+};
+
+/**
+ * Reposition the item, recalculate its left, top, and width, using the current
+ * range and size of the items itemset
+ * @override
+ */
+ItemBox.prototype.reposition = function () {
+ var dom = this.dom,
+ props = this.props,
+ orientation = this.options.orientation;
+
+ if (dom) {
+ var box = dom.box,
+ line = dom.line,
+ dot = dom.dot;
+
+ box.style.left = this.left + 'px';
+ box.style.top = this.top + 'px';
+
+ line.style.left = props.line.left + 'px';
+ if (orientation == 'top') {
+ line.style.top = 0 + 'px';
+ line.style.height = this.top + 'px';
+ }
+ else {
+ // orientation 'bottom'
+ line.style.top = props.line.top + 'px';
+ line.style.top = (this.top + this.height) + 'px';
+ line.style.height = (props.dot.top - this.top - this.height) + 'px';
+
+ }
+
+ dot.style.left = props.dot.left + 'px';
+ dot.style.top = props.dot.top + 'px';
+ }
+};
+
+/**
+ * @constructor ItemPoint
+ * @extends Item
+ * @param {ItemSet} parent
+ * @param {Object} data Object containing parameters start
+ * content, className.
+ * @param {Object} [options] Options to set initial property values
+ * // TODO: describe available options
+ */
+function ItemPoint (parent, data, options) {
+ this.props = {
+ dot: {
+ top: 0,
+ width: 0,
+ height: 0
+ },
+ content: {
+ height: 0,
+ marginLeft: 0
+ }
+ };
+
+ Item.call(this, parent, data, options);
+}
+
+ItemPoint.prototype = new Item (null, null);
+
+// register the ItemPoint in the item types
+itemTypes['point'] = ItemPoint;
+
+/**
+ * Select the item
+ * @override
+ */
+ItemPoint.prototype.select = function () {
+ this.selected = true;
+ // TODO: select and unselect
+};
+
+/**
+ * Unselect the item
+ * @override
+ */
+ItemPoint.prototype.unselect = function () {
+ this.selected = false;
+ // TODO: select and unselect
+};
+
+/**
+ * Repaint the item
+ * @return {Boolean} changed
+ */
+ItemPoint.prototype.repaint = function () {
+ // TODO: make an efficient repaint
+ var changed = false;
+ var dom = this.dom;
+
+ if (this.visible) {
+ if (!dom) {
+ this._create();
+ changed = true;
+ }
+ dom = this.dom;
+
+ if (dom) {
+ if (!this.options && !this.options.parent) {
+ throw new Error('Cannot repaint item: no parent attached');
+ }
+ var parentContainer = this.parent.getContainer();
+ if (!parentContainer) {
+ throw new Error('Cannot repaint time axis: parent has no container element');
+ }
+
+ if (!dom.point.parentNode) {
+ parentContainer.appendChild(dom.point);
+ changed = true;
+ }
+
+ // update contents
+ if (this.data.content != this.content) {
+ this.content = this.data.content;
+ if (this.content instanceof Element) {
+ dom.content.innerHTML = '';
+ dom.content.appendChild(this.content);
+ }
+ else if (this.data.content != undefined) {
+ dom.content.innerHTML = this.content;
+ }
+ else {
+ throw new Error('Property "content" missing in item ' + this.data.id);
+ }
+ changed = true;
+ }
+
+ // update class
+ var className = (this.data.className? ' ' + this.data.className : '') +
+ (this.selected ? ' selected' : '');
+ if (this.className != className) {
+ this.className = className;
+ dom.point.className = 'item point' + className;
+ changed = true;
+ }
+ }
+ }
+ else {
+ // hide when visible
+ if (dom) {
+ if (dom.point.parentNode) {
+ dom.point.parentNode.removeChild(dom.point);
+ changed = true;
+ }
+ }
+ }
+
+ return changed;
+};
+
+/**
+ * Reflow the item: calculate its actual size from the DOM
+ * @return {boolean} resized returns true if the axis is resized
+ * @override
+ */
+ItemPoint.prototype.reflow = function () {
+ if (this.data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + this.data.id);
+ }
+
+ var update = util.updateProperty,
+ dom = this.dom,
+ props = this.props,
+ options = this.options,
+ orientation = options.orientation,
+ start = this.parent.toScreen(this.data.start),
+ changed = 0,
+ top;
+
+ if (dom) {
+ changed += update(this, 'width', dom.point.offsetWidth);
+ changed += update(this, 'height', dom.point.offsetHeight);
+ changed += update(props.dot, 'width', dom.dot.offsetWidth);
+ changed += update(props.dot, 'height', dom.dot.offsetHeight);
+ changed += update(props.content, 'height', dom.content.offsetHeight);
+
+ if (orientation == 'top') {
+ top = options.margin.axis;
+ }
+ else {
+ // default or 'bottom'
+ var parentHeight = this.parent.height;
+ top = parentHeight - this.height - options.margin.axis;
+ }
+ changed += update(this, 'top', top);
+ changed += update(this, 'left', start - props.dot.width / 2);
+ changed += update(props.content, 'marginLeft', 1.5 * props.dot.width);
+ //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO
+
+ changed += update(props.dot, 'top', (this.height - props.dot.height) / 2);
+ }
+ else {
+ changed += 1;
+ }
+
+ return (changed > 0);
+};
+
+/**
+ * Create an items DOM
+ * @private
+ */
+ItemPoint.prototype._create = function () {
+ var dom = this.dom;
+ if (!dom) {
+ this.dom = dom = {};
+
+ // background box
+ dom.point = document.createElement('div');
+ // className is updated in repaint()
+
+ // contents box, right from the dot
+ dom.content = document.createElement('div');
+ dom.content.className = 'content';
+ dom.point.appendChild(dom.content);
+
+ // dot at start
+ dom.dot = document.createElement('div');
+ dom.dot.className = 'dot';
+ dom.point.appendChild(dom.dot);
+ }
+};
+
+/**
+ * Reposition the item, recalculate its left, top, and width, using the current
+ * range and size of the items itemset
+ * @override
+ */
+ItemPoint.prototype.reposition = function () {
+ var dom = this.dom,
+ props = this.props;
+
+ if (dom) {
+ dom.point.style.top = this.top + 'px';
+ dom.point.style.left = this.left + 'px';
+
+ dom.content.style.marginLeft = props.content.marginLeft + 'px';
+ //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO
+
+ dom.dot.style.top = props.dot.top + 'px';
+ }
+};
+
+/**
+ * @constructor ItemRange
+ * @extends Item
+ * @param {ItemSet} parent
+ * @param {Object} data Object containing parameters start, end
+ * content, className.
+ * @param {Object} [options] Options to set initial property values
+ * // TODO: describe available options
+ */
+function ItemRange (parent, data, options) {
+ this.props = {
+ content: {
+ left: 0,
+ width: 0
+ }
+ };
+
+ Item.call(this, parent, data, options);
+}
+
+ItemRange.prototype = new Item (null, null);
+
+// register the ItemBox in the item types
+itemTypes['range'] = ItemRange;
+
+/**
+ * Select the item
+ * @override
+ */
+ItemRange.prototype.select = function () {
+ this.selected = true;
+ // TODO: select and unselect
+};
+
+/**
+ * Unselect the item
+ * @override
+ */
+ItemRange.prototype.unselect = function () {
+ this.selected = false;
+ // TODO: select and unselect
+};
+
+/**
+ * Repaint the item
+ * @return {Boolean} changed
+ */
+ItemRange.prototype.repaint = function () {
+ // TODO: make an efficient repaint
+ var changed = false;
+ var dom = this.dom;
+
+ if (this.visible) {
+ if (!dom) {
+ this._create();
+ changed = true;
+ }
+ dom = this.dom;
+
+ if (dom) {
+ if (!this.options && !this.options.parent) {
+ throw new Error('Cannot repaint item: no parent attached');
+ }
+ var parentContainer = this.parent.getContainer();
+ if (!parentContainer) {
+ throw new Error('Cannot repaint time axis: parent has no container element');
+ }
+
+ if (!dom.box.parentNode) {
+ parentContainer.appendChild(dom.box);
+ changed = true;
+ }
+
+ // update content
+ if (this.data.content != this.content) {
+ this.content = this.data.content;
+ if (this.content instanceof Element) {
+ dom.content.innerHTML = '';
+ dom.content.appendChild(this.content);
+ }
+ else if (this.data.content != undefined) {
+ dom.content.innerHTML = this.content;
+ }
+ else {
+ throw new Error('Property "content" missing in item ' + this.data.id);
+ }
+ changed = true;
+ }
+
+ // update class
+ var className = this.data.className ? ('' + this.data.className) : '';
+ if (this.className != className) {
+ this.className = className;
+ dom.box.className = 'item range' + className;
+ changed = true;
+ }
+ }
+ }
+ else {
+ // hide when visible
+ if (dom) {
+ if (dom.box.parentNode) {
+ dom.box.parentNode.removeChild(dom.box);
+ changed = true;
+ }
+ }
+ }
+
+ return changed;
+};
+
+/**
+ * Reflow the item: calculate its actual size from the DOM
+ * @return {boolean} resized returns true if the axis is resized
+ * @override
+ */
+ItemRange.prototype.reflow = function () {
+ if (this.data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + this.data.id);
+ }
+ if (this.data.end == undefined) {
+ throw new Error('Property "end" missing in item ' + this.data.id);
+ }
+
+ var dom = this.dom,
+ props = this.props,
+ options = this.options,
+ parent = this.parent,
+ start = parent.toScreen(this.data.start),
+ end = parent.toScreen(this.data.end),
+ changed = 0;
+
+ if (dom) {
+ var update = util.updateProperty,
+ box = dom.box,
+ parentWidth = parent.width,
+ orientation = options.orientation,
+ contentLeft,
+ top;
+
+ changed += update(props.content, 'width', dom.content.offsetWidth);
+
+ changed += update(this, 'height', box.offsetHeight);
+
+ // limit the width of the this, as browsers cannot draw very wide divs
+ if (start < -parentWidth) {
+ start = -parentWidth;
+ }
+ if (end > 2 * parentWidth) {
+ end = 2 * parentWidth;
+ }
+
+ // when range exceeds left of the window, position the contents at the left of the visible area
+ if (start < 0) {
+ contentLeft = Math.min(-start,
+ (end - start - props.content.width - 2 * options.padding));
+ // TODO: remove the need for options.padding. it's terrible.
+ }
+ else {
+ contentLeft = 0;
+ }
+ changed += update(props.content, 'left', contentLeft);
+
+ if (orientation == 'top') {
+ top = options.margin.axis;
+ changed += update(this, 'top', top);
+ }
+ else {
+ // default or 'bottom'
+ top = parent.height - this.height - options.margin.axis;
+ changed += update(this, 'top', top);
+ }
+
+ changed += update(this, 'left', start);
+ changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width;
+ }
+ else {
+ changed += 1;
+ }
+
+ return (changed > 0);
+};
+
+/**
+ * Create an items DOM
+ * @private
+ */
+ItemRange.prototype._create = function () {
+ var dom = this.dom;
+ if (!dom) {
+ this.dom = dom = {};
+ // background box
+ dom.box = document.createElement('div');
+ // className is updated in repaint()
+
+ // contents box
+ dom.content = document.createElement('div');
+ dom.content.className = 'content';
+ dom.box.appendChild(dom.content);
+ }
+};
+
+/**
+ * Reposition the item, recalculate its left, top, and width, using the current
+ * range and size of the items itemset
+ * @override
+ */
+ItemRange.prototype.reposition = function () {
+ var dom = this.dom,
+ props = this.props;
+
+ if (dom) {
+ dom.box.style.top = this.top + 'px';
+ dom.box.style.left = this.left + 'px';
+ dom.box.style.width = this.width + 'px';
+
+ dom.content.style.left = props.content.left + 'px';
+ }
+};
+
+/**
+ * Create a timeline visualization
+ * @param {HTMLElement} container
+ * @param {DataSet | Array | DataTable} [data]
+ * @param {Object} [options] See Timeline.setOptions for the available options.
+ * @constructor
+ */
+function Timeline (container, data, options) {
+ var me = this;
+ this.options = {
+ orientation: 'bottom',
+ zoomMin: 10, // milliseconds
+ zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
+ moveable: true,
+ zoomable: true
+ };
+
+ // controller
+ this.controller = new Controller();
+
+ // main panel
+ if (!container) {
+ throw new Error('No container element provided');
+ }
+ this.main = new RootPanel(container, {
+ autoResize: false,
+ height: function () {
+ return me.timeaxis.height + me.itemset.height;
+ }
+ });
+ this.controller.add(this.main);
+
+ // range
+ var now = moment().minutes(0).seconds(0).milliseconds(0);
+ var start = options.start && options.start.valueOf() || now.clone().add('days', -3).valueOf();
+ var end = options.end && options.end.valueOf() || moment(start).clone().add('days', 7).valueOf();
+ // TODO: if start and end are not provided, calculate range from the dataset
+ this.range = new Range({
+ start: start,
+ end: end
+ });
+ // TODO: reckon with options moveable and zoomable
+ this.range.subscribe(this.main, 'move', 'horizontal');
+ this.range.subscribe(this.main, 'zoom', 'horizontal');
+ this.range.on('rangechange', function () {
+ // TODO: fix the delay in reflow/repaint, does not feel snappy
+ me.controller.requestReflow();
+ });
+ this.range.on('rangechanged', function () {
+ me.controller.requestReflow();
+ });
+
+ // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable
+
+ // time axis
+ this.timeaxis = new TimeAxis(this.main, null, {
+ orientation: this.options.orientation,
+ range: this.range
+ });
+ this.timeaxis.setRange(this.range);
+ this.controller.add(this.timeaxis);
+
+ // items panel
+ this.itemset = new ItemSet(this.main, [this.timeaxis], {
+ orientation: this.options.orientation,
+ range: this.range,
+ data: data
+ });
+ this.itemset.setRange(this.range);
+ if (data) {
+ this.setData(data);
+ }
+ this.controller.add(this.itemset);
+
+ this.setOptions(options);
+}
+
+/**
+ * Set options
+ * @param {Object} options TODO: describe the available options
+ */
+Timeline.prototype.setOptions = function (options) {
+ util.extend(this.options, options);
+
+ // update options the timeaxis
+ this.timeaxis.setOptions(this.options);
+
+ // update options for the range
+ this.range.setOptions(this.options);
+
+ // update options the itemset
+ var top,
+ me = this;
+ if (this.options.orientation == 'top') {
+ top = function () {
+ return me.timeaxis.height;
+ }
+ }
+ else {
+ top = function () {
+ return me.main.height - me.timeaxis.height - me.itemset.height;
+ }
+ }
+ this.itemset.setOptions({
+ orientation: this.options.orientation,
+ top: top
+ });
+
+ this.controller.repaint();
+};
+
+/**
+ * Set data
+ * @param {DataSet | Array | DataTable} data
+ */
+Timeline.prototype.setData = function(data) {
+ this.itemset.setData(data);
+};
+
+// moment.js
+// version : 2.0.0
+// author : Tim Wood
+// license : MIT
+// momentjs.com
+
+(function (undefined) {
+
+ /************************************
+ Constants
+ ************************************/
+
+ var moment,
+ VERSION = "2.0.0",
+ round = Math.round, i,
+ // internal storage for language config files
+ languages = {},
+
+ // check for nodeJS
+ hasModule = (typeof module !== 'undefined' && module.exports),
+
+ // ASP.NET json date format regex
+ aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
+
+ // format tokens
+ formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g,
+ localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
+
+ // parsing tokens
+ parseMultipleFormatChunker = /([0-9a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)/gi,
+
+ // parsing token regexes
+ parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
+ parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
+ parseTokenThreeDigits = /\d{3}/, // 000 - 999
+ parseTokenFourDigits = /\d{1,4}/, // 0 - 9999
+ parseTokenSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
+ parseTokenWord = /[0-9]*[a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF]+\s*?[\u0600-\u06FF]+/i, // any word (or two) characters or numbers including two word month in arabic.
+ parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/i, // +00:00 -00:00 +0000 -0000 or Z
+ parseTokenT = /T/i, // T (ISO seperator)
+ parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
+
+ // preliminary iso regex
+ // 0000-00-00 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000
+ isoRegex = /^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,
+ isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
+
+ // iso time formats and regexes
+ isoTimes = [
+ ['HH:mm:ss.S', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/],
+ ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
+ ['HH:mm', /(T| )\d\d:\d\d/],
+ ['HH', /(T| )\d\d/]
+ ],
+
+ // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
+ parseTimezoneChunker = /([\+\-]|\d\d)/gi,
+
+ // getter and setter names
+ proxyGettersAndSetters = 'Month|Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
+ unitMillisecondFactors = {
+ 'Milliseconds' : 1,
+ 'Seconds' : 1e3,
+ 'Minutes' : 6e4,
+ 'Hours' : 36e5,
+ 'Days' : 864e5,
+ 'Months' : 2592e6,
+ 'Years' : 31536e6
+ },
+
+ // format function strings
+ formatFunctions = {},
+
+ // tokens to ordinalize and pad
+ ordinalizeTokens = 'DDD w W M D d'.split(' '),
+ paddedTokens = 'M D H h m s w W'.split(' '),
+
+ formatTokenFunctions = {
+ M : function () {
+ return this.month() + 1;
+ },
+ MMM : function (format) {
+ return this.lang().monthsShort(this, format);
+ },
+ MMMM : function (format) {
+ return this.lang().months(this, format);
+ },
+ D : function () {
+ return this.date();
+ },
+ DDD : function () {
+ return this.dayOfYear();
+ },
+ d : function () {
+ return this.day();
+ },
+ dd : function (format) {
+ return this.lang().weekdaysMin(this, format);
+ },
+ ddd : function (format) {
+ return this.lang().weekdaysShort(this, format);
+ },
+ dddd : function (format) {
+ return this.lang().weekdays(this, format);
+ },
+ w : function () {
+ return this.week();
+ },
+ W : function () {
+ return this.isoWeek();
+ },
+ YY : function () {
+ return leftZeroFill(this.year() % 100, 2);
+ },
+ YYYY : function () {
+ return leftZeroFill(this.year(), 4);
+ },
+ YYYYY : function () {
+ return leftZeroFill(this.year(), 5);
+ },
+ a : function () {
+ return this.lang().meridiem(this.hours(), this.minutes(), true);
+ },
+ A : function () {
+ return this.lang().meridiem(this.hours(), this.minutes(), false);
+ },
+ 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 ~~(this.milliseconds() / 100);
+ },
+ SS : function () {
+ return leftZeroFill(~~(this.milliseconds() / 10), 2);
+ },
+ SSS : function () {
+ return leftZeroFill(this.milliseconds(), 3);
+ },
+ Z : function () {
+ var a = -this.zone(),
+ b = "+";
+ if (a < 0) {
+ a = -a;
+ b = "-";
+ }
+ return b + leftZeroFill(~~(a / 60), 2) + ":" + leftZeroFill(~~a % 60, 2);
+ },
+ ZZ : function () {
+ var a = -this.zone(),
+ b = "+";
+ if (a < 0) {
+ a = -a;
+ b = "-";
+ }
+ return b + leftZeroFill(~~(10 * a / 6), 4);
+ },
+ X : function () {
+ return this.unix();
+ }
+ };
+
+ function padToken(func, count) {
+ return function (a) {
+ return leftZeroFill(func.call(this, a), count);
+ };
+ }
+ function ordinalizeToken(func) {
+ return function (a) {
+ return this.lang().ordinal(func.call(this, a));
+ };
+ }
+
+ while (ordinalizeTokens.length) {
+ i = ordinalizeTokens.pop();
+ formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i]);
+ }
+ while (paddedTokens.length) {
+ i = paddedTokens.pop();
+ formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
+ }
+ formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
+
+
+ /************************************
+ Constructors
+ ************************************/
+
+ function Language() {
+
+ }
+
+ // Moment prototype object
+ function Moment(config) {
+ extend(this, config);
+ }
+
+ // Duration Constructor
+ function Duration(duration) {
+ var data = this._data = {},
+ years = duration.years || duration.year || duration.y || 0,
+ months = duration.months || duration.month || duration.M || 0,
+ weeks = duration.weeks || duration.week || duration.w || 0,
+ days = duration.days || duration.day || duration.d || 0,
+ hours = duration.hours || duration.hour || duration.h || 0,
+ minutes = duration.minutes || duration.minute || duration.m || 0,
+ seconds = duration.seconds || duration.second || duration.s || 0,
+ milliseconds = duration.milliseconds || duration.millisecond || duration.ms || 0;
+
+ // representation for dateAddRemove
+ this._milliseconds = milliseconds +
+ seconds * 1e3 + // 1000
+ minutes * 6e4 + // 1000 * 60
+ hours * 36e5; // 1000 * 60 * 60
+ // Because of dateAddRemove treats 24 hours as different from a
+ // day when working around DST, we need to store them separately
+ this._days = days +
+ weeks * 7;
+ // It is impossible translate months into days without knowing
+ // which months you are are talking about, so we have to store
+ // it separately.
+ this._months = months +
+ years * 12;
+
+ // The following code bubbles up values, see the tests for
+ // examples of what that means.
+ data.milliseconds = milliseconds % 1000;
+ seconds += absRound(milliseconds / 1000);
+
+ data.seconds = seconds % 60;
+ minutes += absRound(seconds / 60);
+
+ data.minutes = minutes % 60;
+ hours += absRound(minutes / 60);
+
+ data.hours = hours % 24;
+ days += absRound(hours / 24);
+
+ days += weeks * 7;
+ data.days = days % 30;
+
+ months += absRound(days / 30);
+
+ data.months = months % 12;
+ years += absRound(months / 12);
+
+ data.years = years;
+ }
+
+
+ /************************************
+ Helpers
+ ************************************/
+
+
+ function extend(a, b) {
+ for (var i in b) {
+ if (b.hasOwnProperty(i)) {
+ a[i] = b[i];
+ }
+ }
+ return a;
+ }
+
+ function absRound(number) {
+ if (number < 0) {
+ return Math.ceil(number);
+ } else {
+ return Math.floor(number);
+ }
+ }
+
+ // left zero fill a number
+ // see http://jsperf.com/left-zero-filling for performance comparison
+ function leftZeroFill(number, targetLength) {
+ var output = number + '';
+ while (output.length < targetLength) {
+ output = '0' + output;
+ }
+ return output;
+ }
+
+ // helper function for _.addTime and _.subtractTime
+ function addOrSubtractDurationFromMoment(mom, duration, isAdding) {
+ var ms = duration._milliseconds,
+ d = duration._days,
+ M = duration._months,
+ currentDate;
+
+ if (ms) {
+ mom._d.setTime(+mom + ms * isAdding);
+ }
+ if (d) {
+ mom.date(mom.date() + d * isAdding);
+ }
+ if (M) {
+ currentDate = mom.date();
+ mom.date(1)
+ .month(mom.month() + M * isAdding)
+ .date(Math.min(currentDate, mom.daysInMonth()));
+ }
+ }
+
+ // check if is an array
+ function isArray(input) {
+ return Object.prototype.toString.call(input) === '[object Array]';
+ }
+
+ // compare two arrays, return the number of differences
+ function compareArrays(array1, array2) {
+ var len = Math.min(array1.length, array2.length),
+ lengthDiff = Math.abs(array1.length - array2.length),
+ diffs = 0,
+ i;
+ for (i = 0; i < len; i++) {
+ if (~~array1[i] !== ~~array2[i]) {
+ diffs++;
+ }
+ }
+ return diffs + lengthDiff;
+ }
+
+
+ /************************************
+ Languages
+ ************************************/
+
+
+ Language.prototype = {
+ set : function (config) {
+ var prop, i;
+ for (i in config) {
+ prop = config[i];
+ if (typeof prop === 'function') {
+ this[i] = prop;
+ } else {
+ this['_' + i] = prop;
+ }
+ }
+ },
+
+ _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
+ months : function (m) {
+ return this._months[m.month()];
+ },
+
+ _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
+ monthsShort : function (m) {
+ return this._monthsShort[m.month()];
+ },
+
+ monthsParse : function (monthName) {
+ var i, mom, regex, output;
+
+ if (!this._monthsParse) {
+ this._monthsParse = [];
+ }
+
+ for (i = 0; i < 12; i++) {
+ // make the regex if we don't have it already
+ if (!this._monthsParse[i]) {
+ mom = moment([2000, i]);
+ regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
+ this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
+ }
+ // test the regex
+ if (this._monthsParse[i].test(monthName)) {
+ return i;
+ }
+ }
+ },
+
+ _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
+ weekdays : function (m) {
+ return this._weekdays[m.day()];
+ },
+
+ _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
+ weekdaysShort : function (m) {
+ return this._weekdaysShort[m.day()];
+ },
+
+ _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
+ weekdaysMin : function (m) {
+ return this._weekdaysMin[m.day()];
+ },
+
+ _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 (key) {
+ var output = this._longDateFormat[key];
+ if (!output && this._longDateFormat[key.toUpperCase()]) {
+ output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
+ return val.slice(1);
+ });
+ this._longDateFormat[key] = output;
+ }
+ return output;
+ },
+
+ meridiem : function (hours, minutes, isLower) {
+ if (hours > 11) {
+ return isLower ? 'pm' : 'PM';
+ } else {
+ return isLower ? '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 (key, mom) {
+ var output = this._calendar[key];
+ return typeof output === 'function' ? output.apply(mom) : output;
+ },
+
+ _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 (number, withoutSuffix, string, isFuture) {
+ var output = this._relativeTime[string];
+ return (typeof output === 'function') ?
+ output(number, withoutSuffix, string, isFuture) :
+ output.replace(/%d/i, number);
+ },
+ pastFuture : function (diff, output) {
+ var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
+ return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
+ },
+
+ ordinal : function (number) {
+ return this._ordinal.replace("%d", number);
+ },
+ _ordinal : "%d",
+
+ preparse : function (string) {
+ return string;
+ },
+
+ postformat : function (string) {
+ return string;
+ },
+
+ week : function (mom) {
+ return weekOfYear(mom, this._week.dow, this._week.doy);
+ },
+ _week : {
+ dow : 0, // Sunday is the first day of the week.
+ doy : 6 // The week that contains Jan 1st is the first week of the year.
+ }
+ };
+
+ // Loads a language definition into the `languages` cache. The function
+ // takes a key and optionally values. If not in the browser and no values
+ // are provided, it will load the language file module. As a convenience,
+ // this function also returns the language values.
+ function loadLang(key, values) {
+ values.abbr = key;
+ if (!languages[key]) {
+ languages[key] = new Language();
+ }
+ languages[key].set(values);
+ return languages[key];
+ }
+
+ // Determines which language definition to use and returns it.
+ //
+ // With no parameters, it will return the global language. If you
+ // pass in a language key, such as 'en', it will return the
+ // definition for 'en', so long as 'en' has already been loaded using
+ // moment.lang.
+ function getLangDefinition(key) {
+ if (!key) {
+ return moment.fn._lang;
+ }
+ if (!languages[key] && hasModule) {
+ require('./lang/' + key);
+ }
+ return languages[key];
+ }
+
+
+ /************************************
+ Formatting
+ ************************************/
+
+
+ function removeFormattingTokens(input) {
+ if (input.match(/\[.*\]/)) {
+ return input.replace(/^\[|\]$/g, "");
+ }
+ return input.replace(/\\/g, "");
+ }
+
+ function makeFormatFunction(format) {
+ var array = format.match(formattingTokens), i, length;
+
+ for (i = 0, length = array.length; i < length; i++) {
+ if (formatTokenFunctions[array[i]]) {
+ array[i] = formatTokenFunctions[array[i]];
+ } else {
+ array[i] = removeFormattingTokens(array[i]);
+ }
+ }
+
+ return function (mom) {
+ var output = "";
+ for (i = 0; i < length; i++) {
+ output += typeof array[i].call === 'function' ? array[i].call(mom, format) : array[i];
+ }
+ return output;
+ };
+ }
+
+ // format date using native date object
+ function formatMoment(m, format) {
+ var i = 5;
+
+ function replaceLongDateFormatTokens(input) {
+ return m.lang().longDateFormat(input) || input;
+ }
+
+ while (i-- && localFormattingTokens.test(format)) {
+ format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
+ }
+
+ if (!formatFunctions[format]) {
+ formatFunctions[format] = makeFormatFunction(format);
+ }
+
+ return formatFunctions[format](m);
+ }
+
+
+ /************************************
+ Parsing
+ ************************************/
+
+
+ // get the regex to find the next token
+ function getParseRegexForToken(token) {
+ switch (token) {
+ case 'DDDD':
+ return parseTokenThreeDigits;
+ case 'YYYY':
+ return parseTokenFourDigits;
+ case 'YYYYY':
+ return parseTokenSixDigits;
+ case 'S':
+ case 'SS':
+ case 'SSS':
+ case 'DDD':
+ return parseTokenOneToThreeDigits;
+ case 'MMM':
+ case 'MMMM':
+ case 'dd':
+ case 'ddd':
+ case 'dddd':
+ case 'a':
+ case 'A':
+ return parseTokenWord;
+ case 'X':
+ return parseTokenTimestampMs;
+ case 'Z':
+ case 'ZZ':
+ return parseTokenTimezone;
+ case 'T':
+ return parseTokenT;
+ case 'MM':
+ case 'DD':
+ case 'YY':
+ case 'HH':
+ case 'hh':
+ case 'mm':
+ case 'ss':
+ case 'M':
+ case 'D':
+ case 'd':
+ case 'H':
+ case 'h':
+ case 'm':
+ case 's':
+ return parseTokenOneOrTwoDigits;
+ default :
+ return new RegExp(token.replace('\\', ''));
+ }
+ }
+
+ // function to convert string input to date
+ function addTimeToArrayFromToken(token, input, config) {
+ var a, b,
+ datePartArray = config._a;
+
+ switch (token) {
+ // MONTH
+ case 'M' : // fall through to MM
+ case 'MM' :
+ datePartArray[1] = (input == null) ? 0 : ~~input - 1;
+ break;
+ case 'MMM' : // fall through to MMMM
+ case 'MMMM' :
+ a = getLangDefinition(config._l).monthsParse(input);
+ // if we didn't find a month name, mark the date as invalid.
+ if (a != null) {
+ datePartArray[1] = a;
+ } else {
+ config._isValid = false;
+ }
+ break;
+ // DAY OF MONTH
+ case 'D' : // fall through to DDDD
+ case 'DD' : // fall through to DDDD
+ case 'DDD' : // fall through to DDDD
+ case 'DDDD' :
+ if (input != null) {
+ datePartArray[2] = ~~input;
+ }
+ break;
+ // YEAR
+ case 'YY' :
+ datePartArray[0] = ~~input + (~~input > 68 ? 1900 : 2000);
+ break;
+ case 'YYYY' :
+ case 'YYYYY' :
+ datePartArray[0] = ~~input;
+ break;
+ // AM / PM
+ case 'a' : // fall through to A
+ case 'A' :
+ config._isPm = ((input + '').toLowerCase() === 'pm');
+ break;
+ // 24 HOUR
+ case 'H' : // fall through to hh
+ case 'HH' : // fall through to hh
+ case 'h' : // fall through to hh
+ case 'hh' :
+ datePartArray[3] = ~~input;
+ break;
+ // MINUTE
+ case 'm' : // fall through to mm
+ case 'mm' :
+ datePartArray[4] = ~~input;
+ break;
+ // SECOND
+ case 's' : // fall through to ss
+ case 'ss' :
+ datePartArray[5] = ~~input;
+ break;
+ // MILLISECOND
+ case 'S' :
+ case 'SS' :
+ case 'SSS' :
+ datePartArray[6] = ~~ (('0.' + input) * 1000);
+ break;
+ // UNIX TIMESTAMP WITH MS
+ case 'X':
+ config._d = new Date(parseFloat(input) * 1000);
+ break;
+ // TIMEZONE
+ case 'Z' : // fall through to ZZ
+ case 'ZZ' :
+ config._useUTC = true;
+ a = (input + '').match(parseTimezoneChunker);
+ if (a && a[1]) {
+ config._tzh = ~~a[1];
+ }
+ if (a && a[2]) {
+ config._tzm = ~~a[2];
+ }
+ // reverse offsets
+ if (a && a[0] === '+') {
+ config._tzh = -config._tzh;
+ config._tzm = -config._tzm;
+ }
+ break;
+ }
+
+ // if the input is null, the date is not valid
+ if (input == null) {
+ config._isValid = false;
+ }
+ }
+
+ // convert an array to a date.
+ // the array should mirror the parameters below
+ // note: all values past the year are optional and will default to the lowest possible value.
+ // [year, month, day , hour, minute, second, millisecond]
+ function dateFromArray(config) {
+ var i, date, input = [];
+
+ if (config._d) {
+ return;
+ }
+
+ for (i = 0; i < 7; i++) {
+ config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
+ }
+
+ // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
+ input[3] += config._tzh || 0;
+ input[4] += config._tzm || 0;
+
+ date = new Date(0);
+
+ if (config._useUTC) {
+ date.setUTCFullYear(input[0], input[1], input[2]);
+ date.setUTCHours(input[3], input[4], input[5], input[6]);
+ } else {
+ date.setFullYear(input[0], input[1], input[2]);
+ date.setHours(input[3], input[4], input[5], input[6]);
+ }
+
+ config._d = date;
+ }
+
+ // date from string and format string
+ function makeDateFromStringAndFormat(config) {
+ // This array is used to make a Date, either with `new Date` or `Date.UTC`
+ var tokens = config._f.match(formattingTokens),
+ string = config._i,
+ i, parsedInput;
+
+ config._a = [];
+
+ for (i = 0; i < tokens.length; i++) {
+ parsedInput = (getParseRegexForToken(tokens[i]).exec(string) || [])[0];
+ if (parsedInput) {
+ string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
+ }
+ // don't parse if its not a known token
+ if (formatTokenFunctions[tokens[i]]) {
+ addTimeToArrayFromToken(tokens[i], parsedInput, config);
+ }
+ }
+ // handle am pm
+ if (config._isPm && config._a[3] < 12) {
+ config._a[3] += 12;
+ }
+ // if is 12 am, change hours to 0
+ if (config._isPm === false && config._a[3] === 12) {
+ config._a[3] = 0;
+ }
+ // return
+ dateFromArray(config);
+ }
+
+ // date from string and array of format strings
+ function makeDateFromStringAndArray(config) {
+ var tempConfig,
+ tempMoment,
+ bestMoment,
+
+ scoreToBeat = 99,
+ i,
+ currentDate,
+ currentScore;
+
+ while (config._f.length) {
+ tempConfig = extend({}, config);
+ tempConfig._f = config._f.pop();
+ makeDateFromStringAndFormat(tempConfig);
+ tempMoment = new Moment(tempConfig);
+
+ if (tempMoment.isValid()) {
+ bestMoment = tempMoment;
+ break;
+ }
+
+ currentScore = compareArrays(tempConfig._a, tempMoment.toArray());
+
+ if (currentScore < scoreToBeat) {
+ scoreToBeat = currentScore;
+ bestMoment = tempMoment;
+ }
+ }
+
+ extend(config, bestMoment);
+ }
+
+ // date from iso format
+ function makeDateFromString(config) {
+ var i,
+ string = config._i;
+ if (isoRegex.exec(string)) {
+ config._f = 'YYYY-MM-DDT';
+ for (i = 0; i < 4; i++) {
+ if (isoTimes[i][1].exec(string)) {
+ config._f += isoTimes[i][0];
+ break;
+ }
+ }
+ if (parseTokenTimezone.exec(string)) {
+ config._f += " Z";
+ }
+ makeDateFromStringAndFormat(config);
+ } else {
+ config._d = new Date(string);
+ }
+ }
+
+ function makeDateFromInput(config) {
+ var input = config._i,
+ matched = aspNetJsonRegex.exec(input);
+
+ if (input === undefined) {
+ config._d = new Date();
+ } else if (matched) {
+ config._d = new Date(+matched[1]);
+ } else if (typeof input === 'string') {
+ makeDateFromString(config);
+ } else if (isArray(input)) {
+ config._a = input.slice(0);
+ dateFromArray(config);
+ } else {
+ config._d = input instanceof Date ? new Date(+input) : new Date(input);
+ }
+ }
+
+
+ /************************************
+ Relative Time
+ ************************************/
+
+
+ // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
+ function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
+ return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
+ }
+
+ function relativeTime(milliseconds, withoutSuffix, lang) {
+ var seconds = round(Math.abs(milliseconds) / 1000),
+ minutes = round(seconds / 60),
+ hours = round(minutes / 60),
+ days = round(hours / 24),
+ years = round(days / 365),
+ args = seconds < 45 && ['s', seconds] ||
+ minutes === 1 && ['m'] ||
+ minutes < 45 && ['mm', minutes] ||
+ hours === 1 && ['h'] ||
+ hours < 22 && ['hh', hours] ||
+ days === 1 && ['d'] ||
+ days <= 25 && ['dd', days] ||
+ days <= 45 && ['M'] ||
+ days < 345 && ['MM', round(days / 30)] ||
+ years === 1 && ['y'] || ['yy', years];
+ args[2] = withoutSuffix;
+ args[3] = milliseconds > 0;
+ args[4] = lang;
+ return substituteTimeAgo.apply({}, args);
+ }
+
+
+ /************************************
+ Week of Year
+ ************************************/
+
+
+ // firstDayOfWeek 0 = sun, 6 = sat
+ // the day of the week that starts the week
+ // (usually sunday or monday)
+ // firstDayOfWeekOfYear 0 = sun, 6 = sat
+ // the first week is the week that contains the first
+ // of this day of the week
+ // (eg. ISO weeks use thursday (4))
+ function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
+ var end = firstDayOfWeekOfYear - firstDayOfWeek,
+ daysToDayOfWeek = firstDayOfWeekOfYear - mom.day();
+
+
+ if (daysToDayOfWeek > end) {
+ daysToDayOfWeek -= 7;
+ }
+
+ if (daysToDayOfWeek < end - 7) {
+ daysToDayOfWeek += 7;
+ }
+
+ return Math.ceil(moment(mom).add('d', daysToDayOfWeek).dayOfYear() / 7);
+ }
+
+
+ /************************************
+ Top Level Functions
+ ************************************/
+
+ function makeMoment(config) {
+ var input = config._i,
+ format = config._f;
+
+ if (input === null || input === '') {
+ return null;
+ }
+
+ if (typeof input === 'string') {
+ config._i = input = getLangDefinition().preparse(input);
+ }
+
+ if (moment.isMoment(input)) {
+ config = extend({}, input);
+ config._d = new Date(+input._d);
+ } else if (format) {
+ if (isArray(format)) {
+ makeDateFromStringAndArray(config);
+ } else {
+ makeDateFromStringAndFormat(config);
+ }
+ } else {
+ makeDateFromInput(config);
+ }
+
+ return new Moment(config);
+ }
+
+ moment = function (input, format, lang) {
+ return makeMoment({
+ _i : input,
+ _f : format,
+ _l : lang,
+ _isUTC : false
+ });
+ };
+
+ // creating with utc
+ moment.utc = function (input, format, lang) {
+ return makeMoment({
+ _useUTC : true,
+ _isUTC : true,
+ _l : lang,
+ _i : input,
+ _f : format
+ });
+ };
+
+ // creating with unix timestamp (in seconds)
+ moment.unix = function (input) {
+ return moment(input * 1000);
+ };
+
+ // duration
+ moment.duration = function (input, key) {
+ var isDuration = moment.isDuration(input),
+ isNumber = (typeof input === 'number'),
+ duration = (isDuration ? input._data : (isNumber ? {} : input)),
+ ret;
+
+ if (isNumber) {
+ if (key) {
+ duration[key] = input;
+ } else {
+ duration.milliseconds = input;
+ }
+ }
+
+ ret = new Duration(duration);
+
+ if (isDuration && input.hasOwnProperty('_lang')) {
+ ret._lang = input._lang;
+ }
+
+ return ret;
+ };
+
+ // version number
+ moment.version = VERSION;
+
+ // default format
+ moment.defaultFormat = isoFormat;
+
+ // This function will load languages and then set the global language. If
+ // no arguments are passed in, it will simply return the current global
+ // language key.
+ moment.lang = function (key, values) {
+ var i;
+
+ if (!key) {
+ return moment.fn._lang._abbr;
+ }
+ if (values) {
+ loadLang(key, values);
+ } else if (!languages[key]) {
+ getLangDefinition(key);
+ }
+ moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
+ };
+
+ // returns language data
+ moment.langData = function (key) {
+ if (key && key._lang && key._lang._abbr) {
+ key = key._lang._abbr;
+ }
+ return getLangDefinition(key);
+ };
+
+ // compare moment object
+ moment.isMoment = function (obj) {
+ return obj instanceof Moment;
+ };
+
+ // for typechecking Duration objects
+ moment.isDuration = function (obj) {
+ return obj instanceof Duration;
+ };
+
+
+ /************************************
+ Moment Prototype
+ ************************************/
+
+
+ moment.fn = Moment.prototype = {
+
+ clone : function () {
+ return moment(this);
+ },
+
+ valueOf : function () {
+ return +this._d;
+ },
+
+ unix : function () {
+ return Math.floor(+this._d / 1000);
+ },
+
+ toString : function () {
+ return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
+ },
+
+ toDate : function () {
+ return this._d;
+ },
+
+ toJSON : function () {
+ return moment.utc(this).format('YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
+ },
+
+ toArray : function () {
+ var m = this;
+ return [
+ m.year(),
+ m.month(),
+ m.date(),
+ m.hours(),
+ m.minutes(),
+ m.seconds(),
+ m.milliseconds()
+ ];
+ },
+
+ isValid : function () {
+ if (this._isValid == null) {
+ if (this._a) {
+ this._isValid = !compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray());
+ } else {
+ this._isValid = !isNaN(this._d.getTime());
+ }
+ }
+ return !!this._isValid;
+ },
+
+ utc : function () {
+ this._isUTC = true;
+ return this;
+ },
+
+ local : function () {
+ this._isUTC = false;
+ return this;
+ },
+
+ format : function (inputString) {
+ var output = formatMoment(this, inputString || moment.defaultFormat);
+ return this.lang().postformat(output);
+ },
+
+ add : function (input, val) {
+ var dur;
+ // switch args to support add('s', 1) and add(1, 's')
+ if (typeof input === 'string') {
+ dur = moment.duration(+val, input);
+ } else {
+ dur = moment.duration(input, val);
+ }
+ addOrSubtractDurationFromMoment(this, dur, 1);
+ return this;
+ },
+
+ subtract : function (input, val) {
+ var dur;
+ // switch args to support subtract('s', 1) and subtract(1, 's')
+ if (typeof input === 'string') {
+ dur = moment.duration(+val, input);
+ } else {
+ dur = moment.duration(input, val);
+ }
+ addOrSubtractDurationFromMoment(this, dur, -1);
+ return this;
+ },
+
+ diff : function (input, units, asFloat) {
+ var that = this._isUTC ? moment(input).utc() : moment(input).local(),
+ zoneDiff = (this.zone() - that.zone()) * 6e4,
+ diff, output;
+
+ if (units) {
+ // standardize on singular form
+ units = units.replace(/s$/, '');
+ }
+
+ if (units === 'year' || units === 'month') {
+ diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
+ output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
+ output += ((this - moment(this).startOf('month')) - (that - moment(that).startOf('month'))) / diff;
+ if (units === 'year') {
+ output = output / 12;
+ }
+ } else {
+ diff = (this - that) - zoneDiff;
+ output = units === 'second' ? diff / 1e3 : // 1000
+ units === 'minute' ? diff / 6e4 : // 1000 * 60
+ units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
+ units === 'day' ? diff / 864e5 : // 1000 * 60 * 60 * 24
+ units === 'week' ? diff / 6048e5 : // 1000 * 60 * 60 * 24 * 7
+ diff;
+ }
+ return asFloat ? output : absRound(output);
+ },
+
+ from : function (time, withoutSuffix) {
+ return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
+ },
+
+ fromNow : function (withoutSuffix) {
+ return this.from(moment(), withoutSuffix);
+ },
+
+ calendar : function () {
+ var diff = this.diff(moment().startOf('day'), 'days', true),
+ format = diff < -6 ? 'sameElse' :
+ diff < -1 ? 'lastWeek' :
+ diff < 0 ? 'lastDay' :
+ diff < 1 ? 'sameDay' :
+ diff < 2 ? 'nextDay' :
+ diff < 7 ? 'nextWeek' : 'sameElse';
+ return this.format(this.lang().calendar(format, this));
+ },
+
+ isLeapYear : function () {
+ var year = this.year();
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
+ },
+
+ isDST : function () {
+ return (this.zone() < moment([this.year()]).zone() ||
+ this.zone() < moment([this.year(), 5]).zone());
+ },
+
+ day : function (input) {
+ var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
+ return input == null ? day :
+ this.add({ d : input - day });
+ },
+
+ startOf: function (units) {
+ units = units.replace(/s$/, '');
+ // the following switch intentionally omits break keywords
+ // to utilize falling through the cases.
+ switch (units) {
+ case 'year':
+ this.month(0);
+ /* falls through */
+ case 'month':
+ this.date(1);
+ /* falls through */
+ case 'week':
+ case 'day':
+ this.hours(0);
+ /* falls through */
+ case 'hour':
+ this.minutes(0);
+ /* falls through */
+ case 'minute':
+ this.seconds(0);
+ /* falls through */
+ case 'second':
+ this.milliseconds(0);
+ /* falls through */
+ }
+
+ // weeks are a special case
+ if (units === 'week') {
+ this.day(0);
+ }
+
+ return this;
+ },
+
+ endOf: function (units) {
+ return this.startOf(units).add(units.replace(/s?$/, 's'), 1).subtract('ms', 1);
+ },
+
+ isAfter: function (input, units) {
+ units = typeof units !== 'undefined' ? units : 'millisecond';
+ return +this.clone().startOf(units) > +moment(input).startOf(units);
+ },
+
+ isBefore: function (input, units) {
+ units = typeof units !== 'undefined' ? units : 'millisecond';
+ return +this.clone().startOf(units) < +moment(input).startOf(units);
+ },
+
+ isSame: function (input, units) {
+ units = typeof units !== 'undefined' ? units : 'millisecond';
+ return +this.clone().startOf(units) === +moment(input).startOf(units);
+ },
+
+ zone : function () {
+ return this._isUTC ? 0 : this._d.getTimezoneOffset();
+ },
+
+ daysInMonth : function () {
+ return moment.utc([this.year(), this.month() + 1, 0]).date();
+ },
+
+ dayOfYear : function (input) {
+ var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
+ return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
+ },
+
+ isoWeek : function (input) {
+ var week = weekOfYear(this, 1, 4);
+ return input == null ? week : this.add("d", (input - week) * 7);
+ },
+
+ week : function (input) {
+ var week = this.lang().week(this);
+ return input == null ? week : this.add("d", (input - week) * 7);
+ },
+
+ // If passed a language key, it will set the language for this
+ // instance. Otherwise, it will return the language configuration
+ // variables for this instance.
+ lang : function (key) {
+ if (key === undefined) {
+ return this._lang;
+ } else {
+ this._lang = getLangDefinition(key);
+ return this;
+ }
+ }
+ };
+
+ // helper for adding shortcuts
+ function makeGetterAndSetter(name, key) {
+ moment.fn[name] = moment.fn[name + 's'] = function (input) {
+ var utc = this._isUTC ? 'UTC' : '';
+ if (input != null) {
+ this._d['set' + utc + key](input);
+ return this;
+ } else {
+ return this._d['get' + utc + key]();
+ }
+ };
+ }
+
+ // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds)
+ for (i = 0; i < proxyGettersAndSetters.length; i ++) {
+ makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]);
+ }
+
+ // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
+ makeGetterAndSetter('year', 'FullYear');
+
+ // add plural methods
+ moment.fn.days = moment.fn.day;
+ moment.fn.weeks = moment.fn.week;
+ moment.fn.isoWeeks = moment.fn.isoWeek;
+
+ /************************************
+ Duration Prototype
+ ************************************/
+
+
+ moment.duration.fn = Duration.prototype = {
+ weeks : function () {
+ return absRound(this.days() / 7);
+ },
+
+ valueOf : function () {
+ return this._milliseconds +
+ this._days * 864e5 +
+ this._months * 2592e6;
+ },
+
+ humanize : function (withSuffix) {
+ var difference = +this,
+ output = relativeTime(difference, !withSuffix, this.lang());
+
+ if (withSuffix) {
+ output = this.lang().pastFuture(difference, output);
+ }
+
+ return this.lang().postformat(output);
+ },
+
+ lang : moment.fn.lang
+ };
+
+ function makeDurationGetter(name) {
+ moment.duration.fn[name] = function () {
+ return this._data[name];
+ };
+ }
+
+ function makeDurationAsGetter(name, factor) {
+ moment.duration.fn['as' + name] = function () {
+ return +this / factor;
+ };
+ }
+
+ for (i in unitMillisecondFactors) {
+ if (unitMillisecondFactors.hasOwnProperty(i)) {
+ makeDurationAsGetter(i, unitMillisecondFactors[i]);
+ makeDurationGetter(i.toLowerCase());
+ }
+ }
+
+ makeDurationAsGetter('Weeks', 6048e5);
+
+
+ /************************************
+ Default Lang
+ ************************************/
+
+
+ // Set default language, other languages will inherit from English.
+ moment.lang('en', {
+ ordinal : function (number) {
+ var b = number % 10,
+ output = (~~ (number % 100 / 10) === 1) ? 'th' :
+ (b === 1) ? 'st' :
+ (b === 2) ? 'nd' :
+ (b === 3) ? 'rd' : 'th';
+ return number + output;
+ }
+ });
+
+
+ /************************************
+ Exposing Moment
+ ************************************/
+
+
+ // CommonJS module is defined
+ if (hasModule) {
+ module.exports = moment;
+ }
+ /*global ender:false */
+ if (typeof ender === 'undefined') {
+ // here, `this` means `window` in the browser, or `global` on the server
+ // add `moment` as a global object via a string identifier,
+ // for Closure Compiler "advanced" mode
+ this['moment'] = moment;
+ }
+ /*global define:false */
+ if (typeof define === "function" && define.amd) {
+ define("moment", [], function () {
+ return moment;
+ });
+ }
+}).call(this);
+
diff --git a/bin/timeline/timeline.min.js b/bin/timeline/timeline.min.js
new file mode 100644
index 00000000..3775617a
--- /dev/null
+++ b/bin/timeline/timeline.min.js
@@ -0,0 +1,27 @@
+/**
+ * timeline
+ * https://github.com/almende/vis
+ *
+ * A dynamic, browser-based visualization library.
+ *
+ * @version 3.0.0-SNAPSHOT
+ * @date 2013-04-16
+ *
+ * @license
+ * Copyright (C) 2011-2013 Almende B.V, http://almende.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+function DataSet(t){var e=this;this.options=t||{},this.data={},this.fieldId=this.options.fieldId||"id",this.fieldTypes={},this.options.fieldTypes&&util.forEach(this.options.fieldTypes,function(t,i){e.fieldTypes[i]="Date"==t||"ISODate"==t||"ASPDate"==t?"Date":t}),this.subscribers={},this.internalIds={}}function Stack(t,e){this.parent=t,this.options={order:function(t,e){return e.width-t.width||t.left-e.left}},this.ordered=[],this.setOptions(e)}function Range(t){this.id=util.randomUUID(),this.start=0,this.end=0,this.options={min:null,max:null,zoomMin:null,zoomMax:null},this.setOptions(t),this.listeners=[]}function Controller(){this.id=util.randomUUID(),this.components={},this.repaintTimer=void 0,this.reflowTimer=void 0}function Component(){this.id=null,this.parent=null,this.depends=null,this.controller=null,this.options=null,this.frame=null,this.top=0,this.left=0,this.width=0,this.height=0}function Panel(t,e,i){this.id=util.randomUUID(),this.parent=t,this.depends=e,this.options={},this.setOptions(i)}function RootPanel(t,e){this.id=util.randomUUID(),this.container=t,this.options={autoResize:!0},this.listeners={},this.setOptions(e)}function TimeAxis(t,e,i){this.id=util.randomUUID(),this.parent=t,this.depends=e,this.dom={majorLines:[],majorTexts:[],minorLines:[],minorTexts:[],redundant:{majorLines:[],majorTexts:[],minorLines:[],minorTexts:[]}},this.props={range:{start:0,end:0,minimumStep:0}},this.options={orientation:"bottom",showMinorLabels:!0,showMajorLabels:!0},this.conversion=null,this.range=null,this.setOptions(i)}function ItemSet(t,e,i){this.id=util.randomUUID(),this.parent=t,this.depends=e,this.options={style:"box",align:"center",orientation:"bottom",margin:{axis:20,item:10},padding:5};var n=this;this.data=null,this.range=null,this.listeners={add:function(t,e){n._onAdd(e.items)},update:function(t,e){n._onUpdate(e.items)},remove:function(t,e){n._onRemove(e.items)}},this.items={},this.queue={},this.stack=new Stack(this),this.conversion=null,this.setOptions(i)}function Item(t,e,i){this.parent=t,this.data=e,this.selected=!1,this.visible=!0,this.dom=null,this.options=i}function ItemBox(t,e,i){this.props={dot:{left:0,top:0,width:0,height:0},line:{top:0,left:0,width:0,height:0}},Item.call(this,t,e,i)}function ItemPoint(t,e,i){this.props={dot:{top:0,width:0,height:0},content:{height:0,marginLeft:0}},Item.call(this,t,e,i)}function ItemRange(t,e,i){this.props={content:{left:0,width:0}},Item.call(this,t,e,i)}function Timeline(t,e,i){var n=this;if(this.options={orientation:"bottom",zoomMin:10,zoomMax:31536e10,moveable:!0,zoomable:!0},this.controller=new Controller,!t)throw Error("No container element provided");this.main=new RootPanel(t,{autoResize:!1,height:function(){return n.timeaxis.height+n.itemset.height}}),this.controller.add(this.main);var o=moment().minutes(0).seconds(0).milliseconds(0),s=i.start&&i.start.valueOf()||o.clone().add("days",-3).valueOf(),r=i.end&&i.end.valueOf()||moment(s).clone().add("days",7).valueOf();this.range=new Range({start:s,end:r}),this.range.subscribe(this.main,"move","horizontal"),this.range.subscribe(this.main,"zoom","horizontal"),this.range.on("rangechange",function(){n.controller.requestReflow()}),this.range.on("rangechanged",function(){n.controller.requestReflow()}),this.timeaxis=new TimeAxis(this.main,null,{orientation:this.options.orientation,range:this.range}),this.timeaxis.setRange(this.range),this.controller.add(this.timeaxis),this.itemset=new ItemSet(this.main,[this.timeaxis],{orientation:this.options.orientation,range:this.range,data:e}),this.itemset.setRange(this.range),e&&this.setData(e),this.controller.add(this.itemset),this.setOptions(i)}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,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t},util.cast=function(t,e){if(void 0===t)return void 0;if(null===t)return null;if(!e)return t;if("function"==typeof e)return e(t);switch(e){case"boolean":case"Boolean":return Boolean(t);case"number":case"Number":return Number(t);case"string":case"String":return t+"";case"Date":if(util.isNumber(t))return new Date(t);if(t instanceof Date)return new Date(t.valueOf());if(util.isString(t)){var i=ASPDateRegex.exec(t);return i?new Date(Number(i[1])):new Date(t)}throw Error("Cannot cast object of type "+util.getType(t)+" to type Date");case"ISODate":if(t instanceof Date)return t.toISOString();if(util.isNumber(t)||util.isString(Object))return new Date(t).toISOString();throw Error("Cannot cast object of type "+util.getType(t)+" to type ISODate");case"ASPDate":if(t instanceof Date)return"/Date("+t.valueOf()+")/";if(util.isNumber(t)||util.isString(Object))return"/Date("+new Date(t).valueOf()+")/";throw Error("Cannot cast object of type "+util.getType(t)+" to type ASPDate");default:throw Error("Cannot cast object of type "+util.getType(t)+' to type "'+e+'"')}};var ASPDateRegex=/^\/?Date\((\-?\d+)/i;if(util.getType=function(t){var e=typeof t;if("object"==e){if(null==t)return"null";if(t&&t.constructor&&t.constructor.name)return t.constructor.name}return e},util.getAbsoluteLeft=function(t){for(var e=document.documentElement,i=document.body,n=t.offsetLeft,o=t.offsetParent;null!=o&&o!=i&&o!=e;)n+=o.offsetLeft,n-=o.scrollLeft,o=o.offsetParent;return n},util.getAbsoluteTop=function(t){for(var e=document.documentElement,i=document.body,n=t.offsetTop,o=t.offsetParent;null!=o&&o!=i&&o!=e;)n+=o.offsetTop,n-=o.scrollTop,o=o.offsetParent;return n},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,n=document.body;return e+(i&&i.scrollTop||n&&n.scrollTop||0)-(i&&i.clientTop||n&&n.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,n=document.body;return e+(i&&i.scrollLeft||n&&n.scrollLeft||0)-(i&&i.clientLeft||n&&n.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(" "),n=i.indexOf(e);-1!=n&&(i.splice(n,1),t.className=i.join(" "))},util.forEach=function(t,e){if(t instanceof Array)t.forEach(e);else for(var i in t)t.hasOwnProperty(i)&&e(t[i],i,t)},util.updateProperty=function(t,e,i){return t[e]!==i?(t[e]=i,!0):!1},util.addEventListener=function(t,e,i,n){t.addEventListener?(void 0===n&&(n=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.addEventListener(e,i,n)):t.attachEvent("on"+e,i)},util.removeEventListener=function(t,e,i,n){t.removeEventListener?(void 0===n&&(n=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.removeEventListener(e,i,n)):t.detachEvent("on"+e,i)},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.stopPropagation=function(t){t||(t=window.event),t.stopPropagation?t.stopPropagation():t.cancelBubble=!0},util.preventDefault=function(t){t||(t=window.event),t.preventDefault?t.preventDefault():t.returnValue=!1},util.option={},util.option.asBoolean=function(t,e){return"function"==typeof t&&(t=t()),null!=t?0!=t:e||null},util.option.asString=function(t,e){return"function"==typeof t&&(t=t()),null!=t?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},!Array.prototype.indexOf){Array.prototype.indexOf=function(t){for(var e=0;this.length>e;e++)if(this[e]==t)return e;return-1};try{console.log("Warning: Ancient browser detected. Please update your browser")}catch(err){}}Array.prototype.forEach||(Array.prototype.forEach=function(t,e){for(var i=0,n=this.length;n>i;++i)t.call(e||this,this[i],i,this)}),Array.prototype.map||(Array.prototype.map=function(t,e){var i,n,o;if(null==this)throw new TypeError(" this is null or not defined");var s=Object(this),r=s.length>>>0;if("function"!=typeof t)throw new TypeError(t+" is not a function");for(e&&(i=e),n=Array(r),o=0;r>o;){var a,h;o in s&&(a=s[o],h=t.call(i,a,o,s),n[o]=h),o++}return n}),Array.prototype.filter||(Array.prototype.filter=function(t){"use strict";if(null==this)throw new TypeError;var e=Object(this),i=e.length>>>0;if("function"!=typeof t)throw new TypeError;for(var n=[],o=arguments[1],s=0;i>s;s++)if(s in e){var r=e[s];t.call(o,r,s,e)&&n.push(r)}return n}),Object.keys||(Object.keys=function(){var t=Object.prototype.hasOwnProperty,e=!{toString:null}.propertyIsEnumerable("toString"),i=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],n=i.length;return function(o){if("object"!=typeof o&&"function"!=typeof o||null===o)throw new TypeError("Object.keys called on non-object");var s=[];for(var r in o)t.call(o,r)&&s.push(r);if(e)for(var a=0;n>a;a++)t.call(o,i[a])&&s.push(i[a]);return s}}());var events={listeners:[],indexOf:function(t){for(var e=this.listeners,i=0,n=this.listeners.length;n>i;i++){var o=e[i];if(o&&o.object==t)return i}return-1},addListener:function(t,e,i){var n=this.indexOf(t),o=this.listeners[n];o||(o={object:t,events:{}},this.listeners.push(o));var s=o.events[e];s||(s=[],o.events[e]=s),-1==s.indexOf(i)&&s.push(i)},removeListener:function(t,e,i){var n=this.indexOf(t),o=this.listeners[n];if(o){var s=o.events[e];s&&(n=s.indexOf(i),-1!=n&&s.splice(n,1),0==s.length&&delete o.events[e]);var r=0,a=o.events;for(var h in a)a.hasOwnProperty(h)&&r++;0==r&&delete this.listeners[n]}},removeAllListeners:function(){this.listeners=[]},trigger:function(t,e,i){var n=this.indexOf(t),o=this.listeners[n];if(o){var s=o.events[e];if(s)for(var r=0,a=s.length;a>r;r++)s[r](i)}}};TimeStep=function(t,e,i){this.current=new Date,this._start=new Date,this._end=new Date,this.autoScale=!0,this.scale=TimeStep.SCALE.DAY,this.step=1,this.setRange(t,e,i)},TimeStep.SCALE={MILLISECOND:1,SECOND:2,MINUTE:3,HOUR:4,DAY:5,WEEKDAY:6,MONTH:7,YEAR:8},TimeStep.prototype.setRange=function(t,e,i){t instanceof Date&&e instanceof Date&&(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);break;default:}},TimeStep.prototype.hasNext=function(){return this.current.valueOf()<=this._end.valueOf()},TimeStep.prototype.next=function(){var t=this.current.valueOf();if(6>this.current.getMonth())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()+60*1e3*this.step);break;case TimeStep.SCALE.HOUR:this.current=new Date(this.current.valueOf()+60*60*1e3*this.step);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);break;default:}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);break;default:}if(1!=this.step)switch(this.scale){case TimeStep.SCALE.MILLISECOND:this.current.getMilliseconds()0&&(this.step=e),this.autoScale=!1},TimeStep.prototype.setAutoScale=function(t){this.autoScale=t},TimeStep.prototype.setMinimumStep=function(t){if(void 0!=t){var e=31104e6,i=2592e6,n=864e5,o=36e5,s=6e4,r=1e3,a=1;1e3*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1e3),500*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=500),100*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=100),50*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=50),10*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=10),5*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=5),e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1),3*i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=3),i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=1),5*n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=5),2*n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=2),n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=1),n/2>t&&(this.scale=TimeStep.SCALE.WEEKDAY,this.step=1),4*o>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=4),o>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=1),15*s>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=15),10*s>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=10),5*s>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=5),s>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=1),15*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=15),10*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=10),5*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=5),r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=1),200*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=200),100*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=100),50*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=50),10*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=10),5*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=5),a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=1)}},TimeStep.prototype.snap=function(t){if(this.scale==TimeStep.SCALE.YEAR){var e=t.getFullYear()+Math.round(t.getMonth()/12);t.setFullYear(Math.round(e/this.step)*this.step),t.setMonth(0),t.setDate(0),t.setHours(0),t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MONTH)t.getDate()>15?(t.setDate(1),t.setMonth(t.getMonth()+1)):t.setDate(1),t.setHours(0),t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0);else if(this.scale==TimeStep.SCALE.DAY||this.scale==TimeStep.SCALE.WEEKDAY){switch(this.step){case 5:case 2:t.setHours(24*Math.round(t.getHours()/24));break;default:t.setHours(12*Math.round(t.getHours()/12))}t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.HOUR){switch(this.step){case 4:t.setMinutes(60*Math.round(t.getMinutes()/60));break;default:t.setMinutes(30*Math.round(t.getMinutes()/30))}t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MINUTE){switch(this.step){case 15:case 10:t.setMinutes(5*Math.round(t.getMinutes()/5)),t.setSeconds(0);break;case 5:t.setSeconds(60*Math.round(t.getSeconds()/60));break;default:t.setSeconds(30*Math.round(t.getSeconds()/30))}t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.SECOND)switch(this.step){case 15:case 10:t.setSeconds(5*Math.round(t.getSeconds()/5)),t.setMilliseconds(0);break;case 5:t.setMilliseconds(1e3*Math.round(t.getMilliseconds()/1e3));break;default:t.setMilliseconds(500*Math.round(t.getMilliseconds()/500))}else if(this.scale==TimeStep.SCALE.MILLISECOND){var i=this.step>5?this.step/2:1;t.setMilliseconds(Math.round(t.getMilliseconds()/i)*i)}},TimeStep.prototype.isMajor=function(){switch(this.scale){case TimeStep.SCALE.MILLISECOND:return 0==this.current.getMilliseconds();case TimeStep.SCALE.SECOND:return 0==this.current.getSeconds();case TimeStep.SCALE.MINUTE:return 0==this.current.getHours()&&0==this.current.getMinutes();case TimeStep.SCALE.HOUR:return 0==this.current.getHours();case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return 1==this.current.getDate();case TimeStep.SCALE.MONTH:return 0==this.current.getMonth();case TimeStep.SCALE.YEAR:return!1;default:return!1}},TimeStep.prototype.getLabelMinor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return 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""}},DataSet.prototype.subscribe=function(t,e,i){var n=this.subscribers[t];n||(n=[],this.subscribers[t]=n),n.push({id:i?i+"":null,callback:e})},DataSet.prototype.unsubscribe=function(t,e){var i=this.subscribers[t];i&&(this.subscribers[t]=i.filter(function(t){return t.callback!=e}))},DataSet.prototype._trigger=function(t,e,i){if("*"==t)throw Error("Cannot trigger event *");var n=[];t in this.subscribers&&(n=n.concat(this.subscribers[t])),"*"in this.subscribers&&(n=n.concat(this.subscribers["*"])),n.forEach(function(n){n.id!=i&&n.callback&&n.callback(t,e,i||null)})},DataSet.prototype.add=function(t,e){var i,n=[],o=this;if(t instanceof Array)t.forEach(function(t){var e=o._addItem(t);n.push(e)});else if(util.isDataTable(t))for(var s=this._getColumnNames(t),r=0,a=t.getNumberOfRows();a>r;r++){var h={};s.forEach(function(e,i){h[e]=t.getValue(r,i)}),i=o._addItem(h),n.push(i)}else{if(!(t instanceof Object))throw Error("Unknown dataType");i=o._addItem(t),n.push(i)}this._trigger("add",{items:n},e)},DataSet.prototype.update=function(t,e){var i,n=[],o=this;if(t instanceof Array)t.forEach(function(t){var e=o._updateItem(t);n.push(e)});else if(util.isDataTable(t))for(var s=this._getColumnNames(t),r=0,a=t.getNumberOfRows();a>r;r++){var h={};s.forEach(function(e,i){h[e]=t.getValue(r,i)}),i=o._updateItem(h),n.push(i)}else{if(!(t instanceof Object))throw Error("Unknown dataType");i=o._updateItem(t),n.push(i)}this._trigger("update",{items:n},e)},DataSet.prototype.get=function(t,e,i){var n=this;"Object"==util.getType(t)&&(i=e,e=t,t=void 0);var o={};this.options&&this.options.fieldTypes&&util.forEach(this.options.fieldTypes,function(t,e){o[e]=t}),e&&e.fieldTypes&&util.forEach(e.fieldTypes,function(t,e){o[e]=t});var s,r=e?e.fields:void 0;if(e&&e.type){if(s="DataTable"==e.type?"DataTable":"Array",i&&s!=util.getType(i))throw Error('Type of parameter "data" ('+util.getType(i)+") "+"does not correspond with specified options.type ("+e.type+")");if("DataTable"==s&&!util.isDataTable(i))throw Error('Parameter "data" must be a DataTable when options.type is "DataTable"')}else s=i?"DataTable"==util.getType(i)?"DataTable":"Array":"Array";if("DataTable"==s){var a=this._getColumnNames(i);if(void 0==t)util.forEach(this.data,function(t){n._appendRow(i,a,n._castItem(t))});else if(util.isNumber(t)||util.isString(t)){var h=n._castItem(n.data[t],o,r);this._appendRow(i,a,h)}else{if(!(t instanceof Array))throw new TypeError('Parameter "ids" must be undefined, a String, Number, or Array');t.forEach(function(t){var e=n._castItem(n.data[t],o,r);n._appendRow(i,a,e)})}}else if(i=i||[],void 0==t)util.forEach(this.data,function(t){i.push(n._castItem(t,o,r))});else{if(util.isNumber(t)||util.isString(t))return this._castItem(n.data[t],o,r);if(!(t instanceof Array))throw new TypeError('Parameter "ids" must be undefined, a String, Number, or Array');t.forEach(function(t){i.push(n._castItem(n.data[t],o,r))})}return i},DataSet.prototype.remove=function(t,e){var i=[],n=this;if(util.isNumber(t)||util.isString(t))delete this.data[t],delete this.internalIds[t],i.push(t);else if(t instanceof Array)t.forEach(function(t){n.remove(t)}),i=i.concat(t);else if(t instanceof Object)for(var o in this.data)this.data.hasOwnProperty(o)&&this.data[o]==t&&(delete this.data[o],delete this.internalIds[o],i.push(o));this._trigger("remove",{items:i},e)},DataSet.prototype.clear=function(t){var e=Object.keys(this.data);this.data=[],this.internalIds={},this._trigger("remove",{items:e},t)},DataSet.prototype._addItem=function(t){var e=t[this.fieldId];void 0==e&&(e=util.randomUUID(),t[this.fieldId]=e,this.internalIds[e]=t);var i={};for(var n in t)if(t.hasOwnProperty(n)){var o=this.fieldTypes[n];i[n]=util.cast(t[n],o)}return this.data[e]=i,e},DataSet.prototype._castItem=function(t,e,i){var n,o=this.fieldId,s=this.internalIds;return t?(n={},e=e||{},i?util.forEach(t,function(t,o){-1!=i.indexOf(o)&&(n[o]=util.cast(t,e[o]))}):util.forEach(t,function(t,i){i==o&&t in s||(n[i]=util.cast(t,e[i]))})):n=null,n},DataSet.prototype._updateItem=function(t){var e=t[this.fieldId];if(void 0==e)throw Error("Item has no id (item: "+JSON.stringify(t)+")");var i=this.data[e];if(i){for(var n in t)if(t.hasOwnProperty(n)){var o=this.fieldTypes[n];i[n]=util.cast(t[n],o)}}else this._addItem(t);return e},DataSet.prototype._getColumnNames=function(t){for(var e=[],i=0,n=t.getNumberOfColumns();n>i;i++)e[i]=t.getColumnId(i)||t.getColumnLabel(i);return e},DataSet.prototype._appendRow=function(t,e,i){var n=t.addRow();e.forEach(function(e,o){t.setValue(n,o,i[e])})},Stack.prototype.setOptions=function(t){util.extend(this.options,t)},Stack.prototype.update=function(){this._order(),this._stack()},Stack.prototype._order=function(){var t=this.parent.items;if(!t)throw Error("Cannot stack items: parent does not contain items");var e=[],i=0;util.forEach(t,function(t){e[i]=t,i++});var n=this.options.order;if("function"!=typeof this.options.order)throw Error("Option order must be a function");e.sort(n),this.ordered=e},Stack.prototype._stack=function(){var t,e,i=this.ordered,n=this.options,o="top"==n.orientation,s=n.margin&&n.margin.item||0;for(t=0,e=i.length;e>t;t++){var r=i[t],a=null;do a=this.checkOverlap(i,t,0,t-1,s),null!=a&&(r.top=o?a.top+a.height+s:a.top-r.height-s);while(a)}},Stack.prototype.checkOverlap=function(t,e,i,n,o){for(var s=this.collision,r=t[e],a=n;a>=i;a--){var h=t[a];if(s(r,h,o)&&a!=e)return h}return null},Stack.prototype.collision=function(t,e,i){return t.left-ie.left&&t.top-ie.top},Range.prototype.setOptions=function(t){util.extend(this.options,t),(null!=t.start||null!=t.end)&&this.setRange(t.start,t.end)},Range.prototype.subscribe=function(t,e,i){var n,o=this;if("horizontal"!=i&&"vertical"!=i)throw new TypeError('Unknown direction "'+i+'". '+'Choose "horizontal" or "vertical".');if("move"==e)n={component:t,event:e,direction:i,callback:function(t){o._onMouseDown(t,n)},params:{}},t.on("mousedown",n.callback),o.listeners.push(n);else{if("zoom"!=e)throw new TypeError('Unknown event "'+e+'". '+'Choose "move" or "zoom".');n={component:t,event:e,direction:i,callback:function(t){o._onMouseWheel(t,n)},params:{}},t.on("mousewheel",n.callback),o.listeners.push(n)}},Range.prototype.on=function(t,e){events.addListener(this,t,e)},Range.prototype._trigger=function(t){events.trigger(this,t,{start:this.start,end:this.end})},Range.prototype.setRange=function(t,e){var i=this._applyRange(t,e);i&&(this._trigger("rangechange"),this._trigger("rangechanged"))},Range.prototype._applyRange=function(t,e){var i,n=util.cast(t,"Number"),o=util.cast(e,"Number");if(null==n||isNaN(n))throw Error('Invalid start "'+t+'"');if(null==o||isNaN(o))throw Error('Invalid end "'+e+'"');if(n>o&&(o=n),null!=this.options.min){var s=this.options.min.valueOf();s>n&&(i=s-n,n+=i,o+=i)}if(null!=this.options.max){var r=this.options.max.valueOf();o>r&&(i=o-r,n-=i,o-=i)}if(null!=this.options.zoomMin){var a=this.options.zoomMin.valueOf();0>a&&(a=0),a>o-n&&(this.end-this.start>a?(i=a-(o-n),n-=i/2,o+=i/2):(n=this.start,o=this.end))}if(null!=this.options.zoomMax){var h=this.options.zoomMax.valueOf();0>h&&(h=0),o-n>h&&(h>this.end-this.start?(i=o-n-h,n+=i/2,o-=i/2):(n=this.start,o=this.end))}var u=this.start!=n||this.end!=o;return this.start=n,this.end=o,u},Range.prototype.getRange=function(){return{start:this.start,end:this.end}},Range.prototype.conversion=function(t){return this.start,this.end,Range.conversion(this.start,this.end,t)},Range.conversion=function(t,e,i){return 0!=i&&0!=e-t?{offset:t,factor:i/(e-t)}:{offset:0,factor:1}},Range.prototype._onMouseDown=function(t,e){t=t||window.event;var i=e.params,n=t.which?1==t.which:1==t.button;if(n){i.mouseX=util.getPageX(t),i.mouseY=util.getPageY(t),i.previousLeft=0,i.previousOffset=0,i.moved=!1,i.start=this.start,i.end=this.end;var o=e.component.frame;o&&(o.style.cursor="move");var s=this;i.onMouseMove||(i.onMouseMove=function(t){s._onMouseMove(t,e)},util.addEventListener(document,"mousemove",i.onMouseMove)),i.onMouseUp||(i.onMouseUp=function(t){s._onMouseUp(t,e)},util.addEventListener(document,"mouseup",i.onMouseUp)),util.preventDefault(t)}},Range.prototype._onMouseMove=function(t,e){t=t||window.event;var i=e.params,n=util.getPageX(t),o=util.getPageY(t);void 0==i.mouseX&&(i.mouseX=n),void 0==i.mouseY&&(i.mouseY=o);var s=n-i.mouseX,r=o-i.mouseY,a="horizontal"==e.direction?s:r;Math.abs(a)>=1&&(i.moved=!0);var h=i.end-i.start,u="horizontal"==e.direction?e.component.width:e.component.height,c=-a/u*h;this._applyRange(i.start+c,i.end+c),this._trigger("rangechange"),util.preventDefault(t)},Range.prototype._onMouseUp=function(t,e){t=t||window.event;var i=e.params;e.component.frame&&(e.component.frame.style.cursor="auto"),i.onMouseMove&&(util.removeEventListener(document,"mousemove",i.onMouseMove),i.onMouseMove=null),i.onMouseUp&&(util.removeEventListener(document,"mouseup",i.onMouseUp),i.onMouseUp=null),i.moved&&this._trigger("rangechanged")},Range.prototype._onMouseWheel=function(t,e){t=t||window.event;var i=0;if(t.wheelDelta?i=t.wheelDelta/120:t.detail&&(i=-t.detail/3),i){var n=this,o=function(){var o=i/5,s=null,r=e.component.frame;if(r){var a,h;if("horizontal"==e.direction){a=e.component.width,h=n.conversion(a);var u=util.getAbsoluteLeft(r),c=util.getPageX(t);s=(c-u)/h.factor+h.offset}else{a=e.component.height,h=n.conversion(a);var l=util.getAbsoluteTop(r),p=util.getPageY(t);s=(l+a-p-l)/h.factor+h.offset}}n.zoom(o,s)};o()}util.preventDefault(t)},Range.prototype.zoom=function(t,e){null==e&&(e=(this.start+this.end)/2),t>=1&&(t=.9),-1>=t&&(t=-.9),0>t&&(t/=1+t);var i=this.start-e,n=this.end-e,o=this.start-i*t,s=this.end-n*t;this.setRange(o,s)},Range.prototype.move=function(t){var e=this.end-this.start,i=this.start+e*t,n=this.end+e*t;this.start=i,this.end=n},Controller.prototype.add=function(t){if(void 0==t.id)throw Error("Component has no field id");if(!(t instanceof Component||t instanceof Controller))throw new TypeError("Component must be an instance of prototype Component or Controller");t.controller=this,this.components[t.id]=t},Controller.prototype.requestReflow=function(){if(!this.reflowTimer){var t=this;this.reflowTimer=setTimeout(function(){t.reflowTimer=void 0,t.reflow()},0)}},Controller.prototype.requestRepaint=function(){if(!this.repaintTimer){var t=this;this.repaintTimer=setTimeout(function(){t.repaintTimer=void 0,t.repaint()},0)}},Controller.prototype.repaint=function(){function t(n,o){o in i||(n.depends&&n.depends.forEach(function(e){t(e,e.id)}),n.parent&&t(n.parent,n.parent.id),e=n.repaint()||e,i[o]=!0)}var e=!1;this.repaintTimer&&(clearTimeout(this.repaintTimer),this.repaintTimer=void 0);var i={};util.forEach(this.components,t),e&&this.reflow()},Controller.prototype.reflow=function(){function t(n,o){o in i||(n.depends&&n.depends.forEach(function(e){t(e,e.id)}),n.parent&&t(n.parent,n.parent.id),e=n.reflow()||e,i[o]=!0)}var e=!1;this.reflowTimer&&(clearTimeout(this.reflowTimer),this.reflowTimer=void 0);var i={};util.forEach(this.components,t),e&&this.repaint()},Component.prototype.setOptions=function(t){t&&util.extend(this.options,t),this.controller&&(this.requestRepaint(),this.requestReflow())},Component.prototype.getContainer=function(){return null},Component.prototype.getFrame=function(){return this.frame},Component.prototype.repaint=function(){return!1},Component.prototype.reflow=function(){return!1},Component.prototype.requestRepaint=function(){if(!this.controller)throw Error("Cannot request a repaint: no controller configured");this.controller.requestRepaint()},Component.prototype.requestReflow=function(){if(!this.controller)throw Error("Cannot request a reflow: no controller configured");this.controller.requestReflow()},Component.prototype.on=function(t,e){if(!this.parent)throw Error("Cannot attach event: no root panel found");this.parent.on(t,e)},Panel.prototype=new Component,Panel.prototype.getContainer=function(){return this.frame},Panel.prototype.repaint=function(){var t=0,e=util.updateProperty,i=util.option.asSize,n=this.options,o=this.frame;
+if(o||(o=document.createElement("div"),o.className="panel",n.className&&("function"==typeof n.className?util.addClassName(o,n.className()+""):util.addClassName(o,n.className+"")),this.frame=o,t+=1),!o.parentNode){if(!this.parent)throw Error("Cannot repaint panel: no parent attached");var s=this.parent.getContainer();if(!s)throw Error("Cannot repaint panel: parent has no container element");s.appendChild(o),t+=1}return t+=e(o.style,"top",i(n.top,"0px")),t+=e(o.style,"left",i(n.left,"0px")),t+=e(o.style,"width",i(n.width,"100%")),t+=e(o.style,"height",i(n.height,"100%")),t>0},Panel.prototype.reflow=function(){var t=0,e=util.updateProperty,i=this.frame;return i?(t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft),t+=e(this,"width",i.offsetWidth),t+=e(this,"height",i.offsetHeight)):t+=1,t>0},RootPanel.prototype=new Panel,RootPanel.prototype.setOptions=function(t){util.extend(this.options,t),this.options.autoResize?this._watch():this._unwatch()},RootPanel.prototype.repaint=function(){var t=0,e=util.updateProperty,i=util.option.asSize,n=this.options,o=this.frame;if(o||(o=document.createElement("div"),o.className="graph panel",n.className&&util.addClassName(o,util.option.asString(n.className)),this.frame=o,t+=1),!o.parentNode){if(!this.container)throw Error("Cannot repaint root panel: no container attached");this.container.appendChild(o),t+=1}return t+=e(o.style,"top",i(n.top,"0px")),t+=e(o.style,"left",i(n.left,"0px")),t+=e(o.style,"width",i(n.width,"100%")),t+=e(o.style,"height",i(n.height,"100%")),this._updateEventEmitters(),t>0},RootPanel.prototype.reflow=function(){var t=0,e=util.updateProperty,i=this.frame;return i?(t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft),t+=e(this,"width",i.offsetWidth),t+=e(this,"height",i.offsetHeight)):t+=1,t>0},RootPanel.prototype._watch=function(){var t=this;this._unwatch();var e=function(){return t.options.autoResize?(t.frame&&(t.frame.clientWidth!=t.width||t.frame.clientHeight!=t.height)&&t.requestReflow(),void 0):(t._unwatch(),void 0)};util.addEventListener(window,"resize",e),this.watchTimer=setInterval(e,1e3)},RootPanel.prototype._unwatch=function(){this.watchTimer&&(clearInterval(this.watchTimer),this.watchTimer=void 0)},RootPanel.prototype.on=function(t,e){var i=this.listeners[t];i||(i=[],this.listeners[t]=i),i.push(e),this._updateEventEmitters()},RootPanel.prototype._updateEventEmitters=function(){if(this.listeners){var t=this;util.forEach(this.listeners,function(e,i){if(t.emitters||(t.emitters={}),!(i in t.emitters)){var n=t.frame;if(n){var o=function(t){e.forEach(function(e){e(t)})};t.emitters[i]=o,util.addEventListener(n,i,o)}}})}},TimeAxis.prototype=new Component,TimeAxis.prototype.setOptions=function(t){util.extend(this.options,t)},TimeAxis.prototype.setRange=function(t){if(!(t instanceof Range||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},TimeAxis.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.factor+e.offset)},TimeAxis.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.factor},TimeAxis.prototype.repaint=function(){var t=0,e=util.updateProperty,i=util.option.asSize,n=this.options,o=this.props,s=this.step,r=this.frame;if(r||(r=document.createElement("div"),this.frame=r,t+=1),r.className="axis "+n.orientation,!r.parentNode){if(!this.parent)throw Error("Cannot repaint time axis: no parent attached");var a=this.parent.getContainer();if(!a)throw Error("Cannot repaint time axis: parent has no container element");a.appendChild(r),t+=1}var h=r.parentNode;if(h){var u=r.nextSibling;h.removeChild(r);var c=n.orientation,l="bottom"==c?this.props.parentHeight-this.height+"px":"0px";if(t+=e(r.style,"top",i(n.top,l)),t+=e(r.style,"left",i(n.left,"0px")),t+=e(r.style,"width",i(n.width,"100%")),t+=e(r.style,"height",i(n.height,this.height+"px")),this._repaintMeasureChars(),this.step){this._repaintStart(),s.first();for(var p=void 0,d=0;s.hasNext()&&1e3>d;){d++;var f=s.getCurrent(),m=this.toScreen(f),g=s.isMajor();n.showMinorLabels&&this._repaintMinorText(m,s.getLabelMinor()),g&&n.showMajorLabels?(m>0&&(void 0==p&&(p=m),this._repaintMajorText(m,s.getLabelMajor())),this._repaintMajorLine(m)):this._repaintMinorLine(m),s.next()}if(n.showMajorLabels){var v=this.toTime(0),y=s.getLabelMajor(v),S=y.length*(o.majorCharWidth||10)+10;(void 0==p||p>S)&&this._repaintMajorText(0,y)}this._repaintEnd()}this._repaintLine(),u?h.insertBefore(r,u):h.appendChild(r)}return t>0},TimeAxis.prototype._repaintStart=function(){var t=this.dom,e=t.redundant;e.majorLines=t.majorLines,e.majorTexts=t.majorTexts,e.minorLines=t.minorLines,e.minorTexts=t.minorTexts,t.majorLines=[],t.majorTexts=[],t.minorLines=[],t.minorTexts=[]},TimeAxis.prototype._repaintEnd=function(){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){var i=this.dom.redundant.minorTexts.shift();if(!i){var n=document.createTextNode("");i=document.createElement("div"),i.appendChild(n),i.className="text minor",this.frame.appendChild(i)}this.dom.minorTexts.push(i),i.childNodes[0].nodeValue=e,i.style.left=t+"px",i.style.top=this.props.minorLabelTop+"px"},TimeAxis.prototype._repaintMajorText=function(t,e){var i=this.dom.redundant.majorTexts.shift();if(!i){var n=document.createTextNode(e);i=document.createElement("div"),i.className="text major",i.appendChild(n),this.frame.appendChild(i)}this.dom.majorTexts.push(i),i.childNodes[0].nodeValue=e,i.style.top=this.props.majorLabelTop+"px",i.style.left=t+"px"},TimeAxis.prototype._repaintMinorLine=function(t){var e=this.dom.redundant.minorLines.shift();e||(e=document.createElement("div"),e.className="grid vertical minor",this.frame.appendChild(e)),this.dom.minorLines.push(e);var i=this.props;e.style.top=i.minorLineTop+"px",e.style.height=i.minorLineHeight+"px",e.style.left=t-i.minorLineWidth/2+"px"},TimeAxis.prototype._repaintMajorLine=function(t){var e=this.dom.redundant.majorLines.shift();e||(e=document.createElement("DIV"),e.className="grid vertical major",this.frame.appendChild(e)),this.dom.majorLines.push(e);var i=this.props;e.style.top=i.majorLineTop+"px",e.style.left=t-i.majorLineWidth/2+"px",e.style.height=i.majorLineHeight+"px"},TimeAxis.prototype._repaintLine=function(){var t=this.dom.line,e=this.frame,i=this.options;i.showMinorLabels||i.showMajorLabels?(t?(e.removeChild(t),e.appendChild(t)):(t=document.createElement("div"),t.className="grid horizontal major",e.appendChild(t),this.dom.line=t),t.style.top=this.props.lineTop+"px"):t&&axis.parentElement&&(e.removeChild(axis.line),delete this.dom.line)},TimeAxis.prototype._repaintMeasureChars=function(){var t,e=this.dom;if(!e.characterMinor){t=document.createTextNode("0");var i=document.createElement("DIV");i.className="text minor measure",i.appendChild(t),this.frame.appendChild(i),e.measureCharMinor=i}if(!e.characterMajor){t=document.createTextNode("0");var n=document.createElement("DIV");n.className="text major measure",n.appendChild(t),this.frame.appendChild(n),e.measureCharMajor=n}},TimeAxis.prototype.reflow=function(){var t=0,e=util.updateProperty,i=this.frame,n=this.range;if(!n)throw Error("Cannot repaint time axis: no range configured");if(i){t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft);var o=this.props,s=this.options.showMinorLabels,r=this.options.showMajorLabels,a=this.dom.measureCharMinor,h=this.dom.measureCharMajor;a&&(o.minorCharHeight=a.clientHeight,o.minorCharWidth=a.clientWidth),h&&(o.majorCharHeight=h.clientHeight,o.majorCharWidth=h.clientWidth);var u=i.parentNode?i.parentNode.offsetHeight:0;switch(u!=o.parentHeight&&(o.parentHeight=u,t+=1),this.options.orientation){case"bottom":o.minorLabelHeight=s?o.minorCharHeight:0,o.majorLabelHeight=r?o.majorCharHeight:0,o.minorLabelTop=0,o.majorLabelTop=o.minorLabelTop+o.minorLabelHeight,o.minorLineTop=-this.top,o.minorLineHeight=this.top+o.majorLabelHeight,o.minorLineWidth=1,o.majorLineTop=-this.top,o.majorLineHeight=this.top+o.minorLabelHeight+o.majorLabelHeight,o.majorLineWidth=1,o.lineTop=0;break;case"top":o.minorLabelHeight=s?o.minorCharHeight:0,o.majorLabelHeight=r?o.majorCharHeight:0,o.majorLabelTop=0,o.minorLabelTop=o.majorLabelTop+o.majorLabelHeight,o.minorLineTop=o.minorLabelTop,o.minorLineHeight=u-o.majorLabelHeight-this.top,o.minorLineWidth=1,o.majorLineTop=0,o.majorLineHeight=u-this.top,o.majorLineWidth=1,o.lineTop=o.majorLabelHeight+o.minorLabelHeight;break;default:throw Error('Unkown orientation "'+this.options.orientation+'"')}var c=o.minorLabelHeight+o.majorLabelHeight;t+=e(this,"width",i.offsetWidth),t+=e(this,"height",c),this._updateConversion();var l=util.cast(n.start,"Date"),p=util.cast(n.end,"Date"),d=this.toTime(5*(o.minorCharWidth||10))-this.toTime(0);this.step=new TimeStep(l,p,d),t+=e(o.range,"start",l.valueOf()),t+=e(o.range,"end",p.valueOf()),t+=e(o.range,"minimumStep",d.valueOf())}return t>0},TimeAxis.prototype._updateConversion=function(){var t=this.range;if(!t)throw Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):Range.conversion(t.start,t.end,this.width)},ItemSet.prototype=new Panel,ItemSet.prototype.setOptions=function(t){util.extend(this.options,t),this.stack.setOptions(this.options)},ItemSet.prototype.setRange=function(t){if(!(t instanceof Range||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},ItemSet.prototype.repaint=function(){var t=0,e=util.updateProperty,i=util.option.asSize,n=this.options,o=this.frame;if(o||(o=document.createElement("div"),o.className="itemset",n.className&&util.addClassName(o,util.option.asString(n.className)),this.frame=o,t+=1),!o.parentNode){if(!this.parent)throw Error("Cannot repaint itemset: no parent attached");var s=this.parent.getContainer();if(!s)throw Error("Cannot repaint itemset: parent has no container element");s.appendChild(o),t+=1}t+=e(o.style,"height",i(n.height,this.height+"px")),t+=e(o.style,"top",i(n.top,"0px")),t+=e(o.style,"left",i(n.left,"0px")),t+=e(o.style,"width",i(n.width,"100%")),this._updateConversion();var r=this,a=this.queue,h=this.data,u=this.items,c={fields:["id","start","end","content","type"]};return Object.keys(a).forEach(function(e){var i=a[e],o=i.item;switch(i.action){case"add":case"update":var s=h.get(e,c),l=s.type||s.start&&s.end&&"range"||"box",p=itemTypes[l];if(o&&(p&&o instanceof p?(o.data=s,t+=o.repaint()):(o.visible=!1,t+=o.repaint(),o=null)),!o){if(!p)throw new TypeError('Unknown item type "'+l+'"');o=new p(r,s,n),t+=o.repaint()}u[e]=o,delete a[e];break;case"remove":o&&(o.visible=!1,t+=o.repaint()),delete u[e],delete a[e];break;default:console.log('Error: unknown action "'+i.action+'"')}}),util.forEach(this.items,function(t){t.reposition()}),t>0},ItemSet.prototype.reflow=function(){var t=0,e=this.options,i=util.updateProperty,n=this.frame;if(n){if(this._updateConversion(),util.forEach(this.items,function(e){t+=e.reflow()}),this.stack.update(),null!=e.height)t+=i(this,"height",n.offsetHeight);else{var o=this.height,s=0;"top"==e.orientation?util.forEach(this.items,function(t){s=Math.max(s,t.top+t.height)}):util.forEach(this.items,function(t){s=Math.max(s,o-t.top)}),t+=i(this,"height",s+e.margin.axis)}t+=i(this,"top",n.offsetTop),t+=i(this,"left",n.offsetLeft),t+=i(this,"width",n.offsetWidth)}else t+=1;return t>0},ItemSet.prototype.setData=function(t){var e=this.data;e&&util.forEach(this.listeners,function(t,i){e.unsubscribe(i,t)}),t instanceof DataSet?this.data=t:(this.data=new DataSet({fieldTypes:{start:"Date",end:"Date"}}),this.data.add(t));var i=this.id,n=this;util.forEach(this.listeners,function(t,e){n.data.subscribe(e,t,i)});var o=this.data.get({filter:["id"]}),s=[];util.forEach(o,function(t,e){s[e]=t.id}),this._onAdd(s)},ItemSet.prototype._onUpdate=function(t){this._toQueue(t,"update")},ItemSet.prototype._onAdd=function(t){this._toQueue(t,"add")},ItemSet.prototype._onRemove=function(t){this._toQueue(t,"remove")},ItemSet.prototype._toQueue=function(t,e){var i=this.items,n=this.queue;t.forEach(function(t){var o=n[t];o?o.action=e:n[t]={item:i[t]||null,action:e}}),this.controller&&this.requestRepaint()},ItemSet.prototype._updateConversion=function(){var t=this.range;if(!t)throw Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):Range.conversion(t.start,t.end,this.width)},ItemSet.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.factor+e.offset)},ItemSet.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.factor},Item.prototype=new Component,Item.prototype.select=function(){this.selected=!0},Item.prototype.unselect=function(){this.selected=!1};var itemTypes={};ItemBox.prototype=new Item(null,null),itemTypes.box=ItemBox,ItemBox.prototype.select=function(){this.selected=!0},ItemBox.prototype.unselect=function(){this.selected=!1},ItemBox.prototype.repaint=function(){var t=!1,e=this.dom;if(this.visible){if(e||(this._create(),t=!0),e=this.dom){if(!this.options&&!this.parent)throw Error("Cannot repaint item: no parent attached");var i=this.parent.getContainer();if(!i)throw Error("Cannot repaint time axis: parent has no container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),e.line.parentNode||(i.appendChild(e.line),t=!0),e.dot.parentNode||(i.appendChild(e.dot),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=n&&(this.className=n,e.box.className="item box"+n,e.line.className="item line"+n,e.dot.className="item dot"+n,t=!0)}}else e&&(e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),e.line.parentNode&&(e.line.parentNode.removeChild(e.line),t=!0),e.dot.parentNode&&(e.dot.parentNode.removeChild(e.dot),t=!0));return t},ItemBox.prototype.reflow=function(){if(void 0==this.data.start)throw Error('Property "start" missing in item '+this.data.id);var t,e,i=util.updateProperty,n=this.dom,o=this.props,s=this.options,r=this.parent.toScreen(this.data.start),a=s&&s.align,h=s.orientation,u=0;if(n)if(u+=i(o.dot,"height",n.dot.offsetHeight),u+=i(o.dot,"width",n.dot.offsetWidth),u+=i(o.line,"width",n.line.offsetWidth),u+=i(o.line,"width",n.line.offsetWidth),u+=i(this,"width",n.box.offsetWidth),u+=i(this,"height",n.box.offsetHeight),e="right"==a?r-this.width:"left"==a?r:r-this.width/2,u+=i(this,"left",e),u+=i(o.line,"left",r-o.line.width/2),u+=i(o.dot,"left",r-o.dot.width/2),"top"==h)t=s.margin.axis,u+=i(this,"top",t),u+=i(o.line,"top",0),u+=i(o.line,"height",t),u+=i(o.dot,"top",-o.dot.height/2);else{var c=this.parent.height;t=c-this.height-s.margin.axis,u+=i(this,"top",t),u+=i(o.line,"top",t+this.height),u+=i(o.line,"height",Math.max(s.margin.axis,0)),u+=i(o.dot,"top",c-o.dot.height/2)}else u+=1;return u>0},ItemBox.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.box=document.createElement("DIV"),t.content=document.createElement("DIV"),t.content.className="content",t.box.appendChild(t.content),t.line=document.createElement("DIV"),t.line.className="line",t.dot=document.createElement("DIV"),t.dot.className="dot")},ItemBox.prototype.reposition=function(){var t=this.dom,e=this.props,i=this.options.orientation;if(t){var n=t.box,o=t.line,s=t.dot;n.style.left=this.left+"px",n.style.top=this.top+"px",o.style.left=e.line.left+"px","top"==i?(o.style.top="0px",o.style.height=this.top+"px"):(o.style.top=e.line.top+"px",o.style.top=this.top+this.height+"px",o.style.height=e.dot.top-this.top-this.height+"px"),s.style.left=e.dot.left+"px",s.style.top=e.dot.top+"px"}},ItemPoint.prototype=new Item(null,null),itemTypes.point=ItemPoint,ItemPoint.prototype.select=function(){this.selected=!0},ItemPoint.prototype.unselect=function(){this.selected=!1},ItemPoint.prototype.repaint=function(){var t=!1,e=this.dom;if(this.visible){if(e||(this._create(),t=!0),e=this.dom){if(!this.options&&!this.options.parent)throw Error("Cannot repaint item: no parent attached");var i=this.parent.getContainer();if(!i)throw Error("Cannot repaint time axis: parent has no container element");if(e.point.parentNode||(i.appendChild(e.point),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=n&&(this.className=n,e.point.className="item point"+n,t=!0)}}else e&&e.point.parentNode&&(e.point.parentNode.removeChild(e.point),t=!0);return t},ItemPoint.prototype.reflow=function(){if(void 0==this.data.start)throw Error('Property "start" missing in item '+this.data.id);var t,e=util.updateProperty,i=this.dom,n=this.props,o=this.options,s=o.orientation,r=this.parent.toScreen(this.data.start),a=0;if(i){if(a+=e(this,"width",i.point.offsetWidth),a+=e(this,"height",i.point.offsetHeight),a+=e(n.dot,"width",i.dot.offsetWidth),a+=e(n.dot,"height",i.dot.offsetHeight),a+=e(n.content,"height",i.content.offsetHeight),"top"==s)t=o.margin.axis;else{var h=this.parent.height;t=h-this.height-o.margin.axis}a+=e(this,"top",t),a+=e(this,"left",r-n.dot.width/2),a+=e(n.content,"marginLeft",1.5*n.dot.width),a+=e(n.dot,"top",(this.height-n.dot.height)/2)}else a+=1;return a>0},ItemPoint.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.point=document.createElement("div"),t.content=document.createElement("div"),t.content.className="content",t.point.appendChild(t.content),t.dot=document.createElement("div"),t.dot.className="dot",t.point.appendChild(t.dot))},ItemPoint.prototype.reposition=function(){var t=this.dom,e=this.props;t&&(t.point.style.top=this.top+"px",t.point.style.left=this.left+"px",t.content.style.marginLeft=e.content.marginLeft+"px",t.dot.style.top=e.dot.top+"px")},ItemRange.prototype=new Item(null,null),itemTypes.range=ItemRange,ItemRange.prototype.select=function(){this.selected=!0},ItemRange.prototype.unselect=function(){this.selected=!1},ItemRange.prototype.repaint=function(){var t=!1,e=this.dom;if(this.visible){if(e||(this._create(),t=!0),e=this.dom){if(!this.options&&!this.options.parent)throw Error("Cannot repaint item: no parent attached");var i=this.parent.getContainer();if(!i)throw Error("Cannot repaint time axis: parent has no container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=this.data.className?""+this.data.className:"";this.className!=n&&(this.className=n,e.box.className="item range"+n,t=!0)}}else e&&e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0);return t},ItemRange.prototype.reflow=function(){if(void 0==this.data.start)throw Error('Property "start" missing in item '+this.data.id);if(void 0==this.data.end)throw Error('Property "end" missing in item '+this.data.id);var t=this.dom,e=this.props,i=this.options,n=this.parent,o=n.toScreen(this.data.start),s=n.toScreen(this.data.end),r=0;if(t){var a,h,u=util.updateProperty,c=t.box,l=n.width,p=i.orientation;r+=u(e.content,"width",t.content.offsetWidth),r+=u(this,"height",c.offsetHeight),-l>o&&(o=-l),s>2*l&&(s=2*l),a=0>o?Math.min(-o,s-o-e.content.width-2*i.padding):0,r+=u(e.content,"left",a),"top"==p?(h=i.margin.axis,r+=u(this,"top",h)):(h=n.height-this.height-i.margin.axis,r+=u(this,"top",h)),r+=u(this,"left",o),r+=u(this,"width",Math.max(s-o,1))}else r+=1;return r>0},ItemRange.prototype._create=function(){var t=this.dom;t||(this.dom=t={},t.box=document.createElement("div"),t.content=document.createElement("div"),t.content.className="content",t.box.appendChild(t.content))},ItemRange.prototype.reposition=function(){var t=this.dom,e=this.props;t&&(t.box.style.top=this.top+"px",t.box.style.left=this.left+"px",t.box.style.width=this.width+"px",t.content.style.left=e.content.left+"px")},Timeline.prototype.setOptions=function(t){util.extend(this.options,t),this.timeaxis.setOptions(this.options),this.range.setOptions(this.options);var e,i=this;e="top"==this.options.orientation?function(){return i.timeaxis.height}:function(){return i.main.height-i.timeaxis.height-i.itemset.height},this.itemset.setOptions({orientation:this.options.orientation,top:e}),this.controller.repaint()},Timeline.prototype.setData=function(t){this.itemset.setData(t)},function(t){function e(t,e){return function(i){return h(t.call(this,i),e)}}function i(t){return function(e){return this.lang().ordinal(t.call(this,e))}}function n(){}function o(t){r(this,t)}function s(t){var e=this._data={},i=t.years||t.year||t.y||0,n=t.months||t.month||t.M||0,o=t.weeks||t.week||t.w||0,s=t.days||t.day||t.d||0,r=t.hours||t.hour||t.h||0,h=t.minutes||t.minute||t.m||0,u=t.seconds||t.second||t.s||0,c=t.milliseconds||t.millisecond||t.ms||0;this._milliseconds=c+1e3*u+6e4*h+36e5*r,this._days=s+7*o,this._months=n+12*i,e.milliseconds=c%1e3,u+=a(c/1e3),e.seconds=u%60,h+=a(u/60),e.minutes=h%60,r+=a(h/60),e.hours=r%24,s+=a(r/24),s+=7*o,e.days=s%30,n+=a(s/30),e.months=n%12,i+=a(n/12),e.years=i}function r(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}function a(t){return 0>t?Math.ceil(t):Math.floor(t)}function h(t,e){for(var i=t+"";e>i.length;)i="0"+i;return i}function u(t,e,i){var n,o=e._milliseconds,s=e._days,r=e._months;o&&t._d.setTime(+t+o*i),s&&t.date(t.date()+s*i),r&&(n=t.date(),t.date(1).month(t.month()+r*i).date(Math.min(n,t.daysInMonth())))}function c(t){return"[object Array]"===Object.prototype.toString.call(t)}function l(t,e){var i,n=Math.min(t.length,e.length),o=Math.abs(t.length-e.length),s=0;for(i=0;n>i;i++)~~t[i]!==~~e[i]&&s++;return s+o}function p(t,e){return e.abbr=t,H[t]||(H[t]=new n),H[t].set(e),H[t]}function d(t){return t?(!H[t]&&k&&require("./lang/"+t),H[t]):O.fn._lang}function f(t){return t.match(/\[.*\]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function m(t){var e,i,n=t.match(P);for(e=0,i=n.length;i>e;e++)n[e]=oe[n[e]]?oe[n[e]]:f(n[e]);return function(o){var s="";for(e=0;i>e;e++)s+="function"==typeof n[e].call?n[e].call(o,t):n[e];return s}}function g(t,e){function i(e){return t.lang().longDateFormat(e)||e}for(var n=5;n--&&j.test(e);)e=e.replace(j,i);return ee[e]||(ee[e]=m(e)),ee[e](t)}function v(t){switch(t){case"DDDD":return W;case"YYYY":return F;case"YYYYY":return V;case"S":case"SS":case"SSS":case"DDD":return z;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":case"a":case"A":return q;case"X":return X;case"Z":case"ZZ":return Z;case"T":return B;case"MM":case"DD":case"YY":case"HH":case"hh":case"mm":case"ss":case"M":case"D":case"d":case"H":case"h":case"m":case"s":return U;default:return RegExp(t.replace("\\",""))}}function y(t,e,i){var n,o=i._a;switch(t){case"M":case"MM":o[1]=null==e?0:~~e-1;break;case"MMM":case"MMMM":n=d(i._l).monthsParse(e),null!=n?o[1]=n:i._isValid=!1;break;case"D":case"DD":case"DDD":case"DDDD":null!=e&&(o[2]=~~e);break;case"YY":o[0]=~~e+(~~e>68?1900:2e3);break;case"YYYY":case"YYYYY":o[0]=~~e;break;case"a":case"A":i._isPm="pm"===(e+"").toLowerCase();break;case"H":case"HH":case"h":case"hh":o[3]=~~e;break;case"m":case"mm":o[4]=~~e;break;case"s":case"ss":o[5]=~~e;break;case"S":case"SS":case"SSS":o[6]=~~(1e3*("0."+e));break;case"X":i._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":i._useUTC=!0,n=(e+"").match(Q),n&&n[1]&&(i._tzh=~~n[1]),n&&n[2]&&(i._tzm=~~n[2]),n&&"+"===n[0]&&(i._tzh=-i._tzh,i._tzm=-i._tzm)}null==e&&(i._isValid=!1)}function S(t){var e,i,n=[];if(!t._d){for(e=0;7>e;e++)t._a[e]=n[e]=null==t._a[e]?2===e?1:0:t._a[e];n[3]+=t._tzh||0,n[4]+=t._tzm||0,i=new Date(0),t._useUTC?(i.setUTCFullYear(n[0],n[1],n[2]),i.setUTCHours(n[3],n[4],n[5],n[6])):(i.setFullYear(n[0],n[1],n[2]),i.setHours(n[3],n[4],n[5],n[6])),t._d=i}}function T(t){var e,i,n=t._f.match(P),o=t._i;for(t._a=[],e=0;n.length>e;e++)i=(v(n[e]).exec(o)||[])[0],i&&(o=o.slice(o.indexOf(i)+i.length)),oe[n[e]]&&y(n[e],i,t);t._isPm&&12>t._a[3]&&(t._a[3]+=12),t._isPm===!1&&12===t._a[3]&&(t._a[3]=0),S(t)}function E(t){for(var e,i,n,s,a=99;t._f.length;){if(e=r({},t),e._f=t._f.pop(),T(e),i=new o(e),i.isValid()){n=i;break}s=l(e._a,i.toArray()),a>s&&(a=s,n=i)}r(t,n)}function w(t){var e,i=t._i;if(K.exec(i)){for(t._f="YYYY-MM-DDT",e=0;4>e;e++)if($[e][1].exec(i)){t._f+=$[e][0];break}Z.exec(i)&&(t._f+=" Z"),T(t)}else t._d=new Date(i)}function M(e){var i=e._i,n=R.exec(i);i===t?e._d=new Date:n?e._d=new Date(+n[1]):"string"==typeof i?w(e):c(i)?(e._a=i.slice(0),S(e)):e._d=i instanceof Date?new Date(+i):new Date(i)}function _(t,e,i,n,o){return o.relativeTime(e||1,!!i,t,n)}function C(t,e,i){var n=I(Math.abs(t)/1e3),o=I(n/60),s=I(o/60),r=I(s/24),a=I(r/365),h=45>n&&["s",n]||1===o&&["m"]||45>o&&["mm",o]||1===s&&["h"]||22>s&&["hh",s]||1===r&&["d"]||25>=r&&["dd",r]||45>=r&&["M"]||345>r&&["MM",I(r/30)]||1===a&&["y"]||["yy",a];return h[2]=e,h[3]=t>0,h[4]=i,_.apply({},h)}function D(t,e,i){var n=i-e,o=i-t.day();return o>n&&(o-=7),n-7>o&&(o+=7),Math.ceil(O(t).add("d",o).dayOfYear()/7)}function b(t){var e=t._i,i=t._f;return null===e||""===e?null:("string"==typeof e&&(t._i=e=d().preparse(e)),O.isMoment(e)?(t=r({},e),t._d=new Date(+e._d)):i?c(i)?E(t):T(t):M(t),new o(t))}function L(t,e){O.fn[t]=O.fn[t+"s"]=function(t){var i=this._isUTC?"UTC":"";return null!=t?(this._d["set"+i+e](t),this):this._d["get"+i+e]()}}function A(t){O.duration.fn[t]=function(){return this._data[t]}}function x(t,e){O.duration.fn["as"+t]=function(){return+this/e}}for(var O,N,Y="2.0.0",I=Math.round,H={},k="undefined"!=typeof module&&module.exports,R=/^\/?Date\((\-?\d+)/i,P=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g,j=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,U=/\d\d?/,z=/\d{1,3}/,W=/\d{3}/,F=/\d{1,4}/,V=/[+\-]?\d{1,6}/,q=/[0-9]*[a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF]+\s*?[\u0600-\u06FF]+/i,Z=/Z|[\+\-]\d\d:?\d\d/i,B=/T/i,X=/[\+\-]?\d+(\.\d{1,3})?/,K=/^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,J="YYYY-MM-DDTHH:mm:ssZ",$=[["HH:mm:ss.S",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],Q=/([\+\-]|\d\d)/gi,G="Month|Date|Hours|Minutes|Seconds|Milliseconds".split("|"),te={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},ee={},ie="DDD w W M D d".split(" "),ne="M D H h m s w W".split(" "),oe={M:function(){return this.month()+1},MMM:function(t){return this.lang().monthsShort(this,t)},MMMM:function(t){return this.lang().months(this,t)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(t){return this.lang().weekdaysMin(this,t)},ddd:function(t){return this.lang().weekdaysShort(this,t)},dddd:function(t){return this.lang().weekdays(this,t)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return h(this.year()%100,2)},YYYY:function(){return h(this.year(),4)},YYYYY:function(){return h(this.year(),5)},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~~(this.milliseconds()/100)},SS:function(){return h(~~(this.milliseconds()/10),2)},SSS:function(){return h(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+h(~~(t/60),2)+":"+h(~~t%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+h(~~(10*t/6),4)},X:function(){return this.unix()}};ie.length;)N=ie.pop(),oe[N+"o"]=i(oe[N]);for(;ne.length;)N=ne.pop(),oe[N+N]=e(oe[N],2);for(oe.DDDD=e(oe.DDD,3),n.prototype={set:function(t){var e,i;for(i in t)e=t[i],"function"==typeof e?this[i]=e:this["_"+i]=e},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(t){return this._months[t.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(t){return this._monthsShort[t.month()]},monthsParse:function(t){var e,i,n;for(this._monthsParse||(this._monthsParse=[]),e=0;12>e;e++)if(this._monthsParse[e]||(i=O([2e3,e]),n="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[e]=RegExp(n.replace(".",""),"i")),this._monthsParse[e].test(t))return e},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(t){return this._weekdays[t.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(t){return this._weekdaysShort[t.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(t){return this._weekdaysMin[t.day()]},_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},meridiem:function(t,e,i){return t>11?i?"pm":"PM":i?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[last] dddd [at] LT",sameElse:"L"},calendar:function(t,e){var i=this._calendar[t];return"function"==typeof i?i.apply(e):i},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(t,e,i,n){var o=this._relativeTime[i];return"function"==typeof o?o(t,e,i,n):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 D(t,this._week.dow,this._week.doy)},_week:{dow:0,doy:6}},O=function(t,e,i){return b({_i:t,_f:e,_l:i,_isUTC:!1})},O.utc=function(t,e,i){return b({_useUTC:!0,_isUTC:!0,_l:i,_i:t,_f:e})},O.unix=function(t){return O(1e3*t)},O.duration=function(t,e){var i,n=O.isDuration(t),o="number"==typeof t,r=n?t._data:o?{}:t;return o&&(e?r[e]=t:r.milliseconds=t),i=new s(r),n&&t.hasOwnProperty("_lang")&&(i._lang=t._lang),i},O.version=Y,O.defaultFormat=J,O.lang=function(e,i){return e?(i?p(e,i):H[e]||d(e),O.duration.fn._lang=O.fn._lang=d(e),t):O.fn._lang._abbr},O.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),d(t)},O.isMoment=function(t){return t instanceof o},O.isDuration=function(t){return t instanceof s},O.fn=o.prototype={clone:function(){return O(this)},valueOf:function(){return+this._d},unix:function(){return Math.floor(+this._d/1e3)},toString:function(){return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._d},toJSON:function(){return O.utc(this).format("YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var t=this;return[t.year(),t.month(),t.date(),t.hours(),t.minutes(),t.seconds(),t.milliseconds()]},isValid:function(){return null==this._isValid&&(this._isValid=this._a?!l(this._a,(this._isUTC?O.utc(this._a):O(this._a)).toArray()):!isNaN(this._d.getTime())),!!this._isValid},utc:function(){return this._isUTC=!0,this
+},local:function(){return this._isUTC=!1,this},format:function(t){var e=g(this,t||O.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var i;return i="string"==typeof t?O.duration(+e,t):O.duration(t,e),u(this,i,1),this},subtract:function(t,e){var i;return i="string"==typeof t?O.duration(+e,t):O.duration(t,e),u(this,i,-1),this},diff:function(t,e,i){var n,o,s=this._isUTC?O(t).utc():O(t).local(),r=6e4*(this.zone()-s.zone());return e&&(e=e.replace(/s$/,"")),"year"===e||"month"===e?(n=432e5*(this.daysInMonth()+s.daysInMonth()),o=12*(this.year()-s.year())+(this.month()-s.month()),o+=(this-O(this).startOf("month")-(s-O(s).startOf("month")))/n,"year"===e&&(o/=12)):(n=this-s-r,o="second"===e?n/1e3:"minute"===e?n/6e4:"hour"===e?n/36e5:"day"===e?n/864e5:"week"===e?n/6048e5:n),i?o:a(o)},from:function(t,e){return O.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(O(),t)},calendar:function(){var t=this.diff(O().startOf("day"),"days",!0),e=-6>t?"sameElse":-1>t?"lastWeek":0>t?"lastDay":1>t?"sameDay":2>t?"nextDay":7>t?"nextWeek":"sameElse";return this.format(this.lang().calendar(e,this))},isLeapYear:function(){var t=this.year();return 0===t%4&&0!==t%100||0===t%400},isDST:function(){return this.zone()+O(e).startOf(i)},isBefore:function(e,i){return i=i!==t?i:"millisecond",+this.clone().startOf(i)<+O(e).startOf(i)},isSame:function(e,i){return i=i!==t?i:"millisecond",+this.clone().startOf(i)===+O(e).startOf(i)},zone:function(){return this._isUTC?0:this._d.getTimezoneOffset()},daysInMonth:function(){return O.utc([this.year(),this.month()+1,0]).date()},dayOfYear:function(t){var e=I((O(this).startOf("day")-O(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},isoWeek:function(t){var e=D(this,1,4);return null==t?e:this.add("d",7*(t-e))},week:function(t){var e=this.lang().week(this);return null==t?e:this.add("d",7*(t-e))},lang:function(e){return e===t?this._lang:(this._lang=d(e),this)}},N=0;G.length>N;N++)L(G[N].toLowerCase().replace(/s$/,""),G[N]);L("year","FullYear"),O.fn.days=O.fn.day,O.fn.weeks=O.fn.week,O.fn.isoWeeks=O.fn.isoWeek,O.duration.fn=s.prototype={weeks:function(){return a(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+2592e6*this._months},humanize:function(t){var e=+this,i=C(e,!t,this.lang());return t&&(i=this.lang().pastFuture(e,i)),this.lang().postformat(i)},lang:O.fn.lang};for(N in te)te.hasOwnProperty(N)&&(x(N,te[N]),A(N.toLowerCase()));x("Weeks",6048e5),O.lang("en",{ordinal:function(t){var e=t%10,i=1===~~(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th";return t+i}}),k&&(module.exports=O),"undefined"==typeof ender&&(this.moment=O),"function"==typeof define&&define.amd&&define("moment",[],function(){return O})}.call(this);
\ No newline at end of file
diff --git a/lib/moment.js b/lib/moment.js
new file mode 100644
index 00000000..9ff57aac
--- /dev/null
+++ b/lib/moment.js
@@ -0,0 +1,1400 @@
+// moment.js
+// version : 2.0.0
+// author : Tim Wood
+// license : MIT
+// momentjs.com
+
+(function (undefined) {
+
+ /************************************
+ Constants
+ ************************************/
+
+ var moment,
+ VERSION = "2.0.0",
+ round = Math.round, i,
+ // internal storage for language config files
+ languages = {},
+
+ // check for nodeJS
+ hasModule = (typeof module !== 'undefined' && module.exports),
+
+ // ASP.NET json date format regex
+ aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
+
+ // format tokens
+ formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g,
+ localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
+
+ // parsing tokens
+ parseMultipleFormatChunker = /([0-9a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)/gi,
+
+ // parsing token regexes
+ parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
+ parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
+ parseTokenThreeDigits = /\d{3}/, // 000 - 999
+ parseTokenFourDigits = /\d{1,4}/, // 0 - 9999
+ parseTokenSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
+ parseTokenWord = /[0-9]*[a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF]+\s*?[\u0600-\u06FF]+/i, // any word (or two) characters or numbers including two word month in arabic.
+ parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/i, // +00:00 -00:00 +0000 -0000 or Z
+ parseTokenT = /T/i, // T (ISO seperator)
+ parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
+
+ // preliminary iso regex
+ // 0000-00-00 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000
+ isoRegex = /^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,
+ isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
+
+ // iso time formats and regexes
+ isoTimes = [
+ ['HH:mm:ss.S', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/],
+ ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
+ ['HH:mm', /(T| )\d\d:\d\d/],
+ ['HH', /(T| )\d\d/]
+ ],
+
+ // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
+ parseTimezoneChunker = /([\+\-]|\d\d)/gi,
+
+ // getter and setter names
+ proxyGettersAndSetters = 'Month|Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
+ unitMillisecondFactors = {
+ 'Milliseconds' : 1,
+ 'Seconds' : 1e3,
+ 'Minutes' : 6e4,
+ 'Hours' : 36e5,
+ 'Days' : 864e5,
+ 'Months' : 2592e6,
+ 'Years' : 31536e6
+ },
+
+ // format function strings
+ formatFunctions = {},
+
+ // tokens to ordinalize and pad
+ ordinalizeTokens = 'DDD w W M D d'.split(' '),
+ paddedTokens = 'M D H h m s w W'.split(' '),
+
+ formatTokenFunctions = {
+ M : function () {
+ return this.month() + 1;
+ },
+ MMM : function (format) {
+ return this.lang().monthsShort(this, format);
+ },
+ MMMM : function (format) {
+ return this.lang().months(this, format);
+ },
+ D : function () {
+ return this.date();
+ },
+ DDD : function () {
+ return this.dayOfYear();
+ },
+ d : function () {
+ return this.day();
+ },
+ dd : function (format) {
+ return this.lang().weekdaysMin(this, format);
+ },
+ ddd : function (format) {
+ return this.lang().weekdaysShort(this, format);
+ },
+ dddd : function (format) {
+ return this.lang().weekdays(this, format);
+ },
+ w : function () {
+ return this.week();
+ },
+ W : function () {
+ return this.isoWeek();
+ },
+ YY : function () {
+ return leftZeroFill(this.year() % 100, 2);
+ },
+ YYYY : function () {
+ return leftZeroFill(this.year(), 4);
+ },
+ YYYYY : function () {
+ return leftZeroFill(this.year(), 5);
+ },
+ a : function () {
+ return this.lang().meridiem(this.hours(), this.minutes(), true);
+ },
+ A : function () {
+ return this.lang().meridiem(this.hours(), this.minutes(), false);
+ },
+ 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 ~~(this.milliseconds() / 100);
+ },
+ SS : function () {
+ return leftZeroFill(~~(this.milliseconds() / 10), 2);
+ },
+ SSS : function () {
+ return leftZeroFill(this.milliseconds(), 3);
+ },
+ Z : function () {
+ var a = -this.zone(),
+ b = "+";
+ if (a < 0) {
+ a = -a;
+ b = "-";
+ }
+ return b + leftZeroFill(~~(a / 60), 2) + ":" + leftZeroFill(~~a % 60, 2);
+ },
+ ZZ : function () {
+ var a = -this.zone(),
+ b = "+";
+ if (a < 0) {
+ a = -a;
+ b = "-";
+ }
+ return b + leftZeroFill(~~(10 * a / 6), 4);
+ },
+ X : function () {
+ return this.unix();
+ }
+ };
+
+ function padToken(func, count) {
+ return function (a) {
+ return leftZeroFill(func.call(this, a), count);
+ };
+ }
+ function ordinalizeToken(func) {
+ return function (a) {
+ return this.lang().ordinal(func.call(this, a));
+ };
+ }
+
+ while (ordinalizeTokens.length) {
+ i = ordinalizeTokens.pop();
+ formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i]);
+ }
+ while (paddedTokens.length) {
+ i = paddedTokens.pop();
+ formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
+ }
+ formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
+
+
+ /************************************
+ Constructors
+ ************************************/
+
+ function Language() {
+
+ }
+
+ // Moment prototype object
+ function Moment(config) {
+ extend(this, config);
+ }
+
+ // Duration Constructor
+ function Duration(duration) {
+ var data = this._data = {},
+ years = duration.years || duration.year || duration.y || 0,
+ months = duration.months || duration.month || duration.M || 0,
+ weeks = duration.weeks || duration.week || duration.w || 0,
+ days = duration.days || duration.day || duration.d || 0,
+ hours = duration.hours || duration.hour || duration.h || 0,
+ minutes = duration.minutes || duration.minute || duration.m || 0,
+ seconds = duration.seconds || duration.second || duration.s || 0,
+ milliseconds = duration.milliseconds || duration.millisecond || duration.ms || 0;
+
+ // representation for dateAddRemove
+ this._milliseconds = milliseconds +
+ seconds * 1e3 + // 1000
+ minutes * 6e4 + // 1000 * 60
+ hours * 36e5; // 1000 * 60 * 60
+ // Because of dateAddRemove treats 24 hours as different from a
+ // day when working around DST, we need to store them separately
+ this._days = days +
+ weeks * 7;
+ // It is impossible translate months into days without knowing
+ // which months you are are talking about, so we have to store
+ // it separately.
+ this._months = months +
+ years * 12;
+
+ // The following code bubbles up values, see the tests for
+ // examples of what that means.
+ data.milliseconds = milliseconds % 1000;
+ seconds += absRound(milliseconds / 1000);
+
+ data.seconds = seconds % 60;
+ minutes += absRound(seconds / 60);
+
+ data.minutes = minutes % 60;
+ hours += absRound(minutes / 60);
+
+ data.hours = hours % 24;
+ days += absRound(hours / 24);
+
+ days += weeks * 7;
+ data.days = days % 30;
+
+ months += absRound(days / 30);
+
+ data.months = months % 12;
+ years += absRound(months / 12);
+
+ data.years = years;
+ }
+
+
+ /************************************
+ Helpers
+ ************************************/
+
+
+ function extend(a, b) {
+ for (var i in b) {
+ if (b.hasOwnProperty(i)) {
+ a[i] = b[i];
+ }
+ }
+ return a;
+ }
+
+ function absRound(number) {
+ if (number < 0) {
+ return Math.ceil(number);
+ } else {
+ return Math.floor(number);
+ }
+ }
+
+ // left zero fill a number
+ // see http://jsperf.com/left-zero-filling for performance comparison
+ function leftZeroFill(number, targetLength) {
+ var output = number + '';
+ while (output.length < targetLength) {
+ output = '0' + output;
+ }
+ return output;
+ }
+
+ // helper function for _.addTime and _.subtractTime
+ function addOrSubtractDurationFromMoment(mom, duration, isAdding) {
+ var ms = duration._milliseconds,
+ d = duration._days,
+ M = duration._months,
+ currentDate;
+
+ if (ms) {
+ mom._d.setTime(+mom + ms * isAdding);
+ }
+ if (d) {
+ mom.date(mom.date() + d * isAdding);
+ }
+ if (M) {
+ currentDate = mom.date();
+ mom.date(1)
+ .month(mom.month() + M * isAdding)
+ .date(Math.min(currentDate, mom.daysInMonth()));
+ }
+ }
+
+ // check if is an array
+ function isArray(input) {
+ return Object.prototype.toString.call(input) === '[object Array]';
+ }
+
+ // compare two arrays, return the number of differences
+ function compareArrays(array1, array2) {
+ var len = Math.min(array1.length, array2.length),
+ lengthDiff = Math.abs(array1.length - array2.length),
+ diffs = 0,
+ i;
+ for (i = 0; i < len; i++) {
+ if (~~array1[i] !== ~~array2[i]) {
+ diffs++;
+ }
+ }
+ return diffs + lengthDiff;
+ }
+
+
+ /************************************
+ Languages
+ ************************************/
+
+
+ Language.prototype = {
+ set : function (config) {
+ var prop, i;
+ for (i in config) {
+ prop = config[i];
+ if (typeof prop === 'function') {
+ this[i] = prop;
+ } else {
+ this['_' + i] = prop;
+ }
+ }
+ },
+
+ _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
+ months : function (m) {
+ return this._months[m.month()];
+ },
+
+ _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
+ monthsShort : function (m) {
+ return this._monthsShort[m.month()];
+ },
+
+ monthsParse : function (monthName) {
+ var i, mom, regex, output;
+
+ if (!this._monthsParse) {
+ this._monthsParse = [];
+ }
+
+ for (i = 0; i < 12; i++) {
+ // make the regex if we don't have it already
+ if (!this._monthsParse[i]) {
+ mom = moment([2000, i]);
+ regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
+ this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
+ }
+ // test the regex
+ if (this._monthsParse[i].test(monthName)) {
+ return i;
+ }
+ }
+ },
+
+ _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
+ weekdays : function (m) {
+ return this._weekdays[m.day()];
+ },
+
+ _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
+ weekdaysShort : function (m) {
+ return this._weekdaysShort[m.day()];
+ },
+
+ _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
+ weekdaysMin : function (m) {
+ return this._weekdaysMin[m.day()];
+ },
+
+ _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 (key) {
+ var output = this._longDateFormat[key];
+ if (!output && this._longDateFormat[key.toUpperCase()]) {
+ output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
+ return val.slice(1);
+ });
+ this._longDateFormat[key] = output;
+ }
+ return output;
+ },
+
+ meridiem : function (hours, minutes, isLower) {
+ if (hours > 11) {
+ return isLower ? 'pm' : 'PM';
+ } else {
+ return isLower ? '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 (key, mom) {
+ var output = this._calendar[key];
+ return typeof output === 'function' ? output.apply(mom) : output;
+ },
+
+ _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 (number, withoutSuffix, string, isFuture) {
+ var output = this._relativeTime[string];
+ return (typeof output === 'function') ?
+ output(number, withoutSuffix, string, isFuture) :
+ output.replace(/%d/i, number);
+ },
+ pastFuture : function (diff, output) {
+ var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
+ return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
+ },
+
+ ordinal : function (number) {
+ return this._ordinal.replace("%d", number);
+ },
+ _ordinal : "%d",
+
+ preparse : function (string) {
+ return string;
+ },
+
+ postformat : function (string) {
+ return string;
+ },
+
+ week : function (mom) {
+ return weekOfYear(mom, this._week.dow, this._week.doy);
+ },
+ _week : {
+ dow : 0, // Sunday is the first day of the week.
+ doy : 6 // The week that contains Jan 1st is the first week of the year.
+ }
+ };
+
+ // Loads a language definition into the `languages` cache. The function
+ // takes a key and optionally values. If not in the browser and no values
+ // are provided, it will load the language file module. As a convenience,
+ // this function also returns the language values.
+ function loadLang(key, values) {
+ values.abbr = key;
+ if (!languages[key]) {
+ languages[key] = new Language();
+ }
+ languages[key].set(values);
+ return languages[key];
+ }
+
+ // Determines which language definition to use and returns it.
+ //
+ // With no parameters, it will return the global language. If you
+ // pass in a language key, such as 'en', it will return the
+ // definition for 'en', so long as 'en' has already been loaded using
+ // moment.lang.
+ function getLangDefinition(key) {
+ if (!key) {
+ return moment.fn._lang;
+ }
+ if (!languages[key] && hasModule) {
+ require('./lang/' + key);
+ }
+ return languages[key];
+ }
+
+
+ /************************************
+ Formatting
+ ************************************/
+
+
+ function removeFormattingTokens(input) {
+ if (input.match(/\[.*\]/)) {
+ return input.replace(/^\[|\]$/g, "");
+ }
+ return input.replace(/\\/g, "");
+ }
+
+ function makeFormatFunction(format) {
+ var array = format.match(formattingTokens), i, length;
+
+ for (i = 0, length = array.length; i < length; i++) {
+ if (formatTokenFunctions[array[i]]) {
+ array[i] = formatTokenFunctions[array[i]];
+ } else {
+ array[i] = removeFormattingTokens(array[i]);
+ }
+ }
+
+ return function (mom) {
+ var output = "";
+ for (i = 0; i < length; i++) {
+ output += typeof array[i].call === 'function' ? array[i].call(mom, format) : array[i];
+ }
+ return output;
+ };
+ }
+
+ // format date using native date object
+ function formatMoment(m, format) {
+ var i = 5;
+
+ function replaceLongDateFormatTokens(input) {
+ return m.lang().longDateFormat(input) || input;
+ }
+
+ while (i-- && localFormattingTokens.test(format)) {
+ format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
+ }
+
+ if (!formatFunctions[format]) {
+ formatFunctions[format] = makeFormatFunction(format);
+ }
+
+ return formatFunctions[format](m);
+ }
+
+
+ /************************************
+ Parsing
+ ************************************/
+
+
+ // get the regex to find the next token
+ function getParseRegexForToken(token) {
+ switch (token) {
+ case 'DDDD':
+ return parseTokenThreeDigits;
+ case 'YYYY':
+ return parseTokenFourDigits;
+ case 'YYYYY':
+ return parseTokenSixDigits;
+ case 'S':
+ case 'SS':
+ case 'SSS':
+ case 'DDD':
+ return parseTokenOneToThreeDigits;
+ case 'MMM':
+ case 'MMMM':
+ case 'dd':
+ case 'ddd':
+ case 'dddd':
+ case 'a':
+ case 'A':
+ return parseTokenWord;
+ case 'X':
+ return parseTokenTimestampMs;
+ case 'Z':
+ case 'ZZ':
+ return parseTokenTimezone;
+ case 'T':
+ return parseTokenT;
+ case 'MM':
+ case 'DD':
+ case 'YY':
+ case 'HH':
+ case 'hh':
+ case 'mm':
+ case 'ss':
+ case 'M':
+ case 'D':
+ case 'd':
+ case 'H':
+ case 'h':
+ case 'm':
+ case 's':
+ return parseTokenOneOrTwoDigits;
+ default :
+ return new RegExp(token.replace('\\', ''));
+ }
+ }
+
+ // function to convert string input to date
+ function addTimeToArrayFromToken(token, input, config) {
+ var a, b,
+ datePartArray = config._a;
+
+ switch (token) {
+ // MONTH
+ case 'M' : // fall through to MM
+ case 'MM' :
+ datePartArray[1] = (input == null) ? 0 : ~~input - 1;
+ break;
+ case 'MMM' : // fall through to MMMM
+ case 'MMMM' :
+ a = getLangDefinition(config._l).monthsParse(input);
+ // if we didn't find a month name, mark the date as invalid.
+ if (a != null) {
+ datePartArray[1] = a;
+ } else {
+ config._isValid = false;
+ }
+ break;
+ // DAY OF MONTH
+ case 'D' : // fall through to DDDD
+ case 'DD' : // fall through to DDDD
+ case 'DDD' : // fall through to DDDD
+ case 'DDDD' :
+ if (input != null) {
+ datePartArray[2] = ~~input;
+ }
+ break;
+ // YEAR
+ case 'YY' :
+ datePartArray[0] = ~~input + (~~input > 68 ? 1900 : 2000);
+ break;
+ case 'YYYY' :
+ case 'YYYYY' :
+ datePartArray[0] = ~~input;
+ break;
+ // AM / PM
+ case 'a' : // fall through to A
+ case 'A' :
+ config._isPm = ((input + '').toLowerCase() === 'pm');
+ break;
+ // 24 HOUR
+ case 'H' : // fall through to hh
+ case 'HH' : // fall through to hh
+ case 'h' : // fall through to hh
+ case 'hh' :
+ datePartArray[3] = ~~input;
+ break;
+ // MINUTE
+ case 'm' : // fall through to mm
+ case 'mm' :
+ datePartArray[4] = ~~input;
+ break;
+ // SECOND
+ case 's' : // fall through to ss
+ case 'ss' :
+ datePartArray[5] = ~~input;
+ break;
+ // MILLISECOND
+ case 'S' :
+ case 'SS' :
+ case 'SSS' :
+ datePartArray[6] = ~~ (('0.' + input) * 1000);
+ break;
+ // UNIX TIMESTAMP WITH MS
+ case 'X':
+ config._d = new Date(parseFloat(input) * 1000);
+ break;
+ // TIMEZONE
+ case 'Z' : // fall through to ZZ
+ case 'ZZ' :
+ config._useUTC = true;
+ a = (input + '').match(parseTimezoneChunker);
+ if (a && a[1]) {
+ config._tzh = ~~a[1];
+ }
+ if (a && a[2]) {
+ config._tzm = ~~a[2];
+ }
+ // reverse offsets
+ if (a && a[0] === '+') {
+ config._tzh = -config._tzh;
+ config._tzm = -config._tzm;
+ }
+ break;
+ }
+
+ // if the input is null, the date is not valid
+ if (input == null) {
+ config._isValid = false;
+ }
+ }
+
+ // convert an array to a date.
+ // the array should mirror the parameters below
+ // note: all values past the year are optional and will default to the lowest possible value.
+ // [year, month, day , hour, minute, second, millisecond]
+ function dateFromArray(config) {
+ var i, date, input = [];
+
+ if (config._d) {
+ return;
+ }
+
+ for (i = 0; i < 7; i++) {
+ config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
+ }
+
+ // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
+ input[3] += config._tzh || 0;
+ input[4] += config._tzm || 0;
+
+ date = new Date(0);
+
+ if (config._useUTC) {
+ date.setUTCFullYear(input[0], input[1], input[2]);
+ date.setUTCHours(input[3], input[4], input[5], input[6]);
+ } else {
+ date.setFullYear(input[0], input[1], input[2]);
+ date.setHours(input[3], input[4], input[5], input[6]);
+ }
+
+ config._d = date;
+ }
+
+ // date from string and format string
+ function makeDateFromStringAndFormat(config) {
+ // This array is used to make a Date, either with `new Date` or `Date.UTC`
+ var tokens = config._f.match(formattingTokens),
+ string = config._i,
+ i, parsedInput;
+
+ config._a = [];
+
+ for (i = 0; i < tokens.length; i++) {
+ parsedInput = (getParseRegexForToken(tokens[i]).exec(string) || [])[0];
+ if (parsedInput) {
+ string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
+ }
+ // don't parse if its not a known token
+ if (formatTokenFunctions[tokens[i]]) {
+ addTimeToArrayFromToken(tokens[i], parsedInput, config);
+ }
+ }
+ // handle am pm
+ if (config._isPm && config._a[3] < 12) {
+ config._a[3] += 12;
+ }
+ // if is 12 am, change hours to 0
+ if (config._isPm === false && config._a[3] === 12) {
+ config._a[3] = 0;
+ }
+ // return
+ dateFromArray(config);
+ }
+
+ // date from string and array of format strings
+ function makeDateFromStringAndArray(config) {
+ var tempConfig,
+ tempMoment,
+ bestMoment,
+
+ scoreToBeat = 99,
+ i,
+ currentDate,
+ currentScore;
+
+ while (config._f.length) {
+ tempConfig = extend({}, config);
+ tempConfig._f = config._f.pop();
+ makeDateFromStringAndFormat(tempConfig);
+ tempMoment = new Moment(tempConfig);
+
+ if (tempMoment.isValid()) {
+ bestMoment = tempMoment;
+ break;
+ }
+
+ currentScore = compareArrays(tempConfig._a, tempMoment.toArray());
+
+ if (currentScore < scoreToBeat) {
+ scoreToBeat = currentScore;
+ bestMoment = tempMoment;
+ }
+ }
+
+ extend(config, bestMoment);
+ }
+
+ // date from iso format
+ function makeDateFromString(config) {
+ var i,
+ string = config._i;
+ if (isoRegex.exec(string)) {
+ config._f = 'YYYY-MM-DDT';
+ for (i = 0; i < 4; i++) {
+ if (isoTimes[i][1].exec(string)) {
+ config._f += isoTimes[i][0];
+ break;
+ }
+ }
+ if (parseTokenTimezone.exec(string)) {
+ config._f += " Z";
+ }
+ makeDateFromStringAndFormat(config);
+ } else {
+ config._d = new Date(string);
+ }
+ }
+
+ function makeDateFromInput(config) {
+ var input = config._i,
+ matched = aspNetJsonRegex.exec(input);
+
+ if (input === undefined) {
+ config._d = new Date();
+ } else if (matched) {
+ config._d = new Date(+matched[1]);
+ } else if (typeof input === 'string') {
+ makeDateFromString(config);
+ } else if (isArray(input)) {
+ config._a = input.slice(0);
+ dateFromArray(config);
+ } else {
+ config._d = input instanceof Date ? new Date(+input) : new Date(input);
+ }
+ }
+
+
+ /************************************
+ Relative Time
+ ************************************/
+
+
+ // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
+ function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
+ return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
+ }
+
+ function relativeTime(milliseconds, withoutSuffix, lang) {
+ var seconds = round(Math.abs(milliseconds) / 1000),
+ minutes = round(seconds / 60),
+ hours = round(minutes / 60),
+ days = round(hours / 24),
+ years = round(days / 365),
+ args = seconds < 45 && ['s', seconds] ||
+ minutes === 1 && ['m'] ||
+ minutes < 45 && ['mm', minutes] ||
+ hours === 1 && ['h'] ||
+ hours < 22 && ['hh', hours] ||
+ days === 1 && ['d'] ||
+ days <= 25 && ['dd', days] ||
+ days <= 45 && ['M'] ||
+ days < 345 && ['MM', round(days / 30)] ||
+ years === 1 && ['y'] || ['yy', years];
+ args[2] = withoutSuffix;
+ args[3] = milliseconds > 0;
+ args[4] = lang;
+ return substituteTimeAgo.apply({}, args);
+ }
+
+
+ /************************************
+ Week of Year
+ ************************************/
+
+
+ // firstDayOfWeek 0 = sun, 6 = sat
+ // the day of the week that starts the week
+ // (usually sunday or monday)
+ // firstDayOfWeekOfYear 0 = sun, 6 = sat
+ // the first week is the week that contains the first
+ // of this day of the week
+ // (eg. ISO weeks use thursday (4))
+ function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
+ var end = firstDayOfWeekOfYear - firstDayOfWeek,
+ daysToDayOfWeek = firstDayOfWeekOfYear - mom.day();
+
+
+ if (daysToDayOfWeek > end) {
+ daysToDayOfWeek -= 7;
+ }
+
+ if (daysToDayOfWeek < end - 7) {
+ daysToDayOfWeek += 7;
+ }
+
+ return Math.ceil(moment(mom).add('d', daysToDayOfWeek).dayOfYear() / 7);
+ }
+
+
+ /************************************
+ Top Level Functions
+ ************************************/
+
+ function makeMoment(config) {
+ var input = config._i,
+ format = config._f;
+
+ if (input === null || input === '') {
+ return null;
+ }
+
+ if (typeof input === 'string') {
+ config._i = input = getLangDefinition().preparse(input);
+ }
+
+ if (moment.isMoment(input)) {
+ config = extend({}, input);
+ config._d = new Date(+input._d);
+ } else if (format) {
+ if (isArray(format)) {
+ makeDateFromStringAndArray(config);
+ } else {
+ makeDateFromStringAndFormat(config);
+ }
+ } else {
+ makeDateFromInput(config);
+ }
+
+ return new Moment(config);
+ }
+
+ moment = function (input, format, lang) {
+ return makeMoment({
+ _i : input,
+ _f : format,
+ _l : lang,
+ _isUTC : false
+ });
+ };
+
+ // creating with utc
+ moment.utc = function (input, format, lang) {
+ return makeMoment({
+ _useUTC : true,
+ _isUTC : true,
+ _l : lang,
+ _i : input,
+ _f : format
+ });
+ };
+
+ // creating with unix timestamp (in seconds)
+ moment.unix = function (input) {
+ return moment(input * 1000);
+ };
+
+ // duration
+ moment.duration = function (input, key) {
+ var isDuration = moment.isDuration(input),
+ isNumber = (typeof input === 'number'),
+ duration = (isDuration ? input._data : (isNumber ? {} : input)),
+ ret;
+
+ if (isNumber) {
+ if (key) {
+ duration[key] = input;
+ } else {
+ duration.milliseconds = input;
+ }
+ }
+
+ ret = new Duration(duration);
+
+ if (isDuration && input.hasOwnProperty('_lang')) {
+ ret._lang = input._lang;
+ }
+
+ return ret;
+ };
+
+ // version number
+ moment.version = VERSION;
+
+ // default format
+ moment.defaultFormat = isoFormat;
+
+ // This function will load languages and then set the global language. If
+ // no arguments are passed in, it will simply return the current global
+ // language key.
+ moment.lang = function (key, values) {
+ var i;
+
+ if (!key) {
+ return moment.fn._lang._abbr;
+ }
+ if (values) {
+ loadLang(key, values);
+ } else if (!languages[key]) {
+ getLangDefinition(key);
+ }
+ moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
+ };
+
+ // returns language data
+ moment.langData = function (key) {
+ if (key && key._lang && key._lang._abbr) {
+ key = key._lang._abbr;
+ }
+ return getLangDefinition(key);
+ };
+
+ // compare moment object
+ moment.isMoment = function (obj) {
+ return obj instanceof Moment;
+ };
+
+ // for typechecking Duration objects
+ moment.isDuration = function (obj) {
+ return obj instanceof Duration;
+ };
+
+
+ /************************************
+ Moment Prototype
+ ************************************/
+
+
+ moment.fn = Moment.prototype = {
+
+ clone : function () {
+ return moment(this);
+ },
+
+ valueOf : function () {
+ return +this._d;
+ },
+
+ unix : function () {
+ return Math.floor(+this._d / 1000);
+ },
+
+ toString : function () {
+ return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
+ },
+
+ toDate : function () {
+ return this._d;
+ },
+
+ toJSON : function () {
+ return moment.utc(this).format('YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
+ },
+
+ toArray : function () {
+ var m = this;
+ return [
+ m.year(),
+ m.month(),
+ m.date(),
+ m.hours(),
+ m.minutes(),
+ m.seconds(),
+ m.milliseconds()
+ ];
+ },
+
+ isValid : function () {
+ if (this._isValid == null) {
+ if (this._a) {
+ this._isValid = !compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray());
+ } else {
+ this._isValid = !isNaN(this._d.getTime());
+ }
+ }
+ return !!this._isValid;
+ },
+
+ utc : function () {
+ this._isUTC = true;
+ return this;
+ },
+
+ local : function () {
+ this._isUTC = false;
+ return this;
+ },
+
+ format : function (inputString) {
+ var output = formatMoment(this, inputString || moment.defaultFormat);
+ return this.lang().postformat(output);
+ },
+
+ add : function (input, val) {
+ var dur;
+ // switch args to support add('s', 1) and add(1, 's')
+ if (typeof input === 'string') {
+ dur = moment.duration(+val, input);
+ } else {
+ dur = moment.duration(input, val);
+ }
+ addOrSubtractDurationFromMoment(this, dur, 1);
+ return this;
+ },
+
+ subtract : function (input, val) {
+ var dur;
+ // switch args to support subtract('s', 1) and subtract(1, 's')
+ if (typeof input === 'string') {
+ dur = moment.duration(+val, input);
+ } else {
+ dur = moment.duration(input, val);
+ }
+ addOrSubtractDurationFromMoment(this, dur, -1);
+ return this;
+ },
+
+ diff : function (input, units, asFloat) {
+ var that = this._isUTC ? moment(input).utc() : moment(input).local(),
+ zoneDiff = (this.zone() - that.zone()) * 6e4,
+ diff, output;
+
+ if (units) {
+ // standardize on singular form
+ units = units.replace(/s$/, '');
+ }
+
+ if (units === 'year' || units === 'month') {
+ diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
+ output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
+ output += ((this - moment(this).startOf('month')) - (that - moment(that).startOf('month'))) / diff;
+ if (units === 'year') {
+ output = output / 12;
+ }
+ } else {
+ diff = (this - that) - zoneDiff;
+ output = units === 'second' ? diff / 1e3 : // 1000
+ units === 'minute' ? diff / 6e4 : // 1000 * 60
+ units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
+ units === 'day' ? diff / 864e5 : // 1000 * 60 * 60 * 24
+ units === 'week' ? diff / 6048e5 : // 1000 * 60 * 60 * 24 * 7
+ diff;
+ }
+ return asFloat ? output : absRound(output);
+ },
+
+ from : function (time, withoutSuffix) {
+ return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
+ },
+
+ fromNow : function (withoutSuffix) {
+ return this.from(moment(), withoutSuffix);
+ },
+
+ calendar : function () {
+ var diff = this.diff(moment().startOf('day'), 'days', true),
+ format = diff < -6 ? 'sameElse' :
+ diff < -1 ? 'lastWeek' :
+ diff < 0 ? 'lastDay' :
+ diff < 1 ? 'sameDay' :
+ diff < 2 ? 'nextDay' :
+ diff < 7 ? 'nextWeek' : 'sameElse';
+ return this.format(this.lang().calendar(format, this));
+ },
+
+ isLeapYear : function () {
+ var year = this.year();
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
+ },
+
+ isDST : function () {
+ return (this.zone() < moment([this.year()]).zone() ||
+ this.zone() < moment([this.year(), 5]).zone());
+ },
+
+ day : function (input) {
+ var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
+ return input == null ? day :
+ this.add({ d : input - day });
+ },
+
+ startOf: function (units) {
+ units = units.replace(/s$/, '');
+ // the following switch intentionally omits break keywords
+ // to utilize falling through the cases.
+ switch (units) {
+ case 'year':
+ this.month(0);
+ /* falls through */
+ case 'month':
+ this.date(1);
+ /* falls through */
+ case 'week':
+ case 'day':
+ this.hours(0);
+ /* falls through */
+ case 'hour':
+ this.minutes(0);
+ /* falls through */
+ case 'minute':
+ this.seconds(0);
+ /* falls through */
+ case 'second':
+ this.milliseconds(0);
+ /* falls through */
+ }
+
+ // weeks are a special case
+ if (units === 'week') {
+ this.day(0);
+ }
+
+ return this;
+ },
+
+ endOf: function (units) {
+ return this.startOf(units).add(units.replace(/s?$/, 's'), 1).subtract('ms', 1);
+ },
+
+ isAfter: function (input, units) {
+ units = typeof units !== 'undefined' ? units : 'millisecond';
+ return +this.clone().startOf(units) > +moment(input).startOf(units);
+ },
+
+ isBefore: function (input, units) {
+ units = typeof units !== 'undefined' ? units : 'millisecond';
+ return +this.clone().startOf(units) < +moment(input).startOf(units);
+ },
+
+ isSame: function (input, units) {
+ units = typeof units !== 'undefined' ? units : 'millisecond';
+ return +this.clone().startOf(units) === +moment(input).startOf(units);
+ },
+
+ zone : function () {
+ return this._isUTC ? 0 : this._d.getTimezoneOffset();
+ },
+
+ daysInMonth : function () {
+ return moment.utc([this.year(), this.month() + 1, 0]).date();
+ },
+
+ dayOfYear : function (input) {
+ var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
+ return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
+ },
+
+ isoWeek : function (input) {
+ var week = weekOfYear(this, 1, 4);
+ return input == null ? week : this.add("d", (input - week) * 7);
+ },
+
+ week : function (input) {
+ var week = this.lang().week(this);
+ return input == null ? week : this.add("d", (input - week) * 7);
+ },
+
+ // If passed a language key, it will set the language for this
+ // instance. Otherwise, it will return the language configuration
+ // variables for this instance.
+ lang : function (key) {
+ if (key === undefined) {
+ return this._lang;
+ } else {
+ this._lang = getLangDefinition(key);
+ return this;
+ }
+ }
+ };
+
+ // helper for adding shortcuts
+ function makeGetterAndSetter(name, key) {
+ moment.fn[name] = moment.fn[name + 's'] = function (input) {
+ var utc = this._isUTC ? 'UTC' : '';
+ if (input != null) {
+ this._d['set' + utc + key](input);
+ return this;
+ } else {
+ return this._d['get' + utc + key]();
+ }
+ };
+ }
+
+ // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds)
+ for (i = 0; i < proxyGettersAndSetters.length; i ++) {
+ makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]);
+ }
+
+ // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
+ makeGetterAndSetter('year', 'FullYear');
+
+ // add plural methods
+ moment.fn.days = moment.fn.day;
+ moment.fn.weeks = moment.fn.week;
+ moment.fn.isoWeeks = moment.fn.isoWeek;
+
+ /************************************
+ Duration Prototype
+ ************************************/
+
+
+ moment.duration.fn = Duration.prototype = {
+ weeks : function () {
+ return absRound(this.days() / 7);
+ },
+
+ valueOf : function () {
+ return this._milliseconds +
+ this._days * 864e5 +
+ this._months * 2592e6;
+ },
+
+ humanize : function (withSuffix) {
+ var difference = +this,
+ output = relativeTime(difference, !withSuffix, this.lang());
+
+ if (withSuffix) {
+ output = this.lang().pastFuture(difference, output);
+ }
+
+ return this.lang().postformat(output);
+ },
+
+ lang : moment.fn.lang
+ };
+
+ function makeDurationGetter(name) {
+ moment.duration.fn[name] = function () {
+ return this._data[name];
+ };
+ }
+
+ function makeDurationAsGetter(name, factor) {
+ moment.duration.fn['as' + name] = function () {
+ return +this / factor;
+ };
+ }
+
+ for (i in unitMillisecondFactors) {
+ if (unitMillisecondFactors.hasOwnProperty(i)) {
+ makeDurationAsGetter(i, unitMillisecondFactors[i]);
+ makeDurationGetter(i.toLowerCase());
+ }
+ }
+
+ makeDurationAsGetter('Weeks', 6048e5);
+
+
+ /************************************
+ Default Lang
+ ************************************/
+
+
+ // Set default language, other languages will inherit from English.
+ moment.lang('en', {
+ ordinal : function (number) {
+ var b = number % 10,
+ output = (~~ (number % 100 / 10) === 1) ? 'th' :
+ (b === 1) ? 'st' :
+ (b === 2) ? 'nd' :
+ (b === 3) ? 'rd' : 'th';
+ return number + output;
+ }
+ });
+
+
+ /************************************
+ Exposing Moment
+ ************************************/
+
+
+ // CommonJS module is defined
+ if (hasModule) {
+ module.exports = moment;
+ }
+ /*global ender:false */
+ if (typeof ender === 'undefined') {
+ // here, `this` means `window` in the browser, or `global` on the server
+ // add `moment` as a global object via a string identifier,
+ // for Closure Compiler "advanced" mode
+ this['moment'] = moment;
+ }
+ /*global define:false */
+ if (typeof define === "function" && define.amd) {
+ define("moment", [], function () {
+ return moment;
+ });
+ }
+}).call(this);
diff --git a/lib/moment.min.js b/lib/moment.min.js
new file mode 100644
index 00000000..4e8497a9
--- /dev/null
+++ b/lib/moment.min.js
@@ -0,0 +1,6 @@
+// moment.js
+// version : 2.0.0
+// author : Tim Wood
+// license : MIT
+// momentjs.com
+(function(e){function O(e,t){return function(n){return j(e.call(this,n),t)}}function M(e){return function(t){return this.lang().ordinal(e.call(this,t))}}function _(){}function D(e){H(this,e)}function P(e){var t=this._data={},n=e.years||e.year||e.y||0,r=e.months||e.month||e.M||0,i=e.weeks||e.week||e.w||0,s=e.days||e.day||e.d||0,o=e.hours||e.hour||e.h||0,u=e.minutes||e.minute||e.m||0,a=e.seconds||e.second||e.s||0,f=e.milliseconds||e.millisecond||e.ms||0;this._milliseconds=f+a*1e3+u*6e4+o*36e5,this._days=s+i*7,this._months=r+n*12,t.milliseconds=f%1e3,a+=B(f/1e3),t.seconds=a%60,u+=B(a/60),t.minutes=u%60,o+=B(u/60),t.hours=o%24,s+=B(o/24),s+=i*7,t.days=s%30,r+=B(s/30),t.months=r%12,n+=B(r/12),t.years=n}function H(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}function B(e){return e<0?Math.ceil(e):Math.floor(e)}function j(e,t){var n=e+"";while(n.length68?1900:2e3);break;case"YYYY":case"YYYYY":s[0]=~~t;break;case"a":case"A":n._isPm=(t+"").toLowerCase()==="pm";break;case"H":case"HH":case"h":case"hh":s[3]=~~t;break;case"m":case"mm":s[4]=~~t;break;case"s":case"ss":s[5]=~~t;break;case"S":case"SS":case"SSS":s[6]=~~(("0."+t)*1e3);break;case"X":n._d=new Date(parseFloat(t)*1e3);break;case"Z":case"ZZ":n._useUTC=!0,r=(t+"").match(x),r&&r[1]&&(n._tzh=~~r[1]),r&&r[2]&&(n._tzm=~~r[2]),r&&r[0]==="+"&&(n._tzh=-n._tzh,n._tzm=-n._tzm)}t==null&&(n._isValid=!1)}function J(e){var t,n,r=[];if(e._d)return;for(t=0;t<7;t++)e._a[t]=r[t]=e._a[t]==null?t===2?1:0:e._a[t];r[3]+=e._tzh||0,r[4]+=e._tzm||0,n=new Date(0),e._useUTC?(n.setUTCFullYear(r[0],r[1],r[2]),n.setUTCHours(r[3],r[4],r[5],r[6])):(n.setFullYear(r[0],r[1],r[2]),n.setHours(r[3],r[4],r[5],r[6])),e._d=n}function K(e){var t=e._f.match(a),n=e._i,r,i;e._a=[];for(r=0;r0,f[4]=n,Z.apply({},f)}function tt(e,n,r){var i=r-n,s=r-e.day();return s>i&&(s-=7),s11?n?"pm":"PM":n?"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(e,t){var n=this._calendar[e];return typeof n=="function"?n.apply(t):n},_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(e,t,n,r){var i=this._relativeTime[n];return typeof i=="function"?i(e,t,n,r):i.replace(/%d/i,e)},pastFuture:function(e,t){var n=this._relativeTime[e>0?"future":"past"];return typeof n=="function"?n(t):n.replace(/%s/i,t)},ordinal:function(e){return this._ordinal.replace("%d",e)},_ordinal:"%d",preparse:function(e){return e},postformat:function(e){return e},week:function(e){return tt(e,this._week.dow,this._week.doy)},_week:{dow:0,doy:6}},t=function(e,t,n){return nt({_i:e,_f:t,_l:n,_isUTC:!1})},t.utc=function(e,t,n){return nt({_useUTC:!0,_isUTC:!0,_l:n,_i:e,_f:t})},t.unix=function(e){return t(e*1e3)},t.duration=function(e,n){var r=t.isDuration(e),i=typeof e=="number",s=r?e._data:i?{}:e,o;return i&&(n?s[n]=e:s.milliseconds=e),o=new P(s),r&&e.hasOwnProperty("_lang")&&(o._lang=e._lang),o},t.version=n,t.defaultFormat=E,t.lang=function(e,n){var r;if(!e)return t.fn._lang._abbr;n?R(e,n):s[e]||U(e),t.duration.fn._lang=t.fn._lang=U(e)},t.langData=function(e){return e&&e._lang&&e._lang._abbr&&(e=e._lang._abbr),U(e)},t.isMoment=function(e){return e instanceof D},t.isDuration=function(e){return e instanceof P},t.fn=D.prototype={clone:function(){return t(this)},valueOf:function(){return+this._d},unix:function(){return Math.floor(+this._d/1e3)},toString:function(){return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._d},toJSON:function(){return t.utc(this).format("YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var e=this;return[e.year(),e.month(),e.date(),e.hours(),e.minutes(),e.seconds(),e.milliseconds()]},isValid:function(){return this._isValid==null&&(this._a?this._isValid=!q(this._a,(this._isUTC?t.utc(this._a):t(this._a)).toArray()):this._isValid=!isNaN(this._d.getTime())),!!this._isValid},utc:function(){return this._isUTC=!0,this},local:function(){return this._isUTC=!1,this},format:function(e){var n=X(this,e||t.defaultFormat);return this.lang().postformat(n)},add:function(e,n){var r;return typeof e=="string"?r=t.duration(+n,e):r=t.duration(e,n),F(this,r,1),this},subtract:function(e,n){var r;return typeof e=="string"?r=t.duration(+n,e):r=t.duration(e,n),F(this,r,-1),this},diff:function(e,n,r){var i=this._isUTC?t(e).utc():t(e).local(),s=(this.zone()-i.zone())*6e4,o,u;return n&&(n=n.replace(/s$/,"")),n==="year"||n==="month"?(o=(this.daysInMonth()+i.daysInMonth())*432e5,u=(this.year()-i.year())*12+(this.month()-i.month()),u+=(this-t(this).startOf("month")-(i-t(i).startOf("month")))/o,n==="year"&&(u/=12)):(o=this-i-s,u=n==="second"?o/1e3:n==="minute"?o/6e4:n==="hour"?o/36e5:n==="day"?o/864e5:n==="week"?o/6048e5:o),r?u:B(u)},from:function(e,n){return t.duration(this.diff(e)).lang(this.lang()._abbr).humanize(!n)},fromNow:function(e){return this.from(t(),e)},calendar:function(){var e=this.diff(t().startOf("day"),"days",!0),n=e<-6?"sameElse":e<-1?"lastWeek":e<0?"lastDay":e<1?"sameDay":e<2?"nextDay":e<7?"nextWeek":"sameElse";return this.format(this.lang().calendar(n,this))},isLeapYear:function(){var e=this.year();return e%4===0&&e%100!==0||e%400===0},isDST:function(){return this.zone()+t(e).startOf(n)},isBefore:function(e,n){return n=typeof n!="undefined"?n:"millisecond",+this.clone().startOf(n)<+t(e).startOf(n)},isSame:function(e,n){return n=typeof n!="undefined"?n:"millisecond",+this.clone().startOf(n)===+t(e).startOf(n)},zone:function(){return this._isUTC?0:this._d.getTimezoneOffset()},daysInMonth:function(){return t.utc([this.year(),this.month()+1,0]).date()},dayOfYear:function(e){var n=r((t(this).startOf("day")-t(this).startOf("year"))/864e5)+1;return e==null?n:this.add("d",e-n)},isoWeek:function(e){var t=tt(this,1,4);return e==null?t:this.add("d",(e-t)*7)},week:function(e){var t=this.lang().week(this);return e==null?t:this.add("d",(e-t)*7)},lang:function(t){return t===e?this._lang:(this._lang=U(t),this)}};for(i=0;i= 0.5.9",
+ "jake-utils": ">= 0.0.18",
+ "uglify-js": ">= 2.2.5",
+ "nodeunit": ">= 0.7.4",
+ "dateable": ">= 0.1.2"
+ }
+}
diff --git a/src/component/component.js b/src/component/component.js
new file mode 100644
index 00000000..a9007549
--- /dev/null
+++ b/src/component/component.js
@@ -0,0 +1,116 @@
+/**
+ * Prototype for visual components
+ */
+function Component () {
+ this.id = null;
+ this.parent = null;
+ this.depends = null;
+ this.controller = null;
+ this.options = null;
+
+ this.frame = null; // main DOM element
+ this.top = 0;
+ this.left = 0;
+ this.width = 0;
+ this.height = 0;
+}
+
+/**
+ * Set parameters for the frame. Parameters will be merged in current parameter
+ * set.
+ * @param {Object} options Available parameters:
+ * {String | function} [className]
+ * {String | Number | function} [left]
+ * {String | Number | function} [top]
+ * {String | Number | function} [width]
+ * {String | Number | function} [height]
+ */
+Component.prototype.setOptions = function(options) {
+ if (options) {
+ util.extend(this.options, options);
+ }
+
+ if (this.controller) {
+ this.requestRepaint();
+ this.requestReflow();
+ }
+};
+
+/**
+ * Get the container element of the component, which can be used by a child to
+ * add its own widgets. Not all components do have a container for childs, in
+ * that case null is returned.
+ * @returns {HTMLElement | null} container
+ */
+Component.prototype.getContainer = function () {
+ // should be implemented by the component
+ return null;
+};
+
+/**
+ * Get the frame element of the component, the outer HTML DOM element.
+ * @returns {HTMLElement | null} frame
+ */
+Component.prototype.getFrame = function () {
+ return this.frame;
+};
+
+/**
+ * Repaint the component
+ * @return {Boolean} changed
+ */
+Component.prototype.repaint = function () {
+ // should be implemented by the component
+ return false;
+};
+
+/**
+ * Reflow the component
+ * @return {Boolean} resized
+ */
+Component.prototype.reflow = function () {
+ // should be implemented by the component
+ return false;
+};
+
+/**
+ * Request a repaint. The controller will schedule a repaint
+ */
+Component.prototype.requestRepaint = function () {
+ if (this.controller) {
+ this.controller.requestRepaint();
+ }
+ else {
+ throw new Error('Cannot request a repaint: no controller configured');
+ // TODO: just do a repaint when no parent is configured?
+ }
+};
+
+/**
+ * Request a reflow. The controller will schedule a reflow
+ */
+Component.prototype.requestReflow = function () {
+ if (this.controller) {
+ this.controller.requestReflow();
+ }
+ else {
+ throw new Error('Cannot request a reflow: no controller configured');
+ // TODO: just do a reflow when no parent is configured?
+ }
+};
+
+/**
+ * Event handler
+ * @param {String} event name of the event, for example 'click', 'mousemove'
+ * @param {function} callback callback handler, invoked with the raw HTML Event
+ * as parameter.
+ */
+Component.prototype.on = function (event, callback) {
+ // TODO: rethink the way of event delegation
+ if (this.parent) {
+ this.parent.on(event, callback);
+ }
+ else {
+ throw new Error('Cannot attach event: no root panel found');
+ }
+};
diff --git a/src/component/css/item.css b/src/component/css/item.css
new file mode 100644
index 00000000..43c3141e
--- /dev/null
+++ b/src/component/css/item.css
@@ -0,0 +1,84 @@
+
+.graph .itemset {
+ position: absolute;
+}
+
+
+.graph .item {
+ position: absolute;
+ color: #1A1A1A;
+ border-color: #97B0F8;
+ background-color: #D5DDF6;
+ display: inline-block;
+}
+
+.graph .item.selected {
+ border-color: #FFC200;
+ background-color: #FFF785;
+ z-index: 999;
+}
+
+.graph .item.cluster {
+ /* TODO: use another color or pattern? */
+ background: #97B0F8 url('img/cluster_bg.png');
+ color: white;
+}
+.graph .item.cluster.point {
+ border-color: #D5DDF6;
+}
+
+.graph .item.box {
+ text-align: center;
+ border-style: solid;
+ border-width: 1px;
+ border-radius: 5px;
+ -moz-border-radius: 5px; /* For Firefox 3.6 and older */
+}
+
+.graph .item.point {
+ background: none;
+}
+
+.graph .dot {
+ border: 5px solid #97B0F8;
+ position: absolute;
+ border-radius: 5px;
+ -moz-border-radius: 5px; /* For Firefox 3.6 and older */
+}
+
+.graph .item.range {
+ overflow: hidden;
+ border-style: solid;
+ border-width: 1px;
+ border-radius: 2px;
+ -moz-border-radius: 2px; /* For Firefox 3.6 and older */
+}
+
+.graph .item.range .drag-left {
+ cursor: w-resize;
+ z-index: 1000;
+}
+
+.graph .item.range .drag-right {
+ cursor: e-resize;
+ z-index: 1000;
+}
+
+.graph .item.range .content {
+ position: relative;
+ display: inline-block;
+}
+
+.graph .item.line {
+ position: absolute;
+ width: 0;
+ border-left-width: 1px;
+ border-left-style: solid;
+ z-index: -1;
+}
+
+.graph .item .content {
+ margin: 5px;
+ white-space: nowrap;
+ overflow: hidden;
+}
diff --git a/src/component/css/panel.css b/src/component/css/panel.css
new file mode 100644
index 00000000..94b6c2c9
--- /dev/null
+++ b/src/component/css/panel.css
@@ -0,0 +1,9 @@
+
+.graph {
+ position: relative;
+ border: 1px solid #bfbfbf;
+}
+
+.graph .panel {
+ position: absolute;
+}
\ No newline at end of file
diff --git a/src/component/css/timeaxis.css b/src/component/css/timeaxis.css
new file mode 100644
index 00000000..b9eecb9e
--- /dev/null
+++ b/src/component/css/timeaxis.css
@@ -0,0 +1,47 @@
+/* TODO: better css name, 'graph' is way to generic */
+
+.graph {
+ overflow: hidden;
+}
+
+.graph .axis {
+ position: relative;
+}
+
+.graph .axis .text {
+ position: absolute;
+ color: #4d4d4d;
+ padding: 3px;
+ white-space: nowrap;
+}
+
+.graph .axis .text.measure {
+ position: absolute;
+ padding-left: 0;
+ padding-right: 0;
+ margin-left: 0;
+ margin-right: 0;
+ visibility: hidden;
+}
+
+.graph .axis .grid.vertical {
+ position: absolute;
+ width: 0;
+ border-right: 1px solid;
+}
+
+.graph .axis .grid.horizontal {
+ position: absolute;
+ left: 0;
+ width: 100%;
+ height: 0;
+ border-bottom: 1px solid;
+}
+
+.graph .axis .grid.minor {
+ border-color: #e5e5e5;
+}
+
+.graph .axis .grid.major {
+ border-color: #bfbfbf;
+}
diff --git a/src/component/item/item.js b/src/component/item/item.js
new file mode 100644
index 00000000..0639ee25
--- /dev/null
+++ b/src/component/item/item.js
@@ -0,0 +1,36 @@
+
+/**
+ * @constructor Item
+ * @param {ItemSet} parent
+ * @param {Object} data Object containing (optional) parameters type,
+ * start, end, content, group, className.
+ * @param {Object} [options] Options to set initial property values
+ * // TODO: describe available options
+ */
+function Item (parent, data, options) {
+ this.parent = parent;
+ this.data = data;
+ this.selected = false;
+ this.visible = true;
+ this.dom = null;
+ this.options = options;
+}
+
+Item.prototype = new Component();
+
+/**
+ * Select current item
+ */
+Item.prototype.select = function () {
+ this.selected = true;
+};
+
+/**
+ * Unselect current item
+ */
+Item.prototype.unselect = function () {
+ this.selected = false;
+};
+
+// create a namespace for all item types
+var itemTypes = {};
\ No newline at end of file
diff --git a/src/component/item/itembox.js b/src/component/item/itembox.js
new file mode 100644
index 00000000..c2fc74ca
--- /dev/null
+++ b/src/component/item/itembox.js
@@ -0,0 +1,270 @@
+/**
+ * @constructor ItemBox
+ * @extends Item
+ * @param {ItemSet} parent
+ * @param {Object} data Object containing parameters start
+ * content, className.
+ * @param {Object} [options] Options to set initial property values
+ * // TODO: describe available options
+ */
+function ItemBox (parent, data, options) {
+ this.props = {
+ dot: {
+ left: 0,
+ top: 0,
+ width: 0,
+ height: 0
+ },
+ line: {
+ top: 0,
+ left: 0,
+ width: 0,
+ height: 0
+ }
+ };
+
+ Item.call(this, parent, data, options);
+}
+
+ItemBox.prototype = new Item (null, null);
+
+// register the ItemBox in the item types
+itemTypes['box'] = ItemBox;
+
+/**
+ * Select the item
+ * @override
+ */
+ItemBox.prototype.select = function () {
+ this.selected = true;
+ // TODO: select and unselect
+};
+
+/**
+ * Unselect the item
+ * @override
+ */
+ItemBox.prototype.unselect = function () {
+ this.selected = false;
+ // TODO: select and unselect
+};
+
+/**
+ * Repaint the item
+ * @return {Boolean} changed
+ */
+ItemBox.prototype.repaint = function () {
+ // TODO: make an efficient repaint
+ var changed = false;
+ var dom = this.dom;
+
+ if (this.visible) {
+ if (!dom) {
+ this._create();
+ changed = true;
+ }
+ dom = this.dom;
+
+ if (dom) {
+ if (!this.options && !this.parent) {
+ throw new Error('Cannot repaint item: no parent attached');
+ }
+ var parentContainer = this.parent.getContainer();
+ if (!parentContainer) {
+ throw new Error('Cannot repaint time axis: parent has no container element');
+ }
+
+ if (!dom.box.parentNode) {
+ parentContainer.appendChild(dom.box);
+ changed = true;
+ }
+ if (!dom.line.parentNode) {
+ parentContainer.appendChild(dom.line);
+ changed = true;
+ }
+ if (!dom.dot.parentNode) {
+ parentContainer.appendChild(dom.dot);
+ changed = true;
+ }
+
+ // update contents
+ if (this.data.content != this.content) {
+ this.content = this.data.content;
+ if (this.content instanceof Element) {
+ dom.content.innerHTML = '';
+ dom.content.appendChild(this.content);
+ }
+ else if (this.data.content != undefined) {
+ dom.content.innerHTML = this.content;
+ }
+ else {
+ throw new Error('Property "content" missing in item ' + this.data.id);
+ }
+ changed = true;
+ }
+
+ // update class
+ var className = (this.data.className? ' ' + this.data.className : '') +
+ (this.selected ? ' selected' : '');
+ if (this.className != className) {
+ this.className = className;
+ dom.box.className = 'item box' + className;
+ dom.line.className = 'item line' + className;
+ dom.dot.className = 'item dot' + className;
+ changed = true;
+ }
+ }
+ }
+ else {
+ // hide when visible
+ if (dom) {
+ if (dom.box.parentNode) {
+ dom.box.parentNode.removeChild(dom.box);
+ changed = true;
+ }
+ if (dom.line.parentNode) {
+ dom.line.parentNode.removeChild(dom.line);
+ changed = true;
+ }
+ if (dom.dot.parentNode) {
+ dom.dot.parentNode.removeChild(dom.dot);
+ changed = true;
+ }
+ }
+ }
+
+ return changed;
+};
+
+/**
+ * Reflow the item: calculate its actual size and position from the DOM
+ * @return {boolean} resized returns true if the axis is resized
+ * @override
+ */
+ItemBox.prototype.reflow = function () {
+ if (this.data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + this.data.id);
+ }
+
+ var update = util.updateProperty,
+ dom = this.dom,
+ props = this.props,
+ options = this.options,
+ start = this.parent.toScreen(this.data.start),
+ align = options && options.align,
+ orientation = options.orientation,
+ changed = 0,
+ top,
+ left;
+
+ if (dom) {
+ changed += update(props.dot, 'height', dom.dot.offsetHeight);
+ changed += update(props.dot, 'width', dom.dot.offsetWidth);
+ changed += update(props.line, 'width', dom.line.offsetWidth);
+ changed += update(props.line, 'width', dom.line.offsetWidth);
+ changed += update(this, 'width', dom.box.offsetWidth);
+ changed += update(this, 'height', dom.box.offsetHeight);
+
+ if (align == 'right') {
+ left = start - this.width;
+ }
+ else if (align == 'left') {
+ left = start;
+ }
+ else {
+ // default or 'center'
+ left = start - this.width / 2;
+ }
+ changed += update(this, 'left', left);
+
+ changed += update(props.line, 'left', start - props.line.width / 2);
+ changed += update(props.dot, 'left', start - props.dot.width / 2);
+ if (orientation == 'top') {
+ top = options.margin.axis;
+
+ changed += update(this, 'top', top);
+ changed += update(props.line, 'top', 0);
+ changed += update(props.line, 'height', top);
+ changed += update(props.dot, 'top', -props.dot.height / 2);
+ }
+ else {
+ // default or 'bottom'
+ var parentHeight = this.parent.height;
+ top = parentHeight - this.height - options.margin.axis;
+
+ changed += update(this, 'top', top);
+ changed += update(props.line, 'top', top + this.height);
+ changed += update(props.line, 'height', Math.max(options.margin.axis, 0));
+ changed += update(props.dot, 'top', parentHeight - props.dot.height / 2);
+ }
+ }
+ else {
+ changed += 1;
+ }
+
+ return (changed > 0);
+};
+
+/**
+ * Create an items DOM
+ * @private
+ */
+ItemBox.prototype._create = function () {
+ var dom = this.dom;
+ if (!dom) {
+ this.dom = dom = {};
+
+ // create the box
+ dom.box = document.createElement('DIV');
+ // className is updated in repaint()
+
+ // contents box (inside the background box). used for making margins
+ dom.content = document.createElement('DIV');
+ dom.content.className = 'content';
+ dom.box.appendChild(dom.content);
+
+ // line to axis
+ dom.line = document.createElement('DIV');
+ dom.line.className = 'line';
+
+ // dot on axis
+ dom.dot = document.createElement('DIV');
+ dom.dot.className = 'dot';
+ }
+};
+
+/**
+ * Reposition the item, recalculate its left, top, and width, using the current
+ * range and size of the items itemset
+ * @override
+ */
+ItemBox.prototype.reposition = function () {
+ var dom = this.dom,
+ props = this.props,
+ orientation = this.options.orientation;
+
+ if (dom) {
+ var box = dom.box,
+ line = dom.line,
+ dot = dom.dot;
+
+ box.style.left = this.left + 'px';
+ box.style.top = this.top + 'px';
+
+ line.style.left = props.line.left + 'px';
+ if (orientation == 'top') {
+ line.style.top = 0 + 'px';
+ line.style.height = this.top + 'px';
+ }
+ else {
+ // orientation 'bottom'
+ line.style.top = props.line.top + 'px';
+ line.style.top = (this.top + this.height) + 'px';
+ line.style.height = (props.dot.top - this.top - this.height) + 'px';
+
+ }
+
+ dot.style.left = props.dot.left + 'px';
+ dot.style.top = props.dot.top + 'px';
+ }
+};
diff --git a/src/component/item/itempoint.js b/src/component/item/itempoint.js
new file mode 100644
index 00000000..9018a3fe
--- /dev/null
+++ b/src/component/item/itempoint.js
@@ -0,0 +1,209 @@
+/**
+ * @constructor ItemPoint
+ * @extends Item
+ * @param {ItemSet} parent
+ * @param {Object} data Object containing parameters start
+ * content, className.
+ * @param {Object} [options] Options to set initial property values
+ * // TODO: describe available options
+ */
+function ItemPoint (parent, data, options) {
+ this.props = {
+ dot: {
+ top: 0,
+ width: 0,
+ height: 0
+ },
+ content: {
+ height: 0,
+ marginLeft: 0
+ }
+ };
+
+ Item.call(this, parent, data, options);
+}
+
+ItemPoint.prototype = new Item (null, null);
+
+// register the ItemPoint in the item types
+itemTypes['point'] = ItemPoint;
+
+/**
+ * Select the item
+ * @override
+ */
+ItemPoint.prototype.select = function () {
+ this.selected = true;
+ // TODO: select and unselect
+};
+
+/**
+ * Unselect the item
+ * @override
+ */
+ItemPoint.prototype.unselect = function () {
+ this.selected = false;
+ // TODO: select and unselect
+};
+
+/**
+ * Repaint the item
+ * @return {Boolean} changed
+ */
+ItemPoint.prototype.repaint = function () {
+ // TODO: make an efficient repaint
+ var changed = false;
+ var dom = this.dom;
+
+ if (this.visible) {
+ if (!dom) {
+ this._create();
+ changed = true;
+ }
+ dom = this.dom;
+
+ if (dom) {
+ if (!this.options && !this.options.parent) {
+ throw new Error('Cannot repaint item: no parent attached');
+ }
+ var parentContainer = this.parent.getContainer();
+ if (!parentContainer) {
+ throw new Error('Cannot repaint time axis: parent has no container element');
+ }
+
+ if (!dom.point.parentNode) {
+ parentContainer.appendChild(dom.point);
+ changed = true;
+ }
+
+ // update contents
+ if (this.data.content != this.content) {
+ this.content = this.data.content;
+ if (this.content instanceof Element) {
+ dom.content.innerHTML = '';
+ dom.content.appendChild(this.content);
+ }
+ else if (this.data.content != undefined) {
+ dom.content.innerHTML = this.content;
+ }
+ else {
+ throw new Error('Property "content" missing in item ' + this.data.id);
+ }
+ changed = true;
+ }
+
+ // update class
+ var className = (this.data.className? ' ' + this.data.className : '') +
+ (this.selected ? ' selected' : '');
+ if (this.className != className) {
+ this.className = className;
+ dom.point.className = 'item point' + className;
+ changed = true;
+ }
+ }
+ }
+ else {
+ // hide when visible
+ if (dom) {
+ if (dom.point.parentNode) {
+ dom.point.parentNode.removeChild(dom.point);
+ changed = true;
+ }
+ }
+ }
+
+ return changed;
+};
+
+/**
+ * Reflow the item: calculate its actual size from the DOM
+ * @return {boolean} resized returns true if the axis is resized
+ * @override
+ */
+ItemPoint.prototype.reflow = function () {
+ if (this.data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + this.data.id);
+ }
+
+ var update = util.updateProperty,
+ dom = this.dom,
+ props = this.props,
+ options = this.options,
+ orientation = options.orientation,
+ start = this.parent.toScreen(this.data.start),
+ changed = 0,
+ top;
+
+ if (dom) {
+ changed += update(this, 'width', dom.point.offsetWidth);
+ changed += update(this, 'height', dom.point.offsetHeight);
+ changed += update(props.dot, 'width', dom.dot.offsetWidth);
+ changed += update(props.dot, 'height', dom.dot.offsetHeight);
+ changed += update(props.content, 'height', dom.content.offsetHeight);
+
+ if (orientation == 'top') {
+ top = options.margin.axis;
+ }
+ else {
+ // default or 'bottom'
+ var parentHeight = this.parent.height;
+ top = parentHeight - this.height - options.margin.axis;
+ }
+ changed += update(this, 'top', top);
+ changed += update(this, 'left', start - props.dot.width / 2);
+ changed += update(props.content, 'marginLeft', 1.5 * props.dot.width);
+ //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO
+
+ changed += update(props.dot, 'top', (this.height - props.dot.height) / 2);
+ }
+ else {
+ changed += 1;
+ }
+
+ return (changed > 0);
+};
+
+/**
+ * Create an items DOM
+ * @private
+ */
+ItemPoint.prototype._create = function () {
+ var dom = this.dom;
+ if (!dom) {
+ this.dom = dom = {};
+
+ // background box
+ dom.point = document.createElement('div');
+ // className is updated in repaint()
+
+ // contents box, right from the dot
+ dom.content = document.createElement('div');
+ dom.content.className = 'content';
+ dom.point.appendChild(dom.content);
+
+ // dot at start
+ dom.dot = document.createElement('div');
+ dom.dot.className = 'dot';
+ dom.point.appendChild(dom.dot);
+ }
+};
+
+/**
+ * Reposition the item, recalculate its left, top, and width, using the current
+ * range and size of the items itemset
+ * @override
+ */
+ItemPoint.prototype.reposition = function () {
+ var dom = this.dom,
+ props = this.props;
+
+ if (dom) {
+ dom.point.style.top = this.top + 'px';
+ dom.point.style.left = this.left + 'px';
+
+ dom.content.style.marginLeft = props.content.marginLeft + 'px';
+ //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO
+
+ dom.dot.style.top = props.dot.top + 'px';
+ }
+};
diff --git a/src/component/item/itemrange.js b/src/component/item/itemrange.js
new file mode 100644
index 00000000..a88a0fff
--- /dev/null
+++ b/src/component/item/itemrange.js
@@ -0,0 +1,219 @@
+/**
+ * @constructor ItemRange
+ * @extends Item
+ * @param {ItemSet} parent
+ * @param {Object} data Object containing parameters start, end
+ * content, className.
+ * @param {Object} [options] Options to set initial property values
+ * // TODO: describe available options
+ */
+function ItemRange (parent, data, options) {
+ this.props = {
+ content: {
+ left: 0,
+ width: 0
+ }
+ };
+
+ Item.call(this, parent, data, options);
+}
+
+ItemRange.prototype = new Item (null, null);
+
+// register the ItemBox in the item types
+itemTypes['range'] = ItemRange;
+
+/**
+ * Select the item
+ * @override
+ */
+ItemRange.prototype.select = function () {
+ this.selected = true;
+ // TODO: select and unselect
+};
+
+/**
+ * Unselect the item
+ * @override
+ */
+ItemRange.prototype.unselect = function () {
+ this.selected = false;
+ // TODO: select and unselect
+};
+
+/**
+ * Repaint the item
+ * @return {Boolean} changed
+ */
+ItemRange.prototype.repaint = function () {
+ // TODO: make an efficient repaint
+ var changed = false;
+ var dom = this.dom;
+
+ if (this.visible) {
+ if (!dom) {
+ this._create();
+ changed = true;
+ }
+ dom = this.dom;
+
+ if (dom) {
+ if (!this.options && !this.options.parent) {
+ throw new Error('Cannot repaint item: no parent attached');
+ }
+ var parentContainer = this.parent.getContainer();
+ if (!parentContainer) {
+ throw new Error('Cannot repaint time axis: parent has no container element');
+ }
+
+ if (!dom.box.parentNode) {
+ parentContainer.appendChild(dom.box);
+ changed = true;
+ }
+
+ // update content
+ if (this.data.content != this.content) {
+ this.content = this.data.content;
+ if (this.content instanceof Element) {
+ dom.content.innerHTML = '';
+ dom.content.appendChild(this.content);
+ }
+ else if (this.data.content != undefined) {
+ dom.content.innerHTML = this.content;
+ }
+ else {
+ throw new Error('Property "content" missing in item ' + this.data.id);
+ }
+ changed = true;
+ }
+
+ // update class
+ var className = this.data.className ? ('' + this.data.className) : '';
+ if (this.className != className) {
+ this.className = className;
+ dom.box.className = 'item range' + className;
+ changed = true;
+ }
+ }
+ }
+ else {
+ // hide when visible
+ if (dom) {
+ if (dom.box.parentNode) {
+ dom.box.parentNode.removeChild(dom.box);
+ changed = true;
+ }
+ }
+ }
+
+ return changed;
+};
+
+/**
+ * Reflow the item: calculate its actual size from the DOM
+ * @return {boolean} resized returns true if the axis is resized
+ * @override
+ */
+ItemRange.prototype.reflow = function () {
+ if (this.data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + this.data.id);
+ }
+ if (this.data.end == undefined) {
+ throw new Error('Property "end" missing in item ' + this.data.id);
+ }
+
+ var dom = this.dom,
+ props = this.props,
+ options = this.options,
+ parent = this.parent,
+ start = parent.toScreen(this.data.start),
+ end = parent.toScreen(this.data.end),
+ changed = 0;
+
+ if (dom) {
+ var update = util.updateProperty,
+ box = dom.box,
+ parentWidth = parent.width,
+ orientation = options.orientation,
+ contentLeft,
+ top;
+
+ changed += update(props.content, 'width', dom.content.offsetWidth);
+
+ changed += update(this, 'height', box.offsetHeight);
+
+ // limit the width of the this, as browsers cannot draw very wide divs
+ if (start < -parentWidth) {
+ start = -parentWidth;
+ }
+ if (end > 2 * parentWidth) {
+ end = 2 * parentWidth;
+ }
+
+ // when range exceeds left of the window, position the contents at the left of the visible area
+ if (start < 0) {
+ contentLeft = Math.min(-start,
+ (end - start - props.content.width - 2 * options.padding));
+ // TODO: remove the need for options.padding. it's terrible.
+ }
+ else {
+ contentLeft = 0;
+ }
+ changed += update(props.content, 'left', contentLeft);
+
+ if (orientation == 'top') {
+ top = options.margin.axis;
+ changed += update(this, 'top', top);
+ }
+ else {
+ // default or 'bottom'
+ top = parent.height - this.height - options.margin.axis;
+ changed += update(this, 'top', top);
+ }
+
+ changed += update(this, 'left', start);
+ changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width;
+ }
+ else {
+ changed += 1;
+ }
+
+ return (changed > 0);
+};
+
+/**
+ * Create an items DOM
+ * @private
+ */
+ItemRange.prototype._create = function () {
+ var dom = this.dom;
+ if (!dom) {
+ this.dom = dom = {};
+ // background box
+ dom.box = document.createElement('div');
+ // className is updated in repaint()
+
+ // contents box
+ dom.content = document.createElement('div');
+ dom.content.className = 'content';
+ dom.box.appendChild(dom.content);
+ }
+};
+
+/**
+ * Reposition the item, recalculate its left, top, and width, using the current
+ * range and size of the items itemset
+ * @override
+ */
+ItemRange.prototype.reposition = function () {
+ var dom = this.dom,
+ props = this.props;
+
+ if (dom) {
+ dom.box.style.top = this.top + 'px';
+ dom.box.style.left = this.left + 'px';
+ dom.box.style.width = this.width + 'px';
+
+ dom.content.style.left = props.content.left + 'px';
+ }
+};
diff --git a/src/component/itemset.js b/src/component/itemset.js
new file mode 100644
index 00000000..5fb317d5
--- /dev/null
+++ b/src/component/itemset.js
@@ -0,0 +1,416 @@
+/**
+ * An ItemSet holds a set of items and ranges which can be displayed in a
+ * range. The width is determined by the parent of the ItemSet, and the height
+ * is determined by the size of the items.
+ * @param {Component} parent
+ * @param {Component[]} [depends] Components on which this components depends
+ * (except for the parent)
+ * @param {Object} [options] See ItemSet.setOptions for the available
+ * options.
+ * @constructor ItemSet
+ * @extends Panel
+ */
+function ItemSet(parent, depends, options) {
+ this.id = util.randomUUID();
+ this.parent = parent;
+ this.depends = depends;
+
+ // one options object is shared by this itemset and all its items
+ this.options = {
+ style: 'box',
+ align: 'center',
+ orientation: 'bottom',
+ margin: {
+ axis: 20,
+ item: 10
+ },
+ padding: 5
+ };
+
+ var me = this;
+ this.data = null; // DataSet
+ this.range = null; // Range or Object {start: number, end: number}
+ this.listeners = {
+ 'add': function (event, params) {
+ me._onAdd(params.items);
+ },
+ 'update': function (event, params) {
+ me._onUpdate(params.items);
+ },
+ 'remove': function (event, params) {
+ me._onRemove(params.items);
+ }
+ };
+
+ this.items = {};
+ this.queue = {}; // queue with items to be added/updated/removed
+ this.stack = new Stack(this);
+ this.conversion = null;
+
+ this.setOptions(options);
+}
+
+ItemSet.prototype = new Panel();
+
+/**
+ * Set options for the ItemSet. Existing options will be extended/overwritten.
+ * @param {Object} [options] The following options are available:
+ * {String | function} [className]
+ * class name for the itemset
+ * {String} [style]
+ * Default style 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).
+ * {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} padding
+ * Padding of the contents of an item in pixels.
+ * Must correspond with the items css. Default is 5.
+ */
+ItemSet.prototype.setOptions = function (options) {
+ util.extend(this.options, options);
+
+ // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis
+
+ this.stack.setOptions(this.options);
+};
+
+/**
+ * Set range (start and end).
+ * @param {Range | Object} range A Range or an object containing start and end.
+ */
+ItemSet.prototype.setRange = function (range) {
+ if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
+ throw new TypeError('Range must be an instance of Range, ' +
+ 'or an object containing start and end.');
+ }
+ this.range = range;
+};
+
+/**
+ * Repaint the component
+ * @return {Boolean} changed
+ */
+ItemSet.prototype.repaint = function () {
+ var changed = 0,
+ update = util.updateProperty,
+ asSize = util.option.asSize,
+ options = this.options,
+ frame = this.frame;
+
+ if (!frame) {
+ frame = document.createElement('div');
+ frame.className = 'itemset';
+
+ if (options.className) {
+ util.addClassName(frame, util.option.asString(options.className));
+ }
+
+ this.frame = frame;
+ changed += 1;
+ }
+ if (!frame.parentNode) {
+ if (!this.parent) {
+ throw new Error('Cannot repaint itemset: no parent attached');
+ }
+ var parentContainer = this.parent.getContainer();
+ if (!parentContainer) {
+ throw new Error('Cannot repaint itemset: parent has no container element');
+ }
+ parentContainer.appendChild(frame);
+ changed += 1;
+ }
+
+ changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
+ changed += update(frame.style, 'top', asSize(options.top, '0px'));
+ changed += update(frame.style, 'left', asSize(options.left, '0px'));
+ changed += update(frame.style, 'width', asSize(options.width, '100%'));
+
+ this._updateConversion();
+
+ var me = this,
+ queue = this.queue,
+ data = this.data,
+ items = this.items,
+ dataOptions = {
+ fields: ['id', 'start', 'end', 'content', 'type']
+ };
+ // TODO: copy options from the itemset itself?
+ // TODO: make orientation dynamically changable for the items
+
+ // show/hide added/changed/removed items
+ Object.keys(queue).forEach(function (id) {
+ var entry = queue[id];
+ var item = entry.item;
+ //noinspection FallthroughInSwitchStatementJS
+ switch (entry.action) {
+ case 'add':
+ case 'update':
+ var itemData = data.get(id, dataOptions);
+ var type = itemData.type ||
+ (itemData.start && itemData.end && 'range') ||
+ 'box';
+ var constructor = itemTypes[type];
+
+ // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
+ if (item) {
+ // update item
+ if (!constructor || !(item instanceof constructor)) {
+ // item type has changed, delete the item
+ item.visible = false;
+ changed += item.repaint();
+ item = null;
+ }
+ else {
+ item.data = itemData; // TODO: create a method item.setData ?
+ changed += item.repaint();
+ }
+ }
+
+ if (!item) {
+ // create item
+ if (constructor) {
+ item = new constructor(me, itemData, options);
+ changed += item.repaint();
+ }
+ else {
+ throw new TypeError('Unknown item type "' + type + '"');
+ }
+ }
+
+ // update lists
+ items[id] = item;
+ delete queue[id];
+ break;
+
+ case 'remove':
+ if (item) {
+ // TODO: remove dom of the item
+ item.visible = false;
+ changed += item.repaint();
+ }
+
+ // update lists
+ delete items[id];
+ delete queue[id];
+ break;
+
+ default:
+ console.log('Error: unknown action "' + entry.action + '"');
+ }
+ });
+
+ // reposition all items
+ util.forEach(this.items, function (item) {
+ item.reposition();
+ });
+
+ return (changed > 0);
+};
+
+/**
+ * Reflow the component
+ * @return {Boolean} resized
+ */
+ItemSet.prototype.reflow = function () {
+ var changed = 0,
+ options = this.options,
+ update = util.updateProperty,
+ frame = this.frame;
+
+ if (frame) {
+ this._updateConversion();
+
+ util.forEach(this.items, function (item) {
+ changed += item.reflow();
+ });
+
+ // TODO: stack.update should be triggered via an event, in stack itself
+ // TODO: only update the stack when there are changed items
+ this.stack.update();
+
+ if (options.height != null) {
+ changed += update(this, 'height', frame.offsetHeight);
+ }
+ else {
+ // height is not specified, determine the height from the height and positioned items
+ var frameHeight = this.height;
+ var maxHeight = 0;
+ if (options.orientation == 'top') {
+ util.forEach(this.items, function (item) {
+ maxHeight = Math.max(maxHeight, item.top + item.height);
+ });
+ }
+ else {
+ // orientation == 'bottom'
+ util.forEach(this.items, function (item) {
+ maxHeight = Math.max(maxHeight, frameHeight - item.top);
+ });
+ }
+
+ changed += update(this, 'height', maxHeight + options.margin.axis);
+ }
+
+ // calculate height from items
+ changed += update(this, 'top', frame.offsetTop);
+ changed += update(this, 'left', frame.offsetLeft);
+ changed += update(this, 'width', frame.offsetWidth);
+ }
+ else {
+ changed += 1;
+ }
+
+ return (changed > 0);
+};
+
+/**
+ * Set data
+ * @param {DataSet | Array | DataTable} data
+ */
+ItemSet.prototype.setData = function(data) {
+ // unsubscribe from current dataset
+ var current = this.data;
+ if (current) {
+ util.forEach(this.listeners, function (callback, event) {
+ current.unsubscribe(event, callback);
+ });
+ }
+
+ if (data instanceof DataSet) {
+ this.data = data;
+ }
+ else {
+ this.data = new DataSet({
+ fieldTypes: {
+ start: 'Date',
+ end: 'Date'
+ }
+ });
+ this.data.add(data);
+ }
+
+ var id = this.id;
+ var me = this;
+ util.forEach(this.listeners, function (callback, event) {
+ me.data.subscribe(event, callback, id);
+ });
+
+ var dataItems = this.data.get({filter: ['id']});
+ var ids = [];
+ util.forEach(dataItems, function (dataItem, index) {
+ ids[index] = dataItem.id;
+ });
+ this._onAdd(ids);
+};
+
+/**
+ * Handle updated items
+ * @param {Number[]} ids
+ * @private
+ */
+ItemSet.prototype._onUpdate = function(ids) {
+ this._toQueue(ids, 'update');
+};
+
+/**
+ * Handle changed items
+ * @param {Number[]} ids
+ * @private
+ */
+ItemSet.prototype._onAdd = function(ids) {
+ this._toQueue(ids, 'add');
+};
+
+/**
+ * Handle removed items
+ * @param {Number[]} ids
+ * @private
+ */
+ItemSet.prototype._onRemove = function(ids) {
+ this._toQueue(ids, 'remove');
+};
+
+/**
+ * Put items in the queue to be added/updated/remove
+ * @param {Number[]} ids
+ * @param {String} action can be 'add', 'update', 'remove'
+ */
+ItemSet.prototype._toQueue = function (ids, action) {
+ var items = this.items;
+ var queue = this.queue;
+ ids.forEach(function (id) {
+ var entry = queue[id];
+ if (entry) {
+ // already queued, update the action of the entry
+ entry.action = action;
+ }
+ else {
+ // not yet queued, add an entry to the queue
+ queue[id] = {
+ item: items[id] || null,
+ action: action
+ };
+ }
+ });
+
+ if (this.controller) {
+ //this.requestReflow();
+ this.requestRepaint();
+ }
+};
+
+/**
+ * Calculate the factor and offset to convert a position on screen to the
+ * corresponding date and vice versa.
+ * After the method _updateConversion is executed once, the methods toTime
+ * and toScreen can be used.
+ * @private
+ */
+ItemSet.prototype._updateConversion = function() {
+ var range = this.range;
+ if (!range) {
+ throw new Error('No range configured');
+ }
+
+ if (range.conversion) {
+ this.conversion = range.conversion(this.width);
+ }
+ else {
+ this.conversion = Range.conversion(range.start, range.end, this.width);
+ }
+};
+
+/**
+ * Convert a position on screen (pixels) to a datetime
+ * Before this method can be used, the method _updateConversion must be
+ * executed once.
+ * @param {int} x Position on the screen in pixels
+ * @return {Date} time The datetime the corresponds with given position x
+ */
+ItemSet.prototype.toTime = function(x) {
+ var conversion = this.conversion;
+ return new Date(x / conversion.factor + conversion.offset);
+};
+
+/**
+ * Convert a datetime (Date object) into a position on the screen
+ * Before this method can be used, the method _updateConversion must be
+ * executed once.
+ * @param {Date} time A date
+ * @return {int} x The position on the screen in pixels which corresponds
+ * with the given date.
+ */
+ItemSet.prototype.toScreen = function(time) {
+ var conversion = this.conversion;
+ return (time.valueOf() - conversion.offset) * conversion.factor;
+};
diff --git a/src/component/panel.js b/src/component/panel.js
new file mode 100644
index 00000000..dcb66eb8
--- /dev/null
+++ b/src/component/panel.js
@@ -0,0 +1,101 @@
+/**
+ * A panel can contain components
+ * @param {Component} [parent]
+ * @param {Component[]} [depends] Components on which this components depends
+ * (except for the parent)
+ * @param {Object} [options] Available parameters:
+ * {String | Number | function} [left]
+ * {String | Number | function} [top]
+ * {String | Number | function} [width]
+ * {String | Number | function} [height]
+ * {String | function} [className]
+ * @constructor Panel
+ * @extends Component
+ */
+function Panel(parent, depends, options) {
+ this.id = util.randomUUID();
+ this.parent = parent;
+ this.depends = depends;
+ this.options = {};
+
+ this.setOptions(options);
+}
+
+Panel.prototype = new Component();
+
+/**
+ * Get the container element of the panel, which can be used by a child to
+ * add its own widgets.
+ * @returns {HTMLElement} container
+ */
+Panel.prototype.getContainer = function () {
+ return this.frame;
+};
+
+/**
+ * Repaint the component
+ * @return {Boolean} changed
+ */
+Panel.prototype.repaint = function () {
+ var changed = 0,
+ update = util.updateProperty,
+ asSize = util.option.asSize,
+ options = this.options,
+ frame = this.frame;
+ if (!frame) {
+ frame = document.createElement('div');
+ frame.className = 'panel';
+
+ if (options.className) {
+ if (typeof options.className == 'function') {
+ util.addClassName(frame, String(options.className()));
+ }
+ else {
+ util.addClassName(frame, String(options.className));
+ }
+ }
+
+ this.frame = frame;
+ changed += 1;
+ }
+ if (!frame.parentNode) {
+ if (!this.parent) {
+ throw new Error('Cannot repaint panel: no parent attached');
+ }
+ var parentContainer = this.parent.getContainer();
+ if (!parentContainer) {
+ throw new Error('Cannot repaint panel: parent has no container element');
+ }
+ parentContainer.appendChild(frame);
+ changed += 1;
+ }
+
+ changed += update(frame.style, 'top', asSize(options.top, '0px'));
+ changed += update(frame.style, 'left', asSize(options.left, '0px'));
+ changed += update(frame.style, 'width', asSize(options.width, '100%'));
+ changed += update(frame.style, 'height', asSize(options.height, '100%'));
+
+ return (changed > 0);
+};
+
+/**
+ * Reflow the component
+ * @return {Boolean} resized
+ */
+Panel.prototype.reflow = function () {
+ var changed = 0,
+ update = util.updateProperty,
+ frame = this.frame;
+
+ if (frame) {
+ changed += update(this, 'top', frame.offsetTop);
+ changed += update(this, 'left', frame.offsetLeft);
+ changed += update(this, 'width', frame.offsetWidth);
+ changed += update(this, 'height', frame.offsetHeight);
+ }
+ else {
+ changed += 1;
+ }
+
+ return (changed > 0);
+};
diff --git a/src/component/rootpanel.js b/src/component/rootpanel.js
new file mode 100644
index 00000000..7265a759
--- /dev/null
+++ b/src/component/rootpanel.js
@@ -0,0 +1,200 @@
+/**
+ * A root panel can hold components. The root panel must be initialized with
+ * a DOM element as container.
+ * @param {HTMLElement} container
+ * @param {Object} [options] Available parameters: see RootPanel.setOptions.
+ * @constructor RootPanel
+ * @extends Panel
+ */
+function RootPanel(container, options) {
+ this.id = util.randomUUID();
+ this.container = container;
+ this.options = {
+ autoResize: true
+ };
+
+ this.listeners = {}; // event listeners
+
+ this.setOptions(options);
+}
+
+RootPanel.prototype = new Panel();
+
+/**
+ * Set options. Will extend the current options.
+ * @param {Object} [options] Available parameters:
+ * {String | function} [className]
+ * {String | Number | function} [left]
+ * {String | Number | function} [top]
+ * {String | Number | function} [width]
+ * {String | Number | function} [height]
+ * {String | Number | function} [height]
+ * {Boolean | function} [autoResize]
+ */
+RootPanel.prototype.setOptions = function (options) {
+ util.extend(this.options, options);
+
+ if (this.options.autoResize) {
+ this._watch();
+ }
+ else {
+ this._unwatch();
+ }
+};
+
+/**
+ * Repaint the component
+ * @return {Boolean} changed
+ */
+RootPanel.prototype.repaint = function () {
+ var changed = 0,
+ update = util.updateProperty,
+ asSize = util.option.asSize,
+ options = this.options,
+ frame = this.frame;
+ if (!frame) {
+ frame = document.createElement('div');
+ frame.className = 'graph panel';
+
+ if (options.className) {
+ util.addClassName(frame, util.option.asString(options.className));
+ }
+
+ this.frame = frame;
+ changed += 1;
+ }
+ if (!frame.parentNode) {
+ if (!this.container) {
+ throw new Error('Cannot repaint root panel: no container attached');
+ }
+ this.container.appendChild(frame);
+ changed += 1;
+ }
+
+ changed += update(frame.style, 'top', asSize(options.top, '0px'));
+ changed += update(frame.style, 'left', asSize(options.left, '0px'));
+ changed += update(frame.style, 'width', asSize(options.width, '100%'));
+ changed += update(frame.style, 'height', asSize(options.height, '100%'));
+
+ this._updateEventEmitters();
+
+ return (changed > 0);
+};
+
+/**
+ * Reflow the component
+ * @return {Boolean} resized
+ */
+RootPanel.prototype.reflow = function () {
+ var changed = 0,
+ update = util.updateProperty,
+ frame = this.frame;
+
+ if (frame) {
+ changed += update(this, 'top', frame.offsetTop);
+ changed += update(this, 'left', frame.offsetLeft);
+ changed += update(this, 'width', frame.offsetWidth);
+ changed += update(this, 'height', frame.offsetHeight);
+ }
+ else {
+ changed += 1;
+ }
+
+ return (changed > 0);
+};
+
+/**
+ * Watch for changes in the size of the frame. On resize, the Panel will
+ * automatically redraw itself.
+ * @private
+ */
+RootPanel.prototype._watch = function () {
+ var me = this;
+
+ this._unwatch();
+
+ var checkSize = function () {
+ if (!me.options.autoResize) {
+ // stop watching when the option autoResize is changed to false
+ me._unwatch();
+ return;
+ }
+
+ if (me.frame) {
+ // check whether the frame is resized
+ if ((me.frame.clientWidth != me.width) ||
+ (me.frame.clientHeight != me.height)) {
+ me.requestReflow();
+ }
+ }
+ };
+
+ // TODO: automatically cleanup the event listener when the frame is deleted
+ util.addEventListener(window, 'resize', checkSize);
+
+ this.watchTimer = setInterval(checkSize, 1000);
+};
+
+/**
+ * Stop watching for a resize of the frame.
+ * @private
+ */
+RootPanel.prototype._unwatch = function () {
+ if (this.watchTimer) {
+ clearInterval(this.watchTimer);
+ this.watchTimer = undefined;
+ }
+
+ // TODO: remove event listener on window.resize
+};
+
+/**
+ * Event handler
+ * @param {String} event name of the event, for example 'click', 'mousemove'
+ * @param {function} callback callback handler, invoked with the raw HTML Event
+ * as parameter.
+ */
+RootPanel.prototype.on = function (event, callback) {
+ // register the listener at this component
+ var arr = this.listeners[event];
+ if (!arr) {
+ arr = [];
+ this.listeners[event] = arr;
+ }
+ arr.push(callback);
+
+ this._updateEventEmitters();
+};
+
+/**
+ * Update the event listeners for all event emitters
+ * @private
+ */
+RootPanel.prototype._updateEventEmitters = function () {
+ if (this.listeners) {
+ var me = this;
+ util.forEach(this.listeners, function (listeners, event) {
+ if (!me.emitters) {
+ me.emitters = {};
+ }
+ if (!(event in me.emitters)) {
+ // create event
+ var frame = me.frame;
+ if (frame) {
+ //console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging
+ var callback = function(event) {
+ listeners.forEach(function (listener) {
+ // TODO: filter on event target!
+ listener(event);
+ });
+ };
+ me.emitters[event] = callback;
+ util.addEventListener(frame, event, callback);
+ }
+ }
+ });
+
+ // TODO: be able to delete event listeners
+ // TODO: be able to move event listeners to a parent when available
+ }
+};
\ No newline at end of file
diff --git a/src/component/timeaxis.js b/src/component/timeaxis.js
new file mode 100644
index 00000000..a236b385
--- /dev/null
+++ b/src/component/timeaxis.js
@@ -0,0 +1,521 @@
+/**
+ * A horizontal time axis
+ * @param {Component} parent
+ * @param {Component[]} [depends] Components on which this components depends
+ * (except for the parent)
+ * @param {Object} [options] See TimeAxis.setOptions for the available
+ * options.
+ * @constructor TimeAxis
+ * @extends Component
+ */
+function TimeAxis (parent, depends, options) {
+ this.id = util.randomUUID();
+ this.parent = parent;
+ this.depends = depends;
+
+ this.dom = {
+ majorLines: [],
+ majorTexts: [],
+ minorLines: [],
+ minorTexts: [],
+ redundant: {
+ majorLines: [],
+ majorTexts: [],
+ minorLines: [],
+ minorTexts: []
+ }
+ };
+ this.props = {
+ range: {
+ start: 0,
+ end: 0,
+ minimumStep: 0
+ }
+ };
+
+ this.options = {
+ orientation: 'bottom', // supported: 'top', 'bottom'
+ // TODO: implement timeaxis orientations 'left' and 'right'
+ showMinorLabels: true,
+ showMajorLabels: true
+ };
+
+ this.conversion = null;
+ this.range = null;
+
+ this.setOptions(options);
+}
+
+TimeAxis.prototype = new Component();
+
+// TODO: comment options
+TimeAxis.prototype.setOptions = function (options) {
+ util.extend(this.options, options);
+};
+
+/**
+ * Set a range (start and end)
+ * @param {Range | Object} range A Range or an object containing start and end.
+ */
+TimeAxis.prototype.setRange = function (range) {
+ if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
+ throw new TypeError('Range must be an instance of Range, ' +
+ 'or an object containing start and end.');
+ }
+ this.range = range;
+};
+
+/**
+ * Convert a position on screen (pixels) to a datetime
+ * @param {int} x Position on the screen in pixels
+ * @return {Date} time The datetime the corresponds with given position x
+ */
+TimeAxis.prototype.toTime = function(x) {
+ var conversion = this.conversion;
+ return new Date(x / conversion.factor + 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
+ */
+TimeAxis.prototype.toScreen = function(time) {
+ var conversion = this.conversion;
+ return (time.valueOf() - conversion.offset) * conversion.factor;
+};
+
+/**
+ * Repaint the component
+ * @return {Boolean} changed
+ */
+TimeAxis.prototype.repaint = function () {
+ var changed = 0,
+ update = util.updateProperty,
+ asSize = util.option.asSize,
+ options = this.options,
+ props = this.props,
+ step = this.step;
+
+ var frame = this.frame;
+ if (!frame) {
+ frame = document.createElement('div');
+ this.frame = frame;
+ changed += 1;
+ }
+ frame.className = 'axis ' + options.orientation;
+ // TODO: custom className?
+
+ if (!frame.parentNode) {
+ if (!this.parent) {
+ throw new Error('Cannot repaint time axis: no parent attached');
+ }
+ var parentContainer = this.parent.getContainer();
+ if (!parentContainer) {
+ throw new Error('Cannot repaint time axis: parent has no container element');
+ }
+ parentContainer.appendChild(frame);
+
+ changed += 1;
+ }
+
+ var parent = frame.parentNode;
+ if (parent) {
+ var beforeChild = frame.nextSibling;
+ parent.removeChild(frame); // take frame offline while updating (is almost twice as fast)
+
+ var orientation = options.orientation;
+ var defaultTop = (orientation == 'bottom') ? (this.props.parentHeight - this.height) + 'px' : '0px';
+ changed += update(frame.style, 'top', asSize(options.top, defaultTop));
+ changed += update(frame.style, 'left', asSize(options.left, '0px'));
+ changed += update(frame.style, 'width', asSize(options.width, '100%'));
+ changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
+
+ // get characters width and height
+ this._repaintMeasureChars();
+
+ if (this.step) {
+ this._repaintStart();
+
+ step.first();
+ var xFirstMajorLabel = undefined;
+ var max = 0;
+ while (step.hasNext() && max < 1000) {
+ max++;
+ var cur = step.getCurrent(),
+ x = this.toScreen(cur),
+ isMajor = step.isMajor();
+
+ // TODO: lines must have a width, such that we can create css backgrounds
+
+ if (options.showMinorLabels) {
+ this._repaintMinorText(x, step.getLabelMinor());
+ }
+
+ if (isMajor && options.showMajorLabels) {
+ if (x > 0) {
+ if (xFirstMajorLabel == undefined) {
+ xFirstMajorLabel = x;
+ }
+ this._repaintMajorText(x, step.getLabelMajor());
+ }
+ this._repaintMajorLine(x);
+ }
+ else {
+ this._repaintMinorLine(x);
+ }
+
+ step.next();
+ }
+
+ // create a major label on the left when needed
+ if (options.showMajorLabels) {
+ var leftTime = this.toTime(0),
+ leftText = step.getLabelMajor(leftTime),
+ widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation
+
+ if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
+ this._repaintMajorText(0, leftText);
+ }
+ }
+
+ this._repaintEnd();
+ }
+
+ this._repaintLine();
+
+ // put frame online again
+ if (beforeChild) {
+ parent.insertBefore(frame, beforeChild);
+ }
+ else {
+ parent.appendChild(frame)
+ }
+ }
+
+ return (changed > 0);
+};
+
+/**
+ * Start a repaint. Move all DOM elements to a redundant list, where they
+ * can be picked for re-use, or can be cleaned up in the end
+ * @private
+ */
+TimeAxis.prototype._repaintStart = function () {
+ var dom = this.dom,
+ redundant = dom.redundant;
+
+ redundant.majorLines = dom.majorLines;
+ redundant.majorTexts = dom.majorTexts;
+ redundant.minorLines = dom.minorLines;
+ redundant.minorTexts = dom.minorTexts;
+
+ dom.majorLines = [];
+ dom.majorTexts = [];
+ dom.minorLines = [];
+ dom.minorTexts = [];
+};
+
+/**
+ * End a repaint. Cleanup leftover DOM elements in the redundant list
+ * @private
+ */
+TimeAxis.prototype._repaintEnd = function () {
+ util.forEach(this.dom.redundant, function (arr) {
+ while (arr.length) {
+ var elem = arr.pop();
+ if (elem && elem.parentNode) {
+ elem.parentNode.removeChild(elem);
+ }
+ }
+ });
+};
+
+
+/**
+ * Create a minor label for the axis at position x
+ * @param {Number} x
+ * @param {String} text
+ * @private
+ */
+TimeAxis.prototype._repaintMinorText = function (x, text) {
+ // 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.frame.appendChild(label);
+ }
+ this.dom.minorTexts.push(label);
+
+ label.childNodes[0].nodeValue = text;
+ label.style.left = x + 'px';
+ label.style.top = this.props.minorLabelTop + 'px';
+ //label.title = title; // TODO: this is a heavy operation
+};
+
+/**
+ * Create a Major label for the axis at position x
+ * @param {Number} x
+ * @param {String} text
+ * @private
+ */
+TimeAxis.prototype._repaintMajorText = function (x, text) {
+ // 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.frame.appendChild(label);
+ }
+ this.dom.majorTexts.push(label);
+
+ label.childNodes[0].nodeValue = text;
+ label.style.top = this.props.majorLabelTop + 'px';
+ label.style.left = x + 'px';
+ //label.title = title; // TODO: this is a heavy operation
+};
+
+/**
+ * Create a minor line for the axis at position x
+ * @param {Number} x
+ * @private
+ */
+TimeAxis.prototype._repaintMinorLine = function (x) {
+ // reuse redundant line
+ var line = this.dom.redundant.minorLines.shift();
+
+ if (!line) {
+ // create vertical line
+ line = document.createElement('div');
+ line.className = 'grid vertical minor';
+ this.frame.appendChild(line);
+ }
+ this.dom.minorLines.push(line);
+
+ var props = this.props;
+ line.style.top = props.minorLineTop + 'px';
+ line.style.height = props.minorLineHeight + 'px';
+ line.style.left = (x - props.minorLineWidth / 2) + 'px';
+};
+
+/**
+ * Create a Major line for the axis at position x
+ * @param {Number} x
+ * @private
+ */
+TimeAxis.prototype._repaintMajorLine = function (x) {
+ // reuse redundant line
+ var line = this.dom.redundant.majorLines.shift();
+
+ if (!line) {
+ // create vertical line
+ line = document.createElement('DIV');
+ line.className = 'grid vertical major';
+ this.frame.appendChild(line);
+ }
+ this.dom.majorLines.push(line);
+
+ var props = this.props;
+ line.style.top = props.majorLineTop + 'px';
+ line.style.left = (x - props.majorLineWidth / 2) + 'px';
+ line.style.height = props.majorLineHeight + 'px';
+};
+
+
+/**
+ * Repaint the horizontal line for the axis
+ * @private
+ */
+TimeAxis.prototype._repaintLine = function() {
+ var line = this.dom.line,
+ frame = this.frame,
+ options = this.options;
+
+ // line before all axis elements
+ if (options.showMinorLabels || options.showMajorLabels) {
+ if (line) {
+ // put this line at the end of all childs
+ frame.removeChild(line);
+ frame.appendChild(line);
+ }
+ else {
+ // create the axis line
+ line = document.createElement('div');
+ line.className = 'grid horizontal major';
+ frame.appendChild(line);
+ this.dom.line = line;
+ }
+
+ line.style.top = this.props.lineTop + 'px';
+ }
+ else {
+ if (line && axis.parentElement) {
+ frame.removeChild(axis.line);
+ delete this.dom.line;
+ }
+ }
+};
+
+/**
+ * Create characters used to determine the size of text on the axis
+ * @private
+ */
+TimeAxis.prototype._repaintMeasureChars = function () {
+ // calculate the width and height of a single character
+ // this is used to calculate the step size, and also the positioning of the
+ // axis
+ var dom = this.dom,
+ text;
+
+ if (!dom.characterMinor) {
+ text = document.createTextNode('0');
+ var measureCharMinor = document.createElement('DIV');
+ measureCharMinor.className = 'text minor measure';
+ measureCharMinor.appendChild(text);
+ this.frame.appendChild(measureCharMinor);
+
+ dom.measureCharMinor = measureCharMinor;
+ }
+
+ if (!dom.characterMajor) {
+ text = document.createTextNode('0');
+ var measureCharMajor = document.createElement('DIV');
+ measureCharMajor.className = 'text major measure';
+ measureCharMajor.appendChild(text);
+ this.frame.appendChild(measureCharMajor);
+
+ dom.measureCharMajor = measureCharMajor;
+ }
+};
+
+/**
+ * Reflow the component
+ * @return {Boolean} resized
+ */
+TimeAxis.prototype.reflow = function () {
+ var changed = 0,
+ update = util.updateProperty,
+ frame = this.frame,
+ range = this.range;
+
+ if (!range) {
+ throw new Error('Cannot repaint time axis: no range configured');
+ }
+
+ if (frame) {
+ changed += update(this, 'top', frame.offsetTop);
+ changed += update(this, 'left', frame.offsetLeft);
+
+ // calculate size of a character
+ var props = this.props,
+ showMinorLabels = this.options.showMinorLabels,
+ showMajorLabels = this.options.showMajorLabels,
+ measureCharMinor = this.dom.measureCharMinor,
+ measureCharMajor = this.dom.measureCharMajor;
+ if (measureCharMinor) {
+ props.minorCharHeight = measureCharMinor.clientHeight;
+ props.minorCharWidth = measureCharMinor.clientWidth;
+ }
+ if (measureCharMajor) {
+ props.majorCharHeight = measureCharMajor.clientHeight;
+ props.majorCharWidth = measureCharMajor.clientWidth;
+ }
+
+ var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0;
+ if (parentHeight != props.parentHeight) {
+ props.parentHeight = parentHeight;
+ changed += 1;
+ }
+ switch (this.options.orientation) {
+ case 'bottom':
+ props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
+ props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
+
+ props.minorLabelTop = 0;
+ props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight;
+
+ props.minorLineTop = -this.top;
+ props.minorLineHeight = this.top + props.majorLabelHeight;
+ props.minorLineWidth = 1; // TODO: really calculate width
+
+ props.majorLineTop = -this.top;
+ props.majorLineHeight = this.top + props.minorLabelHeight + props.majorLabelHeight;
+ props.majorLineWidth = 1; // TODO: really calculate width
+
+ props.lineTop = 0;
+
+ break;
+
+ case 'top':
+ props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
+ props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
+
+ props.majorLabelTop = 0;
+ props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight;
+
+ props.minorLineTop = props.minorLabelTop;
+ props.minorLineHeight = parentHeight - props.majorLabelHeight - this.top;
+ props.minorLineWidth = 1; // TODO: really calculate width
+
+ props.majorLineTop = 0;
+ props.majorLineHeight = parentHeight - this.top;
+ props.majorLineWidth = 1; // TODO: really calculate width
+
+ props.lineTop = props.majorLabelHeight + props.minorLabelHeight;
+
+ break;
+
+ default:
+ throw new Error('Unkown orientation "' + this.options.orientation + '"');
+ }
+
+ var height = props.minorLabelHeight + props.majorLabelHeight;
+ changed += update(this, 'width', frame.offsetWidth);
+ changed += update(this, 'height', height);
+
+ // calculate range and step
+ this._updateConversion();
+
+ var start = util.cast(range.start, 'Date'),
+ end = util.cast(range.end, 'Date'),
+ minimumStep = this.toTime((props.minorCharWidth || 10) * 5) - this.toTime(0);
+ this.step = new TimeStep(start, end, minimumStep);
+ changed += update(props.range, 'start', start.valueOf());
+ changed += update(props.range, 'end', end.valueOf());
+ changed += update(props.range, 'minimumStep', minimumStep.valueOf());
+ }
+
+ return (changed > 0);
+};
+
+/**
+ * Calculate the factor and offset to convert a position on screen to the
+ * corresponding date and vice versa.
+ * After the method _updateConversion is executed once, the methods toTime
+ * and toScreen can be used.
+ * @private
+ */
+TimeAxis.prototype._updateConversion = function() {
+ var range = this.range;
+ if (!range) {
+ throw new Error('No range configured');
+ }
+
+ if (range.conversion) {
+ this.conversion = range.conversion(this.width);
+ }
+ else {
+ this.conversion = Range.conversion(range.start, range.end, this.width);
+ }
+};
diff --git a/src/controller.js b/src/controller.js
new file mode 100644
index 00000000..6c15b3c2
--- /dev/null
+++ b/src/controller.js
@@ -0,0 +1,139 @@
+/**
+ * @constructor Controller
+ *
+ * A Controller controls the reflows and repaints of all visual components
+ */
+function Controller () {
+ this.id = util.randomUUID();
+ this.components = {};
+
+ this.repaintTimer = undefined;
+ this.reflowTimer = undefined;
+}
+
+/**
+ * Add a component to the controller
+ * @param {Component | Controller} component
+ */
+Controller.prototype.add = function (component) {
+ // validate the component
+ if (component.id == undefined) {
+ throw new Error('Component has no field id');
+ }
+ if (!(component instanceof Component) && !(component instanceof Controller)) {
+ throw new TypeError('Component must be an instance of ' +
+ 'prototype Component or Controller');
+ }
+
+ // add the component
+ component.controller = this;
+ this.components[component.id] = component;
+};
+
+/**
+ * Request a reflow. The controller will schedule a reflow
+ */
+Controller.prototype.requestReflow = function () {
+ if (!this.reflowTimer) {
+ var me = this;
+ this.reflowTimer = setTimeout(function () {
+ me.reflowTimer = undefined;
+ me.reflow();
+ }, 0);
+ }
+};
+
+/**
+ * Request a repaint. The controller will schedule a repaint
+ */
+Controller.prototype.requestRepaint = function () {
+ if (!this.repaintTimer) {
+ var me = this;
+ this.repaintTimer = setTimeout(function () {
+ me.repaintTimer = undefined;
+ me.repaint();
+ }, 0);
+ }
+};
+
+/**
+ * Repaint all components
+ */
+Controller.prototype.repaint = function () {
+ var changed = false;
+
+ // cancel any running repaint request
+ if (this.repaintTimer) {
+ clearTimeout(this.repaintTimer);
+ this.repaintTimer = undefined;
+ }
+
+ var done = {};
+
+ function repaint(component, id) {
+ if (!(id in done)) {
+ // first repaint the components on which this component is dependent
+ if (component.depends) {
+ component.depends.forEach(function (dep) {
+ repaint(dep, dep.id);
+ });
+ }
+ if (component.parent) {
+ repaint(component.parent, component.parent.id);
+ }
+
+ // repaint the component itself and mark as done
+ changed = component.repaint() || changed;
+ done[id] = true;
+ }
+ }
+
+ util.forEach(this.components, repaint);
+
+ // immediately reflow when needed
+ if (changed) {
+ this.reflow();
+ }
+ // TODO: limit the number of nested reflows/repaints, prevent loop
+};
+
+/**
+ * Reflow all components
+ */
+Controller.prototype.reflow = function () {
+ var resized = false;
+
+ // cancel any running repaint request
+ if (this.reflowTimer) {
+ clearTimeout(this.reflowTimer);
+ this.reflowTimer = undefined;
+ }
+
+ var done = {};
+
+ function reflow(component, id) {
+ if (!(id in done)) {
+ // first reflow the components on which this component is dependent
+ if (component.depends) {
+ component.depends.forEach(function (dep) {
+ reflow(dep, dep.id);
+ });
+ }
+ if (component.parent) {
+ reflow(component.parent, component.parent.id);
+ }
+
+ // reflow the component itself and mark as done
+ resized = component.reflow() || resized;
+ done[id] = true;
+ }
+ }
+
+ util.forEach(this.components, reflow);
+
+ // immediately repaint when needed
+ if (resized) {
+ this.repaint();
+ }
+ // TODO: limit the number of nested reflows/repaints, prevent loop
+};
diff --git a/src/dataset.js b/src/dataset.js
new file mode 100644
index 00000000..4173f0ad
--- /dev/null
+++ b/src/dataset.js
@@ -0,0 +1,502 @@
+/**
+ * DataSet
+ *
+ * Usage:
+ * var dataSet = new DataSet({
+ * fieldId: '_id',
+ * fieldTypes: {
+ * // ...
+ * }
+ * });
+ *
+ * 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 {Object} [options] Available options:
+ * {String} fieldId Field name of the id in the
+ * items, 'id' by default.
+ * {Object.} [fieldTypes]
+ * {String[]} [fields] filter fields
+ * @param {Array | DataTable} [data] If provided, items will be appended
+ * to this array or table. Required
+ * in case of Google DataTable
+ * @return {Array | Object | DataTable | null} data
+ * @throws Error
+ */
+DataSet.prototype.get = function (ids, options, data) {
+ var me = this;
+
+ // shift arguments when first argument contains the options
+ if (util.getType(ids) == 'Object') {
+ data = options;
+ options = ids;
+ ids = undefined;
+ }
+
+ // merge field types
+ var fieldTypes = {};
+ if (this.options && this.options.fieldTypes) {
+ util.forEach(this.options.fieldTypes, function (value, field) {
+ fieldTypes[field] = value;
+ });
+ }
+ if (options && options.fieldTypes) {
+ util.forEach(options.fieldTypes, function (value, field) {
+ fieldTypes[field] = value;
+ });
+ }
+
+ var fields = options ? options.fields : undefined;
+
+ // determine the return type
+ var type;
+ if (options && options.type) {
+ type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
+
+ if (data && (type != util.getType(data))) {
+ throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
+ 'does not correspond with specified options.type (' + options.type + ')');
+ }
+ if (type == 'DataTable' && !util.isDataTable(data)) {
+ throw new Error('Parameter "data" must be a DataTable ' +
+ 'when options.type is "DataTable"');
+ }
+ }
+ else if (data) {
+ type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
+ }
+ else {
+ type = 'Array';
+ }
+
+ if (type == 'DataTable') {
+ // return a Google DataTable
+ var columns = this._getColumnNames(data);
+ if (ids == undefined) {
+ // return all data
+ util.forEach(this.data, function (item) {
+ me._appendRow(data, columns, me._castItem(item));
+ });
+ }
+ else if (util.isNumber(ids) || util.isString(ids)) {
+ var item = me._castItem(me.data[ids], fieldTypes, fields);
+ this._appendRow(data, columns, item);
+ }
+ else if (ids instanceof Array) {
+ ids.forEach(function (id) {
+ var item = me._castItem(me.data[id], fieldTypes, fields);
+ me._appendRow(data, columns, item);
+ });
+ }
+ else {
+ throw new TypeError('Parameter "ids" must be ' +
+ 'undefined, a String, Number, or Array');
+ }
+ }
+ else {
+ // return an array
+ data = data || [];
+ if (ids == undefined) {
+ // return all data
+ util.forEach(this.data, function (item) {
+ data.push(me._castItem(item, fieldTypes, fields));
+ });
+ }
+ else if (util.isNumber(ids) || util.isString(ids)) {
+ // return a single item
+ return this._castItem(me.data[ids], fieldTypes, fields);
+ }
+ else if (ids instanceof Array) {
+ ids.forEach(function (id) {
+ data.push(me._castItem(me.data[id], fieldTypes, fields));
+ });
+ }
+ else {
+ throw new TypeError('Parameter "ids" must be ' +
+ 'undefined, a String, Number, or Array');
+ }
+ }
+
+ return data;
+};
+
+/**
+ * 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, used to trigger events for
+ * all but this sender's event subscribers.
+ */
+DataSet.prototype.remove = function (id, senderId) {
+ var items = [],
+ me = this;
+
+ if (util.isNumber(id) || util.isString(id)) {
+ delete this.data[id];
+ delete this.internalIds[id];
+ items.push(id);
+ }
+ else if (id instanceof Array) {
+ id.forEach(function (id) {
+ me.remove(id);
+ });
+ items = items.concat(id);
+ }
+ else if (id instanceof Object) {
+ // search for the object
+ for (var i in this.data) {
+ if (this.data.hasOwnProperty(i)) {
+ if (this.data[i] == id) {
+ delete this.data[i];
+ delete this.internalIds[i];
+ items.push(i);
+ }
+ }
+ }
+ }
+
+ this._trigger('remove', {items: items}, senderId);
+};
+
+/**
+ * Clear the data
+ * @param {String} [senderId] Optional sender id, used to trigger events for
+ * all but this sender's event subscribers.
+ */
+DataSet.prototype.clear = function (senderId) {
+ var items = Object.keys(this.data);
+
+ this.data = [];
+ this.internalIds = {};
+
+ this._trigger('remove', {items: items}, senderId);
+};
+
+/**
+ * Add a single item
+ * @param {Object} item
+ * @return {String} id
+ * @private
+ */
+DataSet.prototype._addItem = function (item) {
+ var id = item[this.fieldId];
+ if (id == undefined) {
+ // generate an id
+ id = util.randomUUID();
+ item[this.fieldId] = id;
+
+ this.internalIds[id] = item;
+ }
+
+ var d = {};
+ for (var field in item) {
+ if (item.hasOwnProperty(field)) {
+ var type = this.fieldTypes[field]; // type may be undefined
+ d[field] = util.cast(item[field], type);
+ }
+ }
+ this.data[id] = d;
+ //TODO: fail when an item with this id already exists?
+
+ return id;
+};
+
+/**
+ * Cast and filter the fields of an item
+ * @param {Object | undefined} item
+ * @param {Object.} [fieldTypes]
+ * @param {String[]} [fields]
+ * @return {Object | null} castedItem
+ * @private
+ */
+DataSet.prototype._castItem = function (item, fieldTypes, fields) {
+ var clone,
+ fieldId = this.fieldId,
+ internalIds = this.internalIds;
+
+ if (item) {
+ clone = {};
+ fieldTypes = fieldTypes || {};
+
+ if (fields) {
+ // output filtered fields
+ util.forEach(item, function (value, field) {
+ if (fields.indexOf(field) != -1) {
+ clone[field] = util.cast(value, fieldTypes[field]);
+ }
+ });
+ }
+ else {
+ // output all fields, except internal ids
+ util.forEach(item, function (value, field) {
+ if (field != fieldId || !(value in internalIds)) {
+ clone[field] = util.cast(value, fieldTypes[field]);
+ }
+ });
+ }
+ }
+ else {
+ clone = null;
+ }
+
+ return clone;
+};
+
+/**
+ * Update a single item: merge with existing item
+ * @param {Object} item
+ * @return {String} id
+ * @private
+ */
+DataSet.prototype._updateItem = function (item) {
+ var id = item[this.fieldId];
+ if (id == undefined) {
+ throw new Error('Item has no id (item: ' + JSON.stringify(item) + ')');
+ }
+ var d = this.data[id];
+ if (d) {
+ // merge with current item
+ for (var field in item) {
+ if (item.hasOwnProperty(field)) {
+ var type = this.fieldTypes[field]; // type may be undefined
+ d[field] = util.cast(item[field], type);
+ }
+ }
+ }
+ else {
+ // create new item
+ this._addItem(item);
+ }
+
+ return id;
+};
+
+/**
+ * Get an array with the column names of a Google DataTable
+ * @param {DataTable} dataTable
+ * @return {Array} 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();
+ columns.forEach(function (field, col) {
+ dataTable.setValue(row, col, item[field]);
+ });
+};
diff --git a/src/events.js b/src/events.js
new file mode 100644
index 00000000..44f44c12
--- /dev/null
+++ b/src/events.js
@@ -0,0 +1,116 @@
+
+/**
+ * Event listener (singleton)
+ */
+var events = {
+ 'listeners': [],
+
+ /**
+ * Find a single listener by its object
+ * @param {Object} object
+ * @return {Number} index -1 when not found
+ */
+ 'indexOf': function (object) {
+ var listeners = this.listeners;
+ for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
+ var listener = listeners[i];
+ if (listener && listener.object == object) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ /**
+ * Add an event listener
+ * @param {Object} object
+ * @param {String} event The name of an event, for example 'select'
+ * @param {function} callback The callback method, called when the
+ * event takes place
+ */
+ 'addListener': function (object, event, callback) {
+ var index = this.indexOf(object);
+ var listener = this.listeners[index];
+ if (!listener) {
+ listener = {
+ 'object': object,
+ 'events': {}
+ };
+ this.listeners.push(listener);
+ }
+
+ var callbacks = listener.events[event];
+ if (!callbacks) {
+ callbacks = [];
+ listener.events[event] = callbacks;
+ }
+
+ // add the callback if it does not yet exist
+ if (callbacks.indexOf(callback) == -1) {
+ callbacks.push(callback);
+ }
+ },
+
+ /**
+ * Remove an event listener
+ * @param {Object} object
+ * @param {String} event The name of an event, for example 'select'
+ * @param {function} callback The registered callback method
+ */
+ 'removeListener': function (object, event, callback) {
+ var index = this.indexOf(object);
+ var listener = this.listeners[index];
+ if (listener) {
+ var callbacks = listener.events[event];
+ if (callbacks) {
+ index = callbacks.indexOf(callback);
+ if (index != -1) {
+ callbacks.splice(index, 1);
+ }
+
+ // remove the array when empty
+ if (callbacks.length == 0) {
+ delete listener.events[event];
+ }
+ }
+
+ // count the number of registered events. remove listener when empty
+ var count = 0;
+ var events = listener.events;
+ for (var e in events) {
+ if (events.hasOwnProperty(e)) {
+ count++;
+ }
+ }
+ if (count == 0) {
+ delete this.listeners[index];
+ }
+ }
+ },
+
+ /**
+ * Remove all registered event listeners
+ */
+ 'removeAllListeners': function () {
+ this.listeners = [];
+ },
+
+ /**
+ * Trigger an event. All registered event handlers will be called
+ * @param {Object} object
+ * @param {String} event
+ * @param {Object} properties (optional)
+ */
+ 'trigger': function (object, event, properties) {
+ var index = this.indexOf(object);
+ var listener = this.listeners[index];
+ if (listener) {
+ var callbacks = listener.events[event];
+ if (callbacks) {
+ for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
+ callbacks[i](properties);
+ }
+ }
+ }
+ }
+};
diff --git a/src/examples/timeline/01_basics.html b/src/examples/timeline/01_basics.html
new file mode 100644
index 00000000..42bf45de
--- /dev/null
+++ b/src/examples/timeline/01_basics.html
@@ -0,0 +1,62 @@
+
+
+
+ Timeline demo
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/header.js b/src/header.js
new file mode 100644
index 00000000..37b5a90c
--- /dev/null
+++ b/src/header.js
@@ -0,0 +1,24 @@
+/**
+ * @@name
+ * https://github.com/almende/vis
+ *
+ * A dynamic, browser-based visualization library.
+ *
+ * @version @@version
+ * @date @@date
+ *
+ * @license
+ * Copyright (C) 2011-2013 Almende B.V, http://almende.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
diff --git a/src/range.js b/src/range.js
new file mode 100644
index 00000000..a5044e2b
--- /dev/null
+++ b/src/range.js
@@ -0,0 +1,524 @@
+/**
+ * @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 {Object} [options] See description at Range.setOptions
+ * @extends Controller
+ */
+function Range(options) {
+ this.id = util.randomUUID();
+ this.start = 0; // Number
+ this.end = 0; // Number
+
+ this.options = {
+ min: null,
+ max: null,
+ zoomMin: null,
+ zoomMax: null
+ };
+
+ this.setOptions(options);
+
+ this.listeners = [];
+}
+
+/**
+ * Set options for the range controller
+ * @param {Object} options Available options:
+ * {Number} start Set start value of the range
+ * {Number} end Set end value of 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).
+ */
+Range.prototype.setOptions = function (options) {
+ util.extend(this.options, options);
+
+ if (options.start != null || options.end != null) {
+ this.setRange(options.start, options.end);
+ }
+};
+
+/**
+ * Add listeners for mouse and touch events to the component
+ * @param {Component} component
+ * @param {String} event Available events: 'move', 'zoom'
+ * @param {String} direction Available directions: 'horizontal', 'vertical'
+ */
+Range.prototype.subscribe = function (component, event, direction) {
+ var me = this;
+ var listener;
+
+ if (direction != 'horizontal' && direction != 'vertical') {
+ throw new TypeError('Unknown direction "' + direction + '". ' +
+ 'Choose "horizontal" or "vertical".');
+ }
+
+ //noinspection FallthroughInSwitchStatementJS
+ if (event == 'move') {
+ listener = {
+ component: component,
+ event: event,
+ direction: direction,
+ callback: function (event) {
+ me._onMouseDown(event, listener);
+ },
+ params: {}
+ };
+
+ component.on('mousedown', listener.callback);
+ me.listeners.push(listener);
+ }
+ else if (event == 'zoom') {
+ listener = {
+ component: component,
+ event: event,
+ direction: direction,
+ callback: function (event) {
+ me._onMouseWheel(event, listener);
+ },
+ params: {}
+ };
+
+ component.on('mousewheel', listener.callback);
+ me.listeners.push(listener);
+ }
+ else {
+ throw new TypeError('Unknown event "' + event + '". ' +
+ 'Choose "move" or "zoom".');
+ }
+};
+
+/**
+ * Event handler
+ * @param {String} event name of the event, for example 'click', 'mousemove'
+ * @param {function} callback callback handler, invoked with the raw HTML Event
+ * as parameter.
+ */
+Range.prototype.on = function (event, callback) {
+ events.addListener(this, event, callback);
+};
+
+/**
+ * Trigger an event
+ * @param {String} event name of the event, available events: 'rangechange',
+ * 'rangechanged'
+ * @private
+ */
+Range.prototype._trigger = function (event) {
+ events.trigger(this, event, {
+ start: this.start,
+ end: this.end
+ });
+};
+
+/**
+ * Set a new start and end range
+ * @param {Number} start
+ * @param {Number} end
+ */
+Range.prototype.setRange = function(start, end) {
+ var changed = this._applyRange(start, end);
+ if (changed) {
+ this._trigger('rangechange');
+ this._trigger('rangechanged');
+ }
+};
+
+/**
+ * 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
+ */
+Range.prototype._applyRange = function(start, end) {
+ var newStart = util.cast(start, 'Number');
+ var newEnd = util.cast(end, 'Number');
+ var diff;
+
+ // check for valid number
+ if (newStart == null || isNaN(newStart)) {
+ throw new Error('Invalid start "' + start + '"');
+ }
+ if (newEnd == null || isNaN(newEnd)) {
+ throw new Error('Invalid end "' + end + '"');
+ }
+
+ // prevent start < end
+ if (newEnd < newStart) {
+ newEnd = newStart;
+ }
+
+ // prevent start < min
+ if (this.options.min != null) {
+ var min = this.options.min.valueOf();
+ if (newStart < min) {
+ diff = (min - newStart);
+ newStart += diff;
+ newEnd += diff;
+ }
+ }
+
+ // prevent end > max
+ if (this.options.max != null) {
+ var max = this.options.max.valueOf();
+ if (newEnd > max) {
+ diff = (newEnd - max);
+ newStart -= diff;
+ newEnd -= diff;
+ }
+ }
+
+ // prevent (end-start) > zoomMin
+ if (this.options.zoomMin != null) {
+ var zoomMin = this.options.zoomMin.valueOf();
+ if (zoomMin < 0) {
+ zoomMin = 0;
+ }
+ if ((newEnd - newStart) < zoomMin) {
+ if ((this.end - this.start) > zoomMin) {
+ // zoom to the minimum
+ diff = (zoomMin - (newEnd - newStart));
+ newStart -= diff / 2;
+ newEnd += diff / 2;
+ }
+ else {
+ // ingore this action, we are already zoomed to the minimum
+ newStart = this.start;
+ newEnd = this.end;
+ }
+ }
+ }
+
+ // prevent (end-start) > zoomMin
+ if (this.options.zoomMax != null) {
+ var zoomMax = this.options.zoomMax.valueOf();
+ if (zoomMax < 0) {
+ zoomMax = 0;
+ }
+ if ((newEnd - newStart) > zoomMax) {
+ if ((this.end - this.start) < zoomMax) {
+ // zoom to the maximum
+ diff = ((newEnd - newStart) - zoomMax);
+ newStart += diff / 2;
+ newEnd -= diff / 2;
+ }
+ else {
+ // ingore this action, we are already zoomed to the maximum
+ newStart = this.start;
+ newEnd = this.end;
+ }
+ }
+ }
+
+ var changed = (this.start != newStart || this.end != newEnd);
+
+ this.start = newStart;
+ this.end = newEnd;
+
+ return changed;
+};
+
+/**
+ * Retrieve the current range.
+ * @return {Object} An object with start and end properties
+ */
+Range.prototype.getRange = function() {
+ return {
+ start: this.start,
+ end: this.end
+ };
+};
+
+/**
+ * Calculate the conversion offset and factor for current range, based on
+ * the provided width
+ * @param {Number} width
+ * @returns {{offset: number, factor: number}} conversion
+ */
+Range.prototype.conversion = function (width) {
+ var start = this.start;
+ var end = this.end;
+
+ return Range.conversion(this.start, this.end, width);
+};
+
+/**
+ * Static method to calculate the conversion offset and factor for a range,
+ * based on the provided start, end, and width
+ * @param {Number} start
+ * @param {Number} end
+ * @param {Number} width
+ * @returns {{offset: number, factor: number}} conversion
+ */
+Range.conversion = function (start, end, width) {
+ if (width != 0 && (end - start != 0)) {
+ return {
+ offset: start,
+ factor: width / (end - start)
+ }
+ }
+ else {
+ return {
+ offset: 0,
+ factor: 1
+ };
+ }
+};
+
+/**
+ * Start moving horizontally or vertically
+ * @param {Event} event
+ * @param {Object} listener Listener containing the component and params
+ * @private
+ */
+Range.prototype._onMouseDown = function(event, listener) {
+ event = event || window.event;
+ var params = listener.params;
+
+ // only react on left mouse button down
+ var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
+ if (!leftButtonDown) {
+ return;
+ }
+
+ // get mouse position
+ params.mouseX = util.getPageX(event);
+ params.mouseY = util.getPageY(event);
+ params.previousLeft = 0;
+ params.previousOffset = 0;
+
+ params.moved = false;
+ params.start = this.start;
+ params.end = this.end;
+
+ var frame = listener.component.frame;
+ if (frame) {
+ frame.style.cursor = 'move';
+ }
+
+ // add event listeners to handle moving the contents
+ // we store the function onmousemove and onmouseup in the timeaxis,
+ // so we can remove the eventlisteners lateron in the function onmouseup
+ var me = this;
+ if (!params.onMouseMove) {
+ params.onMouseMove = function (event) {
+ me._onMouseMove(event, listener);
+ };
+ util.addEventListener(document, "mousemove", params.onMouseMove);
+ }
+ if (!params.onMouseUp) {
+ params.onMouseUp = function (event) {
+ me._onMouseUp(event, listener);
+ };
+ util.addEventListener(document, "mouseup", params.onMouseUp);
+ }
+
+ util.preventDefault(event);
+};
+
+/**
+ * Perform moving operating.
+ * This function activated from within the funcion TimeAxis._onMouseDown().
+ * @param {Event} event
+ * @param {Object} listener
+ * @private
+ */
+Range.prototype._onMouseMove = function (event, listener) {
+ event = event || window.event;
+
+ var params = listener.params;
+
+ // calculate change in mouse position
+ var mouseX = util.getPageX(event);
+ var mouseY = util.getPageY(event);
+
+ if (params.mouseX == undefined) {
+ params.mouseX = mouseX;
+ }
+ if (params.mouseY == undefined) {
+ params.mouseY = mouseY;
+ }
+
+ var diffX = mouseX - params.mouseX;
+ var diffY = mouseY - params.mouseY;
+ var diff = (listener.direction == 'horizontal') ? diffX : diffY;
+
+ // if mouse movement is big enough, register it as a "moved" event
+ if (Math.abs(diff) >= 1) {
+ params.moved = true;
+ }
+
+ var interval = (params.end - params.start);
+ var width = (listener.direction == 'horizontal') ?
+ listener.component.width : listener.component.height;
+ var diffRange = -diff / width * interval;
+ this._applyRange(params.start + diffRange, params.end + diffRange);
+
+ // fire a rangechange event
+ this._trigger('rangechange');
+
+ util.preventDefault(event);
+};
+
+/**
+ * Stop moving operating.
+ * This function activated from within the function Range._onMouseDown().
+ * @param {event} event
+ * @param {Object} listener
+ * @private
+ */
+Range.prototype._onMouseUp = function (event, listener) {
+ event = event || window.event;
+
+ var params = listener.params;
+
+ if (listener.component.frame) {
+ listener.component.frame.style.cursor = 'auto';
+ }
+
+ // remove event listeners here, important for Safari
+ if (params.onMouseMove) {
+ util.removeEventListener(document, "mousemove", params.onMouseMove);
+ params.onMouseMove = null;
+ }
+ if (params.onMouseUp) {
+ util.removeEventListener(document, "mouseup", params.onMouseUp);
+ params.onMouseUp = null;
+ }
+ //util.preventDefault(event);
+
+ if (params.moved) {
+ // fire a rangechanged event
+ this._trigger('rangechanged');
+ }
+};
+
+/**
+ * Event handler for mouse wheel event, used to zoom
+ * Code from http://adomas.org/javascript-mouse-wheel/
+ * @param {Event} event
+ * @param {Object} listener
+ * @private
+ */
+Range.prototype._onMouseWheel = function(event, listener) {
+ event = event || window.event;
+
+ // 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;
+ }
+
+ // 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) {
+ var me = this;
+ var zoom = function () {
+ // perform the zoom action. Delta is normally 1 or -1
+ var zoomFactor = delta / 5.0;
+ var zoomAround = null;
+ var frame = listener.component.frame;
+ if (frame) {
+ var size, conversion;
+ if (listener.direction == 'horizontal') {
+ size = listener.component.width;
+ conversion = me.conversion(size);
+ var frameLeft = util.getAbsoluteLeft(frame);
+ var mouseX = util.getPageX(event);
+ zoomAround = (mouseX - frameLeft) / conversion.factor + conversion.offset;
+ }
+ else {
+ size = listener.component.height;
+ conversion = me.conversion(size);
+ var frameTop = util.getAbsoluteTop(frame);
+ var mouseY = util.getPageY(event);
+ zoomAround = ((frameTop + size - mouseY) - frameTop) / conversion.factor + conversion.offset;
+ }
+ }
+
+ me.zoom(zoomFactor, zoomAround);
+ };
+
+ zoom();
+ }
+
+ // Prevent default actions caused by mouse wheel.
+ // That might be ugly, but we handle scrolls somehow
+ // anyway, so don't bother here...
+ util.preventDefault(event);
+};
+
+
+/**
+ * Zoom the range the given zoomfactor 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 zoomfactor = 0.1 or -0.1
+ * @param {Number} zoomFactor Zooming amount. Positive value will zoom in,
+ * negative value will zoom out
+ * @param {Number} zoomAround Value around which will be zoomed. Optional
+ */
+Range.prototype.zoom = function(zoomFactor, zoomAround) {
+ // if zoomAroundDate is not provided, take it half between start Date and end Date
+ if (zoomAround == null) {
+ zoomAround = (this.start + this.end) / 2;
+ }
+
+ // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will
+ // result in a start>=end )
+ if (zoomFactor >= 1) {
+ zoomFactor = 0.9;
+ }
+ if (zoomFactor <= -1) {
+ zoomFactor = -0.9;
+ }
+
+ // adjust a negative factor such that zooming in with 0.1 equals zooming
+ // out with a factor -0.1
+ if (zoomFactor < 0) {
+ zoomFactor = zoomFactor / (1 + zoomFactor);
+ }
+
+ // zoom start and end relative to the zoomAround value
+ var startDiff = (this.start - zoomAround);
+ var endDiff = (this.end - zoomAround);
+
+ // calculate new start and end
+ var newStart = this.start - startDiff * zoomFactor;
+ var newEnd = this.end - endDiff * zoomFactor;
+
+ this.setRange(newStart, newEnd);
+};
+
+/**
+ * Move the range with a given factor to the left or right. Start and end
+ * value will be adjusted. For example, try moveFactor = 0.1 or -0.1
+ * @param {Number} moveFactor Moving amount. Positive value will move right,
+ * negative value will move left
+ */
+Range.prototype.move = function(moveFactor) {
+ // zoom start Date and end Date relative to the zoomAroundDate
+ var diff = (this.end - this.start);
+
+ // apply new values
+ var newStart = this.start + diff * moveFactor;
+ var newEnd = this.end + diff * moveFactor;
+
+ // TODO: reckon with min and max range
+
+ this.start = newStart;
+ this.end = newEnd;
+};
diff --git a/src/stack.js b/src/stack.js
new file mode 100644
index 00000000..b79c419d
--- /dev/null
+++ b/src/stack.js
@@ -0,0 +1,157 @@
+/**
+ * @constructor Stack
+ * Stacks items on top of each other.
+ * @param {ItemSet} parent
+ * @param {Object} [options]
+ */
+function Stack (parent, options) {
+ this.parent = parent;
+ this.options = {
+ order: function (a, b) {
+ return (b.width - a.width) || (a.left - b.left);
+ }
+ };
+
+ this.ordered = []; // ordered items
+
+ this.setOptions(options);
+}
+
+/**
+ * Set options for the stack
+ * @param {Object} options Available options:
+ * {ItemSet} parent
+ * {Number} margin
+ * {function} order Stacking order
+ */
+Stack.prototype.setOptions = function (options) {
+ util.extend(this.options, options);
+
+ // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately
+};
+
+/**
+ * Stack the items such that they don't overlap. The items will have a minimal
+ * distance equal to options.margin.item.
+ */
+Stack.prototype.update = function() {
+ this._order();
+ this._stack();
+};
+
+/**
+ * Order the items. The items are ordered by width first, and by left position
+ * second.
+ * If a custom order function has been provided via the options, then this will
+ * be used.
+ * @private
+ */
+Stack.prototype._order = function() {
+ var items = this.parent.items;
+ if (!items) {
+ throw new Error('Cannot stack items: parent does not contain items');
+ }
+
+ // TODO: store the sorted items, to have less work later on
+ var ordered = [];
+ var index = 0;
+ util.forEach(items, function (item, id) {
+ ordered[index] = item;
+ index++;
+ });
+
+ //if a customer stack order function exists, use it.
+ var order = this.options.order;
+ if (!(typeof this.options.order === 'function')) {
+ throw new Error('Option order must be a function');
+ }
+
+ ordered.sort(order);
+
+ this.ordered = ordered;
+};
+
+/**
+ * Adjust vertical positions of the events such that they don't overlap each
+ * other.
+ * @private
+ */
+Stack.prototype._stack = function() {
+ var i,
+ iMax,
+ ordered = this.ordered,
+ options = this.options,
+ axisOnTop = (options.orientation == 'top'),
+ margin = options.margin && options.margin.item || 0;
+
+ // calculate new, non-overlapping positions
+ for (i = 0, iMax = ordered.length; i < iMax; i++) {
+ var item = ordered[i];
+ var collidingItem = null;
+ do {
+ // TODO: optimize checking for overlap. when there is a gap without items,
+ // you only need to check for items from the next item on, not from zero
+ collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin);
+ if (collidingItem != null) {
+ // There is a collision. Reposition the event above the colliding element
+ if (axisOnTop) {
+ item.top = collidingItem.top + collidingItem.height + margin;
+ }
+ else {
+ item.top = collidingItem.top - item.height - margin;
+ }
+ }
+ } while (collidingItem);
+ }
+};
+
+/**
+ * Check if the destiny position of given item overlaps with any
+ * of the other items from index itemStart to itemEnd.
+ * @param {Array} items Array with items
+ * @param {int} itemIndex Number of the item to be checked for overlap
+ * @param {int} itemStart First item to be checked.
+ * @param {int} itemEnd Last item to be checked.
+ * @return {Object | null} colliding item, or undefined when no collisions
+ * @param {Number} margin A minimum required margin.
+ * If margin is provided, the two items will be
+ * marked colliding when they overlap or
+ * when the margin between the two is smaller than
+ * the requested margin.
+ */
+Stack.prototype.checkOverlap = function(items, itemIndex, itemStart, itemEnd, margin) {
+ var collision = this.collision;
+
+ // we loop from end to start, as we suppose that the chance of a
+ // collision is larger for items at the end, so check these first.
+ var a = items[itemIndex];
+ for (var i = itemEnd; i >= itemStart; i--) {
+ var b = items[i];
+ if (collision(a, b, margin)) {
+ if (i != itemIndex) {
+ return b;
+ }
+ }
+ }
+
+ return null;
+};
+
+/**
+ * Test if the two provided items collide
+ * The items must have parameters left, width, top, and height.
+ * @param {Component} a The first item
+ * @param {Component} 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.prototype.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);
+};
diff --git a/src/timestep.js b/src/timestep.js
new file mode 100644
index 00000000..4360a9c7
--- /dev/null
+++ b/src/timestep.js
@@ -0,0 +1,450 @@
+/**
+ * @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
+ */
+TimeStep = function(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";
+ return;
+ }
+
+ 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;
+};
+
+
+/**
+ * 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;
+ }
+
+ 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
+ */
+TimeStep.prototype.snap = function(date) {
+ if (this.scale == TimeStep.SCALE.YEAR) {
+ var year = date.getFullYear() + Math.round(date.getMonth() / 12);
+ date.setFullYear(Math.round(year / this.step) * this.step);
+ date.setMonth(0);
+ date.setDate(0);
+ date.setHours(0);
+ date.setMinutes(0);
+ date.setSeconds(0);
+ date.setMilliseconds(0);
+ }
+ else if (this.scale == TimeStep.SCALE.MONTH) {
+ if (date.getDate() > 15) {
+ date.setDate(1);
+ date.setMonth(date.getMonth() + 1);
+ // important: first set Date to 1, after that change the month.
+ }
+ else {
+ date.setDate(1);
+ }
+
+ date.setHours(0);
+ date.setMinutes(0);
+ date.setSeconds(0);
+ date.setMilliseconds(0);
+ }
+ else if (this.scale == TimeStep.SCALE.DAY ||
+ this.scale == TimeStep.SCALE.WEEKDAY) {
+ //noinspection FallthroughInSwitchStatementJS
+ switch (this.step) {
+ case 5:
+ case 2:
+ date.setHours(Math.round(date.getHours() / 24) * 24); break;
+ default:
+ date.setHours(Math.round(date.getHours() / 12) * 12); break;
+ }
+ date.setMinutes(0);
+ date.setSeconds(0);
+ date.setMilliseconds(0);
+ }
+ else if (this.scale == TimeStep.SCALE.HOUR) {
+ switch (this.step) {
+ case 4:
+ date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
+ default:
+ date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
+ }
+ date.setSeconds(0);
+ date.setMilliseconds(0);
+ } else if (this.scale == TimeStep.SCALE.MINUTE) {
+ //noinspection FallthroughInSwitchStatementJS
+ switch (this.step) {
+ case 15:
+ case 10:
+ date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
+ date.setSeconds(0);
+ break;
+ case 5:
+ date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
+ default:
+ date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
+ }
+ date.setMilliseconds(0);
+ }
+ else if (this.scale == TimeStep.SCALE.SECOND) {
+ //noinspection FallthroughInSwitchStatementJS
+ switch (this.step) {
+ case 15:
+ case 10:
+ date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
+ date.setMilliseconds(0);
+ break;
+ case 5:
+ date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
+ default:
+ date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
+ }
+ }
+ else if (this.scale == TimeStep.SCALE.MILLISECOND) {
+ var step = this.step > 5 ? this.step / 2 : 1;
+ date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
+ }
+};
+
+/**
+ * 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 '';
+ }
+};
+
+
+/**
+ * Returns formatted text for the major axislabel, 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
+ */
+TimeStep.prototype.getLabelMajor = function(date) {
+ if (date == undefined) {
+ date = this.current;
+ }
+
+ //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 '';
+ }
+};
diff --git a/src/util.js b/src/util.js
new file mode 100644
index 00000000..44f58952
--- /dev/null
+++ b/src/util.js
@@ -0,0 +1,732 @@
+
+// create namespace
+var util = {};
+
+/**
+ * Test whether given object is a number
+ * @param {*} object
+ * @return {Boolean} isNumber
+ */
+util.isNumber = function isNumber(object) {
+ return (object instanceof Number || typeof object == 'number');
+};
+
+/**
+ * Test whether given object is a string
+ * @param {*} object
+ * @return {Boolean} isString
+ */
+util.isString = function isString(object) {
+ return (object instanceof String || typeof object == 'string');
+};
+
+/**
+ * Test whether given object is a Date, or a String containing a Date
+ * @param {Date | String} object
+ * @return {Boolean} isDate
+ */
+util.isDate = function isDate(object) {
+ if (object instanceof Date) {
+ return true;
+ }
+ else if (util.isString(object)) {
+ // test whether this string contains a date
+ var match = ASPDateRegex.exec(object);
+ if (match) {
+ return true;
+ }
+ else if (!isNaN(Date.parse(object))) {
+ return true;
+ }
+ }
+
+ return false;
+};
+
+/**
+ * Test whether given object is an instance of google.visualization.DataTable
+ * @param {*} object
+ * @return {Boolean} isDataTable
+ */
+util.isDataTable = function isDataTable(object) {
+ return (typeof (google) !== 'undefined') &&
+ (google.visualization) &&
+ (google.visualization.DataTable) &&
+ (object instanceof google.visualization.DataTable);
+};
+
+/**
+ * Create a semi UUID
+ * source: http://stackoverflow.com/a/105074/1262753
+ * @return {String} uuid
+ */
+util.randomUUID = function randomUUID () {
+ var S4 = function () {
+ return Math.floor(
+ Math.random() * 0x10000 /* 65536 */
+ ).toString(16);
+ };
+
+ return (
+ S4() + S4() + '-' +
+ S4() + '-' +
+ S4() + '-' +
+ S4() + '-' +
+ S4() + S4() + S4()
+ );
+};
+
+/**
+ * Extend object a with the properties of object b
+ * @param {Object} a
+ * @param {Object} b
+ * @return {Object} a
+ */
+util.extend = function (a, b) {
+ for (var prop in b) {
+ if (b.hasOwnProperty(prop)) {
+ a[prop] = b[prop];
+ }
+ }
+ return a;
+};
+
+/**
+ * Cast an object to another type
+ * @param {Boolean | Number | String | Date | Null | undefined} object
+ * @param {String | function | undefined} type Name of the type or a cast
+ * function. Available types:
+ * 'Boolean', 'Number', 'String',
+ * 'Date', ISODate', 'ASPDate'.
+ * @return {*} object
+ * @throws Error
+ */
+util.cast = function cast(object, type) {
+ if (object === undefined) {
+ return undefined;
+ }
+ if (object === null) {
+ return null;
+ }
+
+ if (!type) {
+ return object;
+ }
+ if (typeof type == 'function') {
+ return type(object);
+ }
+
+ //noinspection FallthroughInSwitchStatementJS
+ switch (type) {
+ case 'boolean':
+ case 'Boolean':
+ return Boolean(object);
+
+ case 'number':
+ case 'Number':
+ return Number(object);
+
+ case 'string':
+ case 'String':
+ return String(object);
+
+ case 'Date':
+ if (util.isNumber(object)) {
+ return new Date(object);
+ }
+ if (object instanceof Date) {
+ return new Date(object.valueOf());
+ }
+ if (util.isString(object)) {
+ // parse ASP.Net Date pattern,
+ // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
+ // code from http://momentjs.com/
+ var match = ASPDateRegex.exec(object);
+ if (match) {
+ return new Date(Number(match[1]));
+ }
+ else {
+ return new Date(object);
+ }
+ }
+ else {
+ throw new Error(
+ 'Cannot cast object of type ' + util.getType(object) +
+ ' to type Date');
+ }
+
+ case 'ISODate':
+ if (object instanceof Date) {
+ return object.toISOString();
+ }
+ else if (util.isNumber(object) || util.isString(Object)) {
+ return (new Date(object)).toISOString()
+ }
+ else {
+ throw new Error(
+ 'Cannot cast object of type ' + util.getType(object) +
+ ' to type ISODate');
+ }
+
+ case 'ASPDate':
+ if (object instanceof Date) {
+ return '/Date(' + object.valueOf() + ')/';
+ }
+ else if (util.isNumber(object) || util.isString(Object)) {
+ return '/Date(' + (new Date(object)).valueOf() + ')/';
+ }
+ else {
+ throw new Error(
+ 'Cannot cast object of type ' + util.getType(object) +
+ ' to type ASPDate');
+ }
+
+ default:
+ throw new Error('Cannot cast object of type ' + util.getType(object) +
+ ' to type "' + type + '"');
+ }
+};
+
+var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
+
+/**
+ * Get the type of an object
+ * @param {*} object
+ * @return {String} type
+ */
+util.getType = function getType(object) {
+ var type = typeof object;
+
+ if (type == 'object') {
+ if (object == null) {
+ return 'null';
+ }
+ if (object && object.constructor && object.constructor.name) {
+ return object.constructor.name;
+ }
+ }
+
+ return type;
+};
+
+/**
+ * Retrieve the absolute left value of a DOM element
+ * @param {Element} elem A dom element, for example a div
+ * @return {number} left The absolute left position of this element
+ * in the browser page.
+ */
+util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
+ var doc = document.documentElement;
+ var body = document.body;
+
+ var left = elem.offsetLeft;
+ var e = elem.offsetParent;
+ while (e != null && e != body && e != doc) {
+ left += e.offsetLeft;
+ left -= e.scrollLeft;
+ e = e.offsetParent;
+ }
+ return left;
+};
+
+/**
+ * Retrieve the absolute top value of a DOM element
+ * @param {Element} elem A dom element, for example a div
+ * @return {number} top The absolute top position of this element
+ * in the browser page.
+ */
+util.getAbsoluteTop = function getAbsoluteTop (elem) {
+ var doc = document.documentElement;
+ var body = document.body;
+
+ var top = elem.offsetTop;
+ var e = elem.offsetParent;
+ while (e != null && e != body && e != doc) {
+ top += e.offsetTop;
+ top -= e.scrollTop;
+ e = e.offsetParent;
+ }
+ return top;
+};
+
+/**
+ * Get the absolute, vertical mouse position from an event.
+ * @param {Event} event
+ * @return {Number} pageY
+ */
+util.getPageY = function getPageY (event) {
+ if ('pageY' in event) {
+ return event.pageY;
+ }
+ else {
+ var clientY;
+ if (('targetTouches' in event) && event.targetTouches.length) {
+ clientY = event.targetTouches[0].clientY;
+ }
+ else {
+ clientY = event.clientY;
+ }
+
+ var doc = document.documentElement;
+ var body = document.body;
+ return clientY +
+ ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
+ ( doc && doc.clientTop || body && body.clientTop || 0 );
+ }
+};
+
+/**
+ * Get the absolute, horizontal mouse position from an event.
+ * @param {Event} event
+ * @return {Number} pageX
+ */
+util.getPageX = function getPageX (event) {
+ if ('pageY' in event) {
+ return event.pageX;
+ }
+ else {
+ var clientX;
+ if (('targetTouches' in event) && event.targetTouches.length) {
+ clientX = event.targetTouches[0].clientX;
+ }
+ else {
+ clientX = event.clientX;
+ }
+
+ var doc = document.documentElement;
+ var body = document.body;
+ return clientX +
+ ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
+ ( doc && doc.clientLeft || body && body.clientLeft || 0 );
+ }
+};
+
+/**
+ * add a className to the given elements style
+ * @param {Element} elem
+ * @param {String} className
+ */
+util.addClassName = function addClassName(elem, className) {
+ var classes = elem.className.split(' ');
+ if (classes.indexOf(className) == -1) {
+ classes.push(className); // add the class to the array
+ elem.className = classes.join(' ');
+ }
+};
+
+/**
+ * add a className to the given elements style
+ * @param {Element} elem
+ * @param {String} className
+ */
+util.removeClassName = function removeClassname(elem, className) {
+ var classes = elem.className.split(' ');
+ var index = classes.indexOf(className);
+ if (index != -1) {
+ classes.splice(index, 1); // remove the class from the array
+ elem.className = classes.join(' ');
+ }
+};
+
+/**
+ * For each method for both arrays and objects.
+ * In case of an array, the built-in Array.forEach() is applied.
+ * In case of an Object, the method loops over all properties of the object.
+ * @param {Object | Array} object An Object or Array
+ * @param {function} callback Callback method, called for each item in
+ * the object or array with three parameters:
+ * callback(value, index, object)
+ */
+util.forEach = function forEach (object, callback) {
+ if (object instanceof Array) {
+ // array
+ object.forEach(callback);
+ }
+ else {
+ // object
+ for (var key in object) {
+ if (object.hasOwnProperty(key)) {
+ callback(object[key], key, object);
+ }
+ }
+ }
+};
+
+/**
+ * Update a property in an object
+ * @param {Object} object
+ * @param {String} key
+ * @param {*} value
+ * @return {Boolean} changed
+ */
+util.updateProperty = function updateProp (object, key, value) {
+ if (object[key] !== value) {
+ object[key] = value;
+ return true;
+ }
+ else {
+ return false;
+ }
+};
+
+/**
+ * Add and event listener. Works for all browsers
+ * @param {Element} element An html element
+ * @param {string} action The action, for example "click",
+ * without the prefix "on"
+ * @param {function} listener The callback function to be executed
+ * @param {boolean} [useCapture]
+ */
+util.addEventListener = function addEventListener(element, action, listener, useCapture) {
+ if (element.addEventListener) {
+ if (useCapture === undefined)
+ useCapture = false;
+
+ if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
+ action = "DOMMouseScroll"; // For Firefox
+ }
+
+ element.addEventListener(action, listener, useCapture);
+ } else {
+ element.attachEvent("on" + action, listener); // IE browsers
+ }
+};
+
+/**
+ * Remove an event listener from an element
+ * @param {Element} element An html dom element
+ * @param {string} action The name of the event, for example "mousedown"
+ * @param {function} listener The listener function
+ * @param {boolean} [useCapture]
+ */
+util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
+ if (element.removeEventListener) {
+ // non-IE browsers
+ if (useCapture === undefined)
+ useCapture = false;
+
+ if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
+ action = "DOMMouseScroll"; // For Firefox
+ }
+
+ element.removeEventListener(action, listener, useCapture);
+ } else {
+ // IE browsers
+ element.detachEvent("on" + action, listener);
+ }
+};
+
+
+/**
+ * Get HTML element which is the target of the event
+ * @param {Event} event
+ * @return {Element} target element
+ */
+util.getTarget = function getTarget(event) {
+ // code from http://www.quirksmode.org/js/events_properties.html
+ if (!event) {
+ event = window.event;
+ }
+
+ var target;
+
+ if (event.target) {
+ target = event.target;
+ }
+ else if (event.srcElement) {
+ target = event.srcElement;
+ }
+
+ if (target.nodeType != undefined && target.nodeType == 3) {
+ // defeat Safari bug
+ target = target.parentNode;
+ }
+
+ return target;
+};
+
+/**
+ * Stop event propagation
+ */
+util.stopPropagation = function stopPropagation(event) {
+ if (!event)
+ event = window.event;
+
+ if (event.stopPropagation) {
+ event.stopPropagation(); // non-IE browsers
+ }
+ else {
+ event.cancelBubble = true; // IE browsers
+ }
+};
+
+
+/**
+ * Cancels the event if it is cancelable, without stopping further propagation of the event.
+ */
+util.preventDefault = function preventDefault (event) {
+ if (!event)
+ event = window.event;
+
+ if (event.preventDefault) {
+ event.preventDefault(); // non-IE browsers
+ }
+ else {
+ event.returnValue = false; // IE browsers
+ }
+};
+
+
+util.option = {};
+
+/**
+ * Cast a value as boolean
+ * @param {Boolean | function | undefined} value
+ * @param {Boolean} [defaultValue]
+ * @returns {Boolean} bool
+ */
+util.option.asBoolean = function (value, defaultValue) {
+ if (typeof value == 'function') {
+ value = value();
+ }
+
+ if (value != null) {
+ return (value != false);
+ }
+
+ return defaultValue || null;
+};
+
+/**
+ * Cast a value as string
+ * @param {String | function | undefined} value
+ * @param {String} [defaultValue]
+ * @returns {String} str
+ */
+util.option.asString = function (value, defaultValue) {
+ if (typeof value == 'function') {
+ value = value();
+ }
+
+ if (value != null) {
+ return String(value);
+ }
+
+ return defaultValue || null;
+};
+
+/**
+ * Cast a size or location in pixels or a percentage
+ * @param {String | Number | function | undefined} value
+ * @param {String} [defaultValue]
+ * @returns {String} size
+ */
+util.option.asSize = function (value, defaultValue) {
+ if (typeof value == 'function') {
+ value = value();
+ }
+
+ if (util.isString(value)) {
+ return value;
+ }
+ else if (util.isNumber(value)) {
+ return value + 'px';
+ }
+ else {
+ return defaultValue || null;
+ }
+};
+
+/**
+ * Cast a value as DOM element
+ * @param {HTMLElement | function | undefined} value
+ * @param {HTMLElement} [defaultValue]
+ * @returns {HTMLElement | null} dom
+ */
+util.option.asElement = function (value, defaultValue) {
+ if (typeof value == 'function') {
+ value = value();
+ }
+
+ return value || defaultValue || null;
+};
+
+
+// Internet Explorer 8 and older does not support Array.indexOf, so we define
+// it here in that case.
+// http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
+if(!Array.prototype.indexOf) {
+ Array.prototype.indexOf = function(obj){
+ for(var i = 0; i < this.length; i++){
+ if(this[i] == obj){
+ return i;
+ }
+ }
+ return -1;
+ };
+
+ try {
+ console.log("Warning: Ancient browser detected. Please update your browser");
+ }
+ catch (err) {
+ }
+}
+
+// Internet Explorer 8 and older does not support Array.forEach, so we define
+// it here in that case.
+// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
+if (!Array.prototype.forEach) {
+ Array.prototype.forEach = function(fn, scope) {
+ for(var i = 0, len = this.length; i < len; ++i) {
+ fn.call(scope || this, this[i], i, this);
+ }
+ }
+}
+
+// Internet Explorer 8 and older does not support Array.map, so we define it
+// here in that case.
+// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map
+// Production steps of ECMA-262, Edition 5, 15.4.4.19
+// Reference: http://es5.github.com/#x15.4.4.19
+if (!Array.prototype.map) {
+ Array.prototype.map = function(callback, thisArg) {
+
+ var T, A, k;
+
+ if (this == null) {
+ throw new TypeError(" this is null or not defined");
+ }
+
+ // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
+ var O = Object(this);
+
+ // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
+ // 3. Let len be ToUint32(lenValue).
+ var len = O.length >>> 0;
+
+ // 4. If IsCallable(callback) is false, throw a TypeError exception.
+ // See: http://es5.github.com/#x9.11
+ if (typeof callback !== "function") {
+ throw new TypeError(callback + " is not a function");
+ }
+
+ // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
+ if (thisArg) {
+ T = thisArg;
+ }
+
+ // 6. Let A be a new array created as if by the expression new Array(len) where Array is
+ // the standard built-in constructor with that name and len is the value of len.
+ A = new Array(len);
+
+ // 7. Let k be 0
+ k = 0;
+
+ // 8. Repeat, while k < len
+ while(k < len) {
+
+ var kValue, mappedValue;
+
+ // a. Let Pk be ToString(k).
+ // This is implicit for LHS operands of the in operator
+ // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
+ // This step can be combined with c
+ // c. If kPresent is true, then
+ if (k in O) {
+
+ // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
+ kValue = O[ k ];
+
+ // ii. Let mappedValue be the result of calling the Call internal method of callback
+ // with T as the this value and argument list containing kValue, k, and O.
+ mappedValue = callback.call(T, kValue, k, O);
+
+ // iii. Call the DefineOwnProperty internal method of A with arguments
+ // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true},
+ // and false.
+
+ // In browsers that support Object.defineProperty, use the following:
+ // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true });
+
+ // For best browser support, use the following:
+ A[ k ] = mappedValue;
+ }
+ // d. Increase k by 1.
+ k++;
+ }
+
+ // 9. return A
+ return A;
+ };
+}
+
+// Internet Explorer 8 and older does not support Array.filter, so we define it
+// here in that case.
+// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter
+if (!Array.prototype.filter) {
+ Array.prototype.filter = function(fun /*, thisp */) {
+ "use strict";
+
+ if (this == null) {
+ throw new TypeError();
+ }
+
+ var t = Object(this);
+ var len = t.length >>> 0;
+ if (typeof fun != "function") {
+ throw new TypeError();
+ }
+
+ var res = [];
+ var thisp = arguments[1];
+ for (var i = 0; i < len; i++) {
+ if (i in t) {
+ var val = t[i]; // in case fun mutates this
+ if (fun.call(thisp, val, i, t))
+ res.push(val);
+ }
+ }
+
+ return res;
+ };
+}
+
+
+// Internet Explorer 8 and older does not support Object.keys, so we define it
+// here in that case.
+// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys
+if (!Object.keys) {
+ Object.keys = (function () {
+ var hasOwnProperty = Object.prototype.hasOwnProperty,
+ hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
+ dontEnums = [
+ 'toString',
+ 'toLocaleString',
+ 'valueOf',
+ 'hasOwnProperty',
+ 'isPrototypeOf',
+ 'propertyIsEnumerable',
+ 'constructor'
+ ],
+ dontEnumsLength = dontEnums.length;
+
+ return function (obj) {
+ if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) {
+ throw new TypeError('Object.keys called on non-object');
+ }
+
+ var result = [];
+
+ for (var prop in obj) {
+ if (hasOwnProperty.call(obj, prop)) result.push(prop);
+ }
+
+ if (hasDontEnumBug) {
+ for (var i=0; i < dontEnumsLength; i++) {
+ if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]);
+ }
+ }
+ return result;
+ }
+ })()
+}
diff --git a/src/visualization/timeline.js b/src/visualization/timeline.js
new file mode 100644
index 00000000..b8874ea6
--- /dev/null
+++ b/src/visualization/timeline.js
@@ -0,0 +1,118 @@
+/**
+ * Create a timeline visualization
+ * @param {HTMLElement} container
+ * @param {DataSet | Array | DataTable} [data]
+ * @param {Object} [options] See Timeline.setOptions for the available options.
+ * @constructor
+ */
+function Timeline (container, data, options) {
+ var me = this;
+ this.options = {
+ orientation: 'bottom',
+ zoomMin: 10, // milliseconds
+ zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
+ moveable: true,
+ zoomable: true
+ };
+
+ // controller
+ this.controller = new Controller();
+
+ // main panel
+ if (!container) {
+ throw new Error('No container element provided');
+ }
+ this.main = new RootPanel(container, {
+ autoResize: false,
+ height: function () {
+ return me.timeaxis.height + me.itemset.height;
+ }
+ });
+ this.controller.add(this.main);
+
+ // range
+ var now = moment().minutes(0).seconds(0).milliseconds(0);
+ var start = options.start && options.start.valueOf() || now.clone().add('days', -3).valueOf();
+ var end = options.end && options.end.valueOf() || moment(start).clone().add('days', 7).valueOf();
+ // TODO: if start and end are not provided, calculate range from the dataset
+ this.range = new Range({
+ start: start,
+ end: end
+ });
+ // TODO: reckon with options moveable and zoomable
+ this.range.subscribe(this.main, 'move', 'horizontal');
+ this.range.subscribe(this.main, 'zoom', 'horizontal');
+ this.range.on('rangechange', function () {
+ // TODO: fix the delay in reflow/repaint, does not feel snappy
+ me.controller.requestReflow();
+ });
+ this.range.on('rangechanged', function () {
+ me.controller.requestReflow();
+ });
+
+ // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable
+
+ // time axis
+ this.timeaxis = new TimeAxis(this.main, null, {
+ orientation: this.options.orientation,
+ range: this.range
+ });
+ this.timeaxis.setRange(this.range);
+ this.controller.add(this.timeaxis);
+
+ // items panel
+ this.itemset = new ItemSet(this.main, [this.timeaxis], {
+ orientation: this.options.orientation,
+ range: this.range,
+ data: data
+ });
+ this.itemset.setRange(this.range);
+ if (data) {
+ this.setData(data);
+ }
+ this.controller.add(this.itemset);
+
+ this.setOptions(options);
+}
+
+/**
+ * Set options
+ * @param {Object} options TODO: describe the available options
+ */
+Timeline.prototype.setOptions = function (options) {
+ util.extend(this.options, options);
+
+ // update options the timeaxis
+ this.timeaxis.setOptions(this.options);
+
+ // update options for the range
+ this.range.setOptions(this.options);
+
+ // update options the itemset
+ var top,
+ me = this;
+ if (this.options.orientation == 'top') {
+ top = function () {
+ return me.timeaxis.height;
+ }
+ }
+ else {
+ top = function () {
+ return me.main.height - me.timeaxis.height - me.itemset.height;
+ }
+ }
+ this.itemset.setOptions({
+ orientation: this.options.orientation,
+ top: top
+ });
+
+ this.controller.repaint();
+};
+
+/**
+ * Set data
+ * @param {DataSet | Array | DataTable} data
+ */
+Timeline.prototype.setData = function(data) {
+ this.itemset.setData(data);
+};
diff --git a/test/dataset.html b/test/dataset.html
new file mode 100644
index 00000000..927b69d5
--- /dev/null
+++ b/test/dataset.html
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/timeline.html b/test/timeline.html
new file mode 100644
index 00000000..e3fee0a7
--- /dev/null
+++ b/test/timeline.html
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/timestep.html b/test/timestep.html
new file mode 100644
index 00000000..0ebe2113
--- /dev/null
+++ b/test/timestep.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file