-
vis.js examples
+
vis.js timeline examples
01_basic.html
02_dataset.html
diff --git a/img/gallery/graph/17_network_info.png b/img/gallery/graph/17_network_info.png
index 1d0ee70b..aabff0e6 100644
Binary files a/img/gallery/graph/17_network_info.png and b/img/gallery/graph/17_network_info.png differ
diff --git a/index.html b/index.html
index 167ebbeb..65261521 100644
--- a/index.html
+++ b/index.html
@@ -76,19 +76,19 @@ bower install vis
Development
- (version 0.1.0)
+ (version 0.2.0)
|
- 384 kB, uncompressed with comments
+ 441 kB, uncompressed with comments
|
Production
- (version 0.1.0)
+ (version 0.2.0)
|
- 34 kB, minified and gzipped
+ 39 kB, minified and gzipped
|
diff --git a/vis.js b/vis.js
index b4a4d086..eeb9980b 100644
--- a/vis.js
+++ b/vis.js
@@ -4,8 +4,8 @@
*
* A dynamic, browser-based visualization library.
*
- * @version 0.1.0
- * @date 2013-06-20
+ * @version 0.2.0
+ * @date 2013-09-20
*
* @license
* Copyright (C) 2011-2013 Almende B.V, http://almende.com
@@ -23,5698 +23,7018 @@
* the License.
*/
(function(e){if("function"==typeof bootstrap)bootstrap("vis",e);else if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeVis=e}else"undefined"!=typeof window?window.vis=e():global.vis=e()})(function(){var define,ses,bootstrap,module,exports;
-return (function(e,t,n){function i(n,s){if(!t[n]){if(!e[n]){var o=typeof require=="function"&&require;if(!s&&o)return o(n,!0);if(r)return r(n,!0);throw new Error("Cannot find module '"+n+"'")}var u=t[n]={exports:{}};e[n][0].call(u.exports,function(t){var r=e[n][1][t];return i(r?r:t)},u,u.exports)}return t[n].exports}var r=typeof require=="function"&&require;for(var s=0;s
;
+ * Licensed under the MIT license */
-/**
- * utility functions
- */
-var util = {};
+(function(window, undefined) {
+ 'use strict';
/**
- * Test whether given object is a number
- * @param {*} object
- * @return {Boolean} isNumber
+ * Hammer
+ * use this to create instances
+ * @param {HTMLElement} element
+ * @param {Object} options
+ * @returns {Hammer.Instance}
+ * @constructor
*/
-util.isNumber = function isNumber(object) {
- return (object instanceof Number || typeof object == 'number');
+var Hammer = function(element, options) {
+ return new Hammer.Instance(element, options || {});
};
-/**
- * Test whether given object is a string
- * @param {*} object
- * @return {Boolean} isString
- */
-util.isString = function isString(object) {
- return (object instanceof String || typeof object == 'string');
+// default settings
+Hammer.defaults = {
+ // add styles and attributes to the element to prevent the browser from doing
+ // its native behavior. this doesnt prevent the scrolling, but cancels
+ // the contextmenu, tap highlighting etc
+ // set to false to disable this
+ stop_browser_behavior: {
+ // this also triggers onselectstart=false for IE
+ userSelect: 'none',
+ // this makes the element blocking in IE10 >, you could experiment with the value
+ // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241
+ touchAction: 'none',
+ touchCallout: 'none',
+ contentZooming: 'none',
+ userDrag: 'none',
+ tapHighlightColor: 'rgba(0,0,0,0)'
+ }
+
+ // more settings are defined per gesture at gestures.js
};
+// detect touchevents
+Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled;
+Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
+
+// dont use mouseevents on mobile devices
+Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
+Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX);
+
+// eventtypes per touchevent (start, move, end)
+// are filled by Hammer.event.determineEventTypes on setup
+Hammer.EVENT_TYPES = {};
+
+// direction defines
+Hammer.DIRECTION_DOWN = 'down';
+Hammer.DIRECTION_LEFT = 'left';
+Hammer.DIRECTION_UP = 'up';
+Hammer.DIRECTION_RIGHT = 'right';
+
+// pointer type
+Hammer.POINTER_MOUSE = 'mouse';
+Hammer.POINTER_TOUCH = 'touch';
+Hammer.POINTER_PEN = 'pen';
+
+// touch event defines
+Hammer.EVENT_START = 'start';
+Hammer.EVENT_MOVE = 'move';
+Hammer.EVENT_END = 'end';
+
+// hammer document where the base events are added at
+Hammer.DOCUMENT = document;
+
+// plugins namespace
+Hammer.plugins = {};
+
+// if the window events are set...
+Hammer.READY = false;
+
/**
- * Test whether given object is a Date, or a String containing a Date
- * @param {Date | String} object
- * @return {Boolean} isDate
+ * setup events to detect gestures on the document
*/
-util.isDate = function isDate(object) {
- if (object instanceof Date) {
- return true;
+function setup() {
+ if(Hammer.READY) {
+ return;
}
- 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;
+
+ // find what eventtypes we add listeners to
+ Hammer.event.determineEventTypes();
+
+ // Register all gestures inside Hammer.gestures
+ for(var name in Hammer.gestures) {
+ if(Hammer.gestures.hasOwnProperty(name)) {
+ Hammer.detection.register(Hammer.gestures[name]);
}
}
- return false;
-};
+ // Add touch events on the document
+ Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect);
+ Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect);
+
+ // Hammer is ready...!
+ Hammer.READY = true;
+}
/**
- * Test whether given object is an instance of google.visualization.DataTable
- * @param {*} object
- * @return {Boolean} isDataTable
+ * create new hammer instance
+ * all methods should return the instance itself, so it is chainable.
+ * @param {HTMLElement} element
+ * @param {Object} [options={}]
+ * @returns {Hammer.Instance}
+ * @constructor
*/
-util.isDataTable = function isDataTable(object) {
- return (typeof (google) !== 'undefined') &&
- (google.visualization) &&
- (google.visualization.DataTable) &&
- (object instanceof google.visualization.DataTable);
+Hammer.Instance = function(element, options) {
+ var self = this;
+
+ // setup HammerJS window events and register all gestures
+ // this also sets up the default options
+ setup();
+
+ this.element = element;
+
+ // start/stop detection option
+ this.enabled = true;
+
+ // merge options
+ this.options = Hammer.utils.extend(
+ Hammer.utils.extend({}, Hammer.defaults),
+ options || {});
+
+ // add some css to the element to prevent the browser from doing its native behavoir
+ if(this.options.stop_browser_behavior) {
+ Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
+ }
+
+ // start detection on touchstart
+ Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) {
+ if(self.enabled) {
+ Hammer.detection.startDetect(self, ev);
+ }
+ });
+
+ // return instance
+ return this;
+};
+
+
+Hammer.Instance.prototype = {
+ /**
+ * bind events to the instance
+ * @param {String} gesture
+ * @param {Function} handler
+ * @returns {Hammer.Instance}
+ */
+ on: function onEvent(gesture, handler){
+ var gestures = gesture.split(' ');
+ for(var t=0; t 0 && eventType == Hammer.EVENT_END) {
+ eventType = Hammer.EVENT_MOVE;
}
- else {
- return moment(object); // parse string
+ // no touches, force the end event
+ else if(!count_touches) {
+ eventType = Hammer.EVENT_END;
}
- }
- else {
- throw new Error(
- 'Cannot convert object of type ' + util.getType(object) +
- ' to type Date');
- }
- case 'ISODate':
- if (util.isNumber(object)) {
- return new Date(object);
- }
- else if (object instanceof Date) {
- return object.toISOString();
- }
- else if (moment.isMoment(object)) {
- return object.toDate().toISOString();
- }
- else if (util.isString(object)) {
- match = ASPDateRegex.exec(object);
- if (match) {
- // object is an ASP date
- return new Date(Number(match[1])).toISOString(); // parse number
+ // because touchend has no touches, and we often want to use these in our gestures,
+ // we send the last move event as our eventData in touchend
+ if(!count_touches && last_move_event !== null) {
+ ev = last_move_event;
}
+ // store the last move event
else {
- return new Date(object).toISOString(); // parse string
+ last_move_event = ev;
}
- }
- else {
- throw new Error(
- 'Cannot convert object of type ' + util.getType(object) +
- ' to type ISODate');
- }
- case 'ASPDate':
- if (util.isNumber(object)) {
- return '/Date(' + object + ')/';
- }
- else if (object instanceof Date) {
- return '/Date(' + object.valueOf() + ')/';
- }
- else if (util.isString(object)) {
- match = ASPDateRegex.exec(object);
- var value;
- if (match) {
- // object is an ASP date
- value = new Date(Number(match[1])).valueOf(); // parse number
- }
- else {
- value = new Date(object).valueOf(); // parse string
+ // trigger the handler
+ handler.call(Hammer.detection, self.collectEventData(element, eventType, ev));
+
+ // remove pointerevent from list
+ if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) {
+ count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
}
- return '/Date(' + value + ')/';
- }
- else {
- throw new Error(
- 'Cannot convert object of type ' + util.getType(object) +
- ' to type ASPDate');
}
- default:
- throw new Error('Cannot convert object of type ' + util.getType(object) +
- ' to type "' + type + '"');
- }
-};
+ //debug(sourceEventType +" "+ eventType);
-// parse ASP.Net Date pattern,
-// for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
-// code from http://momentjs.com/
-var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
+ // on the end we reset everything
+ if(!count_touches) {
+ last_move_event = null;
+ enable_detect = false;
+ touch_triggered = false;
+ Hammer.PointerEvent.reset();
+ }
+ });
+ },
-/**
- * Get the type of an object, for example util.getType([]) returns 'Array'
- * @param {*} object
- * @return {String} type
- */
-util.getType = function getType(object) {
- var type = typeof object;
- if (type == 'object') {
- if (object == null) {
- return 'null';
- }
- if (object instanceof Boolean) {
- return 'Boolean';
+ /**
+ * we have different events for each device/browser
+ * determine what we need and set them in the Hammer.EVENT_TYPES constant
+ */
+ determineEventTypes: function determineEventTypes() {
+ // determine the eventtype we want to set
+ var types;
+
+ // pointerEvents magic
+ if(Hammer.HAS_POINTEREVENTS) {
+ types = Hammer.PointerEvent.getEvents();
+ }
+ // on Android, iOS, blackberry, windows mobile we dont want any mouseevents
+ else if(Hammer.NO_MOUSEEVENTS) {
+ types = [
+ 'touchstart',
+ 'touchmove',
+ 'touchend touchcancel'];
+ }
+ // for non pointer events browsers and mixed browsers,
+ // like chrome on windows8 touch laptop
+ else {
+ types = [
+ 'touchstart mousedown',
+ 'touchmove mousemove',
+ 'touchend touchcancel mouseup'];
}
- if (object instanceof Number) {
- return 'Number';
+
+ Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0];
+ Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1];
+ Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2];
+ },
+
+
+ /**
+ * create touchlist depending on the event
+ * @param {Object} ev
+ * @param {String} eventType used by the fakemultitouch plugin
+ */
+ getTouchList: function getTouchList(ev/*, eventType*/) {
+ // get the fake pointerEvent touchlist
+ if(Hammer.HAS_POINTEREVENTS) {
+ return Hammer.PointerEvent.getTouchList();
}
- if (object instanceof String) {
- return 'String';
+ // get the touchlist
+ else if(ev.touches) {
+ return ev.touches;
}
- if (object instanceof Array) {
- return 'Array';
+ // make fake touchlist from mouse position
+ else {
+ return [{
+ identifier: 1,
+ pageX: ev.pageX,
+ pageY: ev.pageY,
+ target: ev.target
+ }];
}
- if (object instanceof Date) {
- return 'Date';
+ },
+
+
+ /**
+ * collect event data for Hammer js
+ * @param {HTMLElement} element
+ * @param {String} eventType like Hammer.EVENT_MOVE
+ * @param {Object} eventData
+ */
+ collectEventData: function collectEventData(element, eventType, ev) {
+ var touches = this.getTouchList(ev, eventType);
+
+ // find out pointerType
+ var pointerType = Hammer.POINTER_TOUCH;
+ if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) {
+ pointerType = Hammer.POINTER_MOUSE;
}
- return 'Object';
- }
- else if (type == 'number') {
- return 'Number';
- }
- else if (type == 'boolean') {
- return 'Boolean';
- }
- else if (type == 'string') {
- return 'String';
- }
- return type;
-};
+ return {
+ center : Hammer.utils.getCenter(touches),
+ timeStamp : new Date().getTime(),
+ target : ev.target,
+ touches : touches,
+ eventType : eventType,
+ pointerType : pointerType,
+ srcEvent : ev,
-/**
- * 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;
+ /**
+ * prevent the browser default actions
+ * mostly used to disable scrolling of the browser
+ */
+ preventDefault: function() {
+ if(this.srcEvent.preventManipulation) {
+ this.srcEvent.preventManipulation();
+ }
- 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;
-};
+ if(this.srcEvent.preventDefault) {
+ this.srcEvent.preventDefault();
+ }
+ },
-/**
- * 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;
+ /**
+ * stop bubbling the event up to its parents
+ */
+ stopPropagation: function() {
+ this.srcEvent.stopPropagation();
+ },
- var top = elem.offsetTop;
- var e = elem.offsetParent;
- while (e != null && e != body && e != doc) {
- top += e.offsetTop;
- top -= e.scrollTop;
- e = e.offsetParent;
+ /**
+ * immediately stop gesture detection
+ * might be useful after a swipe was detected
+ * @return {*}
+ */
+ stopDetect: function() {
+ return Hammer.detection.stopDetect();
+ }
+ };
}
- 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;
+Hammer.PointerEvent = {
+ /**
+ * holds all pointers
+ * @type {Object}
+ */
+ pointers: {},
+
+ /**
+ * get a list of pointers
+ * @returns {Array} touchlist
+ */
+ getTouchList: function() {
+ var self = this;
+ var touchlist = [];
+
+ // we can use forEach since pointerEvents only is in IE10
+ Object.keys(self.pointers).sort().forEach(function(id) {
+ touchlist.push(self.pointers[id]);
+ });
+ return touchlist;
+ },
+
+ /**
+ * update the position of a pointer
+ * @param {String} type Hammer.EVENT_END
+ * @param {Object} pointerEvent
+ */
+ updatePointer: function(type, pointerEvent) {
+ if(type == Hammer.EVENT_END) {
+ this.pointers = {};
}
else {
- clientY = event.clientY;
+ pointerEvent.identifier = pointerEvent.pointerId;
+ this.pointers[pointerEvent.pointerId] = pointerEvent;
}
- var doc = document.documentElement;
- var body = document.body;
- return clientY +
- ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
- ( doc && doc.clientTop || body && body.clientTop || 0 );
- }
-};
+ return Object.keys(this.pointers).length;
+ },
-/**
- * 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;
+ /**
+ * check if ev matches pointertype
+ * @param {String} pointerType Hammer.POINTER_MOUSE
+ * @param {PointerEvent} ev
+ */
+ matchType: function(pointerType, ev) {
+ if(!ev.pointerType) {
+ return false;
}
- var doc = document.documentElement;
- var body = document.body;
- return clientX +
- ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
- ( doc && doc.clientLeft || body && body.clientLeft || 0 );
- }
-};
+ var types = {};
+ types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE);
+ types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH);
+ types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN);
+ return types[pointerType];
+ },
-/**
- * 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(' ');
+ /**
+ * get events
+ */
+ getEvents: function() {
+ return [
+ 'pointerdown MSPointerDown',
+ 'pointermove MSPointerMove',
+ 'pointerup pointercancel MSPointerUp MSPointerCancel'
+ ];
+ },
+
+ /**
+ * reset the list
+ */
+ reset: function() {
+ this.pointers = {};
}
};
-/**
- * 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) {
- var i,
- len;
- if (object instanceof Array) {
- // array
- for (i = 0, len = object.length; i < len; i++) {
- callback(object[i], i, object);
- }
- }
- else {
- // object
- for (i in object) {
- if (object.hasOwnProperty(i)) {
- callback(object[i], i, object);
+
+Hammer.utils = {
+ /**
+ * extend method,
+ * also used for cloning when dest is an empty object
+ * @param {Object} dest
+ * @param {Object} src
+ * @parm {Boolean} merge do a merge
+ * @returns {Object} dest
+ */
+ extend: function extend(dest, src, merge) {
+ for (var key in src) {
+ if(dest[key] !== undefined && merge) {
+ continue;
+ }
+ dest[key] = src[key];
+ }
+ return dest;
+ },
+
+
+ /**
+ * find if a node is in the given parent
+ * used for event delegation tricks
+ * @param {HTMLElement} node
+ * @param {HTMLElement} parent
+ * @returns {boolean} has_parent
+ */
+ hasParent: function(node, parent) {
+ while(node){
+ if(node == parent) {
+ return true;
}
+ node = node.parentNode;
}
- }
-};
-
-/**
- * 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
+ /**
+ * get the center of all the touches
+ * @param {Array} touches
+ * @returns {Object} center
+ */
+ getCenter: function getCenter(touches) {
+ var valuesX = [], valuesY = [];
+
+ for(var t= 0,len=touches.length; t= 0) {
- action = "DOMMouseScroll"; // For Firefox
- }
+ /**
+ * calculate the velocity between two points
+ * @param {Number} delta_time
+ * @param {Number} delta_x
+ * @param {Number} delta_y
+ * @returns {Object} velocity
+ */
+ getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
+ return {
+ x: Math.abs(delta_x / delta_time) || 0,
+ y: Math.abs(delta_y / delta_time) || 0
+ };
+ },
- element.removeEventListener(action, listener, useCapture);
- } else {
- // IE browsers
- element.detachEvent("on" + action, listener);
- }
-};
+ /**
+ * calculate the angle between two coordinates
+ * @param {Touch} touch1
+ * @param {Touch} touch2
+ * @returns {Number} angle
+ */
+ getAngle: function getAngle(touch1, touch2) {
+ var y = touch2.pageY - touch1.pageY,
+ x = touch2.pageX - touch1.pageX;
+ return Math.atan2(y, x) * 180 / Math.PI;
+ },
-/**
- * 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;
+ /**
+ * angle to direction define
+ * @param {Touch} touch1
+ * @param {Touch} touch2
+ * @returns {String} direction constant, like Hammer.DIRECTION_LEFT
+ */
+ getDirection: function getDirection(touch1, touch2) {
+ var x = Math.abs(touch1.pageX - touch2.pageX),
+ y = Math.abs(touch1.pageY - touch2.pageY);
- if (event.target) {
- target = event.target;
- }
- else if (event.srcElement) {
- target = event.srcElement;
- }
+ if(x >= y) {
+ return touch1.pageX - touch2.pageX > 0 ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
+ }
+ else {
+ return touch1.pageY - touch2.pageY > 0 ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
+ }
+ },
- if (target.nodeType != undefined && target.nodeType == 3) {
- // defeat Safari bug
- target = target.parentNode;
- }
- return target;
-};
+ /**
+ * calculate the distance between two touches
+ * @param {Touch} touch1
+ * @param {Touch} touch2
+ * @returns {Number} distance
+ */
+ getDistance: function getDistance(touch1, touch2) {
+ var x = touch2.pageX - touch1.pageX,
+ y = touch2.pageY - touch1.pageY;
+ return Math.sqrt((x*x) + (y*y));
+ },
-/**
- * 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
- }
-};
+ /**
+ * calculate the scale factor between two touchLists (fingers)
+ * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
+ * @param {Array} start
+ * @param {Array} end
+ * @returns {Number} scale
+ */
+ getScale: function getScale(start, end) {
+ // need two fingers...
+ if(start.length >= 2 && end.length >= 2) {
+ return this.getDistance(end[0], end[1]) /
+ this.getDistance(start[0], start[1]);
+ }
+ return 1;
+ },
-/**
- * Cancels the event if it is cancelable, without stopping further propagation of the event.
- */
-util.preventDefault = function preventDefault (event) {
- if (!event)
- event = window.event;
+ /**
+ * calculate the rotation degrees between two touchLists (fingers)
+ * @param {Array} start
+ * @param {Array} end
+ * @returns {Number} rotation
+ */
+ getRotation: function getRotation(start, end) {
+ // need two fingers
+ if(start.length >= 2 && end.length >= 2) {
+ return this.getAngle(end[1], end[0]) -
+ this.getAngle(start[1], start[0]);
+ }
+ return 0;
+ },
- if (event.preventDefault) {
- event.preventDefault(); // non-IE browsers
- }
- else {
- event.returnValue = false; // IE browsers
- }
-};
+ /**
+ * boolean if the direction is vertical
+ * @param {String} direction
+ * @returns {Boolean} is_vertical
+ */
+ isVertical: function isVertical(direction) {
+ return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN);
+ },
-util.option = {};
-/**
- * Convert a value into a boolean
- * @param {Boolean | function | undefined} value
- * @param {Boolean} [defaultValue]
- * @returns {Boolean} bool
- */
-util.option.asBoolean = function (value, defaultValue) {
- if (typeof value == 'function') {
- value = value();
- }
+ /**
+ * stop browser default behavior with css props
+ * @param {HtmlElement} element
+ * @param {Object} css_props
+ */
+ stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) {
+ var prop,
+ vendors = ['webkit','khtml','moz','ms','o',''];
- if (value != null) {
- return (value != false);
- }
+ if(!css_props || !element.style) {
+ return;
+ }
- return defaultValue || null;
-};
+ // with css properties for modern browsers
+ for(var i = 0; i < vendors.length; i++) {
+ for(var p in css_props) {
+ if(css_props.hasOwnProperty(p)) {
+ prop = p;
-/**
- * Convert a value into a number
- * @param {Boolean | function | undefined} value
- * @param {Number} [defaultValue]
- * @returns {Number} number
- */
-util.option.asNumber = function (value, defaultValue) {
- if (typeof value == 'function') {
- value = value();
- }
+ // vender prefix at the property
+ if(vendors[i]) {
+ prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1);
+ }
- if (value != null) {
- return Number(value) || defaultValue || null;
- }
+ // set the style
+ element.style[prop] = css_props[p];
+ }
+ }
+ }
- return defaultValue || null;
+ // also the disable onselectstart
+ if(css_props.userSelect == 'none') {
+ element.onselectstart = function() {
+ return false;
+ };
+ }
+ }
};
-/**
- * Convert a value into a string
- * @param {String | function | undefined} value
- * @param {String} [defaultValue]
- * @returns {String} str
- */
-util.option.asString = function (value, defaultValue) {
- if (typeof value == 'function') {
- value = value();
- }
+Hammer.detection = {
+ // contains all registred Hammer.gestures in the correct order
+ gestures: [],
- if (value != null) {
- return String(value);
- }
+ // data of the current Hammer.gesture detection session
+ current: null,
- return defaultValue || null;
-};
+ // the previous Hammer.gesture session data
+ // is a full clone of the previous gesture.current object
+ previous: null,
-/**
- * Convert a size or location into a string with 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();
- }
+ // when this becomes true, no gestures are fired
+ stopped: false,
- if (util.isString(value)) {
- return value;
- }
- else if (util.isNumber(value)) {
- return value + 'px';
- }
- else {
- return defaultValue || null;
- }
-};
-/**
- * Convert a value into a 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();
- }
+ /**
+ * start Hammer.gesture detection
+ * @param {Hammer.Instance} inst
+ * @param {Object} eventData
+ */
+ startDetect: function startDetect(inst, eventData) {
+ // already busy with a Hammer.gesture detection on an element
+ if(this.current) {
+ return;
+ }
- return value || defaultValue || null;
-};
+ this.stopped = false;
-/**
- * load css from text
- * @param {String} css Text containing css
- */
-util.loadCss = function (css) {
- if (typeof document === 'undefined') {
- return;
- }
+ this.current = {
+ inst : inst, // reference to HammerInstance we're working for
+ startEvent : Hammer.utils.extend({}, eventData), // start eventData for distances, timing etc
+ lastEvent : false, // last eventData
+ name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
+ };
- // get the script location, and built the css file name from the js file name
- // http://stackoverflow.com/a/2161748/1262753
- // var scripts = document.getElementsByTagName('script');
- // var jsFile = scripts[scripts.length-1].src.split('?')[0];
- // var cssFile = jsFile.substring(0, jsFile.length - 2) + 'css';
+ this.detect(eventData);
+ },
- // inject css
- // http://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript
- var style = document.createElement('style');
- style.type = 'text/css';
- if (style.styleSheet){
- style.styleSheet.cssText = css;
- } else {
- style.appendChild(document.createTextNode(css));
- }
- document.getElementsByTagName('head')[0].appendChild(style);
-};
+ /**
+ * Hammer.gesture detection
+ * @param {Object} eventData
+ * @param {Object} eventData
+ */
+ detect: function detect(eventData) {
+ if(!this.current || this.stopped) {
+ return;
+ }
+ // extend event data with calculations about scale, distance etc
+ eventData = this.extendEventData(eventData);
-// 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;
+ // instance options
+ var inst_options = this.current.inst.options;
+
+ // call Hammer.gesture handlers
+ for(var g=0,len=this.gestures.length; g>> 0;
+ // stopped!
+ this.stopped = true;
+ },
- // 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;
+ /**
+ * extend eventData for Hammer.gestures
+ * @param {Object} ev
+ * @returns {Object} ev
+ */
+ extendEventData: function extendEventData(ev) {
+ var startEv = this.current.startEvent;
+
+ // if the touches change, set the new touches over the startEvent touches
+ // this because touchevents don't have all the touches on touchstart, or the
+ // user must place his fingers at the EXACT same time on the screen, which is not realistic
+ // but, sometimes it happens that both fingers are touching at the EXACT same time
+ if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) {
+ // extend 1 level deep to get the touchlist with the touch objects
+ startEv.touches = [];
+ for(var i=0,len=ev.touches.length; i>> 0;
- if (typeof fun != "function") {
- throw new TypeError();
- }
+ // add Hammer.gesture to the list
+ this.gestures.push(gesture);
- 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);
+ // sort the list by index
+ this.gestures.sort(function(a, b) {
+ if (a.index < b.index) {
+ return -1;
}
- }
-
- return res;
- };
-}
+ if (a.index > b.index) {
+ return 1;
+ }
+ return 0;
+ });
+ return this.gestures;
+ }
+};
-// 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');
- }
+Hammer.gestures = Hammer.gestures || {};
- var result = [];
+/**
+ * Custom gestures
+ * ==============================
+ *
+ * Gesture object
+ * --------------------
+ * The object structure of a gesture:
+ *
+ * { name: 'mygesture',
+ * index: 1337,
+ * defaults: {
+ * mygesture_option: true
+ * }
+ * handler: function(type, ev, inst) {
+ * // trigger gesture event
+ * inst.trigger(this.name, ev);
+ * }
+ * }
+
+ * @param {String} name
+ * this should be the name of the gesture, lowercase
+ * it is also being used to disable/enable the gesture per instance config.
+ *
+ * @param {Number} [index=1000]
+ * the index of the gesture, where it is going to be in the stack of gestures detection
+ * like when you build an gesture that depends on the drag gesture, it is a good
+ * idea to place it after the index of the drag gesture.
+ *
+ * @param {Object} [defaults={}]
+ * the default settings of the gesture. these are added to the instance settings,
+ * and can be overruled per instance. you can also add the name of the gesture,
+ * but this is also added by default (and set to true).
+ *
+ * @param {Function} handler
+ * this handles the gesture detection of your custom gesture and receives the
+ * following arguments:
+ *
+ * @param {Object} eventData
+ * event data containing the following properties:
+ * timeStamp {Number} time the event occurred
+ * target {HTMLElement} target element
+ * touches {Array} touches (fingers, pointers, mouse) on the screen
+ * pointerType {String} kind of pointer that was used. matches Hammer.POINTER_MOUSE|TOUCH
+ * center {Object} center position of the touches. contains pageX and pageY
+ * deltaTime {Number} the total time of the touches in the screen
+ * deltaX {Number} the delta on x axis we haved moved
+ * deltaY {Number} the delta on y axis we haved moved
+ * velocityX {Number} the velocity on the x
+ * velocityY {Number} the velocity on y
+ * angle {Number} the angle we are moving
+ * direction {String} the direction we are moving. matches Hammer.DIRECTION_UP|DOWN|LEFT|RIGHT
+ * distance {Number} the distance we haved moved
+ * scale {Number} scaling of the touches, needs 2 touches
+ * rotation {Number} rotation of the touches, needs 2 touches *
+ * eventType {String} matches Hammer.EVENT_START|MOVE|END
+ * srcEvent {Object} the source event, like TouchStart or MouseDown *
+ * startEvent {Object} contains the same properties as above,
+ * but from the first touch. this is used to calculate
+ * distances, deltaTime, scaling etc
+ *
+ * @param {Hammer.Instance} inst
+ * the instance we are doing the detection for. you can get the options from
+ * the inst.options object and trigger the gesture event by calling inst.trigger
+ *
+ *
+ * Handle gestures
+ * --------------------
+ * inside the handler you can get/set Hammer.detection.current. This is the current
+ * detection session. It has the following properties
+ * @param {String} name
+ * contains the name of the gesture we have detected. it has not a real function,
+ * only to check in other gestures if something is detected.
+ * like in the drag gesture we set it to 'drag' and in the swipe gesture we can
+ * check if the current gesture is 'drag' by accessing Hammer.detection.current.name
+ *
+ * @readonly
+ * @param {Hammer.Instance} inst
+ * the instance we do the detection for
+ *
+ * @readonly
+ * @param {Object} startEvent
+ * contains the properties of the first gesture detection in this session.
+ * Used for calculations about timing, distance, etc.
+ *
+ * @readonly
+ * @param {Object} lastEvent
+ * contains all the properties of the last gesture detect in this session.
+ *
+ * after the gesture detection session has been completed (user has released the screen)
+ * the Hammer.detection.current object is copied into Hammer.detection.previous,
+ * this is usefull for gestures like doubletap, where you need to know if the
+ * previous gesture was a tap
+ *
+ * options that have been set by the instance can be received by calling inst.options
+ *
+ * You can trigger a gesture event by calling inst.trigger("mygesture", event).
+ * The first param is the name of your gesture, the second the event argument
+ *
+ *
+ * Register gestures
+ * --------------------
+ * When an gesture is added to the Hammer.gestures object, it is auto registered
+ * at the setup of the first Hammer instance. You can also call Hammer.detection.register
+ * manually and pass your gesture object as a param
+ *
+ */
- for (var prop in obj) {
- if (hasOwnProperty.call(obj, prop)) result.push(prop);
- }
+/**
+ * Hold
+ * Touch stays at the same place for x time
+ * @events hold
+ */
+Hammer.gestures.Hold = {
+ name: 'hold',
+ index: 10,
+ defaults: {
+ hold_timeout : 500,
+ hold_threshold : 1
+ },
+ timer: null,
+ handler: function holdGesture(ev, inst) {
+ switch(ev.eventType) {
+ case Hammer.EVENT_START:
+ // clear any running timers
+ clearTimeout(this.timer);
+
+ // set the gesture so we can check in the timeout if it still is
+ Hammer.detection.current.name = this.name;
+
+ // set timer and if after the timeout it still is hold,
+ // we trigger the hold event
+ this.timer = setTimeout(function() {
+ if(Hammer.detection.current.name == 'hold') {
+ inst.trigger('hold', ev);
+ }
+ }, inst.options.hold_timeout);
+ break;
- if (hasDontEnumBug) {
- for (var i=0; i < dontEnumsLength; i++) {
- if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]);
+ // when you move or end we clear the timer
+ case Hammer.EVENT_MOVE:
+ if(ev.distance > inst.options.hold_threshold) {
+ clearTimeout(this.timer);
}
- }
- return result;
- }
- })()
-}
-
-// Internet Explorer 8 and older does not support Array.isArray,
-// so we define it here in that case.
-// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray
-if(!Array.isArray) {
- Array.isArray = function (vArg) {
- return Object.prototype.toString.call(vArg) === "[object Array]";
- };
-}
+ break;
-// Internet Explorer 8 and older does not support Function.bind,
-// so we define it here in that case.
-// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
-if (!Function.prototype.bind) {
- Function.prototype.bind = function (oThis) {
- if (typeof this !== "function") {
- // closest thing possible to the ECMAScript 5 internal IsCallable function
- throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
+ case Hammer.EVENT_END:
+ clearTimeout(this.timer);
+ break;
}
+ }
+};
- var aArgs = Array.prototype.slice.call(arguments, 1),
- fToBind = this,
- fNOP = function () {},
- fBound = function () {
- return fToBind.apply(this instanceof fNOP && oThis
- ? this
- : oThis,
- aArgs.concat(Array.prototype.slice.call(arguments)));
- };
- fNOP.prototype = this.prototype;
- fBound.prototype = new fNOP();
+/**
+ * Tap/DoubleTap
+ * Quick touch at a place or double at the same place
+ * @events tap, doubletap
+ */
+Hammer.gestures.Tap = {
+ name: 'tap',
+ index: 100,
+ defaults: {
+ tap_max_touchtime : 250,
+ tap_max_distance : 10,
+ tap_always : true,
+ doubletap_distance : 20,
+ doubletap_interval : 300
+ },
+ handler: function tapGesture(ev, inst) {
+ if(ev.eventType == Hammer.EVENT_END) {
+ // previous gesture, for the double tap since these are two different gesture detections
+ var prev = Hammer.detection.previous,
+ did_doubletap = false;
- return fBound;
- };
-}
+ // when the touchtime is higher then the max touch time
+ // or when the moving distance is too much
+ if(ev.deltaTime > inst.options.tap_max_touchtime ||
+ ev.distance > inst.options.tap_max_distance) {
+ return;
+ }
-// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create
-if (!Object.create) {
- Object.create = function (o) {
- if (arguments.length > 1) {
- throw new Error('Object.create implementation only accepts the first parameter.');
+ // check if double tap
+ if(prev && prev.name == 'tap' &&
+ (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
+ ev.distance < inst.options.doubletap_distance) {
+ inst.trigger('doubletap', ev);
+ did_doubletap = true;
+ }
+
+ // do a single tap
+ if(!did_doubletap || inst.options.tap_always) {
+ Hammer.detection.current.name = 'tap';
+ inst.trigger(Hammer.detection.current.name, ev);
+ }
}
- function F() {}
- F.prototype = o;
- return new F();
- };
-}
+ }
+};
+
/**
- * Event listener (singleton)
+ * Swipe
+ * triggers swipe events when the end velocity is above the threshold
+ * @events swipe, swipeleft, swiperight, swipeup, swipedown
*/
-// TODO: replace usage of the event listener for the EventBus
-var events = {
- 'listeners': [],
+Hammer.gestures.Swipe = {
+ name: 'swipe',
+ index: 40,
+ defaults: {
+ // set 0 for unlimited, but this can conflict with transform
+ swipe_max_touches : 1,
+ swipe_velocity : 0.7
+ },
+ handler: function swipeGesture(ev, inst) {
+ if(ev.eventType == Hammer.EVENT_END) {
+ // max touches
+ if(inst.options.swipe_max_touches > 0 &&
+ ev.touches.length > inst.options.swipe_max_touches) {
+ return;
+ }
+
+ // when the distance we moved is too small we skip this gesture
+ // or we can be already in dragging
+ if(ev.velocityX > inst.options.swipe_velocity ||
+ ev.velocityY > inst.options.swipe_velocity) {
+ // trigger swipe events
+ inst.trigger(this.name, ev);
+ inst.trigger(this.name + ev.direction, ev);
+ }
+ }
+ }
+};
+
+
+/**
+ * Drag
+ * Move with x fingers (default 1) around on the page. Blocking the scrolling when
+ * moving left and right is a good practice. When all the drag events are blocking
+ * you disable scrolling on that area.
+ * @events drag, drapleft, dragright, dragup, dragdown
+ */
+Hammer.gestures.Drag = {
+ name: 'drag',
+ index: 50,
+ defaults: {
+ drag_min_distance : 10,
+ // set 0 for unlimited, but this can conflict with transform
+ drag_max_touches : 1,
+ // prevent default browser behavior when dragging occurs
+ // be careful with it, it makes the element a blocking element
+ // when you are using the drag gesture, it is a good practice to set this true
+ drag_block_horizontal : false,
+ drag_block_vertical : false,
+ // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
+ // It disallows vertical directions if the initial direction was horizontal, and vice versa.
+ drag_lock_to_axis : false,
+ // drag lock only kicks in when distance > drag_lock_min_distance
+ // This way, locking occurs only when the distance has become large enough to reliably determine the direction
+ drag_lock_min_distance : 25
+ },
+ triggered: false,
+ handler: function dragGesture(ev, inst) {
+ // current gesture isnt drag, but dragged is true
+ // this means an other gesture is busy. now call dragend
+ if(Hammer.detection.current.name != this.name && this.triggered) {
+ inst.trigger(this.name +'end', ev);
+ this.triggered = false;
+ return;
+ }
- /**
- * 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;
- }
+ // max touches
+ if(inst.options.drag_max_touches > 0 &&
+ ev.touches.length > inst.options.drag_max_touches) {
+ return;
}
- 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);
- }
+ switch(ev.eventType) {
+ case Hammer.EVENT_START:
+ this.triggered = false;
+ break;
- var callbacks = listener.events[event];
- if (!callbacks) {
- callbacks = [];
- listener.events[event] = callbacks;
- }
+ case Hammer.EVENT_MOVE:
+ // when the distance we moved is too small we skip this gesture
+ // or we can be already in dragging
+ if(ev.distance < inst.options.drag_min_distance &&
+ Hammer.detection.current.name != this.name) {
+ return;
+ }
- // add the callback if it does not yet exist
- if (callbacks.indexOf(callback) == -1) {
- callbacks.push(callback);
- }
- },
+ // we are dragging!
+ Hammer.detection.current.name = this.name;
- /**
- * 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);
+ // lock drag to axis?
+ if(Hammer.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) {
+ ev.drag_locked_to_axis = true;
}
-
- // remove the array when empty
- if (callbacks.length == 0) {
- delete listener.events[event];
+ var last_direction = Hammer.detection.current.lastEvent.direction;
+ if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
+ // keep direction on the axis that the drag gesture started on
+ if(Hammer.utils.isVertical(last_direction)) {
+ ev.direction = (ev.deltaY < 0) ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
+ }
+ else {
+ ev.direction = (ev.deltaX < 0) ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
+ }
}
- }
- // 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++;
+ // first time, trigger dragstart event
+ if(!this.triggered) {
+ inst.trigger(this.name +'start', ev);
+ this.triggered = true;
}
- }
- if (count == 0) {
- delete this.listeners[index];
- }
- }
- },
- /**
- * Remove all registered event listeners
- */
- 'removeAllListeners': function () {
- this.listeners = [];
- },
+ // trigger normal event
+ inst.trigger(this.name, ev);
- /**
- * 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);
+ // direction event, like dragdown
+ inst.trigger(this.name + ev.direction, ev);
+
+ // block the browser events
+ if( (inst.options.drag_block_vertical && Hammer.utils.isVertical(ev.direction)) ||
+ (inst.options.drag_block_horizontal && !Hammer.utils.isVertical(ev.direction))) {
+ ev.preventDefault();
}
- }
+ break;
+
+ case Hammer.EVENT_END:
+ // trigger dragend
+ if(this.triggered) {
+ inst.trigger(this.name +'end', ev);
+ }
+
+ this.triggered = false;
+ break;
}
}
};
-/**
- * An event bus can be used to emit events, and to subscribe to events
- * @constructor EventBus
- */
-function EventBus() {
- this.subscriptions = [];
-}
/**
- * Subscribe to an event
- * @param {String | RegExp} event The event can be a regular expression, or
- * a string with wildcards, like 'server.*'.
- * @param {function} callback. Callback are called with three parameters:
- * {String} event, {*} [data], {*} [source]
- * @param {*} [target]
- * @returns {String} id A subscription id
+ * Transform
+ * User want to scale or rotate with 2 fingers
+ * @events transform, pinch, pinchin, pinchout, rotate
*/
-EventBus.prototype.on = function (event, callback, target) {
- var regexp = (event instanceof RegExp) ?
- event :
- new RegExp(event.replace('*', '\\w+'));
+Hammer.gestures.Transform = {
+ name: 'transform',
+ index: 45,
+ defaults: {
+ // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
+ transform_min_scale : 0.01,
+ // rotation in degrees
+ transform_min_rotation : 1,
+ // prevent default browser behavior when two touches are on the screen
+ // but it makes the element a blocking element
+ // when you are using the transform gesture, it is a good practice to set this true
+ transform_always_block : false
+ },
+ triggered: false,
+ handler: function transformGesture(ev, inst) {
+ // current gesture isnt drag, but dragged is true
+ // this means an other gesture is busy. now call dragend
+ if(Hammer.detection.current.name != this.name && this.triggered) {
+ inst.trigger(this.name +'end', ev);
+ this.triggered = false;
+ return;
+ }
- var subscription = {
- id: util.randomUUID(),
- event: event,
- regexp: regexp,
- callback: (typeof callback === 'function') ? callback : null,
- target: target
- };
+ // atleast multitouch
+ if(ev.touches.length < 2) {
+ return;
+ }
- this.subscriptions.push(subscription);
+ // prevent default when two fingers are on the screen
+ if(inst.options.transform_always_block) {
+ ev.preventDefault();
+ }
- return subscription.id;
+ switch(ev.eventType) {
+ case Hammer.EVENT_START:
+ this.triggered = false;
+ break;
+
+ case Hammer.EVENT_MOVE:
+ var scale_threshold = Math.abs(1-ev.scale);
+ var rotation_threshold = Math.abs(ev.rotation);
+
+ // when the distance we moved is too small we skip this gesture
+ // or we can be already in dragging
+ if(scale_threshold < inst.options.transform_min_scale &&
+ rotation_threshold < inst.options.transform_min_rotation) {
+ return;
+ }
+
+ // we are transforming!
+ Hammer.detection.current.name = this.name;
+
+ // first time, trigger dragstart event
+ if(!this.triggered) {
+ inst.trigger(this.name +'start', ev);
+ this.triggered = true;
+ }
+
+ inst.trigger(this.name, ev); // basic transform event
+
+ // trigger rotate event
+ if(rotation_threshold > inst.options.transform_min_rotation) {
+ inst.trigger('rotate', ev);
+ }
+
+ // trigger pinch event
+ if(scale_threshold > inst.options.transform_min_scale) {
+ inst.trigger('pinch', ev);
+ inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev);
+ }
+ break;
+
+ case Hammer.EVENT_END:
+ // trigger dragend
+ if(this.triggered) {
+ inst.trigger(this.name +'end', ev);
+ }
+
+ this.triggered = false;
+ break;
+ }
+ }
};
+
/**
- * Unsubscribe from an event
- * @param {String | Object} filter Filter for subscriptions to be removed
- * Filter can be a string containing a
- * subscription id, or an object containing
- * one or more of the fields id, event,
- * callback, and target.
+ * Touch
+ * Called as first, tells the user has touched the screen
+ * @events touch
*/
-EventBus.prototype.off = function (filter) {
- var i = 0;
- while (i < this.subscriptions.length) {
- var subscription = this.subscriptions[i];
+Hammer.gestures.Touch = {
+ name: 'touch',
+ index: -Infinity,
+ defaults: {
+ // call preventDefault at touchstart, and makes the element blocking by
+ // disabling the scrolling of the page, but it improves gestures like
+ // transforming and dragging.
+ // be careful with using this, it can be very annoying for users to be stuck
+ // on the page
+ prevent_default: false,
- var match = true;
- if (filter instanceof Object) {
- // filter is an object. All fields must match
- for (var prop in filter) {
- if (filter.hasOwnProperty(prop)) {
- if (filter[prop] !== subscription[prop]) {
- match = false;
- }
- }
- }
- }
- else {
- // filter is a string, filter on id
- match = (subscription.id == filter);
+ // disable mouse events, so only touch (or pen!) input triggers events
+ prevent_mouseevents: false
+ },
+ handler: function touchGesture(ev, inst) {
+ if(inst.options.prevent_mouseevents && ev.pointerType == Hammer.POINTER_MOUSE) {
+ ev.stopDetect();
+ return;
}
- if (match) {
- this.subscriptions.splice(i, 1);
+ if(inst.options.prevent_default) {
+ ev.preventDefault();
}
- else {
- i++;
+
+ if(ev.eventType == Hammer.EVENT_START) {
+ inst.trigger(this.name, ev);
}
}
};
+
/**
- * Emit an event
- * @param {String} event
- * @param {*} [data]
- * @param {*} [source]
+ * Release
+ * Called as last, tells the user has released the screen
+ * @events release
*/
-EventBus.prototype.emit = function (event, data, source) {
- for (var i =0; i < this.subscriptions.length; i++) {
- var subscription = this.subscriptions[i];
- if (subscription.regexp.test(event)) {
- if (subscription.callback) {
- subscription.callback(event, data, source);
- }
+Hammer.gestures.Release = {
+ name: 'release',
+ index: Infinity,
+ handler: function releaseGesture(ev, inst) {
+ if(ev.eventType == Hammer.EVENT_END) {
+ inst.trigger(this.name, ev);
}
}
};
-/**
- * DataSet
- *
- * Usage:
- * var dataSet = new DataSet({
- * fieldId: '_id',
- * convert: {
- * // ...
- * }
- * });
- *
- * dataSet.add(item);
- * dataSet.add(data);
- * dataSet.update(item);
- * dataSet.update(data);
- * dataSet.remove(id);
- * dataSet.remove(ids);
- * var data = dataSet.get();
- * var data = dataSet.get(id);
- * var data = dataSet.get(ids);
- * var data = dataSet.get(ids, options, data);
- * dataSet.clear();
- *
- * A data set can:
- * - add/remove/update data
- * - gives triggers upon changes in the data
- * - can import/export data in various data formats
- *
- * @param {Object} [options] Available options:
- * {String} fieldId Field name of the id in the
- * items, 'id' by default.
- * {Object. ["10", "00"] or "-1530" > ["-15", "30"]
+ parseTimezoneChunker = /([\+\-]|\d\d)/gi,
+
+ // getter and setter names
+ proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
+ unitMillisecondFactors = {
+ 'Milliseconds' : 1,
+ 'Seconds' : 1e3,
+ 'Minutes' : 6e4,
+ 'Hours' : 36e5,
+ 'Days' : 864e5,
+ 'Months' : 2592e6,
+ 'Years' : 31536e6
+ },
+
+ unitAliases = {
+ ms : 'millisecond',
+ s : 'second',
+ m : 'minute',
+ h : 'hour',
+ d : 'day',
+ w : 'week',
+ W : 'isoweek',
+ M : 'month',
+ y : 'year'
+ },
+
+ // 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);
+ },
+ gg : function () {
+ return leftZeroFill(this.weekYear() % 100, 2);
+ },
+ gggg : function () {
+ return this.weekYear();
+ },
+ ggggg : function () {
+ return leftZeroFill(this.weekYear(), 5);
+ },
+ GG : function () {
+ return leftZeroFill(this.isoWeekYear() % 100, 2);
+ },
+ GGGG : function () {
+ return this.isoWeekYear();
+ },
+ GGGGG : function () {
+ return leftZeroFill(this.isoWeekYear(), 5);
+ },
+ e : function () {
+ return this.weekday();
+ },
+ E : function () {
+ return this.isoWeekday();
+ },
+ 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);
+ },
+ z : function () {
+ return this.zoneAbbr();
+ },
+ zz : function () {
+ return this.zoneName();
+ },
+ X : function () {
+ return this.unix();
+ }
+ };
+
+ function padToken(func, count) {
+ return function (a) {
+ return leftZeroFill(func.call(this, a), count);
+ };
+ }
+ function ordinalizeToken(func, period) {
+ return function (a) {
+ return this.lang().ordinal(func.call(this, a), period);
+ };
+ }
+
+ while (ordinalizeTokens.length) {
+ i = ordinalizeTokens.pop();
+ formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], 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 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;
+
+ // store reference to input for deterministic cloning
+ this._input = duration;
+
+ // 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;
+
+ this._data = {};
+
+ this._bubble();
+ }
+
+
+ /************************************
+ 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, ignoreUpdateOffset) {
+ var milliseconds = duration._milliseconds,
+ days = duration._days,
+ months = duration._months,
+ minutes,
+ hours;
+
+ if (milliseconds) {
+ mom._d.setTime(+mom._d + milliseconds * isAdding);
+ }
+ // store the minutes and hours so we can restore them
+ if (days || months) {
+ minutes = mom.minute();
+ hours = mom.hour();
+ }
+ if (days) {
+ mom.date(mom.date() + days * isAdding);
+ }
+ if (months) {
+ mom.month(mom.month() + months * isAdding);
+ }
+ if (milliseconds && !ignoreUpdateOffset) {
+ moment.updateOffset(mom);
+ }
+ // restore the minutes and hours after possibly changing dst
+ if (days || months) {
+ mom.minute(minutes);
+ mom.hour(hours);
+ }
+ }
+
+ // 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;
+ }
+
+ function normalizeUnits(units) {
+ return units ? unitAliases[units] || units.toLowerCase().replace(/(.)s$/, '$1') : units;
+ }
+
+
+ /************************************
+ Languages
+ ************************************/
+
+
+ extend(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;
+
+ 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.utc([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()];
+ },
+
+ weekdaysParse : function (weekdayName) {
+ var i, mom, regex;
+
+ if (!this._weekdaysParse) {
+ this._weekdaysParse = [];
+ }
+
+ for (i = 0; i < 7; i++) {
+ // make the regex if we don't have it already
+ if (!this._weekdaysParse[i]) {
+ mom = moment([2000, 1]).day(i);
+ regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
+ this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
+ }
+ // test the regex
+ if (this._weekdaysParse[i].test(weekdayName)) {
+ return i;
+ }
+ }
+ },
+
+ _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;
+ },
+
+ isPM : function (input) {
+ // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
+ // Using charAt should be more compatible.
+ return ((input + '').toLowerCase().charAt(0) === 'p');
+ },
+
+ _meridiemParse : /[ap]\.?m?\.?/i,
+ 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;
+ },
+ _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];
+ }
+
+ // Remove a language from the `languages` cache. Mostly useful in tests.
+ function unloadLang(key) {
+ delete 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) {
+ try {
+ require('./lang/' + key);
+ } catch (e) {
+ // call with no params to set to default
+ return moment.fn._lang;
+ }
+ }
+ return languages[key] || moment.fn._lang;
+ }
+
+
+ /************************************
+ 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 += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
+ }
+ return output;
+ };
+ }
+
+ // format date using native date object
+ function formatMoment(m, format) {
+
+ format = expandFormat(format, m.lang());
+
+ if (!formatFunctions[format]) {
+ formatFunctions[format] = makeFormatFunction(format);
+ }
+
+ return formatFunctions[format](m);
+ }
+
+ function expandFormat(format, lang) {
+ var i = 5;
+
+ function replaceLongDateFormatTokens(input) {
+ return lang.longDateFormat(input) || input;
+ }
+
+ while (i-- && (localFormattingTokens.lastIndex = 0,
+ localFormattingTokens.test(format))) {
+ format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
+ }
+
+ return format;
+ }
+
+
+ /************************************
+ Parsing
+ ************************************/
+
+
+ // get the regex to find the next token
+ function getParseRegexForToken(token, config) {
+ 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':
+ return parseTokenWord;
+ case 'a':
+ case 'A':
+ return getLangDefinition(config._l)._meridiemParse;
+ 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('\\', ''));
+ }
}
- subscribers.push({
- callback: callback
- });
-};
+ function timezoneMinutesFromString(string) {
+ var tzchunk = (parseTokenTimezone.exec(string) || [])[0],
+ parts = (tzchunk + '').match(parseTimezoneChunker) || ['-', 0, 0],
+ minutes = +(parts[1] * 60) + ~~parts[2];
-/**
- * Unsubscribe from an event, remove an event listener
- * @param {String} event
- * @param {function} callback
- */
-DataSet.prototype.unsubscribe = function (event, callback) {
- var subscribers = this.subscribers[event];
- if (subscribers) {
- this.subscribers[event] = subscribers.filter(function (listener) {
- return (listener.callback != callback);
- });
+ return parts[0] === '+' ? -minutes : minutes;
}
-};
-/**
- * Trigger an event
- * @param {String} event
- * @param {Object | null} params
- * @param {String} [senderId] Optional id of the sender.
- * @private
- */
-DataSet.prototype._trigger = function (event, params, senderId) {
- if (event == '*') {
- throw new Error('Cannot trigger event *');
+ // function to convert string input to date
+ function addTimeToArrayFromToken(token, input, config) {
+ var a, datePartArray = config._a;
+
+ switch (token) {
+ // MONTH
+ case 'M' : // fall through to MM
+ case 'MM' :
+ if (input != null) {
+ datePartArray[1] = ~~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 DD
+ case 'DD' :
+ if (input != null) {
+ datePartArray[2] = ~~input;
+ }
+ break;
+ // DAY OF YEAR
+ case 'DDD' : // fall through to DDDD
+ case 'DDDD' :
+ if (input != null) {
+ datePartArray[1] = 0;
+ 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 = getLangDefinition(config._l).isPM(input);
+ 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;
+ config._tzm = timezoneMinutesFromString(input);
+ break;
+ }
+
+ // if the input is null, the date is not valid
+ if (input == null) {
+ config._isValid = false;
+ }
}
- var subscribers = [];
- if (event in this.subscribers) {
- subscribers = subscribers.concat(this.subscribers[event]);
+ // 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 = [], currentDate;
+
+ if (config._d) {
+ return;
+ }
+
+ // Default to current date.
+ // * if no year, month, day of month are given, default to today
+ // * if day of month is given, default month and year
+ // * if month is given, default only year
+ // * if year is given, don't default anything
+ currentDate = currentDateArray(config);
+ for (i = 0; i < 3 && config._a[i] == null; ++i) {
+ config._a[i] = input[i] = currentDate[i];
+ }
+
+ // Zero out whatever was not defaulted, including time
+ for (; 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._tzm || 0) / 60);
+ input[4] += ~~((config._tzm || 0) % 60);
+
+ 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;
}
- if ('*' in this.subscribers) {
- subscribers = subscribers.concat(this.subscribers['*']);
+
+ function dateFromObject(config) {
+ var o = config._i;
+
+ if (config._d) {
+ return;
+ }
+
+ config._a = [
+ o.years || o.year || o.y,
+ o.months || o.month || o.M,
+ o.days || o.day || o.d,
+ o.hours || o.hour || o.h,
+ o.minutes || o.minute || o.m,
+ o.seconds || o.second || o.s,
+ o.milliseconds || o.millisecond || o.ms
+ ];
+
+ dateFromArray(config);
}
- for (var i = 0; i < subscribers.length; i++) {
- var subscriber = subscribers[i];
- if (subscriber.callback) {
- subscriber.callback(event, params, senderId || null);
+ function currentDateArray(config) {
+ var now = new Date();
+ if (config._useUTC) {
+ return [
+ now.getUTCFullYear(),
+ now.getUTCMonth(),
+ now.getUTCDate()
+ ];
+ } else {
+ return [now.getFullYear(), now.getMonth(), now.getDate()];
}
}
-};
-/**
- * Add data.
- * Adding an item will fail when there already is an item with the same id.
- * @param {Object | Array | DataTable} data
- * @param {String} [senderId] Optional sender id
- * @return {Array} addedIds Array with the ids of the added items
- */
-DataSet.prototype.add = function (data, senderId) {
- var addedIds = [],
- id,
- me = this;
+ // 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 lang = getLangDefinition(config._l),
+ string = '' + config._i,
+ i, parsedInput, tokens;
- if (data instanceof Array) {
- // Array
- for (var i = 0, len = data.length; i < len; i++) {
- id = me._addItem(data[i]);
- addedIds.push(id);
+ tokens = expandFormat(config._f, lang).match(formattingTokens);
+
+ config._a = [];
+
+ for (i = 0; i < tokens.length; i++) {
+ parsedInput = (getParseRegexForToken(tokens[i], config).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);
+ }
+ }
+
+ // add remaining unparsed input to the string
+ if (string) {
+ config._il = string;
+ }
+
+ // 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);
}
- else if (util.isDataTable(data)) {
- // Google DataTable
- var columns = this._getColumnNames(data);
- for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
- var item = {};
- for (var col = 0, cols = columns.length; col < cols; col++) {
- var field = columns[col];
- item[field] = data.getValue(row, col);
+
+ // date from string and array of format strings
+ function makeDateFromStringAndArray(config) {
+ var tempConfig,
+ tempMoment,
+ bestMoment,
+
+ scoreToBeat = 99,
+ i,
+ currentScore;
+
+ for (i = 0; i < config._f.length; i++) {
+ tempConfig = extend({}, config);
+ tempConfig._f = config._f[i];
+ makeDateFromStringAndFormat(tempConfig);
+ tempMoment = new Moment(tempConfig);
+
+ currentScore = compareArrays(tempConfig._a, tempMoment.toArray());
+
+ // if there is any input that was not parsed
+ // add a penalty for that format
+ if (tempMoment._il) {
+ currentScore += tempMoment._il.length;
}
- id = me._addItem(item);
- addedIds.push(id);
+ if (currentScore < scoreToBeat) {
+ scoreToBeat = currentScore;
+ bestMoment = tempMoment;
+ }
}
- }
- else if (data instanceof Object) {
- // Single item
- id = me._addItem(data);
- addedIds.push(id);
- }
- else {
- throw new Error('Unknown dataType');
- }
- if (addedIds.length) {
- this._trigger('add', {items: addedIds}, senderId);
+ extend(config, bestMoment);
}
- return addedIds;
-};
-
-/**
- * Update existing items. When an item does not exist, it will be created
- * @param {Object | Array | DataTable} data
- * @param {String} [senderId] Optional sender id
- * @return {Array} updatedIds The ids of the added or updated items
- */
-DataSet.prototype.update = function (data, senderId) {
- var addedIds = [],
- updatedIds = [],
- me = this,
- fieldId = me.fieldId;
-
- var addOrUpdate = function (item) {
- var id = item[fieldId];
- if (me.data[id]) {
- // update item
- id = me._updateItem(item);
- updatedIds.push(id);
- }
- else {
- // add new item
- id = me._addItem(item);
- addedIds.push(id);
- }
- };
+ // date from iso format
+ function makeDateFromString(config) {
+ var i,
+ string = config._i,
+ match = isoRegex.exec(string);
- if (data instanceof Array) {
- // Array
- for (var i = 0, len = data.length; i < len; i++) {
- addOrUpdate(data[i]);
+ if (match) {
+ // match[2] should be "T" or undefined
+ config._f = 'YYYY-MM-DD' + (match[2] || " ");
+ 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);
}
}
- else if (util.isDataTable(data)) {
- // Google DataTable
- var columns = this._getColumnNames(data);
- for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
- var item = {};
- for (var col = 0, cols = columns.length; col < cols; col++) {
- var field = columns[col];
- item[field] = data.getValue(row, col);
- }
- addOrUpdate(item);
+ 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 if (input instanceof Date) {
+ config._d = new Date(+input);
+ } else if (typeof(input) === 'object') {
+ dateFromObject(config);
+ } else {
+ config._d = new Date(input);
}
}
- else if (data instanceof Object) {
- // Single item
- addOrUpdate(data);
- }
- else {
- throw new Error('Unknown dataType');
- }
- if (addedIds.length) {
- this._trigger('add', {items: addedIds}, senderId);
- }
- if (updatedIds.length) {
- this._trigger('update', {items: updatedIds}, senderId);
- }
- return addedIds.concat(updatedIds);
-};
+ /************************************
+ Relative Time
+ ************************************/
-/**
- * Get a data item or multiple items.
- *
- * Usage:
- *
- * get()
- * get(options: Object)
- * get(options: Object, data: Array | DataTable)
- *
- * get(id: Number | String)
- * get(id: Number | String, options: Object)
- * get(id: Number | String, options: Object, data: Array | DataTable)
- *
- * get(ids: Number[] | String[])
- * get(ids: Number[] | String[], options: Object)
- * get(ids: Number[] | String[], options: Object, data: Array | DataTable)
- *
- * Where:
- *
- * {Number | String} id The id of an item
- * {Number[] | String{}} ids An array with ids of items
- * {Object} options An Object with options. Available options:
- * {String} [type] Type of data to be returned. Can
- * be 'DataTable' or 'Array' (default)
- * {Object.} [convert]
- * {String[]} [fields] field names to be returned
- * {function} [filter] filter items
- * {String | function} [order] Order the items by
- * a field name or custom sort function.
- * {Array | DataTable} [data] If provided, items will be appended to this
- * array or table. Required in case of Google
- * DataTable.
- *
- * @throws Error
- */
-DataSet.prototype.get = function (args) {
- var me = this;
- // parse the arguments
- var id, ids, options, data;
- var firstType = util.getType(arguments[0]);
- if (firstType == 'String' || firstType == 'Number') {
- // get(id [, options] [, data])
- id = arguments[0];
- options = arguments[1];
- data = arguments[2];
- }
- else if (firstType == 'Array') {
- // get(ids [, options] [, data])
- ids = arguments[0];
- options = arguments[1];
- data = arguments[2];
+ // 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);
}
- else {
- // get([, options] [, data])
- options = arguments[0];
- data = arguments[1];
+
+ 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);
}
- // 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 + ')');
+ /************************************
+ 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(),
+ adjustedMoment;
+
+
+ if (daysToDayOfWeek > end) {
+ daysToDayOfWeek -= 7;
}
- if (type == 'DataTable' && !util.isDataTable(data)) {
- throw new Error('Parameter "data" must be a DataTable ' +
- 'when options.type is "DataTable"');
+
+ if (daysToDayOfWeek < end - 7) {
+ daysToDayOfWeek += 7;
}
- }
- else if (data) {
- type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
- }
- else {
- type = 'Array';
+
+ adjustedMoment = moment(mom).add('d', daysToDayOfWeek);
+ return {
+ week: Math.ceil(adjustedMoment.dayOfYear() / 7),
+ year: adjustedMoment.year()
+ };
}
- // build options
- var convert = options && options.convert || this.options.convert;
- var filter = options && options.filter;
- var items = [], item, itemId, i, len;
- // convert items
- if (id != undefined) {
- // return a single item
- item = me._getItem(id, convert);
- if (filter && !filter(item)) {
- item = null;
- }
- }
- else if (ids != undefined) {
- // return a subset of items
- for (i = 0, len = ids.length; i < len; i++) {
- item = me._getItem(ids[i], convert);
- if (!filter || filter(item)) {
- items.push(item);
- }
+ /************************************
+ Top Level Functions
+ ************************************/
+
+ function makeMoment(config) {
+ var input = config._i,
+ format = config._f;
+
+ if (input === null || input === '') {
+ return null;
}
- }
- else {
- // return all items
- for (itemId in this.data) {
- if (this.data.hasOwnProperty(itemId)) {
- item = me._getItem(itemId, convert);
- if (!filter || filter(item)) {
- items.push(item);
- }
+
+ 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);
}
- }
- // order the results
- if (options && options.order && id == undefined) {
- this._sort(items, options.order);
+ return new Moment(config);
}
- // filter fields of the items
- if (options && options.fields) {
- var fields = options.fields;
- if (id != undefined) {
- item = this._filterFields(item, fields);
- }
- else {
- for (i = 0, len = items.length; i < len; i++) {
- items[i] = this._filterFields(items[i], fields);
+ 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
+ }).utc();
+ };
+
+ // 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._input : (isNumber ? {} : input)),
+ matched = aspNetTimeSpanJsonRegex.exec(input),
+ sign,
+ ret;
+
+ if (isNumber) {
+ if (key) {
+ duration[key] = input;
+ } else {
+ duration.milliseconds = input;
}
+ } else if (matched) {
+ sign = (matched[1] === "-") ? -1 : 1;
+ duration = {
+ y: 0,
+ d: ~~matched[2] * sign,
+ h: ~~matched[3] * sign,
+ m: ~~matched[4] * sign,
+ s: ~~matched[5] * sign,
+ ms: ~~matched[6] * sign
+ };
}
- }
- // return the results
- if (type == 'DataTable') {
- var columns = this._getColumnNames(data);
- if (id != undefined) {
- // append a single item to the data table
- me._appendRow(data, columns, item);
+ ret = new Duration(duration);
+
+ if (isDuration && input.hasOwnProperty('_lang')) {
+ ret._lang = input._lang;
}
- else {
- // copy the items to the provided data table
- for (i = 0, len = items.length; i < len; i++) {
- me._appendRow(data, columns, items[i]);
- }
+
+ return ret;
+ };
+
+ // version number
+ moment.version = VERSION;
+
+ // default format
+ moment.defaultFormat = isoFormat;
+
+ // This function will be called whenever a moment is mutated.
+ // It is intended to keep the offset in sync with the timezone.
+ moment.updateOffset = function () {};
+
+ // 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) {
+ if (!key) {
+ return moment.fn._lang._abbr;
}
- return data;
- }
- else {
- // return an array
- if (id != undefined) {
- // a single item
- return item;
+ key = key.toLowerCase();
+ key = key.replace('_', '-');
+ if (values) {
+ loadLang(key, values);
+ } else if (values === null) {
+ unloadLang(key);
+ key = 'en';
+ } else if (!languages[key]) {
+ getLangDefinition(key);
}
- else {
- // multiple items
- if (data) {
- // copy the items to the provided array
- for (i = 0, len = items.length; i < len; i++) {
- data.push(items[i]);
- }
- return data;
- }
- else {
- // just return our array
- return items;
- }
+ 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);
+ };
-/**
- * Get ids of all items or from a filtered set of items.
- * @param {Object} [options] An Object with options. Available options:
- * {function} [filter] filter items
- * {String | function} [order] Order the items by
- * a field name or custom sort function.
- * @return {Array} ids
- */
-DataSet.prototype.getIds = function (options) {
- var data = this.data,
- filter = options && options.filter,
- order = options && options.order,
- convert = options && options.convert || this.options.convert,
- i,
- len,
- id,
- item,
- items,
- ids = [];
+ // compare moment object
+ moment.isMoment = function (obj) {
+ return obj instanceof Moment;
+ };
- if (filter) {
- // get filtered items
- if (order) {
- // create ordered list
- items = [];
- for (id in data) {
- if (data.hasOwnProperty(id)) {
- item = this._getItem(id, convert);
- if (filter(item)) {
- items.push(item);
- }
- }
- }
+ // for typechecking Duration objects
+ moment.isDuration = function (obj) {
+ return obj instanceof Duration;
+ };
- this._sort(items, order);
- for (i = 0, len = items.length; i < len; i++) {
- ids[i] = items[i][this.fieldId];
- }
- }
- else {
- // create unordered list
- for (id in data) {
- if (data.hasOwnProperty(id)) {
- item = this._getItem(id, convert);
- if (filter(item)) {
- ids.push(item[this.fieldId]);
- }
+ /************************************
+ Moment Prototype
+ ************************************/
+
+
+ extend(moment.fn = Moment.prototype, {
+
+ clone : function () {
+ return moment(this);
+ },
+
+ valueOf : function () {
+ return +this._d + ((this._offset || 0) * 60000);
+ },
+
+ unix : function () {
+ return Math.floor(+this / 1000);
+ },
+
+ toString : function () {
+ return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
+ },
+
+ toDate : function () {
+ return this._offset ? new Date(+this) : this._d;
+ },
+
+ toISOString : function () {
+ return formatMoment(moment(this).utc(), '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());
}
}
- }
- }
- else {
- // get all items
- if (order) {
- // create an ordered list
- items = [];
- for (id in data) {
- if (data.hasOwnProperty(id)) {
- items.push(data[id]);
- }
+ return !!this._isValid;
+ },
+
+ invalidAt: function () {
+ var i, arr1 = this._a, arr2 = (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray();
+ for (i = 6; i >= 0 && arr1[i] === arr2[i]; --i) {
+ // empty loop body
}
+ return i;
+ },
- this._sort(items, order);
+ utc : function () {
+ return this.zone(0);
+ },
- for (i = 0, len = items.length; i < len; i++) {
- ids[i] = items[i][this.fieldId];
- }
- }
- else {
- // create unordered list
- for (id in data) {
- if (data.hasOwnProperty(id)) {
- item = data[id];
- ids.push(item[this.fieldId]);
- }
+ local : function () {
+ this.zone(0);
+ 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;
+ },
- return ids;
-};
+ 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;
+ },
-/**
- * Execute a callback function for every item in the dataset.
- * The order of the items is not determined.
- * @param {function} callback
- * @param {Object} [options] Available options:
- * {Object.} [convert]
- * {String[]} [fields] filter fields
- * {function} [filter] filter items
- * {String | function} [order] Order the items by
- * a field name or custom sort function.
- */
-DataSet.prototype.forEach = function (callback, options) {
- var filter = options && options.filter,
- convert = options && options.convert || this.options.convert,
- data = this.data,
- item,
- id;
+ diff : function (input, units, asFloat) {
+ var that = this._isUTC ? moment(input).zone(this._offset || 0) : moment(input).local(),
+ zoneDiff = (this.zone() - that.zone()) * 6e4,
+ diff, output;
- if (options && options.order) {
- // execute forEach on ordered list
- var items = this.get(options);
+ units = normalizeUnits(units);
- for (var i = 0, len = items.length; i < len; i++) {
- item = items[i];
- id = item[this.fieldId];
- callback(item, id);
- }
- }
- else {
- // unordered
- for (id in data) {
- if (data.hasOwnProperty(id)) {
- item = this._getItem(id, convert);
- if (!filter || filter(item)) {
- callback(item, id);
+ if (units === 'year' || units === 'month') {
+ // average number of days in the months in the given dates
+ diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
+ // difference in months
+ output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
+ // adjust by taking difference in days, average number of days
+ // and dst in the given months.
+ output += ((this - moment(this).startOf('month')) -
+ (that - moment(that).startOf('month'))) / diff;
+ // same as above but with zones, to negate all dst
+ output -= ((this.zone() - moment(this).startOf('month').zone()) -
+ (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff;
+ if (units === 'year') {
+ output = output / 12;
}
+ } else {
+ diff = (this - that);
+ output = units === 'second' ? diff / 1e3 : // 1000
+ units === 'minute' ? diff / 6e4 : // 1000 * 60
+ units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
+ units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
+ units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
+ diff;
}
- }
- }
-};
+ return asFloat ? output : absRound(output);
+ },
-/**
- * Map every item in the dataset.
- * @param {function} callback
- * @param {Object} [options] Available options:
- * {Object.} [convert]
- * {String[]} [fields] filter fields
- * {function} [filter] filter items
- * {String | function} [order] Order the items by
- * a field name or custom sort function.
- * @return {Object[]} mappedItems
- */
-DataSet.prototype.map = function (callback, options) {
- var filter = options && options.filter,
- convert = options && options.convert || this.options.convert,
- mappedItems = [],
- data = this.data,
- item;
+ from : function (time, withoutSuffix) {
+ return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
+ },
- // convert and filter items
- for (var id in data) {
- if (data.hasOwnProperty(id)) {
- item = this._getItem(id, convert);
- if (!filter || filter(item)) {
- mappedItems.push(callback(item, id));
- }
- }
- }
+ fromNow : function (withoutSuffix) {
+ return this.from(moment(), withoutSuffix);
+ },
- // order items
- if (options && options.order) {
- this._sort(mappedItems, options.order);
- }
+ calendar : function () {
+ var diff = this.diff(moment().zone(this.zone()).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));
+ },
- return mappedItems;
-};
+ isLeapYear : function () {
+ var year = this.year();
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
+ },
-/**
- * Filter the fields of an item
- * @param {Object} item
- * @param {String[]} fields Field names
- * @return {Object} filteredItem
- * @private
- */
-DataSet.prototype._filterFields = function (item, fields) {
- var filteredItem = {};
+ isDST : function () {
+ return (this.zone() < this.clone().month(0).zone() ||
+ this.zone() < this.clone().month(5).zone());
+ },
- for (var field in item) {
- if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
- filteredItem[field] = item[field];
- }
- }
+ day : function (input) {
+ var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
+ if (input != null) {
+ if (typeof input === 'string') {
+ input = this.lang().weekdaysParse(input);
+ if (typeof input !== 'number') {
+ return this;
+ }
+ }
+ return this.add({ d : input - day });
+ } else {
+ return day;
+ }
+ },
- return filteredItem;
-};
+ month : function (input) {
+ var utc = this._isUTC ? 'UTC' : '',
+ dayOfMonth;
-/**
- * Sort the provided array with items
- * @param {Object[]} items
- * @param {String | function} order A field name or custom sort function.
- * @private
- */
-DataSet.prototype._sort = function (items, order) {
- if (util.isString(order)) {
- // order by provided field name
- var name = order; // field name
- items.sort(function (a, b) {
- var av = a[name];
- var bv = b[name];
- return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
- });
- }
- else if (typeof order === 'function') {
- // order by sort function
- items.sort(order);
- }
- // TODO: extend order by an Object {field:String, direction:String}
- // where direction can be 'asc' or 'desc'
- else {
- throw new TypeError('Order must be a function or a string');
- }
-};
+ if (input != null) {
+ if (typeof input === 'string') {
+ input = this.lang().monthsParse(input);
+ if (typeof input !== 'number') {
+ return this;
+ }
+ }
-/**
- * Remove an object by pointer or by id
- * @param {String | Number | Object | Array} id Object or id, or an array with
- * objects or ids to be removed
- * @param {String} [senderId] Optional sender id
- * @return {Array} removedIds
- */
-DataSet.prototype.remove = function (id, senderId) {
- var removedIds = [],
- i, len, removedId;
+ dayOfMonth = this.date();
+ this.date(1);
+ this._d['set' + utc + 'Month'](input);
+ this.date(Math.min(dayOfMonth, this.daysInMonth()));
- if (id instanceof Array) {
- for (i = 0, len = id.length; i < len; i++) {
- removedId = this._remove(id[i]);
- if (removedId != null) {
- removedIds.push(removedId);
+ moment.updateOffset(this);
+ return this;
+ } else {
+ return this._d['get' + utc + 'Month']();
+ }
+ },
+
+ startOf: function (units) {
+ units = normalizeUnits(units);
+ // 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 'isoweek':
+ 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 */
}
- }
- }
- else {
- removedId = this._remove(id);
- if (removedId != null) {
- removedIds.push(removedId);
- }
- }
- if (removedIds.length) {
- this._trigger('remove', {items: removedIds}, senderId);
- }
+ // weeks are a special case
+ if (units === 'week') {
+ this.weekday(0);
+ } else if (units === 'isoweek') {
+ this.isoWeekday(1);
+ }
- return removedIds;
-};
+ return this;
+ },
-/**
- * Remove an item by its id
- * @param {Number | String | Object} id id or item
- * @returns {Number | String | null} id
- * @private
- */
-DataSet.prototype._remove = function (id) {
- if (util.isNumber(id) || util.isString(id)) {
- if (this.data[id]) {
- delete this.data[id];
- delete this.internalIds[id];
- return id;
- }
- }
- else if (id instanceof Object) {
- var itemId = id[this.fieldId];
- if (itemId && this.data[itemId]) {
- delete this.data[itemId];
- delete this.internalIds[itemId];
- return itemId;
- }
- }
- return null;
-};
+ endOf: function (units) {
+ units = normalizeUnits(units);
+ return this.startOf(units).add((units === 'isoweek' ? 'week' : units), 1).subtract('ms', 1);
+ },
-/**
- * Clear the data
- * @param {String} [senderId] Optional sender id
- * @return {Array} removedIds The ids of all removed items
- */
-DataSet.prototype.clear = function (senderId) {
- var ids = Object.keys(this.data);
+ isAfter: function (input, units) {
+ units = typeof units !== 'undefined' ? units : 'millisecond';
+ return +this.clone().startOf(units) > +moment(input).startOf(units);
+ },
- this.data = {};
- this.internalIds = {};
+ isBefore: function (input, units) {
+ units = typeof units !== 'undefined' ? units : 'millisecond';
+ return +this.clone().startOf(units) < +moment(input).startOf(units);
+ },
- this._trigger('remove', {items: ids}, senderId);
+ isSame: function (input, units) {
+ units = typeof units !== 'undefined' ? units : 'millisecond';
+ return +this.clone().startOf(units) === +moment(input).startOf(units);
+ },
- return ids;
-};
+ min: function (other) {
+ other = moment.apply(null, arguments);
+ return other < this ? this : other;
+ },
-/**
- * Find the item with maximum value of a specified field
- * @param {String} field
- * @return {Object | null} item Item containing max value, or null if no items
- */
-DataSet.prototype.max = function (field) {
- var data = this.data,
- max = null,
- maxField = null;
+ max: function (other) {
+ other = moment.apply(null, arguments);
+ return other > this ? this : other;
+ },
- for (var id in data) {
- if (data.hasOwnProperty(id)) {
- var item = data[id];
- var itemField = item[field];
- if (itemField != null && (!max || itemField > maxField)) {
- max = item;
- maxField = itemField;
+ zone : function (input) {
+ var offset = this._offset || 0;
+ if (input != null) {
+ if (typeof input === "string") {
+ input = timezoneMinutesFromString(input);
+ }
+ if (Math.abs(input) < 16) {
+ input = input * 60;
+ }
+ this._offset = input;
+ this._isUTC = true;
+ if (offset !== input) {
+ addOrSubtractDurationFromMoment(this, moment.duration(offset - input, 'm'), 1, true);
+ }
+ } else {
+ return this._isUTC ? offset : this._d.getTimezoneOffset();
}
- }
- }
+ return this;
+ },
- return max;
-};
+ zoneAbbr : function () {
+ return this._isUTC ? "UTC" : "";
+ },
-/**
- * Find the item with minimum value of a specified field
- * @param {String} field
- * @return {Object | null} item Item containing max value, or null if no items
- */
-DataSet.prototype.min = function (field) {
- var data = this.data,
- min = null,
- minField = null;
+ zoneName : function () {
+ return this._isUTC ? "Coordinated Universal Time" : "";
+ },
- for (var id in data) {
- if (data.hasOwnProperty(id)) {
- var item = data[id];
- var itemField = item[field];
- if (itemField != null && (!min || itemField < minField)) {
- min = item;
- minField = itemField;
+ hasAlignedHourOffset : function (input) {
+ if (!input) {
+ input = 0;
+ }
+ else {
+ input = moment(input).zone();
}
- }
- }
- return min;
-};
+ return (this.zone() - input) % 60 === 0;
+ },
-/**
- * Find all distinct values of a specified field
- * @param {String} field
- * @return {Array} values Array containing all distinct values. If the data
- * items do not contain the specified field, an array
- * containing a single value undefined is returned.
- * The returned array is unordered.
- */
-DataSet.prototype.distinct = function (field) {
- var data = this.data,
- values = [],
- fieldType = this.options.convert[field],
- count = 0;
+ daysInMonth : function () {
+ return moment.utc([this.year(), this.month() + 1, 0]).date();
+ },
- for (var prop in data) {
- if (data.hasOwnProperty(prop)) {
- var item = data[prop];
- var value = util.convert(item[field], fieldType);
- var exists = false;
- for (var i = 0; i < count; i++) {
- if (values[i] == value) {
- exists = true;
- break;
- }
- }
- if (!exists) {
- values[count] = value;
- count++;
- }
- }
- }
+ 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));
+ },
- return values;
-};
+ weekYear : function (input) {
+ var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year;
+ return input == null ? year : this.add("y", (input - year));
+ },
-/**
- * Add a single item. Will fail when an item with the same id already exists.
- * @param {Object} item
- * @return {String} id
- * @private
- */
-DataSet.prototype._addItem = function (item) {
- var id = item[this.fieldId];
+ isoWeekYear : function (input) {
+ var year = weekOfYear(this, 1, 4).year;
+ return input == null ? year : this.add("y", (input - year));
+ },
- if (id != undefined) {
- // check whether this id is already taken
- if (this.data[id]) {
- // item already exists
- throw new Error('Cannot add item: item with id ' + id + ' already exists');
- }
- }
- else {
- // generate an id
- id = util.randomUUID();
- item[this.fieldId] = id;
- this.internalIds[id] = item;
- }
+ week : function (input) {
+ var week = this.lang().week(this);
+ return input == null ? week : this.add("d", (input - week) * 7);
+ },
- var d = {};
- for (var field in item) {
- if (item.hasOwnProperty(field)) {
- var fieldType = this.convert[field]; // type may be undefined
- d[field] = util.convert(item[field], fieldType);
- }
- }
- this.data[id] = d;
+ isoWeek : function (input) {
+ var week = weekOfYear(this, 1, 4).week;
+ return input == null ? week : this.add("d", (input - week) * 7);
+ },
- return id;
-};
+ weekday : function (input) {
+ var weekday = (this._d.getDay() + 7 - this.lang()._week.dow) % 7;
+ return input == null ? weekday : this.add("d", input - weekday);
+ },
-/**
- * Get an item. Fields can be converted to a specific type
- * @param {String} id
- * @param {Object.} [convert] field types to convert
- * @return {Object | null} item
- * @private
- */
-DataSet.prototype._getItem = function (id, convert) {
- var field, value;
+ isoWeekday : function (input) {
+ // behaves the same as moment#day except
+ // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
+ // as a setter, sunday should belong to the previous week.
+ return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
+ },
- // get the item from the dataset
- var raw = this.data[id];
- if (!raw) {
- return null;
- }
+ get : function (units) {
+ units = normalizeUnits(units);
+ return this[units.toLowerCase()]();
+ },
- // convert the items field types
- var converted = {},
- fieldId = this.fieldId,
- internalIds = this.internalIds;
- if (convert) {
- for (field in raw) {
- if (raw.hasOwnProperty(field)) {
- value = raw[field];
- // output all fields, except internal ids
- if ((field != fieldId) || !(value in internalIds)) {
- converted[field] = util.convert(value, convert[field]);
- }
+ set : function (units, value) {
+ units = normalizeUnits(units);
+ this[units.toLowerCase()](value);
+ },
+
+ // 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;
}
}
- }
- else {
- // no field types specified, no converting needed
- for (field in raw) {
- if (raw.hasOwnProperty(field)) {
- value = raw[field];
- // output all fields, except internal ids
- if ((field != fieldId) || !(value in internalIds)) {
- converted[field] = value;
- }
+ });
+
+ // helper for adding shortcuts
+ function makeGetterAndSetter(name, key) {
+ moment.fn[name] = moment.fn[name + 's'] = function (input) {
+ var utc = this._isUTC ? 'UTC' : '';
+ if (input != null) {
+ this._d['set' + utc + key](input);
+ moment.updateOffset(this);
+ return this;
+ } else {
+ return this._d['get' + utc + key]();
}
- }
+ };
}
- return converted;
-};
-
-/**
- * Update a single item: merge with existing item.
- * Will fail when the item has no id, or when there does not exist an item
- * with the same id.
- * @param {Object} item
- * @return {String} id
- * @private
- */
-DataSet.prototype._updateItem = function (item) {
- var id = item[this.fieldId];
- if (id == undefined) {
- throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
- }
- var d = this.data[id];
- if (!d) {
- // item doesn't exist
- throw new Error('Cannot update item: no item with id ' + id + ' found');
+ // 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]);
}
- // merge with current item
- for (var field in item) {
- if (item.hasOwnProperty(field)) {
- var fieldType = this.convert[field]; // type may be undefined
- d[field] = util.convert(item[field], fieldType);
- }
- }
+ // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
+ makeGetterAndSetter('year', 'FullYear');
- return id;
-};
+ // add plural methods
+ moment.fn.days = moment.fn.day;
+ moment.fn.months = moment.fn.month;
+ moment.fn.weeks = moment.fn.week;
+ moment.fn.isoWeeks = moment.fn.isoWeek;
-/**
- * Get an array with the column names of a Google DataTable
- * @param {DataTable} dataTable
- * @return {String[]} columnNames
- * @private
- */
-DataSet.prototype._getColumnNames = function (dataTable) {
- var columns = [];
- for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
- columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
- }
- return columns;
-};
+ // add aliased format methods
+ moment.fn.toJSON = moment.fn.toISOString;
-/**
- * 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();
+ /************************************
+ Duration Prototype
+ ************************************/
- for (var col = 0, cols = columns.length; col < cols; col++) {
- var field = columns[col];
- dataTable.setValue(row, col, item[field]);
- }
-};
-/**
- * DataView
- *
- * a dataview offers a filtered view on a dataset or an other dataview.
- *
- * @param {DataSet | DataView} data
- * @param {Object} [options] Available options: see method get
- *
- * @constructor DataView
- */
-function DataView (data, options) {
- this.id = util.randomUUID();
+ extend(moment.duration.fn = Duration.prototype, {
- this.data = null;
- this.ids = {}; // ids of the items currently in memory (just contains a boolean true)
- this.options = options || {};
- this.fieldId = 'id'; // name of the field containing id
- this.subscribers = {}; // event subscribers
+ _bubble : function () {
+ var milliseconds = this._milliseconds,
+ days = this._days,
+ months = this._months,
+ data = this._data,
+ seconds, minutes, hours, years;
- var me = this;
- this.listener = function () {
- me._onEvent.apply(me, arguments);
- };
+ // The following code bubbles up values, see the tests for
+ // examples of what that means.
+ data.milliseconds = milliseconds % 1000;
- this.setData(data);
-}
+ seconds = absRound(milliseconds / 1000);
+ data.seconds = seconds % 60;
-/**
- * Set a data source for the view
- * @param {DataSet | DataView} data
- */
-DataView.prototype.setData = function (data) {
- var ids, dataItems, i, len;
+ minutes = absRound(seconds / 60);
+ data.minutes = minutes % 60;
- if (this.data) {
- // unsubscribe from current dataset
- if (this.data.unsubscribe) {
- this.data.unsubscribe('*', this.listener);
- }
+ hours = absRound(minutes / 60);
+ data.hours = hours % 24;
- // trigger a remove of all items in memory
- ids = [];
- for (var id in this.ids) {
- if (this.ids.hasOwnProperty(id)) {
- ids.push(id);
+ days += absRound(hours / 24);
+ data.days = days % 30;
+
+ months += absRound(days / 30);
+ data.months = months % 12;
+
+ years = absRound(months / 12);
+ data.years = years;
+ },
+
+ weeks : function () {
+ return absRound(this.days() / 7);
+ },
+
+ valueOf : function () {
+ return this._milliseconds +
+ this._days * 864e5 +
+ (this._months % 12) * 2592e6 +
+ ~~(this._months / 12) * 31536e6;
+ },
+
+ humanize : function (withSuffix) {
+ var difference = +this,
+ output = relativeTime(difference, !withSuffix, this.lang());
+
+ if (withSuffix) {
+ output = this.lang().pastFuture(difference, output);
}
- }
- this.ids = {};
- this._trigger('remove', {items: ids});
- }
- this.data = data;
+ return this.lang().postformat(output);
+ },
- if (this.data) {
- // update fieldId
- this.fieldId = this.options.fieldId ||
- (this.data && this.data.options && this.data.options.fieldId) ||
- 'id';
+ add : function (input, val) {
+ // supports only 2.0-style add(1, 's') or add(moment)
+ var dur = moment.duration(input, val);
- // trigger an add of all added items
- ids = this.data.getIds({filter: this.options && this.options.filter});
- for (i = 0, len = ids.length; i < len; i++) {
- id = ids[i];
- this.ids[id] = true;
- }
- this._trigger('add', {items: ids});
+ this._milliseconds += dur._milliseconds;
+ this._days += dur._days;
+ this._months += dur._months;
- // subscribe to new dataset
- if (this.data.subscribe) {
- this.data.subscribe('*', this.listener);
- }
- }
-};
+ this._bubble();
-/**
- * Get data from the data view
- *
- * Usage:
- *
- * get()
- * get(options: Object)
- * get(options: Object, data: Array | DataTable)
- *
- * get(id: Number)
- * get(id: Number, options: Object)
- * get(id: Number, options: Object, data: Array | DataTable)
- *
- * get(ids: Number[])
- * get(ids: Number[], options: Object)
- * get(ids: Number[], options: Object, data: Array | DataTable)
- *
- * Where:
- *
- * {Number | String} id The id of an item
- * {Number[] | String{}} ids An array with ids of items
- * {Object} options An Object with options. Available options:
- * {String} [type] Type of data to be returned. Can
- * be 'DataTable' or 'Array' (default)
- * {Object.} [convert]
- * {String[]} [fields] field names to be returned
- * {function} [filter] filter items
- * {String | function} [order] Order the items by
- * a field name or custom sort function.
- * {Array | DataTable} [data] If provided, items will be appended to this
- * array or table. Required in case of Google
- * DataTable.
- * @param args
- */
-DataView.prototype.get = function (args) {
- var me = this;
+ return this;
+ },
- // parse the arguments
- var ids, options, data;
- var firstType = util.getType(arguments[0]);
- if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
- // get(id(s) [, options] [, data])
- ids = arguments[0]; // can be a single id or an array with ids
- options = arguments[1];
- data = arguments[2];
+ subtract : function (input, val) {
+ var dur = moment.duration(input, val);
+
+ this._milliseconds -= dur._milliseconds;
+ this._days -= dur._days;
+ this._months -= dur._months;
+
+ this._bubble();
+
+ return this;
+ },
+
+ get : function (units) {
+ units = normalizeUnits(units);
+ return this[units.toLowerCase() + 's']();
+ },
+
+ as : function (units) {
+ units = normalizeUnits(units);
+ return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's']();
+ },
+
+ lang : moment.fn.lang
+ });
+
+ function makeDurationGetter(name) {
+ moment.duration.fn[name] = function () {
+ return this._data[name];
+ };
}
- else {
- // get([, options] [, data])
- options = arguments[0];
- data = arguments[1];
+
+ 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());
+ }
}
- // extend the options with the default options and provided options
- var viewOptions = util.extend({}, this.options, options);
+ makeDurationAsGetter('Weeks', 6048e5);
+ moment.duration.fn.asMonths = function () {
+ return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12;
+ };
+
+
+ /************************************
+ 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;
+ }
+ });
+
+ /* EMBED_LANGUAGES */
- // create a combined filter method when needed
- if (this.options.filter && options && options.filter) {
- viewOptions.filter = function (item) {
- return me.options.filter(item) && options.filter(item);
- }
- }
+ /************************************
+ Exposing Moment
+ ************************************/
- // build up the call to the linked data set
- var getArguments = [];
- if (ids != undefined) {
- getArguments.push(ids);
- }
- getArguments.push(viewOptions);
- getArguments.push(data);
- return this.data && this.data.get.apply(this.data, getArguments);
-};
+ // 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);
+},{}],3:[function(require,module,exports){
/**
- * Get ids of all items or from a filtered set of items.
- * @param {Object} [options] An Object with options. Available options:
- * {function} [filter] filter items
- * {String | function} [order] Order the items by
- * a field name or custom sort function.
- * @return {Array} ids
+ * vis.js module imports
*/
-DataView.prototype.getIds = function (options) {
- var ids;
- if (this.data) {
- var defaultFilter = this.options.filter;
- var filter;
+// Try to load dependencies from the global window object.
+// If not available there, load via require.
+var moment = (typeof window !== 'undefined') && window['moment'] || require('moment');
+var Hammer = (typeof window !== 'undefined') && window['Hammer'] || require('hammerjs');
- if (options && options.filter) {
- if (defaultFilter) {
- filter = function (item) {
- return defaultFilter(item) && options.filter(item);
- }
- }
- else {
- filter = options.filter;
+
+// 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;
}
}
- else {
- filter = defaultFilter;
- }
+ return -1;
+ };
- ids = this.data.getIds({
- filter: filter,
- order: options && options.order
- });
+ try {
+ console.log("Warning: Ancient browser detected. Please update your browser");
}
- else {
- ids = [];
+ catch (err) {
}
+}
- return ids;
-};
-
-/**
- * Event listener. Will propagate all events from the connected data set to
- * the subscribers of the DataView, but will filter the items and only trigger
- * when there are changes in the filtered data set.
- * @param {String} event
- * @param {Object | null} params
- * @param {String} senderId
- * @private
- */
-DataView.prototype._onEvent = function (event, params, senderId) {
- var i, len, id, item,
- ids = params && params.items,
- data = this.data,
- added = [],
- updated = [],
- removed = [];
-
- if (ids && data) {
- switch (event) {
- case 'add':
- // filter the ids of the added items
- for (i = 0, len = ids.length; i < len; i++) {
- id = ids[i];
- item = this.get(id);
- if (item) {
- this.ids[id] = true;
- added.push(id);
- }
- }
+// 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);
+ }
+ }
+}
- break;
+// 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) {
- case 'update':
- // determine the event from the views viewpoint: an updated
- // item can be added, updated, or removed from this view.
- for (i = 0, len = ids.length; i < len; i++) {
- id = ids[i];
- item = this.get(id);
+ var T, A, k;
- if (item) {
- if (this.ids[id]) {
- updated.push(id);
- }
- else {
- this.ids[id] = true;
- added.push(id);
- }
- }
- else {
- if (this.ids[id]) {
- delete this.ids[id];
- removed.push(id);
- }
- else {
- // nothing interesting for me :-(
- }
- }
- }
+ if (this == null) {
+ throw new TypeError(" this is null or not defined");
+ }
- break;
+ // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
+ var O = Object(this);
- case 'remove':
- // filter the ids of the removed items
- for (i = 0, len = ids.length; i < len; i++) {
- id = ids[i];
- if (this.ids[id]) {
- delete this.ids[id];
- removed.push(id);
- }
- }
+ // 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;
- break;
+ // 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");
}
- if (added.length) {
- this._trigger('add', {items: added}, senderId);
- }
- if (updated.length) {
- this._trigger('update', {items: updated}, senderId);
- }
- if (removed.length) {
- this._trigger('remove', {items: removed}, senderId);
+ // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
+ if (thisArg) {
+ T = thisArg;
}
- }
-};
-// copy subscription functionality from DataSet
-DataView.prototype.subscribe = DataSet.prototype.subscribe;
-DataView.prototype.unsubscribe = DataSet.prototype.unsubscribe;
-DataView.prototype._trigger = DataSet.prototype._trigger;
+ // 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);
-/**
- * @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();
+ // 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.
- this.autoScale = true;
- this.scale = TimeStep.SCALE.DAY;
- this.step = 1;
+ // In browsers that support Object.defineProperty, use the following:
+ // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true });
- // initialize the range
- this.setRange(start, end, minimumStep);
-};
+ // For best browser support, use the following:
+ A[ k ] = mappedValue;
+ }
+ // d. Increase k by 1.
+ k++;
+ }
-/// enum scale
-TimeStep.SCALE = {
- MILLISECOND: 1,
- SECOND: 2,
- MINUTE: 3,
- HOUR: 4,
- DAY: 5,
- WEEKDAY: 6,
- MONTH: 7,
- YEAR: 8
-};
+ // 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";
-/**
- * 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;
- }
+ if (this == null) {
+ throw new TypeError();
+ }
- this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
- this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
+ var t = Object(this);
+ var len = t.length >>> 0;
+ if (typeof fun != "function") {
+ throw new TypeError();
+ }
- if (this.autoScale) {
- this.setMinimumStep(minimumStep);
- }
-};
+ 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);
+ }
+ }
-/**
- * Set the range iterator to the start date.
- */
-TimeStep.prototype.first = function() {
- this.current = new Date(this._start.valueOf());
- this.roundToMinor();
-};
+ return res;
+ };
+}
-/**
- * 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;
- }
- }
-};
+// 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;
-/**
- * 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());
-};
+ return function (obj) {
+ if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) {
+ throw new TypeError('Object.keys called on non-object');
+ }
-/**
- * Do the next step
- */
-TimeStep.prototype.next = function() {
- var prev = this.current.valueOf();
+ var result = [];
- // 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:
+ for (var prop in obj) {
+ if (hasOwnProperty.call(obj, prop)) result.push(prop);
+ }
- 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;
+ if (hasDontEnumBug) {
+ for (var i=0; i < dontEnumsLength; i++) {
+ if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]);
+ }
+ }
+ return result;
}
- }
- 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;
+ })()
+}
+
+// Internet Explorer 8 and older does not support Array.isArray,
+// so we define it here in that case.
+// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray
+if(!Array.isArray) {
+ Array.isArray = function (vArg) {
+ return Object.prototype.toString.call(vArg) === "[object Array]";
+ };
+}
+
+// Internet Explorer 8 and older does not support Function.bind,
+// so we define it here in that case.
+// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
+if (!Function.prototype.bind) {
+ Function.prototype.bind = function (oThis) {
+ if (typeof this !== "function") {
+ // closest thing possible to the ECMAScript 5 internal IsCallable function
+ throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}
- }
- 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;
+ var aArgs = Array.prototype.slice.call(arguments, 1),
+ fToBind = this,
+ fNOP = function () {},
+ fBound = function () {
+ return fToBind.apply(this instanceof fNOP && oThis
+ ? this
+ : oThis,
+ aArgs.concat(Array.prototype.slice.call(arguments)));
+ };
+
+ fNOP.prototype = this.prototype;
+ fBound.prototype = new fNOP();
+
+ return fBound;
+ };
+}
+
+// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create
+if (!Object.create) {
+ Object.create = function (o) {
+ if (arguments.length > 1) {
+ throw new Error('Object.create implementation only accepts the first parameter.');
+ }
+ function F() {}
+ F.prototype = o;
+ return new F();
+ };
+}
+
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
+if (!Function.prototype.bind) {
+ Function.prototype.bind = function (oThis) {
+ if (typeof this !== "function") {
+ // closest thing possible to the ECMAScript 5 internal IsCallable function
+ throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}
- }
- // safety mechanism: if current time is still unchanged, move to the end
- if (this.current.valueOf() == prev) {
- this.current = new Date(this._end.valueOf());
- }
-};
+ var aArgs = Array.prototype.slice.call(arguments, 1),
+ fToBind = this,
+ fNOP = function () {},
+ fBound = function () {
+ return fToBind.apply(this instanceof fNOP && oThis
+ ? this
+ : oThis,
+ aArgs.concat(Array.prototype.slice.call(arguments)));
+ };
+
+ fNOP.prototype = this.prototype;
+ fBound.prototype = new fNOP();
+ return fBound;
+ };
+}
/**
- * Get the current datetime
- * @return {Date} current The current date
+ * utility functions
*/
-TimeStep.prototype.getCurrent = function() {
- return this.current;
-};
+var util = {};
/**
- * 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.
+ * Test whether given object is a number
+ * @param {*} object
+ * @return {Boolean} isNumber
*/
-TimeStep.prototype.setScale = function(newScale, newStep) {
- this.scale = newScale;
-
- if (newStep > 0) {
- this.step = newStep;
- }
-
- this.autoScale = false;
+util.isNumber = function isNumber(object) {
+ return (object instanceof Number || typeof object == 'number');
};
/**
- * Enable or disable autoscaling
- * @param {boolean} enable If true, autoascaling is set true
+ * Test whether given object is a string
+ * @param {*} object
+ * @return {Boolean} isString
*/
-TimeStep.prototype.setAutoScale = function (enable) {
- this.autoScale = enable;
+util.isString = function isString(object) {
+ return (object instanceof String || typeof object == 'string');
};
-
/**
- * Automatically determine the scale that bests fits the provided minimum step
- * @param {Number} [minimumStep] The minimum step size in milliseconds
+ * Test whether given object is a Date, or a String containing a Date
+ * @param {Date | String} object
+ * @return {Boolean} isDate
*/
-TimeStep.prototype.setMinimumStep = function(minimumStep) {
- if (minimumStep == undefined) {
- return;
+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;
+ }
}
- 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;}
+ return false;
};
/**
- * 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
+ * Test whether given object is an instance of google.visualization.DataTable
+ * @param {*} object
+ * @return {Boolean} isDataTable
*/
-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);
- }
+util.isDataTable = function isDataTable(object) {
+ return (typeof (google) !== 'undefined') &&
+ (google.visualization) &&
+ (google.visualization.DataTable) &&
+ (object instanceof google.visualization.DataTable);
};
/**
- * 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.
+ * Create a semi UUID
+ * source: http://stackoverflow.com/a/105074/1262753
+ * @return {String} uuid
*/
-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;
- }
+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()
+ );
};
-
/**
- * 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
+ * Extend object a with the properties of object b or a series of objects
+ * Only properties with defined values are copied
+ * @param {Object} a
+ * @param {... Object} b
+ * @return {Object} a
*/
-TimeStep.prototype.getLabelMinor = function(date) {
- if (date == undefined) {
- date = this.current;
+util.extend = function (a, b) {
+ for (var i = 1, len = arguments.length; i < len; i++) {
+ var other = arguments[i];
+ for (var prop in other) {
+ if (other.hasOwnProperty(prop) && other[prop] !== undefined) {
+ a[prop] = other[prop];
+ }
+ }
}
- 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 '';
- }
+ return a;
};
-
/**
- * Returns formatted text for the major axis label, depending on the current
- * date and the scale. For example when scale is MINUTE, the major scale is
- * hours, and the hour will be formatted as "hh".
- * @param {Date} [date] custom date. if not provided, current date is taken
+ * Convert an object to another type
+ * @param {Boolean | Number | String | Date | Moment | Null | undefined} object
+ * @param {String | undefined} type Name of the type. Available types:
+ * 'Boolean', 'Number', 'String',
+ * 'Date', 'Moment', ISODate', 'ASPDate'.
+ * @return {*} object
+ * @throws Error
*/
-TimeStep.prototype.getLabelMajor = function(date) {
- if (date == undefined) {
- date = this.current;
+util.convert = function convert(object, type) {
+ var match;
+
+ if (object === undefined) {
+ return undefined;
+ }
+ if (object === null) {
+ return null;
}
- //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 '';
+ if (!type) {
+ return object;
+ }
+ if (!(typeof type === 'string') && !(type instanceof String)) {
+ throw new Error('Type must be a string');
}
-};
-/**
- * @constructor Stack
- * Stacks items on top of each other.
- * @param {ItemSet} parent
- * @param {Object} [options]
- */
-function Stack (parent, options) {
- this.parent = parent;
+ //noinspection FallthroughInSwitchStatementJS
+ switch (type) {
+ case 'boolean':
+ case 'Boolean':
+ return Boolean(object);
- this.options = options || {};
- this.defaultOptions = {
- order: function (a, b) {
- //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup
- // Order: ranges over non-ranges, ranged ordered by width, and
- // lastly ordered by start.
- if (a instanceof ItemRange) {
- if (b instanceof ItemRange) {
- var aInt = (a.data.end - a.data.start);
- var bInt = (b.data.end - b.data.start);
- return (aInt - bInt) || (a.data.start - b.data.start);
+ case 'number':
+ case 'Number':
+ return Number(object.valueOf());
+
+ 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());
+ }
+ else if (moment.isMoment(object)) {
+ return new Date(object.valueOf());
+ }
+ if (util.isString(object)) {
+ match = ASPDateRegex.exec(object);
+ if (match) {
+ // object is an ASP date
+ return new Date(Number(match[1])); // parse number
}
else {
- return -1;
+ return moment(object).toDate(); // parse string
}
}
else {
- if (b instanceof ItemRange) {
- return 1;
+ throw new Error(
+ 'Cannot convert object of type ' + util.getType(object) +
+ ' to type Date');
+ }
+
+ case 'Moment':
+ if (util.isNumber(object)) {
+ return moment(object);
+ }
+ if (object instanceof Date) {
+ return moment(object.valueOf());
+ }
+ else if (moment.isMoment(object)) {
+ return moment.clone();
+ }
+ if (util.isString(object)) {
+ match = ASPDateRegex.exec(object);
+ if (match) {
+ // object is an ASP date
+ return moment(Number(match[1])); // parse number
}
else {
- return (a.data.start - b.data.start);
+ return moment(object); // parse string
}
}
- },
- margin: {
- item: 10
+ else {
+ throw new Error(
+ 'Cannot convert object of type ' + util.getType(object) +
+ ' to type Date');
+ }
+
+ case 'ISODate':
+ if (util.isNumber(object)) {
+ return new Date(object);
+ }
+ else if (object instanceof Date) {
+ return object.toISOString();
+ }
+ else if (moment.isMoment(object)) {
+ return object.toDate().toISOString();
+ }
+ else if (util.isString(object)) {
+ match = ASPDateRegex.exec(object);
+ if (match) {
+ // object is an ASP date
+ return new Date(Number(match[1])).toISOString(); // parse number
+ }
+ else {
+ return new Date(object).toISOString(); // parse string
+ }
+ }
+ else {
+ throw new Error(
+ 'Cannot convert object of type ' + util.getType(object) +
+ ' to type ISODate');
+ }
+
+ case 'ASPDate':
+ if (util.isNumber(object)) {
+ return '/Date(' + object + ')/';
+ }
+ else if (object instanceof Date) {
+ return '/Date(' + object.valueOf() + ')/';
+ }
+ else if (util.isString(object)) {
+ match = ASPDateRegex.exec(object);
+ var value;
+ if (match) {
+ // object is an ASP date
+ value = new Date(Number(match[1])).valueOf(); // parse number
+ }
+ else {
+ value = new Date(object).valueOf(); // parse string
+ }
+ return '/Date(' + value + ')/';
+ }
+ else {
+ throw new Error(
+ 'Cannot convert object of type ' + util.getType(object) +
+ ' to type ASPDate');
+ }
+
+ default:
+ throw new Error('Cannot convert object of type ' + util.getType(object) +
+ ' to type "' + type + '"');
+ }
+};
+
+// parse ASP.Net Date pattern,
+// for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
+// code from http://momentjs.com/
+var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
+
+/**
+ * Get the type of an object, for example util.getType([]) returns 'Array'
+ * @param {*} object
+ * @return {String} type
+ */
+util.getType = function getType(object) {
+ var type = typeof object;
+
+ if (type == 'object') {
+ if (object == null) {
+ return 'null';
}
- };
+ if (object instanceof Boolean) {
+ return 'Boolean';
+ }
+ if (object instanceof Number) {
+ return 'Number';
+ }
+ if (object instanceof String) {
+ return 'String';
+ }
+ if (object instanceof Array) {
+ return 'Array';
+ }
+ if (object instanceof Date) {
+ return 'Date';
+ }
+ return 'Object';
+ }
+ else if (type == 'number') {
+ return 'Number';
+ }
+ else if (type == 'boolean') {
+ return 'Boolean';
+ }
+ else if (type == 'string') {
+ return 'String';
+ }
- this.ordered = []; // ordered items
-}
+ return type;
+};
/**
- * Set options for the stack
- * @param {Object} options Available options:
- * {ItemSet} parent
- * {Number} margin
- * {function} order Stacking order
+ * 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.
*/
-Stack.prototype.setOptions = function setOptions (options) {
- util.extend(this.options, options);
+util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
+ var doc = document.documentElement;
+ var body = document.body;
- // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately
+ 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;
};
/**
- * Stack the items such that they don't overlap. The items will have a minimal
- * distance equal to options.margin.item.
+ * 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.
*/
-Stack.prototype.update = function update() {
- this._order();
- this._stack();
+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;
};
/**
- * 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
+ * Get the absolute, vertical mouse position from an event.
+ * @param {Event} event
+ * @return {Number} pageY
*/
-Stack.prototype._order = function _order () {
- var items = this.parent.items;
- if (!items) {
- throw new Error('Cannot stack items: parent does not contain items');
+util.getPageY = function getPageY (event) {
+ if ('pageY' in event) {
+ return event.pageY;
}
-
- // TODO: store the sorted items, to have less work later on
- var ordered = [];
- var index = 0;
- // items is a map (no array)
- util.forEach(items, function (item) {
- if (item.visible) {
- ordered[index] = item;
- index++;
+ else {
+ var clientY;
+ if (('targetTouches' in event) && event.targetTouches.length) {
+ clientY = event.targetTouches[0].clientY;
+ }
+ else {
+ clientY = event.clientY;
}
- });
- //if a customer stack order function exists, use it.
- var order = this.options.order || this.defaultOptions.order;
- if (!(typeof order === 'function')) {
- throw new Error('Option order must be a function');
+ var doc = document.documentElement;
+ var body = document.body;
+ return clientY +
+ ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
+ ( doc && doc.clientTop || body && body.clientTop || 0 );
}
-
- ordered.sort(order);
-
- this.ordered = ordered;
};
/**
- * Adjust vertical positions of the events such that they don't overlap each
- * other.
- * @private
+ * Get the absolute, horizontal mouse position from an event.
+ * @param {Event} event
+ * @return {Number} pageX
*/
-Stack.prototype._stack = function _stack () {
- var i,
- iMax,
- ordered = this.ordered,
- options = this.options,
- orientation = options.orientation || this.defaultOptions.orientation,
- axisOnTop = (orientation == 'top'),
- margin;
-
- if (options.margin && options.margin.item !== undefined) {
- margin = options.margin.item;
+util.getPageX = function getPageX (event) {
+ if ('pageY' in event) {
+ return event.pageX;
}
else {
- margin = this.defaultOptions.margin.item
- }
+ var clientX;
+ if (('targetTouches' in event) && event.targetTouches.length) {
+ clientX = event.targetTouches[0].clientX;
+ }
+ else {
+ clientX = event.clientX;
+ }
- // 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);
+ var doc = document.documentElement;
+ var body = document.body;
+ return clientX +
+ ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
+ ( doc && doc.clientLeft || body && body.clientLeft || 0 );
}
};
/**
- * 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.
+ * add a className to the given elements style
+ * @param {Element} elem
+ * @param {String} className
*/
-Stack.prototype.checkOverlap = function checkOverlap (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;
- }
- }
+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(' ');
}
+};
- return null;
+/**
+ * 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(' ');
+ }
};
/**
- * 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
+ * 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)
*/
-Stack.prototype.collision = function collision (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);
+util.forEach = function forEach (object, callback) {
+ var i,
+ len;
+ if (object instanceof Array) {
+ // array
+ for (i = 0, len = object.length; i < len; i++) {
+ callback(object[i], i, object);
+ }
+ }
+ else {
+ // object
+ for (i in object) {
+ if (object.hasOwnProperty(i)) {
+ callback(object[i], i, object);
+ }
+ }
+ }
};
/**
- * @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
+ * Update a property in an object
+ * @param {Object} object
+ * @param {String} key
+ * @param {*} value
+ * @return {Boolean} changed
*/
-function Range(options) {
- this.id = util.randomUUID();
- this.start = 0; // Number
- this.end = 0; // Number
+util.updateProperty = function updateProp (object, key, value) {
+ if (object[key] !== value) {
+ object[key] = value;
+ return true;
+ }
+ else {
+ return false;
+ }
+};
- this.options = {
- min: null,
- max: null,
- zoomMin: null,
- zoomMax: null
- };
+/**
+ * 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;
- this.listeners = [];
+ if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
+ action = "DOMMouseScroll"; // For Firefox
+ }
- this.setOptions(options);
-}
+ element.addEventListener(action, listener, useCapture);
+ } else {
+ element.attachEvent("on" + action, listener); // IE browsers
+ }
+};
/**
- * 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).
+ * 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]
*/
-Range.prototype.setOptions = function (options) {
- util.extend(this.options, options);
+util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
+ if (element.removeEventListener) {
+ // non-IE browsers
+ if (useCapture === undefined)
+ useCapture = false;
- if (options.start != null || options.end != null) {
- this.setRange(options.start, options.end);
+ 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);
}
};
+
/**
- * 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'
+ * Get HTML element which is the target of the event
+ * @param {Event} event
+ * @return {Element} target element
*/
-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".');
+util.getTarget = function getTarget(event) {
+ // code from http://www.quirksmode.org/js/events_properties.html
+ if (!event) {
+ event = window.event;
}
- //noinspection FallthroughInSwitchStatementJS
- if (event == 'move') {
- listener = {
- component: component,
- event: event,
- direction: direction,
- callback: function (event) {
- me._onMouseDown(event, listener);
- },
- params: {}
- };
+ var target;
- component.on('mousedown', listener.callback);
- me.listeners.push(listener);
+ if (event.target) {
+ target = event.target;
}
- 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 if (event.srcElement) {
+ target = event.srcElement;
}
- else {
- throw new TypeError('Unknown event "' + event + '". ' +
- 'Choose "move" or "zoom".');
+
+ if (target.nodeType != undefined && target.nodeType == 3) {
+ // defeat Safari bug
+ target = target.parentNode;
}
-};
-/**
- * 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);
+ return target;
};
/**
- * Trigger an event
- * @param {String} event name of the event, available events: 'rangechange',
- * 'rangechanged'
- * @private
+ * Stop event propagation
*/
-Range.prototype._trigger = function (event) {
- events.trigger(this, event, {
- start: this.start,
- end: this.end
- });
-};
+util.stopPropagation = function stopPropagation(event) {
+ if (!event)
+ event = window.event;
-/**
- * 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');
+ if (event.stopPropagation) {
+ event.stopPropagation(); // non-IE browsers
+ }
+ else {
+ event.cancelBubble = true; // IE browsers
}
};
+
/**
- * 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
+ * Cancels the event if it is cancelable, without stopping further propagation of the event.
*/
-Range.prototype._applyRange = function(start, end) {
- var newStart = (start != null) ? util.convert(start, 'Number') : this.start;
- var newEnd = (end != null) ? util.convert(end, 'Number') : this.end;
- var diff;
-
- // check for valid number
- if (isNaN(newStart)) {
- throw new Error('Invalid start "' + start + '"');
- }
- if (isNaN(newEnd)) {
- throw new Error('Invalid end "' + end + '"');
- }
+util.preventDefault = function preventDefault (event) {
+ if (!event)
+ event = window.event;
- // prevent start < end
- if (newEnd < newStart) {
- newEnd = newStart;
+ if (event.preventDefault) {
+ event.preventDefault(); // non-IE browsers
}
-
- // prevent start < min
- if (this.options.min != null) {
- var min = this.options.min.valueOf();
- if (newStart < min) {
- diff = (min - newStart);
- newStart += diff;
- newEnd += diff;
- }
+ else {
+ event.returnValue = false; // IE browsers
}
+};
- // 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;
- }
- }
- }
+util.option = {};
- // 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;
- }
- }
+/**
+ * Convert a value into a boolean
+ * @param {Boolean | function | undefined} value
+ * @param {Boolean} [defaultValue]
+ * @returns {Boolean} bool
+ */
+util.option.asBoolean = function (value, defaultValue) {
+ if (typeof value == 'function') {
+ value = value();
}
- var changed = (this.start != newStart || this.end != newEnd);
-
- this.start = newStart;
- this.end = newEnd;
+ if (value != null) {
+ return (value != false);
+ }
- return changed;
+ return defaultValue || null;
};
/**
- * Retrieve the current range.
- * @return {Object} An object with start and end properties
+ * Convert a value into a number
+ * @param {Boolean | function | undefined} value
+ * @param {Number} [defaultValue]
+ * @returns {Number} number
*/
-Range.prototype.getRange = function() {
- return {
- start: this.start,
- end: this.end
- };
+util.option.asNumber = function (value, defaultValue) {
+ if (typeof value == 'function') {
+ value = value();
+ }
+
+ if (value != null) {
+ return Number(value) || defaultValue || null;
+ }
+
+ return defaultValue || null;
};
/**
- * Calculate the conversion offset and factor for current range, based on
- * the provided width
- * @param {Number} width
- * @returns {{offset: number, factor: number}} conversion
+ * Convert a value into a string
+ * @param {String | function | undefined} value
+ * @param {String} [defaultValue]
+ * @returns {String} str
*/
-Range.prototype.conversion = function (width) {
- var start = this.start;
- var end = this.end;
+util.option.asString = function (value, defaultValue) {
+ if (typeof value == 'function') {
+ value = value();
+ }
- return Range.conversion(this.start, this.end, width);
+ if (value != null) {
+ return String(value);
+ }
+
+ return defaultValue || null;
};
/**
- * 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
+ * Convert a size or location into a string with pixels or a percentage
+ * @param {String | Number | function | undefined} value
+ * @param {String} [defaultValue]
+ * @returns {String} size
*/
-Range.conversion = function (start, end, width) {
- if (width != 0 && (end - start != 0)) {
- return {
- offset: start,
- factor: width / (end - start)
- }
+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 {
- offset: 0,
- factor: 1
- };
+ return defaultValue || null;
}
};
/**
- * Start moving horizontally or vertically
- * @param {Event} event
- * @param {Object} listener Listener containing the component and params
- * @private
+ * Convert a value into a DOM element
+ * @param {HTMLElement | function | undefined} value
+ * @param {HTMLElement} [defaultValue]
+ * @returns {HTMLElement | null} dom
*/
-Range.prototype._onMouseDown = function(event, listener) {
- event = event || window.event;
- var params = listener.params;
+util.option.asElement = function (value, defaultValue) {
+ if (typeof value == 'function') {
+ value = value();
+ }
- // only react on left mouse button down
- var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
- if (!leftButtonDown) {
+ return value || defaultValue || null;
+};
+
+/**
+ * load css from text
+ * @param {String} css Text containing css
+ */
+util.loadCss = function (css) {
+ if (typeof document === 'undefined') {
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;
+ // get the script location, and built the css file name from the js file name
+ // http://stackoverflow.com/a/2161748/1262753
+ // var scripts = document.getElementsByTagName('script');
+ // var jsFile = scripts[scripts.length-1].src.split('?')[0];
+ // var cssFile = jsFile.substring(0, jsFile.length - 2) + 'css';
- var frame = listener.component.frame;
- if (frame) {
- frame.style.cursor = 'move';
+ // inject css
+ // http://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript
+ var style = document.createElement('style');
+ style.type = 'text/css';
+ if (style.styleSheet){
+ style.styleSheet.cssText = css;
+ } else {
+ style.appendChild(document.createTextNode(css));
}
- // 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);
+ document.getElementsByTagName('head')[0].appendChild(style);
+};
+
+/**
+ * Event listener (singleton)
+ */
+// TODO: replace usage of the event listener for the EventBus
+var events = {
+ 'listeners': [],
+
+ /**
+ * Find a single listener by its object
+ * @param {Object} object
+ * @return {Number} index -1 when not found
+ */
+ 'indexOf': function (object) {
+ var listeners = this.listeners;
+ for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
+ var listener = listeners[i];
+ if (listener && listener.object == object) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ /**
+ * Add an event listener
+ * @param {Object} object
+ * @param {String} event The name of an event, for example 'select'
+ * @param {function} callback The callback method, called when the
+ * event takes place
+ */
+ 'addListener': function (object, event, callback) {
+ var index = this.indexOf(object);
+ var listener = this.listeners[index];
+ if (!listener) {
+ listener = {
+ 'object': object,
+ 'events': {}
+ };
+ this.listeners.push(listener);
+ }
+
+ var callbacks = listener.events[event];
+ if (!callbacks) {
+ callbacks = [];
+ listener.events[event] = callbacks;
+ }
+
+ // add the callback if it does not yet exist
+ if (callbacks.indexOf(callback) == -1) {
+ callbacks.push(callback);
+ }
+ },
+
+ /**
+ * Remove an event listener
+ * @param {Object} object
+ * @param {String} event The name of an event, for example 'select'
+ * @param {function} callback The registered callback method
+ */
+ 'removeListener': function (object, event, callback) {
+ var index = this.indexOf(object);
+ var listener = this.listeners[index];
+ if (listener) {
+ var callbacks = listener.events[event];
+ if (callbacks) {
+ index = callbacks.indexOf(callback);
+ if (index != -1) {
+ callbacks.splice(index, 1);
+ }
+
+ // remove the array when empty
+ if (callbacks.length == 0) {
+ delete listener.events[event];
+ }
+ }
+
+ // count the number of registered events. remove listener when empty
+ var count = 0;
+ var events = listener.events;
+ for (var e in events) {
+ if (events.hasOwnProperty(e)) {
+ count++;
+ }
+ }
+ if (count == 0) {
+ delete this.listeners[index];
+ }
+ }
+ },
+
+ /**
+ * Remove all registered event listeners
+ */
+ 'removeAllListeners': function () {
+ this.listeners = [];
+ },
+
+ /**
+ * Trigger an event. All registered event handlers will be called
+ * @param {Object} object
+ * @param {String} event
+ * @param {Object} properties (optional)
+ */
+ 'trigger': function (object, event, properties) {
+ var index = this.indexOf(object);
+ var listener = this.listeners[index];
+ if (listener) {
+ var callbacks = listener.events[event];
+ if (callbacks) {
+ for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
+ callbacks[i](properties);
+ }
+ }
+ }
}
-
- util.preventDefault(event);
};
/**
- * Perform moving operating.
- * This function activated from within the funcion TimeAxis._onMouseDown().
- * @param {Event} event
- * @param {Object} listener
- * @private
+ * An event bus can be used to emit events, and to subscribe to events
+ * @constructor EventBus
*/
-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;
+function EventBus() {
+ this.subscriptions = [];
+}
- // if mouse movement is big enough, register it as a "moved" event
- if (Math.abs(diff) >= 1) {
- params.moved = true;
- }
+/**
+ * Subscribe to an event
+ * @param {String | RegExp} event The event can be a regular expression, or
+ * a string with wildcards, like 'server.*'.
+ * @param {function} callback. Callback are called with three parameters:
+ * {String} event, {*} [data], {*} [source]
+ * @param {*} [target]
+ * @returns {String} id A subscription id
+ */
+EventBus.prototype.on = function (event, callback, target) {
+ var regexp = (event instanceof RegExp) ?
+ event :
+ new RegExp(event.replace('*', '\\w+'));
- var 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);
+ var subscription = {
+ id: util.randomUUID(),
+ event: event,
+ regexp: regexp,
+ callback: (typeof callback === 'function') ? callback : null,
+ target: target
+ };
- // fire a rangechange event
- this._trigger('rangechange');
+ this.subscriptions.push(subscription);
- util.preventDefault(event);
+ return subscription.id;
};
/**
- * Stop moving operating.
- * This function activated from within the function Range._onMouseDown().
- * @param {event} event
- * @param {Object} listener
- * @private
+ * Unsubscribe from an event
+ * @param {String | Object} filter Filter for subscriptions to be removed
+ * Filter can be a string containing a
+ * subscription id, or an object containing
+ * one or more of the fields id, event,
+ * callback, and target.
*/
-Range.prototype._onMouseUp = function (event, listener) {
- event = event || window.event;
-
- var params = listener.params;
+EventBus.prototype.off = function (filter) {
+ var i = 0;
+ while (i < this.subscriptions.length) {
+ var subscription = this.subscriptions[i];
- if (listener.component.frame) {
- listener.component.frame.style.cursor = 'auto';
- }
+ var match = true;
+ if (filter instanceof Object) {
+ // filter is an object. All fields must match
+ for (var prop in filter) {
+ if (filter.hasOwnProperty(prop)) {
+ if (filter[prop] !== subscription[prop]) {
+ match = false;
+ }
+ }
+ }
+ }
+ else {
+ // filter is a string, filter on id
+ match = (subscription.id == filter);
+ }
- // remove event listeners here, important for Safari
- if (params.onMouseMove) {
- util.removeEventListener(document, "mousemove", params.onMouseMove);
- params.onMouseMove = null;
- }
- if (params.onMouseUp) {
- util.removeEventListener(document, "mouseup", params.onMouseUp);
- params.onMouseUp = null;
+ if (match) {
+ this.subscriptions.splice(i, 1);
+ }
+ else {
+ i++;
+ }
}
- //util.preventDefault(event);
+};
- if (params.moved) {
- // fire a rangechanged event
- this._trigger('rangechanged');
+/**
+ * Emit an event
+ * @param {String} event
+ * @param {*} [data]
+ * @param {*} [source]
+ */
+EventBus.prototype.emit = function (event, data, source) {
+ for (var i =0; i < this.subscriptions.length; i++) {
+ var subscription = this.subscriptions[i];
+ if (subscription.regexp.test(event)) {
+ if (subscription.callback) {
+ subscription.callback(event, data, source);
+ }
+ }
}
};
/**
- * Event handler for mouse wheel event, used to zoom
- * Code from http://adomas.org/javascript-mouse-wheel/
- * @param {Event} event
- * @param {Object} listener
- * @private
+ * DataSet
+ *
+ * Usage:
+ * var dataSet = new DataSet({
+ * fieldId: '_id',
+ * convert: {
+ * // ...
+ * }
+ * });
+ *
+ * dataSet.add(item);
+ * dataSet.add(data);
+ * dataSet.update(item);
+ * dataSet.update(data);
+ * dataSet.remove(id);
+ * dataSet.remove(ids);
+ * var data = dataSet.get();
+ * var data = dataSet.get(id);
+ * var data = dataSet.get(ids);
+ * var data = dataSet.get(ids, options, data);
+ * dataSet.clear();
+ *
+ * A data set can:
+ * - add/remove/update data
+ * - gives triggers upon changes in the data
+ * - can import/export data in various data formats
+ *
+ * @param {Object} [options] Available options:
+ * {String} fieldId Field name of the id in the
+ * items, 'id' by default.
+ * {Object.=end )
- if (zoomFactor >= 1) {
- zoomFactor = 0.9;
+ var subscribers = [];
+ if (event in this.subscribers) {
+ subscribers = subscribers.concat(this.subscribers[event]);
}
- if (zoomFactor <= -1) {
- zoomFactor = -0.9;
+ if ('*' in this.subscribers) {
+ subscribers = subscribers.concat(this.subscribers['*']);
}
- // 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);
+ for (var i = 0; i < subscribers.length; i++) {
+ var subscriber = subscribers[i];
+ if (subscriber.callback) {
+ subscriber.callback(event, params, senderId || null);
+ }
}
+};
- // zoom start and end relative to the zoomAround value
- var startDiff = (this.start - zoomAround);
- var endDiff = (this.end - zoomAround);
+/**
+ * Add data.
+ * Adding an item will fail when there already is an item with the same id.
+ * @param {Object | Array | DataTable} data
+ * @param {String} [senderId] Optional sender id
+ * @return {Array} addedIds Array with the ids of the added items
+ */
+DataSet.prototype.add = function (data, senderId) {
+ var addedIds = [],
+ id,
+ me = this;
- // calculate new start and end
- var newStart = this.start - startDiff * zoomFactor;
- var newEnd = this.end - endDiff * zoomFactor;
+ if (data instanceof Array) {
+ // Array
+ for (var i = 0, len = data.length; i < len; i++) {
+ id = me._addItem(data[i]);
+ addedIds.push(id);
+ }
+ }
+ else if (util.isDataTable(data)) {
+ // Google DataTable
+ var columns = this._getColumnNames(data);
+ for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
+ var item = {};
+ for (var col = 0, cols = columns.length; col < cols; col++) {
+ var field = columns[col];
+ item[field] = data.getValue(row, col);
+ }
- this.setRange(newStart, newEnd);
+ id = me._addItem(item);
+ addedIds.push(id);
+ }
+ }
+ else if (data instanceof Object) {
+ // Single item
+ id = me._addItem(data);
+ addedIds.push(id);
+ }
+ else {
+ throw new Error('Unknown dataType');
+ }
+
+ if (addedIds.length) {
+ this._trigger('add', {items: addedIds}, senderId);
+ }
+
+ return addedIds;
};
/**
- * 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
+ * Update existing items. When an item does not exist, it will be created
+ * @param {Object | Array | DataTable} data
+ * @param {String} [senderId] Optional sender id
+ * @return {Array} updatedIds The ids of the added or updated items
*/
-Range.prototype.move = function(moveFactor) {
- // zoom start Date and end Date relative to the zoomAroundDate
- var diff = (this.end - this.start);
+DataSet.prototype.update = function (data, senderId) {
+ var addedIds = [],
+ updatedIds = [],
+ me = this,
+ fieldId = me.fieldId;
- // apply new values
- var newStart = this.start + diff * moveFactor;
- var newEnd = this.end + diff * moveFactor;
+ var addOrUpdate = function (item) {
+ var id = item[fieldId];
+ if (me.data[id]) {
+ // update item
+ id = me._updateItem(item);
+ updatedIds.push(id);
+ }
+ else {
+ // add new item
+ id = me._addItem(item);
+ addedIds.push(id);
+ }
+ };
- // TODO: reckon with min and max range
+ if (data instanceof Array) {
+ // Array
+ for (var i = 0, len = data.length; i < len; i++) {
+ addOrUpdate(data[i]);
+ }
+ }
+ else if (util.isDataTable(data)) {
+ // Google DataTable
+ var columns = this._getColumnNames(data);
+ for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
+ var item = {};
+ for (var col = 0, cols = columns.length; col < cols; col++) {
+ var field = columns[col];
+ item[field] = data.getValue(row, col);
+ }
- this.start = newStart;
- this.end = newEnd;
+ addOrUpdate(item);
+ }
+ }
+ else if (data instanceof Object) {
+ // Single item
+ addOrUpdate(data);
+ }
+ else {
+ throw new Error('Unknown dataType');
+ }
+
+ if (addedIds.length) {
+ this._trigger('add', {items: addedIds}, senderId);
+ }
+ if (updatedIds.length) {
+ this._trigger('update', {items: updatedIds}, senderId);
+ }
+
+ return addedIds.concat(updatedIds);
};
/**
- * @constructor Controller
+ * Get a data item or multiple items.
*
- * A Controller controls the reflows and repaints of all visual components
+ * Usage:
+ *
+ * get()
+ * get(options: Object)
+ * get(options: Object, data: Array | DataTable)
+ *
+ * get(id: Number | String)
+ * get(id: Number | String, options: Object)
+ * get(id: Number | String, options: Object, data: Array | DataTable)
+ *
+ * get(ids: Number[] | String[])
+ * get(ids: Number[] | String[], options: Object)
+ * get(ids: Number[] | String[], options: Object, data: Array | DataTable)
+ *
+ * Where:
+ *
+ * {Number | String} id The id of an item
+ * {Number[] | String{}} ids An array with ids of items
+ * {Object} options An Object with options. Available options:
+ * {String} [type] Type of data to be returned. Can
+ * be 'DataTable' or 'Array' (default)
+ * {Object.} [convert]
+ * {String[]} [fields] field names to be returned
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ * {Array | DataTable} [data] If provided, items will be appended to this
+ * array or table. Required in case of Google
+ * DataTable.
+ *
+ * @throws Error
*/
-function Controller () {
- this.id = util.randomUUID();
- this.components = {};
+DataSet.prototype.get = function (args) {
+ var me = this;
- this.repaintTimer = undefined;
- this.reflowTimer = undefined;
-}
+ // parse the arguments
+ var id, ids, options, data;
+ var firstType = util.getType(arguments[0]);
+ if (firstType == 'String' || firstType == 'Number') {
+ // get(id [, options] [, data])
+ id = arguments[0];
+ options = arguments[1];
+ data = arguments[2];
+ }
+ else if (firstType == 'Array') {
+ // get(ids [, options] [, data])
+ ids = arguments[0];
+ options = arguments[1];
+ data = arguments[2];
+ }
+ else {
+ // get([, options] [, data])
+ options = arguments[0];
+ data = arguments[1];
+ }
-/**
- * Add a component to the controller
- * @param {Component} component
- */
-Controller.prototype.add = function add(component) {
- // validate the component
- if (component.id == undefined) {
- throw new Error('Component has no field id');
+ // 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"');
+ }
}
- if (!(component instanceof Component) && !(component instanceof Controller)) {
- throw new TypeError('Component must be an instance of ' +
- 'prototype Component or Controller');
+ else if (data) {
+ type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
+ }
+ else {
+ type = 'Array';
}
- // add the component
- component.controller = this;
- this.components[component.id] = component;
-};
+ // build options
+ var convert = options && options.convert || this.options.convert;
+ var filter = options && options.filter;
+ var items = [], item, itemId, i, len;
-/**
- * Remove a component from the controller
- * @param {Component | String} component
- */
-Controller.prototype.remove = function remove(component) {
- var id;
- for (id in this.components) {
- if (this.components.hasOwnProperty(id)) {
- if (id == component || this.components[id] == component) {
- break;
+ // convert items
+ if (id != undefined) {
+ // return a single item
+ item = me._getItem(id, convert);
+ if (filter && !filter(item)) {
+ item = null;
+ }
+ }
+ else if (ids != undefined) {
+ // return a subset of items
+ for (i = 0, len = ids.length; i < len; i++) {
+ item = me._getItem(ids[i], convert);
+ if (!filter || filter(item)) {
+ items.push(item);
}
}
}
-
- if (id) {
- delete this.components[id];
+ else {
+ // return all items
+ for (itemId in this.data) {
+ if (this.data.hasOwnProperty(itemId)) {
+ item = me._getItem(itemId, convert);
+ if (!filter || filter(item)) {
+ items.push(item);
+ }
+ }
+ }
}
-};
-/**
- * Request a reflow. The controller will schedule a reflow
- * @param {Boolean} [force] If true, an immediate reflow is forced. Default
- * is false.
- */
-Controller.prototype.requestReflow = function requestReflow(force) {
- if (force) {
- this.reflow();
+ // order the results
+ if (options && options.order && id == undefined) {
+ this._sort(items, options.order);
}
- else {
- if (!this.reflowTimer) {
- var me = this;
- this.reflowTimer = setTimeout(function () {
- me.reflowTimer = undefined;
- me.reflow();
- }, 0);
+
+ // filter fields of the items
+ if (options && options.fields) {
+ var fields = options.fields;
+ if (id != undefined) {
+ item = this._filterFields(item, fields);
+ }
+ else {
+ for (i = 0, len = items.length; i < len; i++) {
+ items[i] = this._filterFields(items[i], fields);
+ }
}
}
-};
-/**
- * Request a repaint. The controller will schedule a repaint
- * @param {Boolean} [force] If true, an immediate repaint is forced. Default
- * is false.
- */
-Controller.prototype.requestRepaint = function requestRepaint(force) {
- if (force) {
- this.repaint();
+ // return the results
+ if (type == 'DataTable') {
+ var columns = this._getColumnNames(data);
+ if (id != undefined) {
+ // append a single item to the data table
+ me._appendRow(data, columns, item);
+ }
+ else {
+ // copy the items to the provided data table
+ for (i = 0, len = items.length; i < len; i++) {
+ me._appendRow(data, columns, items[i]);
+ }
+ }
+ return data;
}
else {
- if (!this.repaintTimer) {
- var me = this;
- this.repaintTimer = setTimeout(function () {
- me.repaintTimer = undefined;
- me.repaint();
- }, 0);
+ // return an array
+ if (id != undefined) {
+ // a single item
+ return item;
+ }
+ else {
+ // multiple items
+ if (data) {
+ // copy the items to the provided array
+ for (i = 0, len = items.length; i < len; i++) {
+ data.push(items[i]);
+ }
+ return data;
+ }
+ else {
+ // just return our array
+ return items;
+ }
}
}
};
/**
- * Repaint all components
+ * Get ids of all items or from a filtered set of items.
+ * @param {Object} [options] An Object with options. Available options:
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ * @return {Array} ids
*/
-Controller.prototype.repaint = function repaint() {
- var changed = false;
+DataSet.prototype.getIds = function (options) {
+ var data = this.data,
+ filter = options && options.filter,
+ order = options && options.order,
+ convert = options && options.convert || this.options.convert,
+ i,
+ len,
+ id,
+ item,
+ items,
+ ids = [];
- // cancel any running repaint request
- if (this.repaintTimer) {
- clearTimeout(this.repaintTimer);
- this.repaintTimer = undefined;
+ if (filter) {
+ // get filtered items
+ if (order) {
+ // create ordered list
+ items = [];
+ for (id in data) {
+ if (data.hasOwnProperty(id)) {
+ item = this._getItem(id, convert);
+ if (filter(item)) {
+ items.push(item);
+ }
+ }
+ }
+
+ this._sort(items, order);
+
+ for (i = 0, len = items.length; i < len; i++) {
+ ids[i] = items[i][this.fieldId];
+ }
+ }
+ else {
+ // create unordered list
+ for (id in data) {
+ if (data.hasOwnProperty(id)) {
+ item = this._getItem(id, convert);
+ if (filter(item)) {
+ ids.push(item[this.fieldId]);
+ }
+ }
+ }
+ }
}
+ else {
+ // get all items
+ if (order) {
+ // create an ordered list
+ items = [];
+ for (id in data) {
+ if (data.hasOwnProperty(id)) {
+ items.push(data[id]);
+ }
+ }
- var done = {};
+ this._sort(items, order);
- 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);
- });
+ for (i = 0, len = items.length; i < len; i++) {
+ ids[i] = items[i][this.fieldId];
}
- if (component.parent) {
- repaint(component.parent, component.parent.id);
+ }
+ else {
+ // create unordered list
+ for (id in data) {
+ if (data.hasOwnProperty(id)) {
+ item = data[id];
+ ids.push(item[this.fieldId]);
+ }
}
+ }
+ }
- // repaint the component itself and mark as done
- changed = component.repaint() || changed;
- done[id] = true;
+ return ids;
+};
+
+/**
+ * Execute a callback function for every item in the dataset.
+ * The order of the items is not determined.
+ * @param {function} callback
+ * @param {Object} [options] Available options:
+ * {Object.} [convert]
+ * {String[]} [fields] filter fields
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ */
+DataSet.prototype.forEach = function (callback, options) {
+ var filter = options && options.filter,
+ convert = options && options.convert || this.options.convert,
+ data = this.data,
+ item,
+ id;
+
+ if (options && options.order) {
+ // execute forEach on ordered list
+ var items = this.get(options);
+
+ for (var i = 0, len = items.length; i < len; i++) {
+ item = items[i];
+ id = item[this.fieldId];
+ callback(item, id);
}
}
-
- util.forEach(this.components, repaint);
-
- // immediately reflow when needed
- if (changed) {
- this.reflow();
+ else {
+ // unordered
+ for (id in data) {
+ if (data.hasOwnProperty(id)) {
+ item = this._getItem(id, convert);
+ if (!filter || filter(item)) {
+ callback(item, id);
+ }
+ }
+ }
}
- // TODO: limit the number of nested reflows/repaints, prevent loop
};
/**
- * Reflow all components
+ * Map every item in the dataset.
+ * @param {function} callback
+ * @param {Object} [options] Available options:
+ * {Object.} [convert]
+ * {String[]} [fields] filter fields
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ * @return {Object[]} mappedItems
*/
-Controller.prototype.reflow = function reflow() {
- var resized = false;
-
- // cancel any running repaint request
- if (this.reflowTimer) {
- clearTimeout(this.reflowTimer);
- this.reflowTimer = undefined;
- }
-
- var done = {};
+DataSet.prototype.map = function (callback, options) {
+ var filter = options && options.filter,
+ convert = options && options.convert || this.options.convert,
+ mappedItems = [],
+ data = this.data,
+ item;
- 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);
+ // convert and filter items
+ for (var id in data) {
+ if (data.hasOwnProperty(id)) {
+ item = this._getItem(id, convert);
+ if (!filter || filter(item)) {
+ mappedItems.push(callback(item, id));
}
-
- // 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();
+ // order items
+ if (options && options.order) {
+ this._sort(mappedItems, options.order);
}
- // 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;
-}
+ return mappedItems;
+};
/**
- * Set parameters for the frame. Parameters will be merged in current parameter
- * set.
- * @param {Object} options Available parameters:
- * {String | function} [className]
- * {EventBus} [eventBus]
- * {String | Number | function} [left]
- * {String | Number | function} [top]
- * {String | Number | function} [width]
- * {String | Number | function} [height]
+ * Filter the fields of an item
+ * @param {Object} item
+ * @param {String[]} fields Field names
+ * @return {Object} filteredItem
+ * @private
*/
-Component.prototype.setOptions = function setOptions(options) {
- if (options) {
- util.extend(this.options, options);
+DataSet.prototype._filterFields = function (item, fields) {
+ var filteredItem = {};
- if (this.controller) {
- this.requestRepaint();
- this.requestReflow();
+ for (var field in item) {
+ if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
+ filteredItem[field] = item[field];
}
}
+
+ return filteredItem;
};
/**
- * Get an option value by name
- * The function will first check this.options object, and else will check
- * this.defaultOptions.
- * @param {String} name
- * @return {*} value
+ * Sort the provided array with items
+ * @param {Object[]} items
+ * @param {String | function} order A field name or custom sort function.
+ * @private
*/
-Component.prototype.getOption = function getOption(name) {
- var value;
- if (this.options) {
- value = this.options[name];
+DataSet.prototype._sort = function (items, order) {
+ if (util.isString(order)) {
+ // order by provided field name
+ var name = order; // field name
+ items.sort(function (a, b) {
+ var av = a[name];
+ var bv = b[name];
+ return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
+ });
}
- if (value === undefined && this.defaultOptions) {
- value = this.defaultOptions[name];
+ else if (typeof order === 'function') {
+ // order by sort function
+ items.sort(order);
+ }
+ // TODO: extend order by an Object {field:String, direction:String}
+ // where direction can be 'asc' or 'desc'
+ else {
+ throw new TypeError('Order must be a function or a string');
}
- return value;
};
/**
- * Get the container element of the component, which can be used by a child to
- * add its own widgets. Not all components do have a container for childs, in
- * that case null is returned.
- * @returns {HTMLElement | null} container
+ * Remove an object by pointer or by id
+ * @param {String | Number | Object | Array} id Object or id, or an array with
+ * objects or ids to be removed
+ * @param {String} [senderId] Optional sender id
+ * @return {Array} removedIds
*/
-// TODO: get rid of the getContainer and getFrame methods, provide these via the options
-Component.prototype.getContainer = function getContainer() {
- // should be implemented by the component
- return null;
+DataSet.prototype.remove = function (id, senderId) {
+ var removedIds = [],
+ i, len, removedId;
+
+ if (id instanceof Array) {
+ for (i = 0, len = id.length; i < len; i++) {
+ removedId = this._remove(id[i]);
+ if (removedId != null) {
+ removedIds.push(removedId);
+ }
+ }
+ }
+ else {
+ removedId = this._remove(id);
+ if (removedId != null) {
+ removedIds.push(removedId);
+ }
+ }
+
+ if (removedIds.length) {
+ this._trigger('remove', {items: removedIds}, senderId);
+ }
+
+ return removedIds;
};
/**
- * Get the frame element of the component, the outer HTML DOM element.
- * @returns {HTMLElement | null} frame
+ * Remove an item by its id
+ * @param {Number | String | Object} id id or item
+ * @returns {Number | String | null} id
+ * @private
*/
-Component.prototype.getFrame = function getFrame() {
- return this.frame;
+DataSet.prototype._remove = function (id) {
+ if (util.isNumber(id) || util.isString(id)) {
+ if (this.data[id]) {
+ delete this.data[id];
+ delete this.internalIds[id];
+ return id;
+ }
+ }
+ else if (id instanceof Object) {
+ var itemId = id[this.fieldId];
+ if (itemId && this.data[itemId]) {
+ delete this.data[itemId];
+ delete this.internalIds[itemId];
+ return itemId;
+ }
+ }
+ return null;
};
/**
- * Repaint the component
- * @return {Boolean} changed
+ * Clear the data
+ * @param {String} [senderId] Optional sender id
+ * @return {Array} removedIds The ids of all removed items
*/
-Component.prototype.repaint = function repaint() {
- // should be implemented by the component
- return false;
+DataSet.prototype.clear = function (senderId) {
+ var ids = Object.keys(this.data);
+
+ this.data = {};
+ this.internalIds = {};
+
+ this._trigger('remove', {items: ids}, senderId);
+
+ return ids;
};
/**
- * Reflow the component
- * @return {Boolean} resized
+ * Find the item with maximum value of a specified field
+ * @param {String} field
+ * @return {Object | null} item Item containing max value, or null if no items
*/
-Component.prototype.reflow = function reflow() {
- // should be implemented by the component
- return false;
+DataSet.prototype.max = function (field) {
+ var data = this.data,
+ max = null,
+ maxField = null;
+
+ for (var id in data) {
+ if (data.hasOwnProperty(id)) {
+ var item = data[id];
+ var itemField = item[field];
+ if (itemField != null && (!max || itemField > maxField)) {
+ max = item;
+ maxField = itemField;
+ }
+ }
+ }
+
+ return max;
};
/**
- * Hide the component from the DOM
- * @return {Boolean} changed
+ * Find the item with minimum value of a specified field
+ * @param {String} field
+ * @return {Object | null} item Item containing max value, or null if no items
*/
-Component.prototype.hide = function hide() {
- if (this.frame && this.frame.parentNode) {
- this.frame.parentNode.removeChild(this.frame);
- return true;
- }
- else {
- return false;
+DataSet.prototype.min = function (field) {
+ var data = this.data,
+ min = null,
+ minField = null;
+
+ for (var id in data) {
+ if (data.hasOwnProperty(id)) {
+ var item = data[id];
+ var itemField = item[field];
+ if (itemField != null && (!min || itemField < minField)) {
+ min = item;
+ minField = itemField;
+ }
+ }
}
+
+ return min;
};
/**
- * Show the component in the DOM (when not already visible).
- * A repaint will be executed when the component is not visible
- * @return {Boolean} changed
+ * Find all distinct values of a specified field
+ * @param {String} field
+ * @return {Array} values Array containing all distinct values. If the data
+ * items do not contain the specified field, an array
+ * containing a single value undefined is returned.
+ * The returned array is unordered.
*/
-Component.prototype.show = function show() {
- if (!this.frame || !this.frame.parentNode) {
- return this.repaint();
- }
- else {
- return false;
+DataSet.prototype.distinct = function (field) {
+ var data = this.data,
+ values = [],
+ fieldType = this.options.convert[field],
+ count = 0;
+
+ for (var prop in data) {
+ if (data.hasOwnProperty(prop)) {
+ var item = data[prop];
+ var value = util.convert(item[field], fieldType);
+ var exists = false;
+ for (var i = 0; i < count; i++) {
+ if (values[i] == value) {
+ exists = true;
+ break;
+ }
+ }
+ if (!exists) {
+ values[count] = value;
+ count++;
+ }
+ }
}
+
+ return values;
};
/**
- * Request a repaint. The controller will schedule a repaint
+ * Add a single item. Will fail when an item with the same id already exists.
+ * @param {Object} item
+ * @return {String} id
+ * @private
*/
-Component.prototype.requestRepaint = function requestRepaint() {
- if (this.controller) {
- this.controller.requestRepaint();
+DataSet.prototype._addItem = function (item) {
+ var id = item[this.fieldId];
+
+ if (id != undefined) {
+ // check whether this id is already taken
+ if (this.data[id]) {
+ // item already exists
+ throw new Error('Cannot add item: item with id ' + id + ' already exists');
+ }
}
else {
- throw new Error('Cannot request a repaint: no controller configured');
- // TODO: just do a repaint when no parent is configured?
+ // generate an id
+ id = util.randomUUID();
+ item[this.fieldId] = id;
+ this.internalIds[id] = item;
+ }
+
+ var d = {};
+ for (var field in item) {
+ if (item.hasOwnProperty(field)) {
+ var fieldType = this.convert[field]; // type may be undefined
+ d[field] = util.convert(item[field], fieldType);
+ }
}
+ this.data[id] = d;
+
+ return id;
};
/**
- * Request a reflow. The controller will schedule a reflow
+ * Get an item. Fields can be converted to a specific type
+ * @param {String} id
+ * @param {Object.} [convert] field types to convert
+ * @return {Object | null} item
+ * @private
*/
-Component.prototype.requestReflow = function requestReflow() {
- if (this.controller) {
- this.controller.requestReflow();
+DataSet.prototype._getItem = function (id, convert) {
+ var field, value;
+
+ // get the item from the dataset
+ var raw = this.data[id];
+ if (!raw) {
+ return null;
+ }
+
+ // convert the items field types
+ var converted = {},
+ fieldId = this.fieldId,
+ internalIds = this.internalIds;
+ if (convert) {
+ for (field in raw) {
+ if (raw.hasOwnProperty(field)) {
+ value = raw[field];
+ // output all fields, except internal ids
+ if ((field != fieldId) || !(value in internalIds)) {
+ converted[field] = util.convert(value, convert[field]);
+ }
+ }
+ }
}
else {
- throw new Error('Cannot request a reflow: no controller configured');
- // TODO: just do a reflow when no parent is configured?
+ // no field types specified, no converting needed
+ for (field in raw) {
+ if (raw.hasOwnProperty(field)) {
+ value = raw[field];
+ // output all fields, except internal ids
+ if ((field != fieldId) || !(value in internalIds)) {
+ converted[field] = value;
+ }
+ }
+ }
}
+
+ return converted;
};
/**
- * 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
+ * Update a single item: merge with existing item.
+ * Will fail when the item has no id, or when there does not exist an item
+ * with the same id.
+ * @param {Object} item
+ * @return {String} id
+ * @private
*/
-function Panel(parent, depends, options) {
- this.id = util.randomUUID();
- this.parent = parent;
- this.depends = depends;
-
- this.options = options || {};
-}
-
-Panel.prototype = new Component();
+DataSet.prototype._updateItem = function (item) {
+ var id = item[this.fieldId];
+ if (id == undefined) {
+ throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
+ }
+ var d = this.data[id];
+ if (!d) {
+ // item doesn't exist
+ throw new Error('Cannot update item: no item with id ' + id + ' found');
+ }
-/**
- * Set options. Will extend the current options.
- * @param {Object} [options] Available parameters:
- * {String | function} [className]
- * {String | Number | function} [left]
- * {String | Number | function} [top]
- * {String | Number | function} [width]
- * {String | Number | function} [height]
- */
-Panel.prototype.setOptions = Component.prototype.setOptions;
+ // merge with current item
+ for (var field in item) {
+ if (item.hasOwnProperty(field)) {
+ var fieldType = this.convert[field]; // type may be undefined
+ d[field] = util.convert(item[field], fieldType);
+ }
+ }
-/**
- * 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;
+ return id;
};
/**
- * Repaint the component
- * @return {Boolean} changed
+ * Get an array with the column names of a Google DataTable
+ * @param {DataTable} dataTable
+ * @return {String[]} columnNames
+ * @private
*/
-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';
-
- var className = options.className;
- if (className) {
- if (typeof className == 'function') {
- util.addClassName(frame, String(className()));
- }
- else {
- util.addClassName(frame, String(className));
- }
- }
-
- this.frame = frame;
- changed += 1;
- }
- if (!frame.parentNode) {
- if (!this.parent) {
- throw new Error('Cannot repaint panel: no parent attached');
- }
- var parentContainer = this.parent.getContainer();
- if (!parentContainer) {
- throw new Error('Cannot repaint panel: parent has no container element');
- }
- parentContainer.appendChild(frame);
- changed += 1;
+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);
}
-
- 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);
+ return columns;
};
/**
- * Reflow the component
- * @return {Boolean} resized
+ * Append an item as a row to the dataTable
+ * @param dataTable
+ * @param columns
+ * @param item
+ * @private
*/
-Panel.prototype.reflow = function () {
- var changed = 0,
- update = util.updateProperty,
- frame = this.frame;
+DataSet.prototype._appendRow = function (dataTable, columns, item) {
+ var row = dataTable.addRow();
- 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;
+ for (var col = 0, cols = columns.length; col < cols; col++) {
+ var field = columns[col];
+ dataTable.setValue(row, col, item[field]);
}
-
- 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
+ * DataView
+ *
+ * a dataview offers a filtered view on a dataset or an other dataview.
+ *
+ * @param {DataSet | DataView} data
+ * @param {Object} [options] Available options: see method get
+ *
+ * @constructor DataView
*/
-function RootPanel(container, options) {
+function DataView (data, options) {
this.id = util.randomUUID();
- this.container = container;
+ this.data = null;
+ this.ids = {}; // ids of the items currently in memory (just contains a boolean true)
this.options = options || {};
- this.defaultOptions = {
- autoResize: true
+ this.fieldId = 'id'; // name of the field containing id
+ this.subscribers = {}; // event subscribers
+
+ var me = this;
+ this.listener = function () {
+ me._onEvent.apply(me, arguments);
};
- this.listeners = {}; // event listeners
+ this.setData(data);
}
-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]
- * {Boolean | function} [autoResize]
- */
-RootPanel.prototype.setOptions = Component.prototype.setOptions;
-
/**
- * Repaint the component
- * @return {Boolean} changed
+ * Set a data source for the view
+ * @param {DataSet | DataView} data
*/
-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 = 'vis timeline rootpanel';
+DataView.prototype.setData = function (data) {
+ var ids, dataItems, i, len;
- var className = options.className;
- if (className) {
- util.addClassName(frame, util.option.asString(className));
+ if (this.data) {
+ // unsubscribe from current dataset
+ if (this.data.unsubscribe) {
+ this.data.unsubscribe('*', this.listener);
}
- this.frame = frame;
-
- changed += 1;
- }
- if (!frame.parentNode) {
- if (!this.container) {
- throw new Error('Cannot repaint root panel: no container attached');
+ // trigger a remove of all items in memory
+ ids = [];
+ for (var id in this.ids) {
+ if (this.ids.hasOwnProperty(id)) {
+ ids.push(id);
+ }
}
- this.container.appendChild(frame);
- changed += 1;
+ this.ids = {};
+ this._trigger('remove', {items: ids});
}
- 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.data = data;
- this._updateEventEmitters();
- this._updateWatch();
+ if (this.data) {
+ // update fieldId
+ this.fieldId = this.options.fieldId ||
+ (this.data && this.data.options && this.data.options.fieldId) ||
+ 'id';
- return (changed > 0);
+ // trigger an add of all added items
+ ids = this.data.getIds({filter: this.options && this.options.filter});
+ for (i = 0, len = ids.length; i < len; i++) {
+ id = ids[i];
+ this.ids[id] = true;
+ }
+ this._trigger('add', {items: ids});
+
+ // subscribe to new dataset
+ if (this.data.subscribe) {
+ this.data.subscribe('*', this.listener);
+ }
+ }
};
/**
- * Reflow the component
- * @return {Boolean} resized
+ * Get data from the data view
+ *
+ * Usage:
+ *
+ * get()
+ * get(options: Object)
+ * get(options: Object, data: Array | DataTable)
+ *
+ * get(id: Number)
+ * get(id: Number, options: Object)
+ * get(id: Number, options: Object, data: Array | DataTable)
+ *
+ * get(ids: Number[])
+ * get(ids: Number[], options: Object)
+ * get(ids: Number[], options: Object, data: Array | DataTable)
+ *
+ * Where:
+ *
+ * {Number | String} id The id of an item
+ * {Number[] | String{}} ids An array with ids of items
+ * {Object} options An Object with options. Available options:
+ * {String} [type] Type of data to be returned. Can
+ * be 'DataTable' or 'Array' (default)
+ * {Object.} [convert]
+ * {String[]} [fields] field names to be returned
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ * {Array | DataTable} [data] If provided, items will be appended to this
+ * array or table. Required in case of Google
+ * DataTable.
+ * @param args
*/
-RootPanel.prototype.reflow = function () {
- var changed = 0,
- update = util.updateProperty,
- frame = this.frame;
+DataView.prototype.get = function (args) {
+ var me = this;
- 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);
+ // parse the arguments
+ var ids, options, data;
+ var firstType = util.getType(arguments[0]);
+ if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
+ // get(id(s) [, options] [, data])
+ ids = arguments[0]; // can be a single id or an array with ids
+ options = arguments[1];
+ data = arguments[2];
}
else {
- changed += 1;
+ // get([, options] [, data])
+ options = arguments[0];
+ data = arguments[1];
}
- return (changed > 0);
+ // extend the options with the default options and provided options
+ var viewOptions = util.extend({}, this.options, options);
+
+ // create a combined filter method when needed
+ if (this.options.filter && options && options.filter) {
+ viewOptions.filter = function (item) {
+ return me.options.filter(item) && options.filter(item);
+ }
+ }
+
+ // build up the call to the linked data set
+ var getArguments = [];
+ if (ids != undefined) {
+ getArguments.push(ids);
+ }
+ getArguments.push(viewOptions);
+ getArguments.push(data);
+
+ return this.data && this.data.get.apply(this.data, getArguments);
};
/**
- * Update watching for resize, depending on the current option
- * @private
+ * Get ids of all items or from a filtered set of items.
+ * @param {Object} [options] An Object with options. Available options:
+ * {function} [filter] filter items
+ * {String | function} [order] Order the items by
+ * a field name or custom sort function.
+ * @return {Array} ids
*/
-RootPanel.prototype._updateWatch = function () {
- var autoResize = this.getOption('autoResize');
- if (autoResize) {
- this._watch();
+DataView.prototype.getIds = function (options) {
+ var ids;
+
+ if (this.data) {
+ var defaultFilter = this.options.filter;
+ var filter;
+
+ if (options && options.filter) {
+ if (defaultFilter) {
+ filter = function (item) {
+ return defaultFilter(item) && options.filter(item);
+ }
+ }
+ else {
+ filter = options.filter;
+ }
+ }
+ else {
+ filter = defaultFilter;
+ }
+
+ ids = this.data.getIds({
+ filter: filter,
+ order: options && options.order
+ });
}
else {
- this._unwatch();
+ ids = [];
}
+
+ return ids;
};
/**
- * Watch for changes in the size of the frame. On resize, the Panel will
- * automatically redraw itself.
+ * Event listener. Will propagate all events from the connected data set to
+ * the subscribers of the DataView, but will filter the items and only trigger
+ * when there are changes in the filtered data set.
+ * @param {String} event
+ * @param {Object | null} params
+ * @param {String} senderId
* @private
*/
-RootPanel.prototype._watch = function () {
- var me = this;
+DataView.prototype._onEvent = function (event, params, senderId) {
+ var i, len, id, item,
+ ids = params && params.items,
+ data = this.data,
+ added = [],
+ updated = [],
+ removed = [];
- this._unwatch();
+ if (ids && data) {
+ switch (event) {
+ case 'add':
+ // filter the ids of the added items
+ for (i = 0, len = ids.length; i < len; i++) {
+ id = ids[i];
+ item = this.get(id);
+ if (item) {
+ this.ids[id] = true;
+ added.push(id);
+ }
+ }
- var checkSize = function () {
- var autoResize = me.getOption('autoResize');
- if (!autoResize) {
- // stop watching when the option autoResize is changed to false
- me._unwatch();
- return;
- }
+ break;
- if (me.frame) {
- // check whether the frame is resized
- if ((me.frame.clientWidth != me.width) ||
- (me.frame.clientHeight != me.height)) {
- me.requestReflow();
- }
- }
- };
+ case 'update':
+ // determine the event from the views viewpoint: an updated
+ // item can be added, updated, or removed from this view.
+ for (i = 0, len = ids.length; i < len; i++) {
+ id = ids[i];
+ item = this.get(id);
- // TODO: automatically cleanup the event listener when the frame is deleted
- util.addEventListener(window, 'resize', checkSize);
+ if (item) {
+ if (this.ids[id]) {
+ updated.push(id);
+ }
+ else {
+ this.ids[id] = true;
+ added.push(id);
+ }
+ }
+ else {
+ if (this.ids[id]) {
+ delete this.ids[id];
+ removed.push(id);
+ }
+ else {
+ // nothing interesting for me :-(
+ }
+ }
+ }
- this.watchTimer = setInterval(checkSize, 1000);
-};
+ break;
-/**
- * Stop watching for a resize of the frame.
- * @private
- */
-RootPanel.prototype._unwatch = function () {
- if (this.watchTimer) {
- clearInterval(this.watchTimer);
- this.watchTimer = undefined;
- }
+ case 'remove':
+ // filter the ids of the removed items
+ for (i = 0, len = ids.length; i < len; i++) {
+ id = ids[i];
+ if (this.ids[id]) {
+ delete this.ids[id];
+ removed.push(id);
+ }
+ }
- // TODO: remove event listener on window.resize
-};
+ break;
+ }
-/**
- * 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;
+ if (added.length) {
+ this._trigger('add', {items: added}, senderId);
+ }
+ if (updated.length) {
+ this._trigger('update', {items: updated}, senderId);
+ }
+ if (removed.length) {
+ this._trigger('remove', {items: removed}, senderId);
+ }
}
- arr.push(callback);
-
- this._updateEventEmitters();
};
+// copy subscription functionality from DataSet
+DataView.prototype.subscribe = DataSet.prototype.subscribe;
+DataView.prototype.unsubscribe = DataSet.prototype.unsubscribe;
+DataView.prototype._trigger = DataSet.prototype._trigger;
+
/**
- * Update the event listeners for all event emitters
- * @private
+ * @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
*/
-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);
- }
- }
- });
+TimeStep = function(start, end, minimumStep) {
+ // variables
+ this.current = new Date();
+ this._start = new Date();
+ this._end = new Date();
- // TODO: be able to delete event listeners
- // TODO: be able to move event listeners to a parent when available
- }
-};
+ this.autoScale = true;
+ this.scale = TimeStep.SCALE.DAY;
+ this.step = 1;
-/**
- * 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;
+ // initialize the range
+ this.setRange(start, end, minimumStep);
+};
- this.dom = {
- majorLines: [],
- majorTexts: [],
- minorLines: [],
- minorTexts: [],
- redundant: {
- majorLines: [],
- majorTexts: [],
- minorLines: [],
- minorTexts: []
- }
- };
- this.props = {
- range: {
- start: 0,
- end: 0,
- minimumStep: 0
- },
- lineTop: 0
- };
+/// enum scale
+TimeStep.SCALE = {
+ MILLISECOND: 1,
+ SECOND: 2,
+ MINUTE: 3,
+ HOUR: 4,
+ DAY: 5,
+ WEEKDAY: 6,
+ MONTH: 7,
+ YEAR: 8
+};
- this.options = options || {};
- this.defaultOptions = {
- orientation: 'bottom', // supported: 'top', 'bottom'
- // TODO: implement timeaxis orientations 'left' and 'right'
- showMinorLabels: true,
- showMajorLabels: true
- };
- this.conversion = null;
- this.range = null;
-}
+/**
+ * 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;
+ }
-TimeAxis.prototype = new Component();
+ this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
+ this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
-// TODO: comment options
-TimeAxis.prototype.setOptions = Component.prototype.setOptions;
+ if (this.autoScale) {
+ this.setMinimumStep(minimumStep);
+ }
+};
/**
- * Set a range (start and end)
- * @param {Range | Object} range A Range or an object containing start and end.
+ * Set the range iterator to the start date.
*/
-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;
+TimeStep.prototype.first = function() {
+ this.current = new Date(this._start.valueOf());
+ this.roundToMinor();
};
/**
- * 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
+ * Round the current date to the first minor date value
+ * This must be executed once when the current date is set to start Date
*/
-TimeAxis.prototype.toTime = function(x) {
- var conversion = this.conversion;
- return new Date(x / conversion.factor + conversion.offset);
+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;
+ }
+ }
};
/**
- * 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
+ * Check if the there is a next step
+ * @return {boolean} true if the current date has not passed the end date
*/
-TimeAxis.prototype.toScreen = function(time) {
- var conversion = this.conversion;
- return (time.valueOf() - conversion.offset) * conversion.factor;
+TimeStep.prototype.hasNext = function () {
+ return (this.current.valueOf() <= this._end.valueOf());
};
/**
- * Repaint the component
- * @return {Boolean} changed
+ * Do the next step
*/
-TimeAxis.prototype.repaint = function () {
- var changed = 0,
- update = util.updateProperty,
- asSize = util.option.asSize,
- options = this.options,
- orientation = this.getOption('orientation'),
- props = this.props,
- step = this.step;
+TimeStep.prototype.next = function() {
+ var prev = this.current.valueOf();
- var frame = this.frame;
- if (!frame) {
- frame = document.createElement('div');
- this.frame = frame;
- changed += 1;
- }
- frame.className = 'axis ' + orientation;
- // TODO: custom className?
+ // 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:
- if (!frame.parentNode) {
- if (!this.parent) {
- throw new Error('Cannot repaint time axis: no parent attached');
+ 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;
}
- var parentContainer = this.parent.getContainer();
- if (!parentContainer) {
- throw new Error('Cannot repaint time axis: parent has no container element');
+ }
+ 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;
}
- 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 defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ?
- (this.props.parentHeight - this.height) + 'px' :
- '0px';
- changed += update(frame.style, 'top', asSize(options.top, defaultTop));
- changed += update(frame.style, 'left', asSize(options.left, '0px'));
- changed += update(frame.style, 'width', asSize(options.width, '100%'));
- changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
-
- // get characters width and height
- this._repaintMeasureChars();
-
- if (this.step) {
- this._repaintStart();
-
- step.first();
- var xFirstMajorLabel = undefined;
- var max = 0;
- while (step.hasNext() && max < 1000) {
- max++;
- var cur = step.getCurrent(),
- x = this.toScreen(cur),
- isMajor = step.isMajor();
-
- // TODO: lines must have a width, such that we can create css backgrounds
-
- if (this.getOption('showMinorLabels')) {
- this._repaintMinorText(x, step.getLabelMinor());
- }
-
- if (isMajor && this.getOption('showMajorLabels')) {
- if (x > 0) {
- if (xFirstMajorLabel == undefined) {
- xFirstMajorLabel = x;
- }
- this._repaintMajorText(x, step.getLabelMajor());
- }
- this._repaintMajorLine(x);
- }
- else {
- this._repaintMinorLine(x);
- }
-
- step.next();
- }
-
- // create a major label on the left when needed
- if (this.getOption('showMajorLabels')) {
- var leftTime = this.toTime(0),
- leftText = step.getLabelMajor(leftTime),
- widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation
-
- if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
- this._repaintMajorText(0, leftText);
- }
- }
-
- this._repaintEnd();
+ 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;
}
+ }
- this._repaintLine();
-
- // put frame online again
- if (beforeChild) {
- parent.insertBefore(frame, beforeChild);
- }
- else {
- parent.appendChild(frame)
- }
+ // safety mechanism: if current time is still unchanged, move to the end
+ if (this.current.valueOf() == prev) {
+ this.current = new Date(this._end.valueOf());
}
+};
- return (changed > 0);
+
+/**
+ * Get the current datetime
+ * @return {Date} current The current date
+ */
+TimeStep.prototype.getCurrent = function() {
+ return this.current;
};
/**
- * 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
+ * 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.
*/
-TimeAxis.prototype._repaintStart = function () {
- var dom = this.dom,
- redundant = dom.redundant;
+TimeStep.prototype.setScale = function(newScale, newStep) {
+ this.scale = newScale;
- redundant.majorLines = dom.majorLines;
- redundant.majorTexts = dom.majorTexts;
- redundant.minorLines = dom.minorLines;
- redundant.minorTexts = dom.minorTexts;
+ if (newStep > 0) {
+ this.step = newStep;
+ }
- dom.majorLines = [];
- dom.majorTexts = [];
- dom.minorLines = [];
- dom.minorTexts = [];
+ this.autoScale = false;
};
/**
- * End a repaint. Cleanup leftover DOM elements in the redundant list
- * @private
+ * Enable or disable autoscaling
+ * @param {boolean} enable If true, autoascaling is set true
*/
-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);
- }
- }
- });
+TimeStep.prototype.setAutoScale = function (enable) {
+ this.autoScale = enable;
};
/**
- * Create a minor label for the axis at position x
- * @param {Number} x
- * @param {String} text
- * @private
+ * Automatically determine the scale that bests fits the provided minimum step
+ * @param {Number} [minimumStep] The minimum step size in milliseconds
*/
-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);
+TimeStep.prototype.setMinimumStep = function(minimumStep) {
+ if (minimumStep == undefined) {
+ return;
}
- 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
+ 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;}
};
/**
- * Create a Major label for the axis at position x
- * @param {Number} x
- * @param {String} text
- * @private
+ * Snap a date to a rounded value. The snap intervals are dependent on the
+ * current scale and step.
+ * @param {Date} date the date to be snapped
*/
-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);
+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);
}
- this.dom.majorTexts.push(label);
+ 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);
+ }
- 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
+ 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);
+ }
};
/**
- * Create a minor line for the axis at position x
- * @param {Number} x
- * @private
+ * 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.
*/
-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);
+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;
}
- 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
+ * 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
*/
-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);
+TimeStep.prototype.getLabelMinor = function(date) {
+ if (date == undefined) {
+ date = this.current;
}
- 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';
+ 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 '';
+ }
};
/**
- * Repaint the horizontal line for the axis
- * @private
+ * Returns formatted text for the major axis label, depending on the current
+ * date and the scale. For example when scale is MINUTE, the major scale is
+ * hours, and the hour will be formatted as "hh".
+ * @param {Date} [date] custom date. if not provided, current date is taken
*/
-TimeAxis.prototype._repaintLine = function() {
- var line = this.dom.line,
- frame = this.frame,
- options = this.options;
-
- // line before all axis elements
- if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
- if (line) {
- // put this line at the end of all childs
- frame.removeChild(line);
- frame.appendChild(line);
- }
- else {
- // create the axis line
- line = document.createElement('div');
- line.className = 'grid horizontal major';
- frame.appendChild(line);
- this.dom.line = line;
- }
-
- line.style.top = this.props.lineTop + 'px';
+TimeStep.prototype.getLabelMajor = function(date) {
+ if (date == undefined) {
+ date = this.current;
}
- else {
- if (line && axis.parentElement) {
- frame.removeChild(axis.line);
- delete this.dom.line;
- }
+
+ //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 '';
}
};
/**
- * Create characters used to determine the size of text on the axis
- * @private
+ * @constructor Stack
+ * Stacks items on top of each other.
+ * @param {ItemSet} parent
+ * @param {Object} [options]
*/
-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;
+function Stack (parent, options) {
+ this.parent = parent;
- if (!dom.measureCharMinor) {
- text = document.createTextNode('0');
- var measureCharMinor = document.createElement('DIV');
- measureCharMinor.className = 'text minor measure';
- measureCharMinor.appendChild(text);
- this.frame.appendChild(measureCharMinor);
+ this.options = options || {};
+ this.defaultOptions = {
+ order: function (a, b) {
+ //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup
+ // Order: ranges over non-ranges, ranged ordered by width, and
+ // lastly ordered by start.
+ if (a instanceof ItemRange) {
+ if (b instanceof ItemRange) {
+ var aInt = (a.data.end - a.data.start);
+ var bInt = (b.data.end - b.data.start);
+ return (aInt - bInt) || (a.data.start - b.data.start);
+ }
+ else {
+ return -1;
+ }
+ }
+ else {
+ if (b instanceof ItemRange) {
+ return 1;
+ }
+ else {
+ return (a.data.start - b.data.start);
+ }
+ }
+ },
+ margin: {
+ item: 10
+ }
+ };
- dom.measureCharMinor = measureCharMinor;
- }
+ this.ordered = []; // ordered items
+}
- if (!dom.measureCharMajor) {
- text = document.createTextNode('0');
- var measureCharMajor = document.createElement('DIV');
- measureCharMajor.className = 'text major measure';
- measureCharMajor.appendChild(text);
- this.frame.appendChild(measureCharMajor);
+/**
+ * Set options for the stack
+ * @param {Object} options Available options:
+ * {ItemSet} parent
+ * {Number} margin
+ * {function} order Stacking order
+ */
+Stack.prototype.setOptions = function setOptions (options) {
+ util.extend(this.options, options);
- dom.measureCharMajor = measureCharMajor;
- }
+ // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately
};
/**
- * Reflow the component
- * @return {Boolean} resized
+ * Stack the items such that they don't overlap. The items will have a minimal
+ * distance equal to options.margin.item.
*/
-TimeAxis.prototype.reflow = function () {
- var changed = 0,
- update = util.updateProperty,
- frame = this.frame,
- range = this.range;
+Stack.prototype.update = function update() {
+ this._order();
+ this._stack();
+};
- if (!range) {
- throw new Error('Cannot repaint time axis: no range configured');
+/**
+ * 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 _order () {
+ var items = this.parent.items;
+ if (!items) {
+ throw new Error('Cannot stack items: parent does not contain items');
}
- if (frame) {
- changed += update(this, 'top', frame.offsetTop);
- changed += update(this, 'left', frame.offsetLeft);
-
- // calculate size of a character
- var props = this.props,
- showMinorLabels = this.getOption('showMinorLabels'),
- showMajorLabels = this.getOption('showMajorLabels'),
- measureCharMinor = this.dom.measureCharMinor,
- measureCharMajor = this.dom.measureCharMajor;
- if (measureCharMinor) {
- props.minorCharHeight = measureCharMinor.clientHeight;
- props.minorCharWidth = measureCharMinor.clientWidth;
- }
- if (measureCharMajor) {
- props.majorCharHeight = measureCharMajor.clientHeight;
- props.majorCharWidth = measureCharMajor.clientWidth;
- }
-
- var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0;
- if (parentHeight != props.parentHeight) {
- props.parentHeight = parentHeight;
- changed += 1;
+ // TODO: store the sorted items, to have less work later on
+ var ordered = [];
+ var index = 0;
+ // items is a map (no array)
+ util.forEach(items, function (item) {
+ if (item.visible) {
+ ordered[index] = item;
+ index++;
}
- switch (this.getOption('orientation')) {
- case 'bottom':
- props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
- props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
-
- props.minorLabelTop = 0;
- props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight;
-
- props.minorLineTop = -this.top;
- props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0);
- props.minorLineWidth = 1; // TODO: really calculate width
-
- props.majorLineTop = -this.top;
- props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0);
- props.majorLineWidth = 1; // TODO: really calculate width
-
- props.lineTop = 0;
+ });
- break;
+ //if a customer stack order function exists, use it.
+ var order = this.options.order || this.defaultOptions.order;
+ if (!(typeof order === 'function')) {
+ throw new Error('Option order must be a function');
+ }
- case 'top':
- props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
- props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
+ ordered.sort(order);
- props.majorLabelTop = 0;
- props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight;
+ this.ordered = ordered;
+};
- props.minorLineTop = props.minorLabelTop;
- props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top);
- props.minorLineWidth = 1; // TODO: really calculate width
+/**
+ * Adjust vertical positions of the events such that they don't overlap each
+ * other.
+ * @private
+ */
+Stack.prototype._stack = function _stack () {
+ var i,
+ iMax,
+ ordered = this.ordered,
+ options = this.options,
+ orientation = options.orientation || this.defaultOptions.orientation,
+ axisOnTop = (orientation == 'top'),
+ margin;
- props.majorLineTop = 0;
- props.majorLineHeight = Math.max(parentHeight - this.top);
- props.majorLineWidth = 1; // TODO: really calculate width
+ if (options.margin && options.margin.item !== undefined) {
+ margin = options.margin.item;
+ }
+ else {
+ margin = this.defaultOptions.margin.item
+ }
- props.lineTop = props.majorLabelHeight + props.minorLabelHeight;
+ // 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);
+ }
+};
- break;
+/**
+ * Check if the destiny position of given item overlaps with any
+ * of the other items from index itemStart to itemEnd.
+ * @param {Array} items Array with items
+ * @param {int} itemIndex Number of the item to be checked for overlap
+ * @param {int} itemStart First item to be checked.
+ * @param {int} itemEnd Last item to be checked.
+ * @return {Object | null} colliding item, or undefined when no collisions
+ * @param {Number} margin A minimum required margin.
+ * If margin is provided, the two items will be
+ * marked colliding when they overlap or
+ * when the margin between the two is smaller than
+ * the requested margin.
+ */
+Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex,
+ itemStart, itemEnd, margin) {
+ var collision = this.collision;
- default:
- throw new Error('Unkown orientation "' + this.getOption('orientation') + '"');
+ // 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;
+ }
}
-
- var height = props.minorLabelHeight + props.majorLabelHeight;
- changed += update(this, 'width', frame.offsetWidth);
- changed += update(this, 'height', height);
-
- // calculate range and step
- this._updateConversion();
-
- var start = util.convert(range.start, 'Date'),
- end = util.convert(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);
+ return null;
};
/**
- * 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
+ * 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
*/
-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);
- }
+Stack.prototype.collision = function collision (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);
};
/**
- * 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
+ * @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
*/
-// TODO: improve performance by replacing all Array.forEach with a for loop
-function ItemSet(parent, depends, options) {
+function Range(options) {
this.id = util.randomUUID();
- this.parent = parent;
- this.depends = depends;
+ this.start = 0; // Number
+ this.end = 0; // Number
- // one options object is shared by this itemset and all its items
- this.options = options || {};
- this.defaultOptions = {
- type: 'box',
- align: 'center',
- orientation: 'bottom',
- margin: {
- axis: 20,
- item: 10
- },
- padding: 5
+ // this.options = options || {}; // TODO: fix range options
+ this.options = {
+ min: null,
+ max: null,
+ zoomMin: null,
+ zoomMax: null
};
- this.dom = {};
+ this.listeners = [];
- var me = this;
- this.itemsData = null; // DataSet
- this.range = null; // Range or Object {start: number, end: number}
+ this.setOptions(options);
+}
- this.listeners = {
- 'add': function (event, params, senderId) {
- if (senderId != me.id) {
- me._onAdd(params.items);
- }
- },
- 'update': function (event, params, senderId) {
- if (senderId != me.id) {
- me._onUpdate(params.items);
- }
- },
- 'remove': function (event, params, senderId) {
- if (senderId != me.id) {
- me._onRemove(params.items);
- }
- }
- };
+/**
+ * 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);
- this.items = {}; // object with an Item for every data item
- this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
- this.stack = new Stack(this, Object.create(this.options));
- this.conversion = null;
+ if (options.start != null || options.end != null) {
+ this.setRange(options.start, options.end);
+ }
+};
- // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis
-}
+/**
+ * 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;
-ItemSet.prototype = new Panel();
+ if (direction != 'horizontal' && direction != 'vertical') {
+ throw new TypeError('Unknown direction "' + direction + '". ' +
+ 'Choose "horizontal" or "vertical".');
+ }
-// available item types will be registered here
-ItemSet.types = {
- box: ItemBox,
- range: ItemRange,
- point: ItemPoint
+ //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".');
+ }
};
/**
- * Set options for the ItemSet. Existing options will be extended/overwritten.
- * @param {Object} [options] The following options are available:
- * {String | function} [className]
- * class name for the itemset
- * {String} [type]
- * Default type for the items. Choose from 'box'
- * (default), 'point', or 'range'. The default
- * Style can be overwritten by individual items.
- * {String} align
- * Alignment for the items, only applicable for
- * ItemBox. Choose 'center' (default), 'left', or
- * 'right'.
- * {String} orientation
- * Orientation of the item set. Choose 'top' or
- * 'bottom' (default).
- * {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.
+ * 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.
*/
-ItemSet.prototype.setOptions = Component.prototype.setOptions;
+Range.prototype.on = function (event, callback) {
+ events.addListener(this, event, callback);
+};
/**
- * Set range (start and end).
- * @param {Range | Object} range A Range or an object containing start and end.
+ * Trigger an event
+ * @param {String} event name of the event, available events: 'rangechange',
+ * 'rangechanged'
+ * @private
*/
-ItemSet.prototype.setRange = function setRange(range) {
- if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
- throw new TypeError('Range must be an instance of Range, ' +
- 'or an object containing start and end.');
+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');
}
- this.range = range;
};
/**
- * Repaint the component
+ * 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
*/
-ItemSet.prototype.repaint = function repaint() {
- var changed = 0,
- update = util.updateProperty,
- asSize = util.option.asSize,
- options = this.options,
- orientation = this.getOption('orientation'),
- defaultOptions = this.defaultOptions,
- frame = this.frame;
-
- if (!frame) {
- frame = document.createElement('div');
- frame.className = 'itemset';
-
- var className = options.className;
- if (className) {
- util.addClassName(frame, util.option.asString(className));
- }
-
- // create background panel
- var background = document.createElement('div');
- background.className = 'background';
- frame.appendChild(background);
- this.dom.background = background;
-
- // create foreground panel
- var foreground = document.createElement('div');
- foreground.className = 'foreground';
- frame.appendChild(foreground);
- this.dom.foreground = foreground;
-
- // create axis panel
- var axis = document.createElement('div');
- axis.className = 'itemset-axis';
- //frame.appendChild(axis);
- this.dom.axis = axis;
-
- this.frame = frame;
- changed += 1;
- }
+Range.prototype._applyRange = function(start, end) {
+ var newStart = (start != null) ? util.convert(start, 'Number') : this.start;
+ var newEnd = (end != null) ? util.convert(end, 'Number') : this.end;
+ var diff;
- if (!this.parent) {
- throw new Error('Cannot repaint itemset: no parent attached');
- }
- var parentContainer = this.parent.getContainer();
- if (!parentContainer) {
- throw new Error('Cannot repaint itemset: parent has no container element');
- }
- if (!frame.parentNode) {
- parentContainer.appendChild(frame);
- changed += 1;
+ // check for valid number
+ if (isNaN(newStart)) {
+ throw new Error('Invalid start "' + start + '"');
}
- if (!this.dom.axis.parentNode) {
- parentContainer.appendChild(this.dom.axis);
- changed += 1;
+ if (isNaN(newEnd)) {
+ throw new Error('Invalid end "' + end + '"');
}
- // reposition frame
- changed += update(frame.style, 'left', asSize(options.left, '0px'));
- changed += update(frame.style, 'top', asSize(options.top, '0px'));
- changed += update(frame.style, 'width', asSize(options.width, '100%'));
- changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
-
- // reposition axis
- changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px'));
- changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%'));
- if (orientation == 'bottom') {
- changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px');
- }
- else { // orientation == 'top'
- changed += update(this.dom.axis.style, 'top', this.top + 'px');
+ // prevent start < end
+ if (newEnd < newStart) {
+ newEnd = newStart;
}
- this._updateConversion();
-
- var me = this,
- queue = this.queue,
- itemsData = this.itemsData,
- items = this.items,
- dataOptions = {
- fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type']
- };
- // TODO: copy options from the itemset itself?
-
- // show/hide added/changed/removed items
- Object.keys(queue).forEach(function (id) {
- //var entry = queue[id];
- var action = queue[id];
- var item = items[id];
- //var item = entry.item;
- //noinspection FallthroughInSwitchStatementJS
- switch (action) {
- case 'add':
- case 'update':
- var itemData = itemsData && itemsData.get(id, dataOptions);
-
- if (itemData) {
- var type = itemData.type ||
- (itemData.start && itemData.end && 'range') ||
- options.type ||
- 'box';
- var constructor = ItemSet.types[type];
-
- // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
- if (item) {
- // update item
- if (!constructor || !(item instanceof constructor)) {
- // item type has changed, hide and delete the item
- changed += item.hide();
- item = null;
- }
- else {
- item.data = itemData; // TODO: create a method item.setData ?
- changed++;
- }
- }
-
- if (!item) {
- // create item
- if (constructor) {
- item = new constructor(me, itemData, options, defaultOptions);
- changed++;
- }
- else {
- throw new TypeError('Unknown item type "' + type + '"');
- }
- }
-
- // force a repaint (not only a reposition)
- item.repaint();
-
- items[id] = item;
- }
-
- // update queue
- delete queue[id];
- break;
-
- case 'remove':
- if (item) {
- // remove DOM of the item
- changed += item.hide();
- }
+ // prevent start < min
+ if (this.options.min != null) {
+ var min = this.options.min.valueOf();
+ if (newStart < min) {
+ diff = (min - newStart);
+ newStart += diff;
+ newEnd += diff;
+ }
+ }
- // update lists
- delete items[id];
- delete queue[id];
- break;
+ // prevent end > max
+ if (this.options.max != null) {
+ var max = this.options.max.valueOf();
+ if (newEnd > max) {
+ diff = (newEnd - max);
+ newStart -= diff;
+ newEnd -= diff;
+ }
+ }
- default:
- console.log('Error: unknown action "' + action + '"');
+ // 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;
+ }
+ }
+ }
- // reposition all items. Show items only when in the visible area
- util.forEach(this.items, function (item) {
- if (item.visible) {
- changed += item.show();
- item.reposition();
+ // prevent (end-start) > zoomMin
+ if (this.options.zoomMax != null) {
+ var zoomMax = this.options.zoomMax.valueOf();
+ if (zoomMax < 0) {
+ zoomMax = 0;
}
- else {
- changed += item.hide();
+ 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;
+ }
}
- });
+ }
- return (changed > 0);
-};
+ var changed = (this.start != newStart || this.end != newEnd);
-/**
- * Get the foreground container element
- * @return {HTMLElement} foreground
- */
-ItemSet.prototype.getForeground = function getForeground() {
- return this.dom.foreground;
+ this.start = newStart;
+ this.end = newEnd;
+
+ return changed;
};
/**
- * Get the background container element
- * @return {HTMLElement} background
+ * Retrieve the current range.
+ * @return {Object} An object with start and end properties
*/
-ItemSet.prototype.getBackground = function getBackground() {
- return this.dom.background;
+Range.prototype.getRange = function() {
+ return {
+ start: this.start,
+ end: this.end
+ };
};
/**
- * Get the axis container element
- * @return {HTMLElement} axis
+ * Calculate the conversion offset and factor for current range, based on
+ * the provided width
+ * @param {Number} width
+ * @returns {{offset: number, factor: number}} conversion
*/
-ItemSet.prototype.getAxis = function getAxis() {
- return this.dom.axis;
+Range.prototype.conversion = function (width) {
+ var start = this.start;
+ var end = this.end;
+
+ return Range.conversion(this.start, this.end, width);
};
/**
- * Reflow the component
- * @return {Boolean} resized
+ * 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
*/
-ItemSet.prototype.reflow = function reflow () {
- var changed = 0,
- options = this.options,
- marginAxis = options.margin && options.margin.axis || this.defaultOptions.margin.axis,
- marginItem = options.margin && options.margin.item || this.defaultOptions.margin.item,
- update = util.updateProperty,
- asNumber = util.option.asNumber,
- asSize = util.option.asSize,
- frame = this.frame;
-
- if (frame) {
- this._updateConversion();
-
- util.forEach(this.items, function (item) {
- changed += item.reflow();
- });
-
- // TODO: stack.update should be triggered via an event, in stack itself
- // TODO: only update the stack when there are changed items
- this.stack.update();
-
- var maxHeight = asNumber(options.maxHeight);
- var fixedHeight = (asSize(options.height) != null);
- var height;
- if (fixedHeight) {
- height = frame.offsetHeight;
- }
- else {
- // height is not specified, determine the height from the height and positioned items
- var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items
- if (visibleItems.length) {
- var min = visibleItems[0].top;
- var max = visibleItems[0].top + visibleItems[0].height;
- util.forEach(visibleItems, function (item) {
- min = Math.min(min, item.top);
- max = Math.max(max, (item.top + item.height));
- });
- height = (max - min) + marginAxis + marginItem;
- }
- else {
- height = marginAxis + marginItem;
- }
- }
- if (maxHeight != null) {
- height = Math.min(height, maxHeight);
+Range.conversion = function (start, end, width) {
+ if (width != 0 && (end - start != 0)) {
+ return {
+ offset: start,
+ factor: width / (end - start)
}
- changed += update(this, 'height', height);
-
- // calculate height from items
- changed += update(this, 'top', frame.offsetTop);
- changed += update(this, 'left', frame.offsetLeft);
- changed += update(this, 'width', frame.offsetWidth);
}
else {
- changed += 1;
+ return {
+ offset: 0,
+ factor: 1
+ };
}
-
- return (changed > 0);
};
/**
- * Hide this component from the DOM
- * @return {Boolean} changed
+ * Start moving horizontally or vertically
+ * @param {Event} event
+ * @param {Object} listener Listener containing the component and params
+ * @private
*/
-ItemSet.prototype.hide = function hide() {
- var changed = false;
+Range.prototype._onMouseDown = function(event, listener) {
+ event = event || window.event;
+ var params = listener.params;
- // remove the DOM
- if (this.frame && this.frame.parentNode) {
- this.frame.parentNode.removeChild(this.frame);
- changed = true;
- }
- if (this.dom.axis && this.dom.axis.parentNode) {
- this.dom.axis.parentNode.removeChild(this.dom.axis);
- changed = true;
+ // only react on left mouse button down
+ var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
+ if (!leftButtonDown) {
+ return;
}
- return changed;
-};
+ // get mouse position
+ params.mouseX = util.getPageX(event);
+ params.mouseY = util.getPageY(event);
+ params.previousLeft = 0;
+ params.previousOffset = 0;
-/**
- * Set items
- * @param {vis.DataSet | null} items
- */
-ItemSet.prototype.setItems = function setItems(items) {
- var me = this,
- ids,
- oldItemsData = this.itemsData;
+ params.moved = false;
+ params.start = this.start;
+ params.end = this.end;
- // replace the dataset
- if (!items) {
- this.itemsData = null;
- }
- else if (items instanceof DataSet || items instanceof DataView) {
- this.itemsData = items;
- }
- else {
- throw new TypeError('Data must be an instance of DataSet');
+ var frame = listener.component.frame;
+ if (frame) {
+ frame.style.cursor = 'move';
}
- if (oldItemsData) {
- // unsubscribe from old dataset
- util.forEach(this.listeners, function (callback, event) {
- oldItemsData.unsubscribe(event, callback);
- });
-
- // remove all drawn items
- ids = oldItemsData.getIds();
- this._onRemove(ids);
+ // 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 (this.itemsData) {
- // subscribe to new dataset
- var id = this.id;
- util.forEach(this.listeners, function (callback, event) {
- me.itemsData.subscribe(event, callback, id);
- });
-
- // draw all new items
- ids = this.itemsData.getIds();
- this._onAdd(ids);
+ if (!params.onMouseUp) {
+ params.onMouseUp = function (event) {
+ me._onMouseUp(event, listener);
+ };
+ util.addEventListener(document, "mouseup", params.onMouseUp);
}
-};
-/**
- * Get the current items items
- * @returns {vis.DataSet | null}
- */
-ItemSet.prototype.getItems = function getItems() {
- return this.itemsData;
+ util.preventDefault(event);
};
/**
- * Handle updated items
- * @param {Number[]} ids
+ * Perform moving operating.
+ * This function activated from within the funcion TimeAxis._onMouseDown().
+ * @param {Event} event
+ * @param {Object} listener
* @private
*/
-ItemSet.prototype._onUpdate = function _onUpdate(ids) {
- this._toQueue('update', ids);
-};
+Range.prototype._onMouseMove = function (event, listener) {
+ event = event || window.event;
-/**
- * Handle changed items
- * @param {Number[]} ids
- * @private
- */
-ItemSet.prototype._onAdd = function _onAdd(ids) {
- this._toQueue('add', ids);
-};
+ var params = listener.params;
-/**
- * Handle removed items
- * @param {Number[]} ids
- * @private
- */
-ItemSet.prototype._onRemove = function _onRemove(ids) {
- this._toQueue('remove', ids);
-};
+ // calculate change in mouse position
+ var mouseX = util.getPageX(event);
+ var mouseY = util.getPageY(event);
-/**
- * Put items in the queue to be added/updated/remove
- * @param {String} action can be 'add', 'update', 'remove'
- * @param {Number[]} ids
- */
-ItemSet.prototype._toQueue = function _toQueue(action, ids) {
- var queue = this.queue;
- ids.forEach(function (id) {
- queue[id] = action;
- });
+ if (params.mouseX == undefined) {
+ params.mouseX = mouseX;
+ }
+ if (params.mouseY == undefined) {
+ params.mouseY = mouseY;
+ }
- if (this.controller) {
- //this.requestReflow();
- this.requestRepaint();
+ 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);
};
/**
- * 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.
+ * Stop moving operating.
+ * This function activated from within the function Range._onMouseDown().
+ * @param {event} event
+ * @param {Object} listener
* @private
*/
-ItemSet.prototype._updateConversion = function _updateConversion() {
- var range = this.range;
- if (!range) {
- throw new Error('No range configured');
+Range.prototype._onMouseUp = function (event, listener) {
+ event = event || window.event;
+
+ var params = listener.params;
+
+ if (listener.component.frame) {
+ listener.component.frame.style.cursor = 'auto';
}
- if (range.conversion) {
- this.conversion = range.conversion(this.width);
+ // remove event listeners here, important for Safari
+ if (params.onMouseMove) {
+ util.removeEventListener(document, "mousemove", params.onMouseMove);
+ params.onMouseMove = null;
}
- else {
- this.conversion = Range.conversion(range.start, range.end, this.width);
+ if (params.onMouseUp) {
+ util.removeEventListener(document, "mouseup", params.onMouseUp);
+ params.onMouseUp = null;
}
-};
+ //util.preventDefault(event);
-/**
- * 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 toTime(x) {
- var conversion = this.conversion;
- return new Date(x / conversion.factor + conversion.offset);
+ if (params.moved) {
+ // fire a rangechanged event
+ this._trigger('rangechanged');
+ }
};
/**
- * 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.
+ * Event handler for mouse wheel event, used to zoom
+ * Code from http://adomas.org/javascript-mouse-wheel/
+ * @param {Event} event
+ * @param {Object} listener
+ * @private
*/
-ItemSet.prototype.toScreen = function toScreen(time) {
- var conversion = this.conversion;
- return (time.valueOf() - conversion.offset) * conversion.factor;
-};
+Range.prototype._onMouseWheel = function(event, listener) {
+ event = event || window.event;
-/**
- * @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
- * @param {Object} [defaultOptions] default options
- * // TODO: describe available options
- */
-function Item (parent, data, options, defaultOptions) {
- this.parent = parent;
- this.data = data;
- this.dom = null;
- this.options = options || {};
- this.defaultOptions = defaultOptions || {};
+ // 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;
+ }
- this.selected = false;
- this.visible = false;
- this.top = 0;
- this.left = 0;
- this.width = 0;
- this.height = 0;
-}
+ // 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;
+ }
+ }
-/**
- * Select current item
- */
-Item.prototype.select = function select() {
- this.selected = true;
-};
+ me.zoom(zoomFactor, zoomAround);
+ };
-/**
- * Unselect current item
- */
-Item.prototype.unselect = function unselect() {
- this.selected = false;
-};
+ zoom();
+ }
-/**
- * Show the Item in the DOM (when not already visible)
- * @return {Boolean} changed
- */
-Item.prototype.show = function show() {
- return false;
+ // 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);
};
-/**
- * Hide the Item from the DOM (when visible)
- * @return {Boolean} changed
- */
-Item.prototype.hide = function hide() {
- return false;
-};
/**
- * Repaint the item
- * @return {Boolean} changed
+ * 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
*/
-Item.prototype.repaint = function repaint() {
- // should be implemented by the item
- return false;
-};
+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;
+ }
-/**
- * Reflow the item
- * @return {Boolean} resized
- */
-Item.prototype.reflow = function reflow() {
- // should be implemented by the item
- return false;
+ // 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);
};
/**
- * @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
- * @param {Object} [defaultOptions] default options
- * // TODO: describe available options
+ * 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
*/
-function ItemBox (parent, data, options, defaultOptions) {
- this.props = {
- dot: {
- left: 0,
- top: 0,
- width: 0,
- height: 0
- },
- line: {
- top: 0,
- left: 0,
- width: 0,
- height: 0
- }
- };
+Range.prototype.move = function(moveFactor) {
+ // zoom start Date and end Date relative to the zoomAroundDate
+ var diff = (this.end - this.start);
- Item.call(this, parent, data, options, defaultOptions);
-}
+ // apply new values
+ var newStart = this.start + diff * moveFactor;
+ var newEnd = this.end + diff * moveFactor;
-ItemBox.prototype = new Item (null, null);
+ // TODO: reckon with min and max range
+
+ this.start = newStart;
+ this.end = newEnd;
+};
/**
- * Select the item
- * @override
+ * Move the range to a new center point
+ * @param {Number} moveTo New center point of the range
*/
-ItemBox.prototype.select = function select() {
- this.selected = true;
- // TODO: select and unselect
-};
+Range.prototype.moveTo = function(moveTo) {
+ var center = (this.start + this.end) / 2;
+
+ var diff = center - moveTo;
+
+ // calculate new start and end
+ var newStart = this.start - diff;
+ var newEnd = this.end - diff;
+
+ this.setRange(newStart, newEnd);
+}
/**
- * Unselect the item
- * @override
+ * @constructor Controller
+ *
+ * A Controller controls the reflows and repaints of all visual components
*/
-ItemBox.prototype.unselect = function unselect() {
- this.selected = false;
- // TODO: select and unselect
-};
+function Controller () {
+ this.id = util.randomUUID();
+ this.components = {};
+
+ this.repaintTimer = undefined;
+ this.reflowTimer = undefined;
+}
/**
- * Repaint the item
- * @return {Boolean} changed
+ * Add a component to the controller
+ * @param {Component} component
*/
-ItemBox.prototype.repaint = function repaint() {
- // TODO: make an efficient repaint
- var changed = false;
- var dom = this.dom;
-
- if (!dom) {
- this._create();
- dom = this.dom;
- changed = true;
+Controller.prototype.add = function add(component) {
+ // validate the component
+ if (component.id == undefined) {
+ throw new Error('Component has no field id');
+ }
+ if (!(component instanceof Component) && !(component instanceof Controller)) {
+ throw new TypeError('Component must be an instance of ' +
+ 'prototype Component or Controller');
}
- if (dom) {
- if (!this.parent) {
- throw new Error('Cannot repaint item: no parent attached');
- }
- var foreground = this.parent.getForeground();
- if (!foreground) {
- throw new Error('Cannot repaint time axis: ' +
- 'parent has no foreground container element');
- }
- var background = this.parent.getBackground();
- if (!background) {
- throw new Error('Cannot repaint time axis: ' +
- 'parent has no background container element');
- }
- var axis = this.parent.getAxis();
- if (!background) {
- throw new Error('Cannot repaint time axis: ' +
- 'parent has no axis container element');
- }
-
- if (!dom.box.parentNode) {
- foreground.appendChild(dom.box);
- changed = true;
- }
- if (!dom.line.parentNode) {
- background.appendChild(dom.line);
- changed = true;
- }
- if (!dom.dot.parentNode) {
- axis.appendChild(dom.dot);
- changed = true;
- }
+ // add the component
+ component.controller = this;
+ this.components[component.id] = component;
+};
- // 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);
+/**
+ * Remove a component from the controller
+ * @param {Component | String} component
+ */
+Controller.prototype.remove = function remove(component) {
+ var id;
+ for (id in this.components) {
+ if (this.components.hasOwnProperty(id)) {
+ if (id == component || this.components[id] == component) {
+ break;
}
- 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;
}
}
- return changed;
+ if (id) {
+ delete this.components[id];
+ }
};
/**
- * Show the item in the DOM (when not already visible). The items DOM will
- * be created when needed.
- * @return {Boolean} changed
+ * Request a reflow. The controller will schedule a reflow
+ * @param {Boolean} [force] If true, an immediate reflow is forced. Default
+ * is false.
*/
-ItemBox.prototype.show = function show() {
- if (!this.dom || !this.dom.box.parentNode) {
- return this.repaint();
+Controller.prototype.requestReflow = function requestReflow(force) {
+ if (force) {
+ this.reflow();
}
else {
- return false;
+ if (!this.reflowTimer) {
+ var me = this;
+ this.reflowTimer = setTimeout(function () {
+ me.reflowTimer = undefined;
+ me.reflow();
+ }, 0);
+ }
}
};
/**
- * Hide the item from the DOM (when visible)
- * @return {Boolean} changed
+ * Request a repaint. The controller will schedule a repaint
+ * @param {Boolean} [force] If true, an immediate repaint is forced. Default
+ * is false.
*/
-ItemBox.prototype.hide = function hide() {
- var changed = false,
- dom = this.dom;
- if (dom) {
- if (dom.box.parentNode) {
- dom.box.parentNode.removeChild(dom.box);
- changed = true;
- }
- if (dom.line.parentNode) {
- dom.line.parentNode.removeChild(dom.line);
- }
- if (dom.dot.parentNode) {
- dom.dot.parentNode.removeChild(dom.dot);
+Controller.prototype.requestRepaint = function requestRepaint(force) {
+ if (force) {
+ this.repaint();
+ }
+ else {
+ if (!this.repaintTimer) {
+ var me = this;
+ this.repaintTimer = setTimeout(function () {
+ me.repaintTimer = undefined;
+ me.repaint();
+ }, 0);
}
}
- 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
+ * Repaint all components
*/
-ItemBox.prototype.reflow = function reflow() {
- var changed = 0,
- update,
- dom,
- props,
- options,
- margin,
- start,
- align,
- orientation,
- top,
- left,
- data,
- range;
-
- if (this.data.start == undefined) {
- throw new Error('Property "start" missing in item ' + this.data.id);
- }
+Controller.prototype.repaint = function repaint() {
+ var changed = false;
- data = this.data;
- range = this.parent && this.parent.range;
- if (data && range) {
- // TODO: account for the width of the item
- var interval = (range.end - range.start);
- this.visible = (data.start > range.start - interval) && (data.start < range.end + interval);
- }
- else {
- this.visible = false;
+ // cancel any running repaint request
+ if (this.repaintTimer) {
+ clearTimeout(this.repaintTimer);
+ this.repaintTimer = undefined;
}
- if (this.visible) {
- dom = this.dom;
- if (dom) {
- update = util.updateProperty;
- props = this.props;
- options = this.options;
- start = this.parent.toScreen(this.data.start);
- align = options.align || this.defaultOptions.align;
- margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
- orientation = options.orientation || this.defaultOptions.orientation;
-
- changed += update(props.dot, 'height', dom.dot.offsetHeight);
- changed += update(props.dot, 'width', dom.dot.offsetWidth);
- changed += update(props.line, 'width', dom.line.offsetWidth);
- changed += update(props.line, 'height', dom.line.offsetHeight);
- changed += update(props.line, 'top', dom.line.offsetTop);
- changed += update(this, 'width', dom.box.offsetWidth);
- changed += update(this, 'height', dom.box.offsetHeight);
- if (align == 'right') {
- left = start - this.width;
- }
- else if (align == 'left') {
- left = start;
- }
- else {
- // default or 'center'
- left = start - this.width / 2;
- }
- changed += update(this, 'left', left);
-
- changed += update(props.line, 'left', start - props.line.width / 2);
- changed += update(props.dot, 'left', start - props.dot.width / 2);
- changed += update(props.dot, 'top', -props.dot.height / 2);
- if (orientation == 'top') {
- top = margin;
+ var done = {};
- changed += update(this, 'top', top);
+ 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);
+ });
}
- else {
- // default or 'bottom'
- var parentHeight = this.parent.height;
- top = parentHeight - this.height - margin;
-
- changed += update(this, 'top', top);
+ if (component.parent) {
+ repaint(component.parent, component.parent.id);
}
- }
- else {
- changed += 1;
+
+ // repaint the component itself and mark as done
+ changed = component.repaint() || changed;
+ done[id] = true;
}
}
- return (changed > 0);
+ util.forEach(this.components, repaint);
+
+ // immediately reflow when needed
+ if (changed) {
+ this.reflow();
+ }
+ // TODO: limit the number of nested reflows/repaints, prevent loop
};
/**
- * Create an items DOM
- * @private
+ * Reflow all components
*/
-ItemBox.prototype._create = function _create() {
- var dom = this.dom;
- if (!dom) {
- this.dom = dom = {};
+Controller.prototype.reflow = function reflow() {
+ var resized = false;
- // create the box
- dom.box = document.createElement('DIV');
- // className is updated in repaint()
+ // cancel any running repaint request
+ if (this.reflowTimer) {
+ clearTimeout(this.reflowTimer);
+ this.reflowTimer = undefined;
+ }
- // contents box (inside the background box). used for making margins
- dom.content = document.createElement('DIV');
- dom.content.className = 'content';
- dom.box.appendChild(dom.content);
+ var done = {};
- // line to axis
- dom.line = document.createElement('DIV');
- dom.line.className = 'line';
+ 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);
+ }
- // dot on axis
- dom.dot = document.createElement('DIV');
- dom.dot.className = 'dot';
+ // 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
};
/**
- * Reposition the item, recalculate its left, top, and width, using the current
- * range and size of the items itemset
- * @override
+ * Prototype for visual components
*/
-ItemBox.prototype.reposition = function reposition() {
- var dom = this.dom,
- props = this.props,
- orientation = this.options.orientation || this.defaultOptions.orientation;
+function Component () {
+ this.id = null;
+ this.parent = null;
+ this.depends = null;
+ this.controller = null;
+ this.options = null;
- if (dom) {
- var box = dom.box,
- line = dom.line,
- dot = dom.dot;
+ this.frame = null; // main DOM element
+ this.top = 0;
+ this.left = 0;
+ this.width = 0;
+ this.height = 0;
+}
- box.style.left = this.left + 'px';
- box.style.top = this.top + 'px';
+/**
+ * Set parameters for the frame. Parameters will be merged in current parameter
+ * set.
+ * @param {Object} options Available parameters:
+ * {String | function} [className]
+ * {EventBus} [eventBus]
+ * {String | Number | function} [left]
+ * {String | Number | function} [top]
+ * {String | Number | function} [width]
+ * {String | Number | function} [height]
+ */
+Component.prototype.setOptions = function setOptions(options) {
+ if (options) {
+ util.extend(this.options, options);
- line.style.left = props.line.left + 'px';
- if (orientation == 'top') {
- line.style.top = 0 + 'px';
- line.style.height = this.top + 'px';
- }
- else {
- // orientation 'bottom'
- line.style.top = (this.top + this.height) + 'px';
- line.style.height = Math.max(this.parent.height - this.top - this.height +
- this.props.dot.height / 2, 0) + 'px';
+ if (this.controller) {
+ this.requestRepaint();
+ this.requestReflow();
}
-
- 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
- * @param {Object} [defaultOptions] default options
- * // TODO: describe available options
+ * Get an option value by name
+ * The function will first check this.options object, and else will check
+ * this.defaultOptions.
+ * @param {String} name
+ * @return {*} value
*/
-function ItemPoint (parent, data, options, defaultOptions) {
- this.props = {
- dot: {
- top: 0,
- width: 0,
- height: 0
- },
- content: {
- height: 0,
- marginLeft: 0
- }
- };
-
- Item.call(this, parent, data, options, defaultOptions);
-}
-
-ItemPoint.prototype = new Item (null, null);
+Component.prototype.getOption = function getOption(name) {
+ var value;
+ if (this.options) {
+ value = this.options[name];
+ }
+ if (value === undefined && this.defaultOptions) {
+ value = this.defaultOptions[name];
+ }
+ return value;
+};
/**
- * Select the item
- * @override
+ * 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
*/
-ItemPoint.prototype.select = function select() {
- this.selected = true;
- // TODO: select and unselect
+// TODO: get rid of the getContainer and getFrame methods, provide these via the options
+Component.prototype.getContainer = function getContainer() {
+ // should be implemented by the component
+ return null;
};
/**
- * Unselect the item
- * @override
+ * Get the frame element of the component, the outer HTML DOM element.
+ * @returns {HTMLElement | null} frame
*/
-ItemPoint.prototype.unselect = function unselect() {
- this.selected = false;
- // TODO: select and unselect
+Component.prototype.getFrame = function getFrame() {
+ return this.frame;
};
/**
- * Repaint the item
+ * Repaint the component
* @return {Boolean} changed
*/
-ItemPoint.prototype.repaint = function repaint() {
- // TODO: make an efficient repaint
- var changed = false;
- var dom = this.dom;
-
- if (!dom) {
- this._create();
- dom = this.dom;
- changed = true;
- }
-
- if (dom) {
- if (!this.parent) {
- throw new Error('Cannot repaint item: no parent attached');
- }
- var foreground = this.parent.getForeground();
- if (!foreground) {
- throw new Error('Cannot repaint time axis: ' +
- 'parent has no foreground container element');
- }
-
- if (!dom.point.parentNode) {
- foreground.appendChild(dom.point);
- foreground.appendChild(dom.point);
- changed = true;
- }
-
- // update contents
- if (this.data.content != this.content) {
- this.content = this.data.content;
- if (this.content instanceof Element) {
- dom.content.innerHTML = '';
- dom.content.appendChild(this.content);
- }
- 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;
- }
- }
+Component.prototype.repaint = function repaint() {
+ // should be implemented by the component
+ return false;
+};
- return changed;
+/**
+ * Reflow the component
+ * @return {Boolean} resized
+ */
+Component.prototype.reflow = function reflow() {
+ // should be implemented by the component
+ return false;
};
/**
- * Show the item in the DOM (when not already visible). The items DOM will
- * be created when needed.
+ * Hide the component from the DOM
* @return {Boolean} changed
*/
-ItemPoint.prototype.show = function show() {
- if (!this.dom || !this.dom.point.parentNode) {
- return this.repaint();
+Component.prototype.hide = function hide() {
+ if (this.frame && this.frame.parentNode) {
+ this.frame.parentNode.removeChild(this.frame);
+ return true;
}
else {
return false;
@@ -5722,7121 +7042,7626 @@ ItemPoint.prototype.show = function show() {
};
/**
- * Hide the item from the DOM (when visible)
+ * Show the component in the DOM (when not already visible).
+ * A repaint will be executed when the component is not visible
* @return {Boolean} changed
*/
-ItemPoint.prototype.hide = function hide() {
- var changed = false,
- dom = this.dom;
- if (dom) {
- if (dom.point.parentNode) {
- dom.point.parentNode.removeChild(dom.point);
- changed = true;
- }
+Component.prototype.show = function show() {
+ if (!this.frame || !this.frame.parentNode) {
+ return this.repaint();
+ }
+ else {
+ return false;
}
- return changed;
};
/**
- * Reflow the item: calculate its actual size from the DOM
- * @return {boolean} resized returns true if the axis is resized
- * @override
+ * Request a repaint. The controller will schedule a repaint
*/
-ItemPoint.prototype.reflow = function reflow() {
- var changed = 0,
- update,
- dom,
- props,
- options,
- margin,
- orientation,
- start,
- top,
- data,
- range;
-
- if (this.data.start == undefined) {
- throw new Error('Property "start" missing in item ' + this.data.id);
- }
-
- data = this.data;
- range = this.parent && this.parent.range;
- if (data && range) {
- // TODO: account for the width of the item
- var interval = (range.end - range.start);
- this.visible = (data.start > range.start - interval) && (data.start < range.end);
+Component.prototype.requestRepaint = function requestRepaint() {
+ if (this.controller) {
+ this.controller.requestRepaint();
}
else {
- this.visible = false;
- }
-
- if (this.visible) {
- dom = this.dom;
- if (dom) {
- update = util.updateProperty;
- props = this.props;
- options = this.options;
- orientation = options.orientation || this.defaultOptions.orientation;
- margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
- start = this.parent.toScreen(this.data.start);
-
- changed += update(this, 'width', dom.point.offsetWidth);
- changed += update(this, 'height', dom.point.offsetHeight);
- changed += update(props.dot, 'width', dom.dot.offsetWidth);
- changed += update(props.dot, 'height', dom.dot.offsetHeight);
- changed += update(props.content, 'height', dom.content.offsetHeight);
-
- if (orientation == 'top') {
- top = margin;
- }
- else {
- // default or 'bottom'
- var parentHeight = this.parent.height;
- top = Math.max(parentHeight - this.height - margin, 0);
- }
- changed += update(this, 'top', top);
- changed += update(this, 'left', start - props.dot.width / 2);
- changed += update(props.content, 'marginLeft', 1.5 * props.dot.width);
- //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO
-
- changed += update(props.dot, 'top', (this.height - props.dot.height) / 2);
- }
- else {
- changed += 1;
- }
+ throw new Error('Cannot request a repaint: no controller configured');
+ // TODO: just do a repaint when no parent is configured?
}
-
- return (changed > 0);
};
/**
- * Create an items DOM
- * @private
+ * Request a reflow. The controller will schedule a reflow
*/
-ItemPoint.prototype._create = function _create() {
- var dom = this.dom;
- if (!dom) {
- this.dom = dom = {};
-
- // background box
- dom.point = document.createElement('div');
- // className is updated in repaint()
-
- // contents box, right from the dot
- dom.content = document.createElement('div');
- dom.content.className = 'content';
- dom.point.appendChild(dom.content);
-
- // dot at start
- dom.dot = document.createElement('div');
- dom.dot.className = 'dot';
- dom.point.appendChild(dom.dot);
+Component.prototype.requestReflow = function requestReflow() {
+ if (this.controller) {
+ this.controller.requestReflow();
}
-};
-
-/**
- * Reposition the item, recalculate its left, top, and width, using the current
- * range and size of the items itemset
- * @override
- */
-ItemPoint.prototype.reposition = function reposition() {
- var dom = this.dom,
- props = this.props;
-
- if (dom) {
- dom.point.style.top = this.top + 'px';
- dom.point.style.left = this.left + 'px';
-
- dom.content.style.marginLeft = props.content.marginLeft + 'px';
- //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO
-
- dom.dot.style.top = props.dot.top + 'px';
+ else {
+ throw new Error('Cannot request a reflow: no controller configured');
+ // TODO: just do a reflow when no parent is configured?
}
};
/**
- * @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
- * @param {Object} [defaultOptions] default options
- * // TODO: describe available options
+ * 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 ItemRange (parent, data, options, defaultOptions) {
- this.props = {
- content: {
- left: 0,
- width: 0
- }
- };
+function Panel(parent, depends, options) {
+ this.id = util.randomUUID();
+ this.parent = parent;
+ this.depends = depends;
- Item.call(this, parent, data, options, defaultOptions);
+ this.options = options || {};
}
-ItemRange.prototype = new Item (null, null);
+Panel.prototype = new Component();
/**
- * Select the item
- * @override
+ * 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]
*/
-ItemRange.prototype.select = function select() {
- this.selected = true;
- // TODO: select and unselect
-};
+Panel.prototype.setOptions = Component.prototype.setOptions;
/**
- * Unselect the item
- * @override
+ * Get the container element of the panel, which can be used by a child to
+ * add its own widgets.
+ * @returns {HTMLElement} container
*/
-ItemRange.prototype.unselect = function unselect() {
- this.selected = false;
- // TODO: select and unselect
+Panel.prototype.getContainer = function () {
+ return this.frame;
};
/**
- * Repaint the item
+ * Repaint the component
* @return {Boolean} changed
*/
-ItemRange.prototype.repaint = function repaint() {
- // TODO: make an efficient repaint
- var changed = false;
- var dom = this.dom;
-
- if (!dom) {
- this._create();
- dom = this.dom;
- changed = true;
- }
-
- if (dom) {
- if (!this.parent) {
- throw new Error('Cannot repaint item: no parent attached');
- }
- var foreground = this.parent.getForeground();
- if (!foreground) {
- throw new Error('Cannot repaint time axis: ' +
- 'parent has no foreground container element');
- }
-
- if (!dom.box.parentNode) {
- foreground.appendChild(dom.box);
- changed = true;
- }
+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';
- // 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;
+ var className = options.className;
+ if (className) {
+ if (typeof className == 'function') {
+ util.addClassName(frame, String(className()));
}
else {
- throw new Error('Property "content" missing in item ' + this.data.id);
+ util.addClassName(frame, String(className));
}
- 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;
+ 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;
}
- return changed;
+ 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);
};
/**
- * Show the item in the DOM (when not already visible). The items DOM will
- * be created when needed.
- * @return {Boolean} changed
+ * Reflow the component
+ * @return {Boolean} resized
*/
-ItemRange.prototype.show = function show() {
- if (!this.dom || !this.dom.box.parentNode) {
- return this.repaint();
+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 {
- return false;
+ changed += 1;
}
+
+ return (changed > 0);
};
/**
- * Hide the item from the DOM (when visible)
+ * 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 = options || {};
+ this.defaultOptions = {
+ autoResize: true
+ };
+
+ this.listeners = {}; // event listeners
+}
+
+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]
+ * {Boolean | function} [autoResize]
+ */
+RootPanel.prototype.setOptions = Component.prototype.setOptions;
+
+/**
+ * Repaint the component
* @return {Boolean} changed
*/
-ItemRange.prototype.hide = function hide() {
- var changed = false,
- dom = this.dom;
- if (dom) {
- if (dom.box.parentNode) {
- dom.box.parentNode.removeChild(dom.box);
- changed = true;
+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 = 'vis timeline rootpanel';
+
+ var className = options.className;
+ if (className) {
+ util.addClassName(frame, util.option.asString(className));
}
+
+ this.frame = frame;
+
+ changed += 1;
}
- return changed;
+ 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();
+ this._updateWatch();
+
+ return (changed > 0);
};
/**
- * Reflow the item: calculate its actual size from the DOM
- * @return {boolean} resized returns true if the axis is resized
- * @override
+ * Reflow the component
+ * @return {Boolean} resized
*/
-ItemRange.prototype.reflow = function reflow() {
+RootPanel.prototype.reflow = function () {
var changed = 0,
- dom,
- props,
- options,
- margin,
- padding,
- parent,
- start,
- end,
- data,
- range,
- update,
- box,
- parentWidth,
- contentLeft,
- orientation,
- top;
+ update = util.updateProperty,
+ frame = this.frame;
- if (this.data.start == undefined) {
- throw new Error('Property "start" missing in item ' + this.data.id);
+ 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);
}
- if (this.data.end == undefined) {
- throw new Error('Property "end" missing in item ' + this.data.id);
+ else {
+ changed += 1;
}
- data = this.data;
- range = this.parent && this.parent.range;
- if (data && range) {
- // TODO: account for the width of the item. Take some margin
- this.visible = (data.start < range.end) && (data.end > range.start);
+ return (changed > 0);
+};
+
+/**
+ * Update watching for resize, depending on the current option
+ * @private
+ */
+RootPanel.prototype._updateWatch = function () {
+ var autoResize = this.getOption('autoResize');
+ if (autoResize) {
+ this._watch();
}
else {
- this.visible = false;
+ this._unwatch();
}
+};
- if (this.visible) {
- dom = this.dom;
- if (dom) {
- props = this.props;
- options = this.options;
- parent = this.parent;
- start = parent.toScreen(this.data.start);
- end = parent.toScreen(this.data.end);
- update = util.updateProperty;
- box = dom.box;
- parentWidth = parent.width;
- orientation = options.orientation || this.defaultOptions.orientation;
- margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
- padding = options.padding || this.defaultOptions.padding;
-
- changed += update(props.content, 'width', dom.content.offsetWidth);
-
- changed += update(this, 'height', box.offsetHeight);
+/**
+ * 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;
- // 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;
- }
+ this._unwatch();
- // when range exceeds left of the window, position the contents at the left of the visible area
- if (start < 0) {
- contentLeft = Math.min(-start,
- (end - start - props.content.width - 2 * padding));
- // TODO: remove the need for options.padding. it's terrible.
- }
- else {
- contentLeft = 0;
- }
- changed += update(props.content, 'left', contentLeft);
+ var checkSize = function () {
+ var autoResize = me.getOption('autoResize');
+ if (!autoResize) {
+ // stop watching when the option autoResize is changed to false
+ me._unwatch();
+ return;
+ }
- if (orientation == 'top') {
- top = margin;
- changed += update(this, 'top', top);
- }
- else {
- // default or 'bottom'
- top = parent.height - this.height - margin;
- changed += update(this, 'top', top);
+ if (me.frame) {
+ // check whether the frame is resized
+ if ((me.frame.clientWidth != me.width) ||
+ (me.frame.clientHeight != me.height)) {
+ me.requestReflow();
}
-
- changed += update(this, 'left', start);
- changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width;
- }
- else {
- changed += 1;
}
- }
+ };
+
+ // TODO: automatically cleanup the event listener when the frame is deleted
+ util.addEventListener(window, 'resize', checkSize);
- return (changed > 0);
+ this.watchTimer = setInterval(checkSize, 1000);
};
/**
- * Create an items DOM
+ * Stop watching for a resize of the frame.
* @private
*/
-ItemRange.prototype._create = function _create() {
- var dom = this.dom;
- if (!dom) {
- this.dom = dom = {};
- // background box
- dom.box = document.createElement('div');
- // className is updated in repaint()
-
- // contents box
- dom.content = document.createElement('div');
- dom.content.className = 'content';
- dom.box.appendChild(dom.content);
+RootPanel.prototype._unwatch = function () {
+ if (this.watchTimer) {
+ clearInterval(this.watchTimer);
+ this.watchTimer = undefined;
}
+
+ // TODO: remove event listener on window.resize
};
/**
- * Reposition the item, recalculate its left, top, and width, using the current
- * range and size of the items itemset
- * @override
+ * 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.
*/
-ItemRange.prototype.reposition = function reposition() {
- var dom = this.dom,
- props = this.props;
+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);
- if (dom) {
- dom.box.style.top = this.top + 'px';
- dom.box.style.left = this.left + 'px';
- dom.box.style.width = this.width + 'px';
+ this._updateEventEmitters();
+};
- dom.content.style.left = props.content.left + 'px';
+/**
+ * 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
}
};
/**
- * @constructor Group
- * @param {GroupSet} parent
- * @param {Number | String} groupId
- * @param {Object} [options] Options to set initial property values
- * // TODO: describe available options
+ * 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 Group (parent, groupId, options) {
+function TimeAxis (parent, depends, options) {
this.id = util.randomUUID();
this.parent = parent;
+ this.depends = depends;
- this.groupId = groupId;
- this.itemsData = null; // DataSet
- this.itemset = null; // ItemSet
- this.options = options || {};
- this.options.top = 0;
-
- this.props = {
- label: {
- width: 0,
- height: 0
+ this.dom = {
+ majorLines: [],
+ majorTexts: [],
+ minorLines: [],
+ minorTexts: [],
+ redundant: {
+ majorLines: [],
+ majorTexts: [],
+ minorLines: [],
+ minorTexts: []
}
};
+ this.props = {
+ range: {
+ start: 0,
+ end: 0,
+ minimumStep: 0
+ },
+ lineTop: 0
+ };
- this.top = 0;
- this.left = 0;
- this.width = 0;
- this.height = 0;
+ this.options = options || {};
+ this.defaultOptions = {
+ orientation: 'bottom', // supported: 'top', 'bottom'
+ // TODO: implement timeaxis orientations 'left' and 'right'
+ showMinorLabels: true,
+ showMajorLabels: true
+ };
+
+ this.conversion = null;
+ this.range = null;
}
-Group.prototype = new Component();
+TimeAxis.prototype = new Component();
-// TODO: comment
-Group.prototype.setOptions = Component.prototype.setOptions;
+// TODO: comment options
+TimeAxis.prototype.setOptions = Component.prototype.setOptions;
/**
- * Get the container element of the panel, which can be used by a child to
- * add its own widgets.
- * @returns {HTMLElement} container
+ * Set a range (start and end)
+ * @param {Range | Object} range A Range or an object containing start and end.
*/
-Group.prototype.getContainer = function () {
- return this.parent.getContainer();
+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;
};
/**
- * Set item set for the group. The group will create a view on the itemset,
- * filtered by the groups id.
- * @param {DataSet | DataView} items
+ * 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
*/
-Group.prototype.setItems = function setItems(items) {
- if (this.itemset) {
- // remove current item set
- this.itemset.hide();
- this.itemset.setItems();
+TimeAxis.prototype.toTime = function(x) {
+ var conversion = this.conversion;
+ return new Date(x / conversion.factor + conversion.offset);
+};
- this.parent.controller.remove(this.itemset);
- this.itemset = null;
+/**
+ * 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,
+ orientation = this.getOption('orientation'),
+ props = this.props,
+ step = this.step;
+
+ var frame = this.frame;
+ if (!frame) {
+ frame = document.createElement('div');
+ this.frame = frame;
+ changed += 1;
+ }
+ frame.className = 'axis ' + 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;
}
- if (items) {
- var groupId = this.groupId;
+ var parent = frame.parentNode;
+ if (parent) {
+ var beforeChild = frame.nextSibling;
+ parent.removeChild(frame); // take frame offline while updating (is almost twice as fast)
+
+ var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ?
+ (this.props.parentHeight - this.height) + 'px' :
+ '0px';
+ changed += update(frame.style, 'top', asSize(options.top, defaultTop));
+ changed += update(frame.style, 'left', asSize(options.left, '0px'));
+ changed += update(frame.style, 'width', asSize(options.width, '100%'));
+ changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
+
+ // get characters width and height
+ this._repaintMeasureChars();
+
+ if (this.step) {
+ this._repaintStart();
+
+ step.first();
+ var xFirstMajorLabel = undefined;
+ var max = 0;
+ while (step.hasNext() && max < 1000) {
+ max++;
+ var cur = step.getCurrent(),
+ x = this.toScreen(cur),
+ isMajor = step.isMajor();
+
+ // TODO: lines must have a width, such that we can create css backgrounds
+
+ if (this.getOption('showMinorLabels')) {
+ this._repaintMinorText(x, step.getLabelMinor());
+ }
+
+ if (isMajor && this.getOption('showMajorLabels')) {
+ if (x > 0) {
+ if (xFirstMajorLabel == undefined) {
+ xFirstMajorLabel = x;
+ }
+ this._repaintMajorText(x, step.getLabelMajor());
+ }
+ this._repaintMajorLine(x);
+ }
+ else {
+ this._repaintMinorLine(x);
+ }
+
+ step.next();
+ }
- var itemsetOptions = Object.create(this.options);
- this.itemset = new ItemSet(this, null, itemsetOptions);
- this.itemset.setRange(this.parent.range);
+ // create a major label on the left when needed
+ if (this.getOption('showMajorLabels')) {
+ var leftTime = this.toTime(0),
+ leftText = step.getLabelMajor(leftTime),
+ widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation
- this.view = new DataView(items, {
- filter: function (item) {
- return item.group == groupId;
+ if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
+ this._repaintMajorText(0, leftText);
+ }
}
- });
- this.itemset.setItems(this.view);
- this.parent.controller.add(this.itemset);
+ this._repaintEnd();
+ }
+
+ this._repaintLine();
+
+ // put frame online again
+ if (beforeChild) {
+ parent.insertBefore(frame, beforeChild);
+ }
+ else {
+ parent.appendChild(frame)
+ }
}
+
+ return (changed > 0);
};
/**
- * Repaint the item
- * @return {Boolean} changed
+ * 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
*/
-Group.prototype.repaint = function repaint() {
- return false;
+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 = [];
};
/**
- * Reflow the item
- * @return {Boolean} resized
+ * End a repaint. Cleanup leftover DOM elements in the redundant list
+ * @private
*/
-Group.prototype.reflow = function reflow() {
- var changed = 0,
- update = util.updateProperty;
+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);
+ }
+ }
+ });
+};
- changed += update(this, 'top', this.itemset ? this.itemset.top : 0);
- changed += update(this, 'height', this.itemset ? this.itemset.height : 0);
- // TODO: reckon with the height of the group label
+/**
+ * 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 (this.label) {
- var inner = this.label.firstChild;
- changed += update(this.props.label, 'width', inner.clientWidth);
- changed += update(this.props.label, 'height', inner.clientHeight);
- }
- else {
- changed += update(this.props.label, 'width', 0);
- changed += update(this.props.label, 'height', 0);
+ 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);
- return (changed > 0);
+ 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
};
/**
- * An GroupSet holds a set of groups
- * @param {Component} parent
- * @param {Component[]} [depends] Components on which this components depends
- * (except for the parent)
- * @param {Object} [options] See GroupSet.setOptions for the available
- * options.
- * @constructor GroupSet
- * @extends Panel
+ * Create a Major label for the axis at position x
+ * @param {Number} x
+ * @param {String} text
+ * @private
*/
-function GroupSet(parent, depends, options) {
- this.id = util.randomUUID();
- this.parent = parent;
- this.depends = depends;
-
- this.options = options || {};
-
- this.range = null; // Range or Object {start: number, end: number}
- this.itemsData = null; // DataSet with items
- this.groupsData = null; // DataSet with groups
-
- this.groups = {}; // map with groups
+TimeAxis.prototype._repaintMajorText = function (x, text) {
+ // reuse redundant label
+ var label = this.dom.redundant.majorTexts.shift();
- this.dom = {};
- this.props = {
- labels: {
- width: 0
- }
- };
+ 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);
- // TODO: implement right orientation of the labels
+ 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
+};
- // changes in groups are queued key/value map containing id/action
- this.queue = {};
+/**
+ * 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();
- var me = this;
- 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);
- }
- };
-}
+ if (!line) {
+ // create vertical line
+ line = document.createElement('div');
+ line.className = 'grid vertical minor';
+ this.frame.appendChild(line);
+ }
+ this.dom.minorLines.push(line);
-GroupSet.prototype = new Panel();
+ var props = this.props;
+ line.style.top = props.minorLineTop + 'px';
+ line.style.height = props.minorLineHeight + 'px';
+ line.style.left = (x - props.minorLineWidth / 2) + 'px';
+};
/**
- * Set options for the GroupSet. Existing options will be extended/overwritten.
- * @param {Object} [options] The following options are available:
- * {String | function} groupsOrder
- * TODO: describe options
+ * Create a Major line for the axis at position x
+ * @param {Number} x
+ * @private
*/
-GroupSet.prototype.setOptions = Component.prototype.setOptions;
+TimeAxis.prototype._repaintMajorLine = function (x) {
+ // reuse redundant line
+ var line = this.dom.redundant.majorLines.shift();
-GroupSet.prototype.setRange = function (range) {
- // TODO: implement setRange
+ 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';
};
+
/**
- * Set items
- * @param {vis.DataSet | null} items
+ * Repaint the horizontal line for the axis
+ * @private
*/
-GroupSet.prototype.setItems = function setItems(items) {
- this.itemsData = items;
+TimeAxis.prototype._repaintLine = function() {
+ var line = this.dom.line,
+ frame = this.frame,
+ options = this.options;
- for (var id in this.groups) {
- if (this.groups.hasOwnProperty(id)) {
- var group = this.groups[id];
- group.setItems(items);
+ // line before all axis elements
+ if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
+ if (line) {
+ // put this line at the end of all childs
+ frame.removeChild(line);
+ frame.appendChild(line);
+ }
+ else {
+ // create the axis line
+ line = document.createElement('div');
+ line.className = 'grid horizontal major';
+ frame.appendChild(line);
+ this.dom.line = line;
+ }
+
+ line.style.top = this.props.lineTop + 'px';
+ }
+ else {
+ if (line && axis.parentElement) {
+ frame.removeChild(axis.line);
+ delete this.dom.line;
}
}
};
/**
- * Get items
- * @return {vis.DataSet | null} items
- */
-GroupSet.prototype.getItems = function getItems() {
- return this.itemsData;
-};
-
-/**
- * Set range (start and end).
- * @param {Range | Object} range A Range or an object containing start and end.
- */
-GroupSet.prototype.setRange = function setRange(range) {
- this.range = range;
-};
-
-/**
- * Set groups
- * @param {vis.DataSet} groups
+ * Create characters used to determine the size of text on the axis
+ * @private
*/
-GroupSet.prototype.setGroups = function setGroups(groups) {
- var me = this,
- ids;
-
- // unsubscribe from current dataset
- if (this.groupsData) {
- util.forEach(this.listeners, function (callback, event) {
- me.groupsData.unsubscribe(event, callback);
- });
+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;
- // remove all drawn groups
- ids = this.groupsData.getIds();
- this._onRemove(ids);
- }
+ if (!dom.measureCharMinor) {
+ text = document.createTextNode('0');
+ var measureCharMinor = document.createElement('DIV');
+ measureCharMinor.className = 'text minor measure';
+ measureCharMinor.appendChild(text);
+ this.frame.appendChild(measureCharMinor);
- // replace the dataset
- if (!groups) {
- this.groupsData = null;
- }
- else if (groups instanceof DataSet) {
- this.groupsData = groups;
- }
- else {
- this.groupsData = new DataSet({
- convert: {
- start: 'Date',
- end: 'Date'
- }
- });
- this.groupsData.add(groups);
+ dom.measureCharMinor = measureCharMinor;
}
- if (this.groupsData) {
- // subscribe to new dataset
- var id = this.id;
- util.forEach(this.listeners, function (callback, event) {
- me.groupsData.subscribe(event, callback, id);
- });
+ if (!dom.measureCharMajor) {
+ text = document.createTextNode('0');
+ var measureCharMajor = document.createElement('DIV');
+ measureCharMajor.className = 'text major measure';
+ measureCharMajor.appendChild(text);
+ this.frame.appendChild(measureCharMajor);
- // draw all new groups
- ids = this.groupsData.getIds();
- this._onAdd(ids);
+ dom.measureCharMajor = measureCharMajor;
}
};
/**
- * Get groups
- * @return {vis.DataSet | null} groups
- */
-GroupSet.prototype.getGroups = function getGroups() {
- return this.groupsData;
-};
-
-/**
- * Repaint the component
- * @return {Boolean} changed
+ * Reflow the component
+ * @return {Boolean} resized
*/
-GroupSet.prototype.repaint = function repaint() {
+TimeAxis.prototype.reflow = function () {
var changed = 0,
- i, id, group, label,
update = util.updateProperty,
- asSize = util.option.asSize,
- asElement = util.option.asElement,
- options = this.options,
- frame = this.dom.frame,
- labels = this.dom.labels;
+ frame = this.frame,
+ range = this.range;
- // create frame
- if (!this.parent) {
- throw new Error('Cannot repaint groupset: no parent attached');
- }
- var parentContainer = this.parent.getContainer();
- if (!parentContainer) {
- throw new Error('Cannot repaint groupset: parent has no container element');
+ if (!range) {
+ throw new Error('Cannot repaint time axis: no range configured');
}
- if (!frame) {
- frame = document.createElement('div');
- frame.className = 'groupset';
- this.dom.frame = frame;
-
- var className = options.className;
- if (className) {
- util.addClassName(frame, util.option.asString(className));
- }
- changed += 1;
- }
- if (!frame.parentNode) {
- parentContainer.appendChild(frame);
- changed += 1;
- }
+ if (frame) {
+ changed += update(this, 'top', frame.offsetTop);
+ changed += update(this, 'left', frame.offsetLeft);
- // create labels
- var labelContainer = asElement(options.labelContainer);
- if (!labelContainer) {
- throw new Error('Cannot repaint groupset: option "labelContainer" not defined');
- }
- if (!labels) {
- labels = document.createElement('div');
- labels.className = 'labels';
- //frame.appendChild(labels);
- this.dom.labels = labels;
- }
- if (!labels.parentNode || labels.parentNode != labelContainer) {
- if (labels.parentNode) {
- labels.parentNode.removeChild(labels.parentNode);
+ // calculate size of a character
+ var props = this.props,
+ showMinorLabels = this.getOption('showMinorLabels'),
+ showMajorLabels = this.getOption('showMajorLabels'),
+ measureCharMinor = this.dom.measureCharMinor,
+ measureCharMajor = this.dom.measureCharMajor;
+ if (measureCharMinor) {
+ props.minorCharHeight = measureCharMinor.clientHeight;
+ props.minorCharWidth = measureCharMinor.clientWidth;
+ }
+ if (measureCharMajor) {
+ props.majorCharHeight = measureCharMajor.clientHeight;
+ props.majorCharWidth = measureCharMajor.clientWidth;
}
- labelContainer.appendChild(labels);
- }
-
- // reposition frame
- changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
- changed += update(frame.style, 'top', asSize(options.top, '0px'));
- changed += update(frame.style, 'left', asSize(options.left, '0px'));
- changed += update(frame.style, 'width', asSize(options.width, '100%'));
-
- // reposition labels
- changed += update(labels.style, 'top', asSize(options.top, '0px'));
- var me = this,
- queue = this.queue,
- groups = this.groups,
- groupsData = this.groupsData;
+ var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0;
+ if (parentHeight != props.parentHeight) {
+ props.parentHeight = parentHeight;
+ changed += 1;
+ }
+ switch (this.getOption('orientation')) {
+ case 'bottom':
+ props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
+ props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
- // show/hide added/changed/removed groups
- var ids = Object.keys(queue);
- if (ids.length) {
- ids.forEach(function (id) {
- var action = queue[id];
- var group = groups[id];
+ props.minorLabelTop = 0;
+ props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight;
- //noinspection FallthroughInSwitchStatementJS
- switch (action) {
- case 'add':
- case 'update':
- if (!group) {
- var groupOptions = Object.create(me.options);
- group = new Group(me, id, groupOptions);
- group.setItems(me.itemsData); // attach items data
- groups[id] = group;
+ props.minorLineTop = -this.top;
+ props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0);
+ props.minorLineWidth = 1; // TODO: really calculate width
- me.controller.add(group);
- }
+ props.majorLineTop = -this.top;
+ props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0);
+ props.majorLineWidth = 1; // TODO: really calculate width
- // TODO: update group data
- group.data = groupsData.get(id);
+ props.lineTop = 0;
- delete queue[id];
- break;
+ break;
- case 'remove':
- if (group) {
- group.setItems(); // detach items data
- delete groups[id];
+ case 'top':
+ props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
+ props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
- me.controller.remove(group);
- }
+ props.majorLabelTop = 0;
+ props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight;
- // update lists
- delete queue[id];
- break;
+ props.minorLineTop = props.minorLabelTop;
+ props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top);
+ props.minorLineWidth = 1; // TODO: really calculate width
- default:
- console.log('Error: unknown action "' + action + '"');
- }
- });
+ props.majorLineTop = 0;
+ props.majorLineHeight = Math.max(parentHeight - this.top);
+ props.majorLineWidth = 1; // TODO: really calculate width
- // the groupset depends on each of the groups
- //this.depends = this.groups; // TODO: gives a circular reference through the parent
+ props.lineTop = props.majorLabelHeight + props.minorLabelHeight;
- // TODO: apply dependencies of the groupset
+ break;
- // update the top positions of the groups in the correct order
- var orderedGroups = this.groupsData.getIds({
- order: this.options.groupsOrder
- });
- for (i = 0; i < orderedGroups.length; i++) {
- (function (group, prevGroup) {
- var top = 0;
- if (prevGroup) {
- top = function () {
- // TODO: top must reckon with options.maxHeight
- return prevGroup.top + prevGroup.height;
- }
- }
- group.setOptions({
- top: top
- });
- })(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]);
+ default:
+ throw new Error('Unkown orientation "' + this.getOption('orientation') + '"');
}
- // (re)create the labels
- while (labels.firstChild) {
- labels.removeChild(labels.firstChild);
- }
- for (i = 0; i < orderedGroups.length; i++) {
- id = orderedGroups[i];
- label = this._createLabel(id);
- labels.appendChild(label);
- }
+ var height = props.minorLabelHeight + props.majorLabelHeight;
+ changed += update(this, 'width', frame.offsetWidth);
+ changed += update(this, 'height', height);
- changed++;
- }
+ // calculate range and step
+ this._updateConversion();
- // reposition the labels
- // TODO: labels are not displayed correctly when orientation=='top'
- // TODO: width of labelPanel is not immediately updated on a change in groups
- for (id in groups) {
- if (groups.hasOwnProperty(id)) {
- group = groups[id];
- label = group.label;
- if (label) {
- label.style.top = group.top + 'px';
- label.style.height = group.height + 'px';
- }
- }
+ var start = util.convert(range.start, 'Date'),
+ end = util.convert(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);
};
/**
- * Create a label for group with given id
- * @param {Number} id
- * @return {Element} label
+ * 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
*/
-GroupSet.prototype._createLabel = function(id) {
- var group = this.groups[id];
- var label = document.createElement('div');
- label.className = 'label';
- var inner = document.createElement('div');
- inner.className = 'inner';
- label.appendChild(inner);
+TimeAxis.prototype._updateConversion = function() {
+ var range = this.range;
+ if (!range) {
+ throw new Error('No range configured');
+ }
- var content = group.data && group.data.content;
- if (content instanceof Element) {
- inner.appendChild(content);
+ if (range.conversion) {
+ this.conversion = range.conversion(this.width);
}
- else if (content != undefined) {
- inner.innerHTML = content;
+ else {
+ this.conversion = Range.conversion(range.start, range.end, this.width);
}
+};
- var className = group.data && group.data.className;
- if (className) {
- util.addClassName(label, className);
- }
+/**
+ * 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
+ */
+// TODO: improve performance by replacing all Array.forEach with a for loop
+function ItemSet(parent, depends, options) {
+ this.id = util.randomUUID();
+ this.parent = parent;
+ this.depends = depends;
- group.label = label; // TODO: not so nice, parking labels in the group this way!!!
+ // one options object is shared by this itemset and all its items
+ this.options = options || {};
+ this.defaultOptions = {
+ type: 'box',
+ align: 'center',
+ orientation: 'bottom',
+ margin: {
+ axis: 20,
+ item: 10
+ },
+ padding: 5
+ };
- return label;
+ this.dom = {};
+
+ var me = this;
+ this.itemsData = null; // DataSet
+ this.range = null; // Range or Object {start: number, end: number}
+
+ this.listeners = {
+ 'add': function (event, params, senderId) {
+ if (senderId != me.id) {
+ me._onAdd(params.items);
+ }
+ },
+ 'update': function (event, params, senderId) {
+ if (senderId != me.id) {
+ me._onUpdate(params.items);
+ }
+ },
+ 'remove': function (event, params, senderId) {
+ if (senderId != me.id) {
+ me._onRemove(params.items);
+ }
+ }
+ };
+
+ this.items = {}; // object with an Item for every data item
+ this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
+ this.stack = new Stack(this, Object.create(this.options));
+ this.conversion = null;
+
+ // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis
+}
+
+ItemSet.prototype = new Panel();
+
+// available item types will be registered here
+ItemSet.types = {
+ box: ItemBox,
+ range: ItemRange,
+ point: ItemPoint
};
/**
- * Get container element
- * @return {HTMLElement} container
+ * Set options for the ItemSet. Existing options will be extended/overwritten.
+ * @param {Object} [options] The following options are available:
+ * {String | function} [className]
+ * class name for the itemset
+ * {String} [type]
+ * Default type for the items. Choose from 'box'
+ * (default), 'point', or 'range'. The default
+ * Style can be overwritten by individual items.
+ * {String} align
+ * Alignment for the items, only applicable for
+ * ItemBox. Choose 'center' (default), 'left', or
+ * 'right'.
+ * {String} orientation
+ * Orientation of the item set. Choose 'top' or
+ * 'bottom' (default).
+ * {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.
*/
-GroupSet.prototype.getContainer = function getContainer() {
- return this.dom.frame;
-};
+ItemSet.prototype.setOptions = Component.prototype.setOptions;
/**
- * Get the width of the group labels
- * @return {Number} width
+ * Set range (start and end).
+ * @param {Range | Object} range A Range or an object containing start and end.
*/
-GroupSet.prototype.getLabelsWidth = function getContainer() {
- return this.props.labels.width;
+ItemSet.prototype.setRange = function setRange(range) {
+ if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
+ throw new TypeError('Range must be an instance of Range, ' +
+ 'or an object containing start and end.');
+ }
+ this.range = range;
};
/**
- * Reflow the component
- * @return {Boolean} resized
+ * Repaint the component
+ * @return {Boolean} changed
*/
-GroupSet.prototype.reflow = function reflow() {
+ItemSet.prototype.repaint = function repaint() {
var changed = 0,
- id, group,
- options = this.options,
update = util.updateProperty,
- asNumber = util.option.asNumber,
asSize = util.option.asSize,
- frame = this.dom.frame;
+ options = this.options,
+ orientation = this.getOption('orientation'),
+ defaultOptions = this.defaultOptions,
+ frame = this.frame;
- if (frame) {
- var maxHeight = asNumber(options.maxHeight);
- var fixedHeight = (asSize(options.height) != null);
- var height;
- if (fixedHeight) {
- height = frame.offsetHeight;
- }
- else {
- // height is not specified, calculate the sum of the height of all groups
- height = 0;
+ if (!frame) {
+ frame = document.createElement('div');
+ frame.className = 'itemset';
- for (id in this.groups) {
- if (this.groups.hasOwnProperty(id)) {
- group = this.groups[id];
- height += group.height;
- }
- }
- }
- if (maxHeight != null) {
- height = Math.min(height, maxHeight);
+ var className = options.className;
+ if (className) {
+ util.addClassName(frame, util.option.asString(className));
}
- changed += update(this, 'height', height);
- changed += update(this, 'top', frame.offsetTop);
- changed += update(this, 'left', frame.offsetLeft);
- changed += update(this, 'width', frame.offsetWidth);
+ // create background panel
+ var background = document.createElement('div');
+ background.className = 'background';
+ frame.appendChild(background);
+ this.dom.background = background;
+
+ // create foreground panel
+ var foreground = document.createElement('div');
+ foreground.className = 'foreground';
+ frame.appendChild(foreground);
+ this.dom.foreground = foreground;
+
+ // create axis panel
+ var axis = document.createElement('div');
+ axis.className = 'itemset-axis';
+ //frame.appendChild(axis);
+ this.dom.axis = axis;
+
+ this.frame = frame;
+ changed += 1;
}
- // calculate the maximum width of the labels
- var width = 0;
- for (id in this.groups) {
- if (this.groups.hasOwnProperty(id)) {
- group = this.groups[id];
- var labelWidth = group.props && group.props.label && group.props.label.width || 0;
- width = Math.max(width, labelWidth);
- }
+ 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');
}
- changed += update(this.props.labels, 'width', width);
-
- return (changed > 0);
-};
-
-/**
- * Hide the component from the DOM
- * @return {Boolean} changed
- */
-GroupSet.prototype.hide = function hide() {
- if (this.dom.frame && this.dom.frame.parentNode) {
- this.dom.frame.parentNode.removeChild(this.dom.frame);
- return true;
+ if (!frame.parentNode) {
+ parentContainer.appendChild(frame);
+ changed += 1;
}
- else {
- return false;
+ if (!this.dom.axis.parentNode) {
+ parentContainer.appendChild(this.dom.axis);
+ changed += 1;
}
-};
-/**
- * Show the component in the DOM (when not already visible).
- * A repaint will be executed when the component is not visible
- * @return {Boolean} changed
- */
-GroupSet.prototype.show = function show() {
- if (!this.dom.frame || !this.dom.frame.parentNode) {
- return this.repaint();
+ // reposition frame
+ changed += update(frame.style, 'left', asSize(options.left, '0px'));
+ changed += update(frame.style, 'top', asSize(options.top, '0px'));
+ changed += update(frame.style, 'width', asSize(options.width, '100%'));
+ changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
+
+ // reposition axis
+ changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px'));
+ changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%'));
+ if (orientation == 'bottom') {
+ changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px');
}
- else {
- return false;
+ else { // orientation == 'top'
+ changed += update(this.dom.axis.style, 'top', this.top + 'px');
}
-};
-
-/**
- * Handle updated groups
- * @param {Number[]} ids
- * @private
- */
-GroupSet.prototype._onUpdate = function _onUpdate(ids) {
- this._toQueue(ids, 'update');
-};
-/**
- * Handle changed groups
- * @param {Number[]} ids
- * @private
- */
-GroupSet.prototype._onAdd = function _onAdd(ids) {
- this._toQueue(ids, 'add');
-};
+ this._updateConversion();
-/**
- * Handle removed groups
- * @param {Number[]} ids
- * @private
- */
-GroupSet.prototype._onRemove = function _onRemove(ids) {
- this._toQueue(ids, 'remove');
-};
+ var me = this,
+ queue = this.queue,
+ itemsData = this.itemsData,
+ items = this.items,
+ dataOptions = {
+ // TODO: cleanup
+ // fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type', 'className']
+ };
-/**
- * Put groups in the queue to be added/updated/remove
- * @param {Number[]} ids
- * @param {String} action can be 'add', 'update', 'remove'
- */
-GroupSet.prototype._toQueue = function _toQueue(ids, action) {
- var queue = this.queue;
- ids.forEach(function (id) {
- queue[id] = action;
- });
+ // show/hide added/changed/removed items
+ Object.keys(queue).forEach(function (id) {
+ //var entry = queue[id];
+ var action = queue[id];
+ var item = items[id];
+ //var item = entry.item;
+ //noinspection FallthroughInSwitchStatementJS
+ switch (action) {
+ case 'add':
+ case 'update':
+ var itemData = itemsData && itemsData.get(id, dataOptions);
- if (this.controller) {
- //this.requestReflow();
- this.requestRepaint();
- }
-};
+ if (itemData) {
+ var type = itemData.type ||
+ (itemData.start && itemData.end && 'range') ||
+ options.type ||
+ 'box';
+ var constructor = ItemSet.types[type];
-/**
- * Create a timeline visualization
- * @param {HTMLElement} container
- * @param {vis.DataSet | Array | DataTable} [items]
- * @param {Object} [options] See Timeline.setOptions for the available options.
- * @constructor
- */
-function Timeline (container, items, options) {
- var me = this;
- this.options = util.extend({
- orientation: 'bottom',
- min: null,
- max: null,
- zoomMin: 10, // milliseconds
- zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
- // moveable: true, // TODO: option moveable
- // zoomable: true, // TODO: option zoomable
- showMinorLabels: true,
- showMajorLabels: true,
- autoResize: false
- }, options);
+ // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
+ if (item) {
+ // update item
+ if (!constructor || !(item instanceof constructor)) {
+ // item type has changed, hide and delete the item
+ changed += item.hide();
+ item = null;
+ }
+ else {
+ item.data = itemData; // TODO: create a method item.setData ?
+ changed++;
+ }
+ }
- // controller
- this.controller = new Controller();
+ if (!item) {
+ // create item
+ if (constructor) {
+ item = new constructor(me, itemData, options, defaultOptions);
+ changed++;
+ }
+ else {
+ throw new TypeError('Unknown item type "' + type + '"');
+ }
+ }
- // root panel
- if (!container) {
- throw new Error('No container element provided');
- }
- var rootOptions = Object.create(this.options);
- rootOptions.height = function () {
- if (me.options.height) {
- // fixed height
- return me.options.height;
- }
- else {
- // auto height
- return me.timeaxis.height + me.content.height;
- }
- };
- this.rootPanel = new RootPanel(container, rootOptions);
- this.controller.add(this.rootPanel);
+ // force a repaint (not only a reposition)
+ item.repaint();
- // item panel
- var itemOptions = Object.create(this.options);
- itemOptions.left = function () {
- return me.labelPanel.width;
- };
- itemOptions.width = function () {
- return me.rootPanel.width - me.labelPanel.width;
- };
- itemOptions.top = null;
- itemOptions.height = null;
- this.itemPanel = new Panel(this.rootPanel, [], itemOptions);
- this.controller.add(this.itemPanel);
+ items[id] = item;
+ }
- // label panel
- var labelOptions = Object.create(this.options);
- labelOptions.top = null;
- labelOptions.left = null;
- labelOptions.height = null;
- labelOptions.width = function () {
- if (me.content && typeof me.content.getLabelsWidth === 'function') {
- return me.content.getLabelsWidth();
- }
- else {
- return 0;
- }
- };
- this.labelPanel = new Panel(this.rootPanel, [], labelOptions);
- this.controller.add(this.labelPanel);
+ // update queue
+ delete queue[id];
+ break;
- // range
- var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
- this.range = new Range({
- start: now.clone().add('days', -3).valueOf(),
- end: now.clone().add('days', 4).valueOf()
- });
- // TODO: reckon with options moveable and zoomable
- this.range.subscribe(this.rootPanel, 'move', 'horizontal');
- this.range.subscribe(this.rootPanel, 'zoom', 'horizontal');
- this.range.on('rangechange', function () {
- var force = true;
- me.controller.requestReflow(force);
- });
- this.range.on('rangechanged', function () {
- var force = true;
- me.controller.requestReflow(force);
- });
+ case 'remove':
+ if (item) {
+ // remove DOM of the item
+ changed += item.hide();
+ }
- // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable
+ // update lists
+ delete items[id];
+ delete queue[id];
+ break;
- // time axis
- var timeaxisOptions = Object.create(rootOptions);
- timeaxisOptions.range = this.range;
- timeaxisOptions.left = null;
- timeaxisOptions.top = null;
- timeaxisOptions.width = '100%';
- timeaxisOptions.height = null;
- this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions);
- this.timeaxis.setRange(this.range);
- this.controller.add(this.timeaxis);
+ default:
+ console.log('Error: unknown action "' + action + '"');
+ }
+ });
- // create itemset or groupset
- this.setGroups(null);
+ // reposition all items. Show items only when in the visible area
+ util.forEach(this.items, function (item) {
+ if (item.visible) {
+ changed += item.show();
+ item.reposition();
+ }
+ else {
+ changed += item.hide();
+ }
+ });
- this.itemsData = null; // DataSet
- this.groupsData = null; // DataSet
+ return (changed > 0);
+};
- // set data
- if (items) {
- this.setItems(items);
- }
-}
+/**
+ * Get the foreground container element
+ * @return {HTMLElement} foreground
+ */
+ItemSet.prototype.getForeground = function getForeground() {
+ return this.dom.foreground;
+};
/**
- * Set options
- * @param {Object} options TODO: describe the available options
+ * Get the background container element
+ * @return {HTMLElement} background
*/
-Timeline.prototype.setOptions = function (options) {
- if (options) {
- util.extend(this.options, options);
- }
+ItemSet.prototype.getBackground = function getBackground() {
+ return this.dom.background;
+};
- this.controller.reflow();
- this.controller.repaint();
+/**
+ * Get the axis container element
+ * @return {HTMLElement} axis
+ */
+ItemSet.prototype.getAxis = function getAxis() {
+ return this.dom.axis;
};
/**
- * Set items
- * @param {vis.DataSet | Array | DataTable | null} items
+ * Reflow the component
+ * @return {Boolean} resized
*/
-Timeline.prototype.setItems = function(items) {
- var initialLoad = (this.itemsData == null);
+ItemSet.prototype.reflow = function reflow () {
+ var changed = 0,
+ options = this.options,
+ marginAxis = options.margin && options.margin.axis || this.defaultOptions.margin.axis,
+ marginItem = options.margin && options.margin.item || this.defaultOptions.margin.item,
+ update = util.updateProperty,
+ asNumber = util.option.asNumber,
+ asSize = util.option.asSize,
+ frame = this.frame;
- // convert to type DataSet when needed
- var newItemSet;
- if (!items) {
- newItemSet = null;
- }
- else if (items instanceof DataSet) {
- newItemSet = items;
- }
- if (!(items instanceof DataSet)) {
- newItemSet = new DataSet({
- convert: {
- start: 'Date',
- end: 'Date'
- }
- });
- newItemSet.add(items);
- }
+ if (frame) {
+ this._updateConversion();
- // set items
- this.itemsData = newItemSet;
- this.content.setItems(newItemSet);
+ util.forEach(this.items, function (item) {
+ changed += item.reflow();
+ });
- if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
- // apply the data range as range
- var dataRange = this.getItemRange();
+ // 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();
- // add 5% on both sides
- var min = dataRange.min;
- var max = dataRange.max;
- if (min != null && max != null) {
- var interval = (max.valueOf() - min.valueOf());
- min = new Date(min.valueOf() - interval * 0.05);
- max = new Date(max.valueOf() + interval * 0.05);
+ var maxHeight = asNumber(options.maxHeight);
+ var fixedHeight = (asSize(options.height) != null);
+ var height;
+ if (fixedHeight) {
+ height = frame.offsetHeight;
}
-
- // override specified start and/or end date
- if (this.options.start != undefined) {
- min = new Date(this.options.start.valueOf());
+ else {
+ // height is not specified, determine the height from the height and positioned items
+ var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items
+ if (visibleItems.length) {
+ var min = visibleItems[0].top;
+ var max = visibleItems[0].top + visibleItems[0].height;
+ util.forEach(visibleItems, function (item) {
+ min = Math.min(min, item.top);
+ max = Math.max(max, (item.top + item.height));
+ });
+ height = (max - min) + marginAxis + marginItem;
+ }
+ else {
+ height = marginAxis + marginItem;
+ }
}
- if (this.options.end != undefined) {
- max = new Date(this.options.end.valueOf());
+ if (maxHeight != null) {
+ height = Math.min(height, maxHeight);
}
+ changed += update(this, 'height', height);
- // apply range if there is a min or max available
- if (min != null || max != null) {
- this.range.setRange(min, max);
- }
+ // 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 groups
- * @param {vis.DataSet | Array | DataTable} groups
+ * Hide this component from the DOM
+ * @return {Boolean} changed
*/
-Timeline.prototype.setGroups = function(groups) {
- var me = this;
- this.groupsData = groups;
-
- // switch content type between ItemSet or GroupSet when needed
- var type = this.groupsData ? GroupSet : ItemSet;
- if (!(this.content instanceof type)) {
- // remove old content set
- if (this.content) {
- this.content.hide();
- if (this.content.setItems) {
- this.content.setItems(); // disconnect from items
- }
- if (this.content.setGroups) {
- this.content.setGroups(); // disconnect from groups
- }
- this.controller.remove(this.content);
- }
+ItemSet.prototype.hide = function hide() {
+ var changed = false;
- // create new content set
- var options = Object.create(this.options);
- util.extend(options, {
- top: function () {
- if (me.options.orientation == 'top') {
- return me.timeaxis.height;
- }
- else {
- return me.itemPanel.height - me.timeaxis.height - me.content.height;
- }
- },
- left: null,
- width: '100%',
- height: function () {
- if (me.options.height) {
- return me.itemPanel.height - me.timeaxis.height;
- }
- else {
- return null;
- }
- },
- maxHeight: function () {
- if (me.options.maxHeight) {
- if (!util.isNumber(me.options.maxHeight)) {
- throw new TypeError('Number expected for property maxHeight');
- }
- return me.options.maxHeight - me.timeaxis.height;
- }
- else {
- return null;
- }
- },
- labelContainer: function () {
- return me.labelPanel.getContainer();
- }
- });
- this.content = new type(this.itemPanel, [this.timeaxis], options);
- if (this.content.setRange) {
- this.content.setRange(this.range);
- }
- if (this.content.setItems) {
- this.content.setItems(this.itemsData);
- }
- if (this.content.setGroups) {
- this.content.setGroups(this.groupsData);
- }
- this.controller.add(this.content);
+ // remove the DOM
+ if (this.frame && this.frame.parentNode) {
+ this.frame.parentNode.removeChild(this.frame);
+ changed = true;
+ }
+ if (this.dom.axis && this.dom.axis.parentNode) {
+ this.dom.axis.parentNode.removeChild(this.dom.axis);
+ changed = true;
}
+
+ return changed;
};
/**
- * Get the data range of the item set.
- * @returns {{min: Date, max: Date}} range A range with a start and end Date.
- * When no minimum is found, min==null
- * When no maximum is found, max==null
+ * Set items
+ * @param {vis.DataSet | null} items
*/
-Timeline.prototype.getItemRange = function getItemRange() {
- // calculate min from start filed
- var itemsData = this.itemsData,
- min = null,
- max = null;
-
- if (itemsData) {
- // calculate the minimum value of the field 'start'
- var minItem = itemsData.min('start');
- min = minItem ? minItem.start.valueOf() : null;
+ItemSet.prototype.setItems = function setItems(items) {
+ var me = this,
+ ids,
+ oldItemsData = this.itemsData;
- // calculate maximum value of fields 'start' and 'end'
- var maxStartItem = itemsData.max('start');
- if (maxStartItem) {
- max = maxStartItem.start.valueOf();
- }
- var maxEndItem = itemsData.max('end');
- if (maxEndItem) {
- if (max == null) {
- max = maxEndItem.end.valueOf();
- }
- else {
- max = Math.max(max, maxEndItem.end.valueOf());
- }
- }
+ // replace the dataset
+ if (!items) {
+ this.itemsData = null;
+ }
+ else if (items instanceof DataSet || items instanceof DataView) {
+ this.itemsData = items;
+ }
+ else {
+ throw new TypeError('Data must be an instance of DataSet');
}
- return {
- min: (min != null) ? new Date(min) : null,
- max: (max != null) ? new Date(max) : null
- };
-};
+ if (oldItemsData) {
+ // unsubscribe from old dataset
+ util.forEach(this.listeners, function (callback, event) {
+ oldItemsData.unsubscribe(event, callback);
+ });
-(function(exports) {
- /**
- * Parse a text source containing data in DOT language into a JSON object.
- * The object contains two lists: one with nodes and one with edges.
- *
- * DOT language reference: http://www.graphviz.org/doc/info/lang.html
- *
- * @param {String} data Text containing a graph in DOT-notation
- * @return {Object} graph An object containing two parameters:
- * {Object[]} nodes
- * {Object[]} edges
- */
- function parseDOT (data) {
- dot = data;
- return parseGraph();
+ // remove all drawn items
+ ids = oldItemsData.getIds();
+ this._onRemove(ids);
}
- // token types enumeration
- var TOKENTYPE = {
- NULL : 0,
- DELIMITER : 1,
- IDENTIFIER: 2,
- UNKNOWN : 3
- };
+ if (this.itemsData) {
+ // subscribe to new dataset
+ var id = this.id;
+ util.forEach(this.listeners, function (callback, event) {
+ me.itemsData.subscribe(event, callback, id);
+ });
- // map with all delimiters
- var DELIMITERS = {
- '{': true,
- '}': true,
- '[': true,
- ']': true,
- ';': true,
- '=': true,
- ',': true,
+ // draw all new items
+ ids = this.itemsData.getIds();
+ this._onAdd(ids);
+ }
+};
- '->': true,
- '--': true
- };
+/**
+ * Get the current items items
+ * @returns {vis.DataSet | null}
+ */
+ItemSet.prototype.getItems = function getItems() {
+ return this.itemsData;
+};
- var dot = ''; // current dot file
- var index = 0; // current index in dot file
- var c = ''; // current token character in expr
- var token = ''; // current token
- var tokenType = TOKENTYPE.NULL; // type of the token
+/**
+ * Handle updated items
+ * @param {Number[]} ids
+ * @private
+ */
+ItemSet.prototype._onUpdate = function _onUpdate(ids) {
+ this._toQueue('update', ids);
+};
- /**
- * Get the first character from the dot file.
- * The character is stored into the char c. If the end of the dot file is
- * reached, the function puts an empty string in c.
- */
- function first() {
- index = 0;
- c = dot.charAt(0);
- }
+/**
+ * Handle changed items
+ * @param {Number[]} ids
+ * @private
+ */
+ItemSet.prototype._onAdd = function _onAdd(ids) {
+ this._toQueue('add', ids);
+};
- /**
- * Get the next character from the dot file.
- * The character is stored into the char c. If the end of the dot file is
- * reached, the function puts an empty string in c.
- */
- function next() {
- index++;
- c = dot.charAt(index);
- }
+/**
+ * Handle removed items
+ * @param {Number[]} ids
+ * @private
+ */
+ItemSet.prototype._onRemove = function _onRemove(ids) {
+ this._toQueue('remove', ids);
+};
- /**
- * Preview the next character from the dot file.
- * @return {String} cNext
- */
- function nextPreview() {
- return dot.charAt(index + 1);
- }
+/**
+ * Put items in the queue to be added/updated/remove
+ * @param {String} action can be 'add', 'update', 'remove'
+ * @param {Number[]} ids
+ */
+ItemSet.prototype._toQueue = function _toQueue(action, ids) {
+ var queue = this.queue;
+ ids.forEach(function (id) {
+ queue[id] = action;
+ });
- /**
- * Test whether given character is alphabetic or numeric
- * @param {String} c
- * @return {Boolean} isAlphaNumeric
- */
- var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
- function isAlphaNumeric(c) {
- return regexAlphaNumeric.test(c);
+ if (this.controller) {
+ //this.requestReflow();
+ this.requestRepaint();
}
+};
- /**
- * Merge all properties of object b into object b
- * @param {Object} a
- * @param {Object} b
- * @return {Object} a
- */
- function merge (a, b) {
- if (!a) {
- a = {};
- }
-
- if (b) {
- for (var name in b) {
- if (b.hasOwnProperty(name)) {
- a[name] = b[name];
- }
- }
- }
- return a;
+/**
+ * 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 _updateConversion() {
+ var range = this.range;
+ if (!range) {
+ throw new Error('No range configured');
}
- /**
- * Set a value in an object, where the provided parameter name can be a
- * path with nested parameters. For example:
- *
- * var obj = {a: 2};
- * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
- *
- * @param {Object} obj
- * @param {String} path A parameter name or dot-separated parameter path,
- * like "color.highlight.border".
- * @param {*} value
- */
- function setValue(obj, path, value) {
- var keys = path.split('.');
- var o = obj;
- while (keys.length) {
- var key = keys.shift();
- if (keys.length) {
- // this isn't the end point
- if (!o[key]) {
- o[key] = {};
- }
- o = o[key];
- }
- else {
- // this is the end point
- o[key] = value;
- }
- }
+ if (range.conversion) {
+ this.conversion = range.conversion(this.width);
+ }
+ else {
+ this.conversion = Range.conversion(range.start, range.end, this.width);
}
+};
- /**
- * Add a node to a graph object. If there is already a node with
- * the same id, their attributes will be merged.
- * @param {Object} graph
- * @param {Object} node
- */
- function addNode(graph, node) {
- var i, len;
- var current = null;
+/**
+ * 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 toTime(x) {
+ var conversion = this.conversion;
+ return new Date(x / conversion.factor + conversion.offset);
+};
- // find root graph (in case of subgraph)
- var graphs = [graph]; // list with all graphs from current graph to root graph
- var root = graph;
- while (root.parent) {
- graphs.push(root.parent);
- root = root.parent;
- }
+/**
+ * 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 toScreen(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
+ * @param {Object} [defaultOptions] default options
+ * // TODO: describe available options
+ */
+function Item (parent, data, options, defaultOptions) {
+ this.parent = parent;
+ this.data = data;
+ this.dom = null;
+ this.options = options || {};
+ this.defaultOptions = defaultOptions || {};
- // find existing node (at root level) by its id
- if (root.nodes) {
- for (i = 0, len = root.nodes.length; i < len; i++) {
- if (node.id === root.nodes[i].id) {
- current = root.nodes[i];
- break;
- }
- }
- }
+ this.selected = false;
+ this.visible = false;
+ this.top = 0;
+ this.left = 0;
+ this.width = 0;
+ this.height = 0;
+}
- if (!current) {
- // this is a new node
- current = {
- id: node.id
- };
- if (graph.node) {
- // clone default attributes
- current.attr = merge(current.attr, graph.node);
- }
- }
+/**
+ * Select current item
+ */
+Item.prototype.select = function select() {
+ this.selected = true;
+};
- // add node to this (sub)graph and all its parent graphs
- for (i = graphs.length - 1; i >= 0; i--) {
- var g = graphs[i];
+/**
+ * Unselect current item
+ */
+Item.prototype.unselect = function unselect() {
+ this.selected = false;
+};
- if (!g.nodes) {
- g.nodes = [];
- }
- if (g.nodes.indexOf(current) == -1) {
- g.nodes.push(current);
- }
- }
+/**
+ * Show the Item in the DOM (when not already visible)
+ * @return {Boolean} changed
+ */
+Item.prototype.show = function show() {
+ return false;
+};
- // merge attributes
- if (node.attr) {
- current.attr = merge(current.attr, node.attr);
- }
- }
+/**
+ * Hide the Item from the DOM (when visible)
+ * @return {Boolean} changed
+ */
+Item.prototype.hide = function hide() {
+ return false;
+};
- /**
- * Add an edge to a graph object
- * @param {Object} graph
- * @param {Object} edge
- */
- function addEdge(graph, edge) {
- if (!graph.edges) {
- graph.edges = [];
- }
- graph.edges.push(edge);
- if (graph.edge) {
- var attr = merge({}, graph.edge); // clone default attributes
- edge.attr = merge(attr, edge.attr); // merge attributes
- }
- }
+/**
+ * Repaint the item
+ * @return {Boolean} changed
+ */
+Item.prototype.repaint = function repaint() {
+ // should be implemented by the item
+ return false;
+};
- /**
- * Create an edge to a graph object
- * @param {Object} graph
- * @param {String | Number | Object} from
- * @param {String | Number | Object} to
- * @param {String} type
- * @param {Object | null} attr
- * @return {Object} edge
- */
- function createEdge(graph, from, to, type, attr) {
- var edge = {
- from: from,
- to: to,
- type: type
- };
+/**
+ * Reflow the item
+ * @return {Boolean} resized
+ */
+Item.prototype.reflow = function reflow() {
+ // should be implemented by the item
+ return false;
+};
- if (graph.edge) {
- edge.attr = merge({}, graph.edge); // clone default attributes
+/**
+ * @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
+ * @param {Object} [defaultOptions] default options
+ * // TODO: describe available options
+ */
+function ItemBox (parent, data, options, defaultOptions) {
+ this.props = {
+ dot: {
+ left: 0,
+ top: 0,
+ width: 0,
+ height: 0
+ },
+ line: {
+ top: 0,
+ left: 0,
+ width: 0,
+ height: 0
}
- edge.attr = merge(edge.attr || {}, attr); // merge attributes
+ };
- return edge;
- }
+ Item.call(this, parent, data, options, defaultOptions);
+}
- /**
- * Get next token in the current dot file.
- * The token and token type are available as token and tokenType
- */
- function getToken() {
- tokenType = TOKENTYPE.NULL;
- token = '';
+ItemBox.prototype = new Item (null, null);
- // skip over whitespaces
- while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
- next();
- }
+/**
+ * Select the item
+ * @override
+ */
+ItemBox.prototype.select = function select() {
+ this.selected = true;
+ // TODO: select and unselect
+};
- do {
- var isComment = false;
+/**
+ * Unselect the item
+ * @override
+ */
+ItemBox.prototype.unselect = function unselect() {
+ this.selected = false;
+ // TODO: select and unselect
+};
- // skip comment
- if (c == '#') {
- // find the previous non-space character
- var i = index - 1;
- while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
- i--;
- }
- if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
- // the # is at the start of a line, this is indeed a line comment
- while (c != '' && c != '\n') {
- next();
- }
- isComment = true;
- }
- }
- if (c == '/' && nextPreview() == '/') {
- // skip line comment
- while (c != '' && c != '\n') {
- next();
- }
- isComment = true;
- }
- if (c == '/' && nextPreview() == '*') {
- // skip block comment
- while (c != '') {
- if (c == '*' && nextPreview() == '/') {
- // end of block comment found. skip these last two characters
- next();
- next();
- break;
- }
- else {
- next();
- }
- }
- isComment = true;
- }
+/**
+ * Repaint the item
+ * @return {Boolean} changed
+ */
+ItemBox.prototype.repaint = function repaint() {
+ // TODO: make an efficient repaint
+ var changed = false;
+ var dom = this.dom;
- // skip over whitespaces
- while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
- next();
- }
- }
- while (isComment);
+ if (!dom) {
+ this._create();
+ dom = this.dom;
+ changed = true;
+ }
- // check for end of dot file
- if (c == '') {
- // token is still empty
- tokenType = TOKENTYPE.DELIMITER;
- return;
+ if (dom) {
+ if (!this.parent) {
+ throw new Error('Cannot repaint item: no parent attached');
+ }
+ var foreground = this.parent.getForeground();
+ if (!foreground) {
+ throw new Error('Cannot repaint time axis: ' +
+ 'parent has no foreground container element');
+ }
+ var background = this.parent.getBackground();
+ if (!background) {
+ throw new Error('Cannot repaint time axis: ' +
+ 'parent has no background container element');
}
-
- // check for delimiters consisting of 2 characters
- var c2 = c + nextPreview();
- if (DELIMITERS[c2]) {
- tokenType = TOKENTYPE.DELIMITER;
- token = c2;
- next();
- next();
- return;
+ var axis = this.parent.getAxis();
+ if (!background) {
+ throw new Error('Cannot repaint time axis: ' +
+ 'parent has no axis container element');
}
- // check for delimiters consisting of 1 character
- if (DELIMITERS[c]) {
- tokenType = TOKENTYPE.DELIMITER;
- token = c;
- next();
- return;
+ if (!dom.box.parentNode) {
+ foreground.appendChild(dom.box);
+ changed = true;
}
-
- // check for an identifier (number or string)
- // TODO: more precise parsing of numbers/strings (and the port separator ':')
- if (isAlphaNumeric(c) || c == '-') {
- token += c;
- next();
-
- while (isAlphaNumeric(c)) {
- token += c;
- next();
- }
- if (token == 'false') {
- token = false; // convert to boolean
- }
- else if (token == 'true') {
- token = true; // convert to boolean
- }
- else if (!isNaN(Number(token))) {
- token = Number(token); // convert to number
- }
- tokenType = TOKENTYPE.IDENTIFIER;
- return;
+ if (!dom.line.parentNode) {
+ background.appendChild(dom.line);
+ changed = true;
+ }
+ if (!dom.dot.parentNode) {
+ axis.appendChild(dom.dot);
+ changed = true;
}
- // check for a string enclosed by double quotes
- if (c == '"') {
- next();
- while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
- token += c;
- if (c == '"') { // skip the escape character
- next();
- }
- next();
+ // update contents
+ if (this.data.content != this.content) {
+ this.content = this.data.content;
+ if (this.content instanceof Element) {
+ dom.content.innerHTML = '';
+ dom.content.appendChild(this.content);
}
- if (c != '"') {
- throw newSyntaxError('End of string " expected');
+ else if (this.data.content != undefined) {
+ dom.content.innerHTML = this.content;
}
- next();
- tokenType = TOKENTYPE.IDENTIFIER;
- return;
+ else {
+ throw new Error('Property "content" missing in item ' + this.data.id);
+ }
+ changed = true;
}
- // something unknown is found, wrong characters, a syntax error
- tokenType = TOKENTYPE.UNKNOWN;
- while (c != '') {
- token += c;
- next();
+ // 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;
}
- throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
}
- /**
- * Parse a graph.
- * @returns {Object} graph
- */
- function parseGraph() {
- var graph = {};
-
- first();
- getToken();
-
- // optional strict keyword
- if (token == 'strict') {
- graph.strict = true;
- getToken();
- }
-
- // graph or digraph keyword
- if (token == 'graph' || token == 'digraph') {
- graph.type = token;
- getToken();
- }
+ return changed;
+};
- // optional graph id
- if (tokenType == TOKENTYPE.IDENTIFIER) {
- graph.id = token;
- getToken();
- }
+/**
+ * Show the item in the DOM (when not already visible). The items DOM will
+ * be created when needed.
+ * @return {Boolean} changed
+ */
+ItemBox.prototype.show = function show() {
+ if (!this.dom || !this.dom.box.parentNode) {
+ return this.repaint();
+ }
+ else {
+ return false;
+ }
+};
- // open angle bracket
- if (token != '{') {
- throw newSyntaxError('Angle bracket { expected');
+/**
+ * Hide the item from the DOM (when visible)
+ * @return {Boolean} changed
+ */
+ItemBox.prototype.hide = function hide() {
+ var changed = false,
+ dom = this.dom;
+ if (dom) {
+ if (dom.box.parentNode) {
+ dom.box.parentNode.removeChild(dom.box);
+ changed = true;
}
- getToken();
-
- // statements
- parseStatements(graph);
-
- // close angle bracket
- if (token != '}') {
- throw newSyntaxError('Angle bracket } expected');
+ if (dom.line.parentNode) {
+ dom.line.parentNode.removeChild(dom.line);
}
- getToken();
-
- // end of file
- if (token !== '') {
- throw newSyntaxError('End of file expected');
+ if (dom.dot.parentNode) {
+ dom.dot.parentNode.removeChild(dom.dot);
}
- getToken();
+ }
+ return changed;
+};
- // remove temporary default properties
- delete graph.node;
- delete graph.edge;
- delete graph.graph;
+/**
+ * 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 reflow() {
+ var changed = 0,
+ update,
+ dom,
+ props,
+ options,
+ margin,
+ start,
+ align,
+ orientation,
+ top,
+ left,
+ data,
+ range;
- return graph;
+ if (this.data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + this.data.id);
}
- /**
- * Parse a list with statements.
- * @param {Object} graph
- */
- function parseStatements (graph) {
- while (token !== '' && token != '}') {
- parseStatement(graph);
- if (token == ';') {
- getToken();
- }
- }
+ data = this.data;
+ range = this.parent && this.parent.range;
+ if (data && range) {
+ // TODO: account for the width of the item
+ var interval = (range.end - range.start);
+ this.visible = (data.start > range.start - interval) && (data.start < range.end + interval);
+ }
+ else {
+ this.visible = false;
}
- /**
- * Parse a single statement. Can be a an attribute statement, node
- * statement, a series of node statements and edge statements, or a
- * parameter.
- * @param {Object} graph
- */
- function parseStatement(graph) {
- // parse subgraph
- var subgraph = parseSubgraph(graph);
- if (subgraph) {
- // edge statements
- parseEdge(graph, subgraph);
+ if (this.visible) {
+ dom = this.dom;
+ if (dom) {
+ update = util.updateProperty;
+ props = this.props;
+ options = this.options;
+ start = this.parent.toScreen(this.data.start);
+ align = options.align || this.defaultOptions.align;
+ margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
+ orientation = options.orientation || this.defaultOptions.orientation;
- return;
- }
+ changed += update(props.dot, 'height', dom.dot.offsetHeight);
+ changed += update(props.dot, 'width', dom.dot.offsetWidth);
+ changed += update(props.line, 'width', dom.line.offsetWidth);
+ changed += update(props.line, 'height', dom.line.offsetHeight);
+ changed += update(props.line, 'top', dom.line.offsetTop);
+ changed += update(this, 'width', dom.box.offsetWidth);
+ changed += update(this, 'height', dom.box.offsetHeight);
+ if (align == 'right') {
+ left = start - this.width;
+ }
+ else if (align == 'left') {
+ left = start;
+ }
+ else {
+ // default or 'center'
+ left = start - this.width / 2;
+ }
+ changed += update(this, 'left', left);
- // parse an attribute statement
- var attr = parseAttributeStatement(graph);
- if (attr) {
- return;
- }
+ changed += update(props.line, 'left', start - props.line.width / 2);
+ changed += update(props.dot, 'left', start - props.dot.width / 2);
+ changed += update(props.dot, 'top', -props.dot.height / 2);
+ if (orientation == 'top') {
+ top = margin;
- // parse node
- if (tokenType != TOKENTYPE.IDENTIFIER) {
- throw newSyntaxError('Identifier expected');
- }
- var id = token; // id can be a string or a number
- getToken();
+ changed += update(this, 'top', top);
+ }
+ else {
+ // default or 'bottom'
+ var parentHeight = this.parent.height;
+ top = parentHeight - this.height - margin;
- if (token == '=') {
- // id statement
- getToken();
- if (tokenType != TOKENTYPE.IDENTIFIER) {
- throw newSyntaxError('Identifier expected');
+ changed += update(this, 'top', top);
}
- graph[id] = token;
- getToken();
- // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
}
else {
- parseNodeStatement(graph, id);
+ changed += 1;
}
}
- /**
- * Parse a subgraph
- * @param {Object} graph parent graph object
- * @return {Object | null} subgraph
- */
- function parseSubgraph (graph) {
- var subgraph = null;
-
- // optional subgraph keyword
- if (token == 'subgraph') {
- subgraph = {};
- subgraph.type = 'subgraph';
- getToken();
-
- // optional graph id
- if (tokenType == TOKENTYPE.IDENTIFIER) {
- subgraph.id = token;
- getToken();
- }
- }
-
- // open angle bracket
- if (token == '{') {
- getToken();
-
- if (!subgraph) {
- subgraph = {};
- }
- subgraph.parent = graph;
- subgraph.node = graph.node;
- subgraph.edge = graph.edge;
- subgraph.graph = graph.graph;
+ return (changed > 0);
+};
- // statements
- parseStatements(subgraph);
+/**
+ * Create an items DOM
+ * @private
+ */
+ItemBox.prototype._create = function _create() {
+ var dom = this.dom;
+ if (!dom) {
+ this.dom = dom = {};
- // close angle bracket
- if (token != '}') {
- throw newSyntaxError('Angle bracket } expected');
- }
- getToken();
+ // create the box
+ dom.box = document.createElement('DIV');
+ // className is updated in repaint()
- // remove temporary default properties
- delete subgraph.node;
- delete subgraph.edge;
- delete subgraph.graph;
- delete subgraph.parent;
+ // contents box (inside the background box). used for making margins
+ dom.content = document.createElement('DIV');
+ dom.content.className = 'content';
+ dom.box.appendChild(dom.content);
- // register at the parent graph
- if (!graph.subgraphs) {
- graph.subgraphs = [];
- }
- graph.subgraphs.push(subgraph);
- }
+ // line to axis
+ dom.line = document.createElement('DIV');
+ dom.line.className = 'line';
- return subgraph;
+ // dot on axis
+ dom.dot = document.createElement('DIV');
+ dom.dot.className = 'dot';
}
+};
- /**
- * parse an attribute statement like "node [shape=circle fontSize=16]".
- * Available keywords are 'node', 'edge', 'graph'.
- * The previous list with default attributes will be replaced
- * @param {Object} graph
- * @returns {String | null} keyword Returns the name of the parsed attribute
- * (node, edge, graph), or null if nothing
- * is parsed.
- */
- function parseAttributeStatement (graph) {
- // attribute statements
- if (token == 'node') {
- getToken();
+/**
+ * Reposition the item, recalculate its left, top, and width, using the current
+ * range and size of the items itemset
+ * @override
+ */
+ItemBox.prototype.reposition = function reposition() {
+ var dom = this.dom,
+ props = this.props,
+ orientation = this.options.orientation || this.defaultOptions.orientation;
- // node attributes
- graph.node = parseAttributeList();
- return 'node';
- }
- else if (token == 'edge') {
- getToken();
+ if (dom) {
+ var box = dom.box,
+ line = dom.line,
+ dot = dom.dot;
- // edge attributes
- graph.edge = parseAttributeList();
- return 'edge';
- }
- else if (token == 'graph') {
- getToken();
+ box.style.left = this.left + 'px';
+ box.style.top = this.top + 'px';
- // graph attributes
- graph.graph = parseAttributeList();
- return 'graph';
+ line.style.left = props.line.left + 'px';
+ if (orientation == 'top') {
+ line.style.top = 0 + 'px';
+ line.style.height = this.top + 'px';
}
-
- return null;
- }
-
- /**
- * parse a node statement
- * @param {Object} graph
- * @param {String | Number} id
- */
- function parseNodeStatement(graph, id) {
- // node statement
- var node = {
- id: id
- };
- var attr = parseAttributeList();
- if (attr) {
- node.attr = attr;
+ else {
+ // orientation 'bottom'
+ line.style.top = (this.top + this.height) + 'px';
+ line.style.height = Math.max(this.parent.height - this.top - this.height +
+ this.props.dot.height / 2, 0) + 'px';
}
- addNode(graph, node);
- // edge statements
- parseEdge(graph, id);
+ dot.style.left = props.dot.left + 'px';
+ dot.style.top = props.dot.top + 'px';
}
+};
- /**
- * Parse an edge or a series of edges
- * @param {Object} graph
- * @param {String | Number} from Id of the from node
- */
- function parseEdge(graph, from) {
- while (token == '->' || token == '--') {
- var to;
- var type = token;
- getToken();
+/**
+ * @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
+ * @param {Object} [defaultOptions] default options
+ * // TODO: describe available options
+ */
+function ItemPoint (parent, data, options, defaultOptions) {
+ this.props = {
+ dot: {
+ top: 0,
+ width: 0,
+ height: 0
+ },
+ content: {
+ height: 0,
+ marginLeft: 0
+ }
+ };
- var subgraph = parseSubgraph(graph);
- if (subgraph) {
- to = subgraph;
- }
- else {
- if (tokenType != TOKENTYPE.IDENTIFIER) {
- throw newSyntaxError('Identifier or subgraph expected');
- }
- to = token;
- addNode(graph, {
- id: to
- });
- getToken();
- }
+ Item.call(this, parent, data, options, defaultOptions);
+}
- // parse edge attributes
- var attr = parseAttributeList();
+ItemPoint.prototype = new Item (null, null);
- // create edge
- var edge = createEdge(graph, from, to, type, attr);
- addEdge(graph, edge);
+/**
+ * Select the item
+ * @override
+ */
+ItemPoint.prototype.select = function select() {
+ this.selected = true;
+ // TODO: select and unselect
+};
- from = to;
- }
- }
+/**
+ * Unselect the item
+ * @override
+ */
+ItemPoint.prototype.unselect = function unselect() {
+ this.selected = false;
+ // TODO: select and unselect
+};
- /**
- * Parse a set with attributes,
- * for example [label="1.000", shape=solid]
- * @return {Object | null} attr
- */
- function parseAttributeList() {
- var attr = null;
+/**
+ * Repaint the item
+ * @return {Boolean} changed
+ */
+ItemPoint.prototype.repaint = function repaint() {
+ // TODO: make an efficient repaint
+ var changed = false;
+ var dom = this.dom;
- while (token == '[') {
- getToken();
- attr = {};
- while (token !== '' && token != ']') {
- if (tokenType != TOKENTYPE.IDENTIFIER) {
- throw newSyntaxError('Attribute name expected');
- }
- var name = token;
+ if (!dom) {
+ this._create();
+ dom = this.dom;
+ changed = true;
+ }
- getToken();
- if (token != '=') {
- throw newSyntaxError('Equal sign = expected');
- }
- getToken();
+ if (dom) {
+ if (!this.parent) {
+ throw new Error('Cannot repaint item: no parent attached');
+ }
+ var foreground = this.parent.getForeground();
+ if (!foreground) {
+ throw new Error('Cannot repaint time axis: ' +
+ 'parent has no foreground container element');
+ }
- if (tokenType != TOKENTYPE.IDENTIFIER) {
- throw newSyntaxError('Attribute value expected');
- }
- var value = token;
- setValue(attr, name, value); // name can be a path
+ if (!dom.point.parentNode) {
+ foreground.appendChild(dom.point);
+ foreground.appendChild(dom.point);
+ changed = true;
+ }
- getToken();
- if (token ==',') {
- getToken();
- }
+ // update contents
+ if (this.data.content != this.content) {
+ this.content = this.data.content;
+ if (this.content instanceof Element) {
+ dom.content.innerHTML = '';
+ dom.content.appendChild(this.content);
}
-
- if (token != ']') {
- throw newSyntaxError('Bracket ] expected');
+ else if (this.data.content != undefined) {
+ dom.content.innerHTML = this.content;
}
- getToken();
+ else {
+ throw new Error('Property "content" missing in item ' + this.data.id);
+ }
+ changed = true;
}
- return attr;
+ // 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;
+ }
}
- /**
- * Create a syntax error with extra information on current token and index.
- * @param {String} message
- * @returns {SyntaxError} err
- */
- function newSyntaxError(message) {
- return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
- }
+ return changed;
+};
- /**
- * Chop off text after a maximum length
- * @param {String} text
- * @param {Number} maxLength
- * @returns {String}
- */
- function chop (text, maxLength) {
- return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
+/**
+ * Show the item in the DOM (when not already visible). The items DOM will
+ * be created when needed.
+ * @return {Boolean} changed
+ */
+ItemPoint.prototype.show = function show() {
+ if (!this.dom || !this.dom.point.parentNode) {
+ return this.repaint();
}
-
- /**
- * Execute a function fn for each pair of elements in two arrays
- * @param {Array | *} array1
- * @param {Array | *} array2
- * @param {function} fn
- */
- function forEach2(array1, array2, fn) {
- if (array1 instanceof Array) {
- array1.forEach(function (elem1) {
- if (array2 instanceof Array) {
- array2.forEach(function (elem2) {
- fn(elem1, elem2);
- });
- }
- else {
- fn(elem1, array2);
- }
- });
- }
- else {
- if (array2 instanceof Array) {
- array2.forEach(function (elem2) {
- fn(array1, elem2);
- });
- }
- else {
- fn(array1, array2);
- }
- }
+ else {
+ return false;
}
+};
- /**
- * Convert a string containing a graph in DOT language into a map containing
- * with nodes and edges in the format of graph.
- * @param {String} data Text containing a graph in DOT-notation
- * @return {Object} graphData
- */
- function DOTToGraph (data) {
- // parse the DOT file
- var dotData = parseDOT(data);
- var graphData = {
- nodes: [],
- edges: [],
- options: {}
- };
-
- // copy the nodes
- if (dotData.nodes) {
- dotData.nodes.forEach(function (dotNode) {
- var graphNode = {
- id: dotNode.id,
- label: String(dotNode.label || dotNode.id)
- };
- merge(graphNode, dotNode.attr);
- if (graphNode.image) {
- graphNode.shape = 'image';
- }
- graphData.nodes.push(graphNode);
- });
+/**
+ * Hide the item from the DOM (when visible)
+ * @return {Boolean} changed
+ */
+ItemPoint.prototype.hide = function hide() {
+ var changed = false,
+ dom = this.dom;
+ if (dom) {
+ if (dom.point.parentNode) {
+ dom.point.parentNode.removeChild(dom.point);
+ changed = true;
}
+ }
+ return changed;
+};
- // copy the edges
- if (dotData.edges) {
- /**
- * Convert an edge in DOT format to an edge with VisGraph format
- * @param {Object} dotEdge
- * @returns {Object} graphEdge
- */
- function convertEdge(dotEdge) {
- var graphEdge = {
- from: dotEdge.from,
- to: dotEdge.to
- };
- merge(graphEdge, dotEdge.attr);
- graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
- return graphEdge;
- }
+/**
+ * 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 reflow() {
+ var changed = 0,
+ update,
+ dom,
+ props,
+ options,
+ margin,
+ orientation,
+ start,
+ top,
+ data,
+ range;
- dotData.edges.forEach(function (dotEdge) {
- var from, to;
- if (dotEdge.from instanceof Object) {
- from = dotEdge.from.nodes;
- }
- else {
- from = {
- id: dotEdge.from
- }
- }
+ if (this.data.start == undefined) {
+ throw new Error('Property "start" missing in item ' + this.data.id);
+ }
- if (dotEdge.to instanceof Object) {
- to = dotEdge.to.nodes;
- }
- else {
- to = {
- id: dotEdge.to
- }
- }
+ data = this.data;
+ range = this.parent && this.parent.range;
+ if (data && range) {
+ // TODO: account for the width of the item
+ var interval = (range.end - range.start);
+ this.visible = (data.start > range.start - interval) && (data.start < range.end);
+ }
+ else {
+ this.visible = false;
+ }
+
+ if (this.visible) {
+ dom = this.dom;
+ if (dom) {
+ update = util.updateProperty;
+ props = this.props;
+ options = this.options;
+ orientation = options.orientation || this.defaultOptions.orientation;
+ margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
+ start = this.parent.toScreen(this.data.start);
- if (dotEdge.from instanceof Object && dotEdge.from.edges) {
- dotEdge.from.edges.forEach(function (subEdge) {
- var graphEdge = convertEdge(subEdge);
- graphData.edges.push(graphEdge);
- });
- }
+ 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);
- forEach2(from, to, function (from, to) {
- var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
- var graphEdge = convertEdge(subEdge);
- graphData.edges.push(graphEdge);
- });
+ if (orientation == 'top') {
+ top = margin;
+ }
+ else {
+ // default or 'bottom'
+ var parentHeight = this.parent.height;
+ top = Math.max(parentHeight - this.height - margin, 0);
+ }
+ changed += update(this, 'top', top);
+ changed += update(this, 'left', start - props.dot.width / 2);
+ changed += update(props.content, 'marginLeft', 1.5 * props.dot.width);
+ //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO
- if (dotEdge.to instanceof Object && dotEdge.to.edges) {
- dotEdge.to.edges.forEach(function (subEdge) {
- var graphEdge = convertEdge(subEdge);
- graphData.edges.push(graphEdge);
- });
- }
- });
+ changed += update(props.dot, 'top', (this.height - props.dot.height) / 2);
}
-
- // copy the options
- if (dotData.attr) {
- graphData.options = dotData.attr;
+ else {
+ changed += 1;
}
-
- return graphData;
}
- // exports
- exports.parseDOT = parseDOT;
- exports.DOTToGraph = DOTToGraph;
-
-})(typeof util !== 'undefined' ? util : exports);
+ return (changed > 0);
+};
/**
- * Canvas shapes used by the Graph
+ * Create an items DOM
+ * @private
*/
-if (typeof CanvasRenderingContext2D !== 'undefined') {
+ItemPoint.prototype._create = function _create() {
+ var dom = this.dom;
+ if (!dom) {
+ this.dom = dom = {};
- /**
- * Draw a circle shape
- */
- CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
- this.beginPath();
- this.arc(x, y, r, 0, 2*Math.PI, false);
- };
+ // background box
+ dom.point = document.createElement('div');
+ // className is updated in repaint()
- /**
- * Draw a square shape
- * @param {Number} x horizontal center
- * @param {Number} y vertical center
- * @param {Number} r size, width and height of the square
- */
- CanvasRenderingContext2D.prototype.square = function(x, y, r) {
- this.beginPath();
- this.rect(x - r, y - r, r * 2, r * 2);
- };
+ // contents box, right from the dot
+ dom.content = document.createElement('div');
+ dom.content.className = 'content';
+ dom.point.appendChild(dom.content);
- /**
- * Draw a triangle shape
- * @param {Number} x horizontal center
- * @param {Number} y vertical center
- * @param {Number} r radius, half the length of the sides of the triangle
- */
- CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
- // http://en.wikipedia.org/wiki/Equilateral_triangle
- this.beginPath();
+ // dot at start
+ dom.dot = document.createElement('div');
+ dom.dot.className = 'dot';
+ dom.point.appendChild(dom.dot);
+ }
+};
- var s = r * 2;
- var s2 = s / 2;
- var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
- var h = Math.sqrt(s * s - s2 * s2); // height
+/**
+ * Reposition the item, recalculate its left, top, and width, using the current
+ * range and size of the items itemset
+ * @override
+ */
+ItemPoint.prototype.reposition = function reposition() {
+ var dom = this.dom,
+ props = this.props;
- this.moveTo(x, y - (h - ir));
- this.lineTo(x + s2, y + ir);
- this.lineTo(x - s2, y + ir);
- this.lineTo(x, y - (h - ir));
- this.closePath();
- };
+ if (dom) {
+ dom.point.style.top = this.top + 'px';
+ dom.point.style.left = this.left + 'px';
- /**
- * Draw a triangle shape in downward orientation
- * @param {Number} x horizontal center
- * @param {Number} y vertical center
- * @param {Number} r radius
- */
- CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
- // http://en.wikipedia.org/wiki/Equilateral_triangle
- this.beginPath();
+ dom.content.style.marginLeft = props.content.marginLeft + 'px';
+ //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO
- var s = r * 2;
- var s2 = s / 2;
- var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
- var h = Math.sqrt(s * s - s2 * s2); // height
+ dom.dot.style.top = props.dot.top + 'px';
+ }
+};
- this.moveTo(x, y + (h - ir));
- this.lineTo(x + s2, y - ir);
- this.lineTo(x - s2, y - ir);
- this.lineTo(x, y + (h - ir));
- this.closePath();
+/**
+ * @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
+ * @param {Object} [defaultOptions] default options
+ * // TODO: describe available options
+ */
+function ItemRange (parent, data, options, defaultOptions) {
+ this.props = {
+ content: {
+ left: 0,
+ width: 0
+ }
};
- /**
- * Draw a star shape, a star with 5 points
- * @param {Number} x horizontal center
- * @param {Number} y vertical center
- * @param {Number} r radius, half the length of the sides of the triangle
- */
- CanvasRenderingContext2D.prototype.star = function(x, y, r) {
- // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
- this.beginPath();
+ Item.call(this, parent, data, options, defaultOptions);
+}
- for (var n = 0; n < 10; n++) {
- var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
- this.lineTo(
- x + radius * Math.sin(n * 2 * Math.PI / 10),
- y - radius * Math.cos(n * 2 * Math.PI / 10)
- );
- }
+ItemRange.prototype = new Item (null, null);
- this.closePath();
- };
+/**
+ * Select the item
+ * @override
+ */
+ItemRange.prototype.select = function select() {
+ this.selected = true;
+ // TODO: select and unselect
+};
- /**
- * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
- */
- CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
- var r2d = Math.PI/180;
- if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
- if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
- this.beginPath();
- this.moveTo(x+r,y);
- this.lineTo(x+w-r,y);
- this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
- this.lineTo(x+w,y+h-r);
- this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
- this.lineTo(x+r,y+h);
- this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
- this.lineTo(x,y+r);
- this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
- };
+/**
+ * Unselect the item
+ * @override
+ */
+ItemRange.prototype.unselect = function unselect() {
+ this.selected = false;
+ // TODO: select and unselect
+};
- /**
- * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
- */
- CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
- var kappa = .5522848,
- ox = (w / 2) * kappa, // control point offset horizontal
- oy = (h / 2) * kappa, // control point offset vertical
- xe = x + w, // x-end
- ye = y + h, // y-end
- xm = x + w / 2, // x-middle
- ym = y + h / 2; // y-middle
+/**
+ * Repaint the item
+ * @return {Boolean} changed
+ */
+ItemRange.prototype.repaint = function repaint() {
+ // TODO: make an efficient repaint
+ var changed = false;
+ var dom = this.dom;
- this.beginPath();
- this.moveTo(x, ym);
- this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
- this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
- this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
- this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
- };
+ if (!dom) {
+ this._create();
+ dom = this.dom;
+ changed = true;
+ }
+ if (dom) {
+ if (!this.parent) {
+ throw new Error('Cannot repaint item: no parent attached');
+ }
+ var foreground = this.parent.getForeground();
+ if (!foreground) {
+ throw new Error('Cannot repaint time axis: ' +
+ 'parent has no foreground container element');
+ }
+ if (!dom.box.parentNode) {
+ foreground.appendChild(dom.box);
+ changed = true;
+ }
- /**
- * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
- */
- CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
- var f = 1/3;
- var wEllipse = w;
- var hEllipse = h * f;
+ // 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;
+ }
- var kappa = .5522848,
- ox = (wEllipse / 2) * kappa, // control point offset horizontal
- oy = (hEllipse / 2) * kappa, // control point offset vertical
- xe = x + wEllipse, // x-end
- ye = y + hEllipse, // y-end
- xm = x + wEllipse / 2, // x-middle
- ym = y + hEllipse / 2, // y-middle
- ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
- yeb = y + h; // y-end, bottom ellipse
+ // 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;
+ }
+ }
- this.beginPath();
- this.moveTo(xe, ym);
+ return changed;
+};
- this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
- this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
+/**
+ * Show the item in the DOM (when not already visible). The items DOM will
+ * be created when needed.
+ * @return {Boolean} changed
+ */
+ItemRange.prototype.show = function show() {
+ if (!this.dom || !this.dom.box.parentNode) {
+ return this.repaint();
+ }
+ else {
+ return false;
+ }
+};
- this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
- this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
+/**
+ * Hide the item from the DOM (when visible)
+ * @return {Boolean} changed
+ */
+ItemRange.prototype.hide = function hide() {
+ var changed = false,
+ dom = this.dom;
+ if (dom) {
+ if (dom.box.parentNode) {
+ dom.box.parentNode.removeChild(dom.box);
+ changed = true;
+ }
+ }
+ return changed;
+};
- this.lineTo(xe, ymb);
+/**
+ * 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 reflow() {
+ var changed = 0,
+ dom,
+ props,
+ options,
+ margin,
+ padding,
+ parent,
+ start,
+ end,
+ data,
+ range,
+ update,
+ box,
+ parentWidth,
+ contentLeft,
+ orientation,
+ top;
- this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
- this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
+ 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);
+ }
- this.lineTo(x, ym);
- };
+ data = this.data;
+ range = this.parent && this.parent.range;
+ if (data && range) {
+ // TODO: account for the width of the item. Take some margin
+ this.visible = (data.start < range.end) && (data.end > range.start);
+ }
+ else {
+ this.visible = false;
+ }
+ if (this.visible) {
+ dom = this.dom;
+ if (dom) {
+ props = this.props;
+ options = this.options;
+ parent = this.parent;
+ start = parent.toScreen(this.data.start);
+ end = parent.toScreen(this.data.end);
+ update = util.updateProperty;
+ box = dom.box;
+ parentWidth = parent.width;
+ orientation = options.orientation || this.defaultOptions.orientation;
+ margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
+ padding = options.padding || this.defaultOptions.padding;
- /**
- * Draw an arrow point (no line)
- */
- CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
- // tail
- var xt = x - length * Math.cos(angle);
- var yt = y - length * Math.sin(angle);
+ changed += update(props.content, 'width', dom.content.offsetWidth);
- // inner tail
- // TODO: allow to customize different shapes
- var xi = x - length * 0.9 * Math.cos(angle);
- var yi = y - length * 0.9 * Math.sin(angle);
+ changed += update(this, 'height', box.offsetHeight);
- // left
- var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
- var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
+ // 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;
+ }
- // right
- var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
- var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
+ // when range exceeds left of the window, position the contents at the left of the visible area
+ if (start < 0) {
+ contentLeft = Math.min(-start,
+ (end - start - props.content.width - 2 * padding));
+ // TODO: remove the need for options.padding. it's terrible.
+ }
+ else {
+ contentLeft = 0;
+ }
+ changed += update(props.content, 'left', contentLeft);
- this.beginPath();
- this.moveTo(x, y);
- this.lineTo(xl, yl);
- this.lineTo(xi, yi);
- this.lineTo(xr, yr);
- this.closePath();
- };
+ if (orientation == 'top') {
+ top = margin;
+ changed += update(this, 'top', top);
+ }
+ else {
+ // default or 'bottom'
+ top = parent.height - this.height - margin;
+ changed += update(this, 'top', top);
+ }
- /**
- * Sets up the dashedLine functionality for drawing
- * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
- * @author David Jordan
- * @date 2012-08-08
- */
- CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
- if (!dashArray) dashArray=[10,5];
- if (dashLength==0) dashLength = 0.001; // Hack for Safari
- var dashCount = dashArray.length;
- this.moveTo(x, y);
- var dx = (x2-x), dy = (y2-y);
- var slope = dy/dx;
- var distRemaining = Math.sqrt( dx*dx + dy*dy );
- var dashIndex=0, draw=true;
- while (distRemaining>=0.1){
- var dashLength = dashArray[dashIndex++%dashCount];
- if (dashLength > distRemaining) dashLength = distRemaining;
- var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
- if (dx<0) xStep = -xStep;
- x += xStep;
- y += slope*xStep;
- this[draw ? 'lineTo' : 'moveTo'](x,y);
- distRemaining -= dashLength;
- draw = !draw;
+ changed += update(this, 'left', start);
+ changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width;
}
- };
+ else {
+ changed += 1;
+ }
+ }
- // TODO: add diamond shape
-}
+ return (changed > 0);
+};
/**
- * @class Node
- * A node. A node can be connected to other nodes via one or multiple edges.
- * @param {object} properties An object containing properties for the node. All
- * properties are optional, except for the id.
- * {number} id Id of the node. Required
- * {string} label Text label for the node
- * {number} x Horizontal position of the node
- * {number} y Vertical position of the node
- * {string} shape Node shape, available:
- * "database", "circle", "ellipse",
- * "box", "image", "text", "dot",
- * "star", "triangle", "triangleDown",
- * "square"
- * {string} image An image url
- * {string} title An title text, can be HTML
- * {anytype} group A group name or number
- * @param {Graph.Images} imagelist A list with images. Only needed
- * when the node has an image
- * @param {Graph.Groups} grouplist A list with groups. Needed for
- * retrieving group properties
- * @param {Object} constants An object with default values for
- * example for the color
+ * Create an items DOM
+ * @private
+ */
+ItemRange.prototype._create = function _create() {
+ var dom = this.dom;
+ if (!dom) {
+ this.dom = dom = {};
+ // background box
+ dom.box = document.createElement('div');
+ // className is updated in repaint()
+
+ // contents box
+ dom.content = document.createElement('div');
+ dom.content.className = 'content';
+ dom.box.appendChild(dom.content);
+ }
+};
+
+/**
+ * Reposition the item, recalculate its left, top, and width, using the current
+ * range and size of the items itemset
+ * @override
*/
-function Node(properties, imagelist, grouplist, constants) {
- this.selected = false;
+ItemRange.prototype.reposition = function reposition() {
+ var dom = this.dom,
+ props = this.props;
- this.edges = []; // all edges connected to this node
- this.group = constants.nodes.group;
+ if (dom) {
+ dom.box.style.top = this.top + 'px';
+ dom.box.style.left = this.left + 'px';
+ dom.box.style.width = this.width + 'px';
- this.fontSize = constants.nodes.fontSize;
- this.fontFace = constants.nodes.fontFace;
- this.fontColor = constants.nodes.fontColor;
+ dom.content.style.left = props.content.left + 'px';
+ }
+};
- this.color = constants.nodes.color;
+/**
+ * @constructor Group
+ * @param {GroupSet} parent
+ * @param {Number | String} groupId
+ * @param {Object} [options] Options to set initial property values
+ * // TODO: describe available options
+ * @extends Component
+ */
+function Group (parent, groupId, options) {
+ this.id = util.randomUUID();
+ this.parent = parent;
- // set defaults for the properties
- this.id = undefined;
- this.shape = constants.nodes.shape;
- this.image = constants.nodes.image;
- this.x = 0;
- this.y = 0;
- this.xFixed = false;
- this.yFixed = false;
- this.radius = constants.nodes.radius;
- this.radiusFixed = false;
- this.radiusMin = constants.nodes.radiusMin;
- this.radiusMax = constants.nodes.radiusMax;
+ this.groupId = groupId;
+ this.itemset = null; // ItemSet
+ this.options = options || {};
+ this.options.top = 0;
- this.imagelist = imagelist;
- this.grouplist = grouplist;
+ this.props = {
+ label: {
+ width: 0,
+ height: 0
+ }
+ };
- this.setProperties(properties, constants);
+ this.top = 0;
+ this.left = 0;
+ this.width = 0;
+ this.height = 0;
+}
- // mass, force, velocity
- this.mass = 50; // kg (mass is adjusted for the number of connected edges)
- this.fx = 0.0; // external force x
- this.fy = 0.0; // external force y
- this.vx = 0.0; // velocity x
- this.vy = 0.0; // velocity y
- this.minForce = constants.minForce;
- this.damping = 0.9; // damping factor
-};
+Group.prototype = new Component();
+
+// TODO: comment
+Group.prototype.setOptions = Component.prototype.setOptions;
/**
- * Attach a edge to the node
- * @param {Edge} edge
+ * Get the container element of the panel, which can be used by a child to
+ * add its own widgets.
+ * @returns {HTMLElement} container
*/
-Node.prototype.attachEdge = function(edge) {
- if (this.edges.indexOf(edge) == -1) {
- this.edges.push(edge);
- }
- this._updateMass();
+Group.prototype.getContainer = function () {
+ return this.parent.getContainer();
};
/**
- * Detach a edge from the node
- * @param {Edge} edge
+ * Set item set for the group. The group will create a view on the itemset,
+ * filtered by the groups id.
+ * @param {DataSet | DataView} items
*/
-Node.prototype.detachEdge = function(edge) {
- var index = this.edges.indexOf(edge);
- if (index != -1) {
- this.edges.splice(index, 1);
+Group.prototype.setItems = function setItems(items) {
+ if (this.itemset) {
+ // remove current item set
+ this.itemset.hide();
+ this.itemset.setItems();
+
+ this.parent.controller.remove(this.itemset);
+ this.itemset = null;
+ }
+
+ if (items) {
+ var groupId = this.groupId;
+
+ var itemsetOptions = Object.create(this.options);
+ this.itemset = new ItemSet(this, null, itemsetOptions);
+ this.itemset.setRange(this.parent.range);
+
+ this.view = new DataView(items, {
+ filter: function (item) {
+ return item.group == groupId;
+ }
+ });
+ this.itemset.setItems(this.view);
+
+ this.parent.controller.add(this.itemset);
}
- this._updateMass();
};
/**
- * Update the nodes mass, which is determined by the number of edges connecting
- * to it (more edges -> heavier node).
- * @private
+ * Repaint the item
+ * @return {Boolean} changed
*/
-Node.prototype._updateMass = function() {
- this.mass = 50 + 20 * this.edges.length; // kg
+Group.prototype.repaint = function repaint() {
+ return false;
};
/**
- * Set or overwrite properties for the node
- * @param {Object} properties an object with properties
- * @param {Object} constants and object with default, global properties
+ * Reflow the item
+ * @return {Boolean} resized
*/
-Node.prototype.setProperties = function(properties, constants) {
- if (!properties) {
- return;
- }
+Group.prototype.reflow = function reflow() {
+ var changed = 0,
+ update = util.updateProperty;
- // basic properties
- if (properties.id != undefined) {this.id = properties.id;}
- if (properties.label != undefined) {this.label = properties.label;}
- if (properties.title != undefined) {this.title = properties.title;}
- if (properties.group != undefined) {this.group = properties.group;}
- if (properties.x != undefined) {this.x = properties.x;}
- if (properties.y != undefined) {this.y = properties.y;}
- if (properties.value != undefined) {this.value = properties.value;}
+ changed += update(this, 'top', this.itemset ? this.itemset.top : 0);
+ changed += update(this, 'height', this.itemset ? this.itemset.height : 0);
- if (this.id === undefined) {
- throw "Node must have an id";
- }
+ // TODO: reckon with the height of the group label
- // copy group properties
- if (this.group) {
- var groupObj = this.grouplist.get(this.group);
- for (var prop in groupObj) {
- if (groupObj.hasOwnProperty(prop)) {
- this[prop] = groupObj[prop];
- }
- }
+ if (this.label) {
+ var inner = this.label.firstChild;
+ changed += update(this.props.label, 'width', inner.clientWidth);
+ changed += update(this.props.label, 'height', inner.clientHeight);
+ }
+ else {
+ changed += update(this.props.label, 'width', 0);
+ changed += update(this.props.label, 'height', 0);
}
- // individual shape properties
- if (properties.shape != undefined) {this.shape = properties.shape;}
- if (properties.image != undefined) {this.image = properties.image;}
- if (properties.radius != undefined) {this.radius = properties.radius;}
- if (properties.color != undefined) {this.color = Node.parseColor(properties.color);}
+ return (changed > 0);
+};
- if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;}
- if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;}
- if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;}
+/**
+ * An GroupSet holds a set of groups
+ * @param {Component} parent
+ * @param {Component[]} [depends] Components on which this components depends
+ * (except for the parent)
+ * @param {Object} [options] See GroupSet.setOptions for the available
+ * options.
+ * @constructor GroupSet
+ * @extends Panel
+ */
+function GroupSet(parent, depends, options) {
+ this.id = util.randomUUID();
+ this.parent = parent;
+ this.depends = depends;
+ this.options = options || {};
- if (this.image != undefined) {
- if (this.imagelist) {
- this.imageObj = this.imagelist.load(this.image);
- }
- else {
- throw "No imagelist provided";
+ this.range = null; // Range or Object {start: number, end: number}
+ this.itemsData = null; // DataSet with items
+ this.groupsData = null; // DataSet with groups
+
+ this.groups = {}; // map with groups
+
+ this.dom = {};
+ this.props = {
+ labels: {
+ width: 0
}
- }
+ };
- this.xFixed = this.xFixed || (properties.x != undefined);
- this.yFixed = this.yFixed || (properties.y != undefined);
- this.radiusFixed = this.radiusFixed || (properties.radius != undefined);
+ // TODO: implement right orientation of the labels
- if (this.shape == 'image') {
- this.radiusMin = constants.nodes.widthMin;
- this.radiusMax = constants.nodes.widthMax;
- }
+ // changes in groups are queued key/value map containing id/action
+ this.queue = {};
- // choose draw method depending on the shape
- switch (this.shape) {
- case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
- case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
- case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
- case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
- // TODO: add diamond shape
- case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
- case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
- case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
- case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
- case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
- case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
- case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
- default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
- }
+ var me = this;
+ 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);
+ }
+ };
+}
- // reset the size of the node, this can be changed
- this._reset();
+GroupSet.prototype = new Panel();
+
+/**
+ * Set options for the GroupSet. Existing options will be extended/overwritten.
+ * @param {Object} [options] The following options are available:
+ * {String | function} groupsOrder
+ * TODO: describe options
+ */
+GroupSet.prototype.setOptions = Component.prototype.setOptions;
+
+GroupSet.prototype.setRange = function (range) {
+ // TODO: implement setRange
};
/**
- * Parse a color property into an object with border, background, and
- * hightlight colors
- * @param {Object | String} color
- * @return {Object} colorObject
+ * Set items
+ * @param {vis.DataSet | null} items
*/
-Node.parseColor = function(color) {
- var c;
- if (util.isString(color)) {
- c = {
- border: color,
- background: color,
- highlight: {
- border: color,
- background: color
- }
- };
- // TODO: automatically generate a nice highlight color
- }
- else {
- c = {};
- c.background = color.background || 'white';
- c.border = color.border || c.background;
- if (util.isString(color.highlight)) {
- c.highlight = {
- border: color.highlight,
- background: color.highlight
- }
- }
- else {
- c.highlight = {};
- c.highlight.background = color.highlight && color.highlight.background || c.background;
- c.highlight.border = color.highlight && color.highlight.border || c.border;
+GroupSet.prototype.setItems = function setItems(items) {
+ this.itemsData = items;
+
+ for (var id in this.groups) {
+ if (this.groups.hasOwnProperty(id)) {
+ var group = this.groups[id];
+ group.setItems(items);
}
}
- return c;
};
/**
- * select this node
+ * Get items
+ * @return {vis.DataSet | null} items
*/
-Node.prototype.select = function() {
- this.selected = true;
- this._reset();
+GroupSet.prototype.getItems = function getItems() {
+ return this.itemsData;
};
/**
- * unselect this node
+ * Set range (start and end).
+ * @param {Range | Object} range A Range or an object containing start and end.
*/
-Node.prototype.unselect = function() {
- this.selected = false;
- this._reset();
+GroupSet.prototype.setRange = function setRange(range) {
+ this.range = range;
};
/**
- * Reset the calculated size of the node, forces it to recalculate its size
- * @private
+ * Set groups
+ * @param {vis.DataSet} groups
*/
-Node.prototype._reset = function() {
- this.width = undefined;
- this.height = undefined;
+GroupSet.prototype.setGroups = function setGroups(groups) {
+ var me = this,
+ ids;
+
+ // unsubscribe from current dataset
+ if (this.groupsData) {
+ util.forEach(this.listeners, function (callback, event) {
+ me.groupsData.unsubscribe(event, callback);
+ });
+
+ // remove all drawn groups
+ ids = this.groupsData.getIds();
+ this._onRemove(ids);
+ }
+
+ // replace the dataset
+ if (!groups) {
+ this.groupsData = null;
+ }
+ else if (groups instanceof DataSet) {
+ this.groupsData = groups;
+ }
+ else {
+ this.groupsData = new DataSet({
+ convert: {
+ start: 'Date',
+ end: 'Date'
+ }
+ });
+ this.groupsData.add(groups);
+ }
+
+ if (this.groupsData) {
+ // subscribe to new dataset
+ var id = this.id;
+ util.forEach(this.listeners, function (callback, event) {
+ me.groupsData.subscribe(event, callback, id);
+ });
+
+ // draw all new groups
+ ids = this.groupsData.getIds();
+ this._onAdd(ids);
+ }
};
/**
- * get the title of this node.
- * @return {string} title The title of the node, or undefined when no title
- * has been set.
+ * Get groups
+ * @return {vis.DataSet | null} groups
*/
-Node.prototype.getTitle = function() {
- return this.title;
+GroupSet.prototype.getGroups = function getGroups() {
+ return this.groupsData;
};
/**
- * Calculate the distance to the border of the Node
- * @param {CanvasRenderingContext2D} ctx
- * @param {Number} angle Angle in radians
- * @returns {number} distance Distance to the border in pixels
+ * Repaint the component
+ * @return {Boolean} changed
*/
-Node.prototype.distanceToBorder = function (ctx, angle) {
- var borderWidth = 1;
+GroupSet.prototype.repaint = function repaint() {
+ var changed = 0,
+ i, id, group, label,
+ update = util.updateProperty,
+ asSize = util.option.asSize,
+ asElement = util.option.asElement,
+ options = this.options,
+ frame = this.dom.frame,
+ labels = this.dom.labels;
- if (!this.width) {
- this.resize(ctx);
+ // create frame
+ if (!this.parent) {
+ throw new Error('Cannot repaint groupset: no parent attached');
+ }
+ var parentContainer = this.parent.getContainer();
+ if (!parentContainer) {
+ throw new Error('Cannot repaint groupset: parent has no container element');
}
+ if (!frame) {
+ frame = document.createElement('div');
+ frame.className = 'groupset';
+ this.dom.frame = frame;
- //noinspection FallthroughInSwitchStatementJS
- switch (this.shape) {
- case 'circle':
- case 'dot':
- return this.radius + borderWidth;
+ var className = options.className;
+ if (className) {
+ util.addClassName(frame, util.option.asString(className));
+ }
- case 'ellipse':
- var a = this.width / 2;
- var b = this.height / 2;
- var w = (Math.sin(angle) * a);
- var h = (Math.cos(angle) * b);
- return a * b / Math.sqrt(w * w + h * h);
+ changed += 1;
+ }
+ if (!frame.parentNode) {
+ parentContainer.appendChild(frame);
+ changed += 1;
+ }
- // TODO: implement distanceToBorder for database
- // TODO: implement distanceToBorder for triangle
- // TODO: implement distanceToBorder for triangleDown
+ // create labels
+ var labelContainer = asElement(options.labelContainer);
+ if (!labelContainer) {
+ throw new Error('Cannot repaint groupset: option "labelContainer" not defined');
+ }
+ if (!labels) {
+ labels = document.createElement('div');
+ labels.className = 'labels';
+ //frame.appendChild(labels);
+ this.dom.labels = labels;
+ }
+ if (!labels.parentNode || labels.parentNode != labelContainer) {
+ if (labels.parentNode) {
+ labels.parentNode.removeChild(labels.parentNode);
+ }
+ labelContainer.appendChild(labels);
+ }
+
+ // reposition frame
+ changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
+ changed += update(frame.style, 'top', asSize(options.top, '0px'));
+ changed += update(frame.style, 'left', asSize(options.left, '0px'));
+ changed += update(frame.style, 'width', asSize(options.width, '100%'));
+
+ // reposition labels
+ changed += update(labels.style, 'top', asSize(options.top, '0px'));
+
+ var me = this,
+ queue = this.queue,
+ groups = this.groups,
+ groupsData = this.groupsData;
+
+ // show/hide added/changed/removed groups
+ var ids = Object.keys(queue);
+ if (ids.length) {
+ ids.forEach(function (id) {
+ var action = queue[id];
+ var group = groups[id];
+
+ //noinspection FallthroughInSwitchStatementJS
+ switch (action) {
+ case 'add':
+ case 'update':
+ if (!group) {
+ var groupOptions = Object.create(me.options);
+ group = new Group(me, id, groupOptions);
+ group.setItems(me.itemsData); // attach items data
+ groups[id] = group;
+
+ me.controller.add(group);
+ }
+
+ // TODO: update group data
+ group.data = groupsData.get(id);
+
+ delete queue[id];
+ break;
+
+ case 'remove':
+ if (group) {
+ group.setItems(); // detach items data
+ delete groups[id];
+
+ me.controller.remove(group);
+ }
+
+ // update lists
+ delete queue[id];
+ break;
- case 'box':
- case 'image':
- case 'text':
- default:
- if (this.width) {
- return Math.min(
- Math.abs(this.width / 2 / Math.cos(angle)),
- Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
- // TODO: reckon with border radius too in case of box
- }
- else {
- return 0;
+ default:
+ console.log('Error: unknown action "' + action + '"');
}
+ });
- }
+ // the groupset depends on each of the groups
+ //this.depends = this.groups; // TODO: gives a circular reference through the parent
- // TODO: implement calculation of distance to border for all shapes
-};
+ // TODO: apply dependencies of the groupset
-/**
- * Set forces acting on the node
- * @param {number} fx Force in horizontal direction
- * @param {number} fy Force in vertical direction
- */
-Node.prototype._setForce = function(fx, fy) {
- this.fx = fx;
- this.fy = fy;
-};
+ // update the top positions of the groups in the correct order
+ var orderedGroups = this.groupsData.getIds({
+ order: this.options.groupsOrder
+ });
+ for (i = 0; i < orderedGroups.length; i++) {
+ (function (group, prevGroup) {
+ var top = 0;
+ if (prevGroup) {
+ top = function () {
+ // TODO: top must reckon with options.maxHeight
+ return prevGroup.top + prevGroup.height;
+ }
+ }
+ group.setOptions({
+ top: top
+ });
+ })(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]);
+ }
-/**
- * Add forces acting on the node
- * @param {number} fx Force in horizontal direction
- * @param {number} fy Force in vertical direction
- * @private
- */
-Node.prototype._addForce = function(fx, fy) {
- this.fx += fx;
- this.fy += fy;
-};
+ // (re)create the labels
+ while (labels.firstChild) {
+ labels.removeChild(labels.firstChild);
+ }
+ for (i = 0; i < orderedGroups.length; i++) {
+ id = orderedGroups[i];
+ label = this._createLabel(id);
+ labels.appendChild(label);
+ }
-/**
- * Perform one discrete step for the node
- * @param {number} interval Time interval in seconds
- */
-Node.prototype.discreteStep = function(interval) {
- if (!this.xFixed) {
- var dx = -this.damping * this.vx; // damping force
- var ax = (this.fx + dx) / this.mass; // acceleration
- this.vx += ax / interval; // velocity
- this.x += this.vx / interval; // position
+ changed++;
}
- if (!this.yFixed) {
- var dy = -this.damping * this.vy; // damping force
- var ay = (this.fy + dy) / this.mass; // acceleration
- this.vy += ay / interval; // velocity
- this.y += this.vy / interval; // position
+ // reposition the labels
+ // TODO: labels are not displayed correctly when orientation=='top'
+ // TODO: width of labelPanel is not immediately updated on a change in groups
+ for (id in groups) {
+ if (groups.hasOwnProperty(id)) {
+ group = groups[id];
+ label = group.label;
+ if (label) {
+ label.style.top = group.top + 'px';
+ label.style.height = group.height + 'px';
+ }
+ }
}
-};
-
-
-/**
- * Check if this node has a fixed x and y position
- * @return {boolean} true if fixed, false if not
- */
-Node.prototype.isFixed = function() {
- return (this.xFixed && this.yFixed);
-};
-/**
- * Check if this node is moving
- * @param {number} vmin the minimum velocity considered as "moving"
- * @return {boolean} true if moving, false if it has no velocity
- */
-// TODO: replace this method with calculating the kinetic energy
-Node.prototype.isMoving = function(vmin) {
- return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin ||
- (!this.xFixed && Math.abs(this.fx) > this.minForce) ||
- (!this.yFixed && Math.abs(this.fy) > this.minForce));
+ return (changed > 0);
};
/**
- * check if this node is selecte
- * @return {boolean} selected True if node is selected, else false
+ * Create a label for group with given id
+ * @param {Number} id
+ * @return {Element} label
+ * @private
*/
-Node.prototype.isSelected = function() {
- return this.selected;
-};
+GroupSet.prototype._createLabel = function(id) {
+ var group = this.groups[id];
+ var label = document.createElement('div');
+ label.className = 'label';
+ var inner = document.createElement('div');
+ inner.className = 'inner';
+ label.appendChild(inner);
-/**
- * Retrieve the value of the node. Can be undefined
- * @return {Number} value
- */
-Node.prototype.getValue = function() {
- return this.value;
-};
+ var content = group.data && group.data.content;
+ if (content instanceof Element) {
+ inner.appendChild(content);
+ }
+ else if (content != undefined) {
+ inner.innerHTML = content;
+ }
-/**
- * Calculate the distance from the nodes location to the given location (x,y)
- * @param {Number} x
- * @param {Number} y
- * @return {Number} value
- */
-Node.prototype.getDistance = function(x, y) {
- var dx = this.x - x,
- dy = this.y - y;
- return Math.sqrt(dx * dx + dy * dy);
-};
+ var className = group.data && group.data.className;
+ if (className) {
+ util.addClassName(label, className);
+ }
+ group.label = label; // TODO: not so nice, parking labels in the group this way!!!
-/**
- * Adjust the value range of the node. The node will adjust it's radius
- * based on its value.
- * @param {Number} min
- * @param {Number} max
- */
-Node.prototype.setValueRange = function(min, max) {
- if (!this.radiusFixed && this.value !== undefined) {
- var scale = (this.radiusMax - this.radiusMin) / (max - min);
- this.radius = (this.value - min) * scale + this.radiusMin;
- }
+ return label;
};
/**
- * Draw this node in the given canvas
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
- * @param {CanvasRenderingContext2D} ctx
+ * Get container element
+ * @return {HTMLElement} container
*/
-Node.prototype.draw = function(ctx) {
- throw "Draw method not initialized for node";
+GroupSet.prototype.getContainer = function getContainer() {
+ return this.dom.frame;
};
/**
- * Recalculate the size of this node in the given canvas
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
- * @param {CanvasRenderingContext2D} ctx
+ * Get the width of the group labels
+ * @return {Number} width
*/
-Node.prototype.resize = function(ctx) {
- throw "Resize method not initialized for node";
+GroupSet.prototype.getLabelsWidth = function getContainer() {
+ return this.props.labels.width;
};
/**
- * Check if this object is overlapping with the provided object
- * @param {Object} obj an object with parameters left, top, right, bottom
- * @return {boolean} True if location is located on node
+ * Reflow the component
+ * @return {Boolean} resized
*/
-Node.prototype.isOverlappingWith = function(obj) {
- return (this.left < obj.right &&
- this.left + this.width > obj.left &&
- this.top < obj.bottom &&
- this.top + this.height > obj.top);
-};
+GroupSet.prototype.reflow = function reflow() {
+ var changed = 0,
+ id, group,
+ options = this.options,
+ update = util.updateProperty,
+ asNumber = util.option.asNumber,
+ asSize = util.option.asSize,
+ frame = this.dom.frame;
-Node.prototype._resizeImage = function (ctx) {
- // TODO: pre calculate the image size
- if (!this.width) { // undefined or 0
- var width, height;
- if (this.value) {
- var scale = this.imageObj.height / this.imageObj.width;
- width = this.radius || this.imageObj.width;
- height = this.radius * scale || this.imageObj.height;
+ if (frame) {
+ var maxHeight = asNumber(options.maxHeight);
+ var fixedHeight = (asSize(options.height) != null);
+ var height;
+ if (fixedHeight) {
+ height = frame.offsetHeight;
}
else {
- width = this.imageObj.width;
- height = this.imageObj.height;
+ // height is not specified, calculate the sum of the height of all groups
+ height = 0;
+
+ for (id in this.groups) {
+ if (this.groups.hasOwnProperty(id)) {
+ group = this.groups[id];
+ height += group.height;
+ }
+ }
}
- this.width = width;
- this.height = height;
+ if (maxHeight != null) {
+ height = Math.min(height, maxHeight);
+ }
+ changed += update(this, 'height', height);
+
+ changed += update(this, 'top', frame.offsetTop);
+ changed += update(this, 'left', frame.offsetLeft);
+ changed += update(this, 'width', frame.offsetWidth);
}
-};
-Node.prototype._drawImage = function (ctx) {
- this._resizeImage(ctx);
+ // calculate the maximum width of the labels
+ var width = 0;
+ for (id in this.groups) {
+ if (this.groups.hasOwnProperty(id)) {
+ group = this.groups[id];
+ var labelWidth = group.props && group.props.label && group.props.label.width || 0;
+ width = Math.max(width, labelWidth);
+ }
+ }
+ changed += update(this.props.labels, 'width', width);
- this.left = this.x - this.width / 2;
- this.top = this.y - this.height / 2;
+ return (changed > 0);
+};
- var yLabel;
- if (this.imageObj) {
- ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
- yLabel = this.y + this.height / 2;
+/**
+ * Hide the component from the DOM
+ * @return {Boolean} changed
+ */
+GroupSet.prototype.hide = function hide() {
+ if (this.dom.frame && this.dom.frame.parentNode) {
+ this.dom.frame.parentNode.removeChild(this.dom.frame);
+ return true;
}
else {
- // image still loading... just draw the label for now
- yLabel = this.y;
+ return false;
}
-
- this._label(ctx, this.label, this.x, yLabel, undefined, "top");
};
-
-Node.prototype._resizeBox = function (ctx) {
- if (!this.width) {
- var margin = 5;
- var textSize = this.getTextSize(ctx);
- this.width = textSize.width + 2 * margin;
- this.height = textSize.height + 2 * margin;
+/**
+ * Show the component in the DOM (when not already visible).
+ * A repaint will be executed when the component is not visible
+ * @return {Boolean} changed
+ */
+GroupSet.prototype.show = function show() {
+ if (!this.dom.frame || !this.dom.frame.parentNode) {
+ return this.repaint();
+ }
+ else {
+ return false;
}
};
-Node.prototype._drawBox = function (ctx) {
- this._resizeBox(ctx);
-
- this.left = this.x - this.width / 2;
- this.top = this.y - this.height / 2;
-
- ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
- ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
- ctx.lineWidth = this.selected ? 2.0 : 1.0;
- ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
- ctx.fill();
- ctx.stroke();
-
- this._label(ctx, this.label, this.x, this.y);
+/**
+ * Handle updated groups
+ * @param {Number[]} ids
+ * @private
+ */
+GroupSet.prototype._onUpdate = function _onUpdate(ids) {
+ this._toQueue(ids, 'update');
};
-
-Node.prototype._resizeDatabase = function (ctx) {
- if (!this.width) {
- var margin = 5;
- var textSize = this.getTextSize(ctx);
- var size = textSize.width + 2 * margin;
- this.width = size;
- this.height = size;
- }
+/**
+ * Handle changed groups
+ * @param {Number[]} ids
+ * @private
+ */
+GroupSet.prototype._onAdd = function _onAdd(ids) {
+ this._toQueue(ids, 'add');
};
-Node.prototype._drawDatabase = function (ctx) {
- this._resizeDatabase(ctx);
- this.left = this.x - this.width / 2;
- this.top = this.y - this.height / 2;
-
- ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
- ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
- ctx.lineWidth = this.selected ? 2.0 : 1.0;
- ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
- ctx.fill();
- ctx.stroke();
-
- this._label(ctx, this.label, this.x, this.y);
+/**
+ * Handle removed groups
+ * @param {Number[]} ids
+ * @private
+ */
+GroupSet.prototype._onRemove = function _onRemove(ids) {
+ this._toQueue(ids, 'remove');
};
+/**
+ * Put groups in the queue to be added/updated/remove
+ * @param {Number[]} ids
+ * @param {String} action can be 'add', 'update', 'remove'
+ */
+GroupSet.prototype._toQueue = function _toQueue(ids, action) {
+ var queue = this.queue;
+ ids.forEach(function (id) {
+ queue[id] = action;
+ });
-Node.prototype._resizeCircle = function (ctx) {
- if (!this.width) {
- var margin = 5;
- var textSize = this.getTextSize(ctx);
- var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
- this.radius = diameter / 2;
-
- this.width = diameter;
- this.height = diameter;
+ if (this.controller) {
+ //this.requestReflow();
+ this.requestRepaint();
}
};
-Node.prototype._drawCircle = function (ctx) {
- this._resizeCircle(ctx);
- this.left = this.x - this.width / 2;
- this.top = this.y - this.height / 2;
-
- ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
- ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
- ctx.lineWidth = this.selected ? 2.0 : 1.0;
- ctx.circle(this.x, this.y, this.radius);
- ctx.fill();
- ctx.stroke();
-
- this._label(ctx, this.label, this.x, this.y);
-};
+/**
+ * Create a timeline visualization
+ * @param {HTMLElement} container
+ * @param {vis.DataSet | Array | DataTable} [items]
+ * @param {Object} [options] See Timeline.setOptions for the available options.
+ * @constructor
+ */
+function Timeline (container, items, options) {
+ var me = this;
+ this.options = util.extend({
+ orientation: 'bottom',
+ min: null,
+ max: null,
+ zoomMin: 10, // milliseconds
+ zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
+ // moveable: true, // TODO: option moveable
+ // zoomable: true, // TODO: option zoomable
+ showMinorLabels: true,
+ showMajorLabels: true,
+ autoResize: false
+ }, options);
-Node.prototype._resizeEllipse = function (ctx) {
- if (!this.width) {
- var textSize = this.getTextSize(ctx);
+ // controller
+ this.controller = new Controller();
- this.width = textSize.width * 1.5;
- this.height = textSize.height * 2;
- if (this.width < this.height) {
- this.width = this.height;
- }
+ // root panel
+ if (!container) {
+ throw new Error('No container element provided');
}
-};
-
-Node.prototype._drawEllipse = function (ctx) {
- this._resizeEllipse(ctx);
- this.left = this.x - this.width / 2;
- this.top = this.y - this.height / 2;
+ var rootOptions = Object.create(this.options);
+ rootOptions.height = function () {
+ if (me.options.height) {
+ // fixed height
+ return me.options.height;
+ }
+ else {
+ // auto height
+ return me.timeaxis.height + me.content.height;
+ }
+ };
+ this.rootPanel = new RootPanel(container, rootOptions);
+ this.controller.add(this.rootPanel);
- ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
- ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
- ctx.lineWidth = this.selected ? 2.0 : 1.0;
- ctx.ellipse(this.left, this.top, this.width, this.height);
- ctx.fill();
- ctx.stroke();
+ // item panel
+ var itemOptions = Object.create(this.options);
+ itemOptions.left = function () {
+ return me.labelPanel.width;
+ };
+ itemOptions.width = function () {
+ return me.rootPanel.width - me.labelPanel.width;
+ };
+ itemOptions.top = null;
+ itemOptions.height = null;
+ this.itemPanel = new Panel(this.rootPanel, [], itemOptions);
+ this.controller.add(this.itemPanel);
- this._label(ctx, this.label, this.x, this.y);
-};
+ // label panel
+ var labelOptions = Object.create(this.options);
+ labelOptions.top = null;
+ labelOptions.left = null;
+ labelOptions.height = null;
+ labelOptions.width = function () {
+ if (me.content && typeof me.content.getLabelsWidth === 'function') {
+ return me.content.getLabelsWidth();
+ }
+ else {
+ return 0;
+ }
+ };
+ this.labelPanel = new Panel(this.rootPanel, [], labelOptions);
+ this.controller.add(this.labelPanel);
-Node.prototype._drawDot = function (ctx) {
- this._drawShape(ctx, 'circle');
-};
+ // range
+ var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
+ this.range = new Range({
+ start: now.clone().add('days', -3).valueOf(),
+ end: now.clone().add('days', 4).valueOf()
+ });
+ /* TODO: fix range options
+ var rangeOptions = Object.create(this.options);
+ this.range = new Range(rangeOptions);
+ this.range.setRange(
+ now.clone().add('days', -3).valueOf(),
+ now.clone().add('days', 4).valueOf()
+ );
+ */
+ // TODO: reckon with options moveable and zoomable
+ this.range.subscribe(this.rootPanel, 'move', 'horizontal');
+ this.range.subscribe(this.rootPanel, 'zoom', 'horizontal');
+ this.range.on('rangechange', function () {
+ var force = true;
+ me.controller.requestReflow(force);
+ });
+ this.range.on('rangechanged', function () {
+ var force = true;
+ me.controller.requestReflow(force);
+ });
-Node.prototype._drawTriangle = function (ctx) {
- this._drawShape(ctx, 'triangle');
-};
+ // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable
-Node.prototype._drawTriangleDown = function (ctx) {
- this._drawShape(ctx, 'triangleDown');
-};
+ // time axis
+ var timeaxisOptions = Object.create(rootOptions);
+ timeaxisOptions.range = this.range;
+ timeaxisOptions.left = null;
+ timeaxisOptions.top = null;
+ timeaxisOptions.width = '100%';
+ timeaxisOptions.height = null;
+ this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions);
+ this.timeaxis.setRange(this.range);
+ this.controller.add(this.timeaxis);
-Node.prototype._drawSquare = function (ctx) {
- this._drawShape(ctx, 'square');
-};
+ // create itemset or groupset
+ this.setGroups(null);
-Node.prototype._drawStar = function (ctx) {
- this._drawShape(ctx, 'star');
-};
+ this.itemsData = null; // DataSet
+ this.groupsData = null; // DataSet
-Node.prototype._resizeShape = function (ctx) {
- if (!this.width) {
- var size = 2 * this.radius;
- this.width = size;
- this.height = size;
+ // set data
+ if (items) {
+ this.setItems(items);
}
-};
+}
-Node.prototype._drawShape = function (ctx, shape) {
- this._resizeShape(ctx);
+/**
+ * Set options
+ * @param {Object} options TODO: describe the available options
+ */
+Timeline.prototype.setOptions = function (options) {
+ if (options) {
+ util.extend(this.options, options);
+ }
- this.left = this.x - this.width / 2;
- this.top = this.y - this.height / 2;
+ // TODO: apply range min,max
- ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
- ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
- ctx.lineWidth = this.selected ? 2.0 : 1.0;
+ this.controller.reflow();
+ this.controller.repaint();
+};
- ctx[shape](this.x, this.y, this.radius);
- ctx.fill();
- ctx.stroke();
+/**
+ * Set items
+ * @param {vis.DataSet | Array | DataTable | null} items
+ */
+Timeline.prototype.setItems = function(items) {
+ var initialLoad = (this.itemsData == null);
- if (this.label) {
- this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top');
+ // convert to type DataSet when needed
+ var newItemSet;
+ if (!items) {
+ newItemSet = null;
}
-};
-
-Node.prototype._resizeText = function (ctx) {
- if (!this.width) {
- var margin = 5;
- var textSize = this.getTextSize(ctx);
- this.width = textSize.width + 2 * margin;
- this.height = textSize.height + 2 * margin;
+ else if (items instanceof DataSet) {
+ newItemSet = items;
+ }
+ if (!(items instanceof DataSet)) {
+ newItemSet = new DataSet({
+ convert: {
+ start: 'Date',
+ end: 'Date'
+ }
+ });
+ newItemSet.add(items);
}
-};
-
-Node.prototype._drawText = function (ctx) {
- this._resizeText(ctx);
- this.left = this.x - this.width / 2;
- this.top = this.y - this.height / 2;
- this._label(ctx, this.label, this.x, this.y);
-};
+ // set items
+ this.itemsData = newItemSet;
+ this.content.setItems(newItemSet);
+ if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
+ // apply the data range as range
+ var dataRange = this.getItemRange();
-Node.prototype._label = function (ctx, text, x, y, align, baseline) {
- if (text) {
- ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
- ctx.fillStyle = this.fontColor || "black";
- ctx.textAlign = align || "center";
- ctx.textBaseline = baseline || "middle";
+ // add 5% on both sides
+ var min = dataRange.min;
+ var max = dataRange.max;
+ if (min != null && max != null) {
+ var interval = (max.valueOf() - min.valueOf());
+ if (interval <= 0) {
+ // prevent an empty interval
+ interval = 24 * 60 * 60 * 1000; // 1 day
+ }
+ min = new Date(min.valueOf() - interval * 0.05);
+ max = new Date(max.valueOf() + interval * 0.05);
+ }
- var lines = text.split('\n'),
- lineCount = lines.length,
- fontSize = (this.fontSize + 4),
- yLine = y + (1 - lineCount) / 2 * fontSize;
+ // override specified start and/or end date
+ if (this.options.start != undefined) {
+ min = new Date(this.options.start.valueOf());
+ }
+ if (this.options.end != undefined) {
+ max = new Date(this.options.end.valueOf());
+ }
- for (var i = 0; i < lineCount; i++) {
- ctx.fillText(lines[i], x, yLine);
- yLine += fontSize;
+ // apply range if there is a min or max available
+ if (min != null || max != null) {
+ this.range.setRange(min, max);
}
}
};
+/**
+ * Set groups
+ * @param {vis.DataSet | Array | DataTable} groups
+ */
+Timeline.prototype.setGroups = function(groups) {
+ var me = this;
+ this.groupsData = groups;
-Node.prototype.getTextSize = function(ctx) {
- if (this.label != undefined) {
- ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
-
- var lines = this.label.split('\n'),
- height = (this.fontSize + 4) * lines.length,
- width = 0;
-
- for (var i = 0, iMax = lines.length; i < iMax; i++) {
- width = Math.max(width, ctx.measureText(lines[i]).width);
+ // switch content type between ItemSet or GroupSet when needed
+ var type = this.groupsData ? GroupSet : ItemSet;
+ if (!(this.content instanceof type)) {
+ // remove old content set
+ if (this.content) {
+ this.content.hide();
+ if (this.content.setItems) {
+ this.content.setItems(); // disconnect from items
+ }
+ if (this.content.setGroups) {
+ this.content.setGroups(); // disconnect from groups
+ }
+ this.controller.remove(this.content);
}
- return {"width": width, "height": height};
- }
- else {
- return {"width": 0, "height": 0};
+ // create new content set
+ var options = Object.create(this.options);
+ util.extend(options, {
+ top: function () {
+ if (me.options.orientation == 'top') {
+ return me.timeaxis.height;
+ }
+ else {
+ return me.itemPanel.height - me.timeaxis.height - me.content.height;
+ }
+ },
+ left: null,
+ width: '100%',
+ height: function () {
+ if (me.options.height) {
+ return me.itemPanel.height - me.timeaxis.height;
+ }
+ else {
+ return null;
+ }
+ },
+ maxHeight: function () {
+ if (me.options.maxHeight) {
+ if (!util.isNumber(me.options.maxHeight)) {
+ throw new TypeError('Number expected for property maxHeight');
+ }
+ return me.options.maxHeight - me.timeaxis.height;
+ }
+ else {
+ return null;
+ }
+ },
+ labelContainer: function () {
+ return me.labelPanel.getContainer();
+ }
+ });
+ this.content = new type(this.itemPanel, [this.timeaxis], options);
+ if (this.content.setRange) {
+ this.content.setRange(this.range);
+ }
+ if (this.content.setItems) {
+ this.content.setItems(this.itemsData);
+ }
+ if (this.content.setGroups) {
+ this.content.setGroups(this.groupsData);
+ }
+ this.controller.add(this.content);
}
};
/**
- * @class Edge
- *
- * A edge connects two nodes
- * @param {Object} properties Object with properties. Must contain
- * At least properties from and to.
- * Available properties: from (number),
- * to (number), label (string, color (string),
- * width (number), style (string),
- * length (number), title (string)
- * @param {Graph} graph A graph object, used to find and edge to
- * nodes.
- * @param {Object} constants An object with default values for
- * example for the color
+ * Get the data range of the item set.
+ * @returns {{min: Date, max: Date}} range A range with a start and end Date.
+ * When no minimum is found, min==null
+ * When no maximum is found, max==null
*/
-function Edge (properties, graph, constants) {
- if (!graph) {
- throw "No graph provided";
+Timeline.prototype.getItemRange = function getItemRange() {
+ // calculate min from start filed
+ var itemsData = this.itemsData,
+ min = null,
+ max = null;
+
+ if (itemsData) {
+ // calculate the minimum value of the field 'start'
+ var minItem = itemsData.min('start');
+ min = minItem ? minItem.start.valueOf() : null;
+
+ // calculate maximum value of fields 'start' and 'end'
+ var maxStartItem = itemsData.max('start');
+ if (maxStartItem) {
+ max = maxStartItem.start.valueOf();
+ }
+ var maxEndItem = itemsData.max('end');
+ if (maxEndItem) {
+ if (max == null) {
+ max = maxEndItem.end.valueOf();
+ }
+ else {
+ max = Math.max(max, maxEndItem.end.valueOf());
+ }
+ }
+ }
+
+ return {
+ min: (min != null) ? new Date(min) : null,
+ max: (max != null) ? new Date(max) : null
+ };
+};
+
+(function(exports) {
+ /**
+ * Parse a text source containing data in DOT language into a JSON object.
+ * The object contains two lists: one with nodes and one with edges.
+ *
+ * DOT language reference: http://www.graphviz.org/doc/info/lang.html
+ *
+ * @param {String} data Text containing a graph in DOT-notation
+ * @return {Object} graph An object containing two parameters:
+ * {Object[]} nodes
+ * {Object[]} edges
+ */
+ function parseDOT (data) {
+ dot = data;
+ return parseGraph();
}
- this.graph = graph;
- // initialize constants
- this.widthMin = constants.edges.widthMin;
- this.widthMax = constants.edges.widthMax;
+ // token types enumeration
+ var TOKENTYPE = {
+ NULL : 0,
+ DELIMITER : 1,
+ IDENTIFIER: 2,
+ UNKNOWN : 3
+ };
- // initialize variables
- this.id = undefined;
- this.fromId = undefined;
- this.toId = undefined;
- this.style = constants.edges.style;
- this.title = undefined;
- this.width = constants.edges.width;
- this.value = undefined;
- this.length = constants.edges.length;
+ // map with all delimiters
+ var DELIMITERS = {
+ '{': true,
+ '}': true,
+ '[': true,
+ ']': true,
+ ';': true,
+ '=': true,
+ ',': true,
- this.from = null; // a node
- this.to = null; // a node
- this.connected = false;
+ '->': true,
+ '--': true
+ };
- // Added to support dashed lines
- // David Jordan
- // 2012-08-08
- this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
+ var dot = ''; // current dot file
+ var index = 0; // current index in dot file
+ var c = ''; // current token character in expr
+ var token = ''; // current token
+ var tokenType = TOKENTYPE.NULL; // type of the token
- this.stiffness = undefined; // depends on the length of the edge
- this.color = constants.edges.color;
- this.widthFixed = false;
- this.lengthFixed = false;
+ /**
+ * Get the first character from the dot file.
+ * The character is stored into the char c. If the end of the dot file is
+ * reached, the function puts an empty string in c.
+ */
+ function first() {
+ index = 0;
+ c = dot.charAt(0);
+ }
- this.setProperties(properties, constants);
-}
+ /**
+ * Get the next character from the dot file.
+ * The character is stored into the char c. If the end of the dot file is
+ * reached, the function puts an empty string in c.
+ */
+ function next() {
+ index++;
+ c = dot.charAt(index);
+ }
-/**
- * Set or overwrite properties for the edge
- * @param {Object} properties an object with properties
- * @param {Object} constants and object with default, global properties
- */
-Edge.prototype.setProperties = function(properties, constants) {
- if (!properties) {
- return;
+ /**
+ * Preview the next character from the dot file.
+ * @return {String} cNext
+ */
+ function nextPreview() {
+ return dot.charAt(index + 1);
}
- if (properties.from != undefined) {this.fromId = properties.from;}
- if (properties.to != undefined) {this.toId = properties.to;}
+ /**
+ * Test whether given character is alphabetic or numeric
+ * @param {String} c
+ * @return {Boolean} isAlphaNumeric
+ */
+ var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
+ function isAlphaNumeric(c) {
+ return regexAlphaNumeric.test(c);
+ }
- if (properties.id != undefined) {this.id = properties.id;}
- if (properties.style != undefined) {this.style = properties.style;}
- if (properties.label != undefined) {this.label = properties.label;}
- if (this.label) {
- this.fontSize = constants.edges.fontSize;
- this.fontFace = constants.edges.fontFace;
- this.fontColor = constants.edges.fontColor;
- if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;}
- if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;}
- if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;}
+ /**
+ * Merge all properties of object b into object b
+ * @param {Object} a
+ * @param {Object} b
+ * @return {Object} a
+ */
+ function merge (a, b) {
+ if (!a) {
+ a = {};
+ }
+
+ if (b) {
+ for (var name in b) {
+ if (b.hasOwnProperty(name)) {
+ a[name] = b[name];
+ }
+ }
+ }
+ return a;
}
- if (properties.title != undefined) {this.title = properties.title;}
- if (properties.width != undefined) {this.width = properties.width;}
- if (properties.value != undefined) {this.value = properties.value;}
- if (properties.length != undefined) {this.length = properties.length;}
- // Added to support dashed lines
- // David Jordan
- // 2012-08-08
- if (properties.dash) {
- if (properties.dash.length != undefined) {this.dash.length = properties.dash.length;}
- if (properties.dash.gap != undefined) {this.dash.gap = properties.dash.gap;}
- if (properties.dash.altLength != undefined) {this.dash.altLength = properties.dash.altLength;}
+ /**
+ * Set a value in an object, where the provided parameter name can be a
+ * path with nested parameters. For example:
+ *
+ * var obj = {a: 2};
+ * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
+ *
+ * @param {Object} obj
+ * @param {String} path A parameter name or dot-separated parameter path,
+ * like "color.highlight.border".
+ * @param {*} value
+ */
+ function setValue(obj, path, value) {
+ var keys = path.split('.');
+ var o = obj;
+ while (keys.length) {
+ var key = keys.shift();
+ if (keys.length) {
+ // this isn't the end point
+ if (!o[key]) {
+ o[key] = {};
+ }
+ o = o[key];
+ }
+ else {
+ // this is the end point
+ o[key] = value;
+ }
+ }
}
-
- if (properties.color != undefined) {this.color = properties.color;}
- // A node is connected when it has a from and to node.
- this.connect();
+ /**
+ * Add a node to a graph object. If there is already a node with
+ * the same id, their attributes will be merged.
+ * @param {Object} graph
+ * @param {Object} node
+ */
+ function addNode(graph, node) {
+ var i, len;
+ var current = null;
- this.widthFixed = this.widthFixed || (properties.width != undefined);
- this.lengthFixed = this.lengthFixed || (properties.length != undefined);
- this.stiffness = 1 / this.length;
+ // find root graph (in case of subgraph)
+ var graphs = [graph]; // list with all graphs from current graph to root graph
+ var root = graph;
+ while (root.parent) {
+ graphs.push(root.parent);
+ root = root.parent;
+ }
- // set draw method based on style
- switch (this.style) {
- case 'line': this.draw = this._drawLine; break;
- case 'arrow': this.draw = this._drawArrow; break;
- case 'arrow-center': this.draw = this._drawArrowCenter; break;
- case 'dash-line': this.draw = this._drawDashLine; break;
- default: this.draw = this._drawLine; break;
- }
-};
+ // find existing node (at root level) by its id
+ if (root.nodes) {
+ for (i = 0, len = root.nodes.length; i < len; i++) {
+ if (node.id === root.nodes[i].id) {
+ current = root.nodes[i];
+ break;
+ }
+ }
+ }
-/**
- * Connect an edge to its nodes
- */
-Edge.prototype.connect = function () {
- this.disconnect();
+ if (!current) {
+ // this is a new node
+ current = {
+ id: node.id
+ };
+ if (graph.node) {
+ // clone default attributes
+ current.attr = merge(current.attr, graph.node);
+ }
+ }
- this.from = this.graph.nodes[this.fromId] || null;
- this.to = this.graph.nodes[this.toId] || null;
- this.connected = (this.from && this.to);
+ // add node to this (sub)graph and all its parent graphs
+ for (i = graphs.length - 1; i >= 0; i--) {
+ var g = graphs[i];
- if (this.connected) {
- this.from.attachEdge(this);
- this.to.attachEdge(this);
- }
- else {
- if (this.from) {
- this.from.detachEdge(this);
+ if (!g.nodes) {
+ g.nodes = [];
+ }
+ if (g.nodes.indexOf(current) == -1) {
+ g.nodes.push(current);
+ }
}
- if (this.to) {
- this.to.detachEdge(this);
+
+ // merge attributes
+ if (node.attr) {
+ current.attr = merge(current.attr, node.attr);
}
}
-};
-/**
- * Disconnect an edge from its nodes
- */
-Edge.prototype.disconnect = function () {
- if (this.from) {
- this.from.detachEdge(this);
- this.from = null;
- }
- if (this.to) {
- this.to.detachEdge(this);
- this.to = null;
+ /**
+ * Add an edge to a graph object
+ * @param {Object} graph
+ * @param {Object} edge
+ */
+ function addEdge(graph, edge) {
+ if (!graph.edges) {
+ graph.edges = [];
+ }
+ graph.edges.push(edge);
+ if (graph.edge) {
+ var attr = merge({}, graph.edge); // clone default attributes
+ edge.attr = merge(attr, edge.attr); // merge attributes
+ }
}
- this.connected = false;
-};
-
-/**
- * get the title of this edge.
- * @return {string} title The title of the edge, or undefined when no title
- * has been set.
- */
-Edge.prototype.getTitle = function() {
- return this.title;
-};
-
+ /**
+ * Create an edge to a graph object
+ * @param {Object} graph
+ * @param {String | Number | Object} from
+ * @param {String | Number | Object} to
+ * @param {String} type
+ * @param {Object | null} attr
+ * @return {Object} edge
+ */
+ function createEdge(graph, from, to, type, attr) {
+ var edge = {
+ from: from,
+ to: to,
+ type: type
+ };
-/**
- * Retrieve the value of the edge. Can be undefined
- * @return {Number} value
- */
-Edge.prototype.getValue = function() {
- return this.value;
-};
+ if (graph.edge) {
+ edge.attr = merge({}, graph.edge); // clone default attributes
+ }
+ edge.attr = merge(edge.attr || {}, attr); // merge attributes
-/**
- * Adjust the value range of the edge. The edge will adjust it's width
- * based on its value.
- * @param {Number} min
- * @param {Number} max
- */
-Edge.prototype.setValueRange = function(min, max) {
- if (!this.widthFixed && this.value !== undefined) {
- var factor = (this.widthMax - this.widthMin) / (max - min);
- this.width = (this.value - min) * factor + this.widthMin;
+ return edge;
}
-};
-/**
- * Redraw a edge
- * Draw this edge in the given canvas
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
- * @param {CanvasRenderingContext2D} ctx
- */
-Edge.prototype.draw = function(ctx) {
- throw "Method draw not initialized in edge";
-};
+ /**
+ * Get next token in the current dot file.
+ * The token and token type are available as token and tokenType
+ */
+ function getToken() {
+ tokenType = TOKENTYPE.NULL;
+ token = '';
-/**
- * Check if this object is overlapping with the provided object
- * @param {Object} obj an object with parameters left, top
- * @return {boolean} True if location is located on the edge
- */
-Edge.prototype.isOverlappingWith = function(obj) {
- var distMax = 10;
+ // skip over whitespaces
+ while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
+ next();
+ }
- var xFrom = this.from.x;
- var yFrom = this.from.y;
- var xTo = this.to.x;
- var yTo = this.to.y;
- var xObj = obj.left;
- var yObj = obj.top;
+ do {
+ var isComment = false;
+ // skip comment
+ if (c == '#') {
+ // find the previous non-space character
+ var i = index - 1;
+ while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
+ i--;
+ }
+ if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
+ // the # is at the start of a line, this is indeed a line comment
+ while (c != '' && c != '\n') {
+ next();
+ }
+ isComment = true;
+ }
+ }
+ if (c == '/' && nextPreview() == '/') {
+ // skip line comment
+ while (c != '' && c != '\n') {
+ next();
+ }
+ isComment = true;
+ }
+ if (c == '/' && nextPreview() == '*') {
+ // skip block comment
+ while (c != '') {
+ if (c == '*' && nextPreview() == '/') {
+ // end of block comment found. skip these last two characters
+ next();
+ next();
+ break;
+ }
+ else {
+ next();
+ }
+ }
+ isComment = true;
+ }
- var dist = Edge._dist(xFrom, yFrom, xTo, yTo, xObj, yObj);
+ // skip over whitespaces
+ while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
+ next();
+ }
+ }
+ while (isComment);
- return (dist < distMax);
-};
+ // check for end of dot file
+ if (c == '') {
+ // token is still empty
+ tokenType = TOKENTYPE.DELIMITER;
+ return;
+ }
+ // check for delimiters consisting of 2 characters
+ var c2 = c + nextPreview();
+ if (DELIMITERS[c2]) {
+ tokenType = TOKENTYPE.DELIMITER;
+ token = c2;
+ next();
+ next();
+ return;
+ }
-/**
- * Redraw a edge as a line
- * Draw this edge in the given canvas
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
- * @param {CanvasRenderingContext2D} ctx
- * @private
- */
-Edge.prototype._drawLine = function(ctx) {
- // set style
- ctx.strokeStyle = this.color;
- ctx.lineWidth = this._getLineWidth();
+ // check for delimiters consisting of 1 character
+ if (DELIMITERS[c]) {
+ tokenType = TOKENTYPE.DELIMITER;
+ token = c;
+ next();
+ return;
+ }
- var point;
- if (this.from != this.to) {
- // draw line
- this._line(ctx);
+ // check for an identifier (number or string)
+ // TODO: more precise parsing of numbers/strings (and the port separator ':')
+ if (isAlphaNumeric(c) || c == '-') {
+ token += c;
+ next();
- // draw label
- if (this.label) {
- point = this._pointOnLine(0.5);
- this._label(ctx, this.label, point.x, point.y);
- }
- }
- else {
- var x, y;
- var radius = this.length / 4;
- var node = this.from;
- if (!node.width) {
- node.resize(ctx);
+ while (isAlphaNumeric(c)) {
+ token += c;
+ next();
+ }
+ if (token == 'false') {
+ token = false; // convert to boolean
+ }
+ else if (token == 'true') {
+ token = true; // convert to boolean
+ }
+ else if (!isNaN(Number(token))) {
+ token = Number(token); // convert to number
+ }
+ tokenType = TOKENTYPE.IDENTIFIER;
+ return;
}
- if (node.width > node.height) {
- x = node.x + node.width / 2;
- y = node.y - radius;
+
+ // check for a string enclosed by double quotes
+ if (c == '"') {
+ next();
+ while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
+ token += c;
+ if (c == '"') { // skip the escape character
+ next();
+ }
+ next();
+ }
+ if (c != '"') {
+ throw newSyntaxError('End of string " expected');
+ }
+ next();
+ tokenType = TOKENTYPE.IDENTIFIER;
+ return;
}
- else {
- x = node.x + radius;
- y = node.y - node.height / 2;
+
+ // something unknown is found, wrong characters, a syntax error
+ tokenType = TOKENTYPE.UNKNOWN;
+ while (c != '') {
+ token += c;
+ next();
}
- this._circle(ctx, x, y, radius);
- point = this._pointOnCircle(x, y, radius, 0.5);
- this._label(ctx, this.label, point.x, point.y);
+ throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
}
-};
-/**
- * Get the line width of the edge. Depends on width and whether one of the
- * connected nodes is selected.
- * @return {Number} width
- * @private
- */
-Edge.prototype._getLineWidth = function() {
- if (this.from.selected || this.to.selected) {
- return Math.min(this.width * 2, this.widthMax);
- }
- else {
- return this.width;
- }
-};
+ /**
+ * Parse a graph.
+ * @returns {Object} graph
+ */
+ function parseGraph() {
+ var graph = {};
-/**
- * Draw a line between two nodes
- * @param {CanvasRenderingContext2D} ctx
- * @private
- */
-Edge.prototype._line = function (ctx) {
- // draw a straight line
- ctx.beginPath();
- ctx.moveTo(this.from.x, this.from.y);
- ctx.lineTo(this.to.x, this.to.y);
- ctx.stroke();
-};
+ first();
+ getToken();
-/**
- * Draw a line from a node to itself, a circle
- * @param {CanvasRenderingContext2D} ctx
- * @param {Number} x
- * @param {Number} y
- * @param {Number} radius
- * @private
- */
-Edge.prototype._circle = function (ctx, x, y, radius) {
- // draw a circle
- ctx.beginPath();
- ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
- ctx.stroke();
-};
+ // optional strict keyword
+ if (token == 'strict') {
+ graph.strict = true;
+ getToken();
+ }
-/**
- * Draw label with white background and with the middle at (x, y)
- * @param {CanvasRenderingContext2D} ctx
- * @param {String} text
- * @param {Number} x
- * @param {Number} y
- * @private
- */
-Edge.prototype._label = function (ctx, text, x, y) {
- if (text) {
- // TODO: cache the calculated size
- ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
- this.fontSize + "px " + this.fontFace;
- ctx.fillStyle = 'white';
- var width = ctx.measureText(text).width;
- var height = this.fontSize;
- var left = x - width / 2;
- var top = y - height / 2;
+ // graph or digraph keyword
+ if (token == 'graph' || token == 'digraph') {
+ graph.type = token;
+ getToken();
+ }
- ctx.fillRect(left, top, width, height);
+ // optional graph id
+ if (tokenType == TOKENTYPE.IDENTIFIER) {
+ graph.id = token;
+ getToken();
+ }
- // draw text
- ctx.fillStyle = this.fontColor || "black";
- ctx.textAlign = "left";
- ctx.textBaseline = "top";
- ctx.fillText(text, left, top);
- }
-};
+ // open angle bracket
+ if (token != '{') {
+ throw newSyntaxError('Angle bracket { expected');
+ }
+ getToken();
-/**
- * Redraw a edge as a dashed line
- * Draw this edge in the given canvas
- * @author David Jordan
- * @date 2012-08-08
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
- * @param {CanvasRenderingContext2D} ctx
- * @private
- */
-Edge.prototype._drawDashLine = function(ctx) {
- // set style
- ctx.strokeStyle = this.color;
- ctx.lineWidth = this._getLineWidth();
+ // statements
+ parseStatements(graph);
- // draw dashed line
- ctx.beginPath();
- ctx.lineCap = 'round';
- if (this.dash.altLength != undefined) //If an alt dash value has been set add to the array this value
- {
- ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
- [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
- }
- else if (this.dash.length != undefined && this.dash.gap != undefined) //If a dash and gap value has been set add to the array this value
- {
- ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
- [this.dash.length,this.dash.gap]);
- }
- else //If all else fails draw a line
- {
- ctx.moveTo(this.from.x, this.from.y);
- ctx.lineTo(this.to.x, this.to.y);
- }
- ctx.stroke();
+ // close angle bracket
+ if (token != '}') {
+ throw newSyntaxError('Angle bracket } expected');
+ }
+ getToken();
- // draw label
- if (this.label) {
- var point = this._pointOnLine(0.5);
- this._label(ctx, this.label, point.x, point.y);
- }
-};
+ // end of file
+ if (token !== '') {
+ throw newSyntaxError('End of file expected');
+ }
+ getToken();
-/**
- * Get a point on a line
- * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
- * @return {Object} point
- * @private
- */
-Edge.prototype._pointOnLine = function (percentage) {
- return {
- x: (1 - percentage) * this.from.x + percentage * this.to.x,
- y: (1 - percentage) * this.from.y + percentage * this.to.y
- }
-};
+ // remove temporary default properties
+ delete graph.node;
+ delete graph.edge;
+ delete graph.graph;
-/**
- * Get a point on a circle
- * @param {Number} x
- * @param {Number} y
- * @param {Number} radius
- * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
- * @return {Object} point
- * @private
- */
-Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
- var angle = (percentage - 3/8) * 2 * Math.PI;
- return {
- x: x + radius * Math.cos(angle),
- y: y - radius * Math.sin(angle)
+ return graph;
}
-};
-
-/**
- * Redraw a edge as a line with an arrow halfway the line
- * Draw this edge in the given canvas
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
- * @param {CanvasRenderingContext2D} ctx
- * @private
- */
-Edge.prototype._drawArrowCenter = function(ctx) {
- var point;
- // set style
- ctx.strokeStyle = this.color;
- ctx.fillStyle = this.color;
- ctx.lineWidth = this._getLineWidth();
-
- if (this.from != this.to) {
- // draw line
- this._line(ctx);
-
- // draw an arrow halfway the line
- var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
- var length = 10 + 5 * this.width; // TODO: make customizable?
- point = this._pointOnLine(0.5);
- ctx.arrow(point.x, point.y, angle, length);
- ctx.fill();
- ctx.stroke();
- // draw label
- if (this.label) {
- point = this._pointOnLine(0.5);
- this._label(ctx, this.label, point.x, point.y);
+ /**
+ * Parse a list with statements.
+ * @param {Object} graph
+ */
+ function parseStatements (graph) {
+ while (token !== '' && token != '}') {
+ parseStatement(graph);
+ if (token == ';') {
+ getToken();
+ }
}
}
- else {
- // draw circle
- var x, y;
- var radius = this.length / 4;
- var node = this.from;
- if (!node.width) {
- node.resize(ctx);
- }
- if (node.width > node.height) {
- x = node.x + node.width / 2;
- y = node.y - radius;
+
+ /**
+ * Parse a single statement. Can be a an attribute statement, node
+ * statement, a series of node statements and edge statements, or a
+ * parameter.
+ * @param {Object} graph
+ */
+ function parseStatement(graph) {
+ // parse subgraph
+ var subgraph = parseSubgraph(graph);
+ if (subgraph) {
+ // edge statements
+ parseEdge(graph, subgraph);
+
+ return;
}
- else {
- x = node.x + radius;
- y = node.y - node.height / 2;
+
+ // parse an attribute statement
+ var attr = parseAttributeStatement(graph);
+ if (attr) {
+ return;
}
- this._circle(ctx, x, y, radius);
- // draw all arrows
- var angle = 0.2 * Math.PI;
- var length = 10 + 5 * this.width; // TODO: make customizable?
- point = this._pointOnCircle(x, y, radius, 0.5);
- ctx.arrow(point.x, point.y, angle, length);
- ctx.fill();
- ctx.stroke();
+ // parse node
+ if (tokenType != TOKENTYPE.IDENTIFIER) {
+ throw newSyntaxError('Identifier expected');
+ }
+ var id = token; // id can be a string or a number
+ getToken();
- // draw label
- if (this.label) {
- point = this._pointOnCircle(x, y, radius, 0.5);
- this._label(ctx, this.label, point.x, point.y);
+ if (token == '=') {
+ // id statement
+ getToken();
+ if (tokenType != TOKENTYPE.IDENTIFIER) {
+ throw newSyntaxError('Identifier expected');
+ }
+ graph[id] = token;
+ getToken();
+ // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
+ }
+ else {
+ parseNodeStatement(graph, id);
}
}
-};
+ /**
+ * Parse a subgraph
+ * @param {Object} graph parent graph object
+ * @return {Object | null} subgraph
+ */
+ function parseSubgraph (graph) {
+ var subgraph = null;
+ // optional subgraph keyword
+ if (token == 'subgraph') {
+ subgraph = {};
+ subgraph.type = 'subgraph';
+ getToken();
-/**
- * Redraw a edge as a line with an arrow
- * Draw this edge in the given canvas
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
- * @param {CanvasRenderingContext2D} ctx
- * @private
- */
-Edge.prototype._drawArrow = function(ctx) {
- // set style
- ctx.strokeStyle = this.color;
- ctx.fillStyle = this.color;
- ctx.lineWidth = this._getLineWidth();
+ // optional graph id
+ if (tokenType == TOKENTYPE.IDENTIFIER) {
+ subgraph.id = token;
+ getToken();
+ }
+ }
- // draw line
- var angle, length;
- if (this.from != this.to) {
- // calculate length and angle of the line
- angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
- var dx = (this.to.x - this.from.x);
- var dy = (this.to.y - this.from.y);
- var lEdge = Math.sqrt(dx * dx + dy * dy);
+ // open angle bracket
+ if (token == '{') {
+ getToken();
- var lFrom = this.from.distanceToBorder(ctx, angle + Math.PI);
- var pFrom = (lEdge - lFrom) / lEdge;
- var xFrom = (pFrom) * this.from.x + (1 - pFrom) * this.to.x;
- var yFrom = (pFrom) * this.from.y + (1 - pFrom) * this.to.y;
+ if (!subgraph) {
+ subgraph = {};
+ }
+ subgraph.parent = graph;
+ subgraph.node = graph.node;
+ subgraph.edge = graph.edge;
+ subgraph.graph = graph.graph;
- var lTo = this.to.distanceToBorder(ctx, angle);
- var pTo = (lEdge - lTo) / lEdge;
- var xTo = (1 - pTo) * this.from.x + pTo * this.to.x;
- var yTo = (1 - pTo) * this.from.y + pTo * this.to.y;
+ // statements
+ parseStatements(subgraph);
- ctx.beginPath();
- ctx.moveTo(xFrom, yFrom);
- ctx.lineTo(xTo, yTo);
- ctx.stroke();
+ // close angle bracket
+ if (token != '}') {
+ throw newSyntaxError('Angle bracket } expected');
+ }
+ getToken();
- // draw arrow at the end of the line
- length = 10 + 5 * this.width; // TODO: make customizable?
- ctx.arrow(xTo, yTo, angle, length);
- ctx.fill();
- ctx.stroke();
+ // remove temporary default properties
+ delete subgraph.node;
+ delete subgraph.edge;
+ delete subgraph.graph;
+ delete subgraph.parent;
- // draw label
- if (this.label) {
- var point = this._pointOnLine(0.5);
- this._label(ctx, this.label, point.x, point.y);
+ // register at the parent graph
+ if (!graph.subgraphs) {
+ graph.subgraphs = [];
+ }
+ graph.subgraphs.push(subgraph);
}
+
+ return subgraph;
}
- else {
- // draw circle
- var node = this.from;
- var x, y, arrow;
- var radius = this.length / 4;
- if (!node.width) {
- node.resize(ctx);
- }
- if (node.width > node.height) {
- x = node.x + node.width / 2;
- y = node.y - radius;
- arrow = {
- x: x,
- y: node.y,
- angle: 0.9 * Math.PI
- };
- }
- else {
- x = node.x + radius;
- y = node.y - node.height / 2;
- arrow = {
- x: node.x,
- y: y,
- angle: 0.6 * Math.PI
- };
- }
- ctx.beginPath();
- // TODO: do not draw a circle, but an arc
- // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
- ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
- ctx.stroke();
- // draw all arrows
- length = 10 + 5 * this.width; // TODO: make customizable?
- ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
- ctx.fill();
- ctx.stroke();
+ /**
+ * parse an attribute statement like "node [shape=circle fontSize=16]".
+ * Available keywords are 'node', 'edge', 'graph'.
+ * The previous list with default attributes will be replaced
+ * @param {Object} graph
+ * @returns {String | null} keyword Returns the name of the parsed attribute
+ * (node, edge, graph), or null if nothing
+ * is parsed.
+ */
+ function parseAttributeStatement (graph) {
+ // attribute statements
+ if (token == 'node') {
+ getToken();
- // draw label
- if (this.label) {
- point = this._pointOnCircle(x, y, radius, 0.5);
- this._label(ctx, this.label, point.x, point.y);
+ // node attributes
+ graph.node = parseAttributeList();
+ return 'node';
}
- }
-};
-
+ else if (token == 'edge') {
+ getToken();
+ // edge attributes
+ graph.edge = parseAttributeList();
+ return 'edge';
+ }
+ else if (token == 'graph') {
+ getToken();
-/**
- * Calculate the distance between a point (x3,y3) and a line segment from
- * (x1,y1) to (x2,y2).
- * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
- * @param {number} x1
- * @param {number} y1
- * @param {number} x2
- * @param {number} y2
- * @param {number} x3
- * @param {number} y3
- * @private
- */
-Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
- var px = x2-x1,
- py = y2-y1,
- something = px*px + py*py,
- u = ((x3 - x1) * px + (y3 - y1) * py) / something;
+ // graph attributes
+ graph.graph = parseAttributeList();
+ return 'graph';
+ }
- if (u > 1) {
- u = 1;
+ return null;
}
- else if (u < 0) {
- u = 0;
+
+ /**
+ * parse a node statement
+ * @param {Object} graph
+ * @param {String | Number} id
+ */
+ function parseNodeStatement(graph, id) {
+ // node statement
+ var node = {
+ id: id
+ };
+ var attr = parseAttributeList();
+ if (attr) {
+ node.attr = attr;
+ }
+ addNode(graph, node);
+
+ // edge statements
+ parseEdge(graph, id);
}
- var x = x1 + u * px,
- y = y1 + u * py,
- dx = x - x3,
- dy = y - y3;
+ /**
+ * Parse an edge or a series of edges
+ * @param {Object} graph
+ * @param {String | Number} from Id of the from node
+ */
+ function parseEdge(graph, from) {
+ while (token == '->' || token == '--') {
+ var to;
+ var type = token;
+ getToken();
- //# Note: If the actual distance does not matter,
- //# if you only want to compare what this function
- //# returns to other results of this function, you
- //# can just return the squared distance instead
- //# (i.e. remove the sqrt) to gain a little performance
+ var subgraph = parseSubgraph(graph);
+ if (subgraph) {
+ to = subgraph;
+ }
+ else {
+ if (tokenType != TOKENTYPE.IDENTIFIER) {
+ throw newSyntaxError('Identifier or subgraph expected');
+ }
+ to = token;
+ addNode(graph, {
+ id: to
+ });
+ getToken();
+ }
- return Math.sqrt(dx*dx + dy*dy);
-};
+ // parse edge attributes
+ var attr = parseAttributeList();
-/**
- * Popup is a class to create a popup window with some text
- * @param {Element} container The container object.
- * @param {Number} [x]
- * @param {Number} [y]
- * @param {String} [text]
- */
-function Popup(container, x, y, text) {
- if (container) {
- this.container = container;
- }
- else {
- this.container = document.body;
- }
- this.x = 0;
- this.y = 0;
- this.padding = 5;
+ // create edge
+ var edge = createEdge(graph, from, to, type, attr);
+ addEdge(graph, edge);
- if (x !== undefined && y !== undefined ) {
- this.setPosition(x, y);
- }
- if (text !== undefined) {
- this.setText(text);
+ from = to;
+ }
}
- // create the frame
- this.frame = document.createElement("div");
- var style = this.frame.style;
- style.position = "absolute";
- style.visibility = "hidden";
- style.border = "1px solid #666";
- style.color = "black";
- style.padding = this.padding + "px";
- style.backgroundColor = "#FFFFC6";
- style.borderRadius = "3px";
- style.MozBorderRadius = "3px";
- style.WebkitBorderRadius = "3px";
- style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
- style.whiteSpace = "nowrap";
- this.container.appendChild(this.frame);
-};
+ /**
+ * Parse a set with attributes,
+ * for example [label="1.000", shape=solid]
+ * @return {Object | null} attr
+ */
+ function parseAttributeList() {
+ var attr = null;
-/**
- * @param {number} x Horizontal position of the popup window
- * @param {number} y Vertical position of the popup window
- */
-Popup.prototype.setPosition = function(x, y) {
- this.x = parseInt(x);
- this.y = parseInt(y);
-};
+ while (token == '[') {
+ getToken();
+ attr = {};
+ while (token !== '' && token != ']') {
+ if (tokenType != TOKENTYPE.IDENTIFIER) {
+ throw newSyntaxError('Attribute name expected');
+ }
+ var name = token;
-/**
- * Set the text for the popup window. This can be HTML code
- * @param {string} text
- */
-Popup.prototype.setText = function(text) {
- this.frame.innerHTML = text;
-};
+ getToken();
+ if (token != '=') {
+ throw newSyntaxError('Equal sign = expected');
+ }
+ getToken();
-/**
- * Show the popup window
- * @param {boolean} show Optional. Show or hide the window
- */
-Popup.prototype.show = function (show) {
- if (show === undefined) {
- show = true;
- }
+ if (tokenType != TOKENTYPE.IDENTIFIER) {
+ throw newSyntaxError('Attribute value expected');
+ }
+ var value = token;
+ setValue(attr, name, value); // name can be a path
- if (show) {
- var height = this.frame.clientHeight;
- var width = this.frame.clientWidth;
- var maxHeight = this.frame.parentNode.clientHeight;
- var maxWidth = this.frame.parentNode.clientWidth;
+ getToken();
+ if (token ==',') {
+ getToken();
+ }
+ }
- var top = (this.y - height);
- if (top + height + this.padding > maxHeight) {
- top = maxHeight - height - this.padding;
- }
- if (top < this.padding) {
- top = this.padding;
+ if (token != ']') {
+ throw newSyntaxError('Bracket ] expected');
+ }
+ getToken();
}
- var left = this.x;
- if (left + width + this.padding > maxWidth) {
- left = maxWidth - width - this.padding;
- }
- if (left < this.padding) {
- left = this.padding;
- }
+ return attr;
+ }
- this.frame.style.left = left + "px";
- this.frame.style.top = top + "px";
- this.frame.style.visibility = "visible";
+ /**
+ * Create a syntax error with extra information on current token and index.
+ * @param {String} message
+ * @returns {SyntaxError} err
+ */
+ function newSyntaxError(message) {
+ return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
}
- else {
- this.hide();
+
+ /**
+ * Chop off text after a maximum length
+ * @param {String} text
+ * @param {Number} maxLength
+ * @returns {String}
+ */
+ function chop (text, maxLength) {
+ return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
}
-};
-/**
- * Hide the popup window
- */
-Popup.prototype.hide = function () {
- this.frame.style.visibility = "hidden";
-};
+ /**
+ * Execute a function fn for each pair of elements in two arrays
+ * @param {Array | *} array1
+ * @param {Array | *} array2
+ * @param {function} fn
+ */
+ function forEach2(array1, array2, fn) {
+ if (array1 instanceof Array) {
+ array1.forEach(function (elem1) {
+ if (array2 instanceof Array) {
+ array2.forEach(function (elem2) {
+ fn(elem1, elem2);
+ });
+ }
+ else {
+ fn(elem1, array2);
+ }
+ });
+ }
+ else {
+ if (array2 instanceof Array) {
+ array2.forEach(function (elem2) {
+ fn(array1, elem2);
+ });
+ }
+ else {
+ fn(array1, array2);
+ }
+ }
+ }
-/**
- * @class Groups
- * This class can store groups and properties specific for groups.
- */
-Groups = function () {
- this.clear();
- this.defaultIndex = 0;
-};
+ /**
+ * Convert a string containing a graph in DOT language into a map containing
+ * with nodes and edges in the format of graph.
+ * @param {String} data Text containing a graph in DOT-notation
+ * @return {Object} graphData
+ */
+ function DOTToGraph (data) {
+ // parse the DOT file
+ var dotData = parseDOT(data);
+ var graphData = {
+ nodes: [],
+ edges: [],
+ options: {}
+ };
+ // copy the nodes
+ if (dotData.nodes) {
+ dotData.nodes.forEach(function (dotNode) {
+ var graphNode = {
+ id: dotNode.id,
+ label: String(dotNode.label || dotNode.id)
+ };
+ merge(graphNode, dotNode.attr);
+ if (graphNode.image) {
+ graphNode.shape = 'image';
+ }
+ graphData.nodes.push(graphNode);
+ });
+ }
-/**
- * default constants for group colors
- */
-Groups.DEFAULT = [
- {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
- {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
- {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red
- {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green
- {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
- {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
- {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange
- {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
- {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
- {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint
-];
+ // copy the edges
+ if (dotData.edges) {
+ /**
+ * Convert an edge in DOT format to an edge with VisGraph format
+ * @param {Object} dotEdge
+ * @returns {Object} graphEdge
+ */
+ function convertEdge(dotEdge) {
+ var graphEdge = {
+ from: dotEdge.from,
+ to: dotEdge.to
+ };
+ merge(graphEdge, dotEdge.attr);
+ graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
+ return graphEdge;
+ }
+ dotData.edges.forEach(function (dotEdge) {
+ var from, to;
+ if (dotEdge.from instanceof Object) {
+ from = dotEdge.from.nodes;
+ }
+ else {
+ from = {
+ id: dotEdge.from
+ }
+ }
-/**
- * Clear all groups
- */
-Groups.prototype.clear = function () {
- this.groups = {};
- this.groups.length = function()
- {
- var i = 0;
- for ( var p in this ) {
- if (this.hasOwnProperty(p)) {
- i++;
- }
- }
- return i;
- }
-};
+ if (dotEdge.to instanceof Object) {
+ to = dotEdge.to.nodes;
+ }
+ else {
+ to = {
+ id: dotEdge.to
+ }
+ }
+ if (dotEdge.from instanceof Object && dotEdge.from.edges) {
+ dotEdge.from.edges.forEach(function (subEdge) {
+ var graphEdge = convertEdge(subEdge);
+ graphData.edges.push(graphEdge);
+ });
+ }
-/**
- * get group properties of a groupname. If groupname is not found, a new group
- * is added.
- * @param {*} groupname Can be a number, string, Date, etc.
- * @return {Object} group The created group, containing all group properties
- */
-Groups.prototype.get = function (groupname) {
- var group = this.groups[groupname];
+ forEach2(from, to, function (from, to) {
+ var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
+ var graphEdge = convertEdge(subEdge);
+ graphData.edges.push(graphEdge);
+ });
- if (group == undefined) {
- // create new group
- var index = this.defaultIndex % Groups.DEFAULT.length;
- this.defaultIndex++;
- group = {};
- group.color = Groups.DEFAULT[index];
- this.groups[groupname] = group;
- }
+ if (dotEdge.to instanceof Object && dotEdge.to.edges) {
+ dotEdge.to.edges.forEach(function (subEdge) {
+ var graphEdge = convertEdge(subEdge);
+ graphData.edges.push(graphEdge);
+ });
+ }
+ });
+ }
- return group;
-};
+ // copy the options
+ if (dotData.attr) {
+ graphData.options = dotData.attr;
+ }
-/**
- * Add a custom group style
- * @param {String} groupname
- * @param {Object} style An object containing borderColor,
- * backgroundColor, etc.
- * @return {Object} group The created group object
- */
-Groups.prototype.add = function (groupname, style) {
- this.groups[groupname] = style;
- if (style.color) {
- style.color = Node.parseColor(style.color);
+ return graphData;
}
- return style;
-};
-/**
- * @class Images
- * This class loads images and keeps them stored.
- */
-Images = function () {
- this.images = {};
+ // exports
+ exports.parseDOT = parseDOT;
+ exports.DOTToGraph = DOTToGraph;
- this.callback = undefined;
-};
+})(typeof util !== 'undefined' ? util : exports);
/**
- * Set an onload callback function. This will be called each time an image
- * is loaded
- * @param {function} callback
+ * Canvas shapes used by the Graph
*/
-Images.prototype.setOnloadCallback = function(callback) {
- this.callback = callback;
-};
+if (typeof CanvasRenderingContext2D !== 'undefined') {
-/**
- *
- * @param {string} url Url of the image
- * @return {Image} img The image object
- */
-Images.prototype.load = function(url) {
- var img = this.images[url];
- if (img == undefined) {
- // create the image
- var images = this;
- img = new Image();
- this.images[url] = img;
- img.onload = function() {
- if (images.callback) {
- images.callback(this);
- }
- };
- img.src = url;
- }
+ /**
+ * Draw a circle shape
+ */
+ CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
+ this.beginPath();
+ this.arc(x, y, r, 0, 2*Math.PI, false);
+ };
- return img;
-};
+ /**
+ * Draw a square shape
+ * @param {Number} x horizontal center
+ * @param {Number} y vertical center
+ * @param {Number} r size, width and height of the square
+ */
+ CanvasRenderingContext2D.prototype.square = function(x, y, r) {
+ this.beginPath();
+ this.rect(x - r, y - r, r * 2, r * 2);
+ };
-/**
- * @constructor Graph
- * Create a graph visualization, displaying nodes and edges.
- *
- * @param {Element} container The DOM element in which the Graph will
- * be created. Normally a div element.
- * @param {Object} data An object containing parameters
- * {Array} nodes
- * {Array} edges
- * @param {Object} options Options
- */
-function Graph (container, data, options) {
- // create variables and set default values
- this.containerElement = container;
- this.width = '100%';
- this.height = '100%';
- this.refreshRate = 50; // milliseconds
- this.stabilize = true; // stabilize before displaying the graph
- this.selectable = true;
+ /**
+ * Draw a triangle shape
+ * @param {Number} x horizontal center
+ * @param {Number} y vertical center
+ * @param {Number} r radius, half the length of the sides of the triangle
+ */
+ CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
+ // http://en.wikipedia.org/wiki/Equilateral_triangle
+ this.beginPath();
- // set constant values
- this.constants = {
- nodes: {
- radiusMin: 5,
- radiusMax: 20,
- radius: 5,
- distance: 100, // px
- shape: 'ellipse',
- image: undefined,
- widthMin: 16, // px
- widthMax: 64, // px
- fontColor: 'black',
- fontSize: 14, // px
- //fontFace: verdana,
- fontFace: 'arial',
- color: {
- border: '#2B7CE9',
- background: '#97C2FC',
- highlight: {
- border: '#2B7CE9',
- background: '#D2E5FF'
- }
- },
- borderColor: '#2B7CE9',
- backgroundColor: '#97C2FC',
- highlightColor: '#D2E5FF',
- group: undefined
- },
- edges: {
- widthMin: 1,
- widthMax: 15,
- width: 1,
- style: 'line',
- color: '#343434',
- fontColor: '#343434',
- fontSize: 14, // px
- fontFace: 'arial',
- //distance: 100, //px
- length: 100, // px
- dash: {
- length: 10,
- gap: 5,
- altLength: undefined
- }
- },
- minForce: 0.05,
- minVelocity: 0.02, // px/s
- maxIterations: 1000 // maximum number of iteration to stabilize
+ var s = r * 2;
+ var s2 = s / 2;
+ var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
+ var h = Math.sqrt(s * s - s2 * s2); // height
+
+ this.moveTo(x, y - (h - ir));
+ this.lineTo(x + s2, y + ir);
+ this.lineTo(x - s2, y + ir);
+ this.lineTo(x, y - (h - ir));
+ this.closePath();
};
- var graph = this;
- this.nodes = {}; // object with Node objects
- this.edges = {}; // object with Edge objects
- // TODO: create a counter to keep track on the number of nodes having values
- // TODO: create a counter to keep track on the number of nodes currently moving
- // TODO: create a counter to keep track on the number of edges having values
+ /**
+ * Draw a triangle shape in downward orientation
+ * @param {Number} x horizontal center
+ * @param {Number} y vertical center
+ * @param {Number} r radius
+ */
+ CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
+ // http://en.wikipedia.org/wiki/Equilateral_triangle
+ this.beginPath();
+
+ var s = r * 2;
+ var s2 = s / 2;
+ var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
+ var h = Math.sqrt(s * s - s2 * s2); // height
+
+ this.moveTo(x, y + (h - ir));
+ this.lineTo(x + s2, y - ir);
+ this.lineTo(x - s2, y - ir);
+ this.lineTo(x, y + (h - ir));
+ this.closePath();
+ };
- this.nodesData = null; // A DataSet or DataView
- this.edgesData = null; // A DataSet or DataView
+ /**
+ * Draw a star shape, a star with 5 points
+ * @param {Number} x horizontal center
+ * @param {Number} y vertical center
+ * @param {Number} r radius, half the length of the sides of the triangle
+ */
+ CanvasRenderingContext2D.prototype.star = function(x, y, r) {
+ // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
+ this.beginPath();
- // create event listeners used to subscribe on the DataSets of the nodes and edges
- var me = this;
- this.nodesListeners = {
- 'add': function (event, params) {
- me._addNodes(params.items);
- me.start();
- },
- 'update': function (event, params) {
- me._updateNodes(params.items);
- me.start();
- },
- 'remove': function (event, params) {
- me._removeNodes(params.items);
- me.start();
+ for (var n = 0; n < 10; n++) {
+ var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
+ this.lineTo(
+ x + radius * Math.sin(n * 2 * Math.PI / 10),
+ y - radius * Math.cos(n * 2 * Math.PI / 10)
+ );
}
+
+ this.closePath();
};
- this.edgesListeners = {
- 'add': function (event, params) {
- me._addEdges(params.items);
- me.start();
- },
- 'update': function (event, params) {
- me._updateEdges(params.items);
- me.start();
- },
- 'remove': function (event, params) {
- me._removeEdges(params.items);
- me.start();
- }
+
+ /**
+ * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
+ */
+ CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
+ var r2d = Math.PI/180;
+ if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
+ if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
+ this.beginPath();
+ this.moveTo(x+r,y);
+ this.lineTo(x+w-r,y);
+ this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
+ this.lineTo(x+w,y+h-r);
+ this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
+ this.lineTo(x+r,y+h);
+ this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
+ this.lineTo(x,y+r);
+ this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
};
- this.groups = new Groups(); // object with groups
- this.images = new Images(); // object with images
- this.images.setOnloadCallback(function () {
- graph._redraw();
- });
+ /**
+ * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
+ */
+ CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
+ var kappa = .5522848,
+ ox = (w / 2) * kappa, // control point offset horizontal
+ oy = (h / 2) * kappa, // control point offset vertical
+ xe = x + w, // x-end
+ ye = y + h, // y-end
+ xm = x + w / 2, // x-middle
+ ym = y + h / 2; // y-middle
- // properties of the data
- this.moving = false; // True if any of the nodes have an undefined position
+ this.beginPath();
+ this.moveTo(x, ym);
+ this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
+ this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
+ this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
+ this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
+ };
- this.selection = [];
- this.timer = undefined;
- // create a frame and canvas
- this._create();
- // apply options
- this.setOptions(options);
+ /**
+ * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
+ */
+ CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
+ var f = 1/3;
+ var wEllipse = w;
+ var hEllipse = h * f;
- // draw data
- this.setData(data);
-}
+ var kappa = .5522848,
+ ox = (wEllipse / 2) * kappa, // control point offset horizontal
+ oy = (hEllipse / 2) * kappa, // control point offset vertical
+ xe = x + wEllipse, // x-end
+ ye = y + hEllipse, // y-end
+ xm = x + wEllipse / 2, // x-middle
+ ym = y + hEllipse / 2, // y-middle
+ ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
+ yeb = y + h; // y-end, bottom ellipse
-/**
- * Set nodes and edges, and optionally options as well.
- *
- * @param {Object} data Object containing parameters:
- * {Array | DataSet | DataView} [nodes] Array with nodes
- * {Array | DataSet | DataView} [edges] Array with edges
- * {String} [dot] String containing data in DOT format
- * {Options} [options] Object with options
- */
-Graph.prototype.setData = function(data) {
- if (data && data.dot && (data.nodes || data.edges)) {
- throw new SyntaxError('Data must contain either parameter "dot" or ' +
- ' parameter pair "nodes" and "edges", but not both.');
- }
+ this.beginPath();
+ this.moveTo(xe, ym);
- // set options
- this.setOptions(data && data.options);
+ this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
+ this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
- // set all data
- if (data && data.dot) {
- // parse DOT file
- if(data && data.dot) {
- var dotData = vis.util.DOTToGraph(data.dot);
- this.setData(dotData);
- return;
- }
- }
- else {
- this._setNodes(data && data.nodes);
- this._setEdges(data && data.edges);
- }
+ this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
+ this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
+ this.lineTo(xe, ymb);
- // find a stable position or start animating to a stable position
- if (this.stabilize) {
- this._doStabilize();
- }
- this.start();
-};
+ this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
+ this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
-/**
- * Set options
- * @param {Object} options
- */
-Graph.prototype.setOptions = function (options) {
- if (options) {
- // retrieve parameter values
- if (options.width != undefined) {this.width = options.width;}
- if (options.height != undefined) {this.height = options.height;}
- if (options.stabilize != undefined) {this.stabilize = options.stabilize;}
- if (options.selectable != undefined) {this.selectable = options.selectable;}
+ this.lineTo(x, ym);
+ };
- // TODO: work out these options and document them
- if (options.edges) {
- for (var prop in options.edges) {
- if (options.edges.hasOwnProperty(prop)) {
- this.constants.edges[prop] = options.edges[prop];
- }
- }
- if (options.edges.length != undefined &&
- options.nodes && options.nodes.distance == undefined) {
- this.constants.edges.length = options.edges.length;
- this.constants.nodes.distance = options.edges.length * 1.25;
- }
+ /**
+ * Draw an arrow point (no line)
+ */
+ CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
+ // tail
+ var xt = x - length * Math.cos(angle);
+ var yt = y - length * Math.sin(angle);
+
+ // inner tail
+ // TODO: allow to customize different shapes
+ var xi = x - length * 0.9 * Math.cos(angle);
+ var yi = y - length * 0.9 * Math.sin(angle);
+
+ // left
+ var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
+ var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
+
+ // right
+ var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
+ var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
+
+ this.beginPath();
+ this.moveTo(x, y);
+ this.lineTo(xl, yl);
+ this.lineTo(xi, yi);
+ this.lineTo(xr, yr);
+ this.closePath();
+ };
+
+ /**
+ * Sets up the dashedLine functionality for drawing
+ * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
+ * @author David Jordan
+ * @date 2012-08-08
+ */
+ CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
+ if (!dashArray) dashArray=[10,5];
+ if (dashLength==0) dashLength = 0.001; // Hack for Safari
+ var dashCount = dashArray.length;
+ this.moveTo(x, y);
+ var dx = (x2-x), dy = (y2-y);
+ var slope = dy/dx;
+ var distRemaining = Math.sqrt( dx*dx + dy*dy );
+ var dashIndex=0, draw=true;
+ while (distRemaining>=0.1){
+ var dashLength = dashArray[dashIndex++%dashCount];
+ if (dashLength > distRemaining) dashLength = distRemaining;
+ var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
+ if (dx<0) xStep = -xStep;
+ x += xStep;
+ y += slope*xStep;
+ this[draw ? 'lineTo' : 'moveTo'](x,y);
+ distRemaining -= dashLength;
+ draw = !draw;
+ }
+ };
+
+ // TODO: add diamond shape
+}
+
+/**
+ * @class Node
+ * A node. A node can be connected to other nodes via one or multiple edges.
+ * @param {object} properties An object containing properties for the node. All
+ * properties are optional, except for the id.
+ * {number} id Id of the node. Required
+ * {string} label Text label for the node
+ * {number} x Horizontal position of the node
+ * {number} y Vertical position of the node
+ * {string} shape Node shape, available:
+ * "database", "circle", "ellipse",
+ * "box", "image", "text", "dot",
+ * "star", "triangle", "triangleDown",
+ * "square"
+ * {string} image An image url
+ * {string} title An title text, can be HTML
+ * {anytype} group A group name or number
+ * @param {Graph.Images} imagelist A list with images. Only needed
+ * when the node has an image
+ * @param {Graph.Groups} grouplist A list with groups. Needed for
+ * retrieving group properties
+ * @param {Object} constants An object with default values for
+ * example for the color
+ */
+function Node(properties, imagelist, grouplist, constants) {
+ this.selected = false;
- if (!options.edges.fontColor) {
- this.constants.edges.fontColor = options.edges.color;
- }
+ this.edges = []; // all edges connected to this node
+ this.group = constants.nodes.group;
- // Added to support dashed lines
- // David Jordan
- // 2012-08-08
- if (options.edges.dash) {
- if (options.edges.dash.length != undefined) {
- this.constants.edges.dash.length = options.edges.dash.length;
- }
- if (options.edges.dash.gap != undefined) {
- this.constants.edges.dash.gap = options.edges.dash.gap;
- }
- if (options.edges.dash.altLength != undefined) {
- this.constants.edges.dash.altLength = options.edges.dash.altLength;
- }
- }
- }
+ this.fontSize = constants.nodes.fontSize;
+ this.fontFace = constants.nodes.fontFace;
+ this.fontColor = constants.nodes.fontColor;
- if (options.nodes) {
- for (prop in options.nodes) {
- if (options.nodes.hasOwnProperty(prop)) {
- this.constants.nodes[prop] = options.nodes[prop];
- }
- }
+ this.color = constants.nodes.color;
- if (options.nodes.color) {
- this.constants.nodes.color = Node.parseColor(options.nodes.color);
- }
+ // set defaults for the properties
+ this.id = undefined;
+ this.shape = constants.nodes.shape;
+ this.image = constants.nodes.image;
+ this.x = 0;
+ this.y = 0;
+ this.xFixed = false;
+ this.yFixed = false;
+ this.radius = constants.nodes.radius;
+ this.radiusFixed = false;
+ this.radiusMin = constants.nodes.radiusMin;
+ this.radiusMax = constants.nodes.radiusMax;
- /*
- if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
- if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
- */
- }
+ this.imagelist = imagelist;
+ this.grouplist = grouplist;
- if (options.groups) {
- for (var groupname in options.groups) {
- if (options.groups.hasOwnProperty(groupname)) {
- var group = options.groups[groupname];
- this.groups.add(groupname, group);
- }
- }
- }
- }
+ this.setProperties(properties, constants);
- this.setSize(this.width, this.height);
- this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
- this._setScale(1);
+ // mass, force, velocity
+ this.mass = 50; // kg (mass is adjusted for the number of connected edges)
+ this.fx = 0.0; // external force x
+ this.fy = 0.0; // external force y
+ this.vx = 0.0; // velocity x
+ this.vy = 0.0; // velocity y
+ this.minForce = constants.minForce;
+ this.damping = 0.9; // damping factor
};
/**
- * fire an event
- * @param {String} event The name of an event, for example "select"
- * @param {Object} params Optional object with event parameters
- * @private
+ * Attach a edge to the node
+ * @param {Edge} edge
*/
-Graph.prototype._trigger = function (event, params) {
- events.trigger(this, event, params);
+Node.prototype.attachEdge = function(edge) {
+ if (this.edges.indexOf(edge) == -1) {
+ this.edges.push(edge);
+ }
+ this._updateMass();
};
-
/**
- * Create the main frame for the Graph.
- * This function is executed once when a Graph object is created. The frame
- * contains a canvas, and this canvas contains all objects like the axis and
- * nodes.
- * @private
+ * Detach a edge from the node
+ * @param {Edge} edge
*/
-Graph.prototype._create = function () {
- // remove all elements from the container element.
- while (this.containerElement.hasChildNodes()) {
- this.containerElement.removeChild(this.containerElement.firstChild);
- }
-
- this.frame = document.createElement("div");
- this.frame.className = "graph-frame";
- this.frame.style.position = "relative";
- this.frame.style.overflow = "hidden";
-
- // create the graph canvas (HTML canvas element)
- this.frame.canvas = document.createElement( "canvas" );
- this.frame.canvas.style.position = "relative";
- this.frame.appendChild(this.frame.canvas);
- if (!this.frame.canvas.getContext) {
- var noCanvas = document.createElement( "DIV" );
- noCanvas.style.color = "red";
- noCanvas.style.fontWeight = "bold" ;
- noCanvas.style.padding = "10px";
- noCanvas.innerHTML = "Error: your browser does not support HTML canvas";
- this.frame.canvas.appendChild(noCanvas);
+Node.prototype.detachEdge = function(edge) {
+ var index = this.edges.indexOf(edge);
+ if (index != -1) {
+ this.edges.splice(index, 1);
}
-
- // create event listeners
- var me = this;
- var onmousedown = function (event) {me._onMouseDown(event);};
- var onmousemove = function (event) {me._onMouseMoveTitle(event);};
- var onmousewheel = function (event) {me._onMouseWheel(event);};
- var ontouchstart = function (event) {me._onTouchStart(event);};
- vis.util.addEventListener(this.frame.canvas, "mousedown", onmousedown);
- vis.util.addEventListener(this.frame.canvas, "mousemove", onmousemove);
- vis.util.addEventListener(this.frame.canvas, "mousewheel", onmousewheel);
- vis.util.addEventListener(this.frame.canvas, "touchstart", ontouchstart);
-
- // add the frame to the container element
- this.containerElement.appendChild(this.frame);
+ this._updateMass();
};
/**
- * handle on mouse down event
+ * Update the nodes mass, which is determined by the number of edges connecting
+ * to it (more edges -> heavier node).
* @private
*/
-Graph.prototype._onMouseDown = function (event) {
- event = event || window.event;
+Node.prototype._updateMass = function() {
+ this.mass = 50 + 20 * this.edges.length; // kg
+};
- if (!this.selectable) {
+/**
+ * Set or overwrite properties for the node
+ * @param {Object} properties an object with properties
+ * @param {Object} constants and object with default, global properties
+ */
+Node.prototype.setProperties = function(properties, constants) {
+ if (!properties) {
return;
}
- // check if mouse is still down (may be up when focus is lost for example
- // in an iframe)
- if (this.leftButtonDown) {
- this._onMouseUp(event);
- }
+ // basic properties
+ if (properties.id != undefined) {this.id = properties.id;}
+ if (properties.label != undefined) {this.label = properties.label;}
+ if (properties.title != undefined) {this.title = properties.title;}
+ if (properties.group != undefined) {this.group = properties.group;}
+ if (properties.x != undefined) {this.x = properties.x;}
+ if (properties.y != undefined) {this.y = properties.y;}
+ if (properties.value != undefined) {this.value = properties.value;}
- // only react on left mouse button down
- this.leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
- if (!this.leftButtonDown && !this.touchDown) {
- return;
+ if (this.id === undefined) {
+ throw "Node must have an id";
}
- // add event listeners to handle moving the contents
- // we store the function onmousemove and onmouseup in the timeline, so we can
- // remove the eventlisteners lateron in the function mouseUp()
- var me = this;
- if (!this.onmousemove) {
- this.onmousemove = function (event) {me._onMouseMove(event);};
- vis.util.addEventListener(document, "mousemove", me.onmousemove);
- }
- if (!this.onmouseup) {
- this.onmouseup = function (event) {me._onMouseUp(event);};
- vis.util.addEventListener(document, "mouseup", me.onmouseup);
+ // copy group properties
+ if (this.group) {
+ var groupObj = this.grouplist.get(this.group);
+ for (var prop in groupObj) {
+ if (groupObj.hasOwnProperty(prop)) {
+ this[prop] = groupObj[prop];
+ }
+ }
}
- vis.util.preventDefault(event);
-
- // store the start x and y position of the mouse
- this.startMouseX = util.getPageX(event);
- this.startMouseY = util.getPageY(event);
- this.startFrameLeft = vis.util.getAbsoluteLeft(this.frame.canvas);
- this.startFrameTop = vis.util.getAbsoluteTop(this.frame.canvas);
- this.startTranslation = this._getTranslation();
- this.ctrlKeyDown = event.ctrlKey;
- this.shiftKeyDown = event.shiftKey;
-
- var obj = {
- left: this._xToCanvas(this.startMouseX - this.startFrameLeft),
- top: this._yToCanvas(this.startMouseY - this.startFrameTop),
- right: this._xToCanvas(this.startMouseX - this.startFrameLeft),
- bottom: this._yToCanvas(this.startMouseY - this.startFrameTop)
- };
- var overlappingNodes = this._getNodesOverlappingWith(obj);
- // if there are overlapping nodes, select the last one, this is the
- // one which is drawn on top of the others
- this.startClickedObj = (overlappingNodes.length > 0) ?
- overlappingNodes[overlappingNodes.length - 1] : undefined;
+ // individual shape properties
+ if (properties.shape != undefined) {this.shape = properties.shape;}
+ if (properties.image != undefined) {this.image = properties.image;}
+ if (properties.radius != undefined) {this.radius = properties.radius;}
+ if (properties.color != undefined) {this.color = Node.parseColor(properties.color);}
- if (this.startClickedObj) {
- // move clicked node with the mouse
+ if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;}
+ if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;}
+ if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;}
- // make the clicked node temporarily fixed, and store their original state
- var node = this.nodes[this.startClickedObj];
- this.startClickedObj.xFixed = node.xFixed;
- this.startClickedObj.yFixed = node.yFixed;
- node.xFixed = true;
- node.yFixed = true;
- if (!this.ctrlKeyDown || !node.isSelected()) {
- // select this node
- this._selectNodes([this.startClickedObj], this.ctrlKeyDown);
+ if (this.image != undefined) {
+ if (this.imagelist) {
+ this.imageObj = this.imagelist.load(this.image);
}
else {
- // unselect this node
- this._unselectNodes([this.startClickedObj]);
+ throw "No imagelist provided";
}
+ }
- if (!this.moving) {
- this._redraw();
- }
+ this.xFixed = this.xFixed || (properties.x != undefined);
+ this.yFixed = this.yFixed || (properties.y != undefined);
+ this.radiusFixed = this.radiusFixed || (properties.radius != undefined);
+
+ if (this.shape == 'image') {
+ this.radiusMin = constants.nodes.widthMin;
+ this.radiusMax = constants.nodes.widthMax;
+ }
+
+ // choose draw method depending on the shape
+ switch (this.shape) {
+ case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
+ case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
+ case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
+ case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
+ // TODO: add diamond shape
+ case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
+ case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
+ case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
+ case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
+ case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
+ case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
+ case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
+ default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
}
- else if (this.shiftKeyDown) {
- // start selection of multiple nodes
+
+ // reset the size of the node, this can be changed
+ this._reset();
+};
+
+/**
+ * Parse a color property into an object with border, background, and
+ * hightlight colors
+ * @param {Object | String} color
+ * @return {Object} colorObject
+ */
+Node.parseColor = function(color) {
+ var c;
+ if (util.isString(color)) {
+ c = {
+ border: color,
+ background: color,
+ highlight: {
+ border: color,
+ background: color
+ }
+ };
+ // TODO: automatically generate a nice highlight color
}
else {
- // start moving the graph
- this.moved = false;
+ c = {};
+ c.background = color.background || 'white';
+ c.border = color.border || c.background;
+ if (util.isString(color.highlight)) {
+ c.highlight = {
+ border: color.highlight,
+ background: color.highlight
+ }
+ }
+ else {
+ c.highlight = {};
+ c.highlight.background = color.highlight && color.highlight.background || c.background;
+ c.highlight.border = color.highlight && color.highlight.border || c.border;
+ }
}
+ return c;
};
/**
- * handle on mouse move event
- * @param {Event} event
- * @private
+ * select this node
*/
-Graph.prototype._onMouseMove = function (event) {
- event = event || window.event;
-
- if (!this.selectable) {
- return;
- }
+Node.prototype.select = function() {
+ this.selected = true;
+ this._reset();
+};
- var mouseX = util.getPageX(event);
- var mouseY = util.getPageY(event);
- this.mouseX = mouseX;
- this.mouseY = mouseY;
+/**
+ * unselect this node
+ */
+Node.prototype.unselect = function() {
+ this.selected = false;
+ this._reset();
+};
- if (this.startClickedObj) {
- var node = this.nodes[this.startClickedObj];
+/**
+ * Reset the calculated size of the node, forces it to recalculate its size
+ * @private
+ */
+Node.prototype._reset = function() {
+ this.width = undefined;
+ this.height = undefined;
+};
- if (!this.startClickedObj.xFixed)
- node.x = this._xToCanvas(mouseX - this.startFrameLeft);
+/**
+ * get the title of this node.
+ * @return {string} title The title of the node, or undefined when no title
+ * has been set.
+ */
+Node.prototype.getTitle = function() {
+ return this.title;
+};
- if (!this.startClickedObj.yFixed)
- node.y = this._yToCanvas(mouseY - this.startFrameTop);
+/**
+ * Calculate the distance to the border of the Node
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Number} angle Angle in radians
+ * @returns {number} distance Distance to the border in pixels
+ */
+Node.prototype.distanceToBorder = function (ctx, angle) {
+ var borderWidth = 1;
- // start animation if not yet running
- if (!this.moving) {
- this.moving = true;
- this.start();
- }
+ if (!this.width) {
+ this.resize(ctx);
}
- else if (this.shiftKeyDown) {
- // draw a rect from start mouse location to current mouse location
- if (this.frame.selRect == undefined) {
- this.frame.selRect = document.createElement("DIV");
- this.frame.appendChild(this.frame.selRect);
- this.frame.selRect.style.position = "absolute";
- this.frame.selRect.style.border = "1px dashed red";
- }
+ //noinspection FallthroughInSwitchStatementJS
+ switch (this.shape) {
+ case 'circle':
+ case 'dot':
+ return this.radius + borderWidth;
- var left = Math.min(this.startMouseX, mouseX) - this.startFrameLeft;
- var top = Math.min(this.startMouseY, mouseY) - this.startFrameTop;
- var right = Math.max(this.startMouseX, mouseX) - this.startFrameLeft;
- var bottom = Math.max(this.startMouseY, mouseY) - this.startFrameTop;
+ case 'ellipse':
+ var a = this.width / 2;
+ var b = this.height / 2;
+ var w = (Math.sin(angle) * a);
+ var h = (Math.cos(angle) * b);
+ return a * b / Math.sqrt(w * w + h * h);
- this.frame.selRect.style.left = left + "px";
- this.frame.selRect.style.top = top + "px";
- this.frame.selRect.style.width = (right - left) + "px";
- this.frame.selRect.style.height = (bottom - top) + "px";
- }
- else {
- // move the graph
- var diffX = mouseX - this.startMouseX;
- var diffY = mouseY - this.startMouseY;
+ // TODO: implement distanceToBorder for database
+ // TODO: implement distanceToBorder for triangle
+ // TODO: implement distanceToBorder for triangleDown
- this._setTranslation(
- this.startTranslation.x + diffX,
- this.startTranslation.y + diffY);
- this._redraw();
+ case 'box':
+ case 'image':
+ case 'text':
+ default:
+ if (this.width) {
+ return Math.min(
+ Math.abs(this.width / 2 / Math.cos(angle)),
+ Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
+ // TODO: reckon with border radius too in case of box
+ }
+ else {
+ return 0;
+ }
- this.moved = true;
}
- vis.util.preventDefault(event);
+ // TODO: implement calculation of distance to border for all shapes
+};
+
+/**
+ * Set forces acting on the node
+ * @param {number} fx Force in horizontal direction
+ * @param {number} fy Force in vertical direction
+ */
+Node.prototype._setForce = function(fx, fy) {
+ this.fx = fx;
+ this.fy = fy;
};
/**
- * handle on mouse up event
- * @param {Event} event
+ * Add forces acting on the node
+ * @param {number} fx Force in horizontal direction
+ * @param {number} fy Force in vertical direction
* @private
*/
-Graph.prototype._onMouseUp = function (event) {
- event = event || window.event;
+Node.prototype._addForce = function(fx, fy) {
+ this.fx += fx;
+ this.fy += fy;
+};
- if (!this.selectable) {
- return;
+/**
+ * Perform one discrete step for the node
+ * @param {number} interval Time interval in seconds
+ */
+Node.prototype.discreteStep = function(interval) {
+ if (!this.xFixed) {
+ var dx = -this.damping * this.vx; // damping force
+ var ax = (this.fx + dx) / this.mass; // acceleration
+ this.vx += ax / interval; // velocity
+ this.x += this.vx / interval; // position
}
- // remove event listeners here, important for Safari
- if (this.onmousemove) {
- vis.util.removeEventListener(document, "mousemove", this.onmousemove);
- this.onmousemove = undefined;
- }
- if (this.onmouseup) {
- vis.util.removeEventListener(document, "mouseup", this.onmouseup);
- this.onmouseup = undefined;
- }
- vis.util.preventDefault(event);
-
- // check selected nodes
- var endMouseX = util.getPageX(event) || this.mouseX || 0;
- var endMouseY = util.getPageY(event) || this.mouseY || 0;
-
- var ctrlKey = event ? event.ctrlKey : window.event.ctrlKey;
-
- if (this.startClickedObj) {
- // restore the original fixed state
- var node = this.nodes[this.startClickedObj];
- node.xFixed = this.startClickedObj.xFixed;
- node.yFixed = this.startClickedObj.yFixed;
- }
- else if (this.shiftKeyDown) {
- // select nodes inside selection area
- var obj = {
- "left": this._xToCanvas(Math.min(this.startMouseX, endMouseX) - this.startFrameLeft),
- "top": this._yToCanvas(Math.min(this.startMouseY, endMouseY) - this.startFrameTop),
- "right": this._xToCanvas(Math.max(this.startMouseX, endMouseX) - this.startFrameLeft),
- "bottom": this._yToCanvas(Math.max(this.startMouseY, endMouseY) - this.startFrameTop)
- };
- var overlappingNodes = this._getNodesOverlappingWith(obj);
- this._selectNodes(overlappingNodes, ctrlKey);
- this.redraw();
-
- // remove the selection rectangle
- if (this.frame.selRect) {
- this.frame.removeChild(this.frame.selRect);
- this.frame.selRect = undefined;
- }
- }
- else {
- if (!this.ctrlKeyDown && !this.moved) {
- // remove selection
- this._unselectNodes();
- this._redraw();
- }
+ if (!this.yFixed) {
+ var dy = -this.damping * this.vy; // damping force
+ var ay = (this.fy + dy) / this.mass; // acceleration
+ this.vy += ay / interval; // velocity
+ this.y += this.vy / interval; // position
}
-
- this.leftButtonDown = false;
- this.ctrlKeyDown = false;
};
/**
- * Event handler for mouse wheel event, used to zoom the timeline
- * Code from http://adomas.org/javascript-mouse-wheel/
- * @param {Event} event
- * @private
+ * Check if this node has a fixed x and y position
+ * @return {boolean} true if fixed, false if not
*/
-Graph.prototype._onMouseWheel = function(event) {
- event = event || window.event;
- var mouseX = util.getPageX(event);
- var mouseY = util.getPageY(event);
+Node.prototype.isFixed = function() {
+ return (this.xFixed && this.yFixed);
+};
- // 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;
- }
+/**
+ * Check if this node is moving
+ * @param {number} vmin the minimum velocity considered as "moving"
+ * @return {boolean} true if moving, false if it has no velocity
+ */
+// TODO: replace this method with calculating the kinetic energy
+Node.prototype.isMoving = function(vmin) {
+ return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin ||
+ (!this.xFixed && Math.abs(this.fx) > this.minForce) ||
+ (!this.yFixed && Math.abs(this.fy) > this.minForce));
+};
- // 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) {
- // determine zoom factor, and adjust the zoom factor such that zooming in
- // and zooming out correspond wich each other
- var zoom = delta / 10;
- if (delta < 0) {
- zoom = zoom / (1 - zoom);
- }
+/**
+ * check if this node is selecte
+ * @return {boolean} selected True if node is selected, else false
+ */
+Node.prototype.isSelected = function() {
+ return this.selected;
+};
- var scaleOld = this._getScale();
- var scaleNew = scaleOld * (1 + zoom);
- if (scaleNew < 0.01) {
- scaleNew = 0.01;
- }
- if (scaleNew > 10) {
- scaleNew = 10;
- }
+/**
+ * Retrieve the value of the node. Can be undefined
+ * @return {Number} value
+ */
+Node.prototype.getValue = function() {
+ return this.value;
+};
- var frameLeft = vis.util.getAbsoluteLeft(this.frame.canvas);
- var frameTop = vis.util.getAbsoluteTop(this.frame.canvas);
- var x = mouseX - frameLeft;
- var y = mouseY - frameTop;
+/**
+ * Calculate the distance from the nodes location to the given location (x,y)
+ * @param {Number} x
+ * @param {Number} y
+ * @return {Number} value
+ */
+Node.prototype.getDistance = function(x, y) {
+ var dx = this.x - x,
+ dy = this.y - y;
+ return Math.sqrt(dx * dx + dy * dy);
+};
- var translation = this._getTranslation();
- var scaleFrac = scaleNew / scaleOld;
- var tx = (1 - scaleFrac) * x + translation.x * scaleFrac;
- var ty = (1 - scaleFrac) * y + translation.y * scaleFrac;
- this._setScale(scaleNew);
- this._setTranslation(tx, ty);
- this._redraw();
+/**
+ * Adjust the value range of the node. The node will adjust it's radius
+ * based on its value.
+ * @param {Number} min
+ * @param {Number} max
+ */
+Node.prototype.setValueRange = function(min, max) {
+ if (!this.radiusFixed && this.value !== undefined) {
+ var scale = (this.radiusMax - this.radiusMin) / (max - min);
+ this.radius = (this.value - min) * scale + this.radiusMin;
}
-
- // Prevent default actions caused by mouse wheel.
- // That might be ugly, but we handle scrolls somehow
- // anyway, so don't bother here...
- vis.util.preventDefault(event);
};
-
/**
- * Mouse move handler for checking whether the title moves over a node with a title.
- * @param {Event} event
- * @private
+ * Draw this node in the given canvas
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
+ * @param {CanvasRenderingContext2D} ctx
*/
-Graph.prototype._onMouseMoveTitle = function (event) {
- event = event || window.event;
-
- var startMouseX = util.getPageX(event);
- var startMouseY = util.getPageY(event);
- this.startFrameLeft = this.startFrameLeft || vis.util.getAbsoluteLeft(this.frame.canvas);
- this.startFrameTop = this.startFrameTop || vis.util.getAbsoluteTop(this.frame.canvas);
-
- var x = startMouseX - this.startFrameLeft;
- var y = startMouseY - this.startFrameTop;
-
- // check if the previously selected node is still selected
- if (this.popupNode) {
- this._checkHidePopup(x, y);
- }
+Node.prototype.draw = function(ctx) {
+ throw "Draw method not initialized for node";
+};
- // start a timeout that will check if the mouse is positioned above
- // an element
- var me = this;
- var checkShow = function() {
- me._checkShowPopup(x, y);
- };
- if (this.popupTimer) {
- clearInterval(this.popupTimer); // stop any running timer
- }
- if (!this.leftButtonDown) {
- this.popupTimer = setTimeout(checkShow, 300);
- }
+/**
+ * Recalculate the size of this node in the given canvas
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
+ * @param {CanvasRenderingContext2D} ctx
+ */
+Node.prototype.resize = function(ctx) {
+ throw "Resize method not initialized for node";
};
/**
- * Check if there is an element on the given position in the graph
- * (a node or edge). If so, and if this element has a title,
- * show a popup window with its title.
- *
- * @param {number} x
- * @param {number} y
- * @private
+ * Check if this object is overlapping with the provided object
+ * @param {Object} obj an object with parameters left, top, right, bottom
+ * @return {boolean} True if location is located on node
*/
-Graph.prototype._checkShowPopup = function (x, y) {
- var obj = {
- "left" : this._xToCanvas(x),
- "top" : this._yToCanvas(y),
- "right" : this._xToCanvas(x),
- "bottom" : this._yToCanvas(y)
- };
-
- var id;
- var lastPopupNode = this.popupNode;
+Node.prototype.isOverlappingWith = function(obj) {
+ return (this.left < obj.right &&
+ this.left + this.width > obj.left &&
+ this.top < obj.bottom &&
+ this.top + this.height > obj.top);
+};
- if (this.popupNode == undefined) {
- // search the nodes for overlap, select the top one in case of multiple nodes
- var nodes = this.nodes;
- for (id in nodes) {
- if (nodes.hasOwnProperty(id)) {
- var node = nodes[id];
- if (node.getTitle() != undefined && node.isOverlappingWith(obj)) {
- this.popupNode = node;
- break;
- }
- }
+Node.prototype._resizeImage = function (ctx) {
+ // TODO: pre calculate the image size
+ if (!this.width) { // undefined or 0
+ var width, height;
+ if (this.value) {
+ var scale = this.imageObj.height / this.imageObj.width;
+ width = this.radius || this.imageObj.width;
+ height = this.radius * scale || this.imageObj.height;
}
- }
-
- if (this.popupNode == undefined) {
- // search the edges for overlap
- var edges = this.edges;
- for (id in edges) {
- if (edges.hasOwnProperty(id)) {
- var edge = edges[id];
- if (edge.connected && (edge.getTitle() != undefined) &&
- edge.isOverlappingWith(obj)) {
- this.popupNode = edge;
- break;
- }
- }
+ else {
+ width = this.imageObj.width;
+ height = this.imageObj.height;
}
+ this.width = width;
+ this.height = height;
}
+};
- if (this.popupNode) {
- // show popup message window
- if (this.popupNode != lastPopupNode) {
- var me = this;
- if (!me.popup) {
- me.popup = new Popup(me.frame);
- }
+Node.prototype._drawImage = function (ctx) {
+ this._resizeImage(ctx);
- // adjust a small offset such that the mouse cursor is located in the
- // bottom left location of the popup, and you can easily move over the
- // popup area
- me.popup.setPosition(x - 3, y - 3);
- me.popup.setText(me.popupNode.getTitle());
- me.popup.show();
- }
+ this.left = this.x - this.width / 2;
+ this.top = this.y - this.height / 2;
+
+ var yLabel;
+ if (this.imageObj) {
+ ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
+ yLabel = this.y + this.height / 2;
}
else {
- if (this.popup) {
- this.popup.hide();
- }
+ // image still loading... just draw the label for now
+ yLabel = this.y;
}
+
+ this._label(ctx, this.label, this.x, yLabel, undefined, "top");
};
-/**
- * Check if the popup must be hided, which is the case when the mouse is no
- * longer hovering on the object
- * @param {number} x
- * @param {number} y
- * @private
- */
-Graph.prototype._checkHidePopup = function (x, y) {
- var obj = {
- "left" : x,
- "top" : y,
- "right" : x,
- "bottom" : y
- };
- if (!this.popupNode || !this.popupNode.isOverlappingWith(obj) ) {
- this.popupNode = undefined;
- if (this.popup) {
- this.popup.hide();
- }
+Node.prototype._resizeBox = function (ctx) {
+ if (!this.width) {
+ var margin = 5;
+ var textSize = this.getTextSize(ctx);
+ this.width = textSize.width + 2 * margin;
+ this.height = textSize.height + 2 * margin;
}
};
-/**
- * Event handler for touchstart event on mobile devices
- * @param {Event} event
- * @private
- */
-Graph.prototype._onTouchStart = function(event) {
- vis.util.preventDefault(event);
+Node.prototype._drawBox = function (ctx) {
+ this._resizeBox(ctx);
- if (this.touchDown) {
- // if already moving, return
- return;
- }
- this.touchDown = true;
+ this.left = this.x - this.width / 2;
+ this.top = this.y - this.height / 2;
- var me = this;
- if (!this.ontouchmove) {
- this.ontouchmove = function (event) {me._onTouchMove(event);};
- vis.util.addEventListener(document, "touchmove", this.ontouchmove);
- }
- if (!this.ontouchend) {
- this.ontouchend = function (event) {me._onTouchEnd(event);};
- vis.util.addEventListener(document, "touchend", this.ontouchend);
- }
+ ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
+ ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
+ ctx.lineWidth = this.selected ? 2.0 : 1.0;
+ ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
+ ctx.fill();
+ ctx.stroke();
- this._onMouseDown(event);
+ this._label(ctx, this.label, this.x, this.y);
};
-/**
- * Event handler for touchmove event on mobile devices
- * @param {Event} event
- * @private
- */
-Graph.prototype._onTouchMove = function(event) {
- vis.util.preventDefault(event);
- this._onMouseMove(event);
+
+Node.prototype._resizeDatabase = function (ctx) {
+ if (!this.width) {
+ var margin = 5;
+ var textSize = this.getTextSize(ctx);
+ var size = textSize.width + 2 * margin;
+ this.width = size;
+ this.height = size;
+ }
};
-/**
- * Event handler for touchend event on mobile devices
- * @param {Event} event
- * @private
- */
-Graph.prototype._onTouchEnd = function(event) {
- vis.util.preventDefault(event);
+Node.prototype._drawDatabase = function (ctx) {
+ this._resizeDatabase(ctx);
+ this.left = this.x - this.width / 2;
+ this.top = this.y - this.height / 2;
+
+ ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
+ ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
+ ctx.lineWidth = this.selected ? 2.0 : 1.0;
+ ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
+ ctx.fill();
+ ctx.stroke();
+
+ this._label(ctx, this.label, this.x, this.y);
+};
- this.touchDown = false;
- if (this.ontouchmove) {
- vis.util.removeEventListener(document, "touchmove", this.ontouchmove);
- this.ontouchmove = undefined;
- }
- if (this.ontouchend) {
- vis.util.removeEventListener(document, "touchend", this.ontouchend);
- this.ontouchend = undefined;
- }
+Node.prototype._resizeCircle = function (ctx) {
+ if (!this.width) {
+ var margin = 5;
+ var textSize = this.getTextSize(ctx);
+ var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
+ this.radius = diameter / 2;
- this._onMouseUp(event);
+ this.width = diameter;
+ this.height = diameter;
+ }
};
+Node.prototype._drawCircle = function (ctx) {
+ this._resizeCircle(ctx);
+ this.left = this.x - this.width / 2;
+ this.top = this.y - this.height / 2;
-/**
- * Unselect selected nodes. If no selection array is provided, all nodes
- * are unselected
- * @param {Object[]} selection Array with selection objects, each selection
- * object has a parameter row. Optional
- * @param {Boolean} triggerSelect If true (default), the select event
- * is triggered when nodes are unselected
- * @return {Boolean} changed True if the selection is changed
- * @private
- */
-Graph.prototype._unselectNodes = function(selection, triggerSelect) {
- var changed = false;
- var i, iMax, id;
+ ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
+ ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
+ ctx.lineWidth = this.selected ? 2.0 : 1.0;
+ ctx.circle(this.x, this.y, this.radius);
+ ctx.fill();
+ ctx.stroke();
- if (selection) {
- // remove provided selections
- for (i = 0, iMax = selection.length; i < iMax; i++) {
- id = selection[i];
- this.nodes[id].unselect();
+ this._label(ctx, this.label, this.x, this.y);
+};
- var j = 0;
- while (j < this.selection.length) {
- if (this.selection[j] == id) {
- this.selection.splice(j, 1);
- changed = true;
- }
- else {
- j++;
- }
- }
- }
- }
- else if (this.selection && this.selection.length) {
- // remove all selections
- for (i = 0, iMax = this.selection.length; i < iMax; i++) {
- id = this.selection[i];
- this.nodes[id].unselect();
- changed = true;
+Node.prototype._resizeEllipse = function (ctx) {
+ if (!this.width) {
+ var textSize = this.getTextSize(ctx);
+
+ this.width = textSize.width * 1.5;
+ this.height = textSize.height * 2;
+ if (this.width < this.height) {
+ this.width = this.height;
}
- this.selection = [];
}
+};
- if (changed && (triggerSelect == true || triggerSelect == undefined)) {
- // fire the select event
- this._trigger('select');
- }
+Node.prototype._drawEllipse = function (ctx) {
+ this._resizeEllipse(ctx);
+ this.left = this.x - this.width / 2;
+ this.top = this.y - this.height / 2;
- return changed;
+ ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
+ ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
+ ctx.lineWidth = this.selected ? 2.0 : 1.0;
+ ctx.ellipse(this.left, this.top, this.width, this.height);
+ ctx.fill();
+ ctx.stroke();
+
+ this._label(ctx, this.label, this.x, this.y);
};
-/**
- * select all nodes on given location x, y
- * @param {Array} selection an array with node ids
- * @param {boolean} append If true, the new selection will be appended to the
- * current selection (except for duplicate entries)
- * @return {Boolean} changed True if the selection is changed
- * @private
- */
-Graph.prototype._selectNodes = function(selection, append) {
- var changed = false;
- var i, iMax;
+Node.prototype._drawDot = function (ctx) {
+ this._drawShape(ctx, 'circle');
+};
+
+Node.prototype._drawTriangle = function (ctx) {
+ this._drawShape(ctx, 'triangle');
+};
+
+Node.prototype._drawTriangleDown = function (ctx) {
+ this._drawShape(ctx, 'triangleDown');
+};
- // TODO: the selectNodes method is a little messy, rework this
+Node.prototype._drawSquare = function (ctx) {
+ this._drawShape(ctx, 'square');
+};
- // check if the current selection equals the desired selection
- var selectionAlreadyThere = true;
- if (selection.length != this.selection.length) {
- selectionAlreadyThere = false;
- }
- else {
- for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) {
- if (selection[i] != this.selection[i]) {
- selectionAlreadyThere = false;
- break;
- }
- }
- }
- if (selectionAlreadyThere) {
- return changed;
- }
+Node.prototype._drawStar = function (ctx) {
+ this._drawShape(ctx, 'star');
+};
- if (append == undefined || append == false) {
- // first deselect any selected node
- var triggerSelect = false;
- changed = this._unselectNodes(undefined, triggerSelect);
+Node.prototype._resizeShape = function (ctx) {
+ if (!this.width) {
+ var size = 2 * this.radius;
+ this.width = size;
+ this.height = size;
}
+};
- for (i = 0, iMax = selection.length; i < iMax; i++) {
- // add each of the new selections, but only when they are not duplicate
- var id = selection[i];
- var isDuplicate = (this.selection.indexOf(id) != -1);
- if (!isDuplicate) {
- this.nodes[id].select();
- this.selection.push(id);
- changed = true;
- }
- }
+Node.prototype._drawShape = function (ctx, shape) {
+ this._resizeShape(ctx);
- if (changed) {
- // fire the select event
- this._trigger('select');
- }
+ this.left = this.x - this.width / 2;
+ this.top = this.y - this.height / 2;
- return changed;
-};
+ ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
+ ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
+ ctx.lineWidth = this.selected ? 2.0 : 1.0;
-/**
- * retrieve all nodes overlapping with given object
- * @param {Object} obj An object with parameters left, top, right, bottom
- * @return {Object[]} An array with selection objects containing
- * the parameter row.
- * @private
- */
-Graph.prototype._getNodesOverlappingWith = function (obj) {
- var nodes = this.nodes,
- overlappingNodes = [];
+ ctx[shape](this.x, this.y, this.radius);
+ ctx.fill();
+ ctx.stroke();
- for (var id in nodes) {
- if (nodes.hasOwnProperty(id)) {
- if (nodes[id].isOverlappingWith(obj)) {
- overlappingNodes.push(id);
- }
- }
+ if (this.label) {
+ this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top');
}
-
- return overlappingNodes;
};
-/**
- * retrieve the currently selected nodes
- * @return {Number[] | String[]} selection An array with the ids of the
- * selected nodes.
- */
-Graph.prototype.getSelection = function() {
- return this.selection.concat([]);
+Node.prototype._resizeText = function (ctx) {
+ if (!this.width) {
+ var margin = 5;
+ var textSize = this.getTextSize(ctx);
+ this.width = textSize.width + 2 * margin;
+ this.height = textSize.height + 2 * margin;
+ }
};
-/**
- * select zero or more nodes
- * @param {Number[] | String[]} selection An array with the ids of the
- * selected nodes.
- */
-Graph.prototype.setSelection = function(selection) {
- var i, iMax, id;
+Node.prototype._drawText = function (ctx) {
+ this._resizeText(ctx);
+ this.left = this.x - this.width / 2;
+ this.top = this.y - this.height / 2;
- if (selection.length == undefined)
- throw "Selection must be an array with ids";
+ this._label(ctx, this.label, this.x, this.y);
+};
- // first unselect any selected node
- for (i = 0, iMax = this.selection.length; i < iMax; i++) {
- id = this.selection[i];
- this.nodes[id].unselect();
- }
- this.selection = [];
+Node.prototype._label = function (ctx, text, x, y, align, baseline) {
+ if (text) {
+ ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
+ ctx.fillStyle = this.fontColor || "black";
+ ctx.textAlign = align || "center";
+ ctx.textBaseline = baseline || "middle";
- for (i = 0, iMax = selection.length; i < iMax; i++) {
- id = selection[i];
+ var lines = text.split('\n'),
+ lineCount = lines.length,
+ fontSize = (this.fontSize + 4),
+ yLine = y + (1 - lineCount) / 2 * fontSize;
- var node = this.nodes[id];
- if (!node) {
- throw new RangeError('Node with id "' + id + '" not found');
+ for (var i = 0; i < lineCount; i++) {
+ ctx.fillText(lines[i], x, yLine);
+ yLine += fontSize;
}
- node.select();
- this.selection.push(id);
}
-
- this.redraw();
};
-/**
- * Validate the selection: remove ids of nodes which no longer exist
- * @private
- */
-Graph.prototype._updateSelection = function () {
- var i = 0;
- while (i < this.selection.length) {
- var id = this.selection[i];
- if (!this.nodes[id]) {
- this.selection.splice(i, 1);
- }
- else {
- i++;
+
+Node.prototype.getTextSize = function(ctx) {
+ if (this.label != undefined) {
+ ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
+
+ var lines = this.label.split('\n'),
+ height = (this.fontSize + 4) * lines.length,
+ width = 0;
+
+ for (var i = 0, iMax = lines.length; i < iMax; i++) {
+ width = Math.max(width, ctx.measureText(lines[i]).width);
}
+
+ return {"width": width, "height": height};
+ }
+ else {
+ return {"width": 0, "height": 0};
}
};
/**
- * Temporary method to test calculating a hub value for the nodes
- * @param {number} level Maximum number edges between two nodes in order
- * to call them connected. Optional, 1 by default
- * @return {Number[]} connectioncount array with the connection count
- * for each node
- * @private
+ * @class Edge
+ *
+ * A edge connects two nodes
+ * @param {Object} properties Object with properties. Must contain
+ * At least properties from and to.
+ * Available properties: from (number),
+ * to (number), label (string, color (string),
+ * width (number), style (string),
+ * length (number), title (string)
+ * @param {Graph} graph A graph object, used to find and edge to
+ * nodes.
+ * @param {Object} constants An object with default values for
+ * example for the color
*/
-Graph.prototype._getConnectionCount = function(level) {
- if (level == undefined) {
- level = 1;
+function Edge (properties, graph, constants) {
+ if (!graph) {
+ throw "No graph provided";
}
+ this.graph = graph;
- // get the nodes connected to given nodes
- function getConnectedNodes(nodes) {
- var connectedNodes = [];
+ // initialize constants
+ this.widthMin = constants.edges.widthMin;
+ this.widthMax = constants.edges.widthMax;
- for (var j = 0, jMax = nodes.length; j < jMax; j++) {
- var node = nodes[j];
+ // initialize variables
+ this.id = undefined;
+ this.fromId = undefined;
+ this.toId = undefined;
+ this.style = constants.edges.style;
+ this.title = undefined;
+ this.width = constants.edges.width;
+ this.value = undefined;
+ this.length = constants.edges.length;
- // find all nodes connected to this node
- var edges = node.edges;
- for (var i = 0, iMax = edges.length; i < iMax; i++) {
- var edge = edges[i];
- var other = null;
+ this.from = null; // a node
+ this.to = null; // a node
+ this.connected = false;
- // check if connected
- if (edge.from == node)
- other = edge.to;
- else if (edge.to == node)
- other = edge.from;
+ // Added to support dashed lines
+ // David Jordan
+ // 2012-08-08
+ this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
- // check if the other node is not already in the list with nodes
- var k, kMax;
- if (other) {
- for (k = 0, kMax = nodes.length; k < kMax; k++) {
- if (nodes[k] == other) {
- other = null;
- break;
- }
- }
- }
- if (other) {
- for (k = 0, kMax = connectedNodes.length; k < kMax; k++) {
- if (connectedNodes[k] == other) {
- other = null;
- break;
- }
- }
- }
+ this.stiffness = undefined; // depends on the length of the edge
+ this.color = constants.edges.color;
+ this.widthFixed = false;
+ this.lengthFixed = false;
- if (other)
- connectedNodes.push(other);
- }
- }
+ this.setProperties(properties, constants);
+}
- return connectedNodes;
+/**
+ * Set or overwrite properties for the edge
+ * @param {Object} properties an object with properties
+ * @param {Object} constants and object with default, global properties
+ */
+Edge.prototype.setProperties = function(properties, constants) {
+ if (!properties) {
+ return;
}
- var connections = [];
- var nodes = this.nodes;
- for (var id in nodes) {
- if (nodes.hasOwnProperty(id)) {
- var c = [nodes[id]];
- for (var l = 0; l < level; l++) {
- c = c.concat(getConnectedNodes(c));
- }
- connections.push(c);
- }
+ if (properties.from != undefined) {this.fromId = properties.from;}
+ if (properties.to != undefined) {this.toId = properties.to;}
+
+ if (properties.id != undefined) {this.id = properties.id;}
+ if (properties.style != undefined) {this.style = properties.style;}
+ if (properties.label != undefined) {this.label = properties.label;}
+ if (this.label) {
+ this.fontSize = constants.edges.fontSize;
+ this.fontFace = constants.edges.fontFace;
+ this.fontColor = constants.edges.fontColor;
+ if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;}
+ if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;}
+ if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;}
}
+ if (properties.title != undefined) {this.title = properties.title;}
+ if (properties.width != undefined) {this.width = properties.width;}
+ if (properties.value != undefined) {this.value = properties.value;}
+ if (properties.length != undefined) {this.length = properties.length;}
- var hubs = [];
- for (var i = 0, len = connections.length; i < len; i++) {
- hubs.push(connections[i].length);
+ // Added to support dashed lines
+ // David Jordan
+ // 2012-08-08
+ if (properties.dash) {
+ if (properties.dash.length != undefined) {this.dash.length = properties.dash.length;}
+ if (properties.dash.gap != undefined) {this.dash.gap = properties.dash.gap;}
+ if (properties.dash.altLength != undefined) {this.dash.altLength = properties.dash.altLength;}
}
+
+ if (properties.color != undefined) {this.color = properties.color;}
- return hubs;
-};
+ // A node is connected when it has a from and to node.
+ this.connect();
+
+ this.widthFixed = this.widthFixed || (properties.width != undefined);
+ this.lengthFixed = this.lengthFixed || (properties.length != undefined);
+ this.stiffness = 1 / this.length;
+ // set draw method based on style
+ switch (this.style) {
+ case 'line': this.draw = this._drawLine; break;
+ case 'arrow': this.draw = this._drawArrow; break;
+ case 'arrow-center': this.draw = this._drawArrowCenter; break;
+ case 'dash-line': this.draw = this._drawDashLine; break;
+ default: this.draw = this._drawLine; break;
+ }
+};
/**
- * Set a new size for the graph
- * @param {string} width Width in pixels or percentage (for example "800px"
- * or "50%")
- * @param {string} height Height in pixels or percentage (for example "400px"
- * or "30%")
+ * Connect an edge to its nodes
*/
-Graph.prototype.setSize = function(width, height) {
- this.frame.style.width = width;
- this.frame.style.height = height;
+Edge.prototype.connect = function () {
+ this.disconnect();
- this.frame.canvas.style.width = "100%";
- this.frame.canvas.style.height = "100%";
+ this.from = this.graph.nodes[this.fromId] || null;
+ this.to = this.graph.nodes[this.toId] || null;
+ this.connected = (this.from && this.to);
- this.frame.canvas.width = this.frame.canvas.clientWidth;
- this.frame.canvas.height = this.frame.canvas.clientHeight;
+ if (this.connected) {
+ this.from.attachEdge(this);
+ this.to.attachEdge(this);
+ }
+ else {
+ if (this.from) {
+ this.from.detachEdge(this);
+ }
+ if (this.to) {
+ this.to.detachEdge(this);
+ }
+ }
};
/**
- * Set a data set with nodes for the graph
- * @param {Array | DataSet | DataView} nodes The data containing the nodes.
- * @private
+ * Disconnect an edge from its nodes
*/
-Graph.prototype._setNodes = function(nodes) {
- var oldNodesData = this.nodesData;
-
- if (nodes instanceof DataSet || nodes instanceof DataView) {
- this.nodesData = nodes;
- }
- else if (nodes instanceof Array) {
- this.nodesData = new DataSet();
- this.nodesData.add(nodes);
- }
- else if (!nodes) {
- this.nodesData = new DataSet();
+Edge.prototype.disconnect = function () {
+ if (this.from) {
+ this.from.detachEdge(this);
+ this.from = null;
}
- else {
- throw new TypeError('Array or DataSet expected');
+ if (this.to) {
+ this.to.detachEdge(this);
+ this.to = null;
}
- if (oldNodesData) {
- // unsubscribe from old dataset
- util.forEach(this.nodesListeners, function (callback, event) {
- oldNodesData.unsubscribe(event, callback);
- });
- }
+ this.connected = false;
+};
- // remove drawn nodes
- this.nodes = {};
+/**
+ * get the title of this edge.
+ * @return {string} title The title of the edge, or undefined when no title
+ * has been set.
+ */
+Edge.prototype.getTitle = function() {
+ return this.title;
+};
- if (this.nodesData) {
- // subscribe to new dataset
- var me = this;
- util.forEach(this.nodesListeners, function (callback, event) {
- me.nodesData.subscribe(event, callback);
- });
- // draw all new nodes
- var ids = this.nodesData.getIds();
- this._addNodes(ids);
+/**
+ * Retrieve the value of the edge. Can be undefined
+ * @return {Number} value
+ */
+Edge.prototype.getValue = function() {
+ return this.value;
+};
+
+/**
+ * Adjust the value range of the edge. The edge will adjust it's width
+ * based on its value.
+ * @param {Number} min
+ * @param {Number} max
+ */
+Edge.prototype.setValueRange = function(min, max) {
+ if (!this.widthFixed && this.value !== undefined) {
+ var factor = (this.widthMax - this.widthMin) / (max - min);
+ this.width = (this.value - min) * factor + this.widthMin;
}
+};
- this._updateSelection();
+/**
+ * Redraw a edge
+ * Draw this edge in the given canvas
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
+ * @param {CanvasRenderingContext2D} ctx
+ */
+Edge.prototype.draw = function(ctx) {
+ throw "Method draw not initialized in edge";
};
/**
- * Add nodes
- * @param {Number[] | String[]} ids
- * @private
+ * Check if this object is overlapping with the provided object
+ * @param {Object} obj an object with parameters left, top
+ * @return {boolean} True if location is located on the edge
*/
-Graph.prototype._addNodes = function(ids) {
- var id;
- for (var i = 0, len = ids.length; i < len; i++) {
- id = ids[i];
- var data = this.nodesData.get(id);
- var node = new Node(data, this.images, this.groups, this.constants);
- this.nodes[id] = node; // note: this may replace an existing node
+Edge.prototype.isOverlappingWith = function(obj) {
+ var distMax = 10;
- if (!node.isFixed()) {
- // TODO: position new nodes in a smarter way!
- var radius = this.constants.edges.length * 2;
- var count = ids.length;
- var angle = 2 * Math.PI * (i / count);
- node.x = radius * Math.cos(angle);
- node.y = radius * Math.sin(angle);
+ var xFrom = this.from.x;
+ var yFrom = this.from.y;
+ var xTo = this.to.x;
+ var yTo = this.to.y;
+ var xObj = obj.left;
+ var yObj = obj.top;
- // note: no not use node.isMoving() here, as that gives the current
- // velocity of the node, which is zero after creation of the node.
- this.moving = true;
- }
- }
- this._reconnectEdges();
- this._updateValueRange(this.nodes);
+ var dist = Edge._dist(xFrom, yFrom, xTo, yTo, xObj, yObj);
+
+ return (dist < distMax);
};
+
/**
- * Update existing nodes, or create them when not yet existing
- * @param {Number[] | String[]} ids
+ * Redraw a edge as a line
+ * Draw this edge in the given canvas
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
+ * @param {CanvasRenderingContext2D} ctx
* @private
*/
-Graph.prototype._updateNodes = function(ids) {
- var nodes = this.nodes,
- nodesData = this.nodesData;
- for (var i = 0, len = ids.length; i < len; i++) {
- var id = ids[i];
- var node = nodes[id];
- var data = nodesData.get(id);
- if (node) {
- // update node
- node.setProperties(data, this.constants);
+Edge.prototype._drawLine = function(ctx) {
+ // set style
+ ctx.strokeStyle = this.color;
+ ctx.lineWidth = this._getLineWidth();
+
+ var point;
+ if (this.from != this.to) {
+ // draw line
+ this._line(ctx);
+
+ // draw label
+ if (this.label) {
+ point = this._pointOnLine(0.5);
+ this._label(ctx, this.label, point.x, point.y);
+ }
+ }
+ else {
+ var x, y;
+ var radius = this.length / 4;
+ var node = this.from;
+ if (!node.width) {
+ node.resize(ctx);
+ }
+ if (node.width > node.height) {
+ x = node.x + node.width / 2;
+ y = node.y - radius;
}
else {
- // create node
- node = new Node(properties, this.images, this.groups, this.constants);
- nodes[id] = node;
-
- if (!node.isFixed()) {
- this.moving = true;
- }
+ x = node.x + radius;
+ y = node.y - node.height / 2;
}
+ this._circle(ctx, x, y, radius);
+ point = this._pointOnCircle(x, y, radius, 0.5);
+ this._label(ctx, this.label, point.x, point.y);
}
-
- this._reconnectEdges();
- this._updateValueRange(nodes);
};
/**
- * Remove existing nodes. If nodes do not exist, the method will just ignore it.
- * @param {Number[] | String[]} ids
+ * Get the line width of the edge. Depends on width and whether one of the
+ * connected nodes is selected.
+ * @return {Number} width
* @private
*/
-Graph.prototype._removeNodes = function(ids) {
- var nodes = this.nodes;
- for (var i = 0, len = ids.length; i < len; i++) {
- var id = ids[i];
- delete nodes[id];
+Edge.prototype._getLineWidth = function() {
+ if (this.from.selected || this.to.selected) {
+ return Math.min(this.width * 2, this.widthMax);
+ }
+ else {
+ return this.width;
}
-
- this._reconnectEdges();
- this._updateSelection();
- this._updateValueRange(nodes);
};
/**
- * Load edges by reading the data table
- * @param {Array | DataSet | DataView} edges The data containing the edges.
- * @private
+ * Draw a line between two nodes
+ * @param {CanvasRenderingContext2D} ctx
* @private
*/
-Graph.prototype._setEdges = function(edges) {
- var oldEdgesData = this.edgesData;
-
- if (edges instanceof DataSet || edges instanceof DataView) {
- this.edgesData = edges;
- }
- else if (edges instanceof Array) {
- this.edgesData = new DataSet();
- this.edgesData.add(edges);
- }
- else if (!edges) {
- this.edgesData = new DataSet();
- }
- else {
- throw new TypeError('Array or DataSet expected');
- }
-
- if (oldEdgesData) {
- // unsubscribe from old dataset
- util.forEach(this.edgesListeners, function (callback, event) {
- oldEdgesData.unsubscribe(event, callback);
- });
- }
-
- // remove drawn edges
- this.edges = {};
-
- if (this.edgesData) {
- // subscribe to new dataset
- var me = this;
- util.forEach(this.edgesListeners, function (callback, event) {
- me.edgesData.subscribe(event, callback);
- });
-
- // draw all new nodes
- var ids = this.edgesData.getIds();
- this._addEdges(ids);
- }
+Edge.prototype._line = function (ctx) {
+ // draw a straight line
+ ctx.beginPath();
+ ctx.moveTo(this.from.x, this.from.y);
+ ctx.lineTo(this.to.x, this.to.y);
+ ctx.stroke();
+};
- this._reconnectEdges();
+/**
+ * Draw a line from a node to itself, a circle
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} radius
+ * @private
+ */
+Edge.prototype._circle = function (ctx, x, y, radius) {
+ // draw a circle
+ ctx.beginPath();
+ ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
+ ctx.stroke();
};
/**
- * Add edges
- * @param {Number[] | String[]} ids
+ * Draw label with white background and with the middle at (x, y)
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {String} text
+ * @param {Number} x
+ * @param {Number} y
* @private
*/
-Graph.prototype._addEdges = function (ids) {
- var edges = this.edges,
- edgesData = this.edgesData;
- for (var i = 0, len = ids.length; i < len; i++) {
- var id = ids[i];
+Edge.prototype._label = function (ctx, text, x, y) {
+ if (text) {
+ // TODO: cache the calculated size
+ ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
+ this.fontSize + "px " + this.fontFace;
+ ctx.fillStyle = 'white';
+ var width = ctx.measureText(text).width;
+ var height = this.fontSize;
+ var left = x - width / 2;
+ var top = y - height / 2;
- var oldEdge = edges[id];
- if (oldEdge) {
- oldEdge.disconnect();
- }
+ ctx.fillRect(left, top, width, height);
- var data = edgesData.get(id);
- edges[id] = new Edge(data, this, this.constants);
+ // draw text
+ ctx.fillStyle = this.fontColor || "black";
+ ctx.textAlign = "left";
+ ctx.textBaseline = "top";
+ ctx.fillText(text, left, top);
}
-
- this.moving = true;
- this._updateValueRange(edges);
};
/**
- * Update existing edges, or create them when not yet existing
- * @param {Number[] | String[]} ids
+ * Redraw a edge as a dashed line
+ * Draw this edge in the given canvas
+ * @author David Jordan
+ * @date 2012-08-08
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
+ * @param {CanvasRenderingContext2D} ctx
* @private
*/
-Graph.prototype._updateEdges = function (ids) {
- var edges = this.edges,
- edgesData = this.edgesData;
- for (var i = 0, len = ids.length; i < len; i++) {
- var id = ids[i];
+Edge.prototype._drawDashLine = function(ctx) {
+ // set style
+ ctx.strokeStyle = this.color;
+ ctx.lineWidth = this._getLineWidth();
- var data = edgesData.get(id);
- var edge = edges[id];
- if (edge) {
- // update edge
- edge.disconnect();
- edge.setProperties(data, this.constants);
- edge.connect();
- }
- else {
- // create edge
- edge = new Edge(data, this, this.constants);
- this.edges[id] = edge;
- }
+ // draw dashed line
+ ctx.beginPath();
+ ctx.lineCap = 'round';
+ if (this.dash.altLength != undefined) //If an alt dash value has been set add to the array this value
+ {
+ ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
+ [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
+ }
+ else if (this.dash.length != undefined && this.dash.gap != undefined) //If a dash and gap value has been set add to the array this value
+ {
+ ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
+ [this.dash.length,this.dash.gap]);
+ }
+ else //If all else fails draw a line
+ {
+ ctx.moveTo(this.from.x, this.from.y);
+ ctx.lineTo(this.to.x, this.to.y);
}
+ ctx.stroke();
- this.moving = true;
- this._updateValueRange(edges);
+ // draw label
+ if (this.label) {
+ var point = this._pointOnLine(0.5);
+ this._label(ctx, this.label, point.x, point.y);
+ }
};
/**
- * Remove existing edges. Non existing ids will be ignored
- * @param {Number[] | String[]} ids
+ * Get a point on a line
+ * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
+ * @return {Object} point
* @private
*/
-Graph.prototype._removeEdges = function (ids) {
- var edges = this.edges;
- for (var i = 0, len = ids.length; i < len; i++) {
- var id = ids[i];
- var edge = edges[id];
- if (edge) {
- edge.disconnect();
- delete edges[id];
- }
+Edge.prototype._pointOnLine = function (percentage) {
+ return {
+ x: (1 - percentage) * this.from.x + percentage * this.to.x,
+ y: (1 - percentage) * this.from.y + percentage * this.to.y
}
-
- this.moving = true;
- this._updateValueRange(edges);
};
/**
- * Reconnect all edges
+ * Get a point on a circle
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} radius
+ * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
+ * @return {Object} point
* @private
*/
-Graph.prototype._reconnectEdges = function() {
- var id,
- nodes = this.nodes,
- edges = this.edges;
- for (id in nodes) {
- if (nodes.hasOwnProperty(id)) {
- nodes[id].edges = [];
- }
- }
-
- for (id in edges) {
- if (edges.hasOwnProperty(id)) {
- var edge = edges[id];
- edge.from = null;
- edge.to = null;
- edge.connect();
- }
+Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
+ var angle = (percentage - 3/8) * 2 * Math.PI;
+ return {
+ x: x + radius * Math.cos(angle),
+ y: y - radius * Math.sin(angle)
}
};
/**
- * Update the values of all object in the given array according to the current
- * value range of the objects in the array.
- * @param {Object} obj An object containing a set of Edges or Nodes
- * The objects must have a method getValue() and
- * setValueRange(min, max).
+ * Redraw a edge as a line with an arrow halfway the line
+ * Draw this edge in the given canvas
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
+ * @param {CanvasRenderingContext2D} ctx
* @private
*/
-Graph.prototype._updateValueRange = function(obj) {
- var id;
+Edge.prototype._drawArrowCenter = function(ctx) {
+ var point;
+ // set style
+ ctx.strokeStyle = this.color;
+ ctx.fillStyle = this.color;
+ ctx.lineWidth = this._getLineWidth();
+
+ if (this.from != this.to) {
+ // draw line
+ this._line(ctx);
+
+ // draw an arrow halfway the line
+ var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
+ var length = 10 + 5 * this.width; // TODO: make customizable?
+ point = this._pointOnLine(0.5);
+ ctx.arrow(point.x, point.y, angle, length);
+ ctx.fill();
+ ctx.stroke();
- // determine the range of the objects
- var valueMin = undefined;
- var valueMax = undefined;
- for (id in obj) {
- if (obj.hasOwnProperty(id)) {
- var value = obj[id].getValue();
- if (value !== undefined) {
- valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
- valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
- }
+ // draw label
+ if (this.label) {
+ point = this._pointOnLine(0.5);
+ this._label(ctx, this.label, point.x, point.y);
}
}
+ else {
+ // draw circle
+ var x, y;
+ var radius = this.length / 4;
+ var node = this.from;
+ if (!node.width) {
+ node.resize(ctx);
+ }
+ if (node.width > node.height) {
+ x = node.x + node.width / 2;
+ y = node.y - radius;
+ }
+ else {
+ x = node.x + radius;
+ y = node.y - node.height / 2;
+ }
+ this._circle(ctx, x, y, radius);
- // adjust the range of all objects
- if (valueMin !== undefined && valueMax !== undefined) {
- for (id in obj) {
- if (obj.hasOwnProperty(id)) {
- obj[id].setValueRange(valueMin, valueMax);
- }
+ // draw all arrows
+ var angle = 0.2 * Math.PI;
+ var length = 10 + 5 * this.width; // TODO: make customizable?
+ point = this._pointOnCircle(x, y, radius, 0.5);
+ ctx.arrow(point.x, point.y, angle, length);
+ ctx.fill();
+ ctx.stroke();
+
+ // draw label
+ if (this.label) {
+ point = this._pointOnCircle(x, y, radius, 0.5);
+ this._label(ctx, this.label, point.x, point.y);
}
}
};
-/**
- * Redraw the graph with the current data
- * chart will be resized too.
- */
-Graph.prototype.redraw = function() {
- this.setSize(this.width, this.height);
- this._redraw();
-};
/**
- * Redraw the graph with the current data
+ * Redraw a edge as a line with an arrow
+ * Draw this edge in the given canvas
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
+ * @param {CanvasRenderingContext2D} ctx
* @private
*/
-Graph.prototype._redraw = function() {
- var ctx = this.frame.canvas.getContext("2d");
+Edge.prototype._drawArrow = function(ctx) {
+ // set style
+ ctx.strokeStyle = this.color;
+ ctx.fillStyle = this.color;
+ ctx.lineWidth = this._getLineWidth();
- // clear the canvas
- var w = this.frame.canvas.width;
- var h = this.frame.canvas.height;
- ctx.clearRect(0, 0, w, h);
+ // draw line
+ var angle, length;
+ if (this.from != this.to) {
+ // calculate length and angle of the line
+ angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
+ var dx = (this.to.x - this.from.x);
+ var dy = (this.to.y - this.from.y);
+ var lEdge = Math.sqrt(dx * dx + dy * dy);
- // set scaling and translation
- ctx.save();
- ctx.translate(this.translation.x, this.translation.y);
- ctx.scale(this.scale, this.scale);
+ var lFrom = this.from.distanceToBorder(ctx, angle + Math.PI);
+ var pFrom = (lEdge - lFrom) / lEdge;
+ var xFrom = (pFrom) * this.from.x + (1 - pFrom) * this.to.x;
+ var yFrom = (pFrom) * this.from.y + (1 - pFrom) * this.to.y;
- this._drawEdges(ctx);
- this._drawNodes(ctx);
+ var lTo = this.to.distanceToBorder(ctx, angle);
+ var pTo = (lEdge - lTo) / lEdge;
+ var xTo = (1 - pTo) * this.from.x + pTo * this.to.x;
+ var yTo = (1 - pTo) * this.from.y + pTo * this.to.y;
- // restore original scaling and translation
- ctx.restore();
-};
+ ctx.beginPath();
+ ctx.moveTo(xFrom, yFrom);
+ ctx.lineTo(xTo, yTo);
+ ctx.stroke();
-/**
- * Set the translation of the graph
- * @param {Number} offsetX Horizontal offset
- * @param {Number} offsetY Vertical offset
- * @private
- */
-Graph.prototype._setTranslation = function(offsetX, offsetY) {
- if (this.translation === undefined) {
- this.translation = {
- "x": 0,
- "y": 0
- };
- }
+ // draw arrow at the end of the line
+ length = 10 + 5 * this.width; // TODO: make customizable?
+ ctx.arrow(xTo, yTo, angle, length);
+ ctx.fill();
+ ctx.stroke();
- if (offsetX !== undefined) {
- this.translation.x = offsetX;
+ // draw label
+ if (this.label) {
+ var point = this._pointOnLine(0.5);
+ this._label(ctx, this.label, point.x, point.y);
+ }
}
- if (offsetY !== undefined) {
- this.translation.y = offsetY;
+ else {
+ // draw circle
+ var node = this.from;
+ var x, y, arrow;
+ var radius = this.length / 4;
+ if (!node.width) {
+ node.resize(ctx);
+ }
+ if (node.width > node.height) {
+ x = node.x + node.width / 2;
+ y = node.y - radius;
+ arrow = {
+ x: x,
+ y: node.y,
+ angle: 0.9 * Math.PI
+ };
+ }
+ else {
+ x = node.x + radius;
+ y = node.y - node.height / 2;
+ arrow = {
+ x: node.x,
+ y: y,
+ angle: 0.6 * Math.PI
+ };
+ }
+ ctx.beginPath();
+ // TODO: do not draw a circle, but an arc
+ // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
+ ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
+ ctx.stroke();
+
+ // draw all arrows
+ length = 10 + 5 * this.width; // TODO: make customizable?
+ ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
+ ctx.fill();
+ ctx.stroke();
+
+ // draw label
+ if (this.label) {
+ point = this._pointOnCircle(x, y, radius, 0.5);
+ this._label(ctx, this.label, point.x, point.y);
+ }
}
};
-/**
- * Get the translation of the graph
- * @return {Object} translation An object with parameters x and y, both a number
- * @private
- */
-Graph.prototype._getTranslation = function() {
- return {
- "x": this.translation.x,
- "y": this.translation.y
- };
-};
+
/**
- * Scale the graph
- * @param {Number} scale Scaling factor 1.0 is unscaled
- * @private
- */
-Graph.prototype._setScale = function(scale) {
- this.scale = scale;
-};
-/**
- * Get the current scale of the graph
- * @return {Number} scale Scaling factor 1.0 is unscaled
+ * Calculate the distance between a point (x3,y3) and a line segment from
+ * (x1,y1) to (x2,y2).
+ * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
+ * @param {number} x1
+ * @param {number} y1
+ * @param {number} x2
+ * @param {number} y2
+ * @param {number} x3
+ * @param {number} y3
* @private
*/
-Graph.prototype._getScale = function() {
- return this.scale;
-};
+Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
+ var px = x2-x1,
+ py = y2-y1,
+ something = px*px + py*py,
+ u = ((x3 - x1) * px + (y3 - y1) * py) / something;
-Graph.prototype._xToCanvas = function(x) {
- return (x - this.translation.x) / this.scale;
-};
+ if (u > 1) {
+ u = 1;
+ }
+ else if (u < 0) {
+ u = 0;
+ }
-Graph.prototype._canvasToX = function(x) {
- return x * this.scale + this.translation.x;
-};
+ var x = x1 + u * px,
+ y = y1 + u * py,
+ dx = x - x3,
+ dy = y - y3;
-Graph.prototype._yToCanvas = function(y) {
- return (y - this.translation.y) / this.scale;
-};
+ //# Note: If the actual distance does not matter,
+ //# if you only want to compare what this function
+ //# returns to other results of this function, you
+ //# can just return the squared distance instead
+ //# (i.e. remove the sqrt) to gain a little performance
-Graph.prototype._canvasToY = function(y) {
- return y * this.scale + this.translation.y ;
+ return Math.sqrt(dx*dx + dy*dy);
};
/**
- * Redraw all nodes
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
- * @param {CanvasRenderingContext2D} ctx
- * @private
- */
-Graph.prototype._drawNodes = function(ctx) {
- // first draw the unselected nodes
- var nodes = this.nodes;
- var selected = [];
- for (var id in nodes) {
- if (nodes.hasOwnProperty(id)) {
- if (nodes[id].isSelected()) {
- selected.push(id);
- }
- else {
- nodes[id].draw(ctx);
- }
- }
+ * Popup is a class to create a popup window with some text
+ * @param {Element} container The container object.
+ * @param {Number} [x]
+ * @param {Number} [y]
+ * @param {String} [text]
+ */
+function Popup(container, x, y, text) {
+ if (container) {
+ this.container = container;
+ }
+ else {
+ this.container = document.body;
}
+ this.x = 0;
+ this.y = 0;
+ this.padding = 5;
- // draw the selected nodes on top
- for (var s = 0, sMax = selected.length; s < sMax; s++) {
- nodes[selected[s]].draw(ctx);
+ if (x !== undefined && y !== undefined ) {
+ this.setPosition(x, y);
+ }
+ if (text !== undefined) {
+ this.setText(text);
}
+
+ // create the frame
+ this.frame = document.createElement("div");
+ var style = this.frame.style;
+ style.position = "absolute";
+ style.visibility = "hidden";
+ style.border = "1px solid #666";
+ style.color = "black";
+ style.padding = this.padding + "px";
+ style.backgroundColor = "#FFFFC6";
+ style.borderRadius = "3px";
+ style.MozBorderRadius = "3px";
+ style.WebkitBorderRadius = "3px";
+ style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
+ style.whiteSpace = "nowrap";
+ this.container.appendChild(this.frame);
};
/**
- * Redraw all edges
- * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
- * @param {CanvasRenderingContext2D} ctx
- * @private
+ * @param {number} x Horizontal position of the popup window
+ * @param {number} y Vertical position of the popup window
*/
-Graph.prototype._drawEdges = function(ctx) {
- var edges = this.edges;
- for (var id in edges) {
- if (edges.hasOwnProperty(id)) {
- var edge = edges[id];
- if (edge.connected) {
- edges[id].draw(ctx);
- }
- }
- }
+Popup.prototype.setPosition = function(x, y) {
+ this.x = parseInt(x);
+ this.y = parseInt(y);
};
/**
- * Find a stable position for all nodes
- * @private
+ * Set the text for the popup window. This can be HTML code
+ * @param {string} text
*/
-Graph.prototype._doStabilize = function() {
- var start = new Date();
-
- // find stable position
- var count = 0;
- var vmin = this.constants.minVelocity;
- var stable = false;
- while (!stable && count < this.constants.maxIterations) {
- this._calculateForces();
- this._discreteStepNodes();
- stable = !this._isMoving(vmin);
- count++;
- }
-
- var end = new Date();
-
- // console.log("Stabilized in " + (end-start) + " ms, " + count + " iterations" ); // TODO: cleanup
+Popup.prototype.setText = function(text) {
+ this.frame.innerHTML = text;
};
/**
- * Calculate the external forces acting on the nodes
- * Forces are caused by: edges, repulsing forces between nodes, gravity
- * @private
+ * Show the popup window
+ * @param {boolean} show Optional. Show or hide the window
*/
-Graph.prototype._calculateForces = function() {
- // create a local edge to the nodes and edges, that is faster
- var id, dx, dy, angle, distance, fx, fy,
- repulsingForce, springForce, length, edgeLength,
- nodes = this.nodes,
- edges = this.edges;
-
- // gravity, add a small constant force to pull the nodes towards the center of
- // the graph
- // Also, the forces are reset to zero in this loop by using _setForce instead
- // of _addForce
- var gravity = 0.01,
- gx = this.frame.canvas.clientWidth / 2,
- gy = this.frame.canvas.clientHeight / 2;
- for (id in nodes) {
- if (nodes.hasOwnProperty(id)) {
- var node = nodes[id];
- dx = gx - node.x;
- dy = gy - node.y;
- angle = Math.atan2(dy, dx);
- fx = Math.cos(angle) * gravity;
- fy = Math.sin(angle) * gravity;
-
- node._setForce(fx, fy);
- }
+Popup.prototype.show = function (show) {
+ if (show === undefined) {
+ show = true;
}
- // repulsing forces between nodes
- var minimumDistance = this.constants.nodes.distance,
- steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
-
- for (var id1 in nodes) {
- if (nodes.hasOwnProperty(id1)) {
- var node1 = nodes[id1];
- for (var id2 in nodes) {
- if (nodes.hasOwnProperty(id2)) {
- var node2 = nodes[id2];
- // calculate normally distributed force
- dx = node2.x - node1.x;
- dy = node2.y - node1.y;
- distance = Math.sqrt(dx * dx + dy * dy);
- angle = Math.atan2(dy, dx);
-
- // TODO: correct factor for repulsing force
- //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
- //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
- repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
- fx = Math.cos(angle) * repulsingForce;
- fy = Math.sin(angle) * repulsingForce;
+ if (show) {
+ var height = this.frame.clientHeight;
+ var width = this.frame.clientWidth;
+ var maxHeight = this.frame.parentNode.clientHeight;
+ var maxWidth = this.frame.parentNode.clientWidth;
- node1._addForce(-fx, -fy);
- node2._addForce(fx, fy);
- }
- }
+ var top = (this.y - height);
+ if (top + height + this.padding > maxHeight) {
+ top = maxHeight - height - this.padding;
+ }
+ if (top < this.padding) {
+ top = this.padding;
}
- }
-
- /* TODO: re-implement repulsion of edges
- for (var n = 0; n < nodes.length; n++) {
- for (var l = 0; l < edges.length; l++) {
- var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
- ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
-
- // calculate normally distributed force
- dx = nodes[n].x - lx,
- dy = nodes[n].y - ly,
- distance = Math.sqrt(dx * dx + dy * dy),
- angle = Math.atan2(dy, dx),
+ var left = this.x;
+ if (left + width + this.padding > maxWidth) {
+ left = maxWidth - width - this.padding;
+ }
+ if (left < this.padding) {
+ left = this.padding;
+ }
- // TODO: correct factor for repulsing force
- //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
- //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
- repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force
- fx = Math.cos(angle) * repulsingforce,
- fy = Math.sin(angle) * repulsingforce;
- nodes[n]._addForce(fx, fy);
- edges[l].from._addForce(-fx/2,-fy/2);
- edges[l].to._addForce(-fx/2,-fy/2);
- }
+ this.frame.style.left = left + "px";
+ this.frame.style.top = top + "px";
+ this.frame.style.visibility = "visible";
}
- */
-
- // forces caused by the edges, modelled as springs
- for (id in edges) {
- if (edges.hasOwnProperty(id)) {
- var edge = edges[id];
- if (edge.connected) {
- dx = (edge.to.x - edge.from.x);
- dy = (edge.to.y - edge.from.y);
- //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin
- //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin
- //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2;
- edgeLength = edge.length;
- length = Math.sqrt(dx * dx + dy * dy);
- angle = Math.atan2(dy, dx);
-
- springForce = edge.stiffness * (edgeLength - length);
-
- fx = Math.cos(angle) * springForce;
- fy = Math.sin(angle) * springForce;
-
- edge.from._addForce(-fx, -fy);
- edge.to._addForce(fx, fy);
- }
- }
+ else {
+ this.hide();
}
+};
- /* TODO: re-implement repulsion of edges
- // repulsing forces between edges
- var minimumDistance = this.constants.edges.distance,
- steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
- for (var l = 0; l < edges.length; l++) {
- //Keep distance from other edge centers
- for (var l2 = l + 1; l2 < this.edges.length; l2++) {
- //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin
- //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin
- //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0),
- var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
- ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
- l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2,
- l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2,
-
- // calculate normally distributed force
- dx = l2x - lx,
- dy = l2y - ly,
- distance = Math.sqrt(dx * dx + dy * dy),
- angle = Math.atan2(dy, dx),
-
-
- // TODO: correct factor for repulsing force
- //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
- //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
- repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force
- fx = Math.cos(angle) * repulsingforce,
- fy = Math.sin(angle) * repulsingforce;
+/**
+ * Hide the popup window
+ */
+Popup.prototype.hide = function () {
+ this.frame.style.visibility = "hidden";
+};
- edges[l].from._addForce(-fx, -fy);
- edges[l].to._addForce(-fx, -fy);
- edges[l2].from._addForce(fx, fy);
- edges[l2].to._addForce(fx, fy);
- }
- }
- */
+/**
+ * @class Groups
+ * This class can store groups and properties specific for groups.
+ */
+Groups = function () {
+ this.clear();
+ this.defaultIndex = 0;
};
/**
- * Check if any of the nodes is still moving
- * @param {number} vmin the minimum velocity considered as "moving"
- * @return {boolean} true if moving, false if non of the nodes is moving
- * @private
+ * default constants for group colors
*/
-Graph.prototype._isMoving = function(vmin) {
- // TODO: ismoving does not work well: should check the kinetic energy, not its velocity
- var nodes = this.nodes;
- for (var id in nodes) {
- if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
- return true;
- }
- }
- return false;
-};
+Groups.DEFAULT = [
+ {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
+ {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
+ {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red
+ {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green
+ {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
+ {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
+ {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange
+ {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
+ {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
+ {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint
+];
/**
- * Perform one discrete step for all nodes
- * @private
+ * Clear all groups
*/
-Graph.prototype._discreteStepNodes = function() {
- var interval = this.refreshRate / 1000.0; // in seconds
- var nodes = this.nodes;
- for (var id in nodes) {
- if (nodes.hasOwnProperty(id)) {
- nodes[id].discreteStep(interval);
+Groups.prototype.clear = function () {
+ this.groups = {};
+ this.groups.length = function()
+ {
+ var i = 0;
+ for ( var p in this ) {
+ if (this.hasOwnProperty(p)) {
+ i++;
+ }
}
+ return i;
}
};
+
/**
- * Start animating nodes and edges
+ * get group properties of a groupname. If groupname is not found, a new group
+ * is added.
+ * @param {*} groupname Can be a number, string, Date, etc.
+ * @return {Object} group The created group, containing all group properties
*/
-Graph.prototype.start = function() {
- if (this.moving) {
- this._calculateForces();
- this._discreteStepNodes();
+Groups.prototype.get = function (groupname) {
+ var group = this.groups[groupname];
- var vmin = this.constants.minVelocity;
- this.moving = this._isMoving(vmin);
+ if (group == undefined) {
+ // create new group
+ var index = this.defaultIndex % Groups.DEFAULT.length;
+ this.defaultIndex++;
+ group = {};
+ group.color = Groups.DEFAULT[index];
+ this.groups[groupname] = group;
}
- if (this.moving) {
- // start animation. only start timer if it is not already running
- if (!this.timer) {
- var graph = this;
- this.timer = window.setTimeout(function () {
- graph.timer = undefined;
- graph.start();
- graph._redraw();
- }, this.refreshRate);
- }
- }
- else {
- this._redraw();
- }
+ return group;
};
/**
- * Stop animating nodes and edges.
+ * Add a custom group style
+ * @param {String} groupname
+ * @param {Object} style An object containing borderColor,
+ * backgroundColor, etc.
+ * @return {Object} group The created group object
*/
-Graph.prototype.stop = function () {
- if (this.timer) {
- window.clearInterval(this.timer);
- this.timer = undefined;
+Groups.prototype.add = function (groupname, style) {
+ this.groups[groupname] = style;
+ if (style.color) {
+ style.color = Node.parseColor(style.color);
}
+ return style;
};
/**
- * vis.js module exports
+ * @class Images
+ * This class loads images and keeps them stored.
*/
-var vis = {
- util: util,
- events: events,
-
- Controller: Controller,
- DataSet: DataSet,
- DataView: DataView,
- Range: Range,
- Stack: Stack,
- TimeStep: TimeStep,
- EventBus: EventBus,
-
- components: {
- items: {
- Item: Item,
- ItemBox: ItemBox,
- ItemPoint: ItemPoint,
- ItemRange: ItemRange
- },
-
- Component: Component,
- Panel: Panel,
- RootPanel: RootPanel,
- ItemSet: ItemSet,
- TimeAxis: TimeAxis
- },
-
- graph: {
- Node: Node,
- Edge: Edge,
- Popup: Popup,
- Groups: Groups,
- Images: Images
- },
+Images = function () {
+ this.images = {};
- Timeline: Timeline,
- Graph: Graph
+ this.callback = undefined;
};
/**
- * CommonJS module exports
+ * Set an onload callback function. This will be called each time an image
+ * is loaded
+ * @param {function} callback
*/
-if (typeof exports !== 'undefined') {
- exports = vis;
-}
-if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
- module.exports = vis;
-}
+Images.prototype.setOnloadCallback = function(callback) {
+ this.callback = callback;
+};
/**
- * AMD module exports
+ *
+ * @param {string} url Url of the image
+ * @return {Image} img The image object
*/
-if (typeof(define) === 'function') {
- define(function () {
- return vis;
- });
-}
+Images.prototype.load = function(url) {
+ var img = this.images[url];
+ if (img == undefined) {
+ // create the image
+ var images = this;
+ img = new Image();
+ this.images[url] = img;
+ img.onload = function() {
+ if (images.callback) {
+ images.callback(this);
+ }
+ };
+ img.src = url;
+ }
+
+ return img;
+};
/**
- * Window exports
+ * @constructor Graph
+ * Create a graph visualization, displaying nodes and edges.
+ *
+ * @param {Element} container The DOM element in which the Graph will
+ * be created. Normally a div element.
+ * @param {Object} data An object containing parameters
+ * {Array} nodes
+ * {Array} edges
+ * @param {Object} options Options
*/
-if (typeof window !== 'undefined') {
- // attach the module to the window, load as a regular javascript file
- window['vis'] = vis;
-}
+function Graph (container, data, options) {
+ // create variables and set default values
+ this.containerElement = container;
+ this.width = '100%';
+ this.height = '100%';
+ this.refreshRate = 50; // milliseconds
+ this.stabilize = true; // stabilize before displaying the graph
+ this.selectable = true;
+
+ // set constant values
+ this.constants = {
+ nodes: {
+ radiusMin: 5,
+ radiusMax: 20,
+ radius: 5,
+ distance: 100, // px
+ shape: 'ellipse',
+ image: undefined,
+ widthMin: 16, // px
+ widthMax: 64, // px
+ fontColor: 'black',
+ fontSize: 14, // px
+ //fontFace: verdana,
+ fontFace: 'arial',
+ color: {
+ border: '#2B7CE9',
+ background: '#97C2FC',
+ highlight: {
+ border: '#2B7CE9',
+ background: '#D2E5FF'
+ }
+ },
+ borderColor: '#2B7CE9',
+ backgroundColor: '#97C2FC',
+ highlightColor: '#D2E5FF',
+ group: undefined
+ },
+ edges: {
+ widthMin: 1,
+ widthMax: 15,
+ width: 1,
+ style: 'line',
+ color: '#343434',
+ fontColor: '#343434',
+ fontSize: 14, // px
+ fontFace: 'arial',
+ //distance: 100, //px
+ length: 100, // px
+ dash: {
+ length: 10,
+ gap: 5,
+ altLength: undefined
+ }
+ },
+ minForce: 0.05,
+ minVelocity: 0.02, // px/s
+ maxIterations: 1000 // maximum number of iteration to stabilize
+ };
+
+ var graph = this;
+ this.nodes = {}; // object with Node objects
+ this.edges = {}; // object with Edge objects
+ // TODO: create a counter to keep track on the number of nodes having values
+ // TODO: create a counter to keep track on the number of nodes currently moving
+ // TODO: create a counter to keep track on the number of edges having values
-// inject css
-util.loadCss("/* vis.js stylesheet */\n.vis.timeline {\n}\n\n\n.vis.timeline.rootpanel {\n position: relative;\n overflow: hidden;\n\n border: 1px solid #bfbfbf;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n\n.vis.timeline .panel {\n position: absolute;\n overflow: hidden;\n}\n\n\n.vis.timeline .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n.vis.timeline .labels {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n\n padding: 0;\n margin: 0;\n\n border-right: 1px solid #bfbfbf;\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n}\n\n.vis.timeline .labels .label {\n position: absolute;\n left: 0;\n top: 0;\n width: 100%;\n border-bottom: 1px solid #bfbfbf;\n color: #4d4d4d;\n}\n\n.vis.timeline .labels .label .inner {\n display: inline-block;\n padding: 5px;\n}\n\n\n.vis.timeline .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.vis.timeline .background {\n}\n\n.vis.timeline .foreground {\n}\n\n.vis.timeline .itemset-axis {\n position: absolute;\n}\n\n.vis.timeline .groupset .itemset-axis {\n border-top: 1px solid #bfbfbf;\n}\n\n/* TODO: with orientation=='bottom', this will more or less overlap with timeline axis\n.vis.timeline .groupset .itemset-axis:last-child {\n border-top: none;\n}\n*/\n\n\n.vis.timeline .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.vis.timeline .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.vis.timeline .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.vis.timeline .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.vis.timeline .item.box {\n text-align: center;\n border-style: solid;\n border-width: 1px;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.point {\n background: none;\n}\n\n.vis.timeline .dot {\n border: 5px solid #97B0F8;\n position: absolute;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range {\n overflow: hidden;\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .content {\n position: relative;\n display: inline-block;\n}\n\n.vis.timeline .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.vis.timeline .item .content {\n margin: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.vis.timeline .axis {\n position: relative;\n}\n\n.vis.timeline .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.vis.timeline .axis .text.measure {\n position: absolute;\n padding-left: 0;\n padding-right: 0;\n margin-left: 0;\n margin-right: 0;\n visibility: hidden;\n}\n\n.vis.timeline .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.vis.timeline .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.vis.timeline .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.vis.timeline .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n");
+ this.nodesData = null; // A DataSet or DataView
+ this.edgesData = null; // A DataSet or DataView
-})()
-},{"moment":2}],2:[function(require,module,exports){
-(function(){// moment.js
-// version : 2.0.0
-// author : Tim Wood
-// license : MIT
-// momentjs.com
+ // create event listeners used to subscribe on the DataSets of the nodes and edges
+ var me = this;
+ this.nodesListeners = {
+ 'add': function (event, params) {
+ me._addNodes(params.items);
+ me.start();
+ },
+ 'update': function (event, params) {
+ me._updateNodes(params.items);
+ me.start();
+ },
+ 'remove': function (event, params) {
+ me._removeNodes(params.items);
+ me.start();
+ }
+ };
+ this.edgesListeners = {
+ 'add': function (event, params) {
+ me._addEdges(params.items);
+ me.start();
+ },
+ 'update': function (event, params) {
+ me._updateEdges(params.items);
+ me.start();
+ },
+ 'remove': function (event, params) {
+ me._removeEdges(params.items);
+ me.start();
+ }
+ };
-(function (undefined) {
+ this.groups = new Groups(); // object with groups
+ this.images = new Images(); // object with images
+ this.images.setOnloadCallback(function () {
+ graph._redraw();
+ });
- /************************************
- Constants
- ************************************/
+ // properties of the data
+ this.moving = false; // True if any of the nodes have an undefined position
- var moment,
- VERSION = "2.0.0",
- round = Math.round, i,
- // internal storage for language config files
- languages = {},
+ this.selection = [];
+ this.timer = undefined;
- // check for nodeJS
- hasModule = (typeof module !== 'undefined' && module.exports),
+ // create a frame and canvas
+ this._create();
- // ASP.NET json date format regex
- aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
+ // apply options
+ this.setOptions(options);
- // 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,
+ // draw data
+ this.setData(data);
+}
- // parsing tokens
- parseMultipleFormatChunker = /([0-9a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)/gi,
+/**
+ * Set nodes and edges, and optionally options as well.
+ *
+ * @param {Object} data Object containing parameters:
+ * {Array | DataSet | DataView} [nodes] Array with nodes
+ * {Array | DataSet | DataView} [edges] Array with edges
+ * {String} [dot] String containing data in DOT format
+ * {Options} [options] Object with options
+ */
+Graph.prototype.setData = function(data) {
+ if (data && data.dot && (data.nodes || data.edges)) {
+ throw new SyntaxError('Data must contain either parameter "dot" or ' +
+ ' parameter pair "nodes" and "edges", but not both.');
+ }
- // 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
+ // set options
+ this.setOptions(data && data.options);
- // 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',
+ // set all data
+ if (data && data.dot) {
+ // parse DOT file
+ if(data && data.dot) {
+ var dotData = vis.util.DOTToGraph(data.dot);
+ this.setData(dotData);
+ return;
+ }
+ }
+ else {
+ this._setNodes(data && data.nodes);
+ this._setEdges(data && data.edges);
+ }
- // 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/]
- ],
+ // find a stable position or start animating to a stable position
+ if (this.stabilize) {
+ this._doStabilize();
+ }
+ this.start();
+};
- // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
- parseTimezoneChunker = /([\+\-]|\d\d)/gi,
+/**
+ * Set options
+ * @param {Object} options
+ */
+Graph.prototype.setOptions = function (options) {
+ if (options) {
+ // retrieve parameter values
+ if (options.width != undefined) {this.width = options.width;}
+ if (options.height != undefined) {this.height = options.height;}
+ if (options.stabilize != undefined) {this.stabilize = options.stabilize;}
+ if (options.selectable != undefined) {this.selectable = options.selectable;}
- // 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
- },
+ // TODO: work out these options and document them
+ if (options.edges) {
+ for (var prop in options.edges) {
+ if (options.edges.hasOwnProperty(prop)) {
+ this.constants.edges[prop] = options.edges[prop];
+ }
+ }
- // format function strings
- formatFunctions = {},
+ if (options.edges.length != undefined &&
+ options.nodes && options.nodes.distance == undefined) {
+ this.constants.edges.length = options.edges.length;
+ this.constants.nodes.distance = options.edges.length * 1.25;
+ }
- // tokens to ordinalize and pad
- ordinalizeTokens = 'DDD w W M D d'.split(' '),
- paddedTokens = 'M D H h m s w W'.split(' '),
+ if (!options.edges.fontColor) {
+ this.constants.edges.fontColor = options.edges.color;
+ }
- 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 = "-";
+ // Added to support dashed lines
+ // David Jordan
+ // 2012-08-08
+ if (options.edges.dash) {
+ if (options.edges.dash.length != undefined) {
+ this.constants.edges.dash.length = options.edges.dash.length;
}
- return b + leftZeroFill(~~(a / 60), 2) + ":" + leftZeroFill(~~a % 60, 2);
- },
- ZZ : function () {
- var a = -this.zone(),
- b = "+";
- if (a < 0) {
- a = -a;
- b = "-";
+ if (options.edges.dash.gap != undefined) {
+ this.constants.edges.dash.gap = options.edges.dash.gap;
+ }
+ if (options.edges.dash.altLength != undefined) {
+ this.constants.edges.dash.altLength = options.edges.dash.altLength;
}
- 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));
- };
- }
+ if (options.nodes) {
+ for (prop in options.nodes) {
+ if (options.nodes.hasOwnProperty(prop)) {
+ this.constants.nodes[prop] = options.nodes[prop];
+ }
+ }
- 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);
+ if (options.nodes.color) {
+ this.constants.nodes.color = Node.parseColor(options.nodes.color);
+ }
+
+ /*
+ if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
+ if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
+ */
+ }
+
+ if (options.groups) {
+ for (var groupname in options.groups) {
+ if (options.groups.hasOwnProperty(groupname)) {
+ var group = options.groups[groupname];
+ this.groups.add(groupname, group);
+ }
+ }
+ }
}
- formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
+ this.setSize(this.width, this.height);
+ this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
+ this._setScale(1);
+};
- /************************************
- Constructors
- ************************************/
+/**
+ * fire an event
+ * @param {String} event The name of an event, for example 'select'
+ * @param {Object} params Optional object with event parameters
+ * @private
+ */
+Graph.prototype._trigger = function (event, params) {
+ events.trigger(this, event, params);
+};
- function Language() {
+/**
+ * Create the main frame for the Graph.
+ * This function is executed once when a Graph object is created. The frame
+ * contains a canvas, and this canvas contains all objects like the axis and
+ * nodes.
+ * @private
+ */
+Graph.prototype._create = function () {
+ // remove all elements from the container element.
+ while (this.containerElement.hasChildNodes()) {
+ this.containerElement.removeChild(this.containerElement.firstChild);
}
- // Moment prototype object
- function Moment(config) {
- extend(this, config);
+ this.frame = document.createElement('div');
+ this.frame.className = 'graph-frame';
+ this.frame.style.position = 'relative';
+ this.frame.style.overflow = 'hidden';
+
+ // create the graph canvas (HTML canvas element)
+ this.frame.canvas = document.createElement( 'canvas' );
+ this.frame.canvas.style.position = 'relative';
+ this.frame.appendChild(this.frame.canvas);
+ if (!this.frame.canvas.getContext) {
+ var noCanvas = document.createElement( 'DIV' );
+ noCanvas.style.color = 'red';
+ noCanvas.style.fontWeight = 'bold' ;
+ noCanvas.style.padding = '10px';
+ noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
+ this.frame.canvas.appendChild(noCanvas);
}
- // 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;
+ var me = this;
+ this.drag = {};
+ this.pinch = {};
+ this.hammer = Hammer(this.frame.canvas, {
+ prevent_default: true
+ });
+ this.hammer.on('tap', me._onTap.bind(me) );
+ this.hammer.on('hold', me._onHold.bind(me) );
+ this.hammer.on('pinch', me._onPinch.bind(me) );
+ this.hammer.on('touch', me._onTouch.bind(me) );
+ this.hammer.on('dragstart', me._onDragStart.bind(me) );
+ this.hammer.on('drag', me._onDrag.bind(me) );
+ this.hammer.on('dragend', me._onDragEnd.bind(me) );
+ this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
+ this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
- // 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;
+ // add the frame to the container element
+ this.containerElement.appendChild(this.frame);
+};
+
+/**
+ *
+ * @param {{x: Number, y: Number}} pointer
+ * @return {Number | null} node
+ * @private
+ */
+Graph.prototype._getNodeAt = function (pointer) {
+ var x = this._canvasToX(pointer.x);
+ var y = this._canvasToY(pointer.y);
+
+ var obj = {
+ left: x,
+ top: y,
+ right: x,
+ bottom: y
+ };
+
+ // if there are overlapping nodes, select the last one, this is the
+ // one which is drawn on top of the others
+ var overlappingNodes = this._getNodesOverlappingWith(obj);
+ return (overlappingNodes.length > 0) ?
+ overlappingNodes[overlappingNodes.length - 1] : null;
+};
+
+/**
+ * Get the pointer location from a touch location
+ * @param {{pageX: Number, pageY: Number}} touch
+ * @return {{x: Number, y: Number}} pointer
+ * @private
+ */
+Graph.prototype._getPointer = function (touch) {
+ return {
+ x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas),
+ y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas)
+ };
+};
+
+/**
+ * On start of a touch gesture, store the pointer
+ * @param event
+ * @private
+ */
+Graph.prototype._onTouch = function (event) {
+ this.drag.pointer = this._getPointer(event.gesture.touches[0]);
+ this.drag.pinched = false;
+ this.pinch.scale = this._getScale();
+};
- // The following code bubbles up values, see the tests for
- // examples of what that means.
- data.milliseconds = milliseconds % 1000;
- seconds += absRound(milliseconds / 1000);
+/**
+ * handle drag start event
+ * @private
+ */
+Graph.prototype._onDragStart = function () {
+ var drag = this.drag;
- data.seconds = seconds % 60;
- minutes += absRound(seconds / 60);
+ drag.selection = [];
+ drag.translation = this._getTranslation();
+ drag.nodeId = this._getNodeAt(drag.pointer);
+ // note: drag.pointer is set in _onTouch to get the initial touch location
- data.minutes = minutes % 60;
- hours += absRound(minutes / 60);
+ var node = this.nodes[drag.nodeId];
+ if (node) {
+ // select the clicked node if not yet selected
+ if (!node.isSelected()) {
+ this._selectNodes([drag.nodeId]);
+ }
- data.hours = hours % 24;
- days += absRound(hours / 24);
+ // create an array with the selected nodes and their original location and status
+ var me = this;
+ this.selection.forEach(function (id) {
+ var node = me.nodes[id];
+ if (node) {
+ var s = {
+ id: id,
+ node: node,
+
+ // store original x, y, xFixed and yFixed, make the node temporarily Fixed
+ x: node.x,
+ y: node.y,
+ xFixed: node.xFixed,
+ yFixed: node.yFixed
+ };
- days += weeks * 7;
- data.days = days % 30;
+ node.xFixed = true;
+ node.yFixed = true;
- months += absRound(days / 30);
+ drag.selection.push(s);
+ }
+ });
- data.months = months % 12;
- years += absRound(months / 12);
+ }
+};
- data.years = years;
+/**
+ * handle drag event
+ * @private
+ */
+Graph.prototype._onDrag = function (event) {
+ if (this.drag.pinched) {
+ return;
}
+ var pointer = this._getPointer(event.gesture.touches[0]);
- /************************************
- Helpers
- ************************************/
+ var me = this,
+ drag = this.drag,
+ selection = drag.selection;
+ if (selection && selection.length) {
+ // calculate delta's and new location
+ var deltaX = pointer.x - drag.pointer.x,
+ deltaY = pointer.y - drag.pointer.y;
+ // update position of all selected nodes
+ selection.forEach(function (s) {
+ var node = s.node;
- function extend(a, b) {
- for (var i in b) {
- if (b.hasOwnProperty(i)) {
- a[i] = b[i];
+ if (!s.xFixed) {
+ node.x = me._canvasToX(me._xToCanvas(s.x) + deltaX);
+ }
+
+ if (!s.yFixed) {
+ node.y = me._canvasToY(me._yToCanvas(s.y) + deltaY);
}
+ });
+
+ // start animation if not yet running
+ if (!this.moving) {
+ this.moving = true;
+ this.start();
}
- return a;
}
+ else {
+ // move the graph
+ var diffX = pointer.x - this.drag.pointer.x;
+ var diffY = pointer.y - this.drag.pointer.y;
- function absRound(number) {
- if (number < 0) {
- return Math.ceil(number);
- } else {
- return Math.floor(number);
- }
+ this._setTranslation(
+ this.drag.translation.x + diffX,
+ this.drag.translation.y + diffY);
+ this._redraw();
+
+ this.moved = true;
}
+};
- // 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;
+/**
+ * handle drag start event
+ * @private
+ */
+Graph.prototype._onDragEnd = function () {
+ var selection = this.drag.selection;
+ if (selection) {
+ selection.forEach(function (s) {
+ // restore original xFixed and yFixed
+ s.node.xFixed = s.xFixed;
+ s.node.yFixed = s.yFixed;
+ });
}
+};
- // helper function for _.addTime and _.subtractTime
- function addOrSubtractDurationFromMoment(mom, duration, isAdding) {
- var ms = duration._milliseconds,
- d = duration._days,
- M = duration._months,
- currentDate;
+/**
+ * handle tap/click event: select/unselect a node
+ * @private
+ */
+Graph.prototype._onTap = function (event) {
+ var pointer = this._getPointer(event.gesture.touches[0]);
- 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()));
+ var nodeId = this._getNodeAt(pointer);
+ var node = this.nodes[nodeId];
+ if (node) {
+ // select this node
+ this._selectNodes([nodeId]);
+
+ if (!this.moving) {
+ this._redraw();
}
}
-
- // check if is an array
- function isArray(input) {
- return Object.prototype.toString.call(input) === '[object Array]';
+ else {
+ // remove selection
+ this._unselectNodes();
+ this._redraw();
}
+};
- // 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++;
- }
+/**
+ * handle long tap event: multi select nodes
+ * @private
+ */
+Graph.prototype._onHold = function (event) {
+ var pointer = this._getPointer(event.gesture.touches[0]);
+ var nodeId = this._getNodeAt(pointer);
+ var node = this.nodes[nodeId];
+ if (node) {
+ if (!node.isSelected()) {
+ // select this node, keep previous selection
+ var append = true;
+ this._selectNodes([nodeId], append);
+ }
+ else {
+ this._unselectNodes([nodeId]);
}
- return diffs + lengthDiff;
- }
+ if (!this.moving) {
+ this._redraw();
+ }
+ }
+ else {
+ // Do nothing
+ }
+};
- /************************************
- Languages
- ************************************/
+/**
+ * Handle pinch event
+ * @param event
+ * @private
+ */
+Graph.prototype._onPinch = function (event) {
+ var pointer = this._getPointer(event.gesture.center);
+ this.drag.pinched = true;
+ if (!('scale' in this.pinch)) {
+ this.pinch.scale = 1;
+ }
- 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;
- }
- }
- },
+ // TODO: enable moving while pinching?
+ var scale = this.pinch.scale * event.gesture.scale;
+ this._zoom(scale, pointer)
+};
- _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
- months : function (m) {
- return this._months[m.month()];
- },
+/**
+ * Zoom the graph in or out
+ * @param {Number} scale a number around 1, and between 0.01 and 10
+ * @param {{x: Number, y: Number}} pointer
+ * @return {Number} appliedScale scale is limited within the boundaries
+ * @private
+ */
+Graph.prototype._zoom = function(scale, pointer) {
+ var scaleOld = this._getScale();
+ if (scale < 0.01) {
+ scale = 0.01;
+ }
+ if (scale > 10) {
+ scale = 10;
+ }
- _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
- monthsShort : function (m) {
- return this._monthsShort[m.month()];
- },
+ var translation = this._getTranslation();
+ var scaleFrac = scale / scaleOld;
+ var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
+ var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
- monthsParse : function (monthName) {
- var i, mom, regex, output;
+ this._setScale(scale);
+ this._setTranslation(tx, ty);
+ this._redraw();
- if (!this._monthsParse) {
- this._monthsParse = [];
- }
+ return scale;
+};
- 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;
- }
- }
- },
+/**
+ * Event handler for mouse wheel event, used to zoom the timeline
+ * See http://adomas.org/javascript-mouse-wheel/
+ * https://github.com/EightMedia/hammer.js/issues/256
+ * @param {MouseEvent} event
+ * @private
+ */
+Graph.prototype._onMouseWheel = function(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;
+ }
- _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
- weekdays : function (m) {
- return this._weekdays[m.day()];
- },
+ // 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) {
+ if (!('mouswheelScale' in this.pinch)) {
+ this.pinch.mouswheelScale = 1;
+ }
- _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
- weekdaysShort : function (m) {
- return this._weekdaysShort[m.day()];
- },
+ // calculate the new scale
+ var scale = this.pinch.mouswheelScale;
+ var zoom = delta / 10;
+ if (delta < 0) {
+ zoom = zoom / (1 - zoom);
+ }
+ scale *= (1 + zoom);
- _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
- weekdaysMin : function (m) {
- return this._weekdaysMin[m.day()];
- },
+ // calculate the pointer location
+ var gesture = Hammer.event.collectEventData(this, 'scroll', event);
+ var pointer = this._getPointer(gesture.center);
- _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;
- },
+ // apply the new scale
+ scale = this._zoom(scale, pointer);
- meridiem : function (hours, minutes, isLower) {
- if (hours > 11) {
- return isLower ? 'pm' : 'PM';
- } else {
- return isLower ? 'am' : 'AM';
- }
- },
+ // store the new, applied scale
+ this.pinch.mouswheelScale = scale;
+ }
- _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;
- },
+ // Prevent default actions caused by mouse wheel.
+ event.preventDefault();
+};
- _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",
+/**
+ * Mouse move handler for checking whether the title moves over a node with a title.
+ * @param {Event} event
+ * @private
+ */
+Graph.prototype._onMouseMoveTitle = function (event) {
+ var gesture = Hammer.event.collectEventData(this, 'mousemove', event);
+ var pointer = this._getPointer(gesture.center);
- preparse : function (string) {
- return string;
- },
+ // check if the previously selected node is still selected
+ if (this.popupNode) {
+ this._checkHidePopup(pointer);
+ }
- postformat : function (string) {
- return string;
- },
+ // start a timeout that will check if the mouse is positioned above
+ // an element
+ var me = this;
+ var checkShow = function() {
+ me._checkShowPopup(pointer);
+ };
+ if (this.popupTimer) {
+ clearInterval(this.popupTimer); // stop any running timer
+ }
+ if (!this.leftButtonDown) {
+ this.popupTimer = setTimeout(checkShow, 300);
+ }
+};
- 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.
- }
+/**
+ * Check if there is an element on the given position in the graph
+ * (a node or edge). If so, and if this element has a title,
+ * show a popup window with its title.
+ *
+ * @param {{x:Number, y:Number}} pointer
+ * @private
+ */
+Graph.prototype._checkShowPopup = function (pointer) {
+ var obj = {
+ left: this._canvasToX(pointer.x),
+ top: this._canvasToY(pointer.y),
+ right: this._canvasToX(pointer.x),
+ bottom: this._canvasToY(pointer.y)
};
- // 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();
+ var id;
+ var lastPopupNode = this.popupNode;
+
+ if (this.popupNode == undefined) {
+ // search the nodes for overlap, select the top one in case of multiple nodes
+ var nodes = this.nodes;
+ for (id in nodes) {
+ if (nodes.hasOwnProperty(id)) {
+ var node = nodes[id];
+ if (node.getTitle() != undefined && node.isOverlappingWith(obj)) {
+ this.popupNode = node;
+ break;
+ }
+ }
}
- 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);
+ if (this.popupNode == undefined) {
+ // search the edges for overlap
+ var edges = this.edges;
+ for (id in edges) {
+ if (edges.hasOwnProperty(id)) {
+ var edge = edges[id];
+ if (edge.connected && (edge.getTitle() != undefined) &&
+ edge.isOverlappingWith(obj)) {
+ this.popupNode = edge;
+ break;
+ }
+ }
}
- return languages[key];
}
+ if (this.popupNode) {
+ // show popup message window
+ if (this.popupNode != lastPopupNode) {
+ var me = this;
+ if (!me.popup) {
+ me.popup = new Popup(me.frame);
+ }
- /************************************
- Formatting
- ************************************/
-
-
- function removeFormattingTokens(input) {
- if (input.match(/\[.*\]/)) {
- return input.replace(/^\[|\]$/g, "");
+ // adjust a small offset such that the mouse cursor is located in the
+ // bottom left location of the popup, and you can easily move over the
+ // popup area
+ me.popup.setPosition(pointer.x - 3, pointer.y - 3);
+ me.popup.setText(me.popupNode.getTitle());
+ me.popup.show();
}
- return input.replace(/\\/g, "");
}
+ else {
+ if (this.popup) {
+ this.popup.hide();
+ }
+ }
+};
- 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]);
- }
+/**
+ * Check if the popup must be hided, which is the case when the mouse is no
+ * longer hovering on the object
+ * @param {{x:Number, y:Number}} pointer
+ * @private
+ */
+Graph.prototype._checkHidePopup = function (pointer) {
+ if (!this.popupNode || !this._getNodeAt(pointer) ) {
+ this.popupNode = undefined;
+ if (this.popup) {
+ this.popup.hide();
}
-
- 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;
+/**
+ * Unselect selected nodes. If no selection array is provided, all nodes
+ * are unselected
+ * @param {Object[]} selection Array with selection objects, each selection
+ * object has a parameter row. Optional
+ * @param {Boolean} triggerSelect If true (default), the select event
+ * is triggered when nodes are unselected
+ * @return {Boolean} changed True if the selection is changed
+ * @private
+ */
+Graph.prototype._unselectNodes = function(selection, triggerSelect) {
+ var changed = false;
+ var i, iMax, id;
- function replaceLongDateFormatTokens(input) {
- return m.lang().longDateFormat(input) || input;
- }
+ if (selection) {
+ // remove provided selections
+ for (i = 0, iMax = selection.length; i < iMax; i++) {
+ id = selection[i];
+ this.nodes[id].unselect();
- while (i-- && localFormattingTokens.test(format)) {
- format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
+ var j = 0;
+ while (j < this.selection.length) {
+ if (this.selection[j] == id) {
+ this.selection.splice(j, 1);
+ changed = true;
+ }
+ else {
+ j++;
+ }
+ }
}
-
- if (!formatFunctions[format]) {
- formatFunctions[format] = makeFormatFunction(format);
+ }
+ else if (this.selection && this.selection.length) {
+ // remove all selections
+ for (i = 0, iMax = this.selection.length; i < iMax; i++) {
+ id = this.selection[i];
+ this.nodes[id].unselect();
+ changed = true;
}
+ this.selection = [];
+ }
- return formatFunctions[format](m);
+ if (changed && (triggerSelect == true || triggerSelect == undefined)) {
+ // fire the select event
+ this._trigger('select');
}
+ return changed;
+};
- /************************************
- Parsing
- ************************************/
+/**
+ * select all nodes on given location x, y
+ * @param {Array} selection an array with node ids
+ * @param {boolean} append If true, the new selection will be appended to the
+ * current selection (except for duplicate entries)
+ * @return {Boolean} changed True if the selection is changed
+ * @private
+ */
+Graph.prototype._selectNodes = function(selection, append) {
+ var changed = false;
+ var i, iMax;
+ // TODO: the selectNodes method is a little messy, rework this
- // 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('\\', ''));
+ // check if the current selection equals the desired selection
+ var selectionAlreadyThere = true;
+ if (selection.length != this.selection.length) {
+ selectionAlreadyThere = false;
+ }
+ else {
+ for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) {
+ if (selection[i] != this.selection[i]) {
+ selectionAlreadyThere = false;
+ break;
+ }
}
}
+ if (selectionAlreadyThere) {
+ return changed;
+ }
- // function to convert string input to date
- function addTimeToArrayFromToken(token, input, config) {
- var a, b,
- datePartArray = config._a;
+ if (append == undefined || append == false) {
+ // first deselect any selected node
+ var triggerSelect = false;
+ changed = this._unselectNodes(undefined, triggerSelect);
+ }
- 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;
+ for (i = 0, iMax = selection.length; i < iMax; i++) {
+ // add each of the new selections, but only when they are not duplicate
+ var id = selection[i];
+ var isDuplicate = (this.selection.indexOf(id) != -1);
+ if (!isDuplicate) {
+ this.nodes[id].select();
+ this.selection.push(id);
+ changed = true;
}
+ }
- // if the input is null, the date is not valid
- if (input == null) {
- config._isValid = false;
- }
+ if (changed) {
+ // fire the select event
+ this._trigger('select');
}
- // 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 = [];
+ return changed;
+};
- if (config._d) {
- return;
- }
+/**
+ * retrieve all nodes overlapping with given object
+ * @param {Object} obj An object with parameters left, top, right, bottom
+ * @return {Number[]} An array with id's of the overlapping nodes
+ * @private
+ */
+Graph.prototype._getNodesOverlappingWith = function (obj) {
+ var nodes = this.nodes,
+ overlappingNodes = [];
- for (i = 0; i < 7; i++) {
- config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
+ for (var id in nodes) {
+ if (nodes.hasOwnProperty(id)) {
+ if (nodes[id].isOverlappingWith(obj)) {
+ overlappingNodes.push(id);
+ }
}
+ }
+
+ return overlappingNodes;
+};
- // 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;
+/**
+ * retrieve the currently selected nodes
+ * @return {Number[] | String[]} selection An array with the ids of the
+ * selected nodes.
+ */
+Graph.prototype.getSelection = function() {
+ return this.selection.concat([]);
+};
- date = new Date(0);
+/**
+ * select zero or more nodes
+ * @param {Number[] | String[]} selection An array with the ids of the
+ * selected nodes.
+ */
+Graph.prototype.setSelection = function(selection) {
+ var i, iMax, id;
- 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]);
- }
+ if (!selection || (selection.length == undefined))
+ throw 'Selection must be an array with ids';
- config._d = date;
+ // first unselect any selected node
+ for (i = 0, iMax = this.selection.length; i < iMax; i++) {
+ id = this.selection[i];
+ this.nodes[id].unselect();
}
- // 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;
+ this.selection = [];
- config._a = [];
+ for (i = 0, iMax = selection.length; i < iMax; i++) {
+ id = selection[i];
- 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);
- }
+ var node = this.nodes[id];
+ if (!node) {
+ throw new RangeError('Node with id "' + id + '" not found');
}
- // handle am pm
- if (config._isPm && config._a[3] < 12) {
- config._a[3] += 12;
+ node.select();
+ this.selection.push(id);
+ }
+
+ this.redraw();
+};
+
+/**
+ * Validate the selection: remove ids of nodes which no longer exist
+ * @private
+ */
+Graph.prototype._updateSelection = function () {
+ var i = 0;
+ while (i < this.selection.length) {
+ var id = this.selection[i];
+ if (!this.nodes[id]) {
+ this.selection.splice(i, 1);
}
- // if is 12 am, change hours to 0
- if (config._isPm === false && config._a[3] === 12) {
- config._a[3] = 0;
+ else {
+ i++;
}
- // return
- dateFromArray(config);
}
+};
- // date from string and array of format strings
- function makeDateFromStringAndArray(config) {
- var tempConfig,
- tempMoment,
- bestMoment,
+/**
+ * Temporary method to test calculating a hub value for the nodes
+ * @param {number} level Maximum number edges between two nodes in order
+ * to call them connected. Optional, 1 by default
+ * @return {Number[]} connectioncount array with the connection count
+ * for each node
+ * @private
+ */
+Graph.prototype._getConnectionCount = function(level) {
+ if (level == undefined) {
+ level = 1;
+ }
- scoreToBeat = 99,
- i,
- currentDate,
- currentScore;
+ // get the nodes connected to given nodes
+ function getConnectedNodes(nodes) {
+ var connectedNodes = [];
- while (config._f.length) {
- tempConfig = extend({}, config);
- tempConfig._f = config._f.pop();
- makeDateFromStringAndFormat(tempConfig);
- tempMoment = new Moment(tempConfig);
+ for (var j = 0, jMax = nodes.length; j < jMax; j++) {
+ var node = nodes[j];
- if (tempMoment.isValid()) {
- bestMoment = tempMoment;
- break;
- }
+ // find all nodes connected to this node
+ var edges = node.edges;
+ for (var i = 0, iMax = edges.length; i < iMax; i++) {
+ var edge = edges[i];
+ var other = null;
- currentScore = compareArrays(tempConfig._a, tempMoment.toArray());
+ // check if connected
+ if (edge.from == node)
+ other = edge.to;
+ else if (edge.to == node)
+ other = edge.from;
- if (currentScore < scoreToBeat) {
- scoreToBeat = currentScore;
- bestMoment = tempMoment;
+ // check if the other node is not already in the list with nodes
+ var k, kMax;
+ if (other) {
+ for (k = 0, kMax = nodes.length; k < kMax; k++) {
+ if (nodes[k] == other) {
+ other = null;
+ break;
+ }
+ }
+ }
+ if (other) {
+ for (k = 0, kMax = connectedNodes.length; k < kMax; k++) {
+ if (connectedNodes[k] == other) {
+ other = null;
+ break;
+ }
+ }
+ }
+
+ if (other)
+ connectedNodes.push(other);
}
}
- extend(config, bestMoment);
+ return connectedNodes;
}
- // 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";
+ var connections = [];
+ var nodes = this.nodes;
+ for (var id in nodes) {
+ if (nodes.hasOwnProperty(id)) {
+ var c = [nodes[id]];
+ for (var l = 0; l < level; l++) {
+ c = c.concat(getConnectedNodes(c));
}
- makeDateFromStringAndFormat(config);
- } else {
- config._d = new Date(string);
+ connections.push(c);
}
}
- 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);
- }
+ var hubs = [];
+ for (var i = 0, len = connections.length; i < len; i++) {
+ hubs.push(connections[i].length);
}
+ return hubs;
+};
- /************************************
- 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);
- }
+/**
+ * Set a new size for the graph
+ * @param {string} width Width in pixels or percentage (for example '800px'
+ * or '50%')
+ * @param {string} height Height in pixels or percentage (for example '400px'
+ * or '30%')
+ */
+Graph.prototype.setSize = function(width, height) {
+ this.frame.style.width = width;
+ this.frame.style.height = height;
+ this.frame.canvas.style.width = '100%';
+ this.frame.canvas.style.height = '100%';
- /************************************
- Week of Year
- ************************************/
+ this.frame.canvas.width = this.frame.canvas.clientWidth;
+ this.frame.canvas.height = this.frame.canvas.clientHeight;
+};
+/**
+ * Set a data set with nodes for the graph
+ * @param {Array | DataSet | DataView} nodes The data containing the nodes.
+ * @private
+ */
+Graph.prototype._setNodes = function(nodes) {
+ var oldNodesData = this.nodesData;
- // 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 (nodes instanceof DataSet || nodes instanceof DataView) {
+ this.nodesData = nodes;
+ }
+ else if (nodes instanceof Array) {
+ this.nodesData = new DataSet();
+ this.nodesData.add(nodes);
+ }
+ else if (!nodes) {
+ this.nodesData = new DataSet();
+ }
+ else {
+ throw new TypeError('Array or DataSet expected');
+ }
+ if (oldNodesData) {
+ // unsubscribe from old dataset
+ util.forEach(this.nodesListeners, function (callback, event) {
+ oldNodesData.unsubscribe(event, callback);
+ });
+ }
- if (daysToDayOfWeek > end) {
- daysToDayOfWeek -= 7;
- }
+ // remove drawn nodes
+ this.nodes = {};
- if (daysToDayOfWeek < end - 7) {
- daysToDayOfWeek += 7;
- }
+ if (this.nodesData) {
+ // subscribe to new dataset
+ var me = this;
+ util.forEach(this.nodesListeners, function (callback, event) {
+ me.nodesData.subscribe(event, callback);
+ });
- return Math.ceil(moment(mom).add('d', daysToDayOfWeek).dayOfYear() / 7);
+ // draw all new nodes
+ var ids = this.nodesData.getIds();
+ this._addNodes(ids);
}
+ this._updateSelection();
+};
- /************************************
- Top Level Functions
- ************************************/
+/**
+ * Add nodes
+ * @param {Number[] | String[]} ids
+ * @private
+ */
+Graph.prototype._addNodes = function(ids) {
+ var id;
+ for (var i = 0, len = ids.length; i < len; i++) {
+ id = ids[i];
+ var data = this.nodesData.get(id);
+ var node = new Node(data, this.images, this.groups, this.constants);
+ this.nodes[id] = node; // note: this may replace an existing node
- function makeMoment(config) {
- var input = config._i,
- format = config._f;
+ if (!node.isFixed()) {
+ // TODO: position new nodes in a smarter way!
+ var radius = this.constants.edges.length * 2;
+ var count = ids.length;
+ var angle = 2 * Math.PI * (i / count);
+ node.x = radius * Math.cos(angle);
+ node.y = radius * Math.sin(angle);
- if (input === null || input === '') {
- return null;
+ // note: no not use node.isMoving() here, as that gives the current
+ // velocity of the node, which is zero after creation of the node.
+ this.moving = true;
}
+ }
- if (typeof input === 'string') {
- config._i = input = getLangDefinition().preparse(input);
+ this._reconnectEdges();
+ this._updateValueRange(this.nodes);
+};
+
+/**
+ * Update existing nodes, or create them when not yet existing
+ * @param {Number[] | String[]} ids
+ * @private
+ */
+Graph.prototype._updateNodes = function(ids) {
+ var nodes = this.nodes,
+ nodesData = this.nodesData;
+ for (var i = 0, len = ids.length; i < len; i++) {
+ var id = ids[i];
+ var node = nodes[id];
+ var data = nodesData.get(id);
+ if (node) {
+ // update node
+ node.setProperties(data, this.constants);
}
+ else {
+ // create node
+ node = new Node(properties, this.images, this.groups, this.constants);
+ nodes[id] = node;
- if (moment.isMoment(input)) {
- config = extend({}, input);
- config._d = new Date(+input._d);
- } else if (format) {
- if (isArray(format)) {
- makeDateFromStringAndArray(config);
- } else {
- makeDateFromStringAndFormat(config);
+ if (!node.isFixed()) {
+ this.moving = true;
}
- } else {
- makeDateFromInput(config);
}
+ }
- return new Moment(config);
+ this._reconnectEdges();
+ this._updateValueRange(nodes);
+};
+
+/**
+ * Remove existing nodes. If nodes do not exist, the method will just ignore it.
+ * @param {Number[] | String[]} ids
+ * @private
+ */
+Graph.prototype._removeNodes = function(ids) {
+ var nodes = this.nodes;
+ for (var i = 0, len = ids.length; i < len; i++) {
+ var id = ids[i];
+ delete nodes[id];
}
- moment = function (input, format, lang) {
- return makeMoment({
- _i : input,
- _f : format,
- _l : lang,
- _isUTC : false
+ this._reconnectEdges();
+ this._updateSelection();
+ this._updateValueRange(nodes);
+};
+
+/**
+ * Load edges by reading the data table
+ * @param {Array | DataSet | DataView} edges The data containing the edges.
+ * @private
+ * @private
+ */
+Graph.prototype._setEdges = function(edges) {
+ var oldEdgesData = this.edgesData;
+
+ if (edges instanceof DataSet || edges instanceof DataView) {
+ this.edgesData = edges;
+ }
+ else if (edges instanceof Array) {
+ this.edgesData = new DataSet();
+ this.edgesData.add(edges);
+ }
+ else if (!edges) {
+ this.edgesData = new DataSet();
+ }
+ else {
+ throw new TypeError('Array or DataSet expected');
+ }
+
+ if (oldEdgesData) {
+ // unsubscribe from old dataset
+ util.forEach(this.edgesListeners, function (callback, event) {
+ oldEdgesData.unsubscribe(event, callback);
});
- };
+ }
- // creating with utc
- moment.utc = function (input, format, lang) {
- return makeMoment({
- _useUTC : true,
- _isUTC : true,
- _l : lang,
- _i : input,
- _f : format
+ // remove drawn edges
+ this.edges = {};
+
+ if (this.edgesData) {
+ // subscribe to new dataset
+ var me = this;
+ util.forEach(this.edgesListeners, function (callback, event) {
+ me.edgesData.subscribe(event, callback);
});
- };
- // creating with unix timestamp (in seconds)
- moment.unix = function (input) {
- return moment(input * 1000);
- };
+ // draw all new nodes
+ var ids = this.edgesData.getIds();
+ this._addEdges(ids);
+ }
- // duration
- moment.duration = function (input, key) {
- var isDuration = moment.isDuration(input),
- isNumber = (typeof input === 'number'),
- duration = (isDuration ? input._data : (isNumber ? {} : input)),
- ret;
+ this._reconnectEdges();
+};
- if (isNumber) {
- if (key) {
- duration[key] = input;
- } else {
- duration.milliseconds = input;
- }
+/**
+ * Add edges
+ * @param {Number[] | String[]} ids
+ * @private
+ */
+Graph.prototype._addEdges = function (ids) {
+ var edges = this.edges,
+ edgesData = this.edgesData;
+ for (var i = 0, len = ids.length; i < len; i++) {
+ var id = ids[i];
+
+ var oldEdge = edges[id];
+ if (oldEdge) {
+ oldEdge.disconnect();
}
- ret = new Duration(duration);
+ var data = edgesData.get(id);
+ edges[id] = new Edge(data, this, this.constants);
+ }
- if (isDuration && input.hasOwnProperty('_lang')) {
- ret._lang = input._lang;
- }
+ this.moving = true;
+ this._updateValueRange(edges);
+};
- return ret;
- };
+/**
+ * Update existing edges, or create them when not yet existing
+ * @param {Number[] | String[]} ids
+ * @private
+ */
+Graph.prototype._updateEdges = function (ids) {
+ var edges = this.edges,
+ edgesData = this.edgesData;
+ for (var i = 0, len = ids.length; i < len; i++) {
+ var id = ids[i];
- // version number
- moment.version = VERSION;
+ var data = edgesData.get(id);
+ var edge = edges[id];
+ if (edge) {
+ // update edge
+ edge.disconnect();
+ edge.setProperties(data, this.constants);
+ edge.connect();
+ }
+ else {
+ // create edge
+ edge = new Edge(data, this, this.constants);
+ this.edges[id] = edge;
+ }
+ }
- // default format
- moment.defaultFormat = isoFormat;
+ this.moving = true;
+ this._updateValueRange(edges);
+};
+
+/**
+ * Remove existing edges. Non existing ids will be ignored
+ * @param {Number[] | String[]} ids
+ * @private
+ */
+Graph.prototype._removeEdges = function (ids) {
+ var edges = this.edges;
+ for (var i = 0, len = ids.length; i < len; i++) {
+ var id = ids[i];
+ var edge = edges[id];
+ if (edge) {
+ edge.disconnect();
+ delete edges[id];
+ }
+ }
- // 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;
+ this.moving = true;
+ this._updateValueRange(edges);
+};
- if (!key) {
- return moment.fn._lang._abbr;
- }
- if (values) {
- loadLang(key, values);
- } else if (!languages[key]) {
- getLangDefinition(key);
+/**
+ * Reconnect all edges
+ * @private
+ */
+Graph.prototype._reconnectEdges = function() {
+ var id,
+ nodes = this.nodes,
+ edges = this.edges;
+ for (id in nodes) {
+ if (nodes.hasOwnProperty(id)) {
+ nodes[id].edges = [];
}
- 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;
+ for (id in edges) {
+ if (edges.hasOwnProperty(id)) {
+ var edge = edges[id];
+ edge.from = null;
+ edge.to = null;
+ edge.connect();
}
- 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;
- };
+ }
+};
+/**
+ * Update the values of all object in the given array according to the current
+ * value range of the objects in the array.
+ * @param {Object} obj An object containing a set of Edges or Nodes
+ * The objects must have a method getValue() and
+ * setValueRange(min, max).
+ * @private
+ */
+Graph.prototype._updateValueRange = function(obj) {
+ var id;
- /************************************
- Moment Prototype
- ************************************/
+ // determine the range of the objects
+ var valueMin = undefined;
+ var valueMax = undefined;
+ for (id in obj) {
+ if (obj.hasOwnProperty(id)) {
+ var value = obj[id].getValue();
+ if (value !== undefined) {
+ valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
+ valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
+ }
+ }
+ }
+ // adjust the range of all objects
+ if (valueMin !== undefined && valueMax !== undefined) {
+ for (id in obj) {
+ if (obj.hasOwnProperty(id)) {
+ obj[id].setValueRange(valueMin, valueMax);
+ }
+ }
+ }
+};
- moment.fn = Moment.prototype = {
+/**
+ * Redraw the graph with the current data
+ * chart will be resized too.
+ */
+Graph.prototype.redraw = function() {
+ this.setSize(this.width, this.height);
- clone : function () {
- return moment(this);
- },
+ this._redraw();
+};
- valueOf : function () {
- return +this._d;
- },
+/**
+ * Redraw the graph with the current data
+ * @private
+ */
+Graph.prototype._redraw = function() {
+ var ctx = this.frame.canvas.getContext('2d');
- unix : function () {
- return Math.floor(+this._d / 1000);
- },
+ // clear the canvas
+ var w = this.frame.canvas.width;
+ var h = this.frame.canvas.height;
+ ctx.clearRect(0, 0, w, h);
- toString : function () {
- return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
- },
+ // set scaling and translation
+ ctx.save();
+ ctx.translate(this.translation.x, this.translation.y);
+ ctx.scale(this.scale, this.scale);
- toDate : function () {
- return this._d;
- },
+ this._drawEdges(ctx);
+ this._drawNodes(ctx);
- toJSON : function () {
- return moment.utc(this).format('YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
- },
+ // restore original scaling and translation
+ ctx.restore();
+};
- toArray : function () {
- var m = this;
- return [
- m.year(),
- m.month(),
- m.date(),
- m.hours(),
- m.minutes(),
- m.seconds(),
- m.milliseconds()
- ];
- },
+/**
+ * Set the translation of the graph
+ * @param {Number} offsetX Horizontal offset
+ * @param {Number} offsetY Vertical offset
+ * @private
+ */
+Graph.prototype._setTranslation = function(offsetX, offsetY) {
+ if (this.translation === undefined) {
+ this.translation = {
+ x: 0,
+ y: 0
+ };
+ }
- 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;
- },
+ if (offsetX !== undefined) {
+ this.translation.x = offsetX;
+ }
+ if (offsetY !== undefined) {
+ this.translation.y = offsetY;
+ }
+};
- utc : function () {
- this._isUTC = true;
- return this;
- },
+/**
+ * Get the translation of the graph
+ * @return {Object} translation An object with parameters x and y, both a number
+ * @private
+ */
+Graph.prototype._getTranslation = function() {
+ return {
+ x: this.translation.x,
+ y: this.translation.y
+ };
+};
- local : function () {
- this._isUTC = false;
- return this;
- },
+/**
+ * Scale the graph
+ * @param {Number} scale Scaling factor 1.0 is unscaled
+ * @private
+ */
+Graph.prototype._setScale = function(scale) {
+ this.scale = scale;
+};
+/**
+ * Get the current scale of the graph
+ * @return {Number} scale Scaling factor 1.0 is unscaled
+ * @private
+ */
+Graph.prototype._getScale = function() {
+ return this.scale;
+};
- format : function (inputString) {
- var output = formatMoment(this, inputString || moment.defaultFormat);
- return this.lang().postformat(output);
- },
+/**
+ * Convert a horizontal point on the HTML canvas to the x-value of the model
+ * @param {number} x
+ * @returns {number}
+ * @private
+ */
+Graph.prototype._canvasToX = function(x) {
+ return (x - this.translation.x) / this.scale;
+};
- 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;
- },
+/**
+ * Convert an x-value in the model to a horizontal point on the HTML canvas
+ * @param {number} x
+ * @returns {number}
+ * @private
+ */
+Graph.prototype._xToCanvas = function(x) {
+ return x * this.scale + this.translation.x;
+};
- 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;
- },
+/**
+ * Convert a vertical point on the HTML canvas to the y-value of the model
+ * @param {number} y
+ * @returns {number}
+ * @private
+ */
+Graph.prototype._canvasToY = function(y) {
+ return (y - this.translation.y) / this.scale;
+};
- diff : function (input, units, asFloat) {
- var that = this._isUTC ? moment(input).utc() : moment(input).local(),
- zoneDiff = (this.zone() - that.zone()) * 6e4,
- diff, output;
+/**
+ * Convert an y-value in the model to a vertical point on the HTML canvas
+ * @param {number} y
+ * @returns {number}
+ * @private
+ */
+Graph.prototype._yToCanvas = function(y) {
+ return y * this.scale + this.translation.y ;
+};
- if (units) {
- // standardize on singular form
- units = units.replace(/s$/, '');
+/**
+ * Redraw all nodes
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
+ * @param {CanvasRenderingContext2D} ctx
+ * @private
+ */
+Graph.prototype._drawNodes = function(ctx) {
+ // first draw the unselected nodes
+ var nodes = this.nodes;
+ var selected = [];
+ for (var id in nodes) {
+ if (nodes.hasOwnProperty(id)) {
+ if (nodes[id].isSelected()) {
+ selected.push(id);
}
-
- 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;
+ else {
+ nodes[id].draw(ctx);
}
- 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);
- },
+ // draw the selected nodes on top
+ for (var s = 0, sMax = selected.length; s < sMax; s++) {
+ nodes[selected[s]].draw(ctx);
+ }
+};
- 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));
- },
+/**
+ * Redraw all edges
+ * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
+ * @param {CanvasRenderingContext2D} ctx
+ * @private
+ */
+Graph.prototype._drawEdges = function(ctx) {
+ var edges = this.edges;
+ for (var id in edges) {
+ if (edges.hasOwnProperty(id)) {
+ var edge = edges[id];
+ if (edge.connected) {
+ edges[id].draw(ctx);
+ }
+ }
+ }
+};
- isLeapYear : function () {
- var year = this.year();
- return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
- },
+/**
+ * Find a stable position for all nodes
+ * @private
+ */
+Graph.prototype._doStabilize = function() {
+ var start = new Date();
- isDST : function () {
- return (this.zone() < moment([this.year()]).zone() ||
- this.zone() < moment([this.year(), 5]).zone());
- },
+ // find stable position
+ var count = 0;
+ var vmin = this.constants.minVelocity;
+ var stable = false;
+ while (!stable && count < this.constants.maxIterations) {
+ this._calculateForces();
+ this._discreteStepNodes();
+ stable = !this._isMoving(vmin);
+ count++;
+ }
- day : function (input) {
- var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
- return input == null ? day :
- this.add({ d : input - day });
- },
+ var end = new Date();
- 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 */
- }
+ // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup
+};
- // weeks are a special case
- if (units === 'week') {
- this.day(0);
- }
+/**
+ * Calculate the external forces acting on the nodes
+ * Forces are caused by: edges, repulsing forces between nodes, gravity
+ * @private
+ */
+Graph.prototype._calculateForces = function() {
+ // create a local edge to the nodes and edges, that is faster
+ var id, dx, dy, angle, distance, fx, fy,
+ repulsingForce, springForce, length, edgeLength,
+ nodes = this.nodes,
+ edges = this.edges;
- return this;
- },
+ // gravity, add a small constant force to pull the nodes towards the center of
+ // the graph
+ // Also, the forces are reset to zero in this loop by using _setForce instead
+ // of _addForce
+ var gravity = 0.01,
+ gx = this.frame.canvas.clientWidth / 2,
+ gy = this.frame.canvas.clientHeight / 2;
+ for (id in nodes) {
+ if (nodes.hasOwnProperty(id)) {
+ var node = nodes[id];
+ dx = gx - node.x;
+ dy = gy - node.y;
+ angle = Math.atan2(dy, dx);
+ fx = Math.cos(angle) * gravity;
+ fy = Math.sin(angle) * gravity;
- endOf: function (units) {
- return this.startOf(units).add(units.replace(/s?$/, 's'), 1).subtract('ms', 1);
- },
+ node._setForce(fx, fy);
+ }
+ }
- isAfter: function (input, units) {
- units = typeof units !== 'undefined' ? units : 'millisecond';
- return +this.clone().startOf(units) > +moment(input).startOf(units);
- },
+ // repulsing forces between nodes
+ var minimumDistance = this.constants.nodes.distance,
+ steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
- isBefore: function (input, units) {
- units = typeof units !== 'undefined' ? units : 'millisecond';
- return +this.clone().startOf(units) < +moment(input).startOf(units);
- },
+ for (var id1 in nodes) {
+ if (nodes.hasOwnProperty(id1)) {
+ var node1 = nodes[id1];
+ for (var id2 in nodes) {
+ if (nodes.hasOwnProperty(id2)) {
+ var node2 = nodes[id2];
+ // calculate normally distributed force
+ dx = node2.x - node1.x;
+ dy = node2.y - node1.y;
+ distance = Math.sqrt(dx * dx + dy * dy);
+ angle = Math.atan2(dy, dx);
- isSame: function (input, units) {
- units = typeof units !== 'undefined' ? units : 'millisecond';
- return +this.clone().startOf(units) === +moment(input).startOf(units);
- },
+ // TODO: correct factor for repulsing force
+ //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
+ //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
+ repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
+ fx = Math.cos(angle) * repulsingForce;
+ fy = Math.sin(angle) * repulsingForce;
- zone : function () {
- return this._isUTC ? 0 : this._d.getTimezoneOffset();
- },
+ node1._addForce(-fx, -fy);
+ node2._addForce(fx, fy);
+ }
+ }
+ }
+ }
- daysInMonth : function () {
- return moment.utc([this.year(), this.month() + 1, 0]).date();
- },
+ /* TODO: re-implement repulsion of edges
+ for (var n = 0; n < nodes.length; n++) {
+ for (var l = 0; l < edges.length; l++) {
+ var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
+ ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
- 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));
- },
+ // calculate normally distributed force
+ dx = nodes[n].x - lx,
+ dy = nodes[n].y - ly,
+ distance = Math.sqrt(dx * dx + dy * dy),
+ angle = Math.atan2(dy, dx),
- 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);
- },
+ // TODO: correct factor for repulsing force
+ //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
+ //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
+ repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force
+ fx = Math.cos(angle) * repulsingforce,
+ fy = Math.sin(angle) * repulsingforce;
+ nodes[n]._addForce(fx, fy);
+ edges[l].from._addForce(-fx/2,-fy/2);
+ edges[l].to._addForce(-fx/2,-fy/2);
+ }
+ }
+ */
- // 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;
- }
- }
- };
+ // forces caused by the edges, modelled as springs
+ for (id in edges) {
+ if (edges.hasOwnProperty(id)) {
+ var edge = edges[id];
+ if (edge.connected) {
+ dx = (edge.to.x - edge.from.x);
+ dy = (edge.to.y - edge.from.y);
+ //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin
+ //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin
+ //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2;
+ edgeLength = edge.length;
+ length = Math.sqrt(dx * dx + dy * dy);
+ angle = Math.atan2(dy, dx);
- // 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]();
- }
- };
- }
+ springForce = edge.stiffness * (edgeLength - length);
- // 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]);
+ fx = Math.cos(angle) * springForce;
+ fy = Math.sin(angle) * springForce;
+
+ edge.from._addForce(-fx, -fy);
+ edge.to._addForce(fx, fy);
+ }
+ }
}
- // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
- makeGetterAndSetter('year', 'FullYear');
+ /* TODO: re-implement repulsion of edges
+ // repulsing forces between edges
+ var minimumDistance = this.constants.edges.distance,
+ steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
+ for (var l = 0; l < edges.length; l++) {
+ //Keep distance from other edge centers
+ for (var l2 = l + 1; l2 < this.edges.length; l2++) {
+ //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin
+ //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin
+ //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0),
+ var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
+ ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
+ l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2,
+ l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2,
- // add plural methods
- moment.fn.days = moment.fn.day;
- moment.fn.weeks = moment.fn.week;
- moment.fn.isoWeeks = moment.fn.isoWeek;
+ // calculate normally distributed force
+ dx = l2x - lx,
+ dy = l2y - ly,
+ distance = Math.sqrt(dx * dx + dy * dy),
+ angle = Math.atan2(dy, dx),
- /************************************
- Duration Prototype
- ************************************/
+ // TODO: correct factor for repulsing force
+ //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
+ //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
+ repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force
+ fx = Math.cos(angle) * repulsingforce,
+ fy = Math.sin(angle) * repulsingforce;
- moment.duration.fn = Duration.prototype = {
- weeks : function () {
- return absRound(this.days() / 7);
- },
+ edges[l].from._addForce(-fx, -fy);
+ edges[l].to._addForce(-fx, -fy);
+ edges[l2].from._addForce(fx, fy);
+ edges[l2].to._addForce(fx, fy);
+ }
+ }
+ */
+};
- valueOf : function () {
- return this._milliseconds +
- this._days * 864e5 +
- this._months * 2592e6;
- },
- humanize : function (withSuffix) {
- var difference = +this,
- output = relativeTime(difference, !withSuffix, this.lang());
+/**
+ * Check if any of the nodes is still moving
+ * @param {number} vmin the minimum velocity considered as 'moving'
+ * @return {boolean} true if moving, false if non of the nodes is moving
+ * @private
+ */
+Graph.prototype._isMoving = function(vmin) {
+ // TODO: ismoving does not work well: should check the kinetic energy, not its velocity
+ var nodes = this.nodes;
+ for (var id in nodes) {
+ if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
+ return true;
+ }
+ }
+ return false;
+};
- if (withSuffix) {
- output = this.lang().pastFuture(difference, output);
- }
- return this.lang().postformat(output);
- },
+/**
+ * Perform one discrete step for all nodes
+ * @private
+ */
+Graph.prototype._discreteStepNodes = function() {
+ var interval = this.refreshRate / 1000.0; // in seconds
+ var nodes = this.nodes;
+ for (var id in nodes) {
+ if (nodes.hasOwnProperty(id)) {
+ nodes[id].discreteStep(interval);
+ }
+ }
+};
- lang : moment.fn.lang
- };
+/**
+ * Start animating nodes and edges
+ */
+Graph.prototype.start = function() {
+ if (this.moving) {
+ this._calculateForces();
+ this._discreteStepNodes();
- function makeDurationGetter(name) {
- moment.duration.fn[name] = function () {
- return this._data[name];
- };
+ var vmin = this.constants.minVelocity;
+ this.moving = this._isMoving(vmin);
}
- function makeDurationAsGetter(name, factor) {
- moment.duration.fn['as' + name] = function () {
- return +this / factor;
- };
+ if (this.moving) {
+ // start animation. only start timer if it is not already running
+ if (!this.timer) {
+ var graph = this;
+ this.timer = window.setTimeout(function () {
+ graph.timer = undefined;
+ graph.start();
+ graph._redraw();
+ }, this.refreshRate);
+ }
}
+ else {
+ this._redraw();
+ }
+};
- for (i in unitMillisecondFactors) {
- if (unitMillisecondFactors.hasOwnProperty(i)) {
- makeDurationAsGetter(i, unitMillisecondFactors[i]);
- makeDurationGetter(i.toLowerCase());
- }
+/**
+ * Stop animating nodes and edges.
+ */
+Graph.prototype.stop = function () {
+ if (this.timer) {
+ window.clearInterval(this.timer);
+ this.timer = undefined;
}
+};
- makeDurationAsGetter('Weeks', 6048e5);
+/**
+ * vis.js module exports
+ */
+var vis = {
+ util: util,
+ events: events,
+ Controller: Controller,
+ DataSet: DataSet,
+ DataView: DataView,
+ Range: Range,
+ Stack: Stack,
+ TimeStep: TimeStep,
+ EventBus: EventBus,
- /************************************
- Default Lang
- ************************************/
+ components: {
+ items: {
+ Item: Item,
+ ItemBox: ItemBox,
+ ItemPoint: ItemPoint,
+ ItemRange: ItemRange
+ },
+ Component: Component,
+ Panel: Panel,
+ RootPanel: RootPanel,
+ ItemSet: ItemSet,
+ TimeAxis: TimeAxis
+ },
- // 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;
- }
- });
+ graph: {
+ Node: Node,
+ Edge: Edge,
+ Popup: Popup,
+ Groups: Groups,
+ Images: Images
+ },
+
+ Timeline: Timeline,
+ Graph: Graph
+};
+/**
+ * CommonJS module exports
+ */
+if (typeof exports !== 'undefined') {
+ exports = vis;
+}
+if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
+ module.exports = vis;
+}
- /************************************
- Exposing Moment
- ************************************/
+/**
+ * AMD module exports
+ */
+if (typeof(define) === 'function') {
+ define(function () {
+ return vis;
+ });
+}
+/**
+ * Window exports
+ */
+if (typeof window !== 'undefined') {
+ // attach the module to the window, load as a regular javascript file
+ window['vis'] = vis;
+}
- // 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);
+// inject css
+util.loadCss("/* vis.js stylesheet */\n.vis.timeline {\n}\n\n\n.vis.timeline.rootpanel {\n position: relative;\n overflow: hidden;\n\n border: 1px solid #bfbfbf;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n\n.vis.timeline .panel {\n position: absolute;\n overflow: hidden;\n}\n\n\n.vis.timeline .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n.vis.timeline .labels {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n\n padding: 0;\n margin: 0;\n\n border-right: 1px solid #bfbfbf;\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n}\n\n.vis.timeline .labels .label {\n position: absolute;\n left: 0;\n top: 0;\n width: 100%;\n border-bottom: 1px solid #bfbfbf;\n color: #4d4d4d;\n}\n\n.vis.timeline .labels .label .inner {\n display: inline-block;\n padding: 5px;\n}\n\n\n.vis.timeline .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.vis.timeline .background {\n}\n\n.vis.timeline .foreground {\n}\n\n.vis.timeline .itemset-axis {\n position: absolute;\n}\n\n.vis.timeline .groupset .itemset-axis {\n border-top: 1px solid #bfbfbf;\n}\n\n/* TODO: with orientation=='bottom', this will more or less overlap with timeline axis\n.vis.timeline .groupset .itemset-axis:last-child {\n border-top: none;\n}\n*/\n\n\n.vis.timeline .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.vis.timeline .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.vis.timeline .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.vis.timeline .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.vis.timeline .item.box {\n text-align: center;\n border-style: solid;\n border-width: 1px;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.point {\n background: none;\n}\n\n.vis.timeline .dot {\n border: 5px solid #97B0F8;\n position: absolute;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range {\n overflow: hidden;\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .content {\n position: relative;\n display: inline-block;\n}\n\n.vis.timeline .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.vis.timeline .item .content {\n margin: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.vis.timeline .axis {\n position: relative;\n}\n\n.vis.timeline .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.vis.timeline .axis .text.measure {\n position: absolute;\n padding-left: 0;\n padding-right: 0;\n margin-left: 0;\n margin-right: 0;\n visibility: hidden;\n}\n\n.vis.timeline .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.vis.timeline .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.vis.timeline .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.vis.timeline .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n");
-})()
-},{}]},{},[1])(1)
+},{"hammerjs":1,"moment":2}]},{},[3])
+(3)
});
;
\ No newline at end of file
diff --git a/vis.min.js b/vis.min.js
index 523f7179..3f3d76a8 100644
--- a/vis.min.js
+++ b/vis.min.js
@@ -4,8 +4,8 @@
*
* A dynamic, browser-based visualization library.
*
- * @version 0.1.0
- * @date 2013-06-20
+ * @version 0.2.0
+ * @date 2013-09-20
*
* @license
* Copyright (C) 2011-2013 Almende B.V, http://almende.com
@@ -22,8 +22,8 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
-(function(t){if("function"==typeof bootstrap)bootstrap("vis",t);else if("object"==typeof exports)module.exports=t();else if("function"==typeof define&&define.amd)define(t);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeVis=t}else"undefined"!=typeof window?window.vis=t():global.vis=t()})(function(){var t;return function(t,e,i){function s(i,o){if(!e[i]){if(!t[i]){var r="function"==typeof require&&require;if(!o&&r)return r(i,!0);if(n)return n(i,!0);throw Error("Cannot find module '"+i+"'")}var a=e[i]={exports:{}};t[i][0].call(a.exports,function(e){var n=t[i][1][e];return s(n?n:e)},a,a.exports)}return e[i].exports}for(var n="function"==typeof require&&require,o=0;i.length>o;o++)s(i[o]);return s}({1:[function(e,i,s){(function(){function n(){this.subscriptions=[]}function o(t){if(this.id=D.randomUUID(),this.options=t||{},this.data={},this.fieldId=this.options.fieldId||"id",this.convert={},this.options.convert)for(var e in this.options.convert)if(this.options.convert.hasOwnProperty(e)){var i=this.options.convert[e];this.convert[e]="Date"==i||"ISODate"==i||"ASPDate"==i?"Date":i}this.subscribers={},this.internalIds={}}function r(t,e){this.id=D.randomUUID(),this.data=null,this.ids={},this.options=e||{},this.fieldId="id",this.subscribers={};var i=this;this.listener=function(){i._onEvent.apply(i,arguments)},this.setData(t)}function a(t,e){this.parent=t,this.options=e||{},this.defaultOptions={order:function(t,e){if(t instanceof y){if(e instanceof y){var i=t.data.end-t.data.start,s=e.data.end-e.data.start;return i-s||t.data.start-e.data.start}return-1}return e instanceof y?1:t.data.start-e.data.start},margin:{item:10}},this.ordered=[]}function h(t){this.id=D.randomUUID(),this.start=0,this.end=0,this.options={min:null,max:null,zoomMin:null,zoomMax:null},this.listeners=[],this.setOptions(t)}function d(){this.id=D.randomUUID(),this.components={},this.repaintTimer=void 0,this.reflowTimer=void 0}function l(){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 p(t,e,i){this.id=D.randomUUID(),this.parent=t,this.depends=e,this.options=i||{}}function c(t,e){this.id=D.randomUUID(),this.container=t,this.options=e||{},this.defaultOptions={autoResize:!0},this.listeners={}}function u(t,e,i){this.id=D.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},lineTop:0},this.options=i||{},this.defaultOptions={orientation:"bottom",showMinorLabels:!0,showMajorLabels:!0},this.conversion=null,this.range=null}function f(t,e,i){this.id=D.randomUUID(),this.parent=t,this.depends=e,this.options=i||{},this.defaultOptions={type:"box",align:"center",orientation:"bottom",margin:{axis:20,item:10},padding:5},this.dom={};var s=this;this.itemsData=null,this.range=null,this.listeners={add:function(t,e,i){i!=s.id&&s._onAdd(e.items)},update:function(t,e,i){i!=s.id&&s._onUpdate(e.items)},remove:function(t,e,i){i!=s.id&&s._onRemove(e.items)}},this.items={},this.queue={},this.stack=new a(this,Object.create(this.options)),this.conversion=null}function m(t,e,i,s){this.parent=t,this.data=e,this.dom=null,this.options=i||{},this.defaultOptions=s||{},this.selected=!1,this.visible=!1,this.top=0,this.left=0,this.width=0,this.height=0}function g(t,e,i,s){this.props={dot:{left:0,top:0,width:0,height:0},line:{top:0,left:0,width:0,height:0}},m.call(this,t,e,i,s)}function v(t,e,i,s){this.props={dot:{top:0,width:0,height:0},content:{height:0,marginLeft:0}},m.call(this,t,e,i,s)}function y(t,e,i,s){this.props={content:{left:0,width:0}},m.call(this,t,e,i,s)}function b(t,e,i){this.id=D.randomUUID(),this.parent=t,this.groupId=e,this.itemsData=null,this.itemset=null,this.options=i||{},this.options.top=0,this.props={label:{width:0,height:0}},this.top=0,this.left=0,this.width=0,this.height=0}function w(t,e,i){this.id=D.randomUUID(),this.parent=t,this.depends=e,this.options=i||{},this.range=null,this.itemsData=null,this.groupsData=null,this.groups={},this.dom={},this.props={labels:{width:0}},this.queue={};var s=this;this.listeners={add:function(t,e){s._onAdd(e.items)},update:function(t,e){s._onUpdate(e.items)},remove:function(t,e){s._onRemove(e.items)}}}function _(t,e,i){var s=this;if(this.options=D.extend({orientation:"bottom",min:null,max:null,zoomMin:10,zoomMax:31536e10,showMinorLabels:!0,showMajorLabels:!0,autoResize:!1},i),this.controller=new d,!t)throw Error("No container element provided");var n=Object.create(this.options);n.height=function(){return s.options.height?s.options.height:s.timeaxis.height+s.content.height},this.rootPanel=new c(t,n),this.controller.add(this.rootPanel);var o=Object.create(this.options);o.left=function(){return s.labelPanel.width},o.width=function(){return s.rootPanel.width-s.labelPanel.width},o.top=null,o.height=null,this.itemPanel=new p(this.rootPanel,[],o),this.controller.add(this.itemPanel);var r=Object.create(this.options);r.top=null,r.left=null,r.height=null,r.width=function(){return s.content&&"function"==typeof s.content.getLabelsWidth?s.content.getLabelsWidth():0},this.labelPanel=new p(this.rootPanel,[],r),this.controller.add(this.labelPanel);var a=E().hours(0).minutes(0).seconds(0).milliseconds(0);this.range=new h({start:a.clone().add("days",-3).valueOf(),end:a.clone().add("days",4).valueOf()}),this.range.subscribe(this.rootPanel,"move","horizontal"),this.range.subscribe(this.rootPanel,"zoom","horizontal"),this.range.on("rangechange",function(){var t=!0;s.controller.requestReflow(t)}),this.range.on("rangechanged",function(){var t=!0;s.controller.requestReflow(t)});var l=Object.create(n);l.range=this.range,l.left=null,l.top=null,l.width="100%",l.height=null,this.timeaxis=new u(this.itemPanel,[],l),this.timeaxis.setRange(this.range),this.controller.add(this.timeaxis),this.setGroups(null),this.itemsData=null,this.groupsData=null,e&&this.setItems(e)}function x(t,e,i,s){this.selected=!1,this.edges=[],this.group=s.nodes.group,this.fontSize=s.nodes.fontSize,this.fontFace=s.nodes.fontFace,this.fontColor=s.nodes.fontColor,this.color=s.nodes.color,this.id=void 0,this.shape=s.nodes.shape,this.image=s.nodes.image,this.x=0,this.y=0,this.xFixed=!1,this.yFixed=!1,this.radius=s.nodes.radius,this.radiusFixed=!1,this.radiusMin=s.nodes.radiusMin,this.radiusMax=s.nodes.radiusMax,this.imagelist=e,this.grouplist=i,this.setProperties(t,s),this.mass=50,this.fx=0,this.fy=0,this.vx=0,this.vy=0,this.minForce=s.minForce,this.damping=.9}function T(t,e,i){if(!e)throw"No graph provided";this.graph=e,this.widthMin=i.edges.widthMin,this.widthMax=i.edges.widthMax,this.id=void 0,this.fromId=void 0,this.toId=void 0,this.style=i.edges.style,this.title=void 0,this.width=i.edges.width,this.value=void 0,this.length=i.edges.length,this.from=null,this.to=null,this.connected=!1,this.dash=D.extend({},i.edges.dash),this.stiffness=void 0,this.color=i.edges.color,this.widthFixed=!1,this.lengthFixed=!1,this.setProperties(t,i)}function S(t,e,i,s){this.container=t?t:document.body,this.x=0,this.y=0,this.padding=5,void 0!==e&&void 0!==i&&this.setPosition(e,i),void 0!==s&&this.setText(s),this.frame=document.createElement("div");var n=this.frame.style;n.position="absolute",n.visibility="hidden",n.border="1px solid #666",n.color="black",n.padding=this.padding+"px",n.backgroundColor="#FFFFC6",n.borderRadius="3px",n.MozBorderRadius="3px",n.WebkitBorderRadius="3px",n.boxShadow="3px 3px 10px rgba(128, 128, 128, 0.5)",n.whiteSpace="nowrap",this.container.appendChild(this.frame)}function M(t,e,i){this.containerElement=t,this.width="100%",this.height="100%",this.refreshRate=50,this.stabilize=!0,this.selectable=!0,this.constants={nodes:{radiusMin:5,radiusMax:20,radius:5,distance:100,shape:"ellipse",image:void 0,widthMin:16,widthMax:64,fontColor:"black",fontSize:14,fontFace:"arial",color:{border:"#2B7CE9",background:"#97C2FC",highlight:{border:"#2B7CE9",background:"#D2E5FF"}},borderColor:"#2B7CE9",backgroundColor:"#97C2FC",highlightColor:"#D2E5FF",group:void 0},edges:{widthMin:1,widthMax:15,width:1,style:"line",color:"#343434",fontColor:"#343434",fontSize:14,fontFace:"arial",length:100,dash:{length:10,gap:5,altLength:void 0}},minForce:.05,minVelocity:.02,maxIterations:1e3};var s=this;this.nodes={},this.edges={},this.nodesData=null,this.edgesData=null;var n=this;this.nodesListeners={add:function(t,e){n._addNodes(e.items),n.start()},update:function(t,e){n._updateNodes(e.items),n.start()},remove:function(t,e){n._removeNodes(e.items),n.start()}},this.edgesListeners={add:function(t,e){n._addEdges(e.items),n.start()},update:function(t,e){n._updateEdges(e.items),n.start()},remove:function(t,e){n._removeEdges(e.items),n.start()}},this.groups=new Groups,this.images=new Images,this.images.setOnloadCallback(function(){s._redraw()}),this.moving=!1,this.selection=[],this.timer=void 0,this._create(),this.setOptions(i),this.setData(e)}var E=e("moment"),D={};D.isNumber=function(t){return t instanceof Number||"number"==typeof t},D.isString=function(t){return t instanceof String||"string"==typeof t},D.isDate=function(t){if(t instanceof Date)return!0;if(D.isString(t)){var e=C.exec(t);if(e)return!0;if(!isNaN(Date.parse(t)))return!0}return!1},D.isDataTable=function(t){return"undefined"!=typeof google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable},D.randomUUID=function(){var t=function(){return Math.floor(65536*Math.random()).toString(16)};return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()},D.extend=function(t){for(var e=1,i=arguments.length;i>e;e++){var s=arguments[e];for(var n in s)s.hasOwnProperty(n)&&void 0!==s[n]&&(t[n]=s[n])}return t},D.convert=function(t,e){var i;if(void 0===t)return void 0;if(null===t)return null;if(!e)return t;if("string"!=typeof e&&!(e instanceof String))throw Error("Type must be a string");switch(e){case"boolean":case"Boolean":return Boolean(t);case"number":case"Number":return Number(t.valueOf());case"string":case"String":return t+"";case"Date":if(D.isNumber(t))return new Date(t);if(t instanceof Date)return new Date(t.valueOf());if(E.isMoment(t))return new Date(t.valueOf());if(D.isString(t))return i=C.exec(t),i?new Date(Number(i[1])):E(t).toDate();throw Error("Cannot convert object of type "+D.getType(t)+" to type Date");case"Moment":if(D.isNumber(t))return E(t);if(t instanceof Date)return E(t.valueOf());if(E.isMoment(t))return E.clone();if(D.isString(t))return i=C.exec(t),i?E(Number(i[1])):E(t);throw Error("Cannot convert object of type "+D.getType(t)+" to type Date");case"ISODate":if(D.isNumber(t))return new Date(t);if(t instanceof Date)return t.toISOString();if(E.isMoment(t))return t.toDate().toISOString();if(D.isString(t))return i=C.exec(t),i?new Date(Number(i[1])).toISOString():new Date(t).toISOString();throw Error("Cannot convert object of type "+D.getType(t)+" to type ISODate");case"ASPDate":if(D.isNumber(t))return"/Date("+t+")/";if(t instanceof Date)return"/Date("+t.valueOf()+")/";if(D.isString(t)){i=C.exec(t);var s;return s=i?new Date(Number(i[1])).valueOf():new Date(t).valueOf(),"/Date("+s+")/"}throw Error("Cannot convert object of type "+D.getType(t)+" to type ASPDate");default:throw Error("Cannot convert object of type "+D.getType(t)+' to type "'+e+'"')}};var C=/^\/?Date\((\-?\d+)/i;if(D.getType=function(t){var e=typeof t;return"object"==e?null==t?"null":t instanceof Boolean?"Boolean":t instanceof Number?"Number":t instanceof String?"String":t instanceof Array?"Array":t instanceof Date?"Date":"Object":"number"==e?"Number":"boolean"==e?"Boolean":"string"==e?"String":e},D.getAbsoluteLeft=function(t){for(var e=document.documentElement,i=document.body,s=t.offsetLeft,n=t.offsetParent;null!=n&&n!=i&&n!=e;)s+=n.offsetLeft,s-=n.scrollLeft,n=n.offsetParent;return s},D.getAbsoluteTop=function(t){for(var e=document.documentElement,i=document.body,s=t.offsetTop,n=t.offsetParent;null!=n&&n!=i&&n!=e;)s+=n.offsetTop,s-=n.scrollTop,n=n.offsetParent;return s},D.getPageY=function(t){if("pageY"in t)return t.pageY;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientY:t.clientY;var i=document.documentElement,s=document.body;return e+(i&&i.scrollTop||s&&s.scrollTop||0)-(i&&i.clientTop||s&&s.clientTop||0)},D.getPageX=function(t){if("pageY"in t)return t.pageX;var e;e="targetTouches"in t&&t.targetTouches.length?t.targetTouches[0].clientX:t.clientX;var i=document.documentElement,s=document.body;return e+(i&&i.scrollLeft||s&&s.scrollLeft||0)-(i&&i.clientLeft||s&&s.clientLeft||0)},D.addClassName=function(t,e){var i=t.className.split(" ");-1==i.indexOf(e)&&(i.push(e),t.className=i.join(" "))},D.removeClassName=function(t,e){var i=t.className.split(" "),s=i.indexOf(e);-1!=s&&(i.splice(s,1),t.className=i.join(" "))},D.forEach=function(t,e){var i,s;if(t instanceof Array)for(i=0,s=t.length;s>i;i++)e(t[i],i,t);else for(i in t)t.hasOwnProperty(i)&&e(t[i],i,t)},D.updateProperty=function(t,e,i){return t[e]!==i?(t[e]=i,!0):!1},D.addEventListener=function(t,e,i,s){t.addEventListener?(void 0===s&&(s=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.addEventListener(e,i,s)):t.attachEvent("on"+e,i)},D.removeEventListener=function(t,e,i,s){t.removeEventListener?(void 0===s&&(s=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.removeEventListener(e,i,s)):t.detachEvent("on"+e,i)},D.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},D.stopPropagation=function(t){t||(t=window.event),t.stopPropagation?t.stopPropagation():t.cancelBubble=!0},D.preventDefault=function(t){t||(t=window.event),t.preventDefault?t.preventDefault():t.returnValue=!1},D.option={},D.option.asBoolean=function(t,e){return"function"==typeof t&&(t=t()),null!=t?0!=t:e||null},D.option.asNumber=function(t,e){return"function"==typeof t&&(t=t()),null!=t?Number(t)||e||null:e||null},D.option.asString=function(t,e){return"function"==typeof t&&(t=t()),null!=t?t+"":e||null},D.option.asSize=function(t,e){return"function"==typeof t&&(t=t()),D.isString(t)?t:D.isNumber(t)?t+"px":e||null},D.option.asElement=function(t,e){return"function"==typeof t&&(t=t()),t||e||null},D.loadCss=function(t){if("undefined"!=typeof document){var e=document.createElement("style");e.type="text/css",e.styleSheet?e.styleSheet.cssText=t:e.appendChild(document.createTextNode(t)),document.getElementsByTagName("head")[0].appendChild(e)}},!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(L){}}Array.prototype.forEach||(Array.prototype.forEach=function(t,e){for(var i=0,s=this.length;s>i;++i)t.call(e||this,this[i],i,this)}),Array.prototype.map||(Array.prototype.map=function(t,e){var i,s,n;if(null==this)throw new TypeError(" this is null or not defined");var o=Object(this),r=o.length>>>0;if("function"!=typeof t)throw new TypeError(t+" is not a function");for(e&&(i=e),s=Array(r),n=0;r>n;){var a,h;n in o&&(a=o[n],h=t.call(i,a,n,o),s[n]=h),n++}return s}),Array.prototype.filter||(Array.prototype.filter=function(t){"use strict";if(null==this)throw new TypeError;var e=Object(this),i=e.length>>>0;if("function"!=typeof t)throw new TypeError;for(var s=[],n=arguments[1],o=0;i>o;o++)if(o in e){var r=e[o];t.call(n,r,o,e)&&s.push(r)}return s}),Object.keys||(Object.keys=function(){var t=Object.prototype.hasOwnProperty,e=!{toString:null}.propertyIsEnumerable("toString"),i=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],s=i.length;return function(n){if("object"!=typeof n&&"function"!=typeof n||null===n)throw new TypeError("Object.keys called on non-object");var o=[];for(var r in n)t.call(n,r)&&o.push(r);if(e)for(var a=0;s>a;a++)t.call(n,i[a])&&o.push(i[a]);return o}}()),Array.isArray||(Array.isArray=function(t){return"[object Array]"===Object.prototype.toString.call(t)}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,s=function(){},n=function(){return i.apply(this instanceof s&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return s.prototype=this.prototype,n.prototype=new s,n}),Object.create||(Object.create=function(t){function e(){}if(arguments.length>1)throw Error("Object.create implementation only accepts the first parameter.");return e.prototype=t,new e});var O={listeners:[],indexOf:function(t){for(var e=this.listeners,i=0,s=this.listeners.length;s>i;i++){var n=e[i];if(n&&n.object==t)return i}return-1},addListener:function(t,e,i){var s=this.indexOf(t),n=this.listeners[s];n||(n={object:t,events:{}},this.listeners.push(n));var o=n.events[e];o||(o=[],n.events[e]=o),-1==o.indexOf(i)&&o.push(i)},removeListener:function(t,e,i){var s=this.indexOf(t),n=this.listeners[s];if(n){var o=n.events[e];o&&(s=o.indexOf(i),-1!=s&&o.splice(s,1),0==o.length&&delete n.events[e]);var r=0,a=n.events;for(var h in a)a.hasOwnProperty(h)&&r++;0==r&&delete this.listeners[s]}},removeAllListeners:function(){this.listeners=[]},trigger:function(t,e,i){var s=this.indexOf(t),n=this.listeners[s];if(n){var o=n.events[e];if(o)for(var r=0,a=o.length;a>r;r++)o[r](i)}}};n.prototype.on=function(t,e,i){var s=t instanceof RegExp?t:RegExp(t.replace("*","\\w+")),n={id:D.randomUUID(),event:t,regexp:s,callback:"function"==typeof e?e:null,target:i};return this.subscriptions.push(n),n.id},n.prototype.off=function(t){for(var e=0;this.subscriptions.length>e;){var i=this.subscriptions[e],s=!0;if(t instanceof Object)for(var n in t)t.hasOwnProperty(n)&&t[n]!==i[n]&&(s=!1);else s=i.id==t;s?this.subscriptions.splice(e,1):e++}},n.prototype.emit=function(t,e,i){for(var s=0;this.subscriptions.length>s;s++){var n=this.subscriptions[s];n.regexp.test(t)&&n.callback&&n.callback(t,e,i)}},o.prototype.subscribe=function(t,e){var i=this.subscribers[t];i||(i=[],this.subscribers[t]=i),i.push({callback:e})},o.prototype.unsubscribe=function(t,e){var i=this.subscribers[t];i&&(this.subscribers[t]=i.filter(function(t){return t.callback!=e}))},o.prototype._trigger=function(t,e,i){if("*"==t)throw Error("Cannot trigger event *");var s=[];t in this.subscribers&&(s=s.concat(this.subscribers[t])),"*"in this.subscribers&&(s=s.concat(this.subscribers["*"]));for(var n=0;s.length>n;n++){var o=s[n];o.callback&&o.callback(t,e,i||null)}},o.prototype.add=function(t,e){var i,s=[],n=this;if(t instanceof Array)for(var o=0,r=t.length;r>o;o++)i=n._addItem(t[o]),s.push(i);else if(D.isDataTable(t))for(var a=this._getColumnNames(t),h=0,d=t.getNumberOfRows();d>h;h++){for(var l={},p=0,c=a.length;c>p;p++){var u=a[p];l[u]=t.getValue(h,p)}i=n._addItem(l),s.push(i)}else{if(!(t instanceof Object))throw Error("Unknown dataType");i=n._addItem(t),s.push(i)}return s.length&&this._trigger("add",{items:s},e),s},o.prototype.update=function(t,e){var i=[],s=[],n=this,o=n.fieldId,r=function(t){var e=t[o];n.data[e]?(e=n._updateItem(t),s.push(e)):(e=n._addItem(t),i.push(e))};if(t instanceof Array)for(var a=0,h=t.length;h>a;a++)r(t[a]);else if(D.isDataTable(t))for(var d=this._getColumnNames(t),l=0,p=t.getNumberOfRows();p>l;l++){for(var c={},u=0,f=d.length;f>u;u++){var m=d[u];c[m]=t.getValue(l,u)}r(c)}else{if(!(t instanceof Object))throw Error("Unknown dataType");r(t)}return i.length&&this._trigger("add",{items:i},e),s.length&&this._trigger("update",{items:s},e),i.concat(s)},o.prototype.get=function(){var t,e,i,s,n=this,o=D.getType(arguments[0]);"String"==o||"Number"==o?(t=arguments[0],i=arguments[1],s=arguments[2]):"Array"==o?(e=arguments[0],i=arguments[1],s=arguments[2]):(i=arguments[0],s=arguments[1]);var r;if(i&&i.type){if(r="DataTable"==i.type?"DataTable":"Array",s&&r!=D.getType(s))throw Error('Type of parameter "data" ('+D.getType(s)+") "+"does not correspond with specified options.type ("+i.type+")");if("DataTable"==r&&!D.isDataTable(s))throw Error('Parameter "data" must be a DataTable when options.type is "DataTable"')}else r=s?"DataTable"==D.getType(s)?"DataTable":"Array":"Array";var a,h,d,l,p=i&&i.convert||this.options.convert,c=i&&i.filter,u=[];if(void 0!=t)a=n._getItem(t,p),c&&!c(a)&&(a=null);else if(void 0!=e)for(d=0,l=e.length;l>d;d++)a=n._getItem(e[d],p),(!c||c(a))&&u.push(a);else for(h in this.data)this.data.hasOwnProperty(h)&&(a=n._getItem(h,p),(!c||c(a))&&u.push(a));if(i&&i.order&&void 0==t&&this._sort(u,i.order),i&&i.fields){var f=i.fields;if(void 0!=t)a=this._filterFields(a,f);else for(d=0,l=u.length;l>d;d++)u[d]=this._filterFields(u[d],f)}if("DataTable"==r){var m=this._getColumnNames(s);if(void 0!=t)n._appendRow(s,m,a);else for(d=0,l=u.length;l>d;d++)n._appendRow(s,m,u[d]);return s}if(void 0!=t)return a;if(s){for(d=0,l=u.length;l>d;d++)s.push(u[d]);return s}return u},o.prototype.getIds=function(t){var e,i,s,n,o,r=this.data,a=t&&t.filter,h=t&&t.order,d=t&&t.convert||this.options.convert,l=[];if(a)if(h){o=[];for(s in r)r.hasOwnProperty(s)&&(n=this._getItem(s,d),a(n)&&o.push(n));for(this._sort(o,h),e=0,i=o.length;i>e;e++)l[e]=o[e][this.fieldId]}else for(s in r)r.hasOwnProperty(s)&&(n=this._getItem(s,d),a(n)&&l.push(n[this.fieldId]));else if(h){o=[];for(s in r)r.hasOwnProperty(s)&&o.push(r[s]);for(this._sort(o,h),e=0,i=o.length;i>e;e++)l[e]=o[e][this.fieldId]}else for(s in r)r.hasOwnProperty(s)&&(n=r[s],l.push(n[this.fieldId]));return l},o.prototype.forEach=function(t,e){var i,s,n=e&&e.filter,o=e&&e.convert||this.options.convert,r=this.data;if(e&&e.order)for(var a=this.get(e),h=0,d=a.length;d>h;h++)i=a[h],s=i[this.fieldId],t(i,s);else for(s in r)r.hasOwnProperty(s)&&(i=this._getItem(s,o),(!n||n(i))&&t(i,s))},o.prototype.map=function(t,e){var i,s=e&&e.filter,n=e&&e.convert||this.options.convert,o=[],r=this.data;for(var a in r)r.hasOwnProperty(a)&&(i=this._getItem(a,n),(!s||s(i))&&o.push(t(i,a)));return e&&e.order&&this._sort(o,e.order),o},o.prototype._filterFields=function(t,e){var i={};for(var s in t)t.hasOwnProperty(s)&&-1!=e.indexOf(s)&&(i[s]=t[s]);return i},o.prototype._sort=function(t,e){if(D.isString(e)){var i=e;t.sort(function(t,e){var s=t[i],n=e[i];return s>n?1:n>s?-1:0})}else{if("function"!=typeof e)throw new TypeError("Order must be a function or a string");t.sort(e)}},o.prototype.remove=function(t,e){var i,s,n,o=[];if(t instanceof Array)for(i=0,s=t.length;s>i;i++)n=this._remove(t[i]),null!=n&&o.push(n);else n=this._remove(t),null!=n&&o.push(n);return o.length&&this._trigger("remove",{items:o},e),o},o.prototype._remove=function(t){if(D.isNumber(t)||D.isString(t)){if(this.data[t])return delete this.data[t],delete this.internalIds[t],t}else if(t instanceof Object){var e=t[this.fieldId];if(e&&this.data[e])return delete this.data[e],delete this.internalIds[e],e}return null},o.prototype.clear=function(t){var e=Object.keys(this.data);return this.data={},this.internalIds={},this._trigger("remove",{items:e},t),e},o.prototype.max=function(t){var e=this.data,i=null,s=null;for(var n in e)if(e.hasOwnProperty(n)){var o=e[n],r=o[t];null!=r&&(!i||r>s)&&(i=o,s=r)}return i},o.prototype.min=function(t){var e=this.data,i=null,s=null;for(var n in e)if(e.hasOwnProperty(n)){var o=e[n],r=o[t];null!=r&&(!i||s>r)&&(i=o,s=r)}return i},o.prototype.distinct=function(t){var e=this.data,i=[],s=this.options.convert[t],n=0;for(var o in e)if(e.hasOwnProperty(o)){for(var r=e[o],a=D.convert(r[t],s),h=!1,d=0;n>d;d++)if(i[d]==a){h=!0;break}h||(i[n]=a,n++)}return i},o.prototype._addItem=function(t){var e=t[this.fieldId];if(void 0!=e){if(this.data[e])throw Error("Cannot add item: item with id "+e+" already exists")}else e=D.randomUUID(),t[this.fieldId]=e,this.internalIds[e]=t;var i={};for(var s in t)if(t.hasOwnProperty(s)){var n=this.convert[s];i[s]=D.convert(t[s],n)}return this.data[e]=i,e},o.prototype._getItem=function(t,e){var i,s,n=this.data[t];if(!n)return null;var o={},r=this.fieldId,a=this.internalIds;if(e)for(i in n)n.hasOwnProperty(i)&&(s=n[i],i==r&&s in a||(o[i]=D.convert(s,e[i])));else for(i in n)n.hasOwnProperty(i)&&(s=n[i],i==r&&s in a||(o[i]=s));return o},o.prototype._updateItem=function(t){var e=t[this.fieldId];if(void 0==e)throw Error("Cannot update item: item has no id (item: "+JSON.stringify(t)+")");var i=this.data[e];if(!i)throw Error("Cannot update item: no item with id "+e+" found");for(var s in t)if(t.hasOwnProperty(s)){var n=this.convert[s];i[s]=D.convert(t[s],n)}return e},o.prototype._getColumnNames=function(t){for(var e=[],i=0,s=t.getNumberOfColumns();s>i;i++)e[i]=t.getColumnId(i)||t.getColumnLabel(i);return e},o.prototype._appendRow=function(t,e,i){for(var s=t.addRow(),n=0,o=e.length;o>n;n++){var r=e[n];t.setValue(s,n,i[r])}},r.prototype.setData=function(t){var e,i,s;if(this.data){this.data.unsubscribe&&this.data.unsubscribe("*",this.listener),e=[];for(var n in this.ids)this.ids.hasOwnProperty(n)&&e.push(n);this.ids={},this._trigger("remove",{items:e})}if(this.data=t,this.data){for(this.fieldId=this.options.fieldId||this.data&&this.data.options&&this.data.options.fieldId||"id",e=this.data.getIds({filter:this.options&&this.options.filter}),i=0,s=e.length;s>i;i++)n=e[i],this.ids[n]=!0;this._trigger("add",{items:e}),this.data.subscribe&&this.data.subscribe("*",this.listener)}},r.prototype.get=function(){var t,e,i,s=this,n=D.getType(arguments[0]);"String"==n||"Number"==n||"Array"==n?(t=arguments[0],e=arguments[1],i=arguments[2]):(e=arguments[0],i=arguments[1]);var o=D.extend({},this.options,e);this.options.filter&&e&&e.filter&&(o.filter=function(t){return s.options.filter(t)&&e.filter(t)});var r=[];return void 0!=t&&r.push(t),r.push(o),r.push(i),this.data&&this.data.get.apply(this.data,r)},r.prototype.getIds=function(t){var e;if(this.data){var i,s=this.options.filter;i=t&&t.filter?s?function(e){return s(e)&&t.filter(e)}:t.filter:s,e=this.data.getIds({filter:i,order:t&&t.order})}else e=[];return e},r.prototype._onEvent=function(t,e,i){var s,n,o,r,a=e&&e.items,h=this.data,d=[],l=[],p=[];if(a&&h){switch(t){case"add":for(s=0,n=a.length;n>s;s++)o=a[s],r=this.get(o),r&&(this.ids[o]=!0,d.push(o));break;case"update":for(s=0,n=a.length;n>s;s++)o=a[s],r=this.get(o),r?this.ids[o]?l.push(o):(this.ids[o]=!0,d.push(o)):this.ids[o]&&(delete this.ids[o],p.push(o));break;case"remove":for(s=0,n=a.length;n>s;s++)o=a[s],this.ids[o]&&(delete this.ids[o],p.push(o))}d.length&&this._trigger("add",{items:d},i),l.length&&this._trigger("update",{items:l},i),p.length&&this._trigger("remove",{items:p},i)}},r.prototype.subscribe=o.prototype.subscribe,r.prototype.unsubscribe=o.prototype.unsubscribe,r.prototype._trigger=o.prototype._trigger,TimeStep=function(t,e,i){this.current=new Date,this._start=new Date,this._end=new Date,this.autoScale=!0,this.scale=TimeStep.SCALE.DAY,this.step=1,this.setRange(t,e,i)},TimeStep.SCALE={MILLISECOND:1,SECOND:2,MINUTE:3,HOUR:4,DAY:5,WEEKDAY:6,MONTH:7,YEAR:8},TimeStep.prototype.setRange=function(t,e,i){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,s=864e5,n=36e5,o=6e4,r=1e3,a=1;1e3*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1e3),500*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=500),100*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=100),50*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=50),10*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=10),5*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=5),e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1),3*i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=3),i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=1),5*s>t&&(this.scale=TimeStep.SCALE.DAY,this.step=5),2*s>t&&(this.scale=TimeStep.SCALE.DAY,this.step=2),s>t&&(this.scale=TimeStep.SCALE.DAY,this.step=1),s/2>t&&(this.scale=TimeStep.SCALE.WEEKDAY,this.step=1),4*n>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=4),n>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=1),15*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=15),10*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=10),5*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=5),o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=1),15*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=15),10*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=10),5*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=5),r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=1),200*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=200),100*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=100),50*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=50),10*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=10),5*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=5),a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=1)
-}},TimeStep.prototype.snap=function(t){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 E(t).format("SSS");case TimeStep.SCALE.SECOND:return E(t).format("s");case TimeStep.SCALE.MINUTE:return E(t).format("HH:mm");case TimeStep.SCALE.HOUR:return E(t).format("HH:mm");case TimeStep.SCALE.WEEKDAY:return E(t).format("ddd D");case TimeStep.SCALE.DAY:return E(t).format("D");case TimeStep.SCALE.MONTH:return E(t).format("MMM");case TimeStep.SCALE.YEAR:return E(t).format("YYYY");default:return""}},TimeStep.prototype.getLabelMajor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return E(t).format("HH:mm:ss");case TimeStep.SCALE.SECOND:return E(t).format("D MMMM HH:mm");case TimeStep.SCALE.MINUTE:case TimeStep.SCALE.HOUR:return E(t).format("ddd D MMMM");case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return E(t).format("MMMM YYYY");case TimeStep.SCALE.MONTH:return E(t).format("YYYY");case TimeStep.SCALE.YEAR:return"";default:return""}},a.prototype.setOptions=function(t){D.extend(this.options,t)},a.prototype.update=function(){this._order(),this._stack()},a.prototype._order=function(){var t=this.parent.items;if(!t)throw Error("Cannot stack items: parent does not contain items");var e=[],i=0;D.forEach(t,function(t){t.visible&&(e[i]=t,i++)});var s=this.options.order||this.defaultOptions.order;if("function"!=typeof s)throw Error("Option order must be a function");e.sort(s),this.ordered=e},a.prototype._stack=function(){var t,e,i,s=this.ordered,n=this.options,o=n.orientation||this.defaultOptions.orientation,r="top"==o;for(i=n.margin&&void 0!==n.margin.item?n.margin.item:this.defaultOptions.margin.item,t=0,e=s.length;e>t;t++){var a=s[t],h=null;do h=this.checkOverlap(s,t,0,t-1,i),null!=h&&(a.top=r?h.top+h.height+i:h.top-a.height-i);while(h)}},a.prototype.checkOverlap=function(t,e,i,s,n){for(var o=this.collision,r=t[e],a=s;a>=i;a--){var h=t[a];if(o(r,h,n)&&a!=e)return h}return null},a.prototype.collision=function(t,e,i){return t.left-ie.left&&t.top-ie.top},h.prototype.setOptions=function(t){D.extend(this.options,t),(null!=t.start||null!=t.end)&&this.setRange(t.start,t.end)},h.prototype.subscribe=function(t,e,i){var s,n=this;if("horizontal"!=i&&"vertical"!=i)throw new TypeError('Unknown direction "'+i+'". '+'Choose "horizontal" or "vertical".');if("move"==e)s={component:t,event:e,direction:i,callback:function(t){n._onMouseDown(t,s)},params:{}},t.on("mousedown",s.callback),n.listeners.push(s);else{if("zoom"!=e)throw new TypeError('Unknown event "'+e+'". '+'Choose "move" or "zoom".');s={component:t,event:e,direction:i,callback:function(t){n._onMouseWheel(t,s)},params:{}},t.on("mousewheel",s.callback),n.listeners.push(s)}},h.prototype.on=function(t,e){O.addListener(this,t,e)},h.prototype._trigger=function(t){O.trigger(this,t,{start:this.start,end:this.end})},h.prototype.setRange=function(t,e){var i=this._applyRange(t,e);i&&(this._trigger("rangechange"),this._trigger("rangechanged"))},h.prototype._applyRange=function(t,e){var i,s=null!=t?D.convert(t,"Number"):this.start,n=null!=e?D.convert(e,"Number"):this.end;if(isNaN(s))throw Error('Invalid start "'+t+'"');if(isNaN(n))throw Error('Invalid end "'+e+'"');if(s>n&&(n=s),null!=this.options.min){var o=this.options.min.valueOf();o>s&&(i=o-s,s+=i,n+=i)}if(null!=this.options.max){var r=this.options.max.valueOf();n>r&&(i=n-r,s-=i,n-=i)}if(null!=this.options.zoomMin){var a=this.options.zoomMin.valueOf();0>a&&(a=0),a>n-s&&(this.end-this.start>a?(i=a-(n-s),s-=i/2,n+=i/2):(s=this.start,n=this.end))}if(null!=this.options.zoomMax){var h=this.options.zoomMax.valueOf();0>h&&(h=0),n-s>h&&(h>this.end-this.start?(i=n-s-h,s+=i/2,n-=i/2):(s=this.start,n=this.end))}var d=this.start!=s||this.end!=n;return this.start=s,this.end=n,d},h.prototype.getRange=function(){return{start:this.start,end:this.end}},h.prototype.conversion=function(t){return this.start,this.end,h.conversion(this.start,this.end,t)},h.conversion=function(t,e,i){return 0!=i&&0!=e-t?{offset:t,factor:i/(e-t)}:{offset:0,factor:1}},h.prototype._onMouseDown=function(t,e){t=t||window.event;var i=e.params,s=t.which?1==t.which:1==t.button;if(s){i.mouseX=D.getPageX(t),i.mouseY=D.getPageY(t),i.previousLeft=0,i.previousOffset=0,i.moved=!1,i.start=this.start,i.end=this.end;var n=e.component.frame;n&&(n.style.cursor="move");var o=this;i.onMouseMove||(i.onMouseMove=function(t){o._onMouseMove(t,e)},D.addEventListener(document,"mousemove",i.onMouseMove)),i.onMouseUp||(i.onMouseUp=function(t){o._onMouseUp(t,e)},D.addEventListener(document,"mouseup",i.onMouseUp)),D.preventDefault(t)}},h.prototype._onMouseMove=function(t,e){t=t||window.event;var i=e.params,s=D.getPageX(t),n=D.getPageY(t);void 0==i.mouseX&&(i.mouseX=s),void 0==i.mouseY&&(i.mouseY=n);var o=s-i.mouseX,r=n-i.mouseY,a="horizontal"==e.direction?o:r;Math.abs(a)>=1&&(i.moved=!0);var h=i.end-i.start,d="horizontal"==e.direction?e.component.width:e.component.height,l=-a/d*h;this._applyRange(i.start+l,i.end+l),this._trigger("rangechange"),D.preventDefault(t)},h.prototype._onMouseUp=function(t,e){t=t||window.event;var i=e.params;e.component.frame&&(e.component.frame.style.cursor="auto"),i.onMouseMove&&(D.removeEventListener(document,"mousemove",i.onMouseMove),i.onMouseMove=null),i.onMouseUp&&(D.removeEventListener(document,"mouseup",i.onMouseUp),i.onMouseUp=null),i.moved&&this._trigger("rangechanged")},h.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 s=this,n=function(){var n=i/5,o=null,r=e.component.frame;if(r){var a,h;if("horizontal"==e.direction){a=e.component.width,h=s.conversion(a);var d=D.getAbsoluteLeft(r),l=D.getPageX(t);o=(l-d)/h.factor+h.offset}else{a=e.component.height,h=s.conversion(a);var p=D.getAbsoluteTop(r),c=D.getPageY(t);o=(p+a-c-p)/h.factor+h.offset}}s.zoom(n,o)};n()}D.preventDefault(t)},h.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,s=this.end-e,n=this.start-i*t,o=this.end-s*t;this.setRange(n,o)},h.prototype.move=function(t){var e=this.end-this.start,i=this.start+e*t,s=this.end+e*t;this.start=i,this.end=s},d.prototype.add=function(t){if(void 0==t.id)throw Error("Component has no field id");if(!(t instanceof l||t instanceof d))throw new TypeError("Component must be an instance of prototype Component or Controller");t.controller=this,this.components[t.id]=t},d.prototype.remove=function(t){var e;for(e in this.components)if(this.components.hasOwnProperty(e)&&(e==t||this.components[e]==t))break;e&&delete this.components[e]},d.prototype.requestReflow=function(t){if(t)this.reflow();else if(!this.reflowTimer){var e=this;this.reflowTimer=setTimeout(function(){e.reflowTimer=void 0,e.reflow()},0)}},d.prototype.requestRepaint=function(t){if(t)this.repaint();else if(!this.repaintTimer){var e=this;this.repaintTimer=setTimeout(function(){e.repaintTimer=void 0,e.repaint()},0)}},d.prototype.repaint=function(){function t(s,n){n in i||(s.depends&&s.depends.forEach(function(e){t(e,e.id)}),s.parent&&t(s.parent,s.parent.id),e=s.repaint()||e,i[n]=!0)}var e=!1;this.repaintTimer&&(clearTimeout(this.repaintTimer),this.repaintTimer=void 0);var i={};D.forEach(this.components,t),e&&this.reflow()},d.prototype.reflow=function(){function t(s,n){n in i||(s.depends&&s.depends.forEach(function(e){t(e,e.id)}),s.parent&&t(s.parent,s.parent.id),e=s.reflow()||e,i[n]=!0)}var e=!1;this.reflowTimer&&(clearTimeout(this.reflowTimer),this.reflowTimer=void 0);var i={};D.forEach(this.components,t),e&&this.repaint()},l.prototype.setOptions=function(t){t&&(D.extend(this.options,t),this.controller&&(this.requestRepaint(),this.requestReflow()))},l.prototype.getOption=function(t){var e;return this.options&&(e=this.options[t]),void 0===e&&this.defaultOptions&&(e=this.defaultOptions[t]),e},l.prototype.getContainer=function(){return null},l.prototype.getFrame=function(){return this.frame},l.prototype.repaint=function(){return!1},l.prototype.reflow=function(){return!1},l.prototype.hide=function(){return this.frame&&this.frame.parentNode?(this.frame.parentNode.removeChild(this.frame),!0):!1},l.prototype.show=function(){return this.frame&&this.frame.parentNode?!1:this.repaint()},l.prototype.requestRepaint=function(){if(!this.controller)throw Error("Cannot request a repaint: no controller configured");this.controller.requestRepaint()},l.prototype.requestReflow=function(){if(!this.controller)throw Error("Cannot request a reflow: no controller configured");this.controller.requestReflow()},p.prototype=new l,p.prototype.setOptions=l.prototype.setOptions,p.prototype.getContainer=function(){return this.frame},p.prototype.repaint=function(){var t=0,e=D.updateProperty,i=D.option.asSize,s=this.options,n=this.frame;if(!n){n=document.createElement("div"),n.className="panel";var o=s.className;o&&("function"==typeof o?D.addClassName(n,o()+""):D.addClassName(n,o+"")),this.frame=n,t+=1}if(!n.parentNode){if(!this.parent)throw Error("Cannot repaint panel: no parent attached");var r=this.parent.getContainer();if(!r)throw Error("Cannot repaint panel: parent has no container element");r.appendChild(n),t+=1}return t+=e(n.style,"top",i(s.top,"0px")),t+=e(n.style,"left",i(s.left,"0px")),t+=e(n.style,"width",i(s.width,"100%")),t+=e(n.style,"height",i(s.height,"100%")),t>0},p.prototype.reflow=function(){var t=0,e=D.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},c.prototype=new p,c.prototype.setOptions=l.prototype.setOptions,c.prototype.repaint=function(){var t=0,e=D.updateProperty,i=D.option.asSize,s=this.options,n=this.frame;if(!n){n=document.createElement("div"),n.className="vis timeline rootpanel";var o=s.className;o&&D.addClassName(n,D.option.asString(o)),this.frame=n,t+=1}if(!n.parentNode){if(!this.container)throw Error("Cannot repaint root panel: no container attached");this.container.appendChild(n),t+=1}return t+=e(n.style,"top",i(s.top,"0px")),t+=e(n.style,"left",i(s.left,"0px")),t+=e(n.style,"width",i(s.width,"100%")),t+=e(n.style,"height",i(s.height,"100%")),this._updateEventEmitters(),this._updateWatch(),t>0},c.prototype.reflow=function(){var t=0,e=D.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},c.prototype._updateWatch=function(){var t=this.getOption("autoResize");t?this._watch():this._unwatch()},c.prototype._watch=function(){var t=this;this._unwatch();var e=function(){var e=t.getOption("autoResize");return e?(t.frame&&(t.frame.clientWidth!=t.width||t.frame.clientHeight!=t.height)&&t.requestReflow(),void 0):(t._unwatch(),void 0)};D.addEventListener(window,"resize",e),this.watchTimer=setInterval(e,1e3)},c.prototype._unwatch=function(){this.watchTimer&&(clearInterval(this.watchTimer),this.watchTimer=void 0)},c.prototype.on=function(t,e){var i=this.listeners[t];i||(i=[],this.listeners[t]=i),i.push(e),this._updateEventEmitters()},c.prototype._updateEventEmitters=function(){if(this.listeners){var t=this;D.forEach(this.listeners,function(e,i){if(t.emitters||(t.emitters={}),!(i in t.emitters)){var s=t.frame;if(s){var n=function(t){e.forEach(function(e){e(t)})};t.emitters[i]=n,D.addEventListener(s,i,n)}}})}},u.prototype=new l,u.prototype.setOptions=l.prototype.setOptions,u.prototype.setRange=function(t){if(!(t instanceof h||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},u.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.factor+e.offset)},u.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.factor},u.prototype.repaint=function(){var t=0,e=D.updateProperty,i=D.option.asSize,s=this.options,n=this.getOption("orientation"),o=this.props,r=this.step,a=this.frame;if(a||(a=document.createElement("div"),this.frame=a,t+=1),a.className="axis "+n,!a.parentNode){if(!this.parent)throw Error("Cannot repaint time axis: no parent attached");var h=this.parent.getContainer();if(!h)throw Error("Cannot repaint time axis: parent has no container element");h.appendChild(a),t+=1}var d=a.parentNode;if(d){var l=a.nextSibling;d.removeChild(a);var p="bottom"==n&&this.props.parentHeight&&this.height?this.props.parentHeight-this.height+"px":"0px";if(t+=e(a.style,"top",i(s.top,p)),t+=e(a.style,"left",i(s.left,"0px")),t+=e(a.style,"width",i(s.width,"100%")),t+=e(a.style,"height",i(s.height,this.height+"px")),this._repaintMeasureChars(),this.step){this._repaintStart(),r.first();for(var c=void 0,u=0;r.hasNext()&&1e3>u;){u++;var f=r.getCurrent(),m=this.toScreen(f),g=r.isMajor();this.getOption("showMinorLabels")&&this._repaintMinorText(m,r.getLabelMinor()),g&&this.getOption("showMajorLabels")?(m>0&&(void 0==c&&(c=m),this._repaintMajorText(m,r.getLabelMajor())),this._repaintMajorLine(m)):this._repaintMinorLine(m),r.next()}if(this.getOption("showMajorLabels")){var v=this.toTime(0),y=r.getLabelMajor(v),b=y.length*(o.majorCharWidth||10)+10;(void 0==c||c>b)&&this._repaintMajorText(0,y)}this._repaintEnd()}this._repaintLine(),l?d.insertBefore(a,l):d.appendChild(a)}return t>0},u.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=[]},u.prototype._repaintEnd=function(){D.forEach(this.dom.redundant,function(t){for(;t.length;){var e=t.pop();e&&e.parentNode&&e.parentNode.removeChild(e)}})},u.prototype._repaintMinorText=function(t,e){var i=this.dom.redundant.minorTexts.shift();if(!i){var s=document.createTextNode("");i=document.createElement("div"),i.appendChild(s),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"},u.prototype._repaintMajorText=function(t,e){var i=this.dom.redundant.majorTexts.shift();if(!i){var s=document.createTextNode(e);i=document.createElement("div"),i.className="text major",i.appendChild(s),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"},u.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"},u.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"},u.prototype._repaintLine=function(){var t=this.dom.line,e=this.frame;this.options,this.getOption("showMinorLabels")||this.getOption("showMajorLabels")?(t?(e.removeChild(t),e.appendChild(t)):(t=document.createElement("div"),t.className="grid horizontal major",e.appendChild(t),this.dom.line=t),t.style.top=this.props.lineTop+"px"):t&&axis.parentElement&&(e.removeChild(axis.line),delete this.dom.line)},u.prototype._repaintMeasureChars=function(){var t,e=this.dom;if(!e.measureCharMinor){t=document.createTextNode("0");var i=document.createElement("DIV");i.className="text minor measure",i.appendChild(t),this.frame.appendChild(i),e.measureCharMinor=i}if(!e.measureCharMajor){t=document.createTextNode("0");var s=document.createElement("DIV");s.className="text major measure",s.appendChild(t),this.frame.appendChild(s),e.measureCharMajor=s}},u.prototype.reflow=function(){var t=0,e=D.updateProperty,i=this.frame,s=this.range;if(!s)throw Error("Cannot repaint time axis: no range configured");if(i){t+=e(this,"top",i.offsetTop),t+=e(this,"left",i.offsetLeft);var n=this.props,o=this.getOption("showMinorLabels"),r=this.getOption("showMajorLabels"),a=this.dom.measureCharMinor,h=this.dom.measureCharMajor;a&&(n.minorCharHeight=a.clientHeight,n.minorCharWidth=a.clientWidth),h&&(n.majorCharHeight=h.clientHeight,n.majorCharWidth=h.clientWidth);var d=i.parentNode?i.parentNode.offsetHeight:0;switch(d!=n.parentHeight&&(n.parentHeight=d,t+=1),this.getOption("orientation")){case"bottom":n.minorLabelHeight=o?n.minorCharHeight:0,n.majorLabelHeight=r?n.majorCharHeight:0,n.minorLabelTop=0,n.majorLabelTop=n.minorLabelTop+n.minorLabelHeight,n.minorLineTop=-this.top,n.minorLineHeight=Math.max(this.top+n.majorLabelHeight,0),n.minorLineWidth=1,n.majorLineTop=-this.top,n.majorLineHeight=Math.max(this.top+n.minorLabelHeight+n.majorLabelHeight,0),n.majorLineWidth=1,n.lineTop=0;break;case"top":n.minorLabelHeight=o?n.minorCharHeight:0,n.majorLabelHeight=r?n.majorCharHeight:0,n.majorLabelTop=0,n.minorLabelTop=n.majorLabelTop+n.majorLabelHeight,n.minorLineTop=n.minorLabelTop,n.minorLineHeight=Math.max(d-n.majorLabelHeight-this.top),n.minorLineWidth=1,n.majorLineTop=0,n.majorLineHeight=Math.max(d-this.top),n.majorLineWidth=1,n.lineTop=n.majorLabelHeight+n.minorLabelHeight;break;default:throw Error('Unkown orientation "'+this.getOption("orientation")+'"')}var l=n.minorLabelHeight+n.majorLabelHeight;t+=e(this,"width",i.offsetWidth),t+=e(this,"height",l),this._updateConversion();var p=D.convert(s.start,"Date"),c=D.convert(s.end,"Date"),u=this.toTime(5*(n.minorCharWidth||10))-this.toTime(0);this.step=new TimeStep(p,c,u),t+=e(n.range,"start",p.valueOf()),t+=e(n.range,"end",c.valueOf()),t+=e(n.range,"minimumStep",u.valueOf())}return t>0},u.prototype._updateConversion=function(){var t=this.range;if(!t)throw Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):h.conversion(t.start,t.end,this.width)},f.prototype=new p,f.types={box:g,range:y,point:v},f.prototype.setOptions=l.prototype.setOptions,f.prototype.setRange=function(t){if(!(t instanceof h||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},f.prototype.repaint=function(){var t=0,e=D.updateProperty,i=D.option.asSize,s=this.options,n=this.getOption("orientation"),o=this.defaultOptions,r=this.frame;if(!r){r=document.createElement("div"),r.className="itemset";var a=s.className;a&&D.addClassName(r,D.option.asString(a));var h=document.createElement("div");h.className="background",r.appendChild(h),this.dom.background=h;var d=document.createElement("div");d.className="foreground",r.appendChild(d),this.dom.foreground=d;var l=document.createElement("div");l.className="itemset-axis",this.dom.axis=l,this.frame=r,t+=1}if(!this.parent)throw Error("Cannot repaint itemset: no parent attached");var p=this.parent.getContainer();if(!p)throw Error("Cannot repaint itemset: parent has no container element");r.parentNode||(p.appendChild(r),t+=1),this.dom.axis.parentNode||(p.appendChild(this.dom.axis),t+=1),t+=e(r.style,"left",i(s.left,"0px")),t+=e(r.style,"top",i(s.top,"0px")),t+=e(r.style,"width",i(s.width,"100%")),t+=e(r.style,"height",i(s.height,this.height+"px")),t+=e(this.dom.axis.style,"left",i(s.left,"0px")),t+=e(this.dom.axis.style,"width",i(s.width,"100%")),t+="bottom"==n?e(this.dom.axis.style,"top",this.height+this.top+"px"):e(this.dom.axis.style,"top",this.top+"px"),this._updateConversion();var c=this,u=this.queue,m=this.itemsData,g=this.items,v={fields:[m&&m.fieldId||"id","start","end","content","type"]};return Object.keys(u).forEach(function(e){var i=u[e],n=g[e];switch(i){case"add":case"update":var r=m&&m.get(e,v);if(r){var a=r.type||r.start&&r.end&&"range"||s.type||"box",h=f.types[a];if(n&&(h&&n instanceof h?(n.data=r,t++):(t+=n.hide(),n=null)),!n){if(!h)throw new TypeError('Unknown item type "'+a+'"');n=new h(c,r,s,o),t++}n.repaint(),g[e]=n}delete u[e];break;case"remove":n&&(t+=n.hide()),delete g[e],delete u[e];break;default:console.log('Error: unknown action "'+i+'"')}}),D.forEach(this.items,function(e){e.visible?(t+=e.show(),e.reposition()):t+=e.hide()}),t>0},f.prototype.getForeground=function(){return this.dom.foreground},f.prototype.getBackground=function(){return this.dom.background},f.prototype.getAxis=function(){return this.dom.axis},f.prototype.reflow=function(){var t=0,e=this.options,i=e.margin&&e.margin.axis||this.defaultOptions.margin.axis,s=e.margin&&e.margin.item||this.defaultOptions.margin.item,n=D.updateProperty,o=D.option.asNumber,r=D.option.asSize,a=this.frame;if(a){this._updateConversion(),D.forEach(this.items,function(e){t+=e.reflow()}),this.stack.update();var h,d=o(e.maxHeight),l=null!=r(e.height);if(l)h=a.offsetHeight;else{var p=this.stack.ordered;if(p.length){var c=p[0].top,u=p[0].top+p[0].height;D.forEach(p,function(t){c=Math.min(c,t.top),u=Math.max(u,t.top+t.height)}),h=u-c+i+s}else h=i+s}null!=d&&(h=Math.min(h,d)),t+=n(this,"height",h),t+=n(this,"top",a.offsetTop),t+=n(this,"left",a.offsetLeft),t+=n(this,"width",a.offsetWidth)}else t+=1;return t>0},f.prototype.hide=function(){var t=!1;return this.frame&&this.frame.parentNode&&(this.frame.parentNode.removeChild(this.frame),t=!0),this.dom.axis&&this.dom.axis.parentNode&&(this.dom.axis.parentNode.removeChild(this.dom.axis),t=!0),t},f.prototype.setItems=function(t){var e,i=this,s=this.itemsData;if(t){if(!(t instanceof o||t instanceof r))throw new TypeError("Data must be an instance of DataSet");this.itemsData=t}else this.itemsData=null;if(s&&(D.forEach(this.listeners,function(t,e){s.unsubscribe(e,t)}),e=s.getIds(),this._onRemove(e)),this.itemsData){var n=this.id;D.forEach(this.listeners,function(t,e){i.itemsData.subscribe(e,t,n)}),e=this.itemsData.getIds(),this._onAdd(e)}},f.prototype.getItems=function(){return this.itemsData},f.prototype._onUpdate=function(t){this._toQueue("update",t)},f.prototype._onAdd=function(t){this._toQueue("add",t)},f.prototype._onRemove=function(t){this._toQueue("remove",t)},f.prototype._toQueue=function(t,e){var i=this.queue;e.forEach(function(e){i[e]=t}),this.controller&&this.requestRepaint()},f.prototype._updateConversion=function(){var t=this.range;if(!t)throw Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):h.conversion(t.start,t.end,this.width)},f.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.factor+e.offset)},f.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.factor},m.prototype.select=function(){this.selected=!0},m.prototype.unselect=function(){this.selected=!1},m.prototype.show=function(){return!1},m.prototype.hide=function(){return!1},m.prototype.repaint=function(){return!1},m.prototype.reflow=function(){return!1},g.prototype=new m(null,null),g.prototype.select=function(){this.selected=!0},g.prototype.unselect=function(){this.selected=!1},g.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw Error("Cannot repaint time axis: parent has no foreground container element");var s=this.parent.getBackground();if(!s)throw Error("Cannot repaint time axis: parent has no background container element");var n=this.parent.getAxis();if(!s)throw Error("Cannot repaint time axis: parent has no axis container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),e.line.parentNode||(s.appendChild(e.line),t=!0),e.dot.parentNode||(n.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 o=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=o&&(this.className=o,e.box.className="item box"+o,e.line.className="item line"+o,e.dot.className="item dot"+o,t=!0)}return t},g.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},g.prototype.hide=function(){var t=!1,e=this.dom;return e&&(e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),e.line.parentNode&&e.line.parentNode.removeChild(e.line),e.dot.parentNode&&e.dot.parentNode.removeChild(e.dot)),t},g.prototype.reflow=function(){var t,e,i,s,n,o,r,a,h,d,l,p,c=0;if(void 0==this.data.start)throw Error('Property "start" missing in item '+this.data.id);if(l=this.data,p=this.parent&&this.parent.range,l&&p){var u=p.end-p.start;this.visible=l.start>p.start-u&&l.start0},g.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")},g.prototype.reposition=function(){var t=this.dom,e=this.props,i=this.options.orientation||this.defaultOptions.orientation;if(t){var s=t.box,n=t.line,o=t.dot;s.style.left=this.left+"px",s.style.top=this.top+"px",n.style.left=e.line.left+"px","top"==i?(n.style.top="0px",n.style.height=this.top+"px"):(n.style.top=this.top+this.height+"px",n.style.height=Math.max(this.parent.height-this.top-this.height+this.props.dot.height/2,0)+"px"),o.style.left=e.dot.left+"px",o.style.top=e.dot.top+"px"}},v.prototype=new m(null,null),v.prototype.select=function(){this.selected=!0},v.prototype.unselect=function(){this.selected=!1},v.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw Error("Cannot repaint time axis: parent has no foreground container element");if(e.point.parentNode||(i.appendChild(e.point),i.appendChild(e.point),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var s=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=s&&(this.className=s,e.point.className="item point"+s,t=!0)}return t},v.prototype.show=function(){return this.dom&&this.dom.point.parentNode?!1:this.repaint()},v.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.point.parentNode&&(e.point.parentNode.removeChild(e.point),t=!0),t},v.prototype.reflow=function(){var t,e,i,s,n,o,r,a,h,d,l=0;if(void 0==this.data.start)throw Error('Property "start" missing in item '+this.data.id);if(h=this.data,d=this.parent&&this.parent.range,h&&d){var p=d.end-d.start;this.visible=h.start>d.start-p&&h.start0},v.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))},v.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")},y.prototype=new m(null,null),y.prototype.select=function(){this.selected=!0},y.prototype.unselect=function(){this.selected=!1},y.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw Error("Cannot repaint time axis: parent has no foreground container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content
-}t=!0}var s=this.data.className?""+this.data.className:"";this.className!=s&&(this.className=s,e.box.className="item range"+s,t=!0)}return t},y.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},y.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),t},y.prototype.reflow=function(){var t,e,i,s,n,o,r,a,h,d,l,p,c,u,f,m,g=0;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);return h=this.data,d=this.parent&&this.parent.range,this.visible=h&&d?h.startd.start:!1,this.visible&&(t=this.dom,t?(e=this.props,i=this.options,o=this.parent,r=o.toScreen(this.data.start),a=o.toScreen(this.data.end),l=D.updateProperty,p=t.box,c=o.width,f=i.orientation||this.defaultOptions.orientation,s=i.margin&&i.margin.axis||this.defaultOptions.margin.axis,n=i.padding||this.defaultOptions.padding,g+=l(e.content,"width",t.content.offsetWidth),g+=l(this,"height",p.offsetHeight),-c>r&&(r=-c),a>2*c&&(a=2*c),u=0>r?Math.min(-r,a-r-e.content.width-2*n):0,g+=l(e.content,"left",u),"top"==f?(m=s,g+=l(this,"top",m)):(m=o.height-this.height-s,g+=l(this,"top",m)),g+=l(this,"left",r),g+=l(this,"width",Math.max(a-r,1))):g+=1),g>0},y.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))},y.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")},b.prototype=new l,b.prototype.setOptions=l.prototype.setOptions,b.prototype.getContainer=function(){return this.parent.getContainer()},b.prototype.setItems=function(t){if(this.itemset&&(this.itemset.hide(),this.itemset.setItems(),this.parent.controller.remove(this.itemset),this.itemset=null),t){var e=this.groupId,i=Object.create(this.options);this.itemset=new f(this,null,i),this.itemset.setRange(this.parent.range),this.view=new r(t,{filter:function(t){return t.group==e}}),this.itemset.setItems(this.view),this.parent.controller.add(this.itemset)}},b.prototype.repaint=function(){return!1},b.prototype.reflow=function(){var t=0,e=D.updateProperty;if(t+=e(this,"top",this.itemset?this.itemset.top:0),t+=e(this,"height",this.itemset?this.itemset.height:0),this.label){var i=this.label.firstChild;t+=e(this.props.label,"width",i.clientWidth),t+=e(this.props.label,"height",i.clientHeight)}else t+=e(this.props.label,"width",0),t+=e(this.props.label,"height",0);return t>0},w.prototype=new p,w.prototype.setOptions=l.prototype.setOptions,w.prototype.setRange=function(){},w.prototype.setItems=function(t){this.itemsData=t;for(var e in this.groups)if(this.groups.hasOwnProperty(e)){var i=this.groups[e];i.setItems(t)}},w.prototype.getItems=function(){return this.itemsData},w.prototype.setRange=function(t){this.range=t},w.prototype.setGroups=function(t){var e,i=this;if(this.groupsData&&(D.forEach(this.listeners,function(t,e){i.groupsData.unsubscribe(e,t)}),e=this.groupsData.getIds(),this._onRemove(e)),t?t instanceof o?this.groupsData=t:(this.groupsData=new o({convert:{start:"Date",end:"Date"}}),this.groupsData.add(t)):this.groupsData=null,this.groupsData){var s=this.id;D.forEach(this.listeners,function(t,e){i.groupsData.subscribe(e,t,s)}),e=this.groupsData.getIds(),this._onAdd(e)}},w.prototype.getGroups=function(){return this.groupsData},w.prototype.repaint=function(){var t,e,i,s,n=0,o=D.updateProperty,r=D.option.asSize,a=D.option.asElement,h=this.options,d=this.dom.frame,l=this.dom.labels;if(!this.parent)throw Error("Cannot repaint groupset: no parent attached");var p=this.parent.getContainer();if(!p)throw Error("Cannot repaint groupset: parent has no container element");if(!d){d=document.createElement("div"),d.className="groupset",this.dom.frame=d;var c=h.className;c&&D.addClassName(d,D.option.asString(c)),n+=1}d.parentNode||(p.appendChild(d),n+=1);var u=a(h.labelContainer);if(!u)throw Error('Cannot repaint groupset: option "labelContainer" not defined');l||(l=document.createElement("div"),l.className="labels",this.dom.labels=l),l.parentNode&&l.parentNode==u||(l.parentNode&&l.parentNode.removeChild(l.parentNode),u.appendChild(l)),n+=o(d.style,"height",r(h.height,this.height+"px")),n+=o(d.style,"top",r(h.top,"0px")),n+=o(d.style,"left",r(h.left,"0px")),n+=o(d.style,"width",r(h.width,"100%")),n+=o(l.style,"top",r(h.top,"0px"));var f=this,m=this.queue,g=this.groups,v=this.groupsData,y=Object.keys(m);if(y.length){y.forEach(function(t){var e=m[t],i=g[t];switch(e){case"add":case"update":if(!i){var s=Object.create(f.options);i=new b(f,t,s),i.setItems(f.itemsData),g[t]=i,f.controller.add(i)}i.data=v.get(t),delete m[t];break;case"remove":i&&(i.setItems(),delete g[t],f.controller.remove(i)),delete m[t];break;default:console.log('Error: unknown action "'+e+'"')}});var w=this.groupsData.getIds({order:this.options.groupsOrder});for(t=0;w.length>t;t++)(function(t,e){var i=0;e&&(i=function(){return e.top+e.height}),t.setOptions({top:i})})(g[w[t]],g[w[t-1]]);for(;l.firstChild;)l.removeChild(l.firstChild);for(t=0;w.length>t;t++)e=w[t],s=this._createLabel(e),l.appendChild(s);n++}for(e in g)g.hasOwnProperty(e)&&(i=g[e],s=i.label,s&&(s.style.top=i.top+"px",s.style.height=i.height+"px"));return n>0},w.prototype._createLabel=function(t){var e=this.groups[t],i=document.createElement("div");i.className="label";var s=document.createElement("div");s.className="inner",i.appendChild(s);var n=e.data&&e.data.content;n instanceof Element?s.appendChild(n):void 0!=n&&(s.innerHTML=n);var o=e.data&&e.data.className;return o&&D.addClassName(i,o),e.label=i,i},w.prototype.getContainer=function(){return this.dom.frame},w.prototype.getLabelsWidth=function(){return this.props.labels.width},w.prototype.reflow=function(){var t,e,i=0,s=this.options,n=D.updateProperty,o=D.option.asNumber,r=D.option.asSize,a=this.dom.frame;if(a){var h,d=o(s.maxHeight),l=null!=r(s.height);if(l)h=a.offsetHeight;else{h=0;for(t in this.groups)this.groups.hasOwnProperty(t)&&(e=this.groups[t],h+=e.height)}null!=d&&(h=Math.min(h,d)),i+=n(this,"height",h),i+=n(this,"top",a.offsetTop),i+=n(this,"left",a.offsetLeft),i+=n(this,"width",a.offsetWidth)}var p=0;for(t in this.groups)if(this.groups.hasOwnProperty(t)){e=this.groups[t];var c=e.props&&e.props.label&&e.props.label.width||0;p=Math.max(p,c)}return i+=n(this.props.labels,"width",p),i>0},w.prototype.hide=function(){return this.dom.frame&&this.dom.frame.parentNode?(this.dom.frame.parentNode.removeChild(this.dom.frame),!0):!1},w.prototype.show=function(){return this.dom.frame&&this.dom.frame.parentNode?!1:this.repaint()},w.prototype._onUpdate=function(t){this._toQueue(t,"update")},w.prototype._onAdd=function(t){this._toQueue(t,"add")},w.prototype._onRemove=function(t){this._toQueue(t,"remove")},w.prototype._toQueue=function(t,e){var i=this.queue;t.forEach(function(t){i[t]=e}),this.controller&&this.requestRepaint()},_.prototype.setOptions=function(t){t&&D.extend(this.options,t),this.controller.reflow(),this.controller.repaint()},_.prototype.setItems=function(t){var e,i=null==this.itemsData;if(t?t instanceof o&&(e=t):e=null,t instanceof o||(e=new o({convert:{start:"Date",end:"Date"}}),e.add(t)),this.itemsData=e,this.content.setItems(e),i&&(void 0==this.options.start||void 0==this.options.end)){var s=this.getItemRange(),n=s.min,r=s.max;if(null!=n&&null!=r){var a=r.valueOf()-n.valueOf();n=new Date(n.valueOf()-.05*a),r=new Date(r.valueOf()+.05*a)}void 0!=this.options.start&&(n=new Date(this.options.start.valueOf())),void 0!=this.options.end&&(r=new Date(this.options.end.valueOf())),(null!=n||null!=r)&&this.range.setRange(n,r)}},_.prototype.setGroups=function(t){var e=this;this.groupsData=t;var i=this.groupsData?w:f;if(!(this.content instanceof i)){this.content&&(this.content.hide(),this.content.setItems&&this.content.setItems(),this.content.setGroups&&this.content.setGroups(),this.controller.remove(this.content));var s=Object.create(this.options);D.extend(s,{top:function(){return"top"==e.options.orientation?e.timeaxis.height:e.itemPanel.height-e.timeaxis.height-e.content.height},left:null,width:"100%",height:function(){return e.options.height?e.itemPanel.height-e.timeaxis.height:null},maxHeight:function(){if(e.options.maxHeight){if(!D.isNumber(e.options.maxHeight))throw new TypeError("Number expected for property maxHeight");return e.options.maxHeight-e.timeaxis.height}return null},labelContainer:function(){return e.labelPanel.getContainer()}}),this.content=new i(this.itemPanel,[this.timeaxis],s),this.content.setRange&&this.content.setRange(this.range),this.content.setItems&&this.content.setItems(this.itemsData),this.content.setGroups&&this.content.setGroups(this.groupsData),this.controller.add(this.content)}},_.prototype.getItemRange=function(){var t=this.itemsData,e=null,i=null;if(t){var s=t.min("start");e=s?s.start.valueOf():null;var n=t.max("start");n&&(i=n.start.valueOf());var o=t.max("end");o&&(i=null==i?o.end.valueOf():Math.max(i,o.end.valueOf()))}return{min:null!=e?new Date(e):null,max:null!=i?new Date(i):null}},function(t){function e(t){return E=t,c()}function i(){D=0,C=E.charAt(0)}function s(){D++,C=E.charAt(D)}function n(){return E.charAt(D+1)}function o(t){return N.test(t)}function r(t,e){if(t||(t={}),e)for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}function a(t,e,i){for(var s=e.split("."),n=t;s.length;){var o=s.shift();s.length?(n[o]||(n[o]={}),n=n[o]):n[o]=i}}function h(t,e){for(var i,s,n=null,o=[t],a=t;a.parent;)o.push(a.parent),a=a.parent;if(a.nodes)for(i=0,s=a.nodes.length;s>i;i++)if(e.id===a.nodes[i].id){n=a.nodes[i];break}for(n||(n={id:e.id},t.node&&(n.attr=r(n.attr,t.node))),i=o.length-1;i>=0;i--){var h=o[i];h.nodes||(h.nodes=[]),-1==h.nodes.indexOf(n)&&h.nodes.push(n)}e.attr&&(n.attr=r(n.attr,e.attr))}function d(t,e){if(t.edges||(t.edges=[]),t.edges.push(e),t.edge){var i=r({},t.edge);e.attr=r(i,e.attr)}}function l(t,e,i,s,n){var o={from:e,to:i,type:s};return t.edge&&(o.attr=r({},t.edge)),o.attr=r(o.attr||{},n),o}function p(){for(O=S.NULL,L="";" "==C||" "==C||"\n"==C||"\r"==C;)s();do{var t=!1;if("#"==C){for(var e=D-1;" "==E.charAt(e)||" "==E.charAt(e);)e--;if("\n"==E.charAt(e)||""==E.charAt(e)){for(;""!=C&&"\n"!=C;)s();t=!0}}if("/"==C&&"/"==n()){for(;""!=C&&"\n"!=C;)s();t=!0}if("/"==C&&"*"==n()){for(;""!=C;){if("*"==C&&"/"==n()){s(),s();break}s()}t=!0}for(;" "==C||" "==C||"\n"==C||"\r"==C;)s()}while(t);if(""==C)return O=S.DELIMITER,void 0;var i=C+n();if(M[i])return O=S.DELIMITER,L=i,s(),s(),void 0;if(M[C])return O=S.DELIMITER,L=C,s(),void 0;if(o(C)||"-"==C){for(L+=C,s();o(C);)L+=C,s();return"false"==L?L=!1:"true"==L?L=!0:isNaN(Number(L))||(L=Number(L)),O=S.IDENTIFIER,void 0}if('"'==C){for(s();""!=C&&('"'!=C||'"'==C&&'"'==n());)L+=C,'"'==C&&s(),s();if('"'!=C)throw w('End of string " expected');return s(),O=S.IDENTIFIER,void 0}for(O=S.UNKNOWN;""!=C;)L+=C,s();throw new SyntaxError('Syntax error in part "'+_(L,30)+'"')}function c(){var t={};if(i(),p(),"strict"==L&&(t.strict=!0,p()),("graph"==L||"digraph"==L)&&(t.type=L,p()),O==S.IDENTIFIER&&(t.id=L,p()),"{"!=L)throw w("Angle bracket { expected");if(p(),u(t),"}"!=L)throw w("Angle bracket } expected");if(p(),""!==L)throw w("End of file expected");return p(),delete t.node,delete t.edge,delete t.graph,t}function u(t){for(;""!==L&&"}"!=L;)f(t),";"==L&&p()}function f(t){var e=m(t);if(e)return y(t,e),void 0;var i=g(t);if(!i){if(O!=S.IDENTIFIER)throw w("Identifier expected");var s=L;if(p(),"="==L){if(p(),O!=S.IDENTIFIER)throw w("Identifier expected");t[s]=L,p()}else v(t,s)}}function m(t){var e=null;if("subgraph"==L&&(e={},e.type="subgraph",p(),O==S.IDENTIFIER&&(e.id=L,p())),"{"==L){if(p(),e||(e={}),e.parent=t,e.node=t.node,e.edge=t.edge,e.graph=t.graph,u(e),"}"!=L)throw w("Angle bracket } expected");p(),delete e.node,delete e.edge,delete e.graph,delete e.parent,t.subgraphs||(t.subgraphs=[]),t.subgraphs.push(e)}return e}function g(t){return"node"==L?(p(),t.node=b(),"node"):"edge"==L?(p(),t.edge=b(),"edge"):"graph"==L?(p(),t.graph=b(),"graph"):null}function v(t,e){var i={id:e},s=b();s&&(i.attr=s),h(t,i),y(t,e)}function y(t,e){for(;"->"==L||"--"==L;){var i,s=L;p();var n=m(t);if(n)i=n;else{if(O!=S.IDENTIFIER)throw w("Identifier or subgraph expected");i=L,h(t,{id:i}),p()}var o=b(),r=l(t,e,i,s,o);d(t,r),e=i}}function b(){for(var t=null;"["==L;){for(p(),t={};""!==L&&"]"!=L;){if(O!=S.IDENTIFIER)throw w("Attribute name expected");var e=L;if(p(),"="!=L)throw w("Equal sign = expected");if(p(),O!=S.IDENTIFIER)throw w("Attribute value expected");var i=L;a(t,e,i),p(),","==L&&p()}if("]"!=L)throw w("Bracket ] expected");p()}return t}function w(t){return new SyntaxError(t+', got "'+_(L,30)+'" (char '+D+")")}function _(t,e){return e>=t.length?t:t.substr(0,27)+"..."}function x(t,e,i){t instanceof Array?t.forEach(function(t){e instanceof Array?e.forEach(function(e){i(t,e)}):i(t,e)}):e instanceof Array?e.forEach(function(e){i(t,e)}):i(t,e)}function T(t){function i(t){var e={from:t.from,to:t.to};return r(e,t.attr),e.style="->"==t.type?"arrow":"line",e}var s=e(t),n={nodes:[],edges:[],options:{}};return s.nodes&&s.nodes.forEach(function(t){var e={id:t.id,label:(t.label||t.id)+""};r(e,t.attr),e.image&&(e.shape="image"),n.nodes.push(e)}),s.edges&&s.edges.forEach(function(t){var e,s;e=t.from instanceof Object?t.from.nodes:{id:t.from},s=t.to instanceof Object?t.to.nodes:{id:t.to},t.from instanceof Object&&t.from.edges&&t.from.edges.forEach(function(t){var e=i(t);n.edges.push(e)}),x(e,s,function(e,s){var o=l(n,e.id,s.id,t.type,t.attr),r=i(o);n.edges.push(r)}),t.to instanceof Object&&t.to.edges&&t.to.edges.forEach(function(t){var e=i(t);n.edges.push(e)})}),s.attr&&(n.options=s.attr),n}var S={NULL:0,DELIMITER:1,IDENTIFIER:2,UNKNOWN:3},M={"{":!0,"}":!0,"[":!0,"]":!0,";":!0,"=":!0,",":!0,"->":!0,"--":!0},E="",D=0,C="",L="",O=S.NULL,N=/[a-zA-Z_0-9.:#]/;t.parseDOT=e,t.DOTToGraph=T}(D!==void 0?D:s),"undefined"!=typeof CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.circle=function(t,e,i){this.beginPath(),this.arc(t,e,i,0,2*Math.PI,!1)},CanvasRenderingContext2D.prototype.square=function(t,e,i){this.beginPath(),this.rect(t-i,e-i,2*i,2*i)},CanvasRenderingContext2D.prototype.triangle=function(t,e,i){this.beginPath();var s=2*i,n=s/2,o=Math.sqrt(3)/6*s,r=Math.sqrt(s*s-n*n);this.moveTo(t,e-(r-o)),this.lineTo(t+n,e+o),this.lineTo(t-n,e+o),this.lineTo(t,e-(r-o)),this.closePath()},CanvasRenderingContext2D.prototype.triangleDown=function(t,e,i){this.beginPath();var s=2*i,n=s/2,o=Math.sqrt(3)/6*s,r=Math.sqrt(s*s-n*n);this.moveTo(t,e+(r-o)),this.lineTo(t+n,e-o),this.lineTo(t-n,e-o),this.lineTo(t,e+(r-o)),this.closePath()},CanvasRenderingContext2D.prototype.star=function(t,e,i){this.beginPath();for(var s=0;10>s;s++){var n=0===s%2?1.3*i:.5*i;this.lineTo(t+n*Math.sin(2*s*Math.PI/10),e-n*Math.cos(2*s*Math.PI/10))}this.closePath()},CanvasRenderingContext2D.prototype.roundRect=function(t,e,i,s,n){var o=Math.PI/180;0>i-2*n&&(n=i/2),0>s-2*n&&(n=s/2),this.beginPath(),this.moveTo(t+n,e),this.lineTo(t+i-n,e),this.arc(t+i-n,e+n,n,270*o,360*o,!1),this.lineTo(t+i,e+s-n),this.arc(t+i-n,e+s-n,n,0,90*o,!1),this.lineTo(t+n,e+s),this.arc(t+n,e+s-n,n,90*o,180*o,!1),this.lineTo(t,e+n),this.arc(t+n,e+n,n,180*o,270*o,!1)},CanvasRenderingContext2D.prototype.ellipse=function(t,e,i,s){var n=.5522848,o=i/2*n,r=s/2*n,a=t+i,h=e+s,d=t+i/2,l=e+s/2;this.beginPath(),this.moveTo(t,l),this.bezierCurveTo(t,l-r,d-o,e,d,e),this.bezierCurveTo(d+o,e,a,l-r,a,l),this.bezierCurveTo(a,l+r,d+o,h,d,h),this.bezierCurveTo(d-o,h,t,l+r,t,l)},CanvasRenderingContext2D.prototype.database=function(t,e,i,s){var n=1/3,o=i,r=s*n,a=.5522848,h=o/2*a,d=r/2*a,l=t+o,p=e+r,c=t+o/2,u=e+r/2,f=e+(s-r/2),m=e+s;this.beginPath(),this.moveTo(l,u),this.bezierCurveTo(l,u+d,c+h,p,c,p),this.bezierCurveTo(c-h,p,t,u+d,t,u),this.bezierCurveTo(t,u-d,c-h,e,c,e),this.bezierCurveTo(c+h,e,l,u-d,l,u),this.lineTo(l,f),this.bezierCurveTo(l,f+d,c+h,m,c,m),this.bezierCurveTo(c-h,m,t,f+d,t,f),this.lineTo(t,u)},CanvasRenderingContext2D.prototype.arrow=function(t,e,i,s){var n=t-s*Math.cos(i),o=e-s*Math.sin(i),r=t-.9*s*Math.cos(i),a=e-.9*s*Math.sin(i),h=n+s/3*Math.cos(i+.5*Math.PI),d=o+s/3*Math.sin(i+.5*Math.PI),l=n+s/3*Math.cos(i-.5*Math.PI),p=o+s/3*Math.sin(i-.5*Math.PI);this.beginPath(),this.moveTo(t,e),this.lineTo(h,d),this.lineTo(r,a),this.lineTo(l,p),this.closePath()},CanvasRenderingContext2D.prototype.dashedLine=function(t,e,i,s,n){n||(n=[10,5]),0==c&&(c=.001);var o=n.length;this.moveTo(t,e);for(var r=i-t,a=s-e,h=a/r,d=Math.sqrt(r*r+a*a),l=0,p=!0;d>=.1;){var c=n[l++%o];c>d&&(c=d);var u=Math.sqrt(c*c/(1+h*h));0>r&&(u=-u),t+=u,e+=h*u,this[p?"lineTo":"moveTo"](t,e),d-=c,p=!p}}),x.prototype.attachEdge=function(t){-1==this.edges.indexOf(t)&&this.edges.push(t),this._updateMass()},x.prototype.detachEdge=function(t){var e=this.edges.indexOf(t);-1!=e&&this.edges.splice(e,1),this._updateMass()},x.prototype._updateMass=function(){this.mass=50+20*this.edges.length},x.prototype.setProperties=function(t,e){if(t){if(void 0!=t.id&&(this.id=t.id),void 0!=t.label&&(this.label=t.label),void 0!=t.title&&(this.title=t.title),void 0!=t.group&&(this.group=t.group),void 0!=t.x&&(this.x=t.x),void 0!=t.y&&(this.y=t.y),void 0!=t.value&&(this.value=t.value),void 0===this.id)throw"Node must have an id";if(this.group){var i=this.grouplist.get(this.group);for(var s in i)i.hasOwnProperty(s)&&(this[s]=i[s])}if(void 0!=t.shape&&(this.shape=t.shape),void 0!=t.image&&(this.image=t.image),void 0!=t.radius&&(this.radius=t.radius),void 0!=t.color&&(this.color=x.parseColor(t.color)),void 0!=t.fontColor&&(this.fontColor=t.fontColor),void 0!=t.fontSize&&(this.fontSize=t.fontSize),void 0!=t.fontFace&&(this.fontFace=t.fontFace),void 0!=this.image){if(!this.imagelist)throw"No imagelist provided";this.imageObj=this.imagelist.load(this.image)}switch(this.xFixed=this.xFixed||void 0!=t.x,this.yFixed=this.yFixed||void 0!=t.y,this.radiusFixed=this.radiusFixed||void 0!=t.radius,"image"==this.shape&&(this.radiusMin=e.nodes.widthMin,this.radiusMax=e.nodes.widthMax),this.shape){case"database":this.draw=this._drawDatabase,this.resize=this._resizeDatabase;break;case"box":this.draw=this._drawBox,this.resize=this._resizeBox;break;case"circle":this.draw=this._drawCircle,this.resize=this._resizeCircle;break;case"ellipse":this.draw=this._drawEllipse,this.resize=this._resizeEllipse;break;case"image":this.draw=this._drawImage,this.resize=this._resizeImage;break;case"text":this.draw=this._drawText,this.resize=this._resizeText;break;case"dot":this.draw=this._drawDot,this.resize=this._resizeShape;break;case"square":this.draw=this._drawSquare,this.resize=this._resizeShape;break;case"triangle":this.draw=this._drawTriangle,this.resize=this._resizeShape;break;case"triangleDown":this.draw=this._drawTriangleDown,this.resize=this._resizeShape;break;case"star":this.draw=this._drawStar,this.resize=this._resizeShape;break;default:this.draw=this._drawEllipse,this.resize=this._resizeEllipse}this._reset()}},x.parseColor=function(t){var e;return D.isString(t)?e={border:t,background:t,highlight:{border:t,background:t}}:(e={},e.background=t.background||"white",e.border=t.border||e.background,D.isString(t.highlight)?e.highlight={border:t.highlight,background:t.highlight}:(e.highlight={},e.highlight.background=t.highlight&&t.highlight.background||e.background,e.highlight.border=t.highlight&&t.highlight.border||e.border)),e},x.prototype.select=function(){this.selected=!0,this._reset()},x.prototype.unselect=function(){this.selected=!1,this._reset()},x.prototype._reset=function(){this.width=void 0,this.height=void 0},x.prototype.getTitle=function(){return this.title},x.prototype.distanceToBorder=function(t,e){var i=1;switch(this.width||this.resize(t),this.shape){case"circle":case"dot":return this.radius+i;case"ellipse":var s=this.width/2,n=this.height/2,o=Math.sin(e)*s,r=Math.cos(e)*n;return s*n/Math.sqrt(o*o+r*r);case"box":case"image":case"text":default:return this.width?Math.min(Math.abs(this.width/2/Math.cos(e)),Math.abs(this.height/2/Math.sin(e)))+i:0}},x.prototype._setForce=function(t,e){this.fx=t,this.fy=e},x.prototype._addForce=function(t,e){this.fx+=t,this.fy+=e},x.prototype.discreteStep=function(t){if(!this.xFixed){var e=-this.damping*this.vx,i=(this.fx+e)/this.mass;this.vx+=i/t,this.x+=this.vx/t}if(!this.yFixed){var s=-this.damping*this.vy,n=(this.fy+s)/this.mass;this.vy+=n/t,this.y+=this.vy/t}},x.prototype.isFixed=function(){return this.xFixed&&this.yFixed},x.prototype.isMoving=function(t){return Math.abs(this.vx)>t||Math.abs(this.vy)>t||!this.xFixed&&Math.abs(this.fx)>this.minForce||!this.yFixed&&Math.abs(this.fy)>this.minForce},x.prototype.isSelected=function(){return this.selected},x.prototype.getValue=function(){return this.value},x.prototype.getDistance=function(t,e){var i=this.x-t,s=this.y-e;return Math.sqrt(i*i+s*s)},x.prototype.setValueRange=function(t,e){if(!this.radiusFixed&&void 0!==this.value){var i=(this.radiusMax-this.radiusMin)/(e-t);this.radius=(this.value-t)*i+this.radiusMin}},x.prototype.draw=function(){throw"Draw method not initialized for node"},x.prototype.resize=function(){throw"Resize method not initialized for node"},x.prototype.isOverlappingWith=function(t){return this.leftt.left&&this.topt.top},x.prototype._resizeImage=function(){if(!this.width){var t,e;if(this.value){var i=this.imageObj.height/this.imageObj.width;t=this.radius||this.imageObj.width,e=this.radius*i||this.imageObj.height}else t=this.imageObj.width,e=this.imageObj.height;this.width=t,this.height=e}},x.prototype._drawImage=function(t){this._resizeImage(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e;this.imageObj?(t.drawImage(this.imageObj,this.left,this.top,this.width,this.height),e=this.y+this.height/2):e=this.y,this._label(t,this.label,this.x,e,void 0,"top")},x.prototype._resizeBox=function(t){if(!this.width){var e=5,i=this.getTextSize(t);this.width=i.width+2*e,this.height=i.height+2*e}},x.prototype._drawBox=function(t){this._resizeBox(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.lineWidth=this.selected?2:1,t.roundRect(this.left,this.top,this.width,this.height,this.radius),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},x.prototype._resizeDatabase=function(t){if(!this.width){var e=5,i=this.getTextSize(t),s=i.width+2*e;this.width=s,this.height=s}},x.prototype._drawDatabase=function(t){this._resizeDatabase(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.lineWidth=this.selected?2:1,t.database(this.x-this.width/2,this.y-.5*this.height,this.width,this.height),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},x.prototype._resizeCircle=function(t){if(!this.width){var e=5,i=this.getTextSize(t),s=Math.max(i.width,i.height)+2*e;this.radius=s/2,this.width=s,this.height=s}},x.prototype._drawCircle=function(t){this._resizeCircle(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.lineWidth=this.selected?2:1,t.circle(this.x,this.y,this.radius),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},x.prototype._resizeEllipse=function(t){if(!this.width){var e=this.getTextSize(t);this.width=1.5*e.width,this.height=2*e.height,this.widthl;l++)t.fillText(r[l],i,d),d+=h}},x.prototype.getTextSize=function(t){if(void 0!=this.label){t.font=(this.selected?"bold ":"")+this.fontSize+"px "+this.fontFace;for(var e=this.label.split("\n"),i=(this.fontSize+4)*e.length,s=0,n=0,o=e.length;o>n;n++)s=Math.max(s,t.measureText(e[n]).width);return{width:s,height:i}}return{width:0,height:0}},T.prototype.setProperties=function(t,e){if(t)switch(void 0!=t.from&&(this.fromId=t.from),void 0!=t.to&&(this.toId=t.to),void 0!=t.id&&(this.id=t.id),void 0!=t.style&&(this.style=t.style),void 0!=t.label&&(this.label=t.label),this.label&&(this.fontSize=e.edges.fontSize,this.fontFace=e.edges.fontFace,this.fontColor=e.edges.fontColor,void 0!=t.fontColor&&(this.fontColor=t.fontColor),void 0!=t.fontSize&&(this.fontSize=t.fontSize),void 0!=t.fontFace&&(this.fontFace=t.fontFace)),void 0!=t.title&&(this.title=t.title),void 0!=t.width&&(this.width=t.width),void 0!=t.value&&(this.value=t.value),void 0!=t.length&&(this.length=t.length),t.dash&&(void 0!=t.dash.length&&(this.dash.length=t.dash.length),void 0!=t.dash.gap&&(this.dash.gap=t.dash.gap),void 0!=t.dash.altLength&&(this.dash.altLength=t.dash.altLength)),void 0!=t.color&&(this.color=t.color),this.connect(),this.widthFixed=this.widthFixed||void 0!=t.width,this.lengthFixed=this.lengthFixed||void 0!=t.length,this.stiffness=1/this.length,this.style){case"line":this.draw=this._drawLine;break;case"arrow":this.draw=this._drawArrow;break;case"arrow-center":this.draw=this._drawArrowCenter;break;case"dash-line":this.draw=this._drawDashLine;break;default:this.draw=this._drawLine}},T.prototype.connect=function(){this.disconnect(),this.from=this.graph.nodes[this.fromId]||null,this.to=this.graph.nodes[this.toId]||null,this.connected=this.from&&this.to,this.connected?(this.from.attachEdge(this),this.to.attachEdge(this)):(this.from&&this.from.detachEdge(this),this.to&&this.to.detachEdge(this))},T.prototype.disconnect=function(){this.from&&(this.from.detachEdge(this),this.from=null),this.to&&(this.to.detachEdge(this),this.to=null),this.connected=!1},T.prototype.getTitle=function(){return this.title},T.prototype.getValue=function(){return this.value},T.prototype.setValueRange=function(t,e){if(!this.widthFixed&&void 0!==this.value){var i=(this.widthMax-this.widthMin)/(e-t);this.width=(this.value-t)*i+this.widthMin}},T.prototype.draw=function(){throw"Method draw not initialized in edge"},T.prototype.isOverlappingWith=function(t){var e=10,i=this.from.x,s=this.from.y,n=this.to.x,o=this.to.y,r=t.left,a=t.top,h=T._dist(i,s,n,o,r,a);return e>h},T.prototype._drawLine=function(t){t.strokeStyle=this.color,t.lineWidth=this._getLineWidth();var e;if(this.from!=this.to)this._line(t),this.label&&(e=this._pointOnLine(.5),this._label(t,this.label,e.x,e.y));else{var i,s,n=this.length/4,o=this.from;o.width||o.resize(t),o.width>o.height?(i=o.x+o.width/2,s=o.y-n):(i=o.x+n,s=o.y-o.height/2),this._circle(t,i,s,n),e=this._pointOnCircle(i,s,n,.5),this._label(t,this.label,e.x,e.y)}},T.prototype._getLineWidth=function(){return this.from.selected||this.to.selected?Math.min(2*this.width,this.widthMax):this.width},T.prototype._line=function(t){t.beginPath(),t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y),t.stroke()},T.prototype._circle=function(t,e,i,s){t.beginPath(),t.arc(e,i,s,0,2*Math.PI,!1),t.stroke()},T.prototype._label=function(t,e,i,s){if(e){t.font=(this.from.selected||this.to.selected?"bold ":"")+this.fontSize+"px "+this.fontFace,t.fillStyle="white";var n=t.measureText(e).width,o=this.fontSize,r=i-n/2,a=s-o/2;t.fillRect(r,a,n,o),t.fillStyle=this.fontColor||"black",t.textAlign="left",t.textBaseline="top",t.fillText(e,r,a)}},T.prototype._drawDashLine=function(t){if(t.strokeStyle=this.color,t.lineWidth=this._getLineWidth(),t.beginPath(),t.lineCap="round",void 0!=this.dash.altLength?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]):void 0!=this.dash.length&&void 0!=this.dash.gap?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dash.length,this.dash.gap]):(t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y)),t.stroke(),this.label){var e=this._pointOnLine(.5);this._label(t,this.label,e.x,e.y)}},T.prototype._pointOnLine=function(t){return{x:(1-t)*this.from.x+t*this.to.x,y:(1-t)*this.from.y+t*this.to.y}},T.prototype._pointOnCircle=function(t,e,i,s){var n=2*(s-3/8)*Math.PI;return{x:t+i*Math.cos(n),y:e-i*Math.sin(n)}},T.prototype._drawArrowCenter=function(t){var e;if(t.strokeStyle=this.color,t.fillStyle=this.color,t.lineWidth=this._getLineWidth(),this.from!=this.to){this._line(t);var i=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x),s=10+5*this.width;e=this._pointOnLine(.5),t.arrow(e.x,e.y,i,s),t.fill(),t.stroke(),this.label&&(e=this._pointOnLine(.5),this._label(t,this.label,e.x,e.y))}else{var n,o,r=this.length/4,a=this.from;a.width||a.resize(t),a.width>a.height?(n=a.x+a.width/2,o=a.y-r):(n=a.x+r,o=a.y-a.height/2),this._circle(t,n,o,r);var i=.2*Math.PI,s=10+5*this.width;e=this._pointOnCircle(n,o,r,.5),t.arrow(e.x,e.y,i,s),t.fill(),t.stroke(),this.label&&(e=this._pointOnCircle(n,o,r,.5),this._label(t,this.label,e.x,e.y))}},T.prototype._drawArrow=function(t){t.strokeStyle=this.color,t.fillStyle=this.color,t.lineWidth=this._getLineWidth();var e,i;if(this.from!=this.to){e=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x);var s=this.to.x-this.from.x,n=this.to.y-this.from.y,o=Math.sqrt(s*s+n*n),r=this.from.distanceToBorder(t,e+Math.PI),a=(o-r)/o,h=a*this.from.x+(1-a)*this.to.x,d=a*this.from.y+(1-a)*this.to.y,l=this.to.distanceToBorder(t,e),p=(o-l)/o,c=(1-p)*this.from.x+p*this.to.x,u=(1-p)*this.from.y+p*this.to.y;if(t.beginPath(),t.moveTo(h,d),t.lineTo(c,u),t.stroke(),i=10+5*this.width,t.arrow(c,u,e,i),t.fill(),t.stroke(),this.label){var f=this._pointOnLine(.5);this._label(t,this.label,f.x,f.y)}}else{var m,g,v,y=this.from,b=this.length/4;y.width||y.resize(t),y.width>y.height?(m=y.x+y.width/2,g=y.y-b,v={x:m,y:y.y,angle:.9*Math.PI}):(m=y.x+b,g=y.y-y.height/2,v={x:y.x,y:g,angle:.6*Math.PI}),t.beginPath(),t.arc(m,g,b,0,2*Math.PI,!1),t.stroke(),i=10+5*this.width,t.arrow(v.x,v.y,v.angle,i),t.fill(),t.stroke(),this.label&&(f=this._pointOnCircle(m,g,b,.5),this._label(t,this.label,f.x,f.y))}},T._dist=function(t,e,i,s,n,o){var r=i-t,a=s-e,h=r*r+a*a,d=((n-t)*r+(o-e)*a)/h;
-d>1?d=1:0>d&&(d=0);var l=t+d*r,p=e+d*a,c=l-n,u=p-o;return Math.sqrt(c*c+u*u)},S.prototype.setPosition=function(t,e){this.x=parseInt(t),this.y=parseInt(e)},S.prototype.setText=function(t){this.frame.innerHTML=t},S.prototype.show=function(t){if(void 0===t&&(t=!0),t){var e=this.frame.clientHeight,i=this.frame.clientWidth,s=this.frame.parentNode.clientHeight,n=this.frame.parentNode.clientWidth,o=this.y-e;o+e+this.padding>s&&(o=s-e-this.padding),this.padding>o&&(o=this.padding);var r=this.x;r+i+this.padding>n&&(r=n-i-this.padding),this.padding>r&&(r=this.padding),this.frame.style.left=r+"px",this.frame.style.top=o+"px",this.frame.style.visibility="visible"}else this.hide()},S.prototype.hide=function(){this.frame.style.visibility="hidden"},Groups=function(){this.clear(),this.defaultIndex=0},Groups.DEFAULT=[{border:"#2B7CE9",background:"#97C2FC",highlight:{border:"#2B7CE9",background:"#D2E5FF"}},{border:"#FFA500",background:"#FFFF00",highlight:{border:"#FFA500",background:"#FFFFA3"}},{border:"#FA0A10",background:"#FB7E81",highlight:{border:"#FA0A10",background:"#FFAFB1"}},{border:"#41A906",background:"#7BE141",highlight:{border:"#41A906",background:"#A1EC76"}},{border:"#E129F0",background:"#EB7DF4",highlight:{border:"#E129F0",background:"#F0B3F5"}},{border:"#7C29F0",background:"#AD85E4",highlight:{border:"#7C29F0",background:"#D3BDF0"}},{border:"#C37F00",background:"#FFA807",highlight:{border:"#C37F00",background:"#FFCA66"}},{border:"#4220FB",background:"#6E6EFD",highlight:{border:"#4220FB",background:"#9B9BFD"}},{border:"#FD5A77",background:"#FFC0CB",highlight:{border:"#FD5A77",background:"#FFD1D9"}},{border:"#4AD63A",background:"#C2FABC",highlight:{border:"#4AD63A",background:"#E6FFE3"}}],Groups.prototype.clear=function(){this.groups={},this.groups.length=function(){var t=0;for(var e in this)this.hasOwnProperty(e)&&t++;return t}},Groups.prototype.get=function(t){var e=this.groups[t];if(void 0==e){var i=this.defaultIndex%Groups.DEFAULT.length;this.defaultIndex++,e={},e.color=Groups.DEFAULT[i],this.groups[t]=e}return e},Groups.prototype.add=function(t,e){return this.groups[t]=e,e.color&&(e.color=x.parseColor(e.color)),e},Images=function(){this.images={},this.callback=void 0},Images.prototype.setOnloadCallback=function(t){this.callback=t},Images.prototype.load=function(t){var e=this.images[t];if(void 0==e){var i=this;e=new Image,this.images[t]=e,e.onload=function(){i.callback&&i.callback(this)},e.src=t}return e},M.prototype.setData=function(t){if(t&&t.dot&&(t.nodes||t.edges))throw new SyntaxError('Data must contain either parameter "dot" or parameter pair "nodes" and "edges", but not both.');if(this.setOptions(t&&t.options),t&&t.dot){if(t&&t.dot){var e=N.util.DOTToGraph(t.dot);return this.setData(e),void 0}}else this._setNodes(t&&t.nodes),this._setEdges(t&&t.edges);this.stabilize&&this._doStabilize(),this.start()},M.prototype.setOptions=function(t){if(t){if(void 0!=t.width&&(this.width=t.width),void 0!=t.height&&(this.height=t.height),void 0!=t.stabilize&&(this.stabilize=t.stabilize),void 0!=t.selectable&&(this.selectable=t.selectable),t.edges){for(var e in t.edges)t.edges.hasOwnProperty(e)&&(this.constants.edges[e]=t.edges[e]);void 0!=t.edges.length&&t.nodes&&void 0==t.nodes.distance&&(this.constants.edges.length=t.edges.length,this.constants.nodes.distance=1.25*t.edges.length),t.edges.fontColor||(this.constants.edges.fontColor=t.edges.color),t.edges.dash&&(void 0!=t.edges.dash.length&&(this.constants.edges.dash.length=t.edges.dash.length),void 0!=t.edges.dash.gap&&(this.constants.edges.dash.gap=t.edges.dash.gap),void 0!=t.edges.dash.altLength&&(this.constants.edges.dash.altLength=t.edges.dash.altLength))}if(t.nodes){for(e in t.nodes)t.nodes.hasOwnProperty(e)&&(this.constants.nodes[e]=t.nodes[e]);t.nodes.color&&(this.constants.nodes.color=x.parseColor(t.nodes.color))}if(t.groups)for(var i in t.groups)if(t.groups.hasOwnProperty(i)){var s=t.groups[i];this.groups.add(i,s)}}this.setSize(this.width,this.height),this._setTranslation(this.frame.clientWidth/2,this.frame.clientHeight/2),this._setScale(1)},M.prototype._trigger=function(t,e){O.trigger(this,t,e)},M.prototype._create=function(){for(;this.containerElement.hasChildNodes();)this.containerElement.removeChild(this.containerElement.firstChild);if(this.frame=document.createElement("div"),this.frame.className="graph-frame",this.frame.style.position="relative",this.frame.style.overflow="hidden",this.frame.canvas=document.createElement("canvas"),this.frame.canvas.style.position="relative",this.frame.appendChild(this.frame.canvas),!this.frame.canvas.getContext){var t=document.createElement("DIV");t.style.color="red",t.style.fontWeight="bold",t.style.padding="10px",t.innerHTML="Error: your browser does not support HTML canvas",this.frame.canvas.appendChild(t)}var e=this,i=function(t){e._onMouseDown(t)},s=function(t){e._onMouseMoveTitle(t)},n=function(t){e._onMouseWheel(t)},o=function(t){e._onTouchStart(t)};N.util.addEventListener(this.frame.canvas,"mousedown",i),N.util.addEventListener(this.frame.canvas,"mousemove",s),N.util.addEventListener(this.frame.canvas,"mousewheel",n),N.util.addEventListener(this.frame.canvas,"touchstart",o),this.containerElement.appendChild(this.frame)},M.prototype._onMouseDown=function(t){if(t=t||window.event,this.selectable&&(this.leftButtonDown&&this._onMouseUp(t),this.leftButtonDown=t.which?1==t.which:1==t.button,this.leftButtonDown||this.touchDown)){var e=this;this.onmousemove||(this.onmousemove=function(t){e._onMouseMove(t)},N.util.addEventListener(document,"mousemove",e.onmousemove)),this.onmouseup||(this.onmouseup=function(t){e._onMouseUp(t)},N.util.addEventListener(document,"mouseup",e.onmouseup)),N.util.preventDefault(t),this.startMouseX=D.getPageX(t),this.startMouseY=D.getPageY(t),this.startFrameLeft=N.util.getAbsoluteLeft(this.frame.canvas),this.startFrameTop=N.util.getAbsoluteTop(this.frame.canvas),this.startTranslation=this._getTranslation(),this.ctrlKeyDown=t.ctrlKey,this.shiftKeyDown=t.shiftKey;var i={left:this._xToCanvas(this.startMouseX-this.startFrameLeft),top:this._yToCanvas(this.startMouseY-this.startFrameTop),right:this._xToCanvas(this.startMouseX-this.startFrameLeft),bottom:this._yToCanvas(this.startMouseY-this.startFrameTop)},s=this._getNodesOverlappingWith(i);if(this.startClickedObj=s.length>0?s[s.length-1]:void 0,this.startClickedObj){var n=this.nodes[this.startClickedObj];this.startClickedObj.xFixed=n.xFixed,this.startClickedObj.yFixed=n.yFixed,n.xFixed=!0,n.yFixed=!0,this.ctrlKeyDown&&n.isSelected()?this._unselectNodes([this.startClickedObj]):this._selectNodes([this.startClickedObj],this.ctrlKeyDown),this.moving||this._redraw()}else this.shiftKeyDown||(this.moved=!1)}},M.prototype._onMouseMove=function(t){if(t=t||window.event,this.selectable){var e=D.getPageX(t),i=D.getPageY(t);if(this.mouseX=e,this.mouseY=i,this.startClickedObj){var s=this.nodes[this.startClickedObj];this.startClickedObj.xFixed||(s.x=this._xToCanvas(e-this.startFrameLeft)),this.startClickedObj.yFixed||(s.y=this._yToCanvas(i-this.startFrameTop)),this.moving||(this.moving=!0,this.start())}else if(this.shiftKeyDown){void 0==this.frame.selRect&&(this.frame.selRect=document.createElement("DIV"),this.frame.appendChild(this.frame.selRect),this.frame.selRect.style.position="absolute",this.frame.selRect.style.border="1px dashed red");var n=Math.min(this.startMouseX,e)-this.startFrameLeft,o=Math.min(this.startMouseY,i)-this.startFrameTop,r=Math.max(this.startMouseX,e)-this.startFrameLeft,a=Math.max(this.startMouseY,i)-this.startFrameTop;this.frame.selRect.style.left=n+"px",this.frame.selRect.style.top=o+"px",this.frame.selRect.style.width=r-n+"px",this.frame.selRect.style.height=a-o+"px"}else{var h=e-this.startMouseX,d=i-this.startMouseY;this._setTranslation(this.startTranslation.x+h,this.startTranslation.y+d),this._redraw(),this.moved=!0}N.util.preventDefault(t)}},M.prototype._onMouseUp=function(t){if(t=t||window.event,this.selectable){this.onmousemove&&(N.util.removeEventListener(document,"mousemove",this.onmousemove),this.onmousemove=void 0),this.onmouseup&&(N.util.removeEventListener(document,"mouseup",this.onmouseup),this.onmouseup=void 0),N.util.preventDefault(t);var e=D.getPageX(t)||this.mouseX||0,i=D.getPageY(t)||this.mouseY||0,s=t?t.ctrlKey:window.event.ctrlKey;if(this.startClickedObj){var n=this.nodes[this.startClickedObj];n.xFixed=this.startClickedObj.xFixed,n.yFixed=this.startClickedObj.yFixed}else if(this.shiftKeyDown){var o={left:this._xToCanvas(Math.min(this.startMouseX,e)-this.startFrameLeft),top:this._yToCanvas(Math.min(this.startMouseY,i)-this.startFrameTop),right:this._xToCanvas(Math.max(this.startMouseX,e)-this.startFrameLeft),bottom:this._yToCanvas(Math.max(this.startMouseY,i)-this.startFrameTop)},r=this._getNodesOverlappingWith(o);this._selectNodes(r,s),this.redraw(),this.frame.selRect&&(this.frame.removeChild(this.frame.selRect),this.frame.selRect=void 0)}else this.ctrlKeyDown||this.moved||(this._unselectNodes(),this._redraw());this.leftButtonDown=!1,this.ctrlKeyDown=!1}},M.prototype._onMouseWheel=function(t){t=t||window.event;var e=D.getPageX(t),i=D.getPageY(t),s=0;if(t.wheelDelta?s=t.wheelDelta/120:t.detail&&(s=-t.detail/3),s){var n=s/10;0>s&&(n/=1-n);var o=this._getScale(),r=o*(1+n);.01>r&&(r=.01),r>10&&(r=10);var a=N.util.getAbsoluteLeft(this.frame.canvas),h=N.util.getAbsoluteTop(this.frame.canvas),d=e-a,l=i-h,p=this._getTranslation(),c=r/o,u=(1-c)*d+p.x*c,f=(1-c)*l+p.y*c;this._setScale(r),this._setTranslation(u,f),this._redraw()}N.util.preventDefault(t)},M.prototype._onMouseMoveTitle=function(t){t=t||window.event;var e=D.getPageX(t),i=D.getPageY(t);this.startFrameLeft=this.startFrameLeft||N.util.getAbsoluteLeft(this.frame.canvas),this.startFrameTop=this.startFrameTop||N.util.getAbsoluteTop(this.frame.canvas);var s=e-this.startFrameLeft,n=i-this.startFrameTop;this.popupNode&&this._checkHidePopup(s,n);var o=this,r=function(){o._checkShowPopup(s,n)};this.popupTimer&&clearInterval(this.popupTimer),this.leftButtonDown||(this.popupTimer=setTimeout(r,300))},M.prototype._checkShowPopup=function(t,e){var i,s={left:this._xToCanvas(t),top:this._yToCanvas(e),right:this._xToCanvas(t),bottom:this._yToCanvas(e)},n=this.popupNode;if(void 0==this.popupNode){var o=this.nodes;for(i in o)if(o.hasOwnProperty(i)){var r=o[i];if(void 0!=r.getTitle()&&r.isOverlappingWith(s)){this.popupNode=r;break}}}if(void 0==this.popupNode){var a=this.edges;for(i in a)if(a.hasOwnProperty(i)){var h=a[i];if(h.connected&&void 0!=h.getTitle()&&h.isOverlappingWith(s)){this.popupNode=h;break}}}if(this.popupNode){if(this.popupNode!=n){var d=this;d.popup||(d.popup=new S(d.frame)),d.popup.setPosition(t-3,e-3),d.popup.setText(d.popupNode.getTitle()),d.popup.show()}}else this.popup&&this.popup.hide()},M.prototype._checkHidePopup=function(t,e){var i={left:t,top:e,right:t,bottom:e};this.popupNode&&this.popupNode.isOverlappingWith(i)||(this.popupNode=void 0,this.popup&&this.popup.hide())},M.prototype._onTouchStart=function(t){if(N.util.preventDefault(t),!this.touchDown){this.touchDown=!0;var e=this;this.ontouchmove||(this.ontouchmove=function(t){e._onTouchMove(t)},N.util.addEventListener(document,"touchmove",this.ontouchmove)),this.ontouchend||(this.ontouchend=function(t){e._onTouchEnd(t)},N.util.addEventListener(document,"touchend",this.ontouchend)),this._onMouseDown(t)}},M.prototype._onTouchMove=function(t){N.util.preventDefault(t),this._onMouseMove(t)},M.prototype._onTouchEnd=function(t){N.util.preventDefault(t),this.touchDown=!1,this.ontouchmove&&(N.util.removeEventListener(document,"touchmove",this.ontouchmove),this.ontouchmove=void 0),this.ontouchend&&(N.util.removeEventListener(document,"touchend",this.ontouchend),this.ontouchend=void 0),this._onMouseUp(t)},M.prototype._unselectNodes=function(t,e){var i,s,n,o=!1;if(t)for(i=0,s=t.length;s>i;i++){n=t[i],this.nodes[n].unselect();for(var r=0;this.selection.length>r;)this.selection[r]==n?(this.selection.splice(r,1),o=!0):r++}else if(this.selection&&this.selection.length){for(i=0,s=this.selection.length;s>i;i++)n=this.selection[i],this.nodes[n].unselect(),o=!0;this.selection=[]}return!o||1!=e&&void 0!=e||this._trigger("select"),o},M.prototype._selectNodes=function(t,e){var i,s,n=!1,o=!0;if(t.length!=this.selection.length)o=!1;else for(i=0,s=Math.min(t.length,this.selection.length);s>i;i++)if(t[i]!=this.selection[i]){o=!1;break}if(o)return n;if(void 0==e||0==e){var r=!1;n=this._unselectNodes(void 0,r)}for(i=0,s=t.length;s>i;i++){var a=t[i],h=-1!=this.selection.indexOf(a);h||(this.nodes[a].select(),this.selection.push(a),n=!0)}return n&&this._trigger("select"),n},M.prototype._getNodesOverlappingWith=function(t){var e=this.nodes,i=[];for(var s in e)e.hasOwnProperty(s)&&e[s].isOverlappingWith(t)&&i.push(s);return i},M.prototype.getSelection=function(){return this.selection.concat([])},M.prototype.setSelection=function(t){var e,i,s;if(void 0==t.length)throw"Selection must be an array with ids";for(e=0,i=this.selection.length;i>e;e++)s=this.selection[e],this.nodes[s].unselect();for(this.selection=[],e=0,i=t.length;i>e;e++){s=t[e];var n=this.nodes[s];if(!n)throw new RangeError('Node with id "'+s+'" not found');n.select(),this.selection.push(s)}this.redraw()},M.prototype._updateSelection=function(){for(var t=0;this.selection.length>t;){var e=this.selection[t];this.nodes[e]?t++:this.selection.splice(t,1)}},M.prototype._getConnectionCount=function(t){function e(t){for(var e=[],i=0,s=t.length;s>i;i++)for(var n=t[i],o=n.edges,r=0,a=o.length;a>r;r++){var h=o[r],d=null;h.from==n?d=h.to:h.to==n&&(d=h.from);var l,p;if(d)for(l=0,p=t.length;p>l;l++)if(t[l]==d){d=null;break}if(d)for(l=0,p=e.length;p>l;l++)if(e[l]==d){d=null;break}d&&e.push(d)}return e}void 0==t&&(t=1);var i=[],s=this.nodes;for(var n in s)if(s.hasOwnProperty(n)){for(var o=[s[n]],r=0;t>r;r++)o=o.concat(e(o));i.push(o)}for(var a=[],h=0,d=i.length;d>h;h++)a.push(i[h].length);return a},M.prototype.setSize=function(t,e){this.frame.style.width=t,this.frame.style.height=e,this.frame.canvas.style.width="100%",this.frame.canvas.style.height="100%",this.frame.canvas.width=this.frame.canvas.clientWidth,this.frame.canvas.height=this.frame.canvas.clientHeight},M.prototype._setNodes=function(t){var e=this.nodesData;if(t instanceof o||t instanceof r)this.nodesData=t;else if(t instanceof Array)this.nodesData=new o,this.nodesData.add(t);else{if(t)throw new TypeError("Array or DataSet expected");this.nodesData=new o}if(e&&D.forEach(this.nodesListeners,function(t,i){e.unsubscribe(i,t)}),this.nodes={},this.nodesData){var i=this;D.forEach(this.nodesListeners,function(t,e){i.nodesData.subscribe(e,t)});var s=this.nodesData.getIds();this._addNodes(s)}this._updateSelection()},M.prototype._addNodes=function(t){for(var e,i=0,s=t.length;s>i;i++){e=t[i];var n=this.nodesData.get(e),o=new x(n,this.images,this.groups,this.constants);if(this.nodes[e]=o,!o.isFixed()){var r=2*this.constants.edges.length,a=t.length,h=2*Math.PI*(i/a);o.x=r*Math.cos(h),o.y=r*Math.sin(h),this.moving=!0}}this._reconnectEdges(),this._updateValueRange(this.nodes)},M.prototype._updateNodes=function(t){for(var e=this.nodes,i=this.nodesData,s=0,n=t.length;n>s;s++){var o=t[s],r=e[o],a=i.get(o);r?r.setProperties(a,this.constants):(r=new x(properties,this.images,this.groups,this.constants),e[o]=r,r.isFixed()||(this.moving=!0))}this._reconnectEdges(),this._updateValueRange(e)},M.prototype._removeNodes=function(t){for(var e=this.nodes,i=0,s=t.length;s>i;i++){var n=t[i];delete e[n]}this._reconnectEdges(),this._updateSelection(),this._updateValueRange(e)},M.prototype._setEdges=function(t){var e=this.edgesData;if(t instanceof o||t instanceof r)this.edgesData=t;else if(t instanceof Array)this.edgesData=new o,this.edgesData.add(t);else{if(t)throw new TypeError("Array or DataSet expected");this.edgesData=new o}if(e&&D.forEach(this.edgesListeners,function(t,i){e.unsubscribe(i,t)}),this.edges={},this.edgesData){var i=this;D.forEach(this.edgesListeners,function(t,e){i.edgesData.subscribe(e,t)});var s=this.edgesData.getIds();this._addEdges(s)}this._reconnectEdges()},M.prototype._addEdges=function(t){for(var e=this.edges,i=this.edgesData,s=0,n=t.length;n>s;s++){var o=t[s],r=e[o];r&&r.disconnect();var a=i.get(o);e[o]=new T(a,this,this.constants)}this.moving=!0,this._updateValueRange(e)},M.prototype._updateEdges=function(t){for(var e=this.edges,i=this.edgesData,s=0,n=t.length;n>s;s++){var o=t[s],r=i.get(o),a=e[o];a?(a.disconnect(),a.setProperties(r,this.constants),a.connect()):(a=new T(r,this,this.constants),this.edges[o]=a)}this.moving=!0,this._updateValueRange(e)},M.prototype._removeEdges=function(t){for(var e=this.edges,i=0,s=t.length;s>i;i++){var n=t[i],o=e[n];o&&(o.disconnect(),delete e[n])}this.moving=!0,this._updateValueRange(e)},M.prototype._reconnectEdges=function(){var t,e=this.nodes,i=this.edges;for(t in e)e.hasOwnProperty(t)&&(e[t].edges=[]);for(t in i)if(i.hasOwnProperty(t)){var s=i[t];s.from=null,s.to=null,s.connect()}},M.prototype._updateValueRange=function(t){var e,i=void 0,s=void 0;for(e in t)if(t.hasOwnProperty(e)){var n=t[e].getValue();void 0!==n&&(i=void 0===i?n:Math.min(n,i),s=void 0===s?n:Math.max(n,s))}if(void 0!==i&&void 0!==s)for(e in t)t.hasOwnProperty(e)&&t[e].setValueRange(i,s)},M.prototype.redraw=function(){this.setSize(this.width,this.height),this._redraw()},M.prototype._redraw=function(){var t=this.frame.canvas.getContext("2d"),e=this.frame.canvas.width,i=this.frame.canvas.height;t.clearRect(0,0,e,i),t.save(),t.translate(this.translation.x,this.translation.y),t.scale(this.scale,this.scale),this._drawEdges(t),this._drawNodes(t),t.restore()},M.prototype._setTranslation=function(t,e){void 0===this.translation&&(this.translation={x:0,y:0}),void 0!==t&&(this.translation.x=t),void 0!==e&&(this.translation.y=e)},M.prototype._getTranslation=function(){return{x:this.translation.x,y:this.translation.y}},M.prototype._setScale=function(t){this.scale=t},M.prototype._getScale=function(){return this.scale},M.prototype._xToCanvas=function(t){return(t-this.translation.x)/this.scale},M.prototype._canvasToX=function(t){return t*this.scale+this.translation.x},M.prototype._yToCanvas=function(t){return(t-this.translation.y)/this.scale},M.prototype._canvasToY=function(t){return t*this.scale+this.translation.y},M.prototype._drawNodes=function(t){var e=this.nodes,i=[];for(var s in e)e.hasOwnProperty(s)&&(e[s].isSelected()?i.push(s):e[s].draw(t));for(var n=0,o=i.length;o>n;n++)e[i[n]].draw(t)},M.prototype._drawEdges=function(t){var e=this.edges;for(var i in e)if(e.hasOwnProperty(i)){var s=e[i];s.connected&&e[i].draw(t)}},M.prototype._doStabilize=function(){new Date;for(var t=0,e=this.constants.minVelocity,i=!1;!i&&this.constants.maxIterations>t;)this._calculateForces(),this._discreteStepNodes(),i=!this._isMoving(e),t++;new Date},M.prototype._calculateForces=function(){var t,e,i,s,n,o,r,a,h,d,l,p=this.nodes,c=this.edges,u=.01,f=this.frame.canvas.clientWidth/2,m=this.frame.canvas.clientHeight/2;for(t in p)if(p.hasOwnProperty(t)){var g=p[t];e=f-g.x,i=m-g.y,s=Math.atan2(i,e),o=Math.cos(s)*u,r=Math.sin(s)*u,g._setForce(o,r)}var v=this.constants.nodes.distance,y=10;for(var b in p)if(p.hasOwnProperty(b)){var w=p[b];for(var _ in p)if(p.hasOwnProperty(_)){var x=p[_];e=x.x-w.x,i=x.y-w.y,n=Math.sqrt(e*e+i*i),s=Math.atan2(i,e),a=1/(1+Math.exp((n/v-1)*y)),o=Math.cos(s)*a,r=Math.sin(s)*a,w._addForce(-o,-r),x._addForce(o,r)}}for(t in c)if(c.hasOwnProperty(t)){var T=c[t];T.connected&&(e=T.to.x-T.from.x,i=T.to.y-T.from.y,l=T.length,d=Math.sqrt(e*e+i*i),s=Math.atan2(i,e),h=T.stiffness*(l-d),o=Math.cos(s)*h,r=Math.sin(s)*h,T.from._addForce(-o,-r),T.to._addForce(o,r))}},M.prototype._isMoving=function(t){var e=this.nodes;for(var i in e)if(e.hasOwnProperty(i)&&e[i].isMoving(t))return!0;return!1},M.prototype._discreteStepNodes=function(){var t=this.refreshRate/1e3,e=this.nodes;for(var i in e)e.hasOwnProperty(i)&&e[i].discreteStep(t)},M.prototype.start=function(){if(this.moving){this._calculateForces(),this._discreteStepNodes();var t=this.constants.minVelocity;this.moving=this._isMoving(t)}if(this.moving){if(!this.timer){var e=this;this.timer=window.setTimeout(function(){e.timer=void 0,e.start(),e._redraw()},this.refreshRate)}}else this._redraw()},M.prototype.stop=function(){this.timer&&(window.clearInterval(this.timer),this.timer=void 0)};var N={util:D,events:O,Controller:d,DataSet:o,DataView:r,Range:h,Stack:a,TimeStep:TimeStep,EventBus:n,components:{items:{Item:m,ItemBox:g,ItemPoint:v,ItemRange:y},Component:l,Panel:p,RootPanel:c,ItemSet:f,TimeAxis:u},graph:{Node:x,Edge:T,Popup:S,Groups:Groups,Images:Images},Timeline:_,Graph:M};s!==void 0&&(s=N),i!==void 0&&i.exports!==void 0&&(i.exports=N),"function"==typeof t&&t(function(){return N}),"undefined"!=typeof window&&(window.vis=N),D.loadCss("/* vis.js stylesheet */\n.vis.timeline {\n}\n\n\n.vis.timeline.rootpanel {\n position: relative;\n overflow: hidden;\n\n border: 1px solid #bfbfbf;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n\n.vis.timeline .panel {\n position: absolute;\n overflow: hidden;\n}\n\n\n.vis.timeline .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n.vis.timeline .labels {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n\n padding: 0;\n margin: 0;\n\n border-right: 1px solid #bfbfbf;\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n}\n\n.vis.timeline .labels .label {\n position: absolute;\n left: 0;\n top: 0;\n width: 100%;\n border-bottom: 1px solid #bfbfbf;\n color: #4d4d4d;\n}\n\n.vis.timeline .labels .label .inner {\n display: inline-block;\n padding: 5px;\n}\n\n\n.vis.timeline .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.vis.timeline .background {\n}\n\n.vis.timeline .foreground {\n}\n\n.vis.timeline .itemset-axis {\n position: absolute;\n}\n\n.vis.timeline .groupset .itemset-axis {\n border-top: 1px solid #bfbfbf;\n}\n\n/* TODO: with orientation=='bottom', this will more or less overlap with timeline axis\n.vis.timeline .groupset .itemset-axis:last-child {\n border-top: none;\n}\n*/\n\n\n.vis.timeline .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.vis.timeline .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.vis.timeline .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.vis.timeline .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.vis.timeline .item.box {\n text-align: center;\n border-style: solid;\n border-width: 1px;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.point {\n background: none;\n}\n\n.vis.timeline .dot {\n border: 5px solid #97B0F8;\n position: absolute;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range {\n overflow: hidden;\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .content {\n position: relative;\n display: inline-block;\n}\n\n.vis.timeline .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.vis.timeline .item .content {\n margin: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.vis.timeline .axis {\n position: relative;\n}\n\n.vis.timeline .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.vis.timeline .axis .text.measure {\n position: absolute;\n padding-left: 0;\n padding-right: 0;\n margin-left: 0;\n margin-right: 0;\n visibility: hidden;\n}\n\n.vis.timeline .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.vis.timeline .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.vis.timeline .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.vis.timeline .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n")})()},{moment:2}],2:[function(e,i){(function(){(function(s){function n(t,e){return function(i){return p(t.call(this,i),e)}}function o(t){return function(e){return this.lang().ordinal(t.call(this,e))}}function r(){}function a(t){d(this,t)}function h(t){var e=this._data={},i=t.years||t.year||t.y||0,s=t.months||t.month||t.M||0,n=t.weeks||t.week||t.w||0,o=t.days||t.day||t.d||0,r=t.hours||t.hour||t.h||0,a=t.minutes||t.minute||t.m||0,h=t.seconds||t.second||t.s||0,d=t.milliseconds||t.millisecond||t.ms||0;this._milliseconds=d+1e3*h+6e4*a+36e5*r,this._days=o+7*n,this._months=s+12*i,e.milliseconds=d%1e3,h+=l(d/1e3),e.seconds=h%60,a+=l(h/60),e.minutes=a%60,r+=l(a/60),e.hours=r%24,o+=l(r/24),o+=7*n,e.days=o%30,s+=l(o/30),e.months=s%12,i+=l(s/12),e.years=i}function d(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}function l(t){return 0>t?Math.ceil(t):Math.floor(t)}function p(t,e){for(var i=t+"";e>i.length;)i="0"+i;return i}function c(t,e,i){var s,n=e._milliseconds,o=e._days,r=e._months;n&&t._d.setTime(+t+n*i),o&&t.date(t.date()+o*i),r&&(s=t.date(),t.date(1).month(t.month()+r*i).date(Math.min(s,t.daysInMonth())))}function u(t){return"[object Array]"===Object.prototype.toString.call(t)}function f(t,e){var i,s=Math.min(t.length,e.length),n=Math.abs(t.length-e.length),o=0;for(i=0;s>i;i++)~~t[i]!==~~e[i]&&o++;return o+n}function m(t,e){return e.abbr=t,z[t]||(z[t]=new r),z[t].set(e),z[t]}function g(t){return t?(!z[t]&&H&&e("./lang/"+t),z[t]):I.fn._lang}function v(t){return t.match(/\[.*\]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function y(t){var e,i,s=t.match(j);for(e=0,i=s.length;i>e;e++)s[e]=ae[s[e]]?ae[s[e]]:v(s[e]);return function(n){var o="";for(e=0;i>e;e++)o+="function"==typeof s[e].call?s[e].call(n,t):s[e];return o}}function b(t,e){function i(e){return t.lang().longDateFormat(e)||e}for(var s=5;s--&&U.test(e);)e=e.replace(U,i);return ne[e]||(ne[e]=y(e)),ne[e](t)}function w(t){switch(t){case"DDDD":return q;case"YYYY":return V;case"YYYYY":return X;case"S":case"SS":case"SSS":case"DDD":return B;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":case"a":case"A":return K;case"X":return J;case"Z":case"ZZ":return G;case"T":return Z;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 W;default:return RegExp(t.replace("\\",""))}}function _(t,e,i){var s,n=i._a;switch(t){case"M":case"MM":n[1]=null==e?0:~~e-1;break;case"MMM":case"MMMM":s=g(i._l).monthsParse(e),null!=s?n[1]=s:i._isValid=!1;break;case"D":case"DD":case"DDD":case"DDDD":null!=e&&(n[2]=~~e);break;case"YY":n[0]=~~e+(~~e>68?1900:2e3);break;case"YYYY":case"YYYYY":n[0]=~~e;break;case"a":case"A":i._isPm="pm"===(e+"").toLowerCase();break;case"H":case"HH":case"h":case"hh":n[3]=~~e;break;case"m":case"mm":n[4]=~~e;break;case"s":case"ss":n[5]=~~e;break;case"S":case"SS":case"SSS":n[6]=~~(1e3*("0."+e));break;case"X":i._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":i._useUTC=!0,s=(e+"").match(ee),s&&s[1]&&(i._tzh=~~s[1]),s&&s[2]&&(i._tzm=~~s[2]),s&&"+"===s[0]&&(i._tzh=-i._tzh,i._tzm=-i._tzm)}null==e&&(i._isValid=!1)}function x(t){var e,i,s=[];if(!t._d){for(e=0;7>e;e++)t._a[e]=s[e]=null==t._a[e]?2===e?1:0:t._a[e];s[3]+=t._tzh||0,s[4]+=t._tzm||0,i=new Date(0),t._useUTC?(i.setUTCFullYear(s[0],s[1],s[2]),i.setUTCHours(s[3],s[4],s[5],s[6])):(i.setFullYear(s[0],s[1],s[2]),i.setHours(s[3],s[4],s[5],s[6])),t._d=i}}function T(t){var e,i,s=t._f.match(j),n=t._i;for(t._a=[],e=0;s.length>e;e++)i=(w(s[e]).exec(n)||[])[0],i&&(n=n.slice(n.indexOf(i)+i.length)),ae[s[e]]&&_(s[e],i,t);t._isPm&&12>t._a[3]&&(t._a[3]+=12),t._isPm===!1&&12===t._a[3]&&(t._a[3]=0),x(t)}function S(t){for(var e,i,s,n,o=99;t._f.length;){if(e=d({},t),e._f=t._f.pop(),T(e),i=new a(e),i.isValid()){s=i;break}n=f(e._a,i.toArray()),o>n&&(o=n,s=i)}d(t,s)}function M(t){var e,i=t._i;if(Q.exec(i)){for(t._f="YYYY-MM-DDT",e=0;4>e;e++)if(te[e][1].exec(i)){t._f+=te[e][0];break}G.exec(i)&&(t._f+=" Z"),T(t)}else t._d=new Date(i)}function E(t){var e=t._i,i=R.exec(e);e===s?t._d=new Date:i?t._d=new Date(+i[1]):"string"==typeof e?M(t):u(e)?(t._a=e.slice(0),x(t)):t._d=e instanceof Date?new Date(+e):new Date(e)}function D(t,e,i,s,n){return n.relativeTime(e||1,!!i,t,s)}function C(t,e,i){var s=Y(Math.abs(t)/1e3),n=Y(s/60),o=Y(n/60),r=Y(o/24),a=Y(r/365),h=45>s&&["s",s]||1===n&&["m"]||45>n&&["mm",n]||1===o&&["h"]||22>o&&["hh",o]||1===r&&["d"]||25>=r&&["dd",r]||45>=r&&["M"]||345>r&&["MM",Y(r/30)]||1===a&&["y"]||["yy",a];return h[2]=e,h[3]=t>0,h[4]=i,D.apply({},h)}function L(t,e,i){var s=i-e,n=i-t.day();return n>s&&(n-=7),s-7>n&&(n+=7),Math.ceil(I(t).add("d",n).dayOfYear()/7)}function O(t){var e=t._i,i=t._f;return null===e||""===e?null:("string"==typeof e&&(t._i=e=g().preparse(e)),I.isMoment(e)?(t=d({},e),t._d=new Date(+e._d)):i?u(i)?S(t):T(t):E(t),new a(t))}function N(t,e){I.fn[t]=I.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){I.duration.fn[t]=function(){return this._data[t]}}function k(t,e){I.duration.fn["as"+t]=function(){return+this/e}}for(var I,F,P="2.0.0",Y=Math.round,z={},H=i!==s&&i.exports,R=/^\/?Date\((\-?\d+)/i,j=/(\[[^\[]*\])|(\\)?(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,U=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,W=/\d\d?/,B=/\d{1,3}/,q=/\d{3}/,V=/\d{1,4}/,X=/[+\-]?\d{1,6}/,K=/[0-9]*[a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF]+\s*?[\u0600-\u06FF]+/i,G=/Z|[\+\-]\d\d:?\d\d/i,Z=/T/i,J=/[\+\-]?\d+(\.\d{1,3})?/,Q=/^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,$="YYYY-MM-DDTHH:mm:ssZ",te=[["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/]],ee=/([\+\-]|\d\d)/gi,ie="Month|Date|Hours|Minutes|Seconds|Milliseconds".split("|"),se={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},ne={},oe="DDD w W M D d".split(" "),re="M D H h m s w W".split(" "),ae={M:function(){return this.month()+1},MMM:function(t){return this.lang().monthsShort(this,t)},MMMM:function(t){return this.lang().months(this,t)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(t){return this.lang().weekdaysMin(this,t)},ddd:function(t){return this.lang().weekdaysShort(this,t)},dddd:function(t){return this.lang().weekdays(this,t)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return p(this.year()%100,2)},YYYY:function(){return p(this.year(),4)},YYYYY:function(){return p(this.year(),5)},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 p(~~(this.milliseconds()/10),2)},SSS:function(){return p(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+p(~~(t/60),2)+":"+p(~~t%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+p(~~(10*t/6),4)},X:function(){return this.unix()}};oe.length;)F=oe.pop(),ae[F+"o"]=o(ae[F]);for(;re.length;)F=re.pop(),ae[F+F]=n(ae[F],2);for(ae.DDDD=n(ae.DDD,3),r.prototype={set:function(t){var e,i;for(i in t)e=t[i],"function"==typeof e?this[i]=e:this["_"+i]=e
-},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(t){return this._months[t.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(t){return this._monthsShort[t.month()]},monthsParse:function(t){var e,i,s;for(this._monthsParse||(this._monthsParse=[]),e=0;12>e;e++)if(this._monthsParse[e]||(i=I([2e3,e]),s="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[e]=RegExp(s.replace(".",""),"i")),this._monthsParse[e].test(t))return e},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(t){return this._weekdays[t.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(t){return this._weekdaysShort[t.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(t){return this._weekdaysMin[t.day()]},_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,s){var n=this._relativeTime[i];return"function"==typeof n?n(t,e,i,s):n.replace(/%d/i,t)},pastFuture:function(t,e){var i=this._relativeTime[t>0?"future":"past"];return"function"==typeof i?i(e):i.replace(/%s/i,e)},ordinal:function(t){return this._ordinal.replace("%d",t)},_ordinal:"%d",preparse:function(t){return t},postformat:function(t){return t},week:function(t){return L(t,this._week.dow,this._week.doy)},_week:{dow:0,doy:6}},I=function(t,e,i){return O({_i:t,_f:e,_l:i,_isUTC:!1})},I.utc=function(t,e,i){return O({_useUTC:!0,_isUTC:!0,_l:i,_i:t,_f:e})},I.unix=function(t){return I(1e3*t)},I.duration=function(t,e){var i,s=I.isDuration(t),n="number"==typeof t,o=s?t._data:n?{}:t;return n&&(e?o[e]=t:o.milliseconds=t),i=new h(o),s&&t.hasOwnProperty("_lang")&&(i._lang=t._lang),i},I.version=P,I.defaultFormat=$,I.lang=function(t,e){return t?(e?m(t,e):z[t]||g(t),I.duration.fn._lang=I.fn._lang=g(t),s):I.fn._lang._abbr},I.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),g(t)},I.isMoment=function(t){return t instanceof a},I.isDuration=function(t){return t instanceof h},I.fn=a.prototype={clone:function(){return I(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 I.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?!f(this._a,(this._isUTC?I.utc(this._a):I(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=b(this,t||I.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var i;return i="string"==typeof t?I.duration(+e,t):I.duration(t,e),c(this,i,1),this},subtract:function(t,e){var i;return i="string"==typeof t?I.duration(+e,t):I.duration(t,e),c(this,i,-1),this},diff:function(t,e,i){var s,n,o=this._isUTC?I(t).utc():I(t).local(),r=6e4*(this.zone()-o.zone());return e&&(e=e.replace(/s$/,"")),"year"===e||"month"===e?(s=432e5*(this.daysInMonth()+o.daysInMonth()),n=12*(this.year()-o.year())+(this.month()-o.month()),n+=(this-I(this).startOf("month")-(o-I(o).startOf("month")))/s,"year"===e&&(n/=12)):(s=this-o-r,n="second"===e?s/1e3:"minute"===e?s/6e4:"hour"===e?s/36e5:"day"===e?s/864e5:"week"===e?s/6048e5:s),i?n:l(n)},from:function(t,e){return I.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(I(),t)},calendar:function(){var t=this.diff(I().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()+I(t).startOf(e)},isBefore:function(t,e){return e=e!==s?e:"millisecond",+this.clone().startOf(e)<+I(t).startOf(e)},isSame:function(t,e){return e=e!==s?e:"millisecond",+this.clone().startOf(e)===+I(t).startOf(e)},zone:function(){return this._isUTC?0:this._d.getTimezoneOffset()},daysInMonth:function(){return I.utc([this.year(),this.month()+1,0]).date()},dayOfYear:function(t){var e=Y((I(this).startOf("day")-I(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},isoWeek:function(t){var e=L(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(t){return t===s?this._lang:(this._lang=g(t),this)}},F=0;ie.length>F;F++)N(ie[F].toLowerCase().replace(/s$/,""),ie[F]);N("year","FullYear"),I.fn.days=I.fn.day,I.fn.weeks=I.fn.week,I.fn.isoWeeks=I.fn.isoWeek,I.duration.fn=h.prototype={weeks:function(){return l(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:I.fn.lang};for(F in se)se.hasOwnProperty(F)&&(k(F,se[F]),A(F.toLowerCase()));k("Weeks",6048e5),I.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}}),H&&(i.exports=I),"undefined"==typeof ender&&(this.moment=I),"function"==typeof t&&t.amd&&t("moment",[],function(){return I})}).call(this)})()},{}]},{},[1])(1)});
\ No newline at end of file
+(function(t){if("function"==typeof bootstrap)bootstrap("vis",t);else if("object"==typeof exports)module.exports=t();else if("function"==typeof define&&define.amd)define(t);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeVis=t}else"undefined"!=typeof window?window.vis=t():global.vis=t()})(function(){var t;return function e(t,i,n){function s(r,a){if(!i[r]){if(!t[r]){var h="function"==typeof require&&require;if(!a&&h)return h(r,!0);if(o)return o(r,!0);throw Error("Cannot find module '"+r+"'")}var d=i[r]={exports:{}};t[r][0].call(d.exports,function(e){var i=t[r][1][e];return s(i?i:e)},d,d.exports,e,t,i,n)}return i[r].exports}for(var o="function"==typeof require&&require,r=0;n.length>r;r++)s(n[r]);return s}({1:[function(t,e){(function(t,i){"use strict";function n(){if(!s.READY){s.event.determineEventTypes();for(var t in s.gestures)s.gestures.hasOwnProperty(t)&&s.detection.register(s.gestures[t]);s.event.onTouch(s.DOCUMENT,s.EVENT_MOVE,s.detection.detect),s.event.onTouch(s.DOCUMENT,s.EVENT_END,s.detection.detect),s.READY=!0}}var s=function(t,e){return new s.Instance(t,e||{})};s.defaults={stop_browser_behavior:{userSelect:"none",touchAction:"none",touchCallout:"none",contentZooming:"none",userDrag:"none",tapHighlightColor:"rgba(0,0,0,0)"}},s.HAS_POINTEREVENTS=navigator.pointerEnabled||navigator.msPointerEnabled,s.HAS_TOUCHEVENTS="ontouchstart"in t,s.MOBILE_REGEX=/mobile|tablet|ip(ad|hone|od)|android/i,s.NO_MOUSEEVENTS=s.HAS_TOUCHEVENTS&&navigator.userAgent.match(s.MOBILE_REGEX),s.EVENT_TYPES={},s.DIRECTION_DOWN="down",s.DIRECTION_LEFT="left",s.DIRECTION_UP="up",s.DIRECTION_RIGHT="right",s.POINTER_MOUSE="mouse",s.POINTER_TOUCH="touch",s.POINTER_PEN="pen",s.EVENT_START="start",s.EVENT_MOVE="move",s.EVENT_END="end",s.DOCUMENT=document,s.plugins={},s.READY=!1,s.Instance=function(t,e){var i=this;return n(),this.element=t,this.enabled=!0,this.options=s.utils.extend(s.utils.extend({},s.defaults),e||{}),this.options.stop_browser_behavior&&s.utils.stopDefaultBrowserBehavior(this.element,this.options.stop_browser_behavior),s.event.onTouch(t,s.EVENT_START,function(t){i.enabled&&s.detection.startDetect(i,t)}),this},s.Instance.prototype={on:function(t,e){for(var i=t.split(" "),n=0;i.length>n;n++)this.element.addEventListener(i[n],e,!1);return this},off:function(t,e){for(var i=t.split(" "),n=0;i.length>n;n++)this.element.removeEventListener(i[n],e,!1);return this},trigger:function(t,e){var i=s.DOCUMENT.createEvent("Event");i.initEvent(t,!0,!0),i.gesture=e;var n=this.element;return s.utils.hasParent(e.target,n)&&(n=e.target),n.dispatchEvent(i),this},enable:function(t){return this.enabled=t,this}};var o=null,r=!1,a=!1;s.event={bindDom:function(t,e,i){for(var n=e.split(" "),s=0;n.length>s;s++)t.addEventListener(n[s],i,!1)},onTouch:function(t,e,i){var n=this;this.bindDom(t,s.EVENT_TYPES[e],function(h){var d=h.type.toLowerCase();if(!d.match(/mouse/)||!a){(d.match(/touch/)||d.match(/pointerdown/)||d.match(/mouse/)&&1===h.which)&&(r=!0),d.match(/touch|pointer/)&&(a=!0);var l=0;r&&(s.HAS_POINTEREVENTS&&e!=s.EVENT_END?l=s.PointerEvent.updatePointer(e,h):d.match(/touch/)?l=h.touches.length:a||(l=d.match(/up/)?0:1),l>0&&e==s.EVENT_END?e=s.EVENT_MOVE:l||(e=s.EVENT_END),l||null===o?o=h:h=o,i.call(s.detection,n.collectEventData(t,e,h)),s.HAS_POINTEREVENTS&&e==s.EVENT_END&&(l=s.PointerEvent.updatePointer(e,h))),l||(o=null,r=!1,a=!1,s.PointerEvent.reset())}})},determineEventTypes:function(){var t;t=s.HAS_POINTEREVENTS?s.PointerEvent.getEvents():s.NO_MOUSEEVENTS?["touchstart","touchmove","touchend touchcancel"]:["touchstart mousedown","touchmove mousemove","touchend touchcancel mouseup"],s.EVENT_TYPES[s.EVENT_START]=t[0],s.EVENT_TYPES[s.EVENT_MOVE]=t[1],s.EVENT_TYPES[s.EVENT_END]=t[2]},getTouchList:function(t){return s.HAS_POINTEREVENTS?s.PointerEvent.getTouchList():t.touches?t.touches:[{identifier:1,pageX:t.pageX,pageY:t.pageY,target:t.target}]},collectEventData:function(t,e,i){var n=this.getTouchList(i,e),o=s.POINTER_TOUCH;return(i.type.match(/mouse/)||s.PointerEvent.matchType(s.POINTER_MOUSE,i))&&(o=s.POINTER_MOUSE),{center:s.utils.getCenter(n),timeStamp:(new Date).getTime(),target:i.target,touches:n,eventType:e,pointerType:o,srcEvent:i,preventDefault:function(){this.srcEvent.preventManipulation&&this.srcEvent.preventManipulation(),this.srcEvent.preventDefault&&this.srcEvent.preventDefault()},stopPropagation:function(){this.srcEvent.stopPropagation()},stopDetect:function(){return s.detection.stopDetect()}}}},s.PointerEvent={pointers:{},getTouchList:function(){var t=this,e=[];return Object.keys(t.pointers).sort().forEach(function(i){e.push(t.pointers[i])}),e},updatePointer:function(t,e){return t==s.EVENT_END?this.pointers={}:(e.identifier=e.pointerId,this.pointers[e.pointerId]=e),Object.keys(this.pointers).length},matchType:function(t,e){if(!e.pointerType)return!1;var i={};return i[s.POINTER_MOUSE]=e.pointerType==e.MSPOINTER_TYPE_MOUSE||e.pointerType==s.POINTER_MOUSE,i[s.POINTER_TOUCH]=e.pointerType==e.MSPOINTER_TYPE_TOUCH||e.pointerType==s.POINTER_TOUCH,i[s.POINTER_PEN]=e.pointerType==e.MSPOINTER_TYPE_PEN||e.pointerType==s.POINTER_PEN,i[t]},getEvents:function(){return["pointerdown MSPointerDown","pointermove MSPointerMove","pointerup pointercancel MSPointerUp MSPointerCancel"]},reset:function(){this.pointers={}}},s.utils={extend:function(t,e,n){for(var s in e)t[s]!==i&&n||(t[s]=e[s]);return t},hasParent:function(t,e){for(;t;){if(t==e)return!0;t=t.parentNode}return!1},getCenter:function(t){for(var e=[],i=[],n=0,s=t.length;s>n;n++)e.push(t[n].pageX),i.push(t[n].pageY);return{pageX:(Math.min.apply(Math,e)+Math.max.apply(Math,e))/2,pageY:(Math.min.apply(Math,i)+Math.max.apply(Math,i))/2}},getVelocity:function(t,e,i){return{x:Math.abs(e/t)||0,y:Math.abs(i/t)||0}},getAngle:function(t,e){var i=e.pageY-t.pageY,n=e.pageX-t.pageX;return 180*Math.atan2(i,n)/Math.PI},getDirection:function(t,e){var i=Math.abs(t.pageX-e.pageX),n=Math.abs(t.pageY-e.pageY);return i>=n?t.pageX-e.pageX>0?s.DIRECTION_LEFT:s.DIRECTION_RIGHT:t.pageY-e.pageY>0?s.DIRECTION_UP:s.DIRECTION_DOWN},getDistance:function(t,e){var i=e.pageX-t.pageX,n=e.pageY-t.pageY;return Math.sqrt(i*i+n*n)},getScale:function(t,e){return t.length>=2&&e.length>=2?this.getDistance(e[0],e[1])/this.getDistance(t[0],t[1]):1},getRotation:function(t,e){return t.length>=2&&e.length>=2?this.getAngle(e[1],e[0])-this.getAngle(t[1],t[0]):0},isVertical:function(t){return t==s.DIRECTION_UP||t==s.DIRECTION_DOWN},stopDefaultBrowserBehavior:function(t,e){var i,n=["webkit","khtml","moz","ms","o",""];if(e&&t.style){for(var s=0;n.length>s;s++)for(var o in e)e.hasOwnProperty(o)&&(i=o,n[s]&&(i=n[s]+i.substring(0,1).toUpperCase()+i.substring(1)),t.style[i]=e[o]);"none"==e.userSelect&&(t.onselectstart=function(){return!1})}}},s.detection={gestures:[],current:null,previous:null,stopped:!1,startDetect:function(t,e){this.current||(this.stopped=!1,this.current={inst:t,startEvent:s.utils.extend({},e),lastEvent:!1,name:""},this.detect(e))},detect:function(t){if(this.current&&!this.stopped){t=this.extendEventData(t);for(var e=this.current.inst.options,i=0,n=this.gestures.length;n>i;i++){var o=this.gestures[i];if(!this.stopped&&e[o.name]!==!1&&o.handler.call(o,t,this.current.inst)===!1){this.stopDetect();break}}return this.current&&(this.current.lastEvent=t),t.eventType==s.EVENT_END&&!t.touches.length-1&&this.stopDetect(),t}},stopDetect:function(){this.previous=s.utils.extend({},this.current),this.current=null,this.stopped=!0},extendEventData:function(t){var e=this.current.startEvent;if(e&&(t.touches.length!=e.touches.length||t.touches===e.touches)){e.touches=[];for(var i=0,n=t.touches.length;n>i;i++)e.touches.push(s.utils.extend({},t.touches[i]))}var o=t.timeStamp-e.timeStamp,r=t.center.pageX-e.center.pageX,a=t.center.pageY-e.center.pageY,h=s.utils.getVelocity(o,r,a);return s.utils.extend(t,{deltaTime:o,deltaX:r,deltaY:a,velocityX:h.x,velocityY:h.y,distance:s.utils.getDistance(e.center,t.center),angle:s.utils.getAngle(e.center,t.center),direction:s.utils.getDirection(e.center,t.center),scale:s.utils.getScale(e.touches,t.touches),rotation:s.utils.getRotation(e.touches,t.touches),startEvent:e}),t},register:function(t){var e=t.defaults||{};return e[t.name]===i&&(e[t.name]=!0),s.utils.extend(s.defaults,e,!0),t.index=t.index||1e3,this.gestures.push(t),this.gestures.sort(function(t,e){return t.indexe.index?1:0}),this.gestures}},s.gestures=s.gestures||{},s.gestures.Hold={name:"hold",index:10,defaults:{hold_timeout:500,hold_threshold:1},timer:null,handler:function(t,e){switch(t.eventType){case s.EVENT_START:clearTimeout(this.timer),s.detection.current.name=this.name,this.timer=setTimeout(function(){"hold"==s.detection.current.name&&e.trigger("hold",t)},e.options.hold_timeout);break;case s.EVENT_MOVE:t.distance>e.options.hold_threshold&&clearTimeout(this.timer);break;case s.EVENT_END:clearTimeout(this.timer)}}},s.gestures.Tap={name:"tap",index:100,defaults:{tap_max_touchtime:250,tap_max_distance:10,tap_always:!0,doubletap_distance:20,doubletap_interval:300},handler:function(t,e){if(t.eventType==s.EVENT_END){var i=s.detection.previous,n=!1;if(t.deltaTime>e.options.tap_max_touchtime||t.distance>e.options.tap_max_distance)return;i&&"tap"==i.name&&t.timeStamp-i.lastEvent.timeStamp0&&t.touches.length>e.options.swipe_max_touches)return;(t.velocityX>e.options.swipe_velocity||t.velocityY>e.options.swipe_velocity)&&(e.trigger(this.name,t),e.trigger(this.name+t.direction,t))}}},s.gestures.Drag={name:"drag",index:50,defaults:{drag_min_distance:10,drag_max_touches:1,drag_block_horizontal:!1,drag_block_vertical:!1,drag_lock_to_axis:!1,drag_lock_min_distance:25},triggered:!1,handler:function(t,e){if(s.detection.current.name!=this.name&&this.triggered)return e.trigger(this.name+"end",t),this.triggered=!1,i;if(!(e.options.drag_max_touches>0&&t.touches.length>e.options.drag_max_touches))switch(t.eventType){case s.EVENT_START:this.triggered=!1;break;case s.EVENT_MOVE:if(t.distancet.deltaY?s.DIRECTION_UP:s.DIRECTION_DOWN:0>t.deltaX?s.DIRECTION_LEFT:s.DIRECTION_RIGHT),this.triggered||(e.trigger(this.name+"start",t),this.triggered=!0),e.trigger(this.name,t),e.trigger(this.name+t.direction,t),(e.options.drag_block_vertical&&s.utils.isVertical(t.direction)||e.options.drag_block_horizontal&&!s.utils.isVertical(t.direction))&&t.preventDefault();break;case s.EVENT_END:this.triggered&&e.trigger(this.name+"end",t),this.triggered=!1}}},s.gestures.Transform={name:"transform",index:45,defaults:{transform_min_scale:.01,transform_min_rotation:1,transform_always_block:!1},triggered:!1,handler:function(t,e){if(s.detection.current.name!=this.name&&this.triggered)return e.trigger(this.name+"end",t),this.triggered=!1,i;if(!(2>t.touches.length))switch(e.options.transform_always_block&&t.preventDefault(),t.eventType){case s.EVENT_START:this.triggered=!1;break;case s.EVENT_MOVE:var n=Math.abs(1-t.scale),o=Math.abs(t.rotation);if(e.options.transform_min_scale>n&&e.options.transform_min_rotation>o)return;s.detection.current.name=this.name,this.triggered||(e.trigger(this.name+"start",t),this.triggered=!0),e.trigger(this.name,t),o>e.options.transform_min_rotation&&e.trigger("rotate",t),n>e.options.transform_min_scale&&(e.trigger("pinch",t),e.trigger("pinch"+(1>t.scale?"in":"out"),t));break;case s.EVENT_END:this.triggered&&e.trigger(this.name+"end",t),this.triggered=!1}}},s.gestures.Touch={name:"touch",index:-1/0,defaults:{prevent_default:!1,prevent_mouseevents:!1},handler:function(t,e){return e.options.prevent_mouseevents&&t.pointerType==s.POINTER_MOUSE?(t.stopDetect(),i):(e.options.prevent_default&&t.preventDefault(),t.eventType==s.EVENT_START&&e.trigger(this.name,t),i)}},s.gestures.Release={name:"release",index:1/0,handler:function(t,e){t.eventType==s.EVENT_END&&e.trigger(this.name,t)}},"object"==typeof e&&"object"==typeof e.exports?e.exports=s:(t.Hammer=s,"function"==typeof t.define&&t.define.amd&&t.define("hammer",[],function(){return s}))})(this)},{}],2:[function(e,i){(function(n){function s(t,e){return function(i){return p(t.call(this,i),e)}}function o(t,e){return function(i){return this.lang().ordinal(t.call(this,i),e)}}function r(){}function a(t){d(this,t)}function h(t){var e=t.years||t.year||t.y||0,i=t.months||t.month||t.M||0,n=t.weeks||t.week||t.w||0,s=t.days||t.day||t.d||0,o=t.hours||t.hour||t.h||0,r=t.minutes||t.minute||t.m||0,a=t.seconds||t.second||t.s||0,h=t.milliseconds||t.millisecond||t.ms||0;this._input=t,this._milliseconds=+h+1e3*a+6e4*r+36e5*o,this._days=+s+7*n,this._months=+i+12*e,this._data={},this._bubble()}function d(t,e){for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}function l(t){return 0>t?Math.ceil(t):Math.floor(t)}function p(t,e){for(var i=t+"";e>i.length;)i="0"+i;return i}function c(t,e,i,n){var s,o,r=e._milliseconds,a=e._days,h=e._months;r&&t._d.setTime(+t._d+r*i),(a||h)&&(s=t.minute(),o=t.hour()),a&&t.date(t.date()+a*i),h&&t.month(t.month()+h*i),r&&!n&&H.updateOffset(t),(a||h)&&(t.minute(s),t.hour(o))}function u(t){return"[object Array]"===Object.prototype.toString.call(t)}function f(t,e){var i,n=Math.min(t.length,e.length),s=Math.abs(t.length-e.length),o=0;for(i=0;n>i;i++)~~t[i]!==~~e[i]&&o++;return o+s}function m(t){return t?pe[t]||t.toLowerCase().replace(/(.)s$/,"$1"):t}function g(t,e){return e.abbr=t,V[t]||(V[t]=new r),V[t].set(e),V[t]}function v(t){delete V[t]}function y(t){if(!t)return H.fn._lang;if(!V[t]&&B)try{e("./lang/"+t)}catch(i){return H.fn._lang}return V[t]||H.fn._lang}function b(t){return t.match(/\[.*\]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function w(t){var e,i,n=t.match(X);for(e=0,i=n.length;i>e;e++)n[e]=me[n[e]]?me[n[e]]:b(n[e]);return function(s){var o="";for(e=0;i>e;e++)o+=n[e]instanceof Function?n[e].call(s,t):n[e];return o}}function _(t,e){return e=E(e,t.lang()),ce[e]||(ce[e]=w(e)),ce[e](t)}function E(t,e){function i(t){return e.longDateFormat(t)||t}for(var n=5;n--&&(Z.lastIndex=0,Z.test(t));)t=t.replace(Z,i);return t}function T(t,e){switch(t){case"DDDD":return Q;case"YYYY":return $;case"YYYYY":return te;case"S":case"SS":case"SSS":case"DDD":return J;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return ee;case"a":case"A":return y(e._l)._meridiemParse;case"X":return se;case"Z":case"ZZ":return ie;case"T":return ne;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 K;default:return RegExp(t.replace("\\",""))}}function x(t){var e=(ie.exec(t)||[])[0],i=(e+"").match(he)||["-",0,0],n=+(60*i[1])+~~i[2];return"+"===i[0]?-n:n}function S(t,e,i){var n,s=i._a;switch(t){case"M":case"MM":null!=e&&(s[1]=~~e-1);break;case"MMM":case"MMMM":n=y(i._l).monthsParse(e),null!=n?s[1]=n:i._isValid=!1;break;case"D":case"DD":null!=e&&(s[2]=~~e);break;case"DDD":case"DDDD":null!=e&&(s[1]=0,s[2]=~~e);break;case"YY":s[0]=~~e+(~~e>68?1900:2e3);break;case"YYYY":case"YYYYY":s[0]=~~e;break;case"a":case"A":i._isPm=y(i._l).isPM(e);break;case"H":case"HH":case"h":case"hh":s[3]=~~e;break;case"m":case"mm":s[4]=~~e;break;case"s":case"ss":s[5]=~~e;break;case"S":case"SS":case"SSS":s[6]=~~(1e3*("0."+e));break;case"X":i._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":i._useUTC=!0,i._tzm=x(e)}null==e&&(i._isValid=!1)}function M(t){var e,i,n,s=[];if(!t._d){for(n=C(t),e=0;3>e&&null==t._a[e];++e)t._a[e]=s[e]=n[e];for(;7>e;e++)t._a[e]=s[e]=null==t._a[e]?2===e?1:0:t._a[e];s[3]+=~~((t._tzm||0)/60),s[4]+=~~((t._tzm||0)%60),i=new Date(0),t._useUTC?(i.setUTCFullYear(s[0],s[1],s[2]),i.setUTCHours(s[3],s[4],s[5],s[6])):(i.setFullYear(s[0],s[1],s[2]),i.setHours(s[3],s[4],s[5],s[6])),t._d=i}}function D(t){var e=t._i;t._d||(t._a=[e.years||e.year||e.y,e.months||e.month||e.M,e.days||e.day||e.d,e.hours||e.hour||e.h,e.minutes||e.minute||e.m,e.seconds||e.second||e.s,e.milliseconds||e.millisecond||e.ms],M(t))}function C(t){var e=new Date;return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}function O(t){var e,i,n,s=y(t._l),o=""+t._i;for(n=E(t._f,s).match(X),t._a=[],e=0;n.length>e;e++)i=(T(n[e],t).exec(o)||[])[0],i&&(o=o.slice(o.indexOf(i)+i.length)),me[n[e]]&&S(n[e],i,t);o&&(t._il=o),t._isPm&&12>t._a[3]&&(t._a[3]+=12),t._isPm===!1&&12===t._a[3]&&(t._a[3]=0),M(t)}function N(t){var e,i,n,s,o,r=99;for(s=0;t._f.length>s;s++)e=d({},t),e._f=t._f[s],O(e),i=new a(e),o=f(e._a,i.toArray()),i._il&&(o+=i._il.length),r>o&&(r=o,n=i);d(t,n)}function L(t){var e,i=t._i,n=oe.exec(i);if(n){for(t._f="YYYY-MM-DD"+(n[2]||" "),e=0;4>e;e++)if(ae[e][1].exec(i)){t._f+=ae[e][0];break}ie.exec(i)&&(t._f+=" Z"),O(t)}else t._d=new Date(i)}function k(t){var e=t._i,i=q.exec(e);e===n?t._d=new Date:i?t._d=new Date(+i[1]):"string"==typeof e?L(t):u(e)?(t._a=e.slice(0),M(t)):e instanceof Date?t._d=new Date(+e):"object"==typeof e?D(t):t._d=new Date(e)}function I(t,e,i,n,s){return s.relativeTime(e||1,!!i,t,n)}function A(t,e,i){var n=W(Math.abs(t)/1e3),s=W(n/60),o=W(s/60),r=W(o/24),a=W(r/365),h=45>n&&["s",n]||1===s&&["m"]||45>s&&["mm",s]||1===o&&["h"]||22>o&&["hh",o]||1===r&&["d"]||25>=r&&["dd",r]||45>=r&&["M"]||345>r&&["MM",W(r/30)]||1===a&&["y"]||["yy",a];return h[2]=e,h[3]=t>0,h[4]=i,I.apply({},h)}function P(t,e,i){var n,s=i-e,o=i-t.day();return o>s&&(o-=7),s-7>o&&(o+=7),n=H(t).add("d",o),{week:Math.ceil(n.dayOfYear()/7),year:n.year()}}function F(t){var e=t._i,i=t._f;return null===e||""===e?null:("string"==typeof e&&(t._i=e=y().preparse(e)),H.isMoment(e)?(t=d({},e),t._d=new Date(+e._d)):i?u(i)?N(t):O(t):k(t),new a(t))}function Y(t,e){H.fn[t]=H.fn[t+"s"]=function(t){var i=this._isUTC?"UTC":"";return null!=t?(this._d["set"+i+e](t),H.updateOffset(this),this):this._d["get"+i+e]()}}function R(t){H.duration.fn[t]=function(){return this._data[t]}}function z(t,e){H.duration.fn["as"+t]=function(){return+this/e}}for(var H,U,j="2.2.1",W=Math.round,V={},B=i!==n&&i.exports,q=/^\/?Date\((\-?\d+)/i,G=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)\:(\d+)\.?(\d{3})?/,X=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g,Z=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,K=/\d\d?/,J=/\d{1,3}/,Q=/\d{3}/,$=/\d{1,4}/,te=/[+\-]?\d{1,6}/,ee=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,ie=/Z|[\+\-]\d\d:?\d\d/i,ne=/T/i,se=/[\+\-]?\d+(\.\d{1,3})?/,oe=/^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,re="YYYY-MM-DDTHH:mm:ssZ",ae=[["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/]],he=/([\+\-]|\d\d)/gi,de="Date|Hours|Minutes|Seconds|Milliseconds".split("|"),le={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},pe={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",w:"week",W:"isoweek",M:"month",y:"year"},ce={},ue="DDD w W M D d".split(" "),fe="M D H h m s w W".split(" "),me={M:function(){return this.month()+1},MMM:function(t){return this.lang().monthsShort(this,t)},MMMM:function(t){return this.lang().months(this,t)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(t){return this.lang().weekdaysMin(this,t)},ddd:function(t){return this.lang().weekdaysShort(this,t)},dddd:function(t){return this.lang().weekdays(this,t)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return p(this.year()%100,2)},YYYY:function(){return p(this.year(),4)},YYYYY:function(){return p(this.year(),5)},gg:function(){return p(this.weekYear()%100,2)},gggg:function(){return this.weekYear()},ggggg:function(){return p(this.weekYear(),5)},GG:function(){return p(this.isoWeekYear()%100,2)},GGGG:function(){return this.isoWeekYear()},GGGGG:function(){return p(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return~~(this.milliseconds()/100)},SS:function(){return p(~~(this.milliseconds()/10),2)},SSS:function(){return p(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+p(~~(t/60),2)+":"+p(~~t%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+p(~~(10*t/6),4)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()}};ue.length;)U=ue.pop(),me[U+"o"]=o(me[U],U);for(;fe.length;)U=fe.pop(),me[U+U]=s(me[U],2);for(me.DDDD=s(me.DDD,3),d(r.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=H.utc([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()]},weekdaysParse:function(t){var e,i,n;for(this._weekdaysParse||(this._weekdaysParse=[]),e=0;7>e;e++)if(this._weekdaysParse[e]||(i=H([2e3,1]).day(e),n="^"+this.weekdays(i,"")+"|^"+this.weekdaysShort(i,"")+"|^"+this.weekdaysMin(i,""),this._weekdaysParse[e]=RegExp(n.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(t){var e=this._longDateFormat[t];return!e&&this._longDateFormat[t.toUpperCase()]&&(e=this._longDateFormat[t.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t]=e),e},isPM:function(t){return"p"===(t+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(t,e,i){return t>11?i?"pm":"PM":i?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(t,e){var i=this._calendar[t];return"function"==typeof i?i.apply(e):i},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(t,e,i,n){var s=this._relativeTime[i];return"function"==typeof s?s(t,e,i,n):s.replace(/%d/i,t)},pastFuture:function(t,e){var i=this._relativeTime[t>0?"future":"past"];return"function"==typeof i?i(e):i.replace(/%s/i,e)},ordinal:function(t){return this._ordinal.replace("%d",t)},_ordinal:"%d",preparse:function(t){return t},postformat:function(t){return t},week:function(t){return P(t,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6}}),H=function(t,e,i){return F({_i:t,_f:e,_l:i,_isUTC:!1})},H.utc=function(t,e,i){return F({_useUTC:!0,_isUTC:!0,_l:i,_i:t,_f:e}).utc()},H.unix=function(t){return H(1e3*t)},H.duration=function(t,e){var i,n,s=H.isDuration(t),o="number"==typeof t,r=s?t._input:o?{}:t,a=G.exec(t);return o?e?r[e]=t:r.milliseconds=t:a&&(i="-"===a[1]?-1:1,r={y:0,d:~~a[2]*i,h:~~a[3]*i,m:~~a[4]*i,s:~~a[5]*i,ms:~~a[6]*i}),n=new h(r),s&&t.hasOwnProperty("_lang")&&(n._lang=t._lang),n},H.version=j,H.defaultFormat=re,H.updateOffset=function(){},H.lang=function(t,e){return t?(t=t.toLowerCase(),t=t.replace("_","-"),e?g(t,e):null===e?(v(t),t="en"):V[t]||y(t),H.duration.fn._lang=H.fn._lang=y(t),n):H.fn._lang._abbr},H.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),y(t)},H.isMoment=function(t){return t instanceof a},H.isDuration=function(t){return t instanceof h},d(H.fn=a.prototype,{clone:function(){return H(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){return _(H(this).utc(),"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?!f(this._a,(this._isUTC?H.utc(this._a):H(this._a)).toArray()):!isNaN(this._d.getTime())),!!this._isValid},invalidAt:function(){var t,e=this._a,i=(this._isUTC?H.utc(this._a):H(this._a)).toArray();for(t=6;t>=0&&e[t]===i[t];--t);return t},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(t){var e=_(this,t||H.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var i;return i="string"==typeof t?H.duration(+e,t):H.duration(t,e),c(this,i,1),this},subtract:function(t,e){var i;return i="string"==typeof t?H.duration(+e,t):H.duration(t,e),c(this,i,-1),this},diff:function(t,e,i){var n,s,o=this._isUTC?H(t).zone(this._offset||0):H(t).local(),r=6e4*(this.zone()-o.zone());return e=m(e),"year"===e||"month"===e?(n=432e5*(this.daysInMonth()+o.daysInMonth()),s=12*(this.year()-o.year())+(this.month()-o.month()),s+=(this-H(this).startOf("month")-(o-H(o).startOf("month")))/n,s-=6e4*(this.zone()-H(this).startOf("month").zone()-(o.zone()-H(o).startOf("month").zone()))/n,"year"===e&&(s/=12)):(n=this-o,s="second"===e?n/1e3:"minute"===e?n/6e4:"hour"===e?n/36e5:"day"===e?(n-r)/864e5:"week"===e?(n-r)/6048e5:n),i?s:l(s)},from:function(t,e){return H.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(H(),t)},calendar:function(){var t=this.diff(H().zone(this.zone()).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()+H(t).startOf(e)},isBefore:function(t,e){return e=e!==n?e:"millisecond",+this.clone().startOf(e)<+H(t).startOf(e)},isSame:function(t,e){return e=e!==n?e:"millisecond",+this.clone().startOf(e)===+H(t).startOf(e)},min:function(t){return t=H.apply(null,arguments),this>t?this:t},max:function(t){return t=H.apply(null,arguments),t>this?this:t},zone:function(t){var e=this._offset||0;return null==t?this._isUTC?e:this._d.getTimezoneOffset():("string"==typeof t&&(t=x(t)),16>Math.abs(t)&&(t=60*t),this._offset=t,this._isUTC=!0,e!==t&&c(this,H.duration(e-t,"m"),1,!0),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},hasAlignedHourOffset:function(t){return t=t?H(t).zone():0,0===(this.zone()-t)%60},daysInMonth:function(){return H.utc([this.year(),this.month()+1,0]).date()},dayOfYear:function(t){var e=W((H(this).startOf("day")-H(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},weekYear:function(t){var e=P(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==t?e:this.add("y",t-e)},isoWeekYear:function(t){var e=P(this,1,4).year;return null==t?e:this.add("y",t-e)},week:function(t){var e=this.lang().week(this);return null==t?e:this.add("d",7*(t-e))},isoWeek:function(t){var e=P(this,1,4).week;return null==t?e:this.add("d",7*(t-e))},weekday:function(t){var e=(this._d.getDay()+7-this.lang()._week.dow)%7;return null==t?e:this.add("d",t-e)},isoWeekday:function(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)},get:function(t){return t=m(t),this[t.toLowerCase()]()},set:function(t,e){t=m(t),this[t.toLowerCase()](e)},lang:function(t){return t===n?this._lang:(this._lang=y(t),this)}}),U=0;de.length>U;U++)Y(de[U].toLowerCase().replace(/s$/,""),de[U]);Y("year","FullYear"),H.fn.days=H.fn.day,H.fn.months=H.fn.month,H.fn.weeks=H.fn.week,H.fn.isoWeeks=H.fn.isoWeek,H.fn.toJSON=H.fn.toISOString,d(H.duration.fn=h.prototype,{_bubble:function(){var t,e,i,n,s=this._milliseconds,o=this._days,r=this._months,a=this._data;a.milliseconds=s%1e3,t=l(s/1e3),a.seconds=t%60,e=l(t/60),a.minutes=e%60,i=l(e/60),a.hours=i%24,o+=l(i/24),a.days=o%30,r+=l(o/30),a.months=r%12,n=l(r/12),a.years=n},weeks:function(){return l(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+2592e6*(this._months%12)+31536e6*~~(this._months/12)},humanize:function(t){var e=+this,i=A(e,!t,this.lang());return t&&(i=this.lang().pastFuture(e,i)),this.lang().postformat(i)},add:function(t,e){var i=H.duration(t,e);return this._milliseconds+=i._milliseconds,this._days+=i._days,this._months+=i._months,this._bubble(),this},subtract:function(t,e){var i=H.duration(t,e);return this._milliseconds-=i._milliseconds,this._days-=i._days,this._months-=i._months,this._bubble(),this},get:function(t){return t=m(t),this[t.toLowerCase()+"s"]()},as:function(t){return t=m(t),this["as"+t.charAt(0).toUpperCase()+t.slice(1)+"s"]()},lang:H.fn.lang});for(U in le)le.hasOwnProperty(U)&&(z(U,le[U]),R(U.toLowerCase()));z("Weeks",6048e5),H.duration.fn.asMonths=function(){return(+this-31536e6*this.years())/2592e6+12*this.years()},H.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}}),B&&(i.exports=H),"undefined"==typeof ender&&(this.moment=H),"function"==typeof t&&t.amd&&t("moment",[],function(){return H})}).call(this)},{}],3:[function(e,i,n){function s(){this.subscriptions=[]}function o(t){if(this.id=O.randomUUID(),this.options=t||{},this.data={},this.fieldId=this.options.fieldId||"id",this.convert={},this.options.convert)for(var e in this.options.convert)if(this.options.convert.hasOwnProperty(e)){var i=this.options.convert[e];this.convert[e]="Date"==i||"ISODate"==i||"ASPDate"==i?"Date":i
+}this.subscribers={},this.internalIds={}}function r(t,e){this.id=O.randomUUID(),this.data=null,this.ids={},this.options=e||{},this.fieldId="id",this.subscribers={};var i=this;this.listener=function(){i._onEvent.apply(i,arguments)},this.setData(t)}function a(t,e){this.parent=t,this.options=e||{},this.defaultOptions={order:function(t,e){if(t instanceof y){if(e instanceof y){var i=t.data.end-t.data.start,n=e.data.end-e.data.start;return i-n||t.data.start-e.data.start}return-1}return e instanceof y?1:t.data.start-e.data.start},margin:{item:10}},this.ordered=[]}function h(t){this.id=O.randomUUID(),this.start=0,this.end=0,this.options={min:null,max:null,zoomMin:null,zoomMax:null},this.listeners=[],this.setOptions(t)}function d(){this.id=O.randomUUID(),this.components={},this.repaintTimer=void 0,this.reflowTimer=void 0}function l(){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 p(t,e,i){this.id=O.randomUUID(),this.parent=t,this.depends=e,this.options=i||{}}function c(t,e){this.id=O.randomUUID(),this.container=t,this.options=e||{},this.defaultOptions={autoResize:!0},this.listeners={}}function u(t,e,i){this.id=O.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},lineTop:0},this.options=i||{},this.defaultOptions={orientation:"bottom",showMinorLabels:!0,showMajorLabels:!0},this.conversion=null,this.range=null}function f(t,e,i){this.id=O.randomUUID(),this.parent=t,this.depends=e,this.options=i||{},this.defaultOptions={type:"box",align:"center",orientation:"bottom",margin:{axis:20,item:10},padding:5},this.dom={};var n=this;this.itemsData=null,this.range=null,this.listeners={add:function(t,e,i){i!=n.id&&n._onAdd(e.items)},update:function(t,e,i){i!=n.id&&n._onUpdate(e.items)},remove:function(t,e,i){i!=n.id&&n._onRemove(e.items)}},this.items={},this.queue={},this.stack=new a(this,Object.create(this.options)),this.conversion=null}function m(t,e,i,n){this.parent=t,this.data=e,this.dom=null,this.options=i||{},this.defaultOptions=n||{},this.selected=!1,this.visible=!1,this.top=0,this.left=0,this.width=0,this.height=0}function g(t,e,i,n){this.props={dot:{left:0,top:0,width:0,height:0},line:{top:0,left:0,width:0,height:0}},m.call(this,t,e,i,n)}function v(t,e,i,n){this.props={dot:{top:0,width:0,height:0},content:{height:0,marginLeft:0}},m.call(this,t,e,i,n)}function y(t,e,i,n){this.props={content:{left:0,width:0}},m.call(this,t,e,i,n)}function b(t,e,i){this.id=O.randomUUID(),this.parent=t,this.groupId=e,this.itemset=null,this.options=i||{},this.options.top=0,this.props={label:{width:0,height:0}},this.top=0,this.left=0,this.width=0,this.height=0}function w(t,e,i){this.id=O.randomUUID(),this.parent=t,this.depends=e,this.options=i||{},this.range=null,this.itemsData=null,this.groupsData=null,this.groups={},this.dom={},this.props={labels:{width:0}},this.queue={};var n=this;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)}}}function _(t,e,i){var n=this;if(this.options=O.extend({orientation:"bottom",min:null,max:null,zoomMin:10,zoomMax:31536e10,showMinorLabels:!0,showMajorLabels:!0,autoResize:!1},i),this.controller=new d,!t)throw Error("No container element provided");var s=Object.create(this.options);s.height=function(){return n.options.height?n.options.height:n.timeaxis.height+n.content.height},this.rootPanel=new c(t,s),this.controller.add(this.rootPanel);var o=Object.create(this.options);o.left=function(){return n.labelPanel.width},o.width=function(){return n.rootPanel.width-n.labelPanel.width},o.top=null,o.height=null,this.itemPanel=new p(this.rootPanel,[],o),this.controller.add(this.itemPanel);var r=Object.create(this.options);r.top=null,r.left=null,r.height=null,r.width=function(){return n.content&&"function"==typeof n.content.getLabelsWidth?n.content.getLabelsWidth():0},this.labelPanel=new p(this.rootPanel,[],r),this.controller.add(this.labelPanel);var a=M().hours(0).minutes(0).seconds(0).milliseconds(0);this.range=new h({start:a.clone().add("days",-3).valueOf(),end:a.clone().add("days",4).valueOf()}),this.range.subscribe(this.rootPanel,"move","horizontal"),this.range.subscribe(this.rootPanel,"zoom","horizontal"),this.range.on("rangechange",function(){var t=!0;n.controller.requestReflow(t)}),this.range.on("rangechanged",function(){var t=!0;n.controller.requestReflow(t)});var l=Object.create(s);l.range=this.range,l.left=null,l.top=null,l.width="100%",l.height=null,this.timeaxis=new u(this.itemPanel,[],l),this.timeaxis.setRange(this.range),this.controller.add(this.timeaxis),this.setGroups(null),this.itemsData=null,this.groupsData=null,e&&this.setItems(e)}function E(t,e,i,n){this.selected=!1,this.edges=[],this.group=n.nodes.group,this.fontSize=n.nodes.fontSize,this.fontFace=n.nodes.fontFace,this.fontColor=n.nodes.fontColor,this.color=n.nodes.color,this.id=void 0,this.shape=n.nodes.shape,this.image=n.nodes.image,this.x=0,this.y=0,this.xFixed=!1,this.yFixed=!1,this.radius=n.nodes.radius,this.radiusFixed=!1,this.radiusMin=n.nodes.radiusMin,this.radiusMax=n.nodes.radiusMax,this.imagelist=e,this.grouplist=i,this.setProperties(t,n),this.mass=50,this.fx=0,this.fy=0,this.vx=0,this.vy=0,this.minForce=n.minForce,this.damping=.9}function T(t,e,i){if(!e)throw"No graph provided";this.graph=e,this.widthMin=i.edges.widthMin,this.widthMax=i.edges.widthMax,this.id=void 0,this.fromId=void 0,this.toId=void 0,this.style=i.edges.style,this.title=void 0,this.width=i.edges.width,this.value=void 0,this.length=i.edges.length,this.from=null,this.to=null,this.connected=!1,this.dash=O.extend({},i.edges.dash),this.stiffness=void 0,this.color=i.edges.color,this.widthFixed=!1,this.lengthFixed=!1,this.setProperties(t,i)}function x(t,e,i,n){this.container=t?t:document.body,this.x=0,this.y=0,this.padding=5,void 0!==e&&void 0!==i&&this.setPosition(e,i),void 0!==n&&this.setText(n),this.frame=document.createElement("div");var s=this.frame.style;s.position="absolute",s.visibility="hidden",s.border="1px solid #666",s.color="black",s.padding=this.padding+"px",s.backgroundColor="#FFFFC6",s.borderRadius="3px",s.MozBorderRadius="3px",s.WebkitBorderRadius="3px",s.boxShadow="3px 3px 10px rgba(128, 128, 128, 0.5)",s.whiteSpace="nowrap",this.container.appendChild(this.frame)}function S(t,e,i){this.containerElement=t,this.width="100%",this.height="100%",this.refreshRate=50,this.stabilize=!0,this.selectable=!0,this.constants={nodes:{radiusMin:5,radiusMax:20,radius:5,distance:100,shape:"ellipse",image:void 0,widthMin:16,widthMax:64,fontColor:"black",fontSize:14,fontFace:"arial",color:{border:"#2B7CE9",background:"#97C2FC",highlight:{border:"#2B7CE9",background:"#D2E5FF"}},borderColor:"#2B7CE9",backgroundColor:"#97C2FC",highlightColor:"#D2E5FF",group:void 0},edges:{widthMin:1,widthMax:15,width:1,style:"line",color:"#343434",fontColor:"#343434",fontSize:14,fontFace:"arial",length:100,dash:{length:10,gap:5,altLength:void 0}},minForce:.05,minVelocity:.02,maxIterations:1e3};var n=this;this.nodes={},this.edges={},this.nodesData=null,this.edgesData=null;var s=this;this.nodesListeners={add:function(t,e){s._addNodes(e.items),s.start()},update:function(t,e){s._updateNodes(e.items),s.start()},remove:function(t,e){s._removeNodes(e.items),s.start()}},this.edgesListeners={add:function(t,e){s._addEdges(e.items),s.start()},update:function(t,e){s._updateEdges(e.items),s.start()},remove:function(t,e){s._removeEdges(e.items),s.start()}},this.groups=new Groups,this.images=new Images,this.images.setOnloadCallback(function(){n._redraw()}),this.moving=!1,this.selection=[],this.timer=void 0,this._create(),this.setOptions(i),this.setData(e)}var M="undefined"!=typeof window&&window.moment||e("moment"),D="undefined"!=typeof window&&window.Hammer||e("hammerjs");if(!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(C){}}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,s;if(null==this)throw new TypeError(" this is null or not defined");var o=Object(this),r=o.length>>>0;if("function"!=typeof t)throw new TypeError(t+" is not a function");for(e&&(i=e),n=Array(r),s=0;r>s;){var a,h;s in o&&(a=o[s],h=t.call(i,a,s,o),n[s]=h),s++}return n}),Array.prototype.filter||(Array.prototype.filter=function(t){"use strict";if(null==this)throw new TypeError;var e=Object(this),i=e.length>>>0;if("function"!=typeof t)throw new TypeError;for(var n=[],s=arguments[1],o=0;i>o;o++)if(o in e){var r=e[o];t.call(s,r,o,e)&&n.push(r)}return n}),Object.keys||(Object.keys=function(){var t=Object.prototype.hasOwnProperty,e=!{toString:null}.propertyIsEnumerable("toString"),i=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],n=i.length;return function(s){if("object"!=typeof s&&"function"!=typeof s||null===s)throw new TypeError("Object.keys called on non-object");var o=[];for(var r in s)t.call(s,r)&&o.push(r);if(e)for(var a=0;n>a;a++)t.call(s,i[a])&&o.push(i[a]);return o}}()),Array.isArray||(Array.isArray=function(t){return"[object Array]"===Object.prototype.toString.call(t)}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,n=function(){},s=function(){return i.apply(this instanceof n&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return n.prototype=this.prototype,s.prototype=new n,s}),Object.create||(Object.create=function(t){function e(){}if(arguments.length>1)throw Error("Object.create implementation only accepts the first parameter.");return e.prototype=t,new e}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var e=Array.prototype.slice.call(arguments,1),i=this,n=function(){},s=function(){return i.apply(this instanceof n&&t?this:t,e.concat(Array.prototype.slice.call(arguments)))};return n.prototype=this.prototype,s.prototype=new n,s});var O={};O.isNumber=function(t){return t instanceof Number||"number"==typeof t},O.isString=function(t){return t instanceof String||"string"==typeof t},O.isDate=function(t){if(t instanceof Date)return!0;if(O.isString(t)){var e=N.exec(t);if(e)return!0;if(!isNaN(Date.parse(t)))return!0}return!1},O.isDataTable=function(t){return"undefined"!=typeof google&&google.visualization&&google.visualization.DataTable&&t instanceof google.visualization.DataTable},O.randomUUID=function(){var t=function(){return Math.floor(65536*Math.random()).toString(16)};return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()},O.extend=function(t){for(var e=1,i=arguments.length;i>e;e++){var n=arguments[e];for(var s in n)n.hasOwnProperty(s)&&void 0!==n[s]&&(t[s]=n[s])}return t},O.convert=function(t,e){var i;if(void 0===t)return void 0;if(null===t)return null;if(!e)return t;if("string"!=typeof e&&!(e instanceof String))throw Error("Type must be a string");switch(e){case"boolean":case"Boolean":return Boolean(t);case"number":case"Number":return Number(t.valueOf());case"string":case"String":return t+"";case"Date":if(O.isNumber(t))return new Date(t);if(t instanceof Date)return new Date(t.valueOf());if(M.isMoment(t))return new Date(t.valueOf());if(O.isString(t))return i=N.exec(t),i?new Date(Number(i[1])):M(t).toDate();throw Error("Cannot convert object of type "+O.getType(t)+" to type Date");case"Moment":if(O.isNumber(t))return M(t);if(t instanceof Date)return M(t.valueOf());if(M.isMoment(t))return M.clone();if(O.isString(t))return i=N.exec(t),i?M(Number(i[1])):M(t);throw Error("Cannot convert object of type "+O.getType(t)+" to type Date");case"ISODate":if(O.isNumber(t))return new Date(t);if(t instanceof Date)return t.toISOString();if(M.isMoment(t))return t.toDate().toISOString();if(O.isString(t))return i=N.exec(t),i?new Date(Number(i[1])).toISOString():new Date(t).toISOString();throw Error("Cannot convert object of type "+O.getType(t)+" to type ISODate");case"ASPDate":if(O.isNumber(t))return"/Date("+t+")/";if(t instanceof Date)return"/Date("+t.valueOf()+")/";if(O.isString(t)){i=N.exec(t);var n;return n=i?new Date(Number(i[1])).valueOf():new Date(t).valueOf(),"/Date("+n+")/"}throw Error("Cannot convert object of type "+O.getType(t)+" to type ASPDate");default:throw Error("Cannot convert object of type "+O.getType(t)+' to type "'+e+'"')}};var N=/^\/?Date\((\-?\d+)/i;O.getType=function(t){var e=typeof t;return"object"==e?null==t?"null":t instanceof Boolean?"Boolean":t instanceof Number?"Number":t instanceof String?"String":t instanceof Array?"Array":t instanceof Date?"Date":"Object":"number"==e?"Number":"boolean"==e?"Boolean":"string"==e?"String":e},O.getAbsoluteLeft=function(t){for(var e=document.documentElement,i=document.body,n=t.offsetLeft,s=t.offsetParent;null!=s&&s!=i&&s!=e;)n+=s.offsetLeft,n-=s.scrollLeft,s=s.offsetParent;return n},O.getAbsoluteTop=function(t){for(var e=document.documentElement,i=document.body,n=t.offsetTop,s=t.offsetParent;null!=s&&s!=i&&s!=e;)n+=s.offsetTop,n-=s.scrollTop,s=s.offsetParent;return n},O.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)},O.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)},O.addClassName=function(t,e){var i=t.className.split(" ");-1==i.indexOf(e)&&(i.push(e),t.className=i.join(" "))},O.removeClassName=function(t,e){var i=t.className.split(" "),n=i.indexOf(e);-1!=n&&(i.splice(n,1),t.className=i.join(" "))},O.forEach=function(t,e){var i,n;if(t instanceof Array)for(i=0,n=t.length;n>i;i++)e(t[i],i,t);else for(i in t)t.hasOwnProperty(i)&&e(t[i],i,t)},O.updateProperty=function(t,e,i){return t[e]!==i?(t[e]=i,!0):!1},O.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)},O.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)},O.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},O.stopPropagation=function(t){t||(t=window.event),t.stopPropagation?t.stopPropagation():t.cancelBubble=!0},O.preventDefault=function(t){t||(t=window.event),t.preventDefault?t.preventDefault():t.returnValue=!1},O.option={},O.option.asBoolean=function(t,e){return"function"==typeof t&&(t=t()),null!=t?0!=t:e||null},O.option.asNumber=function(t,e){return"function"==typeof t&&(t=t()),null!=t?Number(t)||e||null:e||null},O.option.asString=function(t,e){return"function"==typeof t&&(t=t()),null!=t?t+"":e||null},O.option.asSize=function(t,e){return"function"==typeof t&&(t=t()),O.isString(t)?t:O.isNumber(t)?t+"px":e||null},O.option.asElement=function(t,e){return"function"==typeof t&&(t=t()),t||e||null},O.loadCss=function(t){if("undefined"!=typeof document){var e=document.createElement("style");e.type="text/css",e.styleSheet?e.styleSheet.cssText=t:e.appendChild(document.createTextNode(t)),document.getElementsByTagName("head")[0].appendChild(e)}};var L={listeners:[],indexOf:function(t){for(var e=this.listeners,i=0,n=this.listeners.length;n>i;i++){var s=e[i];if(s&&s.object==t)return i}return-1},addListener:function(t,e,i){var n=this.indexOf(t),s=this.listeners[n];s||(s={object:t,events:{}},this.listeners.push(s));var o=s.events[e];o||(o=[],s.events[e]=o),-1==o.indexOf(i)&&o.push(i)},removeListener:function(t,e,i){var n=this.indexOf(t),s=this.listeners[n];if(s){var o=s.events[e];o&&(n=o.indexOf(i),-1!=n&&o.splice(n,1),0==o.length&&delete s.events[e]);var r=0,a=s.events;for(var h in a)a.hasOwnProperty(h)&&r++;0==r&&delete this.listeners[n]}},removeAllListeners:function(){this.listeners=[]},trigger:function(t,e,i){var n=this.indexOf(t),s=this.listeners[n];if(s){var o=s.events[e];if(o)for(var r=0,a=o.length;a>r;r++)o[r](i)}}};s.prototype.on=function(t,e,i){var n=t instanceof RegExp?t:RegExp(t.replace("*","\\w+")),s={id:O.randomUUID(),event:t,regexp:n,callback:"function"==typeof e?e:null,target:i};return this.subscriptions.push(s),s.id},s.prototype.off=function(t){for(var e=0;this.subscriptions.length>e;){var i=this.subscriptions[e],n=!0;if(t instanceof Object)for(var s in t)t.hasOwnProperty(s)&&t[s]!==i[s]&&(n=!1);else n=i.id==t;n?this.subscriptions.splice(e,1):e++}},s.prototype.emit=function(t,e,i){for(var n=0;this.subscriptions.length>n;n++){var s=this.subscriptions[n];s.regexp.test(t)&&s.callback&&s.callback(t,e,i)}},o.prototype.subscribe=function(t,e){var i=this.subscribers[t];i||(i=[],this.subscribers[t]=i),i.push({callback:e})},o.prototype.unsubscribe=function(t,e){var i=this.subscribers[t];i&&(this.subscribers[t]=i.filter(function(t){return t.callback!=e}))},o.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["*"]));for(var s=0;n.length>s;s++){var o=n[s];o.callback&&o.callback(t,e,i||null)}},o.prototype.add=function(t,e){var i,n=[],s=this;if(t instanceof Array)for(var o=0,r=t.length;r>o;o++)i=s._addItem(t[o]),n.push(i);else if(O.isDataTable(t))for(var a=this._getColumnNames(t),h=0,d=t.getNumberOfRows();d>h;h++){for(var l={},p=0,c=a.length;c>p;p++){var u=a[p];l[u]=t.getValue(h,p)}i=s._addItem(l),n.push(i)}else{if(!(t instanceof Object))throw Error("Unknown dataType");i=s._addItem(t),n.push(i)}return n.length&&this._trigger("add",{items:n},e),n},o.prototype.update=function(t,e){var i=[],n=[],s=this,o=s.fieldId,r=function(t){var e=t[o];s.data[e]?(e=s._updateItem(t),n.push(e)):(e=s._addItem(t),i.push(e))};if(t instanceof Array)for(var a=0,h=t.length;h>a;a++)r(t[a]);else if(O.isDataTable(t))for(var d=this._getColumnNames(t),l=0,p=t.getNumberOfRows();p>l;l++){for(var c={},u=0,f=d.length;f>u;u++){var m=d[u];c[m]=t.getValue(l,u)}r(c)}else{if(!(t instanceof Object))throw Error("Unknown dataType");r(t)}return i.length&&this._trigger("add",{items:i},e),n.length&&this._trigger("update",{items:n},e),i.concat(n)},o.prototype.get=function(){var t,e,i,n,s=this,o=O.getType(arguments[0]);"String"==o||"Number"==o?(t=arguments[0],i=arguments[1],n=arguments[2]):"Array"==o?(e=arguments[0],i=arguments[1],n=arguments[2]):(i=arguments[0],n=arguments[1]);var r;if(i&&i.type){if(r="DataTable"==i.type?"DataTable":"Array",n&&r!=O.getType(n))throw Error('Type of parameter "data" ('+O.getType(n)+") "+"does not correspond with specified options.type ("+i.type+")");if("DataTable"==r&&!O.isDataTable(n))throw Error('Parameter "data" must be a DataTable when options.type is "DataTable"')}else r=n?"DataTable"==O.getType(n)?"DataTable":"Array":"Array";var a,h,d,l,p=i&&i.convert||this.options.convert,c=i&&i.filter,u=[];if(void 0!=t)a=s._getItem(t,p),c&&!c(a)&&(a=null);else if(void 0!=e)for(d=0,l=e.length;l>d;d++)a=s._getItem(e[d],p),(!c||c(a))&&u.push(a);else for(h in this.data)this.data.hasOwnProperty(h)&&(a=s._getItem(h,p),(!c||c(a))&&u.push(a));if(i&&i.order&&void 0==t&&this._sort(u,i.order),i&&i.fields){var f=i.fields;if(void 0!=t)a=this._filterFields(a,f);else for(d=0,l=u.length;l>d;d++)u[d]=this._filterFields(u[d],f)}if("DataTable"==r){var m=this._getColumnNames(n);if(void 0!=t)s._appendRow(n,m,a);else for(d=0,l=u.length;l>d;d++)s._appendRow(n,m,u[d]);return n}if(void 0!=t)return a;if(n){for(d=0,l=u.length;l>d;d++)n.push(u[d]);return n}return u},o.prototype.getIds=function(t){var e,i,n,s,o,r=this.data,a=t&&t.filter,h=t&&t.order,d=t&&t.convert||this.options.convert,l=[];if(a)if(h){o=[];for(n in r)r.hasOwnProperty(n)&&(s=this._getItem(n,d),a(s)&&o.push(s));for(this._sort(o,h),e=0,i=o.length;i>e;e++)l[e]=o[e][this.fieldId]}else for(n in r)r.hasOwnProperty(n)&&(s=this._getItem(n,d),a(s)&&l.push(s[this.fieldId]));else if(h){o=[];for(n in r)r.hasOwnProperty(n)&&o.push(r[n]);for(this._sort(o,h),e=0,i=o.length;i>e;e++)l[e]=o[e][this.fieldId]}else for(n in r)r.hasOwnProperty(n)&&(s=r[n],l.push(s[this.fieldId]));return l},o.prototype.forEach=function(t,e){var i,n,s=e&&e.filter,o=e&&e.convert||this.options.convert,r=this.data;if(e&&e.order)for(var a=this.get(e),h=0,d=a.length;d>h;h++)i=a[h],n=i[this.fieldId],t(i,n);else for(n in r)r.hasOwnProperty(n)&&(i=this._getItem(n,o),(!s||s(i))&&t(i,n))},o.prototype.map=function(t,e){var i,n=e&&e.filter,s=e&&e.convert||this.options.convert,o=[],r=this.data;for(var a in r)r.hasOwnProperty(a)&&(i=this._getItem(a,s),(!n||n(i))&&o.push(t(i,a)));return e&&e.order&&this._sort(o,e.order),o},o.prototype._filterFields=function(t,e){var i={};for(var n in t)t.hasOwnProperty(n)&&-1!=e.indexOf(n)&&(i[n]=t[n]);return i},o.prototype._sort=function(t,e){if(O.isString(e)){var i=e;t.sort(function(t,e){var n=t[i],s=e[i];return n>s?1:s>n?-1:0})}else{if("function"!=typeof e)throw new TypeError("Order must be a function or a string");t.sort(e)}},o.prototype.remove=function(t,e){var i,n,s,o=[];if(t instanceof Array)for(i=0,n=t.length;n>i;i++)s=this._remove(t[i]),null!=s&&o.push(s);else s=this._remove(t),null!=s&&o.push(s);return o.length&&this._trigger("remove",{items:o},e),o},o.prototype._remove=function(t){if(O.isNumber(t)||O.isString(t)){if(this.data[t])return delete this.data[t],delete this.internalIds[t],t}else if(t instanceof Object){var e=t[this.fieldId];if(e&&this.data[e])return delete this.data[e],delete this.internalIds[e],e}return null},o.prototype.clear=function(t){var e=Object.keys(this.data);return this.data={},this.internalIds={},this._trigger("remove",{items:e},t),e},o.prototype.max=function(t){var e=this.data,i=null,n=null;for(var s in e)if(e.hasOwnProperty(s)){var o=e[s],r=o[t];null!=r&&(!i||r>n)&&(i=o,n=r)}return i},o.prototype.min=function(t){var e=this.data,i=null,n=null;for(var s in e)if(e.hasOwnProperty(s)){var o=e[s],r=o[t];null!=r&&(!i||n>r)&&(i=o,n=r)}return i},o.prototype.distinct=function(t){var e=this.data,i=[],n=this.options.convert[t],s=0;for(var o in e)if(e.hasOwnProperty(o)){for(var r=e[o],a=O.convert(r[t],n),h=!1,d=0;s>d;d++)if(i[d]==a){h=!0;break}h||(i[s]=a,s++)}return i},o.prototype._addItem=function(t){var e=t[this.fieldId];if(void 0!=e){if(this.data[e])throw Error("Cannot add item: item with id "+e+" already exists")}else e=O.randomUUID(),t[this.fieldId]=e,this.internalIds[e]=t;var i={};for(var n in t)if(t.hasOwnProperty(n)){var s=this.convert[n];i[n]=O.convert(t[n],s)}return this.data[e]=i,e},o.prototype._getItem=function(t,e){var i,n,s=this.data[t];if(!s)return null;var o={},r=this.fieldId,a=this.internalIds;if(e)for(i in s)s.hasOwnProperty(i)&&(n=s[i],i==r&&n in a||(o[i]=O.convert(n,e[i])));else for(i in s)s.hasOwnProperty(i)&&(n=s[i],i==r&&n in a||(o[i]=n));return o},o.prototype._updateItem=function(t){var e=t[this.fieldId];if(void 0==e)throw Error("Cannot update item: item has no id (item: "+JSON.stringify(t)+")");var i=this.data[e];if(!i)throw Error("Cannot update item: no item with id "+e+" found");for(var n in t)if(t.hasOwnProperty(n)){var s=this.convert[n];i[n]=O.convert(t[n],s)}return e},o.prototype._getColumnNames=function(t){for(var e=[],i=0,n=t.getNumberOfColumns();n>i;i++)e[i]=t.getColumnId(i)||t.getColumnLabel(i);return e},o.prototype._appendRow=function(t,e,i){for(var n=t.addRow(),s=0,o=e.length;o>s;s++){var r=e[s];t.setValue(n,s,i[r])}},r.prototype.setData=function(t){var e,i,n;if(this.data){this.data.unsubscribe&&this.data.unsubscribe("*",this.listener),e=[];for(var s in this.ids)this.ids.hasOwnProperty(s)&&e.push(s);this.ids={},this._trigger("remove",{items:e})}if(this.data=t,this.data){for(this.fieldId=this.options.fieldId||this.data&&this.data.options&&this.data.options.fieldId||"id",e=this.data.getIds({filter:this.options&&this.options.filter}),i=0,n=e.length;n>i;i++)s=e[i],this.ids[s]=!0;this._trigger("add",{items:e}),this.data.subscribe&&this.data.subscribe("*",this.listener)}},r.prototype.get=function(){var t,e,i,n=this,s=O.getType(arguments[0]);"String"==s||"Number"==s||"Array"==s?(t=arguments[0],e=arguments[1],i=arguments[2]):(e=arguments[0],i=arguments[1]);var o=O.extend({},this.options,e);this.options.filter&&e&&e.filter&&(o.filter=function(t){return n.options.filter(t)&&e.filter(t)});var r=[];return void 0!=t&&r.push(t),r.push(o),r.push(i),this.data&&this.data.get.apply(this.data,r)},r.prototype.getIds=function(t){var e;if(this.data){var i,n=this.options.filter;i=t&&t.filter?n?function(e){return n(e)&&t.filter(e)}:t.filter:n,e=this.data.getIds({filter:i,order:t&&t.order})}else e=[];return e},r.prototype._onEvent=function(t,e,i){var n,s,o,r,a=e&&e.items,h=this.data,d=[],l=[],p=[];if(a&&h){switch(t){case"add":for(n=0,s=a.length;s>n;n++)o=a[n],r=this.get(o),r&&(this.ids[o]=!0,d.push(o));break;case"update":for(n=0,s=a.length;s>n;n++)o=a[n],r=this.get(o),r?this.ids[o]?l.push(o):(this.ids[o]=!0,d.push(o)):this.ids[o]&&(delete this.ids[o],p.push(o));break;case"remove":for(n=0,s=a.length;s>n;n++)o=a[n],this.ids[o]&&(delete this.ids[o],p.push(o))}d.length&&this._trigger("add",{items:d},i),l.length&&this._trigger("update",{items:l},i),p.length&&this._trigger("remove",{items:p},i)}},r.prototype.subscribe=o.prototype.subscribe,r.prototype.unsubscribe=o.prototype.unsubscribe,r.prototype._trigger=o.prototype._trigger,TimeStep=function(t,e,i){this.current=new Date,this._start=new Date,this._end=new Date,this.autoScale=!0,this.scale=TimeStep.SCALE.DAY,this.step=1,this.setRange(t,e,i)},TimeStep.SCALE={MILLISECOND:1,SECOND:2,MINUTE:3,HOUR:4,DAY:5,WEEKDAY:6,MONTH:7,YEAR:8},TimeStep.prototype.setRange=function(t,e,i){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,s=36e5,o=6e4,r=1e3,a=1;1e3*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1e3),500*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=500),100*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=100),50*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=50),10*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=10),5*e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=5),e>t&&(this.scale=TimeStep.SCALE.YEAR,this.step=1),3*i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=3),i>t&&(this.scale=TimeStep.SCALE.MONTH,this.step=1),5*n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=5),2*n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=2),n>t&&(this.scale=TimeStep.SCALE.DAY,this.step=1),n/2>t&&(this.scale=TimeStep.SCALE.WEEKDAY,this.step=1),4*s>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=4),s>t&&(this.scale=TimeStep.SCALE.HOUR,this.step=1),15*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=15),10*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=10),5*o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=5),o>t&&(this.scale=TimeStep.SCALE.MINUTE,this.step=1),15*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=15),10*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=10),5*r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=5),r>t&&(this.scale=TimeStep.SCALE.SECOND,this.step=1),200*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=200),100*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=100),50*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=50),10*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=10),5*a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=5),a>t&&(this.scale=TimeStep.SCALE.MILLISECOND,this.step=1)
+}},TimeStep.prototype.snap=function(t){if(this.scale==TimeStep.SCALE.YEAR){var e=t.getFullYear()+Math.round(t.getMonth()/12);t.setFullYear(Math.round(e/this.step)*this.step),t.setMonth(0),t.setDate(0),t.setHours(0),t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MONTH)t.getDate()>15?(t.setDate(1),t.setMonth(t.getMonth()+1)):t.setDate(1),t.setHours(0),t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0);else if(this.scale==TimeStep.SCALE.DAY||this.scale==TimeStep.SCALE.WEEKDAY){switch(this.step){case 5:case 2:t.setHours(24*Math.round(t.getHours()/24));break;default:t.setHours(12*Math.round(t.getHours()/12))}t.setMinutes(0),t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.HOUR){switch(this.step){case 4:t.setMinutes(60*Math.round(t.getMinutes()/60));break;default:t.setMinutes(30*Math.round(t.getMinutes()/30))}t.setSeconds(0),t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.MINUTE){switch(this.step){case 15:case 10:t.setMinutes(5*Math.round(t.getMinutes()/5)),t.setSeconds(0);break;case 5:t.setSeconds(60*Math.round(t.getSeconds()/60));break;default:t.setSeconds(30*Math.round(t.getSeconds()/30))}t.setMilliseconds(0)}else if(this.scale==TimeStep.SCALE.SECOND)switch(this.step){case 15:case 10:t.setSeconds(5*Math.round(t.getSeconds()/5)),t.setMilliseconds(0);break;case 5:t.setMilliseconds(1e3*Math.round(t.getMilliseconds()/1e3));break;default:t.setMilliseconds(500*Math.round(t.getMilliseconds()/500))}else if(this.scale==TimeStep.SCALE.MILLISECOND){var i=this.step>5?this.step/2:1;t.setMilliseconds(Math.round(t.getMilliseconds()/i)*i)}},TimeStep.prototype.isMajor=function(){switch(this.scale){case TimeStep.SCALE.MILLISECOND:return 0==this.current.getMilliseconds();case TimeStep.SCALE.SECOND:return 0==this.current.getSeconds();case TimeStep.SCALE.MINUTE:return 0==this.current.getHours()&&0==this.current.getMinutes();case TimeStep.SCALE.HOUR:return 0==this.current.getHours();case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return 1==this.current.getDate();case TimeStep.SCALE.MONTH:return 0==this.current.getMonth();case TimeStep.SCALE.YEAR:return!1;default:return!1}},TimeStep.prototype.getLabelMinor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return M(t).format("SSS");case TimeStep.SCALE.SECOND:return M(t).format("s");case TimeStep.SCALE.MINUTE:return M(t).format("HH:mm");case TimeStep.SCALE.HOUR:return M(t).format("HH:mm");case TimeStep.SCALE.WEEKDAY:return M(t).format("ddd D");case TimeStep.SCALE.DAY:return M(t).format("D");case TimeStep.SCALE.MONTH:return M(t).format("MMM");case TimeStep.SCALE.YEAR:return M(t).format("YYYY");default:return""}},TimeStep.prototype.getLabelMajor=function(t){switch(void 0==t&&(t=this.current),this.scale){case TimeStep.SCALE.MILLISECOND:return M(t).format("HH:mm:ss");case TimeStep.SCALE.SECOND:return M(t).format("D MMMM HH:mm");case TimeStep.SCALE.MINUTE:case TimeStep.SCALE.HOUR:return M(t).format("ddd D MMMM");case TimeStep.SCALE.WEEKDAY:case TimeStep.SCALE.DAY:return M(t).format("MMMM YYYY");case TimeStep.SCALE.MONTH:return M(t).format("YYYY");case TimeStep.SCALE.YEAR:return"";default:return""}},a.prototype.setOptions=function(t){O.extend(this.options,t)},a.prototype.update=function(){this._order(),this._stack()},a.prototype._order=function(){var t=this.parent.items;if(!t)throw Error("Cannot stack items: parent does not contain items");var e=[],i=0;O.forEach(t,function(t){t.visible&&(e[i]=t,i++)});var n=this.options.order||this.defaultOptions.order;if("function"!=typeof n)throw Error("Option order must be a function");e.sort(n),this.ordered=e},a.prototype._stack=function(){var t,e,i,n=this.ordered,s=this.options,o=s.orientation||this.defaultOptions.orientation,r="top"==o;for(i=s.margin&&void 0!==s.margin.item?s.margin.item:this.defaultOptions.margin.item,t=0,e=n.length;e>t;t++){var a=n[t],h=null;do h=this.checkOverlap(n,t,0,t-1,i),null!=h&&(a.top=r?h.top+h.height+i:h.top-a.height-i);while(h)}},a.prototype.checkOverlap=function(t,e,i,n,s){for(var o=this.collision,r=t[e],a=n;a>=i;a--){var h=t[a];if(o(r,h,s)&&a!=e)return h}return null},a.prototype.collision=function(t,e,i){return t.left-ie.left&&t.top-ie.top},h.prototype.setOptions=function(t){O.extend(this.options,t),(null!=t.start||null!=t.end)&&this.setRange(t.start,t.end)},h.prototype.subscribe=function(t,e,i){var n,s=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){s._onMouseDown(t,n)},params:{}},t.on("mousedown",n.callback),s.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){s._onMouseWheel(t,n)},params:{}},t.on("mousewheel",n.callback),s.listeners.push(n)}},h.prototype.on=function(t,e){L.addListener(this,t,e)},h.prototype._trigger=function(t){L.trigger(this,t,{start:this.start,end:this.end})},h.prototype.setRange=function(t,e){var i=this._applyRange(t,e);i&&(this._trigger("rangechange"),this._trigger("rangechanged"))},h.prototype._applyRange=function(t,e){var i,n=null!=t?O.convert(t,"Number"):this.start,s=null!=e?O.convert(e,"Number"):this.end;if(isNaN(n))throw Error('Invalid start "'+t+'"');if(isNaN(s))throw Error('Invalid end "'+e+'"');if(n>s&&(s=n),null!=this.options.min){var o=this.options.min.valueOf();o>n&&(i=o-n,n+=i,s+=i)}if(null!=this.options.max){var r=this.options.max.valueOf();s>r&&(i=s-r,n-=i,s-=i)}if(null!=this.options.zoomMin){var a=this.options.zoomMin.valueOf();0>a&&(a=0),a>s-n&&(this.end-this.start>a?(i=a-(s-n),n-=i/2,s+=i/2):(n=this.start,s=this.end))}if(null!=this.options.zoomMax){var h=this.options.zoomMax.valueOf();0>h&&(h=0),s-n>h&&(h>this.end-this.start?(i=s-n-h,n+=i/2,s-=i/2):(n=this.start,s=this.end))}var d=this.start!=n||this.end!=s;return this.start=n,this.end=s,d},h.prototype.getRange=function(){return{start:this.start,end:this.end}},h.prototype.conversion=function(t){return this.start,this.end,h.conversion(this.start,this.end,t)},h.conversion=function(t,e,i){return 0!=i&&0!=e-t?{offset:t,factor:i/(e-t)}:{offset:0,factor:1}},h.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=O.getPageX(t),i.mouseY=O.getPageY(t),i.previousLeft=0,i.previousOffset=0,i.moved=!1,i.start=this.start,i.end=this.end;var s=e.component.frame;s&&(s.style.cursor="move");var o=this;i.onMouseMove||(i.onMouseMove=function(t){o._onMouseMove(t,e)},O.addEventListener(document,"mousemove",i.onMouseMove)),i.onMouseUp||(i.onMouseUp=function(t){o._onMouseUp(t,e)},O.addEventListener(document,"mouseup",i.onMouseUp)),O.preventDefault(t)}},h.prototype._onMouseMove=function(t,e){t=t||window.event;var i=e.params,n=O.getPageX(t),s=O.getPageY(t);void 0==i.mouseX&&(i.mouseX=n),void 0==i.mouseY&&(i.mouseY=s);var o=n-i.mouseX,r=s-i.mouseY,a="horizontal"==e.direction?o:r;Math.abs(a)>=1&&(i.moved=!0);var h=i.end-i.start,d="horizontal"==e.direction?e.component.width:e.component.height,l=-a/d*h;this._applyRange(i.start+l,i.end+l),this._trigger("rangechange"),O.preventDefault(t)},h.prototype._onMouseUp=function(t,e){t=t||window.event;var i=e.params;e.component.frame&&(e.component.frame.style.cursor="auto"),i.onMouseMove&&(O.removeEventListener(document,"mousemove",i.onMouseMove),i.onMouseMove=null),i.onMouseUp&&(O.removeEventListener(document,"mouseup",i.onMouseUp),i.onMouseUp=null),i.moved&&this._trigger("rangechanged")},h.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,s=function(){var s=i/5,o=null,r=e.component.frame;if(r){var a,h;if("horizontal"==e.direction){a=e.component.width,h=n.conversion(a);var d=O.getAbsoluteLeft(r),l=O.getPageX(t);o=(l-d)/h.factor+h.offset}else{a=e.component.height,h=n.conversion(a);var p=O.getAbsoluteTop(r),c=O.getPageY(t);o=(p+a-c-p)/h.factor+h.offset}}n.zoom(s,o)};s()}O.preventDefault(t)},h.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,s=this.start-i*t,o=this.end-n*t;this.setRange(s,o)},h.prototype.move=function(t){var e=this.end-this.start,i=this.start+e*t,n=this.end+e*t;this.start=i,this.end=n},h.prototype.moveTo=function(t){var e=(this.start+this.end)/2,i=e-t,n=this.start-i,s=this.end-i;this.setRange(n,s)},d.prototype.add=function(t){if(void 0==t.id)throw Error("Component has no field id");if(!(t instanceof l||t instanceof d))throw new TypeError("Component must be an instance of prototype Component or Controller");t.controller=this,this.components[t.id]=t},d.prototype.remove=function(t){var e;for(e in this.components)if(this.components.hasOwnProperty(e)&&(e==t||this.components[e]==t))break;e&&delete this.components[e]},d.prototype.requestReflow=function(t){if(t)this.reflow();else if(!this.reflowTimer){var e=this;this.reflowTimer=setTimeout(function(){e.reflowTimer=void 0,e.reflow()},0)}},d.prototype.requestRepaint=function(t){if(t)this.repaint();else if(!this.repaintTimer){var e=this;this.repaintTimer=setTimeout(function(){e.repaintTimer=void 0,e.repaint()},0)}},d.prototype.repaint=function(){function t(n,s){s 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[s]=!0)}var e=!1;this.repaintTimer&&(clearTimeout(this.repaintTimer),this.repaintTimer=void 0);var i={};O.forEach(this.components,t),e&&this.reflow()},d.prototype.reflow=function(){function t(n,s){s 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[s]=!0)}var e=!1;this.reflowTimer&&(clearTimeout(this.reflowTimer),this.reflowTimer=void 0);var i={};O.forEach(this.components,t),e&&this.repaint()},l.prototype.setOptions=function(t){t&&(O.extend(this.options,t),this.controller&&(this.requestRepaint(),this.requestReflow()))},l.prototype.getOption=function(t){var e;return this.options&&(e=this.options[t]),void 0===e&&this.defaultOptions&&(e=this.defaultOptions[t]),e},l.prototype.getContainer=function(){return null},l.prototype.getFrame=function(){return this.frame},l.prototype.repaint=function(){return!1},l.prototype.reflow=function(){return!1},l.prototype.hide=function(){return this.frame&&this.frame.parentNode?(this.frame.parentNode.removeChild(this.frame),!0):!1},l.prototype.show=function(){return this.frame&&this.frame.parentNode?!1:this.repaint()},l.prototype.requestRepaint=function(){if(!this.controller)throw Error("Cannot request a repaint: no controller configured");this.controller.requestRepaint()},l.prototype.requestReflow=function(){if(!this.controller)throw Error("Cannot request a reflow: no controller configured");this.controller.requestReflow()},p.prototype=new l,p.prototype.setOptions=l.prototype.setOptions,p.prototype.getContainer=function(){return this.frame},p.prototype.repaint=function(){var t=0,e=O.updateProperty,i=O.option.asSize,n=this.options,s=this.frame;if(!s){s=document.createElement("div"),s.className="panel";var o=n.className;o&&("function"==typeof o?O.addClassName(s,o()+""):O.addClassName(s,o+"")),this.frame=s,t+=1}if(!s.parentNode){if(!this.parent)throw Error("Cannot repaint panel: no parent attached");var r=this.parent.getContainer();if(!r)throw Error("Cannot repaint panel: parent has no container element");r.appendChild(s),t+=1}return t+=e(s.style,"top",i(n.top,"0px")),t+=e(s.style,"left",i(n.left,"0px")),t+=e(s.style,"width",i(n.width,"100%")),t+=e(s.style,"height",i(n.height,"100%")),t>0},p.prototype.reflow=function(){var t=0,e=O.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},c.prototype=new p,c.prototype.setOptions=l.prototype.setOptions,c.prototype.repaint=function(){var t=0,e=O.updateProperty,i=O.option.asSize,n=this.options,s=this.frame;if(!s){s=document.createElement("div"),s.className="vis timeline rootpanel";var o=n.className;o&&O.addClassName(s,O.option.asString(o)),this.frame=s,t+=1}if(!s.parentNode){if(!this.container)throw Error("Cannot repaint root panel: no container attached");this.container.appendChild(s),t+=1}return t+=e(s.style,"top",i(n.top,"0px")),t+=e(s.style,"left",i(n.left,"0px")),t+=e(s.style,"width",i(n.width,"100%")),t+=e(s.style,"height",i(n.height,"100%")),this._updateEventEmitters(),this._updateWatch(),t>0},c.prototype.reflow=function(){var t=0,e=O.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},c.prototype._updateWatch=function(){var t=this.getOption("autoResize");t?this._watch():this._unwatch()},c.prototype._watch=function(){var t=this;this._unwatch();var e=function(){var e=t.getOption("autoResize");return e?(t.frame&&(t.frame.clientWidth!=t.width||t.frame.clientHeight!=t.height)&&t.requestReflow(),void 0):(t._unwatch(),void 0)};O.addEventListener(window,"resize",e),this.watchTimer=setInterval(e,1e3)},c.prototype._unwatch=function(){this.watchTimer&&(clearInterval(this.watchTimer),this.watchTimer=void 0)},c.prototype.on=function(t,e){var i=this.listeners[t];i||(i=[],this.listeners[t]=i),i.push(e),this._updateEventEmitters()},c.prototype._updateEventEmitters=function(){if(this.listeners){var t=this;O.forEach(this.listeners,function(e,i){if(t.emitters||(t.emitters={}),!(i in t.emitters)){var n=t.frame;if(n){var s=function(t){e.forEach(function(e){e(t)})};t.emitters[i]=s,O.addEventListener(n,i,s)}}})}},u.prototype=new l,u.prototype.setOptions=l.prototype.setOptions,u.prototype.setRange=function(t){if(!(t instanceof h||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},u.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.factor+e.offset)},u.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.factor},u.prototype.repaint=function(){var t=0,e=O.updateProperty,i=O.option.asSize,n=this.options,s=this.getOption("orientation"),o=this.props,r=this.step,a=this.frame;if(a||(a=document.createElement("div"),this.frame=a,t+=1),a.className="axis "+s,!a.parentNode){if(!this.parent)throw Error("Cannot repaint time axis: no parent attached");var h=this.parent.getContainer();if(!h)throw Error("Cannot repaint time axis: parent has no container element");h.appendChild(a),t+=1}var d=a.parentNode;if(d){var l=a.nextSibling;d.removeChild(a);var p="bottom"==s&&this.props.parentHeight&&this.height?this.props.parentHeight-this.height+"px":"0px";if(t+=e(a.style,"top",i(n.top,p)),t+=e(a.style,"left",i(n.left,"0px")),t+=e(a.style,"width",i(n.width,"100%")),t+=e(a.style,"height",i(n.height,this.height+"px")),this._repaintMeasureChars(),this.step){this._repaintStart(),r.first();for(var c=void 0,u=0;r.hasNext()&&1e3>u;){u++;var f=r.getCurrent(),m=this.toScreen(f),g=r.isMajor();this.getOption("showMinorLabels")&&this._repaintMinorText(m,r.getLabelMinor()),g&&this.getOption("showMajorLabels")?(m>0&&(void 0==c&&(c=m),this._repaintMajorText(m,r.getLabelMajor())),this._repaintMajorLine(m)):this._repaintMinorLine(m),r.next()}if(this.getOption("showMajorLabels")){var v=this.toTime(0),y=r.getLabelMajor(v),b=y.length*(o.majorCharWidth||10)+10;(void 0==c||c>b)&&this._repaintMajorText(0,y)}this._repaintEnd()}this._repaintLine(),l?d.insertBefore(a,l):d.appendChild(a)}return t>0},u.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=[]},u.prototype._repaintEnd=function(){O.forEach(this.dom.redundant,function(t){for(;t.length;){var e=t.pop();e&&e.parentNode&&e.parentNode.removeChild(e)}})},u.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"},u.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"},u.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"},u.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"},u.prototype._repaintLine=function(){var t=this.dom.line,e=this.frame;this.options,this.getOption("showMinorLabels")||this.getOption("showMajorLabels")?(t?(e.removeChild(t),e.appendChild(t)):(t=document.createElement("div"),t.className="grid horizontal major",e.appendChild(t),this.dom.line=t),t.style.top=this.props.lineTop+"px"):t&&axis.parentElement&&(e.removeChild(axis.line),delete this.dom.line)},u.prototype._repaintMeasureChars=function(){var t,e=this.dom;if(!e.measureCharMinor){t=document.createTextNode("0");var i=document.createElement("DIV");i.className="text minor measure",i.appendChild(t),this.frame.appendChild(i),e.measureCharMinor=i}if(!e.measureCharMajor){t=document.createTextNode("0");var n=document.createElement("DIV");n.className="text major measure",n.appendChild(t),this.frame.appendChild(n),e.measureCharMajor=n}},u.prototype.reflow=function(){var t=0,e=O.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 s=this.props,o=this.getOption("showMinorLabels"),r=this.getOption("showMajorLabels"),a=this.dom.measureCharMinor,h=this.dom.measureCharMajor;a&&(s.minorCharHeight=a.clientHeight,s.minorCharWidth=a.clientWidth),h&&(s.majorCharHeight=h.clientHeight,s.majorCharWidth=h.clientWidth);var d=i.parentNode?i.parentNode.offsetHeight:0;switch(d!=s.parentHeight&&(s.parentHeight=d,t+=1),this.getOption("orientation")){case"bottom":s.minorLabelHeight=o?s.minorCharHeight:0,s.majorLabelHeight=r?s.majorCharHeight:0,s.minorLabelTop=0,s.majorLabelTop=s.minorLabelTop+s.minorLabelHeight,s.minorLineTop=-this.top,s.minorLineHeight=Math.max(this.top+s.majorLabelHeight,0),s.minorLineWidth=1,s.majorLineTop=-this.top,s.majorLineHeight=Math.max(this.top+s.minorLabelHeight+s.majorLabelHeight,0),s.majorLineWidth=1,s.lineTop=0;break;case"top":s.minorLabelHeight=o?s.minorCharHeight:0,s.majorLabelHeight=r?s.majorCharHeight:0,s.majorLabelTop=0,s.minorLabelTop=s.majorLabelTop+s.majorLabelHeight,s.minorLineTop=s.minorLabelTop,s.minorLineHeight=Math.max(d-s.majorLabelHeight-this.top),s.minorLineWidth=1,s.majorLineTop=0,s.majorLineHeight=Math.max(d-this.top),s.majorLineWidth=1,s.lineTop=s.majorLabelHeight+s.minorLabelHeight;break;default:throw Error('Unkown orientation "'+this.getOption("orientation")+'"')}var l=s.minorLabelHeight+s.majorLabelHeight;t+=e(this,"width",i.offsetWidth),t+=e(this,"height",l),this._updateConversion();var p=O.convert(n.start,"Date"),c=O.convert(n.end,"Date"),u=this.toTime(5*(s.minorCharWidth||10))-this.toTime(0);this.step=new TimeStep(p,c,u),t+=e(s.range,"start",p.valueOf()),t+=e(s.range,"end",c.valueOf()),t+=e(s.range,"minimumStep",u.valueOf())}return t>0},u.prototype._updateConversion=function(){var t=this.range;if(!t)throw Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):h.conversion(t.start,t.end,this.width)},f.prototype=new p,f.types={box:g,range:y,point:v},f.prototype.setOptions=l.prototype.setOptions,f.prototype.setRange=function(t){if(!(t instanceof h||t&&t.start&&t.end))throw new TypeError("Range must be an instance of Range, or an object containing start and end.");this.range=t},f.prototype.repaint=function(){var t=0,e=O.updateProperty,i=O.option.asSize,n=this.options,s=this.getOption("orientation"),o=this.defaultOptions,r=this.frame;if(!r){r=document.createElement("div"),r.className="itemset";var a=n.className;a&&O.addClassName(r,O.option.asString(a));var h=document.createElement("div");h.className="background",r.appendChild(h),this.dom.background=h;var d=document.createElement("div");d.className="foreground",r.appendChild(d),this.dom.foreground=d;var l=document.createElement("div");l.className="itemset-axis",this.dom.axis=l,this.frame=r,t+=1}if(!this.parent)throw Error("Cannot repaint itemset: no parent attached");var p=this.parent.getContainer();if(!p)throw Error("Cannot repaint itemset: parent has no container element");r.parentNode||(p.appendChild(r),t+=1),this.dom.axis.parentNode||(p.appendChild(this.dom.axis),t+=1),t+=e(r.style,"left",i(n.left,"0px")),t+=e(r.style,"top",i(n.top,"0px")),t+=e(r.style,"width",i(n.width,"100%")),t+=e(r.style,"height",i(n.height,this.height+"px")),t+=e(this.dom.axis.style,"left",i(n.left,"0px")),t+=e(this.dom.axis.style,"width",i(n.width,"100%")),t+="bottom"==s?e(this.dom.axis.style,"top",this.height+this.top+"px"):e(this.dom.axis.style,"top",this.top+"px"),this._updateConversion();var c=this,u=this.queue,m=this.itemsData,g=this.items,v={};return Object.keys(u).forEach(function(e){var i=u[e],s=g[e];switch(i){case"add":case"update":var r=m&&m.get(e,v);if(r){var a=r.type||r.start&&r.end&&"range"||n.type||"box",h=f.types[a];if(s&&(h&&s instanceof h?(s.data=r,t++):(t+=s.hide(),s=null)),!s){if(!h)throw new TypeError('Unknown item type "'+a+'"');s=new h(c,r,n,o),t++}s.repaint(),g[e]=s}delete u[e];break;case"remove":s&&(t+=s.hide()),delete g[e],delete u[e];break;default:console.log('Error: unknown action "'+i+'"')}}),O.forEach(this.items,function(e){e.visible?(t+=e.show(),e.reposition()):t+=e.hide()}),t>0},f.prototype.getForeground=function(){return this.dom.foreground},f.prototype.getBackground=function(){return this.dom.background},f.prototype.getAxis=function(){return this.dom.axis},f.prototype.reflow=function(){var t=0,e=this.options,i=e.margin&&e.margin.axis||this.defaultOptions.margin.axis,n=e.margin&&e.margin.item||this.defaultOptions.margin.item,s=O.updateProperty,o=O.option.asNumber,r=O.option.asSize,a=this.frame;if(a){this._updateConversion(),O.forEach(this.items,function(e){t+=e.reflow()}),this.stack.update();var h,d=o(e.maxHeight),l=null!=r(e.height);if(l)h=a.offsetHeight;else{var p=this.stack.ordered;if(p.length){var c=p[0].top,u=p[0].top+p[0].height;O.forEach(p,function(t){c=Math.min(c,t.top),u=Math.max(u,t.top+t.height)}),h=u-c+i+n}else h=i+n}null!=d&&(h=Math.min(h,d)),t+=s(this,"height",h),t+=s(this,"top",a.offsetTop),t+=s(this,"left",a.offsetLeft),t+=s(this,"width",a.offsetWidth)}else t+=1;return t>0},f.prototype.hide=function(){var t=!1;return this.frame&&this.frame.parentNode&&(this.frame.parentNode.removeChild(this.frame),t=!0),this.dom.axis&&this.dom.axis.parentNode&&(this.dom.axis.parentNode.removeChild(this.dom.axis),t=!0),t},f.prototype.setItems=function(t){var e,i=this,n=this.itemsData;if(t){if(!(t instanceof o||t instanceof r))throw new TypeError("Data must be an instance of DataSet");this.itemsData=t}else this.itemsData=null;if(n&&(O.forEach(this.listeners,function(t,e){n.unsubscribe(e,t)}),e=n.getIds(),this._onRemove(e)),this.itemsData){var s=this.id;O.forEach(this.listeners,function(t,e){i.itemsData.subscribe(e,t,s)}),e=this.itemsData.getIds(),this._onAdd(e)}},f.prototype.getItems=function(){return this.itemsData},f.prototype._onUpdate=function(t){this._toQueue("update",t)},f.prototype._onAdd=function(t){this._toQueue("add",t)},f.prototype._onRemove=function(t){this._toQueue("remove",t)},f.prototype._toQueue=function(t,e){var i=this.queue;e.forEach(function(e){i[e]=t}),this.controller&&this.requestRepaint()},f.prototype._updateConversion=function(){var t=this.range;if(!t)throw Error("No range configured");this.conversion=t.conversion?t.conversion(this.width):h.conversion(t.start,t.end,this.width)},f.prototype.toTime=function(t){var e=this.conversion;return new Date(t/e.factor+e.offset)},f.prototype.toScreen=function(t){var e=this.conversion;return(t.valueOf()-e.offset)*e.factor},m.prototype.select=function(){this.selected=!0},m.prototype.unselect=function(){this.selected=!1},m.prototype.show=function(){return!1},m.prototype.hide=function(){return!1},m.prototype.repaint=function(){return!1},m.prototype.reflow=function(){return!1},g.prototype=new m(null,null),g.prototype.select=function(){this.selected=!0},g.prototype.unselect=function(){this.selected=!1},g.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw Error("Cannot repaint time axis: parent has no foreground container element");var n=this.parent.getBackground();if(!n)throw Error("Cannot repaint time axis: parent has no background container element");var s=this.parent.getAxis();if(!n)throw Error("Cannot repaint time axis: parent has no axis container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),e.line.parentNode||(n.appendChild(e.line),t=!0),e.dot.parentNode||(s.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 o=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=o&&(this.className=o,e.box.className="item box"+o,e.line.className="item line"+o,e.dot.className="item dot"+o,t=!0)}return t},g.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},g.prototype.hide=function(){var t=!1,e=this.dom;return e&&(e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),e.line.parentNode&&e.line.parentNode.removeChild(e.line),e.dot.parentNode&&e.dot.parentNode.removeChild(e.dot)),t},g.prototype.reflow=function(){var t,e,i,n,s,o,r,a,h,d,l,p,c=0;if(void 0==this.data.start)throw Error('Property "start" missing in item '+this.data.id);if(l=this.data,p=this.parent&&this.parent.range,l&&p){var u=p.end-p.start;this.visible=l.start>p.start-u&&l.start0},g.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")},g.prototype.reposition=function(){var t=this.dom,e=this.props,i=this.options.orientation||this.defaultOptions.orientation;if(t){var n=t.box,s=t.line,o=t.dot;n.style.left=this.left+"px",n.style.top=this.top+"px",s.style.left=e.line.left+"px","top"==i?(s.style.top="0px",s.style.height=this.top+"px"):(s.style.top=this.top+this.height+"px",s.style.height=Math.max(this.parent.height-this.top-this.height+this.props.dot.height/2,0)+"px"),o.style.left=e.dot.left+"px",o.style.top=e.dot.top+"px"}},v.prototype=new m(null,null),v.prototype.select=function(){this.selected=!0},v.prototype.unselect=function(){this.selected=!1},v.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw Error("Cannot repaint time axis: parent has no foreground container element");if(e.point.parentNode||(i.appendChild(e.point),i.appendChild(e.point),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw Error('Property "content" missing in item '+this.data.id);e.content.innerHTML=this.content}t=!0}var n=(this.data.className?" "+this.data.className:"")+(this.selected?" selected":"");this.className!=n&&(this.className=n,e.point.className="item point"+n,t=!0)}return t},v.prototype.show=function(){return this.dom&&this.dom.point.parentNode?!1:this.repaint()},v.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.point.parentNode&&(e.point.parentNode.removeChild(e.point),t=!0),t},v.prototype.reflow=function(){var t,e,i,n,s,o,r,a,h,d,l=0;if(void 0==this.data.start)throw Error('Property "start" missing in item '+this.data.id);if(h=this.data,d=this.parent&&this.parent.range,h&&d){var p=d.end-d.start;this.visible=h.start>d.start-p&&h.start0},v.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))},v.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")},y.prototype=new m(null,null),y.prototype.select=function(){this.selected=!0},y.prototype.unselect=function(){this.selected=!1},y.prototype.repaint=function(){var t=!1,e=this.dom;if(e||(this._create(),e=this.dom,t=!0),e){if(!this.parent)throw Error("Cannot repaint item: no parent attached");var i=this.parent.getForeground();if(!i)throw Error("Cannot repaint time axis: parent has no foreground container element");if(e.box.parentNode||(i.appendChild(e.box),t=!0),this.data.content!=this.content){if(this.content=this.data.content,this.content instanceof Element)e.content.innerHTML="",e.content.appendChild(this.content);else{if(void 0==this.data.content)throw 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)}return t},y.prototype.show=function(){return this.dom&&this.dom.box.parentNode?!1:this.repaint()},y.prototype.hide=function(){var t=!1,e=this.dom;return e&&e.box.parentNode&&(e.box.parentNode.removeChild(e.box),t=!0),t},y.prototype.reflow=function(){var t,e,i,n,s,o,r,a,h,d,l,p,c,u,f,m,g=0;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);return h=this.data,d=this.parent&&this.parent.range,this.visible=h&&d?h.startd.start:!1,this.visible&&(t=this.dom,t?(e=this.props,i=this.options,o=this.parent,r=o.toScreen(this.data.start),a=o.toScreen(this.data.end),l=O.updateProperty,p=t.box,c=o.width,f=i.orientation||this.defaultOptions.orientation,n=i.margin&&i.margin.axis||this.defaultOptions.margin.axis,s=i.padding||this.defaultOptions.padding,g+=l(e.content,"width",t.content.offsetWidth),g+=l(this,"height",p.offsetHeight),-c>r&&(r=-c),a>2*c&&(a=2*c),u=0>r?Math.min(-r,a-r-e.content.width-2*s):0,g+=l(e.content,"left",u),"top"==f?(m=n,g+=l(this,"top",m)):(m=o.height-this.height-n,g+=l(this,"top",m)),g+=l(this,"left",r),g+=l(this,"width",Math.max(a-r,1))):g+=1),g>0},y.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))},y.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")},b.prototype=new l,b.prototype.setOptions=l.prototype.setOptions,b.prototype.getContainer=function(){return this.parent.getContainer()},b.prototype.setItems=function(t){if(this.itemset&&(this.itemset.hide(),this.itemset.setItems(),this.parent.controller.remove(this.itemset),this.itemset=null),t){var e=this.groupId,i=Object.create(this.options);this.itemset=new f(this,null,i),this.itemset.setRange(this.parent.range),this.view=new r(t,{filter:function(t){return t.group==e}}),this.itemset.setItems(this.view),this.parent.controller.add(this.itemset)}},b.prototype.repaint=function(){return!1},b.prototype.reflow=function(){var t=0,e=O.updateProperty;if(t+=e(this,"top",this.itemset?this.itemset.top:0),t+=e(this,"height",this.itemset?this.itemset.height:0),this.label){var i=this.label.firstChild;t+=e(this.props.label,"width",i.clientWidth),t+=e(this.props.label,"height",i.clientHeight)}else t+=e(this.props.label,"width",0),t+=e(this.props.label,"height",0);return t>0},w.prototype=new p,w.prototype.setOptions=l.prototype.setOptions,w.prototype.setRange=function(){},w.prototype.setItems=function(t){this.itemsData=t;for(var e in this.groups)if(this.groups.hasOwnProperty(e)){var i=this.groups[e];i.setItems(t)}},w.prototype.getItems=function(){return this.itemsData},w.prototype.setRange=function(t){this.range=t},w.prototype.setGroups=function(t){var e,i=this;if(this.groupsData&&(O.forEach(this.listeners,function(t,e){i.groupsData.unsubscribe(e,t)}),e=this.groupsData.getIds(),this._onRemove(e)),t?t instanceof o?this.groupsData=t:(this.groupsData=new o({convert:{start:"Date",end:"Date"}}),this.groupsData.add(t)):this.groupsData=null,this.groupsData){var n=this.id;O.forEach(this.listeners,function(t,e){i.groupsData.subscribe(e,t,n)}),e=this.groupsData.getIds(),this._onAdd(e)}},w.prototype.getGroups=function(){return this.groupsData},w.prototype.repaint=function(){var t,e,i,n,s=0,o=O.updateProperty,r=O.option.asSize,a=O.option.asElement,h=this.options,d=this.dom.frame,l=this.dom.labels;if(!this.parent)throw Error("Cannot repaint groupset: no parent attached");var p=this.parent.getContainer();if(!p)throw Error("Cannot repaint groupset: parent has no container element");if(!d){d=document.createElement("div"),d.className="groupset",this.dom.frame=d;var c=h.className;c&&O.addClassName(d,O.option.asString(c)),s+=1}d.parentNode||(p.appendChild(d),s+=1);var u=a(h.labelContainer);if(!u)throw Error('Cannot repaint groupset: option "labelContainer" not defined');l||(l=document.createElement("div"),l.className="labels",this.dom.labels=l),l.parentNode&&l.parentNode==u||(l.parentNode&&l.parentNode.removeChild(l.parentNode),u.appendChild(l)),s+=o(d.style,"height",r(h.height,this.height+"px")),s+=o(d.style,"top",r(h.top,"0px")),s+=o(d.style,"left",r(h.left,"0px")),s+=o(d.style,"width",r(h.width,"100%")),s+=o(l.style,"top",r(h.top,"0px"));var f=this,m=this.queue,g=this.groups,v=this.groupsData,y=Object.keys(m);if(y.length){y.forEach(function(t){var e=m[t],i=g[t];switch(e){case"add":case"update":if(!i){var n=Object.create(f.options);i=new b(f,t,n),i.setItems(f.itemsData),g[t]=i,f.controller.add(i)}i.data=v.get(t),delete m[t];break;case"remove":i&&(i.setItems(),delete g[t],f.controller.remove(i)),delete m[t];break;default:console.log('Error: unknown action "'+e+'"')}});var w=this.groupsData.getIds({order:this.options.groupsOrder});for(t=0;w.length>t;t++)(function(t,e){var i=0;e&&(i=function(){return e.top+e.height}),t.setOptions({top:i})})(g[w[t]],g[w[t-1]]);for(;l.firstChild;)l.removeChild(l.firstChild);for(t=0;w.length>t;t++)e=w[t],n=this._createLabel(e),l.appendChild(n);s++}for(e in g)g.hasOwnProperty(e)&&(i=g[e],n=i.label,n&&(n.style.top=i.top+"px",n.style.height=i.height+"px"));return s>0},w.prototype._createLabel=function(t){var e=this.groups[t],i=document.createElement("div");i.className="label";var n=document.createElement("div");n.className="inner",i.appendChild(n);var s=e.data&&e.data.content;s instanceof Element?n.appendChild(s):void 0!=s&&(n.innerHTML=s);var o=e.data&&e.data.className;return o&&O.addClassName(i,o),e.label=i,i},w.prototype.getContainer=function(){return this.dom.frame},w.prototype.getLabelsWidth=function(){return this.props.labels.width},w.prototype.reflow=function(){var t,e,i=0,n=this.options,s=O.updateProperty,o=O.option.asNumber,r=O.option.asSize,a=this.dom.frame;if(a){var h,d=o(n.maxHeight),l=null!=r(n.height);if(l)h=a.offsetHeight;else{h=0;for(t in this.groups)this.groups.hasOwnProperty(t)&&(e=this.groups[t],h+=e.height)}null!=d&&(h=Math.min(h,d)),i+=s(this,"height",h),i+=s(this,"top",a.offsetTop),i+=s(this,"left",a.offsetLeft),i+=s(this,"width",a.offsetWidth)}var p=0;for(t in this.groups)if(this.groups.hasOwnProperty(t)){e=this.groups[t];var c=e.props&&e.props.label&&e.props.label.width||0;p=Math.max(p,c)}return i+=s(this.props.labels,"width",p),i>0},w.prototype.hide=function(){return this.dom.frame&&this.dom.frame.parentNode?(this.dom.frame.parentNode.removeChild(this.dom.frame),!0):!1},w.prototype.show=function(){return this.dom.frame&&this.dom.frame.parentNode?!1:this.repaint()},w.prototype._onUpdate=function(t){this._toQueue(t,"update")},w.prototype._onAdd=function(t){this._toQueue(t,"add")},w.prototype._onRemove=function(t){this._toQueue(t,"remove")},w.prototype._toQueue=function(t,e){var i=this.queue;t.forEach(function(t){i[t]=e}),this.controller&&this.requestRepaint()},_.prototype.setOptions=function(t){t&&O.extend(this.options,t),this.controller.reflow(),this.controller.repaint()},_.prototype.setItems=function(t){var e,i=null==this.itemsData;if(t?t instanceof o&&(e=t):e=null,t instanceof o||(e=new o({convert:{start:"Date",end:"Date"}}),e.add(t)),this.itemsData=e,this.content.setItems(e),i&&(void 0==this.options.start||void 0==this.options.end)){var n=this.getItemRange(),s=n.min,r=n.max;if(null!=s&&null!=r){var a=r.valueOf()-s.valueOf();0>=a&&(a=864e5),s=new Date(s.valueOf()-.05*a),r=new Date(r.valueOf()+.05*a)}void 0!=this.options.start&&(s=new Date(this.options.start.valueOf())),void 0!=this.options.end&&(r=new Date(this.options.end.valueOf())),(null!=s||null!=r)&&this.range.setRange(s,r)}},_.prototype.setGroups=function(t){var e=this;this.groupsData=t;var i=this.groupsData?w:f;if(!(this.content instanceof i)){this.content&&(this.content.hide(),this.content.setItems&&this.content.setItems(),this.content.setGroups&&this.content.setGroups(),this.controller.remove(this.content));var n=Object.create(this.options);O.extend(n,{top:function(){return"top"==e.options.orientation?e.timeaxis.height:e.itemPanel.height-e.timeaxis.height-e.content.height},left:null,width:"100%",height:function(){return e.options.height?e.itemPanel.height-e.timeaxis.height:null},maxHeight:function(){if(e.options.maxHeight){if(!O.isNumber(e.options.maxHeight))throw new TypeError("Number expected for property maxHeight");return e.options.maxHeight-e.timeaxis.height}return null},labelContainer:function(){return e.labelPanel.getContainer()}}),this.content=new i(this.itemPanel,[this.timeaxis],n),this.content.setRange&&this.content.setRange(this.range),this.content.setItems&&this.content.setItems(this.itemsData),this.content.setGroups&&this.content.setGroups(this.groupsData),this.controller.add(this.content)}},_.prototype.getItemRange=function(){var t=this.itemsData,e=null,i=null;if(t){var n=t.min("start");e=n?n.start.valueOf():null;var s=t.max("start");s&&(i=s.start.valueOf());var o=t.max("end");o&&(i=null==i?o.end.valueOf():Math.max(i,o.end.valueOf()))}return{min:null!=e?new Date(e):null,max:null!=i?new Date(i):null}},function(t){function e(t){return M=t,c()}function i(){D=0,C=M.charAt(0)}function n(){D++,C=M.charAt(D)}function s(){return M.charAt(D+1)}function o(t){return L.test(t)}function r(t,e){if(t||(t={}),e)for(var i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t}function a(t,e,i){for(var n=e.split("."),s=t;n.length;){var o=n.shift();n.length?(s[o]||(s[o]={}),s=s[o]):s[o]=i}}function h(t,e){for(var i,n,s=null,o=[t],a=t;a.parent;)o.push(a.parent),a=a.parent;if(a.nodes)for(i=0,n=a.nodes.length;n>i;i++)if(e.id===a.nodes[i].id){s=a.nodes[i];break}for(s||(s={id:e.id},t.node&&(s.attr=r(s.attr,t.node))),i=o.length-1;i>=0;i--){var h=o[i];h.nodes||(h.nodes=[]),-1==h.nodes.indexOf(s)&&h.nodes.push(s)}e.attr&&(s.attr=r(s.attr,e.attr))}function d(t,e){if(t.edges||(t.edges=[]),t.edges.push(e),t.edge){var i=r({},t.edge);e.attr=r(i,e.attr)}}function l(t,e,i,n,s){var o={from:e,to:i,type:n};return t.edge&&(o.attr=r({},t.edge)),o.attr=r(o.attr||{},s),o}function p(){for(N=x.NULL,O="";" "==C||" "==C||"\n"==C||"\r"==C;)n();do{var t=!1;if("#"==C){for(var e=D-1;" "==M.charAt(e)||" "==M.charAt(e);)e--;if("\n"==M.charAt(e)||""==M.charAt(e)){for(;""!=C&&"\n"!=C;)n();t=!0}}if("/"==C&&"/"==s()){for(;""!=C&&"\n"!=C;)n();t=!0}if("/"==C&&"*"==s()){for(;""!=C;){if("*"==C&&"/"==s()){n(),n();break}n()}t=!0}for(;" "==C||" "==C||"\n"==C||"\r"==C;)n()}while(t);if(""==C)return N=x.DELIMITER,void 0;var i=C+s();if(S[i])return N=x.DELIMITER,O=i,n(),n(),void 0;if(S[C])return N=x.DELIMITER,O=C,n(),void 0;if(o(C)||"-"==C){for(O+=C,n();o(C);)O+=C,n();return"false"==O?O=!1:"true"==O?O=!0:isNaN(Number(O))||(O=Number(O)),N=x.IDENTIFIER,void 0}if('"'==C){for(n();""!=C&&('"'!=C||'"'==C&&'"'==s());)O+=C,'"'==C&&n(),n();if('"'!=C)throw w('End of string " expected');return n(),N=x.IDENTIFIER,void 0}for(N=x.UNKNOWN;""!=C;)O+=C,n();throw new SyntaxError('Syntax error in part "'+_(O,30)+'"')}function c(){var t={};if(i(),p(),"strict"==O&&(t.strict=!0,p()),("graph"==O||"digraph"==O)&&(t.type=O,p()),N==x.IDENTIFIER&&(t.id=O,p()),"{"!=O)throw w("Angle bracket { expected");if(p(),u(t),"}"!=O)throw w("Angle bracket } expected");if(p(),""!==O)throw w("End of file expected");return p(),delete t.node,delete t.edge,delete t.graph,t}function u(t){for(;""!==O&&"}"!=O;)f(t),";"==O&&p()}function f(t){var e=m(t);if(e)return y(t,e),void 0;var i=g(t);if(!i){if(N!=x.IDENTIFIER)throw w("Identifier expected");var n=O;if(p(),"="==O){if(p(),N!=x.IDENTIFIER)throw w("Identifier expected");t[n]=O,p()}else v(t,n)}}function m(t){var e=null;if("subgraph"==O&&(e={},e.type="subgraph",p(),N==x.IDENTIFIER&&(e.id=O,p())),"{"==O){if(p(),e||(e={}),e.parent=t,e.node=t.node,e.edge=t.edge,e.graph=t.graph,u(e),"}"!=O)throw w("Angle bracket } expected");p(),delete e.node,delete e.edge,delete e.graph,delete e.parent,t.subgraphs||(t.subgraphs=[]),t.subgraphs.push(e)}return e}function g(t){return"node"==O?(p(),t.node=b(),"node"):"edge"==O?(p(),t.edge=b(),"edge"):"graph"==O?(p(),t.graph=b(),"graph"):null}function v(t,e){var i={id:e},n=b();n&&(i.attr=n),h(t,i),y(t,e)}function y(t,e){for(;"->"==O||"--"==O;){var i,n=O;p();var s=m(t);if(s)i=s;else{if(N!=x.IDENTIFIER)throw w("Identifier or subgraph expected");i=O,h(t,{id:i}),p()}var o=b(),r=l(t,e,i,n,o);d(t,r),e=i}}function b(){for(var t=null;"["==O;){for(p(),t={};""!==O&&"]"!=O;){if(N!=x.IDENTIFIER)throw w("Attribute name expected");var e=O;if(p(),"="!=O)throw w("Equal sign = expected");if(p(),N!=x.IDENTIFIER)throw w("Attribute value expected");var i=O;a(t,e,i),p(),","==O&&p()}if("]"!=O)throw w("Bracket ] expected");p()}return t}function w(t){return new SyntaxError(t+', got "'+_(O,30)+'" (char '+D+")")}function _(t,e){return e>=t.length?t:t.substr(0,27)+"..."}function E(t,e,i){t instanceof Array?t.forEach(function(t){e instanceof Array?e.forEach(function(e){i(t,e)}):i(t,e)}):e instanceof Array?e.forEach(function(e){i(t,e)}):i(t,e)}function T(t){function i(t){var e={from:t.from,to:t.to};return r(e,t.attr),e.style="->"==t.type?"arrow":"line",e}var n=e(t),s={nodes:[],edges:[],options:{}};return n.nodes&&n.nodes.forEach(function(t){var e={id:t.id,label:(t.label||t.id)+""};r(e,t.attr),e.image&&(e.shape="image"),s.nodes.push(e)}),n.edges&&n.edges.forEach(function(t){var e,n;e=t.from instanceof Object?t.from.nodes:{id:t.from},n=t.to instanceof Object?t.to.nodes:{id:t.to},t.from instanceof Object&&t.from.edges&&t.from.edges.forEach(function(t){var e=i(t);s.edges.push(e)}),E(e,n,function(e,n){var o=l(s,e.id,n.id,t.type,t.attr),r=i(o);s.edges.push(r)}),t.to instanceof Object&&t.to.edges&&t.to.edges.forEach(function(t){var e=i(t);s.edges.push(e)})}),n.attr&&(s.options=n.attr),s}var x={NULL:0,DELIMITER:1,IDENTIFIER:2,UNKNOWN:3},S={"{":!0,"}":!0,"[":!0,"]":!0,";":!0,"=":!0,",":!0,"->":!0,"--":!0},M="",D=0,C="",O="",N=x.NULL,L=/[a-zA-Z_0-9.:#]/;t.parseDOT=e,t.DOTToGraph=T}(O!==void 0?O:n),"undefined"!=typeof CanvasRenderingContext2D&&(CanvasRenderingContext2D.prototype.circle=function(t,e,i){this.beginPath(),this.arc(t,e,i,0,2*Math.PI,!1)},CanvasRenderingContext2D.prototype.square=function(t,e,i){this.beginPath(),this.rect(t-i,e-i,2*i,2*i)},CanvasRenderingContext2D.prototype.triangle=function(t,e,i){this.beginPath();var n=2*i,s=n/2,o=Math.sqrt(3)/6*n,r=Math.sqrt(n*n-s*s);this.moveTo(t,e-(r-o)),this.lineTo(t+s,e+o),this.lineTo(t-s,e+o),this.lineTo(t,e-(r-o)),this.closePath()},CanvasRenderingContext2D.prototype.triangleDown=function(t,e,i){this.beginPath();var n=2*i,s=n/2,o=Math.sqrt(3)/6*n,r=Math.sqrt(n*n-s*s);this.moveTo(t,e+(r-o)),this.lineTo(t+s,e-o),this.lineTo(t-s,e-o),this.lineTo(t,e+(r-o)),this.closePath()},CanvasRenderingContext2D.prototype.star=function(t,e,i){this.beginPath();for(var n=0;10>n;n++){var s=0===n%2?1.3*i:.5*i;this.lineTo(t+s*Math.sin(2*n*Math.PI/10),e-s*Math.cos(2*n*Math.PI/10))}this.closePath()},CanvasRenderingContext2D.prototype.roundRect=function(t,e,i,n,s){var o=Math.PI/180;0>i-2*s&&(s=i/2),0>n-2*s&&(s=n/2),this.beginPath(),this.moveTo(t+s,e),this.lineTo(t+i-s,e),this.arc(t+i-s,e+s,s,270*o,360*o,!1),this.lineTo(t+i,e+n-s),this.arc(t+i-s,e+n-s,s,0,90*o,!1),this.lineTo(t+s,e+n),this.arc(t+s,e+n-s,s,90*o,180*o,!1),this.lineTo(t,e+s),this.arc(t+s,e+s,s,180*o,270*o,!1)},CanvasRenderingContext2D.prototype.ellipse=function(t,e,i,n){var s=.5522848,o=i/2*s,r=n/2*s,a=t+i,h=e+n,d=t+i/2,l=e+n/2;this.beginPath(),this.moveTo(t,l),this.bezierCurveTo(t,l-r,d-o,e,d,e),this.bezierCurveTo(d+o,e,a,l-r,a,l),this.bezierCurveTo(a,l+r,d+o,h,d,h),this.bezierCurveTo(d-o,h,t,l+r,t,l)},CanvasRenderingContext2D.prototype.database=function(t,e,i,n){var s=1/3,o=i,r=n*s,a=.5522848,h=o/2*a,d=r/2*a,l=t+o,p=e+r,c=t+o/2,u=e+r/2,f=e+(n-r/2),m=e+n;this.beginPath(),this.moveTo(l,u),this.bezierCurveTo(l,u+d,c+h,p,c,p),this.bezierCurveTo(c-h,p,t,u+d,t,u),this.bezierCurveTo(t,u-d,c-h,e,c,e),this.bezierCurveTo(c+h,e,l,u-d,l,u),this.lineTo(l,f),this.bezierCurveTo(l,f+d,c+h,m,c,m),this.bezierCurveTo(c-h,m,t,f+d,t,f),this.lineTo(t,u)},CanvasRenderingContext2D.prototype.arrow=function(t,e,i,n){var s=t-n*Math.cos(i),o=e-n*Math.sin(i),r=t-.9*n*Math.cos(i),a=e-.9*n*Math.sin(i),h=s+n/3*Math.cos(i+.5*Math.PI),d=o+n/3*Math.sin(i+.5*Math.PI),l=s+n/3*Math.cos(i-.5*Math.PI),p=o+n/3*Math.sin(i-.5*Math.PI);this.beginPath(),this.moveTo(t,e),this.lineTo(h,d),this.lineTo(r,a),this.lineTo(l,p),this.closePath()},CanvasRenderingContext2D.prototype.dashedLine=function(t,e,i,n,s){s||(s=[10,5]),0==c&&(c=.001);var o=s.length;this.moveTo(t,e);for(var r=i-t,a=n-e,h=a/r,d=Math.sqrt(r*r+a*a),l=0,p=!0;d>=.1;){var c=s[l++%o];c>d&&(c=d);var u=Math.sqrt(c*c/(1+h*h));0>r&&(u=-u),t+=u,e+=h*u,this[p?"lineTo":"moveTo"](t,e),d-=c,p=!p}}),E.prototype.attachEdge=function(t){-1==this.edges.indexOf(t)&&this.edges.push(t),this._updateMass()},E.prototype.detachEdge=function(t){var e=this.edges.indexOf(t);-1!=e&&this.edges.splice(e,1),this._updateMass()},E.prototype._updateMass=function(){this.mass=50+20*this.edges.length},E.prototype.setProperties=function(t,e){if(t){if(void 0!=t.id&&(this.id=t.id),void 0!=t.label&&(this.label=t.label),void 0!=t.title&&(this.title=t.title),void 0!=t.group&&(this.group=t.group),void 0!=t.x&&(this.x=t.x),void 0!=t.y&&(this.y=t.y),void 0!=t.value&&(this.value=t.value),void 0===this.id)throw"Node must have an id";if(this.group){var i=this.grouplist.get(this.group);for(var n in i)i.hasOwnProperty(n)&&(this[n]=i[n])}if(void 0!=t.shape&&(this.shape=t.shape),void 0!=t.image&&(this.image=t.image),void 0!=t.radius&&(this.radius=t.radius),void 0!=t.color&&(this.color=E.parseColor(t.color)),void 0!=t.fontColor&&(this.fontColor=t.fontColor),void 0!=t.fontSize&&(this.fontSize=t.fontSize),void 0!=t.fontFace&&(this.fontFace=t.fontFace),void 0!=this.image){if(!this.imagelist)throw"No imagelist provided";this.imageObj=this.imagelist.load(this.image)}switch(this.xFixed=this.xFixed||void 0!=t.x,this.yFixed=this.yFixed||void 0!=t.y,this.radiusFixed=this.radiusFixed||void 0!=t.radius,"image"==this.shape&&(this.radiusMin=e.nodes.widthMin,this.radiusMax=e.nodes.widthMax),this.shape){case"database":this.draw=this._drawDatabase,this.resize=this._resizeDatabase;break;case"box":this.draw=this._drawBox,this.resize=this._resizeBox;break;case"circle":this.draw=this._drawCircle,this.resize=this._resizeCircle;break;case"ellipse":this.draw=this._drawEllipse,this.resize=this._resizeEllipse;break;case"image":this.draw=this._drawImage,this.resize=this._resizeImage;break;case"text":this.draw=this._drawText,this.resize=this._resizeText;break;case"dot":this.draw=this._drawDot,this.resize=this._resizeShape;break;case"square":this.draw=this._drawSquare,this.resize=this._resizeShape;break;case"triangle":this.draw=this._drawTriangle,this.resize=this._resizeShape;break;case"triangleDown":this.draw=this._drawTriangleDown,this.resize=this._resizeShape;break;case"star":this.draw=this._drawStar,this.resize=this._resizeShape;break;default:this.draw=this._drawEllipse,this.resize=this._resizeEllipse}this._reset()}},E.parseColor=function(t){var e;return O.isString(t)?e={border:t,background:t,highlight:{border:t,background:t}}:(e={},e.background=t.background||"white",e.border=t.border||e.background,O.isString(t.highlight)?e.highlight={border:t.highlight,background:t.highlight}:(e.highlight={},e.highlight.background=t.highlight&&t.highlight.background||e.background,e.highlight.border=t.highlight&&t.highlight.border||e.border)),e},E.prototype.select=function(){this.selected=!0,this._reset()},E.prototype.unselect=function(){this.selected=!1,this._reset()},E.prototype._reset=function(){this.width=void 0,this.height=void 0},E.prototype.getTitle=function(){return this.title},E.prototype.distanceToBorder=function(t,e){var i=1;switch(this.width||this.resize(t),this.shape){case"circle":case"dot":return this.radius+i;case"ellipse":var n=this.width/2,s=this.height/2,o=Math.sin(e)*n,r=Math.cos(e)*s;return n*s/Math.sqrt(o*o+r*r);case"box":case"image":case"text":default:return this.width?Math.min(Math.abs(this.width/2/Math.cos(e)),Math.abs(this.height/2/Math.sin(e)))+i:0}},E.prototype._setForce=function(t,e){this.fx=t,this.fy=e},E.prototype._addForce=function(t,e){this.fx+=t,this.fy+=e},E.prototype.discreteStep=function(t){if(!this.xFixed){var e=-this.damping*this.vx,i=(this.fx+e)/this.mass;this.vx+=i/t,this.x+=this.vx/t}if(!this.yFixed){var n=-this.damping*this.vy,s=(this.fy+n)/this.mass;this.vy+=s/t,this.y+=this.vy/t}},E.prototype.isFixed=function(){return this.xFixed&&this.yFixed},E.prototype.isMoving=function(t){return Math.abs(this.vx)>t||Math.abs(this.vy)>t||!this.xFixed&&Math.abs(this.fx)>this.minForce||!this.yFixed&&Math.abs(this.fy)>this.minForce},E.prototype.isSelected=function(){return this.selected},E.prototype.getValue=function(){return this.value},E.prototype.getDistance=function(t,e){var i=this.x-t,n=this.y-e;return Math.sqrt(i*i+n*n)},E.prototype.setValueRange=function(t,e){if(!this.radiusFixed&&void 0!==this.value){var i=(this.radiusMax-this.radiusMin)/(e-t);this.radius=(this.value-t)*i+this.radiusMin}},E.prototype.draw=function(){throw"Draw method not initialized for node"},E.prototype.resize=function(){throw"Resize method not initialized for node"},E.prototype.isOverlappingWith=function(t){return this.leftt.left&&this.topt.top},E.prototype._resizeImage=function(){if(!this.width){var t,e;if(this.value){var i=this.imageObj.height/this.imageObj.width;t=this.radius||this.imageObj.width,e=this.radius*i||this.imageObj.height}else t=this.imageObj.width,e=this.imageObj.height;this.width=t,this.height=e}},E.prototype._drawImage=function(t){this._resizeImage(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2;var e;this.imageObj?(t.drawImage(this.imageObj,this.left,this.top,this.width,this.height),e=this.y+this.height/2):e=this.y,this._label(t,this.label,this.x,e,void 0,"top")},E.prototype._resizeBox=function(t){if(!this.width){var e=5,i=this.getTextSize(t);this.width=i.width+2*e,this.height=i.height+2*e}},E.prototype._drawBox=function(t){this._resizeBox(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.lineWidth=this.selected?2:1,t.roundRect(this.left,this.top,this.width,this.height,this.radius),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},E.prototype._resizeDatabase=function(t){if(!this.width){var e=5,i=this.getTextSize(t),n=i.width+2*e;this.width=n,this.height=n}},E.prototype._drawDatabase=function(t){this._resizeDatabase(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.lineWidth=this.selected?2:1,t.database(this.x-this.width/2,this.y-.5*this.height,this.width,this.height),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},E.prototype._resizeCircle=function(t){if(!this.width){var e=5,i=this.getTextSize(t),n=Math.max(i.width,i.height)+2*e;this.radius=n/2,this.width=n,this.height=n}},E.prototype._drawCircle=function(t){this._resizeCircle(t),this.left=this.x-this.width/2,this.top=this.y-this.height/2,t.strokeStyle=this.selected?this.color.highlight.border:this.color.border,t.fillStyle=this.selected?this.color.highlight.background:this.color.background,t.lineWidth=this.selected?2:1,t.circle(this.x,this.y,this.radius),t.fill(),t.stroke(),this._label(t,this.label,this.x,this.y)},E.prototype._resizeEllipse=function(t){if(!this.width){var e=this.getTextSize(t);this.width=1.5*e.width,this.height=2*e.height,this.widthl;l++)t.fillText(r[l],i,d),d+=h}},E.prototype.getTextSize=function(t){if(void 0!=this.label){t.font=(this.selected?"bold ":"")+this.fontSize+"px "+this.fontFace;for(var e=this.label.split("\n"),i=(this.fontSize+4)*e.length,n=0,s=0,o=e.length;o>s;s++)n=Math.max(n,t.measureText(e[s]).width);return{width:n,height:i}}return{width:0,height:0}},T.prototype.setProperties=function(t,e){if(t)switch(void 0!=t.from&&(this.fromId=t.from),void 0!=t.to&&(this.toId=t.to),void 0!=t.id&&(this.id=t.id),void 0!=t.style&&(this.style=t.style),void 0!=t.label&&(this.label=t.label),this.label&&(this.fontSize=e.edges.fontSize,this.fontFace=e.edges.fontFace,this.fontColor=e.edges.fontColor,void 0!=t.fontColor&&(this.fontColor=t.fontColor),void 0!=t.fontSize&&(this.fontSize=t.fontSize),void 0!=t.fontFace&&(this.fontFace=t.fontFace)),void 0!=t.title&&(this.title=t.title),void 0!=t.width&&(this.width=t.width),void 0!=t.value&&(this.value=t.value),void 0!=t.length&&(this.length=t.length),t.dash&&(void 0!=t.dash.length&&(this.dash.length=t.dash.length),void 0!=t.dash.gap&&(this.dash.gap=t.dash.gap),void 0!=t.dash.altLength&&(this.dash.altLength=t.dash.altLength)),void 0!=t.color&&(this.color=t.color),this.connect(),this.widthFixed=this.widthFixed||void 0!=t.width,this.lengthFixed=this.lengthFixed||void 0!=t.length,this.stiffness=1/this.length,this.style){case"line":this.draw=this._drawLine;break;case"arrow":this.draw=this._drawArrow;break;case"arrow-center":this.draw=this._drawArrowCenter;break;case"dash-line":this.draw=this._drawDashLine;break;default:this.draw=this._drawLine}},T.prototype.connect=function(){this.disconnect(),this.from=this.graph.nodes[this.fromId]||null,this.to=this.graph.nodes[this.toId]||null,this.connected=this.from&&this.to,this.connected?(this.from.attachEdge(this),this.to.attachEdge(this)):(this.from&&this.from.detachEdge(this),this.to&&this.to.detachEdge(this))},T.prototype.disconnect=function(){this.from&&(this.from.detachEdge(this),this.from=null),this.to&&(this.to.detachEdge(this),this.to=null),this.connected=!1},T.prototype.getTitle=function(){return this.title},T.prototype.getValue=function(){return this.value},T.prototype.setValueRange=function(t,e){if(!this.widthFixed&&void 0!==this.value){var i=(this.widthMax-this.widthMin)/(e-t);this.width=(this.value-t)*i+this.widthMin}},T.prototype.draw=function(){throw"Method draw not initialized in edge"},T.prototype.isOverlappingWith=function(t){var e=10,i=this.from.x,n=this.from.y,s=this.to.x,o=this.to.y,r=t.left,a=t.top,h=T._dist(i,n,s,o,r,a);return e>h},T.prototype._drawLine=function(t){t.strokeStyle=this.color,t.lineWidth=this._getLineWidth();var e;if(this.from!=this.to)this._line(t),this.label&&(e=this._pointOnLine(.5),this._label(t,this.label,e.x,e.y));else{var i,n,s=this.length/4,o=this.from;o.width||o.resize(t),o.width>o.height?(i=o.x+o.width/2,n=o.y-s):(i=o.x+s,n=o.y-o.height/2),this._circle(t,i,n,s),e=this._pointOnCircle(i,n,s,.5),this._label(t,this.label,e.x,e.y)}},T.prototype._getLineWidth=function(){return this.from.selected||this.to.selected?Math.min(2*this.width,this.widthMax):this.width},T.prototype._line=function(t){t.beginPath(),t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y),t.stroke()},T.prototype._circle=function(t,e,i,n){t.beginPath(),t.arc(e,i,n,0,2*Math.PI,!1),t.stroke()},T.prototype._label=function(t,e,i,n){if(e){t.font=(this.from.selected||this.to.selected?"bold ":"")+this.fontSize+"px "+this.fontFace,t.fillStyle="white";var s=t.measureText(e).width,o=this.fontSize,r=i-s/2,a=n-o/2;t.fillRect(r,a,s,o),t.fillStyle=this.fontColor||"black",t.textAlign="left",t.textBaseline="top",t.fillText(e,r,a)}},T.prototype._drawDashLine=function(t){if(t.strokeStyle=this.color,t.lineWidth=this._getLineWidth(),t.beginPath(),t.lineCap="round",void 0!=this.dash.altLength?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]):void 0!=this.dash.length&&void 0!=this.dash.gap?t.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,[this.dash.length,this.dash.gap]):(t.moveTo(this.from.x,this.from.y),t.lineTo(this.to.x,this.to.y)),t.stroke(),this.label){var e=this._pointOnLine(.5);this._label(t,this.label,e.x,e.y)}},T.prototype._pointOnLine=function(t){return{x:(1-t)*this.from.x+t*this.to.x,y:(1-t)*this.from.y+t*this.to.y}},T.prototype._pointOnCircle=function(t,e,i,n){var s=2*(n-3/8)*Math.PI;return{x:t+i*Math.cos(s),y:e-i*Math.sin(s)}},T.prototype._drawArrowCenter=function(t){var e;if(t.strokeStyle=this.color,t.fillStyle=this.color,t.lineWidth=this._getLineWidth(),this.from!=this.to){this._line(t);var i=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x),n=10+5*this.width;e=this._pointOnLine(.5),t.arrow(e.x,e.y,i,n),t.fill(),t.stroke(),this.label&&(e=this._pointOnLine(.5),this._label(t,this.label,e.x,e.y))}else{var s,o,r=this.length/4,a=this.from;a.width||a.resize(t),a.width>a.height?(s=a.x+a.width/2,o=a.y-r):(s=a.x+r,o=a.y-a.height/2),this._circle(t,s,o,r);var i=.2*Math.PI,n=10+5*this.width;e=this._pointOnCircle(s,o,r,.5),t.arrow(e.x,e.y,i,n),t.fill(),t.stroke(),this.label&&(e=this._pointOnCircle(s,o,r,.5),this._label(t,this.label,e.x,e.y))}},T.prototype._drawArrow=function(t){t.strokeStyle=this.color,t.fillStyle=this.color,t.lineWidth=this._getLineWidth();var e,i;if(this.from!=this.to){e=Math.atan2(this.to.y-this.from.y,this.to.x-this.from.x);var n=this.to.x-this.from.x,s=this.to.y-this.from.y,o=Math.sqrt(n*n+s*s),r=this.from.distanceToBorder(t,e+Math.PI),a=(o-r)/o,h=a*this.from.x+(1-a)*this.to.x,d=a*this.from.y+(1-a)*this.to.y,l=this.to.distanceToBorder(t,e),p=(o-l)/o,c=(1-p)*this.from.x+p*this.to.x,u=(1-p)*this.from.y+p*this.to.y;if(t.beginPath(),t.moveTo(h,d),t.lineTo(c,u),t.stroke(),i=10+5*this.width,t.arrow(c,u,e,i),t.fill(),t.stroke(),this.label){var f=this._pointOnLine(.5);this._label(t,this.label,f.x,f.y)}}else{var m,g,v,y=this.from,b=this.length/4;y.width||y.resize(t),y.width>y.height?(m=y.x+y.width/2,g=y.y-b,v={x:m,y:y.y,angle:.9*Math.PI}):(m=y.x+b,g=y.y-y.height/2,v={x:y.x,y:g,angle:.6*Math.PI}),t.beginPath(),t.arc(m,g,b,0,2*Math.PI,!1),t.stroke(),i=10+5*this.width,t.arrow(v.x,v.y,v.angle,i),t.fill(),t.stroke(),this.label&&(f=this._pointOnCircle(m,g,b,.5),this._label(t,this.label,f.x,f.y))
+}},T._dist=function(t,e,i,n,s,o){var r=i-t,a=n-e,h=r*r+a*a,d=((s-t)*r+(o-e)*a)/h;d>1?d=1:0>d&&(d=0);var l=t+d*r,p=e+d*a,c=l-s,u=p-o;return Math.sqrt(c*c+u*u)},x.prototype.setPosition=function(t,e){this.x=parseInt(t),this.y=parseInt(e)},x.prototype.setText=function(t){this.frame.innerHTML=t},x.prototype.show=function(t){if(void 0===t&&(t=!0),t){var e=this.frame.clientHeight,i=this.frame.clientWidth,n=this.frame.parentNode.clientHeight,s=this.frame.parentNode.clientWidth,o=this.y-e;o+e+this.padding>n&&(o=n-e-this.padding),this.padding>o&&(o=this.padding);var r=this.x;r+i+this.padding>s&&(r=s-i-this.padding),this.padding>r&&(r=this.padding),this.frame.style.left=r+"px",this.frame.style.top=o+"px",this.frame.style.visibility="visible"}else this.hide()},x.prototype.hide=function(){this.frame.style.visibility="hidden"},Groups=function(){this.clear(),this.defaultIndex=0},Groups.DEFAULT=[{border:"#2B7CE9",background:"#97C2FC",highlight:{border:"#2B7CE9",background:"#D2E5FF"}},{border:"#FFA500",background:"#FFFF00",highlight:{border:"#FFA500",background:"#FFFFA3"}},{border:"#FA0A10",background:"#FB7E81",highlight:{border:"#FA0A10",background:"#FFAFB1"}},{border:"#41A906",background:"#7BE141",highlight:{border:"#41A906",background:"#A1EC76"}},{border:"#E129F0",background:"#EB7DF4",highlight:{border:"#E129F0",background:"#F0B3F5"}},{border:"#7C29F0",background:"#AD85E4",highlight:{border:"#7C29F0",background:"#D3BDF0"}},{border:"#C37F00",background:"#FFA807",highlight:{border:"#C37F00",background:"#FFCA66"}},{border:"#4220FB",background:"#6E6EFD",highlight:{border:"#4220FB",background:"#9B9BFD"}},{border:"#FD5A77",background:"#FFC0CB",highlight:{border:"#FD5A77",background:"#FFD1D9"}},{border:"#4AD63A",background:"#C2FABC",highlight:{border:"#4AD63A",background:"#E6FFE3"}}],Groups.prototype.clear=function(){this.groups={},this.groups.length=function(){var t=0;for(var e in this)this.hasOwnProperty(e)&&t++;return t}},Groups.prototype.get=function(t){var e=this.groups[t];if(void 0==e){var i=this.defaultIndex%Groups.DEFAULT.length;this.defaultIndex++,e={},e.color=Groups.DEFAULT[i],this.groups[t]=e}return e},Groups.prototype.add=function(t,e){return this.groups[t]=e,e.color&&(e.color=E.parseColor(e.color)),e},Images=function(){this.images={},this.callback=void 0},Images.prototype.setOnloadCallback=function(t){this.callback=t},Images.prototype.load=function(t){var e=this.images[t];if(void 0==e){var i=this;e=new Image,this.images[t]=e,e.onload=function(){i.callback&&i.callback(this)},e.src=t}return e},S.prototype.setData=function(t){if(t&&t.dot&&(t.nodes||t.edges))throw new SyntaxError('Data must contain either parameter "dot" or parameter pair "nodes" and "edges", but not both.');if(this.setOptions(t&&t.options),t&&t.dot){if(t&&t.dot){var e=k.util.DOTToGraph(t.dot);return this.setData(e),void 0}}else this._setNodes(t&&t.nodes),this._setEdges(t&&t.edges);this.stabilize&&this._doStabilize(),this.start()},S.prototype.setOptions=function(t){if(t){if(void 0!=t.width&&(this.width=t.width),void 0!=t.height&&(this.height=t.height),void 0!=t.stabilize&&(this.stabilize=t.stabilize),void 0!=t.selectable&&(this.selectable=t.selectable),t.edges){for(var e in t.edges)t.edges.hasOwnProperty(e)&&(this.constants.edges[e]=t.edges[e]);void 0!=t.edges.length&&t.nodes&&void 0==t.nodes.distance&&(this.constants.edges.length=t.edges.length,this.constants.nodes.distance=1.25*t.edges.length),t.edges.fontColor||(this.constants.edges.fontColor=t.edges.color),t.edges.dash&&(void 0!=t.edges.dash.length&&(this.constants.edges.dash.length=t.edges.dash.length),void 0!=t.edges.dash.gap&&(this.constants.edges.dash.gap=t.edges.dash.gap),void 0!=t.edges.dash.altLength&&(this.constants.edges.dash.altLength=t.edges.dash.altLength))}if(t.nodes){for(e in t.nodes)t.nodes.hasOwnProperty(e)&&(this.constants.nodes[e]=t.nodes[e]);t.nodes.color&&(this.constants.nodes.color=E.parseColor(t.nodes.color))}if(t.groups)for(var i in t.groups)if(t.groups.hasOwnProperty(i)){var n=t.groups[i];this.groups.add(i,n)}}this.setSize(this.width,this.height),this._setTranslation(this.frame.clientWidth/2,this.frame.clientHeight/2),this._setScale(1)},S.prototype._trigger=function(t,e){L.trigger(this,t,e)},S.prototype._create=function(){for(;this.containerElement.hasChildNodes();)this.containerElement.removeChild(this.containerElement.firstChild);if(this.frame=document.createElement("div"),this.frame.className="graph-frame",this.frame.style.position="relative",this.frame.style.overflow="hidden",this.frame.canvas=document.createElement("canvas"),this.frame.canvas.style.position="relative",this.frame.appendChild(this.frame.canvas),!this.frame.canvas.getContext){var t=document.createElement("DIV");t.style.color="red",t.style.fontWeight="bold",t.style.padding="10px",t.innerHTML="Error: your browser does not support HTML canvas",this.frame.canvas.appendChild(t)}var e=this;this.drag={},this.pinch={},this.hammer=D(this.frame.canvas,{prevent_default:!0}),this.hammer.on("tap",e._onTap.bind(e)),this.hammer.on("hold",e._onHold.bind(e)),this.hammer.on("pinch",e._onPinch.bind(e)),this.hammer.on("touch",e._onTouch.bind(e)),this.hammer.on("dragstart",e._onDragStart.bind(e)),this.hammer.on("drag",e._onDrag.bind(e)),this.hammer.on("dragend",e._onDragEnd.bind(e)),this.hammer.on("mousewheel",e._onMouseWheel.bind(e)),this.hammer.on("mousemove",e._onMouseMoveTitle.bind(e)),this.containerElement.appendChild(this.frame)},S.prototype._getNodeAt=function(t){var e=this._canvasToX(t.x),i=this._canvasToY(t.y),n={left:e,top:i,right:e,bottom:i},s=this._getNodesOverlappingWith(n);return s.length>0?s[s.length-1]:null},S.prototype._getPointer=function(t){return{x:t.pageX-k.util.getAbsoluteLeft(this.frame.canvas),y:t.pageY-k.util.getAbsoluteTop(this.frame.canvas)}},S.prototype._onTouch=function(t){this.drag.pointer=this._getPointer(t.gesture.touches[0]),this.drag.pinched=!1,this.pinch.scale=this._getScale()},S.prototype._onDragStart=function(){var t=this.drag;t.selection=[],t.translation=this._getTranslation(),t.nodeId=this._getNodeAt(t.pointer);var e=this.nodes[t.nodeId];if(e){e.isSelected()||this._selectNodes([t.nodeId]);var i=this;this.selection.forEach(function(e){var n=i.nodes[e];if(n){var s={id:e,node:n,x:n.x,y:n.y,xFixed:n.xFixed,yFixed:n.yFixed};n.xFixed=!0,n.yFixed=!0,t.selection.push(s)}})}},S.prototype._onDrag=function(t){if(!this.drag.pinched){var e=this._getPointer(t.gesture.touches[0]),i=this,n=this.drag,s=n.selection;if(s&&s.length){var o=e.x-n.pointer.x,r=e.y-n.pointer.y;s.forEach(function(t){var e=t.node;t.xFixed||(e.x=i._canvasToX(i._xToCanvas(t.x)+o)),t.yFixed||(e.y=i._canvasToY(i._yToCanvas(t.y)+r))}),this.moving||(this.moving=!0,this.start())}else{var a=e.x-this.drag.pointer.x,h=e.y-this.drag.pointer.y;this._setTranslation(this.drag.translation.x+a,this.drag.translation.y+h),this._redraw(),this.moved=!0}}},S.prototype._onDragEnd=function(){var t=this.drag.selection;t&&t.forEach(function(t){t.node.xFixed=t.xFixed,t.node.yFixed=t.yFixed})},S.prototype._onTap=function(t){var e=this._getPointer(t.gesture.touches[0]),i=this._getNodeAt(e),n=this.nodes[i];n?(this._selectNodes([i]),this.moving||this._redraw()):(this._unselectNodes(),this._redraw())},S.prototype._onHold=function(t){var e=this._getPointer(t.gesture.touches[0]),i=this._getNodeAt(e),n=this.nodes[i];if(n){if(n.isSelected())this._unselectNodes([i]);else{var s=!0;this._selectNodes([i],s)}this.moving||this._redraw()}},S.prototype._onPinch=function(t){var e=this._getPointer(t.gesture.center);this.drag.pinched=!0,"scale"in this.pinch||(this.pinch.scale=1);var i=this.pinch.scale*t.gesture.scale;this._zoom(i,e)},S.prototype._zoom=function(t,e){var i=this._getScale();.01>t&&(t=.01),t>10&&(t=10);var n=this._getTranslation(),s=t/i,o=(1-s)*e.x+n.x*s,r=(1-s)*e.y+n.y*s;return this._setScale(t),this._setTranslation(o,r),this._redraw(),t},S.prototype._onMouseWheel=function(t){var e=0;if(t.wheelDelta?e=t.wheelDelta/120:t.detail&&(e=-t.detail/3),e){"mouswheelScale"in this.pinch||(this.pinch.mouswheelScale=1);var i=this.pinch.mouswheelScale,n=e/10;0>e&&(n/=1-n),i*=1+n;var s=D.event.collectEventData(this,"scroll",t),o=this._getPointer(s.center);i=this._zoom(i,o),this.pinch.mouswheelScale=i}t.preventDefault()},S.prototype._onMouseMoveTitle=function(t){var e=D.event.collectEventData(this,"mousemove",t),i=this._getPointer(e.center);this.popupNode&&this._checkHidePopup(i);var n=this,s=function(){n._checkShowPopup(i)};this.popupTimer&&clearInterval(this.popupTimer),this.leftButtonDown||(this.popupTimer=setTimeout(s,300))},S.prototype._checkShowPopup=function(t){var e,i={left:this._canvasToX(t.x),top:this._canvasToY(t.y),right:this._canvasToX(t.x),bottom:this._canvasToY(t.y)},n=this.popupNode;if(void 0==this.popupNode){var s=this.nodes;for(e in s)if(s.hasOwnProperty(e)){var o=s[e];if(void 0!=o.getTitle()&&o.isOverlappingWith(i)){this.popupNode=o;break}}}if(void 0==this.popupNode){var r=this.edges;for(e in r)if(r.hasOwnProperty(e)){var a=r[e];if(a.connected&&void 0!=a.getTitle()&&a.isOverlappingWith(i)){this.popupNode=a;break}}}if(this.popupNode){if(this.popupNode!=n){var h=this;h.popup||(h.popup=new x(h.frame)),h.popup.setPosition(t.x-3,t.y-3),h.popup.setText(h.popupNode.getTitle()),h.popup.show()}}else this.popup&&this.popup.hide()},S.prototype._checkHidePopup=function(t){this.popupNode&&this._getNodeAt(t)||(this.popupNode=void 0,this.popup&&this.popup.hide())},S.prototype._unselectNodes=function(t,e){var i,n,s,o=!1;if(t)for(i=0,n=t.length;n>i;i++){s=t[i],this.nodes[s].unselect();for(var r=0;this.selection.length>r;)this.selection[r]==s?(this.selection.splice(r,1),o=!0):r++}else if(this.selection&&this.selection.length){for(i=0,n=this.selection.length;n>i;i++)s=this.selection[i],this.nodes[s].unselect(),o=!0;this.selection=[]}return!o||1!=e&&void 0!=e||this._trigger("select"),o},S.prototype._selectNodes=function(t,e){var i,n,s=!1,o=!0;if(t.length!=this.selection.length)o=!1;else for(i=0,n=Math.min(t.length,this.selection.length);n>i;i++)if(t[i]!=this.selection[i]){o=!1;break}if(o)return s;if(void 0==e||0==e){var r=!1;s=this._unselectNodes(void 0,r)}for(i=0,n=t.length;n>i;i++){var a=t[i],h=-1!=this.selection.indexOf(a);h||(this.nodes[a].select(),this.selection.push(a),s=!0)}return s&&this._trigger("select"),s},S.prototype._getNodesOverlappingWith=function(t){var e=this.nodes,i=[];for(var n in e)e.hasOwnProperty(n)&&e[n].isOverlappingWith(t)&&i.push(n);return i},S.prototype.getSelection=function(){return this.selection.concat([])},S.prototype.setSelection=function(t){var e,i,n;if(!t||void 0==t.length)throw"Selection must be an array with ids";for(e=0,i=this.selection.length;i>e;e++)n=this.selection[e],this.nodes[n].unselect();for(this.selection=[],e=0,i=t.length;i>e;e++){n=t[e];var s=this.nodes[n];if(!s)throw new RangeError('Node with id "'+n+'" not found');s.select(),this.selection.push(n)}this.redraw()},S.prototype._updateSelection=function(){for(var t=0;this.selection.length>t;){var e=this.selection[t];this.nodes[e]?t++:this.selection.splice(t,1)}},S.prototype._getConnectionCount=function(t){function e(t){for(var e=[],i=0,n=t.length;n>i;i++)for(var s=t[i],o=s.edges,r=0,a=o.length;a>r;r++){var h=o[r],d=null;h.from==s?d=h.to:h.to==s&&(d=h.from);var l,p;if(d)for(l=0,p=t.length;p>l;l++)if(t[l]==d){d=null;break}if(d)for(l=0,p=e.length;p>l;l++)if(e[l]==d){d=null;break}d&&e.push(d)}return e}void 0==t&&(t=1);var i=[],n=this.nodes;for(var s in n)if(n.hasOwnProperty(s)){for(var o=[n[s]],r=0;t>r;r++)o=o.concat(e(o));i.push(o)}for(var a=[],h=0,d=i.length;d>h;h++)a.push(i[h].length);return a},S.prototype.setSize=function(t,e){this.frame.style.width=t,this.frame.style.height=e,this.frame.canvas.style.width="100%",this.frame.canvas.style.height="100%",this.frame.canvas.width=this.frame.canvas.clientWidth,this.frame.canvas.height=this.frame.canvas.clientHeight},S.prototype._setNodes=function(t){var e=this.nodesData;if(t instanceof o||t instanceof r)this.nodesData=t;else if(t instanceof Array)this.nodesData=new o,this.nodesData.add(t);else{if(t)throw new TypeError("Array or DataSet expected");this.nodesData=new o}if(e&&O.forEach(this.nodesListeners,function(t,i){e.unsubscribe(i,t)}),this.nodes={},this.nodesData){var i=this;O.forEach(this.nodesListeners,function(t,e){i.nodesData.subscribe(e,t)});var n=this.nodesData.getIds();this._addNodes(n)}this._updateSelection()},S.prototype._addNodes=function(t){for(var e,i=0,n=t.length;n>i;i++){e=t[i];var s=this.nodesData.get(e),o=new E(s,this.images,this.groups,this.constants);if(this.nodes[e]=o,!o.isFixed()){var r=2*this.constants.edges.length,a=t.length,h=2*Math.PI*(i/a);o.x=r*Math.cos(h),o.y=r*Math.sin(h),this.moving=!0}}this._reconnectEdges(),this._updateValueRange(this.nodes)},S.prototype._updateNodes=function(t){for(var e=this.nodes,i=this.nodesData,n=0,s=t.length;s>n;n++){var o=t[n],r=e[o],a=i.get(o);r?r.setProperties(a,this.constants):(r=new E(properties,this.images,this.groups,this.constants),e[o]=r,r.isFixed()||(this.moving=!0))}this._reconnectEdges(),this._updateValueRange(e)},S.prototype._removeNodes=function(t){for(var e=this.nodes,i=0,n=t.length;n>i;i++){var s=t[i];delete e[s]}this._reconnectEdges(),this._updateSelection(),this._updateValueRange(e)},S.prototype._setEdges=function(t){var e=this.edgesData;if(t instanceof o||t instanceof r)this.edgesData=t;else if(t instanceof Array)this.edgesData=new o,this.edgesData.add(t);else{if(t)throw new TypeError("Array or DataSet expected");this.edgesData=new o}if(e&&O.forEach(this.edgesListeners,function(t,i){e.unsubscribe(i,t)}),this.edges={},this.edgesData){var i=this;O.forEach(this.edgesListeners,function(t,e){i.edgesData.subscribe(e,t)});var n=this.edgesData.getIds();this._addEdges(n)}this._reconnectEdges()},S.prototype._addEdges=function(t){for(var e=this.edges,i=this.edgesData,n=0,s=t.length;s>n;n++){var o=t[n],r=e[o];r&&r.disconnect();var a=i.get(o);e[o]=new T(a,this,this.constants)}this.moving=!0,this._updateValueRange(e)},S.prototype._updateEdges=function(t){for(var e=this.edges,i=this.edgesData,n=0,s=t.length;s>n;n++){var o=t[n],r=i.get(o),a=e[o];a?(a.disconnect(),a.setProperties(r,this.constants),a.connect()):(a=new T(r,this,this.constants),this.edges[o]=a)}this.moving=!0,this._updateValueRange(e)},S.prototype._removeEdges=function(t){for(var e=this.edges,i=0,n=t.length;n>i;i++){var s=t[i],o=e[s];o&&(o.disconnect(),delete e[s])}this.moving=!0,this._updateValueRange(e)},S.prototype._reconnectEdges=function(){var t,e=this.nodes,i=this.edges;for(t in e)e.hasOwnProperty(t)&&(e[t].edges=[]);for(t in i)if(i.hasOwnProperty(t)){var n=i[t];n.from=null,n.to=null,n.connect()}},S.prototype._updateValueRange=function(t){var e,i=void 0,n=void 0;for(e in t)if(t.hasOwnProperty(e)){var s=t[e].getValue();void 0!==s&&(i=void 0===i?s:Math.min(s,i),n=void 0===n?s:Math.max(s,n))}if(void 0!==i&&void 0!==n)for(e in t)t.hasOwnProperty(e)&&t[e].setValueRange(i,n)},S.prototype.redraw=function(){this.setSize(this.width,this.height),this._redraw()},S.prototype._redraw=function(){var t=this.frame.canvas.getContext("2d"),e=this.frame.canvas.width,i=this.frame.canvas.height;t.clearRect(0,0,e,i),t.save(),t.translate(this.translation.x,this.translation.y),t.scale(this.scale,this.scale),this._drawEdges(t),this._drawNodes(t),t.restore()},S.prototype._setTranslation=function(t,e){void 0===this.translation&&(this.translation={x:0,y:0}),void 0!==t&&(this.translation.x=t),void 0!==e&&(this.translation.y=e)},S.prototype._getTranslation=function(){return{x:this.translation.x,y:this.translation.y}},S.prototype._setScale=function(t){this.scale=t},S.prototype._getScale=function(){return this.scale},S.prototype._canvasToX=function(t){return(t-this.translation.x)/this.scale},S.prototype._xToCanvas=function(t){return t*this.scale+this.translation.x},S.prototype._canvasToY=function(t){return(t-this.translation.y)/this.scale},S.prototype._yToCanvas=function(t){return t*this.scale+this.translation.y},S.prototype._drawNodes=function(t){var e=this.nodes,i=[];for(var n in e)e.hasOwnProperty(n)&&(e[n].isSelected()?i.push(n):e[n].draw(t));for(var s=0,o=i.length;o>s;s++)e[i[s]].draw(t)},S.prototype._drawEdges=function(t){var e=this.edges;for(var i in e)if(e.hasOwnProperty(i)){var n=e[i];n.connected&&e[i].draw(t)}},S.prototype._doStabilize=function(){new Date;for(var t=0,e=this.constants.minVelocity,i=!1;!i&&this.constants.maxIterations>t;)this._calculateForces(),this._discreteStepNodes(),i=!this._isMoving(e),t++;new Date},S.prototype._calculateForces=function(){var t,e,i,n,s,o,r,a,h,d,l,p=this.nodes,c=this.edges,u=.01,f=this.frame.canvas.clientWidth/2,m=this.frame.canvas.clientHeight/2;for(t in p)if(p.hasOwnProperty(t)){var g=p[t];e=f-g.x,i=m-g.y,n=Math.atan2(i,e),o=Math.cos(n)*u,r=Math.sin(n)*u,g._setForce(o,r)}var v=this.constants.nodes.distance,y=10;for(var b in p)if(p.hasOwnProperty(b)){var w=p[b];for(var _ in p)if(p.hasOwnProperty(_)){var E=p[_];e=E.x-w.x,i=E.y-w.y,s=Math.sqrt(e*e+i*i),n=Math.atan2(i,e),a=1/(1+Math.exp((s/v-1)*y)),o=Math.cos(n)*a,r=Math.sin(n)*a,w._addForce(-o,-r),E._addForce(o,r)}}for(t in c)if(c.hasOwnProperty(t)){var T=c[t];T.connected&&(e=T.to.x-T.from.x,i=T.to.y-T.from.y,l=T.length,d=Math.sqrt(e*e+i*i),n=Math.atan2(i,e),h=T.stiffness*(l-d),o=Math.cos(n)*h,r=Math.sin(n)*h,T.from._addForce(-o,-r),T.to._addForce(o,r))}},S.prototype._isMoving=function(t){var e=this.nodes;for(var i in e)if(e.hasOwnProperty(i)&&e[i].isMoving(t))return!0;return!1},S.prototype._discreteStepNodes=function(){var t=this.refreshRate/1e3,e=this.nodes;for(var i in e)e.hasOwnProperty(i)&&e[i].discreteStep(t)},S.prototype.start=function(){if(this.moving){this._calculateForces(),this._discreteStepNodes();var t=this.constants.minVelocity;this.moving=this._isMoving(t)}if(this.moving){if(!this.timer){var e=this;this.timer=window.setTimeout(function(){e.timer=void 0,e.start(),e._redraw()},this.refreshRate)}}else this._redraw()},S.prototype.stop=function(){this.timer&&(window.clearInterval(this.timer),this.timer=void 0)};var k={util:O,events:L,Controller:d,DataSet:o,DataView:r,Range:h,Stack:a,TimeStep:TimeStep,EventBus:s,components:{items:{Item:m,ItemBox:g,ItemPoint:v,ItemRange:y},Component:l,Panel:p,RootPanel:c,ItemSet:f,TimeAxis:u},graph:{Node:E,Edge:T,Popup:x,Groups:Groups,Images:Images},Timeline:_,Graph:S};n!==void 0&&(n=k),i!==void 0&&i.exports!==void 0&&(i.exports=k),"function"==typeof t&&t(function(){return k}),"undefined"!=typeof window&&(window.vis=k),O.loadCss("/* vis.js stylesheet */\n.vis.timeline {\n}\n\n\n.vis.timeline.rootpanel {\n position: relative;\n overflow: hidden;\n\n border: 1px solid #bfbfbf;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n\n.vis.timeline .panel {\n position: absolute;\n overflow: hidden;\n}\n\n\n.vis.timeline .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n.vis.timeline .labels {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n\n padding: 0;\n margin: 0;\n\n border-right: 1px solid #bfbfbf;\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n}\n\n.vis.timeline .labels .label {\n position: absolute;\n left: 0;\n top: 0;\n width: 100%;\n border-bottom: 1px solid #bfbfbf;\n color: #4d4d4d;\n}\n\n.vis.timeline .labels .label .inner {\n display: inline-block;\n padding: 5px;\n}\n\n\n.vis.timeline .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.vis.timeline .background {\n}\n\n.vis.timeline .foreground {\n}\n\n.vis.timeline .itemset-axis {\n position: absolute;\n}\n\n.vis.timeline .groupset .itemset-axis {\n border-top: 1px solid #bfbfbf;\n}\n\n/* TODO: with orientation=='bottom', this will more or less overlap with timeline axis\n.vis.timeline .groupset .itemset-axis:last-child {\n border-top: none;\n}\n*/\n\n\n.vis.timeline .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.vis.timeline .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.vis.timeline .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.vis.timeline .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.vis.timeline .item.box {\n text-align: center;\n border-style: solid;\n border-width: 1px;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.point {\n background: none;\n}\n\n.vis.timeline .dot {\n border: 5px solid #97B0F8;\n position: absolute;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range {\n overflow: hidden;\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .content {\n position: relative;\n display: inline-block;\n}\n\n.vis.timeline .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.vis.timeline .item .content {\n margin: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.vis.timeline .axis {\n position: relative;\n}\n\n.vis.timeline .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.vis.timeline .axis .text.measure {\n position: absolute;\n padding-left: 0;\n padding-right: 0;\n margin-left: 0;\n margin-right: 0;\n visibility: hidden;\n}\n\n.vis.timeline .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.vis.timeline .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.vis.timeline .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.vis.timeline .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n")},{hammerjs:1,moment:2}]},{},[3])(3)});
\ No newline at end of file